Skip to content

Commit

Permalink
Replace PyCrypto with tinyaes for encryption support
Browse files Browse the repository at this point in the history
[tinyaes](https://github.com/naufraghi/tinyaes-py) is a minimal AES-only
library that wraps the C library [tiny-AES-c](https://github.com/kokke/tiny-AES-c).

Currently the library wraps only the CTR mode that can be used in
PyInstaller instead of the CFB mode used before.

Reading various sources, CFB is [somewhat similar to CBC](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Feedback_(CFB)))
and CTR is suggested as a go-to stream cipher (https://crypto.stackexchange.com/questions/6029/aes-cbc-mode-or-aes-ctr-mode-recommended and links)
that can be used instead of CBC.

See issue #2365

PR iterations:

- use tinyaes from pypi (installing from git triggers a key confirmation)
- make lint job happy
  • Loading branch information
naufraghi committed Feb 7, 2020
1 parent d08a42c commit ce127b9
Show file tree
Hide file tree
Showing 6 changed files with 30 additions and 78 deletions.
39 changes: 6 additions & 33 deletions PyInstaller/archive/pyz_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,6 @@
BLOCK_SIZE = 16


def import_aes(module_name):
"""
Tries to import the AES module from PyCrypto.
PyCrypto 2.4 and 2.6 uses different name of the AES extension.
"""
return __import__(module_name, fromlist=[module_name.split('.')[-1]])


def get_crypto_hiddenimports():
"""
These module names are appended to the PyInstaller analysis phase.
:return: Name of the AES module.
"""
try:
# The _AES.so module exists only in PyCrypto 2.6 and later. Try to import
# that first.
modname = 'Crypto.Cipher._AES'
import_aes(modname)
except ImportError:
# Fallback to AES.so, which should be there in PyCrypto 2.4 and earlier.
modname = 'Crypto.Cipher.AES'
import_aes(modname)
return modname


class PyiBlockCipher(object):
"""
This class is used only to encrypt Python modules.
Expand All @@ -52,15 +26,14 @@ def __init__(self, key=None):
self.key = key.zfill(BLOCK_SIZE)
assert len(self.key) == BLOCK_SIZE

# Import the right AES module.
self._aesmod = import_aes(get_crypto_hiddenimports())
import tinyaes
self._aesmod = tinyaes

def encrypt(self, data):
iv = os.urandom(BLOCK_SIZE)
return iv + self.__create_cipher(iv).encrypt(data)
return iv + self.__create_cipher(iv).CTR_xcrypt_buffer(data)

def __create_cipher(self, iv):
# The 'BlockAlgo' class is stateful, this factory method is used to
# re-initialize the block cipher class with each call to encrypt() and
# decrypt().
return self._aesmod.new(self.key.encode(), self._aesmod.MODE_CFB, iv)
# The 'AES' class is stateful, this factory method is used to
# re-initialize the block cipher class with each call to xcrypt().
return self._aesmod.AES(self.key.encode(), iv)
6 changes: 4 additions & 2 deletions PyInstaller/building/build_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ def __init__(self, scripts, pathex=None, binaries=None, datas=None,
runtime_hooks
An optional list of scripts to use as users' runtime hooks. Specified
as file names.
cipher
Add optional instance of the pyz_crypto.PyiBlockCipher class
(with a provided key).
win_no_prefer_redirects
If True, prefers not to follow version redirects when searching for
Windows SxS Assemblies.
Expand Down Expand Up @@ -221,8 +224,7 @@ def __init__(self, scripts, pathex=None, binaries=None, datas=None,
with open_file(pyi_crypto_key_path, 'w', encoding='utf-8') as f:
f.write('# -*- coding: utf-8 -*-\n'
'key = %r\n' % cipher.key)
logger.info('Adding dependencies on pyi_crypto.py module')
self.hiddenimports.append(pyz_crypto.get_crypto_hiddenimports())
self.hiddenimports.append('tinyaes')

self.excludes = excludes or []
self.scripts = TOC()
Expand Down
14 changes: 5 additions & 9 deletions PyInstaller/building/makespec.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,18 +393,14 @@ def main(scripts, name=None, onefile=None,
scripts = list(map(Path, scripts))

if key:
# Tries to import PyCrypto since we need it for bytecode obfuscation. Also make sure its
# version is >= 2.4.
# Tries to import tinyaes since we need it for bytecode obfuscation.
try:
import Crypto
is_version_acceptable = LooseVersion(Crypto.__version__) >= LooseVersion('2.4')
if not is_version_acceptable:
logger.error('PyCrypto version must be >= 2.4, older versions are not supported.')
sys.exit(1)
import tinyaes # noqa: F401 (test import)
except ImportError:
logger.error('We need PyCrypto >= 2.4 to use byte-code obfuscation but we could not')
logger.error('We need tinyaes to use byte-code obfuscation but we '
'could not')
logger.error('find it. You can install it with pip by running:')
logger.error(' pip install PyCrypto')
logger.error(' pip install tinyaes')
sys.exit(1)
cipher_init = cipher_init_template % {'key': key}
else:
Expand Down
38 changes: 9 additions & 29 deletions PyInstaller/loader/pyimod02_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,40 +249,20 @@ def __init__(self):
self.key = key.zfill(CRYPT_BLOCK_SIZE)
assert len(self.key) == CRYPT_BLOCK_SIZE

# Import the right AES module.
self._aes = self._import_aesmod()

def _import_aesmod(self):
"""
Tries to import the AES module from PyCrypto.
PyCrypto 2.4 and 2.6 uses different name of the AES extension.
"""
# The _AES.so module exists only in PyCrypto 2.6 and later. Try to import
# that first.
modname = 'Crypto.Cipher._AES'

kwargs = dict(fromlist=['Crypto', 'Cipher'])
try:
mod = __import__(modname, **kwargs)
except ImportError:
modname = 'Crypto.Cipher.AES'
mod = __import__(modname, **kwargs)

import tinyaes
self._aesmod = tinyaes
# Issue #1663: Remove the AES module from sys.modules list. Otherwise
# it interferes with using 'Crypto.Cipher' module in users' code.
if modname in sys.modules:
del sys.modules[modname]
return mod
# it interferes with using 'tinyaes' module in users' code.
del sys.modules['tinyaes']

def __create_cipher(self, iv):
# The 'BlockAlgo' class is stateful, this factory method is used to
# re-initialize the block cipher class with each call to encrypt() and
# decrypt().
return self._aes.new(self.key, self._aes.MODE_CFB, iv)
# The 'AES' class is stateful, this factory method is used to
# re-initialize the block cipher class with each call to xcrypt().
return self._aesmod.AES(self.key.encode(), iv)

def decrypt(self, data):
return self.__create_cipher(data[:CRYPT_BLOCK_SIZE]).decrypt(data[CRYPT_BLOCK_SIZE:])
cipher = self.__create_cipher(data[:CRYPT_BLOCK_SIZE])
return cipher.CTR_xcrypt_buffer(data[CRYPT_BLOCK_SIZE:])


class ZlibArchiveReader(ArchiveReader):
Expand Down
8 changes: 3 additions & 5 deletions tests/functional/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,15 @@ def test_email(pyi_builder):
""")


@skipif(is_module_satisfies('Crypto >= 3'), reason='Bytecode encryption is not '
'compatible with pycryptodome.')
@importorskip('Crypto')
@importorskip('tinyaes')
def test_feature_crypto(pyi_builder):
pyi_builder.test_source(
"""
from pyimod00_crypto_key import key
from pyimod02_archive import CRYPT_BLOCK_SIZE
# Issue 1663: Crypto feature caused issues when using PyCrypto module.
import Crypto.Cipher.AES
# An issue similar to the 1663 with PyCrypto may arise for tinyaes
import tinyaes
assert type(key) is str
# The test runner uses 'test_key' as key.
Expand Down
3 changes: 3 additions & 0 deletions tests/requirements-tools.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ flake8-diff
pywin32; sys_platform == 'win32'

lxml

# crypto support (`--key` option)
tinyaes

0 comments on commit ce127b9

Please sign in to comment.