Skip to content

Commit

Permalink
Remove the --key/cipher bytecode encryption.
Browse files Browse the repository at this point in the history
Bytecode encryption, given that the decryption key has to be stored somewhere in
the built application for the application to be able to function, was only ever
a mild deterrent against prying eyes. It could be cracked by anyone willing to
dig around PyInstaller's source code for the exact layout of the executable
archive and a quick hexdump to get the key once you know where to look.

These days however, PyInstaller reverse engineering tools like PyExtractor have
this all built in. For example, in the steps below, our would be prying user
doesn't even need to know that the application they are trying to break open is
encrypted, let alone have to do anything clever to decrypt it.

    git clone https://github.com/Rdimo/PyExtractor.git
    cd PyExtractor
    pip install -r requirements.txt
    python main.py some/pyinstaller/application

So since the knowledge barrier to reverse engineer an encrypted build is now
identical to that of a regular one, and because users are being misled into
thinking that an encrypted PyInstaller build is a safe place to put things like
API keys, and since adding further code obfuscation will eventually lead to the
same outcome, remove the encryption feature entirely.

Users looking for a replacement should look for code obfuscation methods that
don't require lossless de-obfuscation at runtime in order for the code to be
runable. This means PyArmour or any DIY bytecode encryption scheme should be
avoided for the same reasons that this feature is being dropped. Instead, you
can use pyminifier's obfuscation feature which mangles variable names or if (and
only if) you understand the perils of Linux ABI compatibility, are aware of the
macOS deployment target and understand that PyInstaller can't detect imports
made by C extensions (i.e. you will need to use
--hidden-import/--collect-submodules a lot) then you may consider running Cython
on the more confidential Python files in your project.
  • Loading branch information
bwoodsend committed Jul 30, 2022
1 parent ba38ba4 commit ac32b58
Show file tree
Hide file tree
Showing 15 changed files with 36 additions and 232 deletions.
39 changes: 0 additions & 39 deletions PyInstaller/archive/pyz_crypto.py

This file was deleted.

14 changes: 1 addition & 13 deletions PyInstaller/archive/writers.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,12 @@ class ZlibArchiveWriter(ArchiveWriter):
HDRLEN = ArchiveWriter.HDRLEN + 5
COMPRESSION_LEVEL = 6 # Default level of the 'zlib' module from Python.

def __init__(self, archive_path, logical_toc, code_dict=None, cipher=None):
def __init__(self, archive_path, logical_toc, code_dict=None):
"""
code_dict dict containing module code objects from ModuleGraph.
"""
# Keep references to module code objects constructed by ModuleGraph to avoid writing .pyc/pyo files to hdd.
self.code_dict = code_dict or {}
self.cipher = cipher or None

super().__init__(archive_path, logical_toc)

Expand All @@ -191,20 +190,9 @@ def add(self, entry):

obj = zlib.compress(data, self.COMPRESSION_LEVEL)

# First compress then encrypt.
if self.cipher:
obj = self.cipher.encrypt(obj)

self.toc.append((name, (typ, self.lib.tell(), len(obj))))
self.lib.write(obj)

def update_headers(self, tocpos):
"""
Add level.
"""
ArchiveWriter.update_headers(self, tocpos)
self.lib.write(struct.pack('!B', self.cipher is not None))


class CTOC:
"""
Expand Down
18 changes: 7 additions & 11 deletions PyInstaller/building/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,17 @@ def __init__(self, *tocs, **kwargs):
name
A filename for the .pyz. Normally not needed, as the generated name will do fine.
cipher
The block cipher that will be used to encrypt Python bytecode.
"""
if kwargs.get("cipher"):
raise SystemExit(
"The 'cipher' option (i.e. bytecode encryption feature) has been removed. Please "
"remove the 'cipher' arguments to PYZ() and Analysis() in your spec file. "
"For the rational and alternatives see https://github.com/pyinstaller/pyinstaller/pull/6999"
)

from PyInstaller.config import CONF
Target.__init__(self)
name = kwargs.get('name', None)
cipher = kwargs.get('cipher', None)
self.toc = TOC()
# If available, use code objects directly from ModuleGraph to speed up PyInstaller.
self.code_dict = {}
Expand All @@ -84,13 +87,6 @@ def __init__(self, *tocs, **kwargs):
self.name = os.path.splitext(self.tocfilename)[0] + '.pyz'
# PyInstaller bootstrapping modules.
bootstrap_dependencies = get_bootstrap_modules()
# Bundle the crypto key.
self.cipher = cipher
if cipher:
key_file = ('pyimod00_crypto_key', os.path.join(CONF['workpath'], 'pyimod00_crypto_key.py'), 'PYMODULE')
# Insert the key as the first module in the list. The key module contains just variables and does not depend
# on other modules.
bootstrap_dependencies.insert(0, key_file)

