0

I'm not a security expert but I have to encrypt and decrypt files using openssl in PHP in a way that the user can define a password for the encryption. I'm using aes-256-gcm as the cipher method.

My actual code to generate a secure key is:

$password = '12345'; //Entered by the end user

$salt = openssl_random_pseudo_bytes(16);
$key = openssl_pbkdf2($password, $salt, 32, 10000, 'sha256');

I store the generated salt prefixed to the encrypted file. The password is not stored, it is known only by the user.

The solution works. My question would be whether this solution is good and secure enough today?

Vmxes
  • 1,333
  • 14
  • 24
  • You always wonder the security or point of a salt, when you end up storing the salt right there with the encrypted output. – IncredibleHat Jun 23 '20 at 13:22
  • 1
    pbkdf2 is not state-of-the-art, but it is still pretty good and considered Good Enough™. Use an unpredictable salt. I'm not sure what that `openssl_random_pseudo_bytes` call does. An important parameter is the iteration count. 10,000 is somewhat low. In general this number should be as large as possible consistent with a good user experience and acceptable performance. – President James K. Polk Jun 23 '20 at 13:35
  • What would be better instead of pbkdf2? – Vmxes Jun 23 '20 at 13:48
  • If you are on PHP >= 7.2 I would recommend Argon2 or (better) Argon2ID (needs PHP 7.3). An explanation is here: https://framework.zend.com/blog/2017-08-17-php72-argon2-hash-password.html, the PHP-manpage has infos: https://wiki.php.net/rfc/argon2_password_hash and here on SO many, e.g. https://stackoverflow.com/questions/47602044/how-do-i-use-the-argon2-algorithm-with-password-hash – Michael Fehr Jun 23 '20 at 15:08
  • @Michael Fehr: Maybe I'm wrong, but Argon2 hashes the password nicely, but in my case, I need the exact same key at encryption time and decryption time based on the password. Argon2 is good for storing user passwords but I don't see how can I use it in case of symmetric encryption. – Vmxes Jun 23 '20 at 15:48
  • 1
    @IncredibleHat See https://security.stackexchange.com/questions/17421/how-to-store-salt for the security benefits of salting the password hash. The answer by polynomial does a nice job of explaining why this method still protects against certain types of attacks (e.g. rainbow tables), even if the salt is known to the attacker. – mti2935 Jun 23 '20 at 16:22
  • Why should Argon2 not be applicable in your case if PBKDF2 is? [Here](https://argon2.online/) Argon2 is presented in a nice and simple way. – Topaco Jun 24 '20 at 05:54
  • 1
    @Topaco: Thanks for the link. However, according to the PHP manual, I can not use my own salt with Argon2 and password_hash. This is worth another question on how to implement the functionality of the linked site in PHP. – Vmxes Jun 24 '20 at 06:34
  • I see, thanks. Though it's possible to set a salt for [`password_hash`](https://www.php.net/manual/en/function.password-hash.php) via the `$options` parameter (also for Argon2, I checked it), this is deprecated (for security reasons) since v 7.0. What a pity, imo this option shouldn't have been removed. – Topaco Jun 24 '20 at 07:46

2 Answers2

2

As @Topaco stated the "set salt" option was disabled in PHP/openssl. Fortunately there is another crypto library available in a lot of modern PHP-versions - libsodium (sodium). Here we find the necessary method to hash passwords in a reproducible way as you need it for encryption purposes.

The "sodium_crypto_pwhash" method is described here: https://www.php.net/manual/de/function.sodium-crypto-pwhash.php. You should check that libsodium is enabled (the easiest way is phpinfo()):

sodium
sodium support => enabled
libsodium headers version => 1.0.17
libsodium library version => 1.0.17

Now you are ready to hash your passwords. Please note that I used a fixed salt (that is totally insecure) just for demonstration purposes. Same to the "salt" the "opslimit" and "memlimit" variables need to get stored to get a reproducible (decryption) password.

<?php
echo 'php version: ' . PHP_VERSION . ' openssl version: '
    . OPENSSL_VERSION_TEXT . ' sodium: '
    . SODIUM_LIBRARY_VERSION . '<br>';
echo 'key derivation with PHP-libsodium' . '<br>';
echo 'https://stackoverflow.com/questions/62535675/proper-way-to-generate-key-from-password-to-openssl-encrypt-in-php' . '<br>';
$password = '12345'; //Entered by the end user
//$salt = openssl_random_pseudo_bytes(16); // openssl-randombytes
//$salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES); // libsodium randombytes
// ### just for demonstration purpose I'm using a static salt
$salt = "1234567890123456";
$key = sodium_crypto_pwhash(
    32,
    $password,
    $salt,
    SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
    // or SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE  or SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE
    SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE,
    // or SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE or SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE
    SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
);
echo PHP_EOL . '<br>' . 'key: ' . bin2hex($key) . PHP_EOL . '<br>';
?>

the reproducible result:

php version: 7.4.6 openssl version: OpenSSL 1.1.1g 21 Apr 2020 sodium: 1.0.17
key derivation with PHP-libsodium
https://stackoverflow.com/questions/62535675/proper-way-to-generate-key-from-password-to-openssl-encrypt-in-php

key: 8a2a973c147a18fe2a60f7a2c4513e75141a12be0bdc793243fa12b067a3acbe

Edit: complete file encryption for small files with PHP/libsodium, Argon2ID as KDF and XCHACHA20POLY1305 authenticated encryption

A good source for short examples on sodium/libsodium is https://www.zend.com/blog/libsodium-and-php-encrypt. The "intro-pages" of libsodium do have some examples on how to use it - the following codes are taken from this side (https://www.php.net/manual/en/intro.sodium.php). There are no checks or error handling, you just need a file "plaintext.txt".

encryption.php:

<?php
// https://www.php.net/manual/de/intro.sodium.php
$password = 'password';
$input_file = 'plaintext.txt';
$encrypted_file = 'encryption1.enc';
$chunk_size = 4096;

$alg = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13;
$opslimit = SODIUM_CRYPTO_PWHASH_OPSLIMIT_MODERATE;
$memlimit = SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE;
$salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES);

