5

Today I learned that "password" tends to mean a memorizable string of an arbitrary number of characters, while "key" means a highly random string of bits (of a specific length based on the encryption algorithm used).

And so today I first heard of the concept of a Key derivation function.

I'm confused about how to derive a 32-byte key from a password of arbitrary length (in PHP).

The following approach works but ignores the instruction of "[The salt] should be generated randomly" (so does Sodium):

$salt = 'this salt remains constant';
$iterations = 10;
$length = 32;
$aesKey = hash_pbkdf2('sha256', $somePasswordOfArbitraryLength, $salt, $iterations, $length, false);

The following approach also works but doesn't quite feel right either:

$hash = password_hash($somePasswordOfArbitraryLength, PASSWORD_BCRYPT, ['cost' => $iterations]);
$aesKey = substr($hash, -$length);//this smells weird

With all of my searching, I'm surprised I haven't found an "official" way in PHP to derive a 32-byte key from a password deterministically.

How should I do it?

P.S. I'm using Laravel in PHP and want to use AES-256-CBC encryption like this:

$encrypter = new \Illuminate\Encryption\Encrypter($aesKey, 'AES-256-CBC');
$encryptedText = $encrypter->encrypt($text);

Laravel's encryption helper (e.g. Crypt::encryptString('Hello world.')) seems unfit for my requirements since I want each user's data to be encrypted separately based on each individual's password.

Whatever key derivation function I use needs produce the same key every time since I'll be using symmetric encryption to then decrypt strings that that user had encrypted with that key.

P.P.S. Thanks to questions 1, 2, and 3 for introducing certain concepts to me.

Ryan
  • 22,332
  • 31
  • 176
  • 357
  • So you want to derive a key from the user’s password and use that key to encrypt their data? – Pieter van den Ham Aug 10 '18 at 17:49
  • 1
    Why would you not generate a completely random 32 byte string instead of trying to derive it from another source? – Devon Bessemer Aug 10 '18 at 17:50
  • @Pete Yes, that's what I'm saying. – Ryan Aug 10 '18 at 17:51
  • @Devon I'm not sure I understand. I don't want the encryption key to be stored anywhere (or derivable from anything stored anywhere) other than the user's mind. – Ryan Aug 10 '18 at 17:53
  • Learn the word "hash" and "hash function" -- that will help a lot. Also, read up on SALT and finally Initialization vectors. Each one is a progressively deeper level of security added to encryption. – Joe Love Aug 10 '18 at 17:56
  • A "hash" is irreversable, so if you convert "password" to "123456", you cannot convert "123456" back to password.. but the trick is to compare the hash of the password they type in (plus salt, etc) to the hash stored in the DB. Hope this helps. – Joe Love Aug 10 '18 at 17:57
  • 2
    If you encrypt the users data based on their password, it's going to make changing passwords incredibly difficult – Joe Love Aug 10 '18 at 17:58
  • @JoeLove I already know that hashing is "one way", and I wonder if you misread my post. The hashing in the first example is only to generate `$aesKey`. As for your point about making "changing passwords incredibly difficult", I agree, and that drawback is acceptable in my particular case. – Ryan Aug 10 '18 at 18:03
  • @Devon I'm still having trouble following you. Neither of my code examples in the post above require storing the 32-byte key anywhere (or storing anything that could derive it). We could label either one of those functions as `deriveKeyFromPassword($pw)`. Then, whenever a user needs to encrypt or decrypt text, he uses that function (providing his password to it) to get a key and then calls something like `encrypt($text, $key)` (and later `decrypt($encryptedText, $key)`). Right? – Ryan Aug 10 '18 at 18:13
  • 2
    OK I think I understand now. So every time they encrypt or decrypt, you are requiring them to re-send their password to you? In that case, a consistent password hash should be used, but you'll need to store the salt and iterations somewhere to get the same hash each time, you'll also need to re-encrypt all of the data when they change their password. You don't need to use the same salt per person, but you do need to store the salt somewhere, perhaps in your users table. – Devon Bessemer Aug 10 '18 at 18:19
  • @Devon so it sounds like you're saying that the hash_pbkdf2 example looks good and could be called `deriveKeyFromPassword($pw, $salt)`? Thanks. – Ryan Aug 10 '18 at 18:38
  • Instead of using symmetric encryption and requiring the user to both send their password over the wire and returning unencrypted data back over the wire, use asymmetric encryption, store the user's public key, and have the decryption with the private key happen client-side. Because of the way asymmetric crypto works you can also use more than one public/private pair to decrypt a given message. – Sammitch Aug 10 '18 at 19:54
  • @Sammitch I'd do that if I knew the answers to: What's a great JS library for asymmetric encryption? And how can I use it in a way that allows users to set up their private key once, choose a secure but memorizable password (e.g. "correct horse battery stapler"), and then only ever be prompted for their username and the password they've memorized? I don't want to burden them with dealing with long private keys. Maybe there's a way for their password to be used to symmetrically encrypt their private key, which gets saved in the DB in their user record. So I need a JS lib for asym & sym crypto. – Ryan Aug 10 '18 at 21:10
  • 1
    https://download.libsodium.org/doc/public-key_cryptography/ – Sammitch Aug 10 '18 at 22:00
  • @Sammitch I guess I'll check out https://github.com/jedisct1/libsodium.js It looks super complicated so far (like I need a PhD in cryptography), but it seems like it might support both asym and sym in JS and might cover everything I want to do. Thanks. – Ryan Aug 10 '18 at 22:15

3 Answers3

3

For hash-pbkdf2 you say:

"The following approach works but ignores the instruction of "[The salt] should be generated randomly"

Well, the fix to that is to do generate the salt randomly, and store it with the ciphertext. See this question for methods on how to generate secure random bytes within PHP. The output can then be used as key to encrypt; of course the key will always be regenerated using the stored salt and memorized password, and doesn't need to be stored. Note that keys consist of raw bytes; it's probably best to retrieve a raw key from hash-pbkdf2 (the last parameter).

Note that the iteration count should be as high as possible. Normally 100,000 or so is considered optimal nowadays, but the higher the more secure. It takes about as much time for an attacker to calculate the resulting key for each password, and as passwords only contain 30 to 60 bits (for good passwords) it really helps against dictionary attacks.

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
  • It's funny how I kept misreading that instruction and was interpreting it as needing to be randomly generated every time (instead of it being acceptable to generate it once with `random_bytes` and store it alongside the ciphertext. Thanks. – Ryan Aug 12 '18 at 00:11
2

If you wish to have them re-send their password every time you want to decrypt or encrypt the stored strings, you will have to use a consistent password hash and store the salt and iterations somewhere.

If you use the password_hash function, you'll never end up with the same value because of the randomly generated salt.

>>> password_hash('abc', PASSWORD_BCRYPT)
=> "$2y$10$xR8tZQd0ljF5Ks3QrQt7i.vAbv.xVUc97uh.fX4w0mi/A647HlEWS"
>>> password_hash('abc', PASSWORD_BCRYPT)
=> "$2y$10$KzZWeg.o/4TyJVryWrz/oeWQ6VGj0JnPDW.d.Cp0svu8k6qKBcbWu"

You can pass a salt through the options but this is deprecated through password_hash, so I'd recommend you stick with your first solution.

You don't need to use the same salt for every person, you can generate a random salt and store that somewhere, such as the users table.

Keep in mind, with this type of key derivation, you'll need to re-encrypt all of the values every time the user changes their password.

Devon Bessemer
  • 34,461
  • 9
  • 69
  • 95
0

Here is my updated function

But I'd still appreciate answers from experts since this feels very unofficial and home-grown and makes me wonder whether it's breaking any "best practices" of security.

I was surprised not to find a simple function built into PHP.

/**
 * It seems like we just need a way of getting a 32-byte key when all we have is a human-memorizable password and a salt for that user. But this function feels home-grown; what is the most secure way to do PBE (password-based encryption)?
 * 
 * @param string $password
 * @param string $salt      Since this function must be deterministic (return a value consistently based on the inputs), it must accept a salt as an argument rather than generate a random salt every time. Storing a different salt for each user improves security.
 * @param int $length
 * @return string
 */
public static function deriveKey($password, $salt, $length = self::KEY_BYTES) {
    $iterations = max([intval(config('hashing.bcrypt.rounds')), 15]);
    $chars = 2 * $length; //In hex, a byte is always expressed as 2 characters. See more comments below and https://stackoverflow.com/a/43132091/.
    $rawOutput = false; //Default is false. When set to TRUE, outputs raw binary data. FALSE outputs lowercase hexits. Hexit = hexadecimal digit (like "bit" = binary digit). There are 16 hexits: the numbers 0 to 9 and the letters A to F.
    $key = hash_pbkdf2('sha256', $password, $salt, $iterations, $chars, $rawOutput); //A sha256 is 256 bits long (32 bytes), but the raw_output argument will determine how many characters the result has. https://stackoverflow.com/a/2241014/ and https://crypto.stackexchange.com/q/34995/
    return $key;
}

I wonder if Halite or Libsodium-php offer this kind of function.

It seems like Libsodium has a crypto_pwhash function that probably is what I'm looking for (and uses Argon2).

Ryan
  • 22,332
  • 31
  • 176
  • 357