I want to make a site for storing sensitive user data, that retains per-user data encryption in the event that the database is compromised. This is the model I was thinking of, to give the best tradeoff between user privacy/security and usability:
Assumptions
- Hashing and encryption use a reasonably strong round-based key derivation function, like PBKDF2. Encryption uses a block cipher like AES.
- All client/server communication is encrypted and, where not otherwise noted, ephemeral. This is about weaknesses in the design of the storage.
Setup
- Generate a global salt for hashing all user IDs.
User IDs are an arbitrary string chosen by the user, like a username or email address. Using an email address as a User ID is advantageous from a usability standpoint, but it opens the user up to targeted/dump-based known-address hacks, and to guaranteed data compromise in the event of a server key breach (unless they opt-out to notifications).
Account creation / configuration
Before creating the user account, it should be checked that the User ID is not already taken, using the Login procedure described below.
- Save the user's ID, hashed with the current global salt.
- If the user wishes to opt into notifications, save the user's email address encrypted separately, with a key not included in the database (set as part of the live configuration).
- Save the user's password, hashed with a unique salt.
User data manipulation
There are two approaches to user data encryption. The former allows for passwords to be reset, while the latter allows for true encryption of user data. The implementation may allow users to pick which they want, or it may use one across-the-board.
User ID-based encryption
User data is encrypted with the User ID as the key.
Password-based encryption
User data is encrypted with the User ID and a hash of the password, using a different unique salt, as the key.
Maintenance
- Generate a new salt periodically (with long periods, on the order of weeks to months).
- For space and record security purposes (and to prevent the time for an invalid login from ballooning indefinitely), delete users who haven't logged in within X periods.
- If the user is opted-in to notifications, they should be notified of this well in advance.
- If the User ID is their email address and they are using User ID-based encryption, the user may opt in to have their account data exported and sent to them via email before being deleted.
The salts cycle over time so that leaked backups remain secure in the event of an undiscovered transitive Heartbleed-style glimpse into the server's internal state during a request. (Salts would be stored separately from the rest of the database.)
Login
- Client provides their User ID and password.
- Server hashes the User ID against the current global salt.
- If a match is found, server proceeds to the next step.
- If no match is found, server hashes against the previous global salt.
- If no more salts are available, perform "no user" behavior.
- Server does password comparison against the saved password hash.
- If the password is invalid, the user may be prompted for password recovery, depending on whether their configuration and/or the implementation allows it.
Session tokens
On the back-end, the server keeps a "password entered successfully" token in a session database, with a hash of the User ID alongside it. Further authenticated interactions should provide this token as well as the User ID.
If the user is using password-based encryption, the server should generate a random key to encrypt the hash of the password with the salt for the user's data, then return the encrypted hash. (This allows the server to not store the password for decryption, while still ensuring that if a client is compromised, the attacker does not have a vector to decrypt the user data in the event of a server breach after the session expires.)
Refreshing
When a user logs in, their User ID should be re-hashed using the latest global salt, and the "last login date" should be stored (unencrypted) alongside the user's other records.
For performance reasons, the implementation may choose to refresh the User ID hash whenever a User ID is matched, regardless of whether or not the login is successful.
The implementation may also choose to refresh User IDs that match the stored email for accounts with notifications enabled.
"No user" behavior
For usability, the user should be told that the account does not exist, and prompted with the option (if the entered User ID was an email address) to be sent a link to register.
(This does not reveal otherwise secure information since the difference in timing of a password mismatch vs. the timing of a missing user ID already gives it away.)
Are there parts of it that are needlessly complex (they could be simplified without being weaker in any breach scenario)? What are the vulnerabilities of this, and what could be added to make them less vulnerable?