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:


  • 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.


  • 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.


  • 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.)


  • 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.)


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?

  • 2
    A few minor nitpicks: bcrypt and scrypt aren't really encryption, they are key derivation / hash functions, and a "global salt" is actually called pepper.
    – Ajedi32
    Mar 20, 2015 at 13:53
  • scrypt is a KDF? I was under the impression it was some kind of two-way encryption algorithm based on bcrypt. How does Tarsnap use scyrpt in its file encryption? Mar 20, 2015 at 13:56
  • @StuartP.Bentley scrypt will likely be used as a KDF to derive a key from the user's password.
    – Polynomial
    Mar 20, 2015 at 13:58
  • 1
    I'm not familiar with Tarsnap, but key derivation functions are usually used to slow down offline brute force attacks against data encrypted using a relatively weak key (like a user's password, as opposed to a strong key such as one stored in a file). They are used as a form of key stretching.
    – Ajedi32
    Mar 20, 2015 at 14:00
  • I'm not sure I understand your use case. You give the example of hashing a password using a KDF, which is a normal approach. But then your use case talks about encrypting user data. Passwords aren't really considered user data, and as someone else noted scrypt and bcrypt are one way. (Which is itself the point of using a hash algorithm to store passwords) Mar 20, 2015 at 17:14

1 Answer 1


The "constantly regenerating pepper" component feels like too much complexity for the relatively niche attack scenario it protects against. It seems to me that the moving parts required to support it are more likely to introduce an attack vector (or a scenario for unrecoverable data loss) than protect against one.

  • I considered replacing it with a fixed list of 2^n salts (with the salt to use coming from the first n bits of a hash mod-added against the current period number), but then I realized that doesn't really inhibit anything more than a single pepper would. Mar 21, 2015 at 2:41

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .