Skip to content

Commit

Permalink
building/loader: replace PyCrypto with tinyaes for encryption support. (
Browse files Browse the repository at this point in the history
#4652)

This re-enables encrypting the Python bytecode (`--key`) by replacing PyCrypto with tinyaes.

tinyaes is a minimal AES-only library that wraps the C library tiny-AES-c. See
https://github.com/naufraghi/tinyaes-py for source code. 

Closes #2365.
  • Loading branch information
naufraghi committed May 31, 2020
1 parent 7eb1c66 commit d914a23
Show file tree
Hide file tree
Showing 18 changed files with 53 additions and 84 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 @@ -150,6 +150,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 @@ -216,8 +219,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
15 changes: 5 additions & 10 deletions PyInstaller/building/makespec.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import os
import sys
import argparse
from distutils.version import LooseVersion

from .. import HOMEPATH, DEFAULT_SPECPATH
from .. import log as logging
Expand Down Expand Up @@ -405,18 +404,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
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Requirements and Tested Platforms
- Python:

- 3.5-3.7
- PyCrypto_ 2.4+ (only if using bytecode encryption)
- tinyaes_ 1.0+ (only if using bytecode encryption)

- Windows (32bit/64bit):

Expand Down Expand Up @@ -159,6 +159,6 @@ http://www.pyinstaller.org/funding.html for how to support PyInstaller.



.. _PyCrypto: https://www.dlitz.net/software/pycrypto/
.. _tinyaes: https://github.com/naufraghi/tinyaes-py
.. _`manual`: https://pyinstaller.readthedocs.io/en/latest/

2 changes: 1 addition & 1 deletion doc/_common_definitions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
.. _pip-Win: https://sites.google.com/site/pydatalog/python/pip-for-windows
.. _plistlib: https://docs.python.org/3/library/plistlib.html
.. _png2icns: http://icns.sourceforge.net/
.. _PyCrypto: https://pypi.python.org/pypi/pycrypto/
.. _tinyaes: https://github.com/naufraghi/tinyaes-py
.. _PyInstaller.org: https://github.com/pyinstaller/pyinstaller/wiki/Community
.. _`PyInstaller at GitHub`: https://github.com/pyinstaller/pyinstaller
.. _`PyInstaller code signing recipe`: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-OSX-Code-Signing
Expand Down
2 changes: 1 addition & 1 deletion doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ To encrypt the Python bytecode modules stored in the bundle,
pass the ``--key=``\ *key-string* argument on
the command line.

For this to work, you must have the PyCrypto_
For this to work, you must have the tinyaes_
module installed.
The *key-string* is a string of 16 characters which is used to
encrypt each file of Python byte-code before it is stored in
Expand Down
2 changes: 2 additions & 0 deletions news/2365.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the base library to support encrypting Python bytecode (``--key``
option) again. Many thanks to Matteo Bertini for finally fixing this.
2 changes: 2 additions & 0 deletions news/3093.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the base library to support encrypting Python bytecode (``--key``
option) again. Many thanks to Matteo Bertini for finally fixing this.
2 changes: 2 additions & 0 deletions news/3133.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the base library to support encrypting Python bytecode (``--key``
option) again. Many thanks to Matteo Bertini for finally fixing this.
2 changes: 2 additions & 0 deletions news/3160.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the base library to support encrypting Python bytecode (``--key``
option) again. Many thanks to Matteo Bertini for finally fixing this.
2 changes: 2 additions & 0 deletions news/3198.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the base library to support encrypting Python bytecode (``--key``
option) again. Many thanks to Matteo Bertini for finally fixing this.
2 changes: 2 additions & 0 deletions news/3316.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the base library to support encrypting Python bytecode (``--key``
option) again. Many thanks to Matteo Bertini for finally fixing this.
2 changes: 2 additions & 0 deletions news/3619.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the base library to support encrypting Python bytecode (``--key``
option) again. Many thanks to Matteo Bertini for finally fixing this.
2 changes: 2 additions & 0 deletions news/4241.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the base library to support encrypting Python bytecode (``--key``
option) again. Many thanks to Matteo Bertini for finally fixing this.
2 changes: 2 additions & 0 deletions news/4652.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update the base library to support encrypting Python bytecode (``--key``
option) again. Many thanks to Matteo Bertini for finally fixing this.
10 changes: 4 additions & 6 deletions tests/functional/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from PyInstaller.utils.tests import importorskip, skipif, skipif_win, \
skipif_winorosx, skipif_notwin, skipif_notosx, skipif_no_compiler, \
skipif_notlinux, xfail
from PyInstaller.utils.hooks import is_module_satisfies


def test_run_from_path_environ(pyi_builder):
Expand Down Expand Up @@ -162,17 +161,16 @@ 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
# Test against issue #1663: importing a package in the bootstrap
# phase should not interfere with subsequent imports.
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 ~= 1.0

0 comments on commit d914a23

Please sign in to comment.