Building encrypted systems
Worth their salt

MidwestPHP 2020

Ian Littman / @iansltx

Follow along at https://ian.im/cryptmw20

Let's start with a demo

Okay, what did we just see?

  • User logins
  • File uploads
  • File sharing
  • Password resets

Okay, what did we just see?

  • User logins
  • File uploads
  • File sharing
  • Password resets
  • ...so what's so special about that?

Okay, what did we just see?

  • User logins...with encrypted + hashed passwords
  • File uploads
  • File sharing
  • Password resets

Okay, what did we just see?

  • User logins...with encrypted + hashed passwords
  • File uploads...encrypted at rest with encrypted per-file keys
  • File sharing
  • Password resets

Okay, what did we just see?

  • User logins...with encrypted + hashed passwords
  • File uploads...encrypted at rest with encrypted per-file keys
  • File sharing...while keeping files secret to everyone else
  • Password resets

Okay, what did we just see?

  • User logins...with encrypted + hashed passwords
  • File uploads...encrypted at rest with encrypted per-file keys
  • File sharing...while keeping files secret to everyone else
  • Password resets...without giving the app power to reopen accounts on its own

Okay, but what attacks does this prevent?

OKAY, BUT WHAT ATTACKS DOES THIS PREVENT?

Assuming separate DB + Web servers

As an attacker we can't...

  • See user passwords
  • See password hashes
  • Read files in the clear
  • Access a user's account
  • Recover a user's encryption key


     
  • Change file ownership in a
    non-tamper-evident way

...even if we have...

  • Access to app key, code, and DB
  • Realtime access to the database
  • Access to app key, code, DB, and storage
  • Access to the user's email account
  • A DB dump, username, and password,
    if the app server is offline...or...
  • A valid, but expired, session token, and a DB dump
  • Write access to the database

Every system has its limitations...

...including salty-files

As an attacker we can

  • Act on behalf of a user,
    including key recovery
  • Send users encrypted files
  • Read files in the clear
     
  • Read file metadata, and see who a file is shared with

...if we have...

  • A valid, non-revoked session token
     
  • Read + write access to the database only
  • Access to RAM of the running app
    while a user is accessing a file
  • Access to the database (live or a dump)
/code/salty-files master $ find . "(" -name "*.php" ")" | grep -v vendor | xargs wc -l

      42 ./bootstrap/services.php
     107 ./bootstrap/routes.php
      17 ./public/index.php
     334 ./templates/home.php
      14 ./templates/layout.php
      35 ./src/User.php
      47 ./src/FileMeta.php
     147 ./src/UserRepository.php
     189 ./src/FileRepository.php
      23 ./src/ErrorMiddleware.php
      29 ./src/View.php
     100 ./src/AuthRepository.php
      35 ./src/AuthMiddleware.php

    1119 total

...and it's really 771 LOC; templates are static!

/code/salty-files master $ cat composer.json

// snip
    "php": "^7.4",
    "slim/slim": "^4.4",
    "slim/http": "^1.0",
    "slim/psr7": "^1.0",
    "aura/sql": "^3.0",
    "pimple/pimple": "^3.2",
    "paragonie/paseto": "^1.0",
    "paragonie/halite": "^4.6",
    "ramsey/uuid": "^4.0",
    "ext-json": "*",
    "league/flysystem": "^1.0"
// snip
/code/salty-files master $ cat composer.json

// snip
    "php": "^7.4",
    "slim/slim": "^4.4",
    "slim/http": "^1.0",
    "slim/psr7": "^1.0",
    "aura/sql": "^3.0",
    "pimple/pimple": "^3.2",
    "paragonie/paseto": "^1.0",
    "paragonie/halite": "^4.6",
    "ramsey/uuid": "^4.0",
    "ext-json": "*",
    "league/flysystem": "^1.0"
// snip

Paseto (Paw-Set-Oh): Platform-Agnostic Security Tokens

  • Similar use case as JOSE (which includes JWTs), except...
  • Versioned protocol with pre-specified ciphers forspecific use cases
  • For v2, leading-edge cryptography thanks to libsodium
  • Harder to shoot yourself in the foot

Halite: A friendlier interface for sodium in PHP

  • PHP 7.2+ on supported versions
  • Modern, secure-by-default cryptography primitives
    • Symmetric + asymmetric
    • Authenticated encryption (encrypt, then MAC)
  • Harder to shoot yourself in the foot (though Sodium itself helps with this)

This is what sodium looks like in PHP...

  • sodium_crypto_scalarmult()
    (Elliptic Curve Diffie Hellman over Curve25519)
  • sodium_crypto_secretbox_open()
    (decrypt via Xsalsa20 + Poly1305)
  • sodium_crypto_box_keypair()
    (Generate an X25519 keypair for use with the crypto_box API)

...versus something like this in Halite (after imports/aliases)

$reEncryptedFileKey = AsymmCrypto::encrypt(
    $decryptedFileKey,
    $user->getKey(),
    new EncryptionPublicKey(
        new HiddenString($recipientPublicKeyStr)
    )
);

Let's apply these libraries to salty-files

User signup

  1. Hash and then encrypt password for storage using app key
  2. Generate a key pair for the user
  3. Derive a symmetric key from the password
  4. Encrypt the private part of the key pair with the derived key
  5. Store everything
    • Unencrypted username + ID
    • Hashed + encrypted password
    • Unencrypted public key
    • Encrypted private key

User Login

  1. Decrypt + verify password for selected username
  2. Derive symmetric key from password
  3. Decrypt private key via derived symmetric key
  4. Create a session ID, persist ID + user ID + expiration to DB
  5. Build + deliver a PASETO token
    • Local-mode (authenticated encryption using server-side key)
    • Includes
      • Issuer ID
      • Session ID
      • Expiration date
      • Private key
    • Does not include user ID

Session Validation

  1. Parse + validate PASETO token...all of the following must be true
    • Local v2 format
    • Able to decrypt (including MAC verification)
    • Not expired
    • Issued by us
  2. Confirm session ID is listed as unexpired in the database
  3. Grab user metadata (user ID, username) via session -> user join
  4. Pull private key from decrypted token for use within request context

Password Resets

  1. Set up user-provided private key (base64-encoded)
  2. Derive public key from private key
  3. Confirm that public key matches the public key for that username in the DB
  4. Hash + encrypt the newly provided password
  5. Derive a symmetric key from the new password
  6. Encrypt the private key with the derived symmetric key
  7. Store the new password hash + encrypted private key

File uploads

  1. Generate a symmetric key specific to this file
  2. Encrypt the symmetric key with our own key pair
  3. Encrypt and save the file using the symmetric key
  4. Save file metadata to the database
  5. Save encrypted file key to the database (incl. user ID + file ID association)

File sharing

  1. Ensure the current user owns the file
  2. Decrypt the per-file symmetric encryption key using our private key
  3. Re-encrypt the per-file key using a key derived from
    our private key and the recipient's public key
  4. Save the newly encrypted key to the DB, along with file + recipient user IDs

File Downloads

  1. Decrypt the per-file symmetric encryption key using our private key
    and the owner's public key (the owner might be us)
  2. Load the encrypted file from disk
  3. Decrypt and deliver the file using the per-file key

Thanks! Questions?