From ce127b9b3dffaefd0b71cb5693e5c83465482df7 Mon Sep 17 00:00:00 2001 From: Matteo Bertini Date: Wed, 29 Jan 2020 09:55:38 +0100 Subject: [PATCH] Replace PyCrypto with tinyaes for encryption support [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 --- PyInstaller/archive/pyz_crypto.py | 39 ++++---------------------- PyInstaller/building/build_main.py | 6 ++-- PyInstaller/building/makespec.py | 14 ++++----- PyInstaller/loader/pyimod02_archive.py | 38 ++++++------------------- tests/functional/test_basic.py | 8 ++---- tests/requirements-tools.txt | 3 ++ 6 files changed, 30 insertions(+), 78 deletions(-) diff --git a/PyInstaller/archive/pyz_crypto.py b/PyInstaller/archive/pyz_crypto.py index 47d2f1caec5..60092a7cfc6 100644 --- a/PyInstaller/archive/pyz_crypto.py +++ b/PyInstaller/archive/pyz_crypto.py @@ -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. @@ -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) diff --git a/PyInstaller/building/build_main.py b/PyInstaller/building/build_main.py index 3442e632c56..1b827bbda47 100644 --- a/PyInstaller/building/build_main.py +++ b/PyInstaller/building/build_main.py @@ -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. @@ -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() diff --git a/PyInstaller/building/makespec.py b/PyInstaller/building/makespec.py index 0ddc06513ed..0feafab1439 100644 --- a/PyInstaller/building/makespec.py +++ b/PyInstaller/building/makespec.py @@ -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: diff --git a/PyInstaller/loader/pyimod02_archive.py b/PyInstaller/loader/pyimod02_archive.py index c61a50ad628..8aabdfbfd25 100644 --- a/PyInstaller/loader/pyimod02_archive.py +++ b/PyInstaller/loader/pyimod02_archive.py @@ -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): diff --git a/tests/functional/test_basic.py b/tests/functional/test_basic.py index 24ec77b737d..358a4bcd65b 100644 --- a/tests/functional/test_basic.py +++ b/tests/functional/test_basic.py @@ -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. diff --git a/tests/requirements-tools.txt b/tests/requirements-tools.txt index 19b10bd6f32..adcf7ec0b08 100644 --- a/tests/requirements-tools.txt +++ b/tests/requirements-tools.txt @@ -36,3 +36,6 @@ flake8-diff pywin32; sys_platform == 'win32' lxml + +# crypto support (`--key` option) +tinyaes