regluit/utils/encryption.py

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)
# ------------------------------------------------------------------------