Skip to content

Commit

Permalink
Switched to PyCA Cryptography
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Bellaccini committed Aug 26, 2017
1 parent 377f43b commit a70e2b3
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 57 deletions.
11 changes: 5 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
language: python
python:
- "3.3"
- "3.4"
- "3.5"
- "3.6"
- "3.7-dev" # 3.7 development branch
- "nightly" # currently points to 3.7-dev
# command to install program dependencies (pycrypto) and compatibility test dependencies (AES Crypt)
# command to install program dependencies (PyCA cryptography) and compatibility test dependencies (AES Crypt)
install:
- pip install pycrypto
- wget https://www.aescrypt.com/download/v3/linux/aescrypt-3.11.tgz
- tar -xzf aescrypt-3.11.tgz
- pushd aescrypt-3.11/src && make && sudo make install && popd
- pip install cryptography
- wget https://www.aescrypt.com/download/v3/linux/aescrypt-3.13.tgz
- tar -xzf aescrypt-3.13.tgz
- pushd aescrypt-3.13/src && make && sudo make install && popd

# command to run tests
script: python -m unittest discover
Expand Down
8 changes: 8 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
History
===============

0.3 (Aug 2017)
~~~~~~~~~~~~~~~~~~
* Switched from `pycrypto`_ to `PyCA Cryptography`_ for crypto primitives
* Unittests clean-up

0.2.2 (Aug 2017)
~~~~~~~~~~~~~~~~~~
* Option to pass password as command-line argument to the script
Expand All @@ -27,3 +32,6 @@ History
0.1 (Jan 2016)
~~~~~~~~~~~~~~~~~~
* First public release

.. _pycrypto: https://github.com/dlitz/pycrypto
.. _PyCA Cryptography: https://github.com/pyca/cryptography
107 changes: 61 additions & 46 deletions pyAesCrypt/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
# encrypt/decrypt files.
# pyAesCrypt is compatible with the AES Crypt (https://www.aescrypt.com/)
# file format (version 2).
# It uses PyCA Cryptography for crypto primitives and the operating system's
# random number generator (/dev/urandom on UNIX platforms, CryptGenRandom
# on Windows).
#
# IMPORTANT SECURITY NOTE: version 2 of the AES Crypt file format does not
# authenticate the "file size modulo 16" byte. This implies that an attacker
Expand All @@ -33,22 +36,24 @@

# pyAesCrypt module

from Crypto.Hash import SHA256
from Crypto.Hash import HMAC
from Crypto.Cipher import AES
from Crypto import Random
from os import stat
from os import remove
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, hmac
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from os import urandom
from os import stat, remove

# pyAesCrypt version
version = "0.2.1"
version = "0.3"

# encryption/decryption buffer size - 64K
bufferSize = 64 * 1024

# maximum password length (number of chars)
maxPassLen = 1024

# AES block size in bytes
AESBlockSize = 16


# password stretching function
def stretch(passw, iv1):
Expand All @@ -57,10 +62,10 @@ def stretch(passw, iv1):
digest = iv1 + (16 * b"\x00")

for i in range(8192):
passHash = SHA256.new()
passHash = hashes.Hash(hashes.SHA256(), backend=default_backend())
passHash.update(digest)
passHash.update(bytes(passw, "utf_16_le"))
digest = passHash.digest()
digest = passHash.finalize()

return digest

Expand All @@ -76,47 +81,50 @@ def stretch(passw, iv1):
# with big files
def encryptFile(infile, outfile, passw, bufferSize):
# validate bufferSize
if bufferSize % AES.block_size != 0:
if bufferSize % AESBlockSize != 0:
raise ValueError("Buffer size must be a multiple of AES block size.")

if len(passw) > maxPassLen:
raise ValueError("Password is too long.")

# get input file size
inputFileSize = stat(infile).st_size

try:
with open(infile, "rb") as fIn:
# initialize random number generator
# using pycrypto cryptographic PRNG (based on "Fortuna" by
# N. Ferguson and B. Schneier, with the OS RNG, time.clock()
# and time.time() as entropy sources)
rng = Random.new()

# generate external iv (used to encrypt the main iv and the
# encryption key)
iv1 = rng.read(AES.block_size)
iv1 = urandom(AESBlockSize)

# stretch password and iv
key = stretch(passw, iv1)

# generate random main iv
iv0 = rng.read(AES.block_size)
iv0 = urandom(AESBlockSize)

