I'm writing a little script that encrypts a file using the user's SSH identity.
- I read the RSA private key from
- Use the private key to encrypt a random 32 byte symmetric key (PKCS#1 OAEP)
- Encrypt a stream of data with AES-256 in counter mode (no extra padding)
- Write all of this to the output stream in 65535 byte chunks
- Append an HMAC-SHA256 of the ciphertext
The idea is that data is encrypted on a user's computer before being pushed to the cloud. I'm using the SSH identity so that there's no passwords to worry about. If the user's ssh private key is compromised, the data being encrypted is compromised as well.
If the user wants to use a password, they'll just encrypt their private key.
Here's the code:
from Crypto import Random from Crypto.Cipher import PKCS1_OAEP from Crypto.PublicKey import RSA from Crypto.Cipher import AES from Crypto.Util import Counter from Crypto.Hash import HMAC, SHA256 def encrypt_stream_v1(identity, stdin, stdout): rng = Random.new() iv = rng.read(8) # counter mode prefix key = rng.read(32) # random AES-256 key # AES-256 in counter mode aes = AES.new(key, mode=AES.MODE_CTR, counter=Counter.new(64, iv)) mac = HMAC.new(key, digestmod=SHA256) # HMAC-SHA256 for ciphertext with open(identity) as f: # the AES-256 key is encrypte with the users rsa private key key = PKCS1_OAEP.new(RSA.importKey(f.read())).encrypt(key) # the output stream begins with the 8 byte iv, the length of the # encrypted AES-256 key in two bytes and the encrypted key itself stdout.write(iv + len(key).to_bytes(2, "big") + key) while True: # encrypt in chunks of 65535 bytes (a two byte integer) chunk = aes.encrypt(stdin.read(0xFFFF)) if chunk: # write the length of the encrypted chunk and the chunk itself stdout.write(len(chunk).to_bytes(2, "big") + chunk) # update the hmac with the ciphertext mac.update(chunk) if len(chunk) < 0xFFFF: break # the two null bytes represent an empty chunk, i.e. the end of the # ciphertext, the last 32 bytes of the output stream are the # HMAC-SHA256 digest stdout.write(b"\x00\x00" + mac.digest())
So the file format is as follows:
7a 70 79 01 # magic bytes and version number (01) 2c 81 6a 25 97 9c 8b 68 # counter iv 01 00 # length of... 46 3e ........ 47 55 7e # symmetric key encrypted with public key 00 08 # length of... 22 12 11 01 2d 66 2e 66 # first encrypted block (repeated) 00 00 # zero-length block signifies end of stream 18 9a ........ 0f 5a 0e # hmac of the ciphetext
The reason the IV is 8 bytes is the counter. In order to get a 128 bit counter (the block size of AES), we use an incrementing 64 bit integer (via
Crypto.Util.Counter) and prepend the random 64 bit iv.
Is there anything wrong with how I'm using RSA/OAEP or AES-256-CTR? Is there anything I can optimize with the file format? Any mistakes I'm not seeing?
Changes so far
- I'm including the entire file header (magic bytes, encrypted key length, encrypted key) in the HMAC-SHA256 calculation
- I'm using a random 128 bit number as the initial value for the counter instead of effectively 64 bits with zeros appended.
Update: The project is now on GitHub