3
\$\begingroup\$

I need a rather simple file encryptor/decryptor in Python, after some research, I decided to use tye pynacl library reading the file in blocks, writing them back out, and then at the end using Blake2b to generate a signature for the file. Each file is encrypted with a unique key, which will be distributed along side the encrypted file, with the file key RSA encrypted using a pre-shared key pair, and that whole message signed with ECDSA to verify it came from me.

The encryption/decryption example code:

import base64
import struct
import nacl.secret
import nacl.utils
import nacl.hashlib
import nacl.hash

BUFFER_SIZE = 4 * (1024 * 1024)

def read_file_blocks(file, extra_bytes=0):
    while True:
        data = file.read(BUFFER_SIZE + extra_bytes)
        if not data:
            break
        yield data

def hmac_file(file, key):
    blake = nacl.hashlib.blake2b(key=key)
    with open(file, 'rb') as in_file:
        for block in read_file_blocks(in_file):
            blake.update(block)
    return blake.hexdigest()

def encrypt_archive(archive_name, encrypted_name):
    key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE)
    #Use 4 bytes less than the nonce size to make room for the block counter
    nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE - 4)
    block_num = 0

    box = nacl.secret.SecretBox(key)
    with open(archive_name, 'rb') as in_file, open(encrypted_name, 'wb') as out_file:
        for data in read_file_blocks(in_file):
            #Append the block counter to the nonce, so each block has a unique nonce
            block_nonce = nonce + struct.pack(">I", block_num)
            block = box.encrypt(data, block_nonce)
            out_file.write(block.ciphertext)
            block_num += 1

    hmac_key = nacl.hash.sha256(key + nonce, encoder=nacl.encoding.RawEncoder)
    output = {}
    output['key'] = base64.b64encode(key + nonce)
    output['signature'] = hmac_file(encrypted_name, hmac_key)
    return output

def decrypt_archive(encrypted_name, archive_name, key_info):
    key_bytes = base64.b64decode(key_info['key'])

    key = key_bytes[:nacl.secret.SecretBox.KEY_SIZE]
    nonce = key_bytes[nacl.secret.SecretBox.KEY_SIZE:]

    extra_bytes = nacl.secret.SecretBox.MACBYTES
    hmac_key = nacl.hash.sha256(key_bytes, encoder=nacl.encoding.RawEncoder)
    hmac = hmac_file(encrypted_name, hmac_key)
    if hmac != key_info['signature']:
        print('hmac mismatch')
        return

    block_num = 0
    box = nacl.secret.SecretBox(key)
    with open(encrypted_name, 'rb') as in_file, open(archive_name, 'wb') as out_file:
        # nacl adds a MAC to each block, when reading the file in, this needs to be taken into account
        for data in read_file_blocks(in_file, extra_bytes=extra_bytes):
            block_nonce = nonce + struct.pack(">I", block_num)
            block = box.decrypt(data, block_nonce)
            out_file.write(block)
            block_num += 1

key_info = encrypt_archive("C:\\temp\\test.csv", "C:\\temp\\test.enc")
print(key_info)
decrypt_archive("C:\\temp\\test.enc", "C:\\temp\\test.enc.csv", key_info)

Outside of general mistakes, the two things I'm doing I'm not entirely sure are sound:

  1. To keep the block nonces unique, I create a slightly smaller random list of bytes for the nonce than required, then when encrypting the blocks I append the block number, as a four byte integer to the nonce.

  2. When generating the blake2b hash, for a key, I hash the file key and nonce. This seems somewhat useless overall, since if they have the key and nonce, they could just replace the file. Although, I can't really think of a better alternative that doesn't have similar weaknesses. Should I just ditch that bit, since NaCl does per-block MACs anyhow? (which I found out only after I wrote the hmac code)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.