$secret_key = sodium_crypto_pwhash(SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES,
    $password, $salt, $opslimit, $memlimit, $alg);

$fd_in = fopen($input_file, 'rb');
$fd_out = fopen($encrypted_file, 'wb');

fwrite($fd_out, pack('C', $alg));
fwrite($fd_out, pack('P', $opslimit));
fwrite($fd_out, pack('P', $memlimit));
fwrite($fd_out, $salt);

list($stream, $header) = sodium_crypto_secretstream_xchacha20poly1305_init_push($secret_key);

fwrite($fd_out, $header);

$tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
do {
    $chunk = fread($fd_in, $chunk_size);
    if (stream_get_meta_data($fd_in)['unread_bytes'] <= 0) {
        $tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL;
    }
    $encrypted_chunk = sodium_crypto_secretstream_xchacha20poly1305_push($stream, $chunk, '', $tag);
    fwrite($fd_out, $encrypted_chunk);
} while ($tag !== SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL);

fclose($fd_out);
fclose($fd_in);
?>

decryption.php

<?php
// https://www.php.net/manual/de/intro.sodium.php
$encrypted_file = 'encryption1.enc';
$decrypted_file = 'decrypt1.txt';
$password = 'password';
$fd_in = fopen($encrypted_file, 'rb');
$fd_out = fopen($decrypted_file, 'wb');
$chunk_size = 4096;
$alg = unpack('C', fread($fd_in, 1))[1];
$opslimit = unpack('P', fread($fd_in, 8))[1];
$memlimit = unpack('P', fread($fd_in, 8))[1];
$salt = fread($fd_in, SODIUM_CRYPTO_PWHASH_SALTBYTES);

$header = fread($fd_in, SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES);

$secret_key = sodium_crypto_pwhash(SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_KEYBYTES,
    $password, $salt, $opslimit, $memlimit, $alg);

$stream = sodium_crypto_secretstream_xchacha20poly1305_init_pull($header, $secret_key);

$tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
while (stream_get_meta_data($fd_in)['unread_bytes'] > 0 &&
    $tag !== SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL) {
    $chunk = fread($fd_in, $chunk_size + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES);
    $res = sodium_crypto_secretstream_xchacha20poly1305_pull($stream, $chunk);
    if ($res === FALSE) {
        break;
    }
    list($decrypted_chunk, $tag) = $res;
    fwrite($fd_out, $decrypted_chunk);
}
$ok = stream_get_meta_data($fd_in)['unread_bytes'] <= 0;

fclose($fd_out);
fclose($fd_in);

if (!$ok) {
    die('Invalid/corrupted input');
}
?>
Michael Fehr
  • 3,946
  • 2
  • 9
  • 25
  • Thanks! Maybe the entire encryption process should be switched from openSSL to Sodium. Too bad the documentation is pretty incomplete yet. – Vmxes Jun 25 '20 at 13:43
0

I found the following code, which contains a class that encrypts/decrypts securely using libsodium with a password

https://gist.github.com/bcremer/858e4a3c279b276751335dc38fc162c5

Shai Coleman
  • 718
  • 11
  • 15