Building encrypted systems
Worth their salt
Let's start with a demo
of salty-files
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
- 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
- Hash and then encrypt password for storage using app key
- Generate a key pair for the user
- Derive a symmetric key from the password
- Encrypt the private part of the key pair with the derived key
- Store everything
- Unencrypted username + ID
- Hashed + encrypted password
- Unencrypted public key
- Encrypted private key
User Login
- Decrypt + verify password for selected username
- Derive symmetric key from password
- Decrypt private key via derived symmetric key
- Create a session ID, persist ID + user ID + expiration to DB
- 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
- 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
- Confirm session ID is listed as unexpired in the database
- Grab user metadata (user ID, username) via session -> user join
- Pull private key from decrypted token for use within request context
Password Resets
- Set up user-provided private key (base64-encoded)
- Derive public key from private key
- Confirm that public key matches the public key for that username in the DB
- Hash + encrypt the newly provided password
- Derive a symmetric key from the new password
- Encrypt the private key with the derived symmetric key
- Store the new password hash + encrypted private key
File uploads
- Generate a symmetric key specific to this file
- Encrypt the symmetric key with our own key pair
- Encrypt and save the file using the symmetric key
- Save file metadata to the database
- Save encrypted file key to the database (incl. user ID + file ID association)
File sharing
- Ensure the current user owns the file
- Decrypt the per-file symmetric encryption key using our private key
- Re-encrypt the per-file key using a key derived from
our private key and the recipient's public key - Save the newly encrypted key to the DB, along with file + recipient user IDs
File Downloads
- Decrypt the per-file symmetric encryption key using our private key
and the owner's public key (the owner might be us) - Load the encrypted file from disk
- Decrypt and deliver the file using the per-file key
Thanks! Questions?
- https://midwestphp.org/chat - throw questions in le Discord
- https://ian.im/cryptmw20 - this presentation
- https://github.com/iansltx/salty-files - the code
- https://twitter.com/iansltx - me
- https://github.com/iansltx - my code
Building Encrypted Systems Worth Their Salt - MidwestPHP 2020
By Ian Littman
Building Encrypted Systems Worth Their Salt - MidwestPHP 2020
Thanks to libsodium's availability in PHP as of 7.2, and the Halite userland library on top of it, building cryptographically secure applications in PHP has never been easier. We'll look at functionality made available to Halite, and use it to build a secure file storage application where full database and app server file system access doesn't allow for in-the-clear file reads.
- 1,633