Bad Crypto: Encrypting long messages with OpenSSL

Reading Time: 7 min. read

A friend wanted me to look at some bad crypto code he found recently. It was floating around the internet and Reddit until the the blog post was removed.

Here’s the original link:
Encrypt/Decrypt long string with PHP OpenSSL & key pairs

Here’s the saved PDF of the blog post.

The idea behind this post is pretty simple, encrypting some long string securely with OpenSSL by using low level functions in OpenSSL. This is not the way to do it with OpenSSL though.

Cryptography is hard to understand and often gets confused in people’s minds. They assume it’s easy and then they make mistakes later on.

Many programmers think that they just need to encrypt their data using a key and it’s good. While this is generally right, the implementation for this is the hard part. People don’t realize the extra steps and planning that they need to do in order to do some encrypting.

Things like what kind of encryption libraries you’ll use and how you will generate the private/public keys (if any) are important to think about. The modes of operation in the chosen algorithms is even more important. Some certain modes might be vulnerable in a situation you’re using them and other times it’s fine. The list goes on.

With that said, let’s get to the code. This code assumes that you’re using private/public key encryption.

The first line is making a password using system time.

password = sha1(microtime(true) . rand());

Using time is not a way to create any kind of key or password. We can figure out the time that this piece of code ran and recreate the same hash after a few hours. In addition, rand() might return predictable numbers (it will).

One possible fix for this could be using a cryptographically secure pseudo-random number generator (CSPRNG). Instead of a timestamp and a regular random number generator to make this password. See this post by Paragon Initiative for more info on making good random strings and integers.

openssl_private_encrypt($password,$passwordCrypted,file_get_contents("key.private"),OPENSSL_PKCS1_OAEP_PADDING);

The openssl_private_encrypt function is being used in the wrong way. This function is not for encrypting keys or data.

I believe this line was trying to encrypt the symmetric key, so that to decrypt anything you first need to decrypt the symmetric key then use that to decrypt the data.

The function shouldn’t be exposed in PHP because it is made for something else entirely. It is used to make a to implement a signature scheme based on RSA. A signature scheme is a algorithm to make signatures and basically “RSA” is the concept of raising a number to a power modulo, a product of two primes. This concept can be used to implement public-key encryption as well as digital signatures.

The correct way to make an IV is to use a good CSPRNG. The Initialization Vector is part of a encryption mode that allows block ciphers work correctly. Why this is necessary requires a bit of understanding of how block ciphers work.

A block cipher is just something that takes a block (usually 128 bits) and a key as input and spits out another block of the same size, there’s no concept of an IV yet. You use a block cipher in an encryption “mode” (e.g. ECB, CBC, OFB, OCB, etc.) The encryption mode is how you turn the block cipher (which can only encrypt a small block of data) into something that can encrypt a bigger chunk of data. It’s the encryption mode that usually has an IV.

For example, AES Cipher Block Chaining (CBC) works by XORing the previous block with the current block. The very first block has no previous block, so the IV is XORed with it.

The short explanation of IVs is:

  • IVs should be random and generated by a CSPRNG.
  • IVs should not be reused. That is, don’t encrypt plaintext “A” and plaintext “B” with the same IV. Everytime you want to encrypt something, make a new IV.
  • IVs are not secret. They can be stored in plaintext along with the cipher text.

$dataCrypted=openssl_encrypt(json_encode($data),'aes128',$password,false,$iv);

The last line encrypts the message with the password and the IV. The defaults for AES-128 are used here. Which I think openssl_encrypt defaults to AES-128-CBC. This mode can be attacked by using crafted plaintext and IV. You’ll be able to recover the data easily if the IV isn’t random.

A few notes to make here. The average developer should AVOID any library that tries to make the developer think about what encrypting mode to use. They should use libraries that just do the right thing so the dev doesn’t have to think about it.

For example in libsodium, a good crypto library, there are practically 0 options to tweak, the library has been designed to just be secure and easy to use.

Phew! This was a long post, but I hope you’ll learn some new things from it.

A quick recap:

  • Don’t use libraries with multiple options until you know what you’re doing.
  • Do not make your own cryptography code without getting audits and proper knowledge!
  • Study some cryptography basics.

The Bad Encryption Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Create random key
$password = sha1(microtime(true) . rand());
// Encrypt the key (given private key = key.private)
openssl_private_encrypt($password, $passwordCrypted, file_get_contents("key.private"), OPENSSL_PKCS1_OAEP_PADDING);
// Base 64 encoding
$passwordCrypted = base64_encode($passwordCrypted);
// Create IV, here I just make IV based on key, to simplify the process
$iv = substr(md5($passwordCrypted), 0, 16);
// Finally, encrypt data with the generated key
$dataCrypted = openssl_encrypt(json_encode($data), 'aes128', $password, false, $iv);

The Bad Decryption Code

1
2
3
4
5
6
7
8
// Decrypt the key
openssl_public_decrypt(base64_decode($passwordCrypted), $passwordDecrypted, file_get_contents("key.pub"));
// Get the IV
$iv = substr(md5($passwordCrypted), 0, 16);
// Decrypt the data
$dataDecrypted = json_decode(openssl_decrypt($dataCrypted, 'aes128', $passwordDecrypted, false, $iv, OPENSSL_PKCS1_OAEP_PADDING));