Key Wrapping with a passwordThe protection of encryption keys is important, and where they often have to be protected. This is especially important for symmetric keys and the private key of a public key pair. One standard for this is RFC 5649 [here], and which supports Advanced Encryption Standard (AES) Key Wrap algorithm [AES-KW1, AES-KW2]. We then use an AES key-encryption key (KEK) with a length of 128, 192, or 256 bits, and where we will get 64-bit blocks as an output. We can then encrypt data with a key (\(K_1\)) and which will then be wrapped to give WRAP(\(K_1\)). To decrypt, we then need the KEK to recover \(K_1\). The unwrapping process also checks the integrity of the key. One method is to perhaps use a production environment, and where the keys are stored and wrapped within a Cloud-based system. The KEK will then be protected from access, and used to produce the actual encryption key. In this case we will generate a master key from a password using PBKDF2, and this key will protect an existing data encryption key. This the method that the TrueCrypt disk encryption tool would use, and where the encryption key used on the disk will be protected with a user password. As a salt value is used to generate KEK, we will need to store the salt value used in order to recover the KEK [article][Key wrapping]. |
Outline
TrueCrypt creates an encrypted volume, and where the user just has to reveal their password in order to access it. In order to convert a password into an encryption key, we often use a key derivation function (KDF) such as PBKDF2 or bcrypt, so why is it not the case that we have to re-encrypt the whole of the encrypted disk with the new symmetric key? Well, TrueCrypt uses two keys: a data encryption key and a key-encryption key (KEK). The data encryption key stays the same for every password change and is used to perform the encryption on the data on the disk. Whereas the KEK protects the data encryption key.
In the following, we have a data encryption key, and which will not change (unless there is a reset of the disk). We now need a way to protect this key for someone examining the disk. For this Bob uses a password and a salt value (Figure 1), and generates the master key using PBKDF2. The PBKDF2 is a robust method against hash cracking and does this by implementing a number of hashing rounds. The KEK is then used to wrap the key so that it can be stored on the disk. Along with this, we need the salt value used for the key generation. When the data encryption key is required, we feed in Bob’s password and the salt value and unwrap the key. The unwrapped key should be the key used to encrypt the disk.
Figure 1: Using key wrapping to protect a key
The KEK is then the master key, and can be changed at any time and generated by the user's password. The wrapped version of the data encryption key is then protected when it is stored on the system, and where the user can export the wrapped key as a backup, and then regenerate the KEK using their password.
The following is the code:
import binascii import struct from Crypto.Cipher import AES from Crypto.Protocol.KDF import PBKDF2, scrypt,HKDF import bcrypt from Crypto.Hash import SHA256 from Crypto.Random import get_random_bytes import sys import base64 QUAD = struct.Struct('>Q') # code from https://github.com/kurtbrose/aes_keywrap/blob/master/aes_keywrap.py def aes_unwrap_key(kek, wrapped): n = len(wrapped)//8 - 1 #NOTE: R[0] is never accessed, left in for consistency with RFC indices R = [None]+[wrapped[i*8:i*8+8] for i in range(1, n+1)] A = QUAD.unpack(wrapped[:8])[0] decrypt = AES.new(kek, AES.MODE_ECB).decrypt for j in range(5,-1,-1): #counting down for i in range(n, 0, -1): #(n, n-1, ..., 1) ciphertext = QUAD.pack(A^(n*j+i)) + R[i] B = decrypt(ciphertext) A = QUAD.unpack(B[:8])[0] R[i] = B[8:] return b"".join(R[1:]), A # code from https://github.com/kurtbrose/aes_keywrap/blob/master/aes_keywrap.py def aes_wrap_key(kek, plaintext, iv=0xa6a6a6a6a6a6a6a6): n = len(plaintext)//8 R = [None]+[plaintext[i*8:i*8+8] for i in range(0, n)] A = iv encrypt = AES.new(kek, AES.MODE_ECB).encrypt for j in range(6): for i in range(1, n+1): B = encrypt(QUAD.pack(A) + R[i]) A = QUAD.unpack(B[:8])[0] ^ (n*j + i) R[i] = B[8:] return QUAD.pack(A) + b"".join(R[1:]) kek="000102030405060708090A0B0C0D0E0F" key="00112233445566778899AABBCCDDEEFF" password="qwerty" type=1 if (len(sys.argv)>1): password=str(sys.argv[1]) if (len(sys.argv)>2): key=str(sys.argv[2]) if (len(sys.argv)>3): type=str(sys.argv[3]) KEY = binascii.unhexlify(kek) salt = get_random_bytes(16) if (type==1): KEK = PBKDF2(password, salt, 16, count=1000, hmac_hash_module=SHA256) print ("Using PBKDF2") elif (type==2): KEK = scrypt(password, salt, 16, N=2**14, r=8, p=1) print ("Using scrypt") elif (type==3): KEK = bcrypt.kdf(password=password.encode(),salt=b'salt',desired_key_bytes=16,rounds=100) print ("Using bcrypt") else: KEK = HKDF(password.encode(), 32, salt, SHA256, 1) print ("Using HKDF") wrapped=aes_wrap_key(KEK,KEY) rtn,iv=aes_unwrap_key(KEK,wrapped) print (f"Password: {password}, Salt: {binascii.hexlify(salt)}") print ("\nKEK: ",binascii.hexlify(KEK)) print ("Key: ",binascii.hexlify(KEY)) print ("\nWrapped Key: ",binascii.hexlify(wrapped)) print ("Unwrapped key: ",binascii.hexlify(rtn))
A sample run is:
Using PBKDF2 Password: qwerty123, Salt: b'bdcb4c6762e8937c157f3b1bb77098e2' KEK: b'4e114021b245526a6c0438ca44301e88' Key: b'000102030405060708090a0b0c0d0e0f' Wrapped Key: b'be0a52505ec3fc09ac8b2923c44c35b1c58a2e9a85a81003' Unwrapped key: b'000102030405060708090a0b0c0d0e0f'
The following is the code:
References
[1] Advanced Encryption Standard (AES) Key Wrap with Padding Algorithm [here].