'Calling openssl_pkey_export() twice with the same passphrase gives different results

I have basic knowledge on asymmetric cryptography but I'm a newbie regarding OpenSSL PHP Extension and I'm confused after executing the following piece of code (please, note that openssl_pkey_new() is called just once but openssl_pkey_export() with passphrase is called twice):

// Based on:
// PHP: openssl_pkey_new - Manual
// => https://www.php.net/manual/es/function.openssl-pkey-new.php#111769
$config=array(
    "digest_alg" => "sha512",
    "private_key_bits" => 4096,
    "private_key_type" => OPENSSL_KEYTYPE_RSA,
);
// Create the private and public key
$res=openssl_pkey_new($config);
// Extract the private key from $res
openssl_pkey_export($res, $privKey);
openssl_pkey_export($res, $privKeyEnc1, "12345678");
openssl_pkey_export($res, $privKeyEnc2, "12345678");
print_r($privKeyEnc1); /* Output 1 */
print_r($privKeyEnc2); /* Output 2 */

I run it using php -a and I figure out that Output 1 and Output 2 ($privKeyEnc1 and $privKeyEnc2) are different (I post first and last characters only):

Output 1 ($privKeyEnc1):

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIJnDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIu9pGQ4UYhHMCAggA
...
b/Jzga55d9CZAez70XZ1IcDlqhtfCS0Q7+RDwdXgsAd9IYrZaVKBrUOxhaSc/Xe8
9GBsV9M67b7uyJ1wAeEpJw==
-----END ENCRYPTED PRIVATE KEY-----

Output 2 ($privKeyEnc2):

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIJnDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIiNaCw3b9l9wCAggA
...
bOL2WGBOymb6db0G5/IdIs7zQx6aQjOtoFx4hm0cY4YmEmNKKdiXOoVpRZT4SBRw
t8ksuWHoESag0z4NETpetw==
-----END ENCRYPTED PRIVATE KEY-----

Is this wrong or is it normal behavior? Can anybody explain in detail why this occurs?



Solution 1:[1]

The PHP code creates a PEM encoded encrypted PKCS#8 key. During encryption, random values for salt and IV are generated, which result in a different encryption each time even for the same (unencrypted) key, which is why the encrypted keys differ.


To illustrate this, consider the following encrypted key generated with your code (for simplicity a 512 bit key is used, in practice the key size must be >= 2048 bits for security reasons):

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBtDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIzjf3uTuaOSQCAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECNaB+caJ1Ew7BIIBYJRrmSSelAKO
7PQR62MscnZmKcOnlJJ++5zryaV/B9qQdWQYRi9KYiyNAuVsvJgw9GsNZVjB99Mf
Ldx1a1ppOQ4tznhAl8db7cYYsREw1AbyTgQ5c58VQCNCVUqGWSS09zSooZA9dkBT
CE52vx0/SwNfDOUlW/NH9eQwQy3WAex7mYPSwOZfuTl00Wz7UuJE4HaXAS3F65Wb
4mt7nmtLk1hEFEPUFzxHh7xpXxDFFG2SN1iMUavQFp61947Y7JS+LhiP+B3gREiA
OFibK8q2sBJGZM2DnN+irA+meuQE/mUtsyWx4KSinUBvfFAXI+448lkV3+Wke7yK
M+BizIK6ua+4oAn33oj7Vd/SFiq0/h0rtXE/OvRMeTKGr1wBx64y+PhzuIsXarpQ
OunNUN2rnqBYlyRUY6PUB9XPJ8DlScgvKetseby+yg5aDSejjcdZyC3rwX+HvdVO
cQ8mPDn6Zrg=
-----END ENCRYPTED PRIVATE KEY-----

With an ASN.1 parser, e.g. https://lapo.it/asn1js/, the following decoding results:

enter image description here

This provides the following information about the encrypted key:

  • The symmetric key applied for encryption is derived using PBKDF2. In addition to the password, HMAC/SHA256, the randomly generated salt 0xCE37F7B93B9A3924 and an iteration count of 2048 are applied.
  • The encryption itself takes place with des-EDE3-CBC, i.e. with Triple DES in CBC mode, which implies a 192 bits key. The randomly generated IV 0xD681F9C689D44C3B is used. The byte sequence 0x946B99249E... is the ciphertext.

The random salt results in a different symmetric key for each encryption, even with identical password. Both, the random symmetric key and the random IV cause a different ciphertext i.e. different encrypted key for each encryption, which strengthens security.


Test:

The logic described above can be easily tested by decrypting the key. For decryption ciphertext, password, salt and IV are required, e.g.:

$encryptedPkcs8 = hex2bin("946b99249e94028eecf411eb632c72766629c3a794927efb9cebc9a57f07da90756418462f4a622c8d02e56cbc9830f46b0d6558c1f7d31f2ddc756b5a69390e2dce784097c75bedc618b11130d406f24e0439739f15402342554a865924b4f734a8a1903d764053084e76bf1d3f4b035f0ce5255bf347f5e430432dd601ec7b9983d2c0e65fb93974d16cfb52e244e07697012dc5eb959be26b7b9e6b4b9358441443d4173c4787bc695f10c5146d9237588c51abd0169eb5f78ed8ec94be2e188ff81de044488038589b2bcab6b0124664cd839cdfa2ac0fa67ae404fe652db325b1e0a4a29d406f7c501723ee38f25915dfe5a47bbc8a33e062cc82bab9afb8a009f7de88fb55dfd2162ab4fe1d2bb5713f3af44c793286af5c01c7ae32f8f873b88b176aba503ae9cd50ddab9ea05897245463a3d407d5cf27c0e549c82f29eb6c79bcbeca0e5a0d27a38dc759c82debc17f87bdd54e710f263c39fa66b8"); 
$tripleDesKey = hash_pbkdf2("sha256", "12345678", hex2bin("ce37f7b93b9a3924"), 2048, 24, true);
$pkcs8Der = openssl_decrypt($encryptedPkcs8, "des-EDE3-CBC", $tripleDesKey, OPENSSL_RAW_DATA, hex2bin("d681f9c689d44c3b"));
$pkcs8Pem = "-----BEGIN PRIVATE KEY-----\n" . chunk_split(base64_encode($pkcs8Der), 64, "\n") . "-----END PRIVATE KEY-----";
print($pkcs8Pem . PHP_EOL);

which gives the following PKCS#8 key:

-----BEGIN PRIVATE KEY-----
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzw++K1mInb7pX8tH
LEwE/ZCpoRXcKvwCYPCVK4Rfor4Wt32BrdHGY6d8tTFFAdGKH5fPSyVDQWPBjyEM
I51+1wIDAQABAkBx7UyKF4I2oSNQ5MztT4pzZZQfoKJ6OByq79RzlCr2pDkEQHw+
PIFKOXXa0jKPFD9HqmNTyCJBXkrDg40RpThxAiEA6Vgs+h1wpR4ZD+woJSFhRkqZ
QY/rKZNDPL0aZZE0cxkCIQDjKkyZmxDfrns0wua9Mvp5mqOWjgdGX/qtfNKr2G4v
bwIgL6D45UCXGozvLqnUc+fBVDir2Y8HwB+37LDor2yZGRkCIQCp6QqQXe66D+yx
oxIo88drS2IOiz8fwUxjlRiSVnjb2wIhAJg4TNnK/0kx5yUrd/dT88ekQ7Pp7md2
DqALeeYGX1gE
-----END PRIVATE KEY-----

An equivalent result would be obtained with e.g.:

openssl pkcs8 -topk8 -inform pem -in <encrypted PKCS#8 key> -outform pem -nocrypt -out <PKCS#8 key>

For completeness: The encryption algorithm can be changed by the 4th parameter in openssl_pkey_export(), e.g.:

openssl_pkey_export($res, $privKeyEnc1, "12345678", array('encrypt_key_cipher' => OPENSSL_CIPHER_AES_256_CBC));

applies AES-256 in CBC mode instead of Triple DES in CBC mode.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1