# generate random internal key
intKey = rng.read(32)
intKey = urandom(32)

# instantiate AES cipher
cipher0 = AES.new(intKey, AES.MODE_CBC, iv0)
cipher0 = Cipher(algorithms.AES(intKey), modes.CBC(iv0),
backend=default_backend())
encryptor0 = cipher0.encryptor()

# instantiate HMAC-SHA256 for the ciphertext
hmac0 = HMAC.new(intKey, digestmod=SHA256)
hmac0 = hmac.HMAC(intKey, hashes.SHA256(),
backend=default_backend())

# instantiate another AES cipher
cipher1 = AES.new(key, AES.MODE_CBC, iv1)
cipher1 = Cipher(algorithms.AES(key), modes.CBC(iv1),
backend=default_backend())
encryptor1 = cipher1.encryptor()

# encrypt main iv and key
c_iv_key = cipher1.encrypt(iv0 + intKey)
c_iv_key = encryptor1.update(iv0 + intKey) + encryptor1.finalize()

# calculate HMAC-SHA256 of the encrypted iv and key
hmac1 = HMAC.new(key, digestmod=SHA256)
hmac1 = hmac.HMAC(key, hashes.SHA256(),
backend=default_backend())
hmac1.update(c_iv_key)

try:
Expand Down Expand Up @@ -159,7 +167,7 @@ def encryptFile(infile, outfile, passw, bufferSize):
fOut.write(c_iv_key)

# write HMAC-SHA256 of the encrypted iv and key
fOut.write(hmac1.digest())
fOut.write(hmac1.finalize())

# encrypt file while reading it
while True:
Expand All @@ -172,17 +180,18 @@ def encryptFile(infile, outfile, passw, bufferSize):
# check if EOF was reached
if bytesRead < bufferSize:
# file size mod 16, lsb positions
fs16 = bytes([bytesRead % AES.block_size])
fs16 = bytes([bytesRead % AESBlockSize])
# pad data (this is NOT PKCS#7!)
# ...unless no bytes or a multiple of a block size
# of bytes was read
if bytesRead % AES.block_size == 0:
if bytesRead % AESBlockSize == 0:
padLen = 0
else:
padLen = 16 - bytesRead % AES.block_size
padLen = 16 - bytesRead % AESBlockSize
fdata += bytes([padLen])*padLen
# encrypt data
cText = cipher0.encrypt(fdata)
cText = encryptor0.update(fdata) \
+ encryptor0.finalize()
# update HMAC
hmac0.update(cText)
# write encrypted file content
Expand All @@ -192,7 +201,7 @@ def encryptFile(infile, outfile, passw, bufferSize):
# ...otherwise a full bufferSize was read
else:
# encrypt data
cText = cipher0.encrypt(fdata)
cText = encryptor0.update(fdata)
# update HMAC
hmac0.update(cText)
# write encrypted file content
Expand All @@ -202,7 +211,7 @@ def encryptFile(infile, outfile, passw, bufferSize):
fOut.write(fs16)

# write HMAC-SHA256 of the encrypted file
fOut.write(hmac0.digest())
fOut.write(hmac0.finalize())

except IOError:
raise IOError("Unable to write output file.")
Expand All @@ -221,7 +230,7 @@ def encryptFile(infile, outfile, passw, bufferSize):
# big files
def decryptFile(infile, outfile, passw, bufferSize):
# validate bufferSize
if bufferSize % AES.block_size != 0:
if bufferSize % AESBlockSize != 0:
raise ValueError("Buffer size must be a multiple of AES block size")

if len(passw) > maxPassLen:
Expand Down Expand Up @@ -279,28 +288,34 @@ def decryptFile(infile, outfile, passw, bufferSize):
raise ValueError("File is corrupted.")

# compute actual HMAC-SHA256 of the encrypted iv and key
hmac1Act = HMAC.new(key, digestmod=SHA256)
hmac1Act = hmac.HMAC(key, hashes.SHA256(),
backend=default_backend())
hmac1Act.update(c_iv_key)

# HMAC check
if hmac1 != hmac1Act.digest():
if hmac1 != hmac1Act.finalize():
raise ValueError("Wrong password (or file is corrupted).")

# instantiate AES cipher
cipher1 = AES.new(key, AES.MODE_CBC, iv1)
cipher1 = Cipher(algorithms.AES(key), modes.CBC(iv1),
backend=default_backend())
decryptor1 = cipher1.decryptor()

