Examples

Authenticated encryption with password

Below is the source for a command line tool that can be used to encrypt and decrypt files with a password. It derives key from a user supplied password, uses Camellia with a 256-bit key in CBC mode and uses HMAC-SHA512 to authenticate the cipher text. The example is written for Python 3.5 or newer.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
import base64
import getpass
import hashlib
import hmac
import os
import sys

import camellia

HMAC_ALGO = "sha512"

PBKDF_ROUNDS = 10000000  # Larger = better but slower
PBKDF_HASH = "sha512"


def _print_usage():
    print("Usage: {} --encrypt|--decrypt INFILE OUTFILE".format(sys.argv[0]))


def _pad(data):
    byte_and_len = camellia.block_size - len(data) % camellia.block_size
    return data + bytes([byte_and_len] * byte_and_len)


def _unpad(data):
    return data[0:-data[-1]]


def encrypt(password: str, plaintext: bytes) -> str:
    salt = os.urandom(16)  # Random salt each time
    # Derive key from password, to compensate weaker passwords
    key = hashlib.pbkdf2_hmac(PBKDF_HASH, password.encode(), salt,
                              PBKDF_ROUNDS, dklen=64)
    # Use individual keys for encryption and authentication
    key_encryption, key_authentication = key[:32], key[32:]

    iv = os.urandom(16)  # Random IV, this is important
    encrypter = camellia.new(key_encryption, camellia.MODE_CBC, IV=iv)

    # The data is padded with PKCS#5
    cipher_text = encrypter.encrypt(_pad(plaintext))

    # Authentication tag
    mac = hmac.new(key_authentication, iv + cipher_text, HMAC_ALGO).digest()

    # Rounds are serialized to potentially increase it for new files
    return "{}.{}.{}.{}".format(
        base64.b64encode(salt).decode(),
        PBKDF_ROUNDS,
        base64.b64encode(iv + cipher_text).decode(),
        base64.b64encode(mac).decode()
    )


def decrypt(password: str, encrypted: str) -> bytes:
    encoded_salt, rounds, encoded_iv_cipher, encoded_mac = encrypted.split(".")
    rounds = int(rounds)

    # Generate key
    key = hashlib.pbkdf2_hmac(PBKDF_HASH, password.encode(),
                              base64.b64decode(encoded_salt),
                              rounds, dklen=64)

    key_encryption, key_authentication = key[:32], key[32:]

    iv_cipher = base64.b64decode(encoded_iv_cipher)

    # Compare in time-safe manner, to prevent an attacker learning
    # about the newly computed MAC.
    if not hmac.compare_digest(hmac.new(key_authentication,
                                        iv_cipher, HMAC_ALGO).digest(),
                               base64.b64decode(encoded_mac)):
        raise ValueError("mac does not match, invalid password or data")

    iv, cipher_text = iv_cipher[:16], iv_cipher[16:]

    decrypter = camellia.new(key_encryption, mode=camellia.MODE_CBC, IV=iv)

    # Decrypt and remove padding
    return _unpad(decrypter.decrypt(cipher_text))


if __name__ == "__main__":
    if len(sys.argv) != 4:
        _print_usage()
        exit(1)

    if not os.path.isfile(sys.argv[2]):
        print("Not found: {}".format(sys.argv[2]))
        exit(2)

    password = getpass.getpass()

    try:
        if sys.argv[1] == "--encrypt":
            with open(sys.argv[2], 'rb') as infile:
                with open(sys.argv[3], 'wt') as outfile:
                    outfile.write(encrypt(password, infile.read()))
        elif sys.argv[1] == "--decrypt":
            with open(sys.argv[2], 'rt') as infile:
                with open(sys.argv[3], 'wb') as outfile:
                    outfile.write(decrypt(password, infile.read()))
        else:
            _print_usage()
            exit(1)
    except (IOError, ValueError) as e:
        print(e)
        exit(4)