From 29fd0929035672e494599b732540f948ee5ebc49 Mon Sep 17 00:00:00 2001 From: Markus Kaiserswerth Date: Sun, 23 Sep 2018 16:39:36 +0200 Subject: [PATCH 1/2] Look up encryption keys using file contents, not paths (#6) This is an attempt at solving issue #6 where the encryption key lookup would only work when the code was executed from a specific path. Instead of using relative file system paths, this change computes a hash of the encrypted file's contents and stores the encryption key with it. Upon import, the same hash is produced again to find the right decryption key for the file in question. --- README.md | 4 ++-- pyce/_crypto.py | 23 ++++++++++++++++------- pyce/_imports.py | 9 +++++---- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 13b3b4e..2ca7371 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ similar to `python3 -mcompileall -b` where `-b` does an in place compilation. ```python from pyce import encrypt_path encrypt_path('pyce/hello.pyc') -[('pyce/hello.pyce', '443df1d5f9914d13ed27950dd81aa2dd9d3b708be416c388f3226ad398d71a14')] +[('da78a5bfe655d01334d7121dc0f021de870dc11515424f528045eb5f29098504', 'd11d969ab1d345df86f4180db549f5ffc400da10d03d43e1fa93eae373199dae')] ``` Second, register your keys and try importing from the encrypted module or @@ -33,7 +33,7 @@ package: ```python from pyce import PYCEPathFinder -PYCEPathFinder.KEYS = {'pyce/hello.pyce' : '443df1d5f9914d13ed27950dd81aa2dd9d3b708be416c388f3226ad398d71a14'} +PYCEPathFinder.KEYS = {'da78a5bfe655d01334d7121dc0f021de870dc11515424f528045eb5f29098504': 'd11d969ab1d345df86f4180db549f5ffc400da10d03d43e1fa93eae373199dae'} import sys sys.meta_path.insert(0, PYCEPathFinder) diff --git a/pyce/_crypto.py b/pyce/_crypto.py index 7626a96..fe3d0af 100644 --- a/pyce/_crypto.py +++ b/pyce/_crypto.py @@ -24,8 +24,8 @@ Example use: >>> from pyce.crypto import encrypt_path, decryptf ->>> path, key = encrypt_path('pyce/hello.pyc')[0] ->>> print(decryptf(path, key)) +>>> hash, key = encrypt_path('pyce/hello.pyc')[0] +>>> print(decryptf(hash, key)) """ @@ -79,7 +79,7 @@ def encrypt_path(path: str, extensions: Set[str] = ENC_EXT, exclusions: exclusions: files that should not be encrypted Returns: - List of all paths and their encryption key + List of hashes of all encrypted files with their encryption key """ if exclusions is None: exclusions = set() @@ -118,8 +118,8 @@ def encrypt_path(path: str, extensions: Set[str] = ENC_EXT, exclusions: data = openf.read() # hash - hashv = srchash(data) - key = hashv.digest() + plaintext_hash = srchash(data) + key = plaintext_hash.digest() # encrypt cipher = Cipher(CIPHER(key), MODE(key[0:16]), backend=BACKEND) @@ -131,12 +131,21 @@ def encrypt_path(path: str, extensions: Set[str] = ENC_EXT, exclusions: openf.write(ciphertext) # append HMAC - openf.write(hmac(key, ciphertext, HMAC_HS).digest()) + ciphertext_hmac = hmac(key, ciphertext, HMAC_HS).digest() + openf.write(ciphertext_hmac) new_absolute_path, ext = splitext(absolute_path) new_absolute_path += ext + 'e' rename(absolute_path, new_absolute_path) - manifest.append((new_absolute_path, hashv.hexdigest())) + + # add the decryption key for this file together with a hash of its + # encrypted contents. The import mechanism will then look up the + # required key by producing this same hash again at runtime. + encrypted_data = ciphertext + ciphertext_hmac + ciphertext_hash = srchash(encrypted_data) + manifest.append( + (ciphertext_hash.hexdigest(), plaintext_hash.hexdigest()) + ) return manifest diff --git a/pyce/_imports.py b/pyce/_imports.py index 06dd52a..72ca534 100644 --- a/pyce/_imports.py +++ b/pyce/_imports.py @@ -26,10 +26,9 @@ _classify_pyc) from importlib.machinery import (FileFinder, ModuleSpec, PathFinder, SourcelessFileLoader) -from os.path import normcase, relpath from typing import Any, Dict, List, Optional, Tuple -from pyce._crypto import decrypt +from pyce._crypto import decrypt, srchash # Globals EXTENSIONS = ['.pyce'] @@ -69,8 +68,10 @@ def get_code(self, fullname: str) -> Any: path = self.get_filename(fullname) data = self.get_data(path) - # It is important to normalize path case for platforms like Windows - data = decrypt(data, PYCEPathFinder.KEYS[normcase(relpath(path))]) + # Decrypt the code. Use a hash of the encrypted data to look up the + # required encryption key. + data_hash = srchash(data).hexdigest() + data = decrypt(data, PYCEPathFinder.KEYS[data_hash]) # Call _classify_pyc to do basic validation of the pyc but ignore the # result. There's no source to check against. From 7a87405dc6dc6c8c57fa2d737c5215607c4f0ef0 Mon Sep 17 00:00:00 2001 From: Markus Kaiserswerth Date: Tue, 25 Sep 2018 09:22:46 +0200 Subject: [PATCH 2/2] Corrected encrypt/decrypt example in module docstring --- pyce/_crypto.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyce/_crypto.py b/pyce/_crypto.py index fe3d0af..b97c1d7 100644 --- a/pyce/_crypto.py +++ b/pyce/_crypto.py @@ -25,7 +25,7 @@ >>> from pyce.crypto import encrypt_path, decryptf >>> hash, key = encrypt_path('pyce/hello.pyc')[0] ->>> print(decryptf(hash, key)) +>>> print(decryptf('pyce/hello.pyce', key)) """