# Compile the python modules that are part of bootstrap dependencies, so that they can be collected into the
# CArchive and imported by the bootstrap script.
Expand Down Expand Up @@ -136,7 +132,7 @@ def assemble(self):
# Remove leading parts of paths in code objects.
self.code_dict = {key: strip_paths_in_code(code) for key, code in self.code_dict.items()}

ZlibArchiveWriter(self.name, toc, code_dict=self.code_dict, cipher=self.cipher)
ZlibArchiveWriter(self.name, toc, code_dict=self.code_dict)
logger.info("Building PYZ (ZlibArchive) %s completed successfully.", self.name)


Expand Down
21 changes: 6 additions & 15 deletions PyInstaller/building/build_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

from PyInstaller import DEFAULT_DISTPATH, DEFAULT_WORKPATH, HOMEPATH, compat
from PyInstaller import log as logging
from PyInstaller.archive import pyz_crypto
from PyInstaller.building.api import COLLECT, EXE, MERGE, PYZ
from PyInstaller.building.datastruct import TOC, Target, Tree, _check_guts_eq
from PyInstaller.building.osx import BUNDLE
Expand Down Expand Up @@ -282,8 +281,6 @@ def __init__(
ignored (as though they were not found).
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, prefer not to follow version redirects when searching for Windows SxS Assemblies.
win_private_assemblies
Expand All @@ -294,6 +291,12 @@ def __init__(
An optional dict of package/module names and collection mode strings. Valid collection mode strings:
'pyz' (default), 'pyc', 'py', 'pyz+py' (or 'py+pyz')
"""
if cipher is not None:
raise SystemExit(
"The 'cipher' option (i.e. bytecode encryption feature) has been removed. Please remove the 'cipher' "
"arguments to PYZ() and Analysis() in your spec file. "
"For the rational and alternatives see https://github.com/pyinstaller/pyinstaller/pull/6999"
)
super().__init__()
from PyInstaller.config import CONF

Expand Down Expand Up @@ -354,15 +357,6 @@ def __init__(
# Custom runtime hook files that should be included and started before any existing PyInstaller runtime hooks.
self.custom_runtime_hooks = runtime_hooks or []

if cipher:
logger.info('Will encrypt Python bytecode with provided cipher key')
# Create a Python module which contains the decryption key which will be used at runtime by
# pyi_crypto.PyiBlockCipher.
pyi_crypto_key_path = os.path.join(CONF['workpath'], 'pyimod00_crypto_key.py')
with open(pyi_crypto_key_path, 'w', encoding='utf-8') as f:
f.write('# -*- coding: utf-8 -*-\nkey = %r\n' % cipher.key)
self.hiddenimports.append('tinyaes')

self.excludes = excludes or []
self.scripts = TOC()
self.pure = TOC()
Expand Down Expand Up @@ -404,8 +398,6 @@ def __init__(
('noarchive', _check_guts_eq),
('module_collection_mode', _check_guts_eq),

# 'cipher': no need to check as it is implied by an additional hidden import

# calculated/analysed values
('_python_version', _check_guts_eq),
('scripts', _check_guts_toc_mtime),
Expand Down Expand Up @@ -843,7 +835,6 @@ def build(spec, distpath, workpath, clean_build):
'Splash': Splash,
# Python modules available for .spec.
'os': os,
'pyi_crypto': pyz_crypto,
}

# Execute the specfile. Read it as a binary file...
Expand Down
30 changes: 10 additions & 20 deletions PyInstaller/building/makespec.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,10 @@

import argparse
import os
import sys

from PyInstaller import DEFAULT_SPECPATH, HOMEPATH
from PyInstaller import log as logging
from PyInstaller.building.templates import (
bundleexetmplt, bundletmplt, cipher_absent_template, cipher_init_template, onedirtmplt, onefiletmplt, splashtmpl
)
from PyInstaller.building.templates import bundleexetmplt, bundletmplt, onedirtmplt, onefiletmplt, splashtmpl
from PyInstaller.compat import expand_path, is_darwin, is_win

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -91,6 +88,13 @@ def make_variable_path(filename, conversions=path_conversions):
return None, filename


def removed_key_option(x):
raise SystemExit(
f"Bytecode encryption has been removed from PyInstaller. Remove your --key={x} argument. "
"For the rational and alternatives see https://github.com/pyinstaller/pyinstaller/pull/6999"
)


# An object used in place of a "path string", which knows how to repr() itself using variable names instead of
# hard-coded paths.
class Path:
Expand Down Expand Up @@ -346,7 +350,8 @@ def __add_options(parser):
g.add_argument(
'--key',
dest='key',
help='The key used to encrypt Python bytecode.',
help=argparse.SUPPRESS,
type=removed_key_option,
)
g.add_argument(
'--splash',
Expand Down Expand Up @@ -718,20 +723,6 @@ def main(
# With absolute paths replace prefix with variable HOMEPATH.
scripts = list(map(Path, scripts))

if key:
# Try to import tinyaes as we need it for bytecode obfuscation.
try:
import tinyaes # noqa: F401 (test import)
except ImportError:
logger.error(
'We need tinyaes to use byte-code obfuscation but we could not find it. You can install it '
'with pip by running:\n pip install tinyaes'
)
sys.exit(1)
cipher_init = cipher_init_template % {'key': key}
else:
cipher_init = cipher_absent_template

# Translate the default of ``debug=None`` to an empty list.
if debug is None:
debug = []
Expand Down Expand Up @@ -774,7 +765,6 @@ def main(
'upx_exclude': upx_exclude,
'runtime_tmpdir': runtime_tmpdir,
'exe_options': exe_options,
'cipher_init': cipher_init,
# Directory with additional custom import hooks.
'hookspath': hookspath,
# List with custom runtime hook files.
Expand Down
16 changes: 2 additions & 14 deletions PyInstaller/building/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

onefiletmplt = """# -*- mode: python ; coding: utf-8 -*-
%(preamble)s
%(cipher_init)s
a = Analysis(
%(scripts)s,
Expand All @@ -28,10 +27,9 @@
excludes=%(excludes)s,
win_no_prefer_redirects=%(win_no_prefer_redirects)s,
win_private_assemblies=%(win_private_assemblies)s,
cipher=block_cipher,
noarchive=%(noarchive)s,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data)
%(splash_init)s
exe = EXE(
pyz,
Expand All @@ -58,7 +56,6 @@

onedirtmplt = """# -*- mode: python ; coding: utf-8 -*-
%(preamble)s
%(cipher_init)s
a = Analysis(
%(scripts)s,
Expand All @@ -72,10 +69,9 @@
excludes=%(excludes)s,
win_no_prefer_redirects=%(win_no_prefer_redirects)s,
win_private_assemblies=%(win_private_assemblies)s,
cipher=block_cipher,
noarchive=%(noarchive)s,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data)
%(splash_init)s
exe = EXE(
pyz,
Expand Down Expand Up @@ -106,14 +102,6 @@
)
"""

cipher_absent_template = """
block_cipher = None
"""

cipher_init_template = """
block_cipher = pyi_crypto.PyiBlockCipher(key=%(key)r)
"""

bundleexetmplt = """app = BUNDLE(
exe,
name='%(name)s.app',
Expand Down
43 changes: 0 additions & 43 deletions PyInstaller/loader/pyimod01_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import _thread as thread
import marshal
import struct
import sys
import zlib

# For decrypting Python modules.
Expand Down Expand Up @@ -201,39 +200,6 @@ def checkmagic(self):
self.lib.read(4)


class Cipher:
"""
This class is used only to decrypt Python modules.
"""
def __init__(self):
# At build-time the key is given to us from inside the spec file. At bootstrap-time, we must look for it
# ourselves, by trying to import the generated 'pyi_crypto_key' module.
import pyimod00_crypto_key
key = pyimod00_crypto_key.key

assert type(key) is str
if len(key) > CRYPT_BLOCK_SIZE:
self.key = key[0:CRYPT_BLOCK_SIZE]
else:
self.key = key.zfill(CRYPT_BLOCK_SIZE)
assert len(self.key) == CRYPT_BLOCK_SIZE

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

def __create_cipher(self, iv):
# The 'AES' class is stateful, and 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):
cipher = self.__create_cipher(data[:CRYPT_BLOCK_SIZE])
return cipher.CTR_xcrypt_buffer(data[CRYPT_BLOCK_SIZE:])


class ZlibArchiveReader(ArchiveReader):
"""
ZlibArchive - an archive with compressed entries. Archive is read from the executable created by PyInstaller.
Expand Down Expand Up @@ -264,13 +230,6 @@ def __init__(self, path=None, offset=None):

super().__init__(path, offset)

# Try to import the key module. Its lack of availability indicates that the encryption is disabled.
try:
import pyimod00_crypto_key # noqa: F401
self.cipher = Cipher()
except ImportError:
self.cipher = None

def is_package(self, name):
(typ, pos, length) = self.toc.get(name, (0, None, 0))
if pos is None:
Expand All @@ -297,8 +256,6 @@ def extract(self, name):
"Continouation from this state is impossible. Exiting now."
)
try:
if self.cipher:
obj = self.cipher.decrypt(obj)
obj = zlib.decompress(obj)
if typ in (PYZ_TYPE_MODULE, PYZ_TYPE_PKG, PYZ_TYPE_NSPKG):
obj = marshal.loads(obj)
Expand Down

0 comments on commit ac32b58

Please sign in to comment.