This answer is based on openssl v1.1.1, which supports a stronger key derivation process for AES encryption, than that of previous versions of openssl.
This answer is based on the following command:
echo -n 'Hello World!' | openssl aes-256-cbc -e -a -salt -pbkdf2 -iter 10000
This command encrypts the plaintext 'Hello World!' using aes-256-cbc. The key is derived using pbkdf2 from the password and a random salt, with 10,000 iterations of sha256 hashing. When prompted for the password, I entered the password, 'p4$$w0rd'. The ciphertext output produced by the command was:
U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE=
The process for decrypting of the ciphertext above produced by openssl is as follows:
- base64-decode the output from openssl, and utf-8 decode the
password, so that we have the underlying bytes for both of these.
- The salt is bytes 8-15 of the base64-decoded openssl output.
- Derive a 48-byte key using pbkdf2 given the password bytes and salt with
10,000 iterations of sha256 hashing.
- The key is bytes 0-31 of the derived key, the iv is bytes 32-47 of the derived key.
- The ciphertext is bytes 16 through the end of the base64-decoded openssl
output.
- Decrypt the ciphertext using aes-256-cbc, given the key, iv, and
ciphertext.
- Remove PKCS#7 padding from plaintext. The last byte of
plaintext indicates the number of padding bytes appended to the end
of the plaintext. This is the number of bytes to be removed.
Below is a python3 implementation of the above process:
import binascii
import base64
import hashlib
from Crypto.Cipher import AES #requires pycrypto
#inputs
openssloutputb64='U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE='
password='p4$$w0rd'
pbkdf2iterations=10000
#convert inputs to bytes
openssloutputbytes=base64.b64decode(openssloutputb64)
passwordbytes=password.encode('utf-8')
#salt is bytes 8 through 15 of openssloutputbytes
salt=openssloutputbytes[8:16]
#derive a 48-byte key using pbkdf2 given the password and salt with 10,000 iterations of sha256 hashing
derivedkey=hashlib.pbkdf2_hmac('sha256', passwordbytes, salt, pbkdf2iterations, 48)
#key is bytes 0-31 of derivedkey, iv is bytes 32-47 of derivedkey
key=derivedkey[0:32]
iv=derivedkey[32:48]
#ciphertext is bytes 16-end of openssloutputbytes
ciphertext=openssloutputbytes[16:]
#decrypt ciphertext using aes-cbc, given key, iv, and ciphertext
decryptor=AES.new(key, AES.MODE_CBC, iv)
plaintext=decryptor.decrypt(ciphertext)
#remove PKCS#7 padding.
#Last byte of plaintext indicates the number of padding bytes appended to end of plaintext. This is the number of bytes to be removed.
plaintext = plaintext[:-plaintext[-1]]
#output results
print('openssloutputb64:', openssloutputb64)
print('password:', password)
print('salt:', salt.hex())
print('key: ', key.hex())
print('iv: ', iv.hex())
print('ciphertext: ', ciphertext.hex())
print('plaintext: ', plaintext.decode('utf-8'))
As expected, the above python3 script produces the following:
openssloutputb64: U2FsdGVkX1/Kf8Yo6JjBh+qELWhirAXr78+bbPQjlxE=
password: p4$$w0rd
salt: ca7fc628e898c187
key: 444ab886d5721fc87e58f86f3e7734659007bea7fbe790541d9e73c481d9d983
iv: 7f4597a18096715d7f9830f0125be8fd
ciphertext: ea842d6862ac05ebefcf9b6cf4239711
plaintext: Hello World!
Note: An equivalent/compatible implementation in javascript (using the web crypto api) can be found at https://github.com/meixler/web-browser-based-file-encryption-decryption.
apps/enc.c
makes use of EVP_BytesToKey with an iteration count of 1. For normal passwords, this is totally unsuitable since this can it can be trivially bruteforced. The manual page suggests use of PBKDF2 which is a more appropriate solution. Seeing that this code was used in Ansible Vault, what about starting with an explicit warning not to use this except for backwards compat? – Clamper