# decrypt main iv and key
iv_key = cipher1.decrypt(c_iv_key)
iv_key = decryptor1.update(c_iv_key) + decryptor1.finalize()

# get internal iv and key
iv0 = iv_key[:16]
intKey = iv_key[16:]

# instantiate another AES cipher
cipher0 = AES.new(intKey, AES.MODE_CBC, iv0)
cipher0 = Cipher(algorithms.AES(intKey), modes.CBC(iv0),
backend=default_backend())
decryptor0 = cipher0.decryptor()

# instantiate actual HMAC-SHA256 of the ciphertext
hmac0Act = HMAC.new(intKey, digestmod=SHA256)
hmac0Act = hmac.HMAC(intKey, hashes.SHA256(),
backend=default_backend())

try:
with open(outfile, "wb") as fOut:
Expand All @@ -310,24 +325,24 @@ def decryptFile(infile, outfile, passw, bufferSize):
# update HMAC
hmac0Act.update(cText)
# decrypt data and write it to output file
fOut.write(cipher0.decrypt(cText))
fOut.write(decryptor0.update(cText))

# decrypt remaining ciphertext, until last block is reached
while fIn.tell() < inputFileSize - 32 - 1 - AES.block_size:
while fIn.tell() < inputFileSize - 32 - 1 - AESBlockSize:
# read data
cText = fIn.read(AES.block_size)
cText = fIn.read(AESBlockSize)
# update HMAC
hmac0Act.update(cText)
# decrypt data and write it to output file
fOut.write(cipher0.decrypt(cText))
fOut.write(decryptor0.update(cText))

# last block reached, remove padding if needed
# read last block

# this is for empty files
if fIn.tell() != inputFileSize - 32 - 1:
cText = fIn.read(AES.block_size)
if len(cText) < AES.block_size:
cText = fIn.read(AESBlockSize)
if len(cText) < AESBlockSize:
# remove outfile and raise exception
remove(outfile)
raise ValueError("File is corrupted.")
Expand All @@ -345,7 +360,7 @@ def decryptFile(infile, outfile, passw, bufferSize):
raise ValueError("File is corrupted.")

# decrypt last block
pText = cipher0.decrypt(cText)
pText = decryptor0.update(cText) + decryptor0.finalize()

# remove padding
toremove = ((16 - fs16[0]) % 16)
Expand All @@ -363,7 +378,7 @@ def decryptFile(infile, outfile, passw, bufferSize):
raise ValueError("File is corrupted.")

# HMAC check
if hmac0 != hmac0Act.digest():
if hmac0 != hmac0Act.finalize():
# remove outfile and raise exception
remove(outfile)
raise ValueError("Bad HMAC (file is corrupted).")
Expand Down
5 changes: 2 additions & 3 deletions pyAesCrypt/test_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import shutil
import filecmp
import subprocess
from os import stat
from os.path import isfile
import pyAesCrypt

Expand Down Expand Up @@ -240,7 +239,7 @@ def test_dec_bad_hmac(self):
bufferSize)

# get file size
fsize = stat(self.tfile+'.aes').st_size
fsize = os.stat(self.tfile+'.aes').st_size

# corrupt hmac
corruptFile(self.tfile+'.aes', fsize-1)
Expand All @@ -262,7 +261,7 @@ def test_dec_trunc_file(self):
bufferSize)

# get file size
fsize = stat(self.tfile+'.aes').st_size
fsize = os.stat(self.tfile+'.aes').st_size

# truncate hmac (i.e.: truncate end of the file)
with open(self.tfile+'.aes', 'r+b') as ftc:
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
README = readme.read()

setup(name='pyAesCrypt',
version='0.2.2',
version='0.3',
packages = find_packages(),
include_package_data=True,
description='Encrypt and decrypt files in AES Crypt format (version 2)',
Expand All @@ -14,7 +14,7 @@
url='https://github.com/marcobellaccini/pyAesCrypt',
license='Apache License 2.0',
scripts=['bin/pyAesCrypt'],
install_requires=['pycrypto'],
install_requires=['cryptography'],
keywords = "AES Crypt encrypt decrypt",
classifiers=[
'License :: OSI Approved :: Apache Software License',
Expand Down

0 comments on commit a70e2b3

Please sign in to comment.