354 lines
16 KiB
Python
354 lines
16 KiB
Python
# https://raw.github.com/asimihsan/crypto_example/master/src/utilities/crypto.py
|
|
# (https://github.com/asimihsan/crypto_example/blob/655694e0bea974813d2252a54e69478e272b1d1e/src/utilities/crypto.py)
|
|
# ---------------------------------------------------------------------------
|
|
# Copyright (c) 2012 Asim Ihsan (asim dot ihsan at gmail dot com)
|
|
# Distributed under the MIT/X11 software license, see the accompanying
|
|
# file license.txt or http://www.opensource.org/licenses/mit-license.php.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
import os
|
|
import sys
|
|
import struct
|
|
import cStringIO as StringIO
|
|
import bz2
|
|
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Hash import SHA256, HMAC
|
|
from Crypto.Protocol.KDF import PBKDF2
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Constants.
|
|
# ----------------------------------------------------------------------------
|
|
|
|
# Length of salts in bytes.
|
|
salt_length_in_bytes = 16
|
|
|
|
# Hash function to use in general.
|
|
hash_function = SHA256
|
|
|
|
# PBKDF pseudo-random function. Used to mix a password and a salt.
|
|
# See Crypto\Protocol\KDF.py
|
|
pbkdf2_prf = lambda p, s: HMAC.new(p, s, hash_function).digest()
|
|
|
|
# PBKDF count, number of iterations.
|
|
pbkdf2_count = 1000
|
|
|
|
# PBKDF derived key length.
|
|
pbkdf2_dk_len = 32
|
|
|
|
# ----------------------------------------------------------------------------
|
|
|
|
class HMACIsNotValidException(Exception):
|
|
pass
|
|
|
|
class InvalidFormatException(Exception):
|
|
def __init__(self, reason):
|
|
self.reason = reason
|
|
def __str__(self):
|
|
return repr(self.reason)
|
|
|
|
class CTRCounter:
|
|
""" Callable class that returns an iterating counter for PyCrypto
|
|
AES in CTR mode."""
|
|
|
|
def __init__(self, nonce):
|
|
""" Initialize the counter object.
|
|
|
|
@nonce An 8-byte binary string.
|
|
"""
|
|
assert(len(nonce)==8)
|
|
self.nonce = nonce
|
|
self.cnt = 0
|
|
|
|
def __call__(self):
|
|
""" Return the next 16 byte counter, as a binary string. """
|
|
right_half = struct.pack('>Q', self.cnt)
|
|
self.cnt += 1
|
|
return self.nonce + right_half
|
|
|
|
def encrypt_string(plaintext, key, compress=False):
|
|
plaintext_obj = StringIO.StringIO(plaintext)
|
|
ciphertext_obj = StringIO.StringIO()
|
|
encrypt_file(plaintext_obj, key, ciphertext_obj, compress=compress)
|
|
return ciphertext_obj.getvalue()
|
|
|
|
def decrypt_string(ciphertext, key):
|
|
plaintext_obj = StringIO.StringIO()
|
|
ciphertext_obj = StringIO.StringIO(ciphertext)
|
|
decrypt_file(ciphertext_obj, key, plaintext_obj)
|
|
return plaintext_obj.getvalue()
|
|
|
|
def decrypt_file(ciphertext_file_obj,
|
|
key,
|
|
plaintext_file_obj,
|
|
chunk_size=4096):
|
|
# ------------------------------------------------------------------------
|
|
# Unpack the header values from the ciphertext.
|
|
# ------------------------------------------------------------------------
|
|
header_format = ">HHHHHQ?H"
|
|
header_size = struct.calcsize(header_format)
|
|
header_string = ciphertext_file_obj.read(header_size)
|
|
try:
|
|
header = struct.unpack(header_format, header_string)
|
|
except struct.error:
|
|
raise InvalidFormatException("Header is invalid.")
|
|
pbkdf2_count = header[0]
|
|
pbkdf2_dk_len = header[1]
|
|
password_salt_size = header[2]
|
|
nonce_size = header[3]
|
|
hmac_salt_size = header[4]
|
|
ciphertext_size = header[5]
|
|
compress = header[6]
|
|
hmac_size = header[7]
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Unpack everything except the ciphertext and HMAC, which are the
|
|
# last two strings in the ciphertext file.
|
|
# ------------------------------------------------------------------------
|
|
encrypted_string_format = ''.join([">",
|
|
"%ss" % (password_salt_size, ) ,
|
|
"%ss" % (nonce_size, ),
|
|
"%ss" % (hmac_salt_size, )])
|
|
encrypted_string_size = struct.calcsize(encrypted_string_format)
|
|
body_string = ciphertext_file_obj.read(encrypted_string_size)
|
|
try:
|
|
body = struct.unpack(encrypted_string_format, body_string)
|
|
except struct.error:
|
|
raise InvalidFormatException("Start of body is invalid.")
|
|
password_salt = body[0]
|
|
nonce = body[1]
|
|
hmac_salt = body[2]
|
|
# ------------------------------------------------------------------------
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Prepare the HMAC with everything except the ciphertext.
|
|
#
|
|
# Notice we do not HMAC the ciphertext_size, just like the encrypt
|
|
# stage.
|
|
# ------------------------------------------------------------------------
|
|
hmac_password_derived = PBKDF2(password = key,
|
|
salt = hmac_salt,
|
|
dkLen = pbkdf2_dk_len,
|
|
count = pbkdf2_count,
|
|
prf = pbkdf2_prf)
|
|
elems_to_hmac = [str(pbkdf2_count),
|
|
str(pbkdf2_dk_len),
|
|
str(len(password_salt)),
|
|
password_salt,
|
|
str(len(nonce)),
|
|
nonce,
|
|
str(len(hmac_salt)),
|
|
hmac_salt]
|
|
hmac_object = HMAC.new(key = hmac_password_derived,
|
|
msg = ''.join(elems_to_hmac),
|
|
digestmod = hash_function)
|
|
# ------------------------------------------------------------------------
|
|
|
|
# ------------------------------------------------------------------------
|
|
# First pass: stream in the ciphertext object into the HMAC object
|
|
# and verify that the HMAC is correct.
|
|
#
|
|
# Notice we don't need to decompress anything here even if compression
|
|
# is in use. We're using Encrypt-Then-MAC.
|
|
# ------------------------------------------------------------------------
|
|
ciphertext_file_pos = ciphertext_file_obj.tell()
|
|
ciphertext_bytes_read = 0
|
|
while True:
|
|
bytes_remaining = ciphertext_size - ciphertext_bytes_read
|
|
current_chunk_size = min(bytes_remaining, chunk_size)
|
|
ciphertext_chunk = ciphertext_file_obj.read(current_chunk_size)
|
|
if ciphertext_chunk == '':
|
|
break
|
|
ciphertext_bytes_read += len(ciphertext_chunk)
|
|
hmac_object.update(ciphertext_chunk)
|
|
if ciphertext_bytes_read != ciphertext_size:
|
|
raise InvalidFormatException("first pass ciphertext_bytes_read %s != ciphertext_size %s" % (ciphertext_bytes_read, ciphertext_size))
|
|
|
|
# the rest of the file is the HMAC.
|
|
hmac = ciphertext_file_obj.read()
|
|
if len(hmac) != hmac_size:
|
|
raise InvalidFormatException("len(hmac) %s != hmac_size %s" % (len(hmac), hmac_size))
|
|
|
|
hmac_calculated = hmac_object.digest()
|
|
if hmac != hmac_calculated:
|
|
raise HMACIsNotValidException
|
|
# ------------------------------------------------------------------------
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Second pass: stream in the ciphertext object and decrypt it into the
|
|
# plaintext object.
|
|
# ------------------------------------------------------------------------
|
|
cipher_password_derived = PBKDF2(password = key,
|
|
salt = password_salt,
|
|
dkLen = pbkdf2_dk_len,
|
|
count = pbkdf2_count,
|
|
prf = pbkdf2_prf)
|
|
cipher_ctr = AES.new(key = cipher_password_derived,
|
|
mode = AES.MODE_CTR,
|
|
counter = CTRCounter(nonce))
|
|
ciphertext_file_obj.seek(ciphertext_file_pos, os.SEEK_SET)
|
|
ciphertext_bytes_read = 0
|
|
if compress:
|
|
decompressor = bz2.BZ2Decompressor()
|
|
while True:
|
|
bytes_remaining = ciphertext_size - ciphertext_bytes_read
|
|
current_chunk_size = min(bytes_remaining, chunk_size)
|
|
ciphertext_chunk = ciphertext_file_obj.read(current_chunk_size)
|
|
end_of_file = ciphertext_chunk == ''
|
|
ciphertext_bytes_read += len(ciphertext_chunk)
|
|
plaintext_chunk = cipher_ctr.decrypt(ciphertext_chunk)
|
|
if compress:
|
|
try:
|
|
decompressed = decompressor.decompress(plaintext_chunk)
|
|
except EOFError:
|
|
decompressed = ""
|
|
plaintext_chunk = decompressed
|
|
plaintext_file_obj.write(plaintext_chunk)
|
|
if end_of_file:
|
|
break
|
|
if ciphertext_bytes_read != ciphertext_size:
|
|
raise InvalidFormatException("second pass ciphertext_bytes_read %s != ciphertext_size %s" % (ciphertext_bytes_read, ciphertext_size))
|
|
# ------------------------------------------------------------------------
|
|
|
|
def encrypt_file(plaintext_file_obj,
|
|
key,
|
|
ciphertext_file_obj,
|
|
chunk_size = 4096,
|
|
compress = False):
|
|
# ------------------------------------------------------------------------
|
|
# Prepare input key.
|
|
# ------------------------------------------------------------------------
|
|
password_salt = os.urandom(salt_length_in_bytes)
|
|
cipher_password_derived = PBKDF2(password = key,
|
|
salt = password_salt,
|
|
dkLen = pbkdf2_dk_len,
|
|
count = pbkdf2_count,
|
|
prf = pbkdf2_prf)
|
|
# ------------------------------------------------------------------------
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Prepare cipher object.
|
|
# ------------------------------------------------------------------------
|
|
nonce_size = 8
|
|
nonce = os.urandom(nonce_size)
|
|
cipher_ctr = AES.new(key = cipher_password_derived,
|
|
mode = AES.MODE_CTR,
|
|
counter = CTRCounter(nonce))
|
|
# ------------------------------------------------------------------------
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Prepare HMAC object, and hash what we have so far.
|
|
#
|
|
# Notice that we do not HMAC the size of the ciphertext. We don't
|
|
# know how big it'll be until we compress it, if we do, and we can't
|
|
# compress it without reading it into memory. So let the HMAC of
|
|
# the ciphertext itself do.
|
|
# ------------------------------------------------------------------------
|
|
hmac_salt = os.urandom(salt_length_in_bytes)
|
|
hmac_password_derived = PBKDF2(password = key,
|
|
salt = hmac_salt,
|
|
dkLen = pbkdf2_dk_len,
|
|
count = pbkdf2_count,
|
|
prf = pbkdf2_prf)
|
|
elems_to_hmac = [str(pbkdf2_count),
|
|
str(pbkdf2_dk_len),
|
|
str(len(password_salt)),
|
|
password_salt,
|
|
str(len(nonce)),
|
|
nonce,
|
|
str(len(hmac_salt)),
|
|
hmac_salt]
|
|
hmac_object = HMAC.new(key = hmac_password_derived,
|
|
msg = ''.join(elems_to_hmac),
|
|
digestmod = hash_function)
|
|
# ------------------------------------------------------------------------
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Write in what we have so far into the output, ciphertext file.
|
|
#
|
|
# Given that the plaintext may be compressed we don't know what
|
|
# it's final length will be without compressing it, and we can't
|
|
# do this without reading it all into memory. Hence let's
|
|
# put 0 as the ciphertext length for now and fill it in after.
|
|
# ------------------------------------------------------------------------
|
|
header_format = ''.join([">",
|
|
"H", # PBKDF2 count
|
|
"H", # PBKDF2 derived key length
|
|
"H", # Length of password salt
|
|
"H", # Length of CTR nonce
|
|
"H", # Length of HMAC salt
|
|
"Q", # Length of ciphertext
|
|
"?", # Is compression used?
|
|
"H", # Length of HMAC
|
|
"%ss" % (len(password_salt), ) , # Password salt
|
|
"%ss" % (nonce_size, ), # CTR nonce
|
|
"%ss" % (len(hmac_salt), )]) # HMAC salt
|
|
header = struct.pack(header_format,
|
|
pbkdf2_count,
|
|
pbkdf2_dk_len,
|
|
len(password_salt),
|
|
len(nonce),
|
|
len(hmac_salt),
|
|
0, # This is the ciphertext size, wrong for now.
|
|
compress,
|
|
hmac_object.digest_size,
|
|
password_salt,
|
|
nonce,
|
|
hmac_salt)
|
|
ciphertext_file_obj.write(header)
|
|
# ------------------------------------------------------------------------
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Stream in the input file and stream out ciphertext into the
|
|
# ciphertext file.
|
|
# ------------------------------------------------------------------------
|
|
ciphertext_size = 0
|
|
if compress:
|
|
compressor = bz2.BZ2Compressor()
|
|
while True:
|
|
plaintext_chunk = plaintext_file_obj.read(chunk_size)
|
|
end_of_file = plaintext_chunk == ''
|
|
if compress:
|
|
if end_of_file:
|
|
compressed = compressor.flush()
|
|
else:
|
|
compressed = compressor.compress(plaintext_chunk)
|
|
plaintext_chunk = compressed
|
|
ciphertext_chunk = cipher_ctr.encrypt(plaintext_chunk)
|
|
ciphertext_size += len(ciphertext_chunk)
|
|
ciphertext_file_obj.write(ciphertext_chunk)
|
|
hmac_object.update(ciphertext_chunk)
|
|
if end_of_file:
|
|
break
|
|
# ------------------------------------------------------------------------
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Write the HMAC to the ciphertext file.
|
|
# ------------------------------------------------------------------------
|
|
hmac = hmac_object.digest()
|
|
ciphertext_file_obj.write(hmac)
|
|
# ------------------------------------------------------------------------
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Go back to the header and update the ciphertext size.
|
|
#
|
|
# Notice that we capture the header such that the last unpacked
|
|
# element of the struct is the unsigned long long of the ciphertext
|
|
# length.
|
|
# ------------------------------------------------------------------------
|
|
|
|
# Read in.
|
|
ciphertext_file_obj.seek(0, os.SEEK_SET)
|
|
header_format = ">HHHHHQ"
|
|
header_size = struct.calcsize(header_format)
|
|
header = ciphertext_file_obj.read(header_size)
|
|
|
|
# Modify.
|
|
header_elems = list(struct.unpack(header_format, header))
|
|
header_elems[-1] = ciphertext_size
|
|
|
|
# Write out.
|
|
header = struct.pack(header_format, *header_elems)
|
|
ciphertext_file_obj.seek(0, os.SEEK_SET)
|
|
ciphertext_file_obj.write(header)
|
|
# ------------------------------------------------------------------------ |