12

I'm building a secure system which stores messages on a server in a Postgresql database. The messages are stored encrypted with PHP's openssl_encrypt() function with the AES-256-CBC method.

At the moment the key used for this function (lets call it the ApplicationKey) is stored as a file on the server with only read access for the www-data user (0400).

But the file exists on the server....Something I actually don't want. So I constructed another layer of security which takes the key locally via a UserHash.

The definition of the new layer:

Instead of storing the key in plain text on the server's file system, I create an encrypted version of the ApplicationKey per user and store these in the database:

UserCipherKey = encrypt(ApplicationKey with UserHash)
storeInDb(UserId, UserCipherKey)

The table then contains the encrypted cipher from the ApplicationKey from every user. This UserHash used to encrypt is constructed via:

UserHash = sha256(UserPassword+UserName+Blowfish)

Where both UserPassword and UserName are filled out by the client, the Blowfish comes from the server. This UserHash is the key to the ApplicationKey to unlock the encrypted message.

Because I don't want the user to enter his/her password every time he requests a page I want to store the UserHash some where, preferably with a time limit eg 30 minutes of inactive use.

It sounded right to use a PHP session for this as it's bound to the client, the client closes the window-> session gets destroyed; after inactivity the session gets destroyed etc... But the data in this session is also stored on the servers file system; so we're back at square one.... Because if the filesystem gets compromised the directory with the sess_* files will be exposed and so will the hashes be...... You see my concern?

My problems / concerns
1 - If I want to store the UserHash locally, is there an other thing to use then the php-session. Is using HTML5's Local Storage better/safer then php-sessions?

2 - If someone looses his/her password, and we need to reset it; what is the best way to do this?

3 - Or am I doing something terribly wrong?

atk
  • 2,156
  • 14
  • 15
stUrb
  • 277
  • 1
  • 3
  • 12
  • What about storing the key encrypted in a client cookie? – Lucas Kauffman Jan 03 '14 at 20:02
  • 2
    Be carefull with using the password alone: when the user changes the password he wont be able to access the old data. You eighter have to re-encode all old data or you introduce a new indirection: the user hash decrypt a session key. When you set a new password you keep the session key and re-encode it with new user hash. BTW: if you need to store encrypted data from a non user-process, use public key crypto for the session key. – eckes Jan 04 '14 at 05:10
  • @eckes I thought about that, as there always will be a administrator group with access to the key via their own login. So they can then create a new `UserCipherKey` of the original ApplicationKey. (which they can get via their own UserCipherKey – stUrb Jan 04 '14 at 09:56
  • @LucasKauffman storing in a Cookie makes the `UserHash` being sent over the wire with every request. Although the system only works if SSl is present, it just sounds wrong. – stUrb Jan 04 '14 at 09:57

4 Answers4

16

As I understand your situation:

  • You have to store, on the server, some data ("messages") which the user must be able to read.
  • The server may be able to read the data at some point, if only transiently.
  • But the server should not keep this power permanently. If an attacker steals a complete copy of the server hard disk (e.g. a full backup), then he should not be able to read the data.
  • The user, being a user, has very little storage capacity on his side; at best, he may remember a password and/or possibly a recovery code which the user wrote down on some paper at his home (but while the user may accept to type his password on a daily basis, he will refuse to enter a huge, fat recovery code that often; the "recovery code" should be a way to recover from a forgotten password).

Under these conditions, the only possible solution is that some password-based encryption is done. Indeed, the combination of the data stored server side and the user password should be enough to recover the data. Since we are talking about the Web, the client is feeble, when it comes to computational tasks; so the encryption and decryption will have to be done on the server. Anyway, you want the decryption to be done on the server, so that any data formatting (turning the message into HTML) occurs on the server as well.

What you describe is not far from the truth (where "the truth" is "the best you can hope for"). Namely, you will have to do the following:

  • For each user, there is a user-specific symmetric key Ku. That key is generated server-side when the user account is created, with a strong PRNG, and has appropriate length (say 128 bits -- 256 bits are overkill, but hey, go for 256 bits if you need a big number to woo investors).
  • For each user, a "recovery code" Ru is generated along with Ku, with similar characteristics. This will be used if the user forgets his password. The encryption of Ku, using Ru as key, is stored on the server (let's call it Fu). Ru is sent to the user, with instructions to print it or write it down and store it safely.
  • Each user has a password Pu. A hash of the password is computed on the server, using a good password hashing function like, indeed, bcrypt. This operation requires a user-specific random salt Su, which is stored on the server. The hash value is extended into some bits (say, 256 bits) with a key derivation function. If you only need 256 bits, then SHA-256 will fit nicely as a KDF.

    Note that we want to extend the hash output only; most bcrypt libraries produce an output as a string which encodes the salt and the hash output. You want to recover both values separately, because you want to store the salt, and you want to have the hash output alone for input to the KDF. It may be simpler to replace bcrypt with PBKDF2: PBKDF2 is its own KDF (output size is configurable) and most PBKDF2 implementations already handle salt and output separately.

  • The KDF output is split into two halves (e.g. two 128-bit values). The first half is Vu, and is the password verification token. The server stores it. The second half is used as a key to encrypt Ku; that encrypted value (let's call it Eu) is stored on the server.

So, for the user U, the server stores Su (the user salt), Fu (the encryption of the user key Ku with the recovery code), Vu (the password verification token) and Eu (the encryption of the user key Ku with the password-derived key). All "messages" for user U will be encrypted with key Ku.

When the user logs in, he sends his name (U) and his password (Pu). Using U as index, the server recovers the stored data. With Pu and Su, the server recomputes the password hash, then extended with the KDF. If the first half of the KDF output is not equal to Vu, then the user's password is wrong, and the user shall be rejected. Otherwise, the user is authenticated (the server has some guarantee that the client is indeed the genuine user U), and the second half of the KDF output can be used to decrypt Eu, yielding the key Ku. At that point, the server knows Ku and can do all the encryption/decryption business.

If the user wants to change his password, then a new salt will be generated, the new password entered, the hash function computed again, yielding a new verification token, and a new Eu. The key Ku, though, is not changed, so the stored messages need not be touched in any way.

If the user forgets his password, then the recovery code can be used as a sort of "alternate password" in order to recover Ku, at which point we are back to the "password change" situation.


Now for some important notes.

When the server learns the user key Ku, then the server should keep it in RAM only. You do not want Ku to hit the disk (that's the whole point of the exercise). Therefore, you shall mind a few thing:

  • PHP sessions are written to disk. What you put in session variables become files ! But you can choose the destination; e.g. you can use a RAM-based filesystem that will be shown to PHP as files, but is really RAM. Alternatively, there is a shared memory support so that no file at all is created (this may be safer; if an attacker hijacks the live server than he can read all files, including those on tmpfs; but such an attacker could also plunder PHP's RAM directly, and read all session data anyway).

  • Virtual Memory, aka "swap space", can induce the kernel to write to disk what is usually in RAM. Swap space is not backupped, but that's still writing. An industrious attacker will scan your garbage cans in search for old, discard hard disks. If a disk fails (electronic board fried), then you cannot wipe out its contents (at least, not easily), but the attacker may recover the data (by replacing the board).

    It might be a good idea to disable swap altogether. Running without swap is fine as long as you have sufficient RAM; and you really want to have sufficient RAM anyway because hitting swap space really kills performance, especially in GC-based languages like PHP. Virtual Memory is one of these ideas which were very good.

  • To some extent, what you do not want to put in session variables can be stored as a cookie on the client. Cookies are not really permanent (if the user has several devices, cookies might not be shared between his browsers) but they can benefit from some "secure storage" -- as secure as things can get on the user's computer.

  • You are talking about messages... so maybe you want to encrypt some data for a user, when this user is not logged in. In that case, the server might not know Ku. To solve this problem, you will need asymmetric encryption: make Ku the private key of a pubic/private key pair, the public key being stored "as is" (unencrypted) in the database. Encryption uses the public key, while decryption requires the private key.

    Doing asymmetric encryption properly is kind of complex; it requires the assembly of several cryptographic algorithms, and there is ample room for devastating mistakes that cannot be detected with tests. You are warmly encouraged to rely on an existing format and software; e.g. GnuPG bindings for PHP.

  • I strongly hope that all communications between client and server use SSL, i.e. HTTPS; and any relevant cookies are marked HttpOnly and Secure.

Thomas Pornin
  • 322,884
  • 58
  • 787
  • 955
  • 2
    Wow Thomas. Thank You! I'm still trying to understand everything in your post, but it is very very helpfull. And I'm trying to adjust it to my web-app – stUrb Jan 04 '14 at 16:40
  • Just a quick question: after the user entered his/her credentials, and they authneticated with the first half of the hash (Vu) where should I store the second half? In a session using a RAM based filesystem? And is [memcached](http://nl1.php.net/manual/en/intro.memcached.php) something to be used with this? – stUrb Jan 04 '14 at 17:37
  • The second half, in the scheme I described, is used to decrypt _Ku_. That's _Ku_ that you keep (in RAM); the second half of the KDF output is just discarded (you don't need it to keep it anywhere). – Thomas Pornin Jan 05 '14 at 00:11
  • Ah, so you keep the Ku in the ram instead of the second half.Fair enough, if the ram gets compromised they have the password derived key (second half) and is the private Ku also within reach. So then you can store the key as good. Now just finding the right memory storage: memcached or APC... – stUrb Jan 05 '14 at 12:01
  • What would happen if the DB got compromised? Wouldn't the attacker be able to decrypt the Ku if he had the recovery code and Fu? – Jason Silberman Jul 26 '15 at 22:00
  • 1
    This is slightly incomplete. The recovery must be authenticated somehow. When $R_u$ is submitted to the server it should check whether it is a valid $R_u$. Otherwise, an attacker might overwrite a legitimate $S_u$, $F_u$, $V_u$ and $E_u$. Storing `HMAC(R_u, S_u)` on the server might be sufficient. – Artjom B. Aug 19 '15 at 13:08
  • What if the service does not use passwords to authenticate users? For example using single sign on there is no user password sent to the server. Is there an alternate way available to this procedure in this case? – David Casillas Aug 09 '16 at 14:03
  • @ThomasPornin That is such an helpful answer, thank you for that! Can you tell me, why it is necessary to create a sha256 hash from the password hash? Isn't it possible to encrypt Ku directly with the password hash from bcrypt? Thanks in advance! – Tom Apr 24 '18 at 20:08
  • 1
    @Tom bcrypt has an output of 192 bits, no more, no less. If you use an encryption algorithm that accepts 192-bit keys, then it works. Otherwise, you have to _do something_. Using a KDF (or a hash function with a sufficiently large output) is a "something" that does not have nasty pitfalls. – Thomas Pornin Apr 24 '18 at 22:56
2

You should consider using HTML5 local storage and decrypting the content on the client side. That way your server never has access to the plaintext data.

That said, there's no perfect solution. XSS vulnerabilities or a server compromise and modified files (and modified javascript) could grab the client-side secret and post it to a 3rd-party site.

"What about storing the key encrypted in a client cookie?" -- the server would have access to that on every request, potentially HTTP requests (for image resources, perhaps) if it's not configured to be used for SSL only.

u2702
  • 2,086
  • 10
  • 11
0

You don't need to make it impossible for the server to get to the data (since it needs it anyway, at least in your example), so you just need to make sure it does not leak easily (especially on disk).

Two possible storage locations would therefore be RAM/Memory of the server (but I dont know how to do that in PHP) or an external process (for example memcached) which can expire the material, authenticate the server.

If you store the key in a memcached instance you could also encrypt it with a secret only known by the web server so you don't have to trust the memcached operator. memcached has a good PHP API and it might generally be a good session store.

eckes
  • 962
  • 8
  • 19
  • If I would use memcached and use `a secret only know by the server` then this secret should also be put on the server right? – stUrb Jan 04 '14 at 10:00
  • And where is this information stored on the server? – stUrb Jan 04 '14 at 10:19
  • 1
    Yes, the encryption secret and the memcached credentials would have to be stored persistent on the filesystem (accessible to the www-run user). There is not really a way to avoid that (unless you use hardware based protection, but there is not really a good solution as TPM sealing is too slow for transactional data). – eckes Jan 09 '14 at 14:38
-2

to me its very simple :) create an extra column on the Users Table called Key for example , you are going to store the key there encrypted with the Hashed Password , Basically you will need the right Password to decrypt the encrypted key , what i used before , if you want to be extremly secure is the concatenation of the hashed password with the IP address or Possible Addresses of your Users this will be impossible to guess , even if someone have access to the Database where you have your Users , it will never have access to the encrypted Encryption Key :) and to the data you have on the other DB . Good Luck .

  • Forgive me for my ignorance, but why was this down-voted? Is there a security vulnerability doing this? – duckbrain May 28 '15 at 22:13