From 7f9439a779b1387531a2aa0e4d6816f0a5eaa576 Mon Sep 17 00:00:00 2001 From: Sebastien Chapiron Date: Thu, 14 Mar 2024 23:05:58 +0100 Subject: [PATCH] New version fixing several bugs, especially on Windows platform, adding tests and github workflow, and adding support for preserving input archives' directory structure in the output directory --- .github/workflows/python-package.yml | 44 ++ .gitignore | 10 +- Makefile | 11 - setup.py | 43 +- src/anssi_orcdecrypt/__init__.py | 3 - src/anssi_orcdecrypt/decrypt.py | 318 ------------ src/anssi_orcdecrypt/utils.py | 56 --- src/orcdecrypt/__init__.py | 0 src/{anssi_orcdecrypt => orcdecrypt}/cli.py | 89 ++-- src/orcdecrypt/decrypt.py | 456 +++++++++++++++++ src/orcdecrypt/utils.py | 49 ++ src/unstream.c | 7 +- test_data/archive_aes.7z | Bin 0 -> 4544 bytes test_data/archive_aes.7z.p7b | Bin 0 -> 5614 bytes test_data/archive_aes_corrupted.7z.p7b | Bin 0 -> 2429 bytes test_data/archive_aes_padded.7z.p7b | Bin 0 -> 5814 bytes test_data/archive_aes_truncated.7z.p7b | Bin 0 -> 5000 bytes test_data/archive_des.7z.p7b | Bin 0 -> 5509 bytes test_data/archive_des_corrupted.7z.p7b | Bin 0 -> 2429 bytes test_data/archive_des_padded.7z.p7b | Bin 0 -> 5709 bytes test_data/archive_des_truncated.7z.p7b | Bin 0 -> 5000 bytes test_data/orc-cert-202202251900.pem | 21 + test_data/orc-cert-202202260752.pem | 21 + test_data/orc-key-202202260752.pfx | Bin 0 -> 2485 bytes test_data/recipient1-key.enc.pem | 30 ++ test_data/recipient1-key.pem | 28 ++ test_data/recipient2-key.pem | 28 ++ test_data/recipient3-key.pem | 28 ++ test_data/subdir_a/archive_aes.7z.p7b | Bin 0 -> 5614 bytes .../subdir_a/subdir_1/archive_aes.7z.p7b | Bin 0 -> 5614 bytes .../subdir_a/subdir_2/archive_aes.7z.p7b | Bin 0 -> 5614 bytes test_data/subdir_b/archive_des.7z.p7b | Bin 0 -> 5509 bytes tests/__init__.py | 0 tests/conftest.py | 217 ++++++++ tests/test_cli.py | 474 ++++++++++++++++++ tests/test_decrypt.py | 399 +++++++++++++++ 36 files changed, 1904 insertions(+), 428 deletions(-) create mode 100644 .github/workflows/python-package.yml delete mode 100644 Makefile delete mode 100644 src/anssi_orcdecrypt/__init__.py delete mode 100644 src/anssi_orcdecrypt/decrypt.py delete mode 100644 src/anssi_orcdecrypt/utils.py create mode 100644 src/orcdecrypt/__init__.py rename src/{anssi_orcdecrypt => orcdecrypt}/cli.py (72%) create mode 100644 src/orcdecrypt/decrypt.py create mode 100644 src/orcdecrypt/utils.py create mode 100644 test_data/archive_aes.7z create mode 100644 test_data/archive_aes.7z.p7b create mode 100644 test_data/archive_aes_corrupted.7z.p7b create mode 100644 test_data/archive_aes_padded.7z.p7b create mode 100644 test_data/archive_aes_truncated.7z.p7b create mode 100644 test_data/archive_des.7z.p7b create mode 100644 test_data/archive_des_corrupted.7z.p7b create mode 100644 test_data/archive_des_padded.7z.p7b create mode 100644 test_data/archive_des_truncated.7z.p7b create mode 100644 test_data/orc-cert-202202251900.pem create mode 100644 test_data/orc-cert-202202260752.pem create mode 100644 test_data/orc-key-202202260752.pfx create mode 100644 test_data/recipient1-key.enc.pem create mode 100644 test_data/recipient1-key.pem create mode 100644 test_data/recipient2-key.pem create mode 100644 test_data/recipient3-key.pem create mode 100644 test_data/subdir_a/archive_aes.7z.p7b create mode 100644 test_data/subdir_a/subdir_1/archive_aes.7z.p7b create mode 100644 test_data/subdir_a/subdir_2/archive_aes.7z.p7b create mode 100644 test_data/subdir_b/archive_des.7z.p7b create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_decrypt.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..0e040de --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,44 @@ +name: Python tests + +on: + push: + pull_request: + +jobs: + test: + + runs-on: "${{ matrix.os }}" + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install ruff pytest pytest-cov pytest-console-scripts + python -m pip install -vvv . + - name: Lint with Ruff + run: | + ruff check . + continue-on-error: true + - name: "Test with pytest on ${{ matrix.os }} Python ${{ matrix.python-version }}" + run: | + coverage run -m pytest --log-level INFO + - name: Generate Coverage Report + run: | + coverage report -m diff --git a/.gitignore b/.gitignore index 30d388a..7471637 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,9 @@ -build* \ No newline at end of file +.idea +dist +build +*.egg-info +__pycache__ +.tox +.coverage +report.xml +coverage.xml diff --git a/Makefile b/Makefile deleted file mode 100644 index ccb24b6..0000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -CC=gcc -CCOPTS=-W -Wall -O2 -fPIC -Wno-strict-aliasing -LDOPTS=-s -pie - -all: unstream - -%: %.c - $(CC) $(CCOPTS) -o $@ $+ $(LDOPTS) - -clean: - rm -f *.o diff --git a/setup.py b/setup.py index c74b0bf..34030b9 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,12 @@ import os +import shutil import subprocess import sys from pathlib import Path from setuptools import find_packages from setuptools import setup +from setuptools.command.build import build from setuptools.command.install import install here = Path(__file__).parent.resolve() @@ -12,20 +14,33 @@ build_type = "Release" -class MyInstall(install): +class MyBuild(build): def run(self): if subprocess.call(["cmake", "-S", ".", "-B", build_dir.resolve()]) != 0: sys.exit(-1) - if subprocess.call(["cmake", "--build", build_dir.resolve(), "--config", build_type]) != 0: + if ( + subprocess.call( + ["cmake", "--build", build_dir.resolve(), "--config", build_type] + ) + != 0 + ): sys.exit(-1) - # Try to install but ignore errors if permission is denied - subprocess.call(["cmake", "--install", build_dir.resolve(), "--config", build_type]) - install.run(self) + return super().run() + + +class MyInstall(install): + def run(self): + if "VIRTUAL_ENV" in os.environ and sys.platform == "win32": + shutil.copy( + build_dir / build_type / "unstream.exe", + Path(os.environ["VIRTUAL_ENV"]) / "Scripts" / "unstream.exe", + ) + return super().run() setup( name="anssi-orcdecrypt", - version="4.1", + version="5.3.2", url="https://github.com/DFIR-ORC/orc-decrypt", license="LGPL-2.1+", author="Sebastien Chapiron", @@ -36,10 +51,22 @@ def run(self): install_requires=["cryptography>=0.6"], packages=find_packages(where="src"), package_dir={"": "src"}, + data_files=[ + ( + "bin", + [ + ( + str(build_dir / build_type / "unstream.exe") + if sys.platform == "win32" + else str(build_dir / "unstream") + ), + ], + ), + ], zip_safe=False, entry_points={ "console_scripts": [ - "orc-decrypt=anssi_orcdecrypt.cli:main", + "orc-decrypt=orcdecrypt.cli:entrypoint", ], }, classifiers=[ # Optional @@ -49,11 +76,13 @@ def run(self): "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", "Operating System :: Unix", ], python_requires=">=3.8, <4", cmdclass={ + "build": MyBuild, "install": MyInstall, }, ) diff --git a/src/anssi_orcdecrypt/__init__.py b/src/anssi_orcdecrypt/__init__.py deleted file mode 100644 index dc796d4..0000000 --- a/src/anssi_orcdecrypt/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .decrypt import decrypt_archive - -__all__ = ["decrypt_archive"] diff --git a/src/anssi_orcdecrypt/decrypt.py b/src/anssi_orcdecrypt/decrypt.py deleted file mode 100644 index f76d393..0000000 --- a/src/anssi_orcdecrypt/decrypt.py +++ /dev/null @@ -1,318 +0,0 @@ -#! /usr/bin/env python -# -*- coding:utf-8 -*- -# -# SPDX-License-Identifier: LGPL-2.1-or-later -# -# Copyright © 2011-2020 ANSSI. All Rights Reserved. -# -# Author(s): Ryad Benadjila (ANSSI), Sebastien Chapiron (ANSSI), Arnaud Ebalard (ANSSI) -# - -import logging -import re -import struct -import subprocess # nosec B404 - checked -from pathlib import Path -from shutil import which - -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import padding -from cryptography.hazmat.primitives.ciphers import Cipher -from cryptography.hazmat.primitives.ciphers import algorithms -from cryptography.hazmat.primitives.ciphers import modes -from cryptography.hazmat.primitives.hashes import HashAlgorithm -from cryptography.hazmat.primitives.serialization import load_pem_private_key - -from anssi_orcdecrypt.utils import ContextFilter -from anssi_orcdecrypt.utils import decode_oid - -rsa_oaep_oid = re.compile(re.escape(bytes.fromhex("06092a864886f70d010107"))) -rsa_encryption_oid = re.compile(re.escape(bytes.fromhex("06092a864886f70d0101010500"))) -pkcs7_data_oid = bytes.fromhex("06092a864886f70d010701") - - -def decrypt_archive( - archive_path: Path, - private_key: Path, - output_file: Path, - unstream_cmd: Path, - method: str = "auto", - force: bool = False, -) -> bool: - logging.getLogger("").addFilter(ContextFilter(archive_path.name)) - logging.debug("Processing archive %s", archive_path) - - if output_file.exists() and not force: - logging.error( - "Output file %s already exists, skipping (use --force to overwrite)", - output_file, - ) - return False - - if archive_path.stat().st_size >= 2**32 / 2 - 1: - if method == "auto" or method == "python": - res = decrypt_archive_python( - archive_path, private_key, output_file, unstream_cmd - ) - else: - logging.warning( - "Processing archive %s with OpenSSL will likely fail because file is too big (%d bytes) !", - archive_path, - archive_path.stat().st_size, - ) - res = decrypt_archive_openssl( - archive_path, private_key, output_file, unstream_cmd - ) - else: - if method == "auto" or method == "openssl": - res = decrypt_archive_openssl( - archive_path, private_key, output_file, unstream_cmd - ) - else: - res = decrypt_archive_python( - archive_path, private_key, output_file, unstream_cmd - ) - - if res: - logging.info("Successfully decrypted archive into %s", output_file) - else: - logging.error("Failed to decrypt archive") - return res - - -def decrypt_archive_python( - archive_path: Path, - private_key: Path, - output_file: Path, - unstream_cmd: Path, - hash_algo: HashAlgorithm = hashes.SHA1(), # nosec B303 - Not used for encryption, only decryption -) -> bool: - logger = logging.getLogger(__name__) - logger.addFilter(ContextFilter(archive_path.name)) - logger.debug( - "Decrypting archive with private key %s using Python implementation", - private_key, - ) - - # Load the private key and the base 64 ciphertext. - pkey = load_pem_private_key(open(private_key, "rb").read(), password=None) - - # We grab the beginning of the file so that we can extract the - # various information we need (RSA-encrypted symmetric key, - # encryption algorithm, etc). After having decrypted the key - # we open the file at the right offset (beginning of octet - # strings containing data) and decrypt the data. - s = open(archive_path, "rb").read(50 * 1024) - - symkey = None - # First try with RSAES-OAEP OID - for match in rsa_oaep_oid.finditer(s): - # Next we should have 04|82|len|rsaencryptedsymkey - key_offset = match.end() + 4 - encsymkeylen = struct.unpack(">H", s[key_offset : key_offset + 2])[0] - encsymkey = s[key_offset + 2 : key_offset + 2 + encsymkeylen] - try: - symkey = pkey.decrypt( - encsymkey, - padding=padding.OAEP( - mgf=padding.MGF1(algorithm=hash_algo), - algorithm=hash_algo, - label=None, - ), - ) - logger.debug( - "Successfully decrypted symmetric key found with RSAES-OAEP OID at offset 0x%x", - match.start(), - ) - break - except ValueError: - pass - else: - logger.warning( - "Failed to decrypt any of the %d symmetric keys found with RSAES-OAEP OID", - len(rsa_oaep_oid.findall(s)), - ) - - if not symkey: - # Try with rsaEncryption OID - for match in rsa_encryption_oid.finditer(s): - # Next we should have 04|82|len|rsaencryptedsymkey - key_offset = match.end() + 2 - encsymkeylen = struct.unpack(">H", s[key_offset : key_offset + 2])[0] - encsymkey = s[key_offset + 2 : key_offset + 2 + encsymkeylen] - try: - symkey = pkey.decrypt(encsymkey, padding=padding.PKCS1v15()) - logger.debug( - "Successfully decrypted symmetric key found with rsaEncryption OID at offset 0x%x", - match.start(), - ) - break - except ValueError: - pass - else: - logger.warning( - "Failed to decrypt any of the %d symmetric keys found with rsaEncryption OID", - len(rsa_encryption_oid.findall(s)), - ) - - if not symkey: - return False - - # Next, we jump to pkcs7-data OID. It is followed by three bytes - # before the beginning of symmetric encryption method OID - pkcs7_offset = s.find(pkcs7_data_oid) + len(pkcs7_data_oid) + 2 - oidlen = s[pkcs7_offset + 1] - sym_enc_oid = decode_oid(s[pkcs7_offset : pkcs7_offset + oidlen + 2]) - - # Next elements is IV - iv_offset = pkcs7_offset + oidlen + 2 - ivlen = s[iv_offset + 1] - iv = s[iv_offset + 2 : iv_offset + ivlen + 2] - - if sym_enc_oid == "2.16.840.1.101.3.4.1.42": # AES 256 CBC - logger.debug("Using AES-256-CBC (OID %s) for decryption", sym_enc_oid) - cipher = Cipher(algorithms.AES(symkey), modes.CBC(iv)) - elif sym_enc_oid == "2.16.840.1.101.3.4.1.2": # AES 128 CBC - logger.debug("Using AES-128-CBC (OID %s) for decryption", sym_enc_oid) - cipher = Cipher(algorithms.AES(symkey), modes.CBC(iv)) - elif sym_enc_oid == "1.2.840.113549.3.7": # dES-EDE3-CBC - logger.debug("Using 3DES-CBC (OID %s) for decryption", sym_enc_oid) - cipher = Cipher(algorithms.TripleDES(symkey), modes.CBC(iv)) - else: - raise NotImplementedError(f"Unknown encryption algorithm w/ OID {sym_enc_oid}") - - # We should now have all our octet strings providing encrypted - # content after a A0 80 - content_offset = iv_offset + ivlen + 2 - if s[content_offset : content_offset + 2] != b"\xA0\x80": - logger.error( - "File does not match what we expected (\\xA0\\x80) at offset %d: %s. Found at %d", - content_offset, - s[content_offset - 10 : content_offset + 20].hex(), - s[813:].find(b"\xA0\x80"), - ) - return False - - with open(archive_path, "rb") as f: - f.seek(content_offset + 2) - logger.debug("Writing to %s", output_file) - # Remove output file if it exists so that unstream does not fail - if output_file.exists(): - output_file.unlink() - - decryptor = cipher.decryptor() - try: - p = subprocess.Popen( # nosec B603 - unstream_cmd is checked for type and existence, variables are quoted - [unstream_cmd.resolve(), "-", output_file.resolve()], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - assert p.stdin is not None # nosec B101 - only for type hinting - t, c = struct.unpack("BB", f.read(2)) - prev = bytes() - while t == 0x04: - if c & 0x80 == 0: - oslen = c - else: - oslen = int.from_bytes(f.read(c & 0x7F), byteorder="big") - # Revisit to deal with incomplete read - p.stdin.write(prev) - try: - prev = decryptor.update(f.read(oslen)) - except ValueError as e: - logger.error( - "Failed to decrypt %d bytes at offset %d: %s", - oslen, - f.tell(), - e, - ) - h = f.read(2) - if not h: - break - t, c = struct.unpack("BB", h) - - # We need to remove possible padding from last decrypted chunk - if len(prev) > 1 and len(prev) > prev[-1]: - prev = prev[: -prev[-1]] - p.stdin.write(prev) - except BrokenPipeError: - pass - out, err = p.communicate() - if err: - for line in err.splitlines(): - logger.error("[unstream] %s", line.decode("utf-8")) - if p.returncode != 0: - return False - return True - - -def decrypt_archive_openssl( - archive_path: Path, private_key: Path, output_file: Path, unstream_cmd: Path -) -> bool: - openssl_cmd = which("openssl") - if not openssl_cmd or not Path(openssl_cmd).exists(): - raise FileNotFoundError("OpenSSL binary could not be found in the PATH") - openssl_cmd = Path(openssl_cmd).resolve() # type: ignore[assignment] - - logger = logging.getLogger(__name__) - logger.addFilter(ContextFilter(archive_path.name)) - logger.debug("Decrypting archive with private key %s using OpenSSL", private_key) - - # Remove output file if it exists so that unstream does not fail - if output_file.exists(): - output_file.unlink() - openssl_p = subprocess.Popen( # nosec B603 - unstream_cmd is checked for type and existence, and variables are quoted - [ - openssl_cmd, - "cms", - "-decrypt", - "-in", - archive_path.resolve(), - "-inform", - "DER", - "-inkey", - private_key.resolve(), - "-binary", - ], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - unstream_p = subprocess.Popen( # nosec B603 - unstream_cmd is checked for type and existence, and variables are quoted - [unstream_cmd.resolve(), "-", output_file.resolve()], - stdin=openssl_p.stdout, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - assert openssl_p.stdout is not None # nosec B101 - only for type hinting - openssl_p.stdout.close() - - _, err = openssl_p.communicate() - if err: - for line in err.splitlines(): - logger.error("[openssl stderr] %s", line.decode("utf-8")) - - out, err = unstream_p.communicate() - if err: - for line in err.splitlines(): - logger.error("[unstream stderr] %s", line.decode("utf-8")) - - if ( - openssl_p.returncode == 0 - and unstream_p.returncode == 0 - and output_file.exists() - and output_file.stat().st_size > 0 - ): - return True - else: - logger.error( - "Decrypting archive %s with openssl (exit code: %d) and unstream (exit code: %d) failed. " - "Decrypted output size was: %d", - archive_path, - openssl_p.returncode, - unstream_p.returncode, - output_file.stat().st_size, - ) - if output_file.exists(): - output_file.unlink() - return False diff --git a/src/anssi_orcdecrypt/utils.py b/src/anssi_orcdecrypt/utils.py deleted file mode 100644 index 9ae73d0..0000000 --- a/src/anssi_orcdecrypt/utils.py +++ /dev/null @@ -1,56 +0,0 @@ -#! /usr/bin/env python -# -*- coding:utf-8 -*- -# -# SPDX-License-Identifier: LGPL-2.1-or-later -# -# Copyright © 2011-2020 ANSSI. All Rights Reserved. -# -# Author(s): Ryad Benadjila (ANSSI), Sebastien Chapiron (ANSSI), Arnaud Ebalard (ANSSI) -# -import logging - - -def decode_oid(oid: bytes) -> str: - if not (oid[0] == 6 and oid[1] == len(oid) - 2): - raise ValueError( - f"Not an OID: {oid.hex()}. Check first byte ({oid[0]}) and length {len(oid) - 2} with second byte ({oid[1]})." - ) - orig_oid = oid - oid = oid[2:] - - res = [] - res.append(int(oid[0] / 40)) - res.append(oid[0] - (40 * res[0])) - oid = oid[1:] - - cur = 0 - while oid: - tmp = oid[0] - cur <<= 7 - if tmp & 0x80: - cur |= tmp - 0x80 - else: - cur |= tmp - res.append(cur) - cur = 0 - oid = oid[1:] - - logging.debug( - "Decoded bytes %s as OID %s", - orig_oid.hex(), - ".".join(map(lambda x: "%d" % x, res)), - ) - return ".".join(map(lambda x: "%d" % x, res)) - - -class ContextFilter(logging.Filter): - """ - This is a filter which injects contextual information into the log. - """ - - def __init__(self, ctx_str: str): - self._ctx_str = ctx_str - - def filter(self, record): - record.ctx = self._ctx_str - return True diff --git a/src/orcdecrypt/__init__.py b/src/orcdecrypt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/anssi_orcdecrypt/cli.py b/src/orcdecrypt/cli.py similarity index 72% rename from src/anssi_orcdecrypt/cli.py rename to src/orcdecrypt/cli.py index daa6b2f..de6e7af 100644 --- a/src/anssi_orcdecrypt/cli.py +++ b/src/orcdecrypt/cli.py @@ -11,18 +11,21 @@ import argparse import logging import multiprocessing -import sys from datetime import datetime from pathlib import Path from shutil import which +from typing import Dict +from typing import List +from typing import Optional +from typing import Sequence from cryptography.hazmat.primitives.serialization import load_pem_private_key -from anssi_orcdecrypt import decrypt_archive -from anssi_orcdecrypt.utils import ContextFilter +from .decrypt import decrypt_archive +from .utils import ContextFilter -def parse_args(): +def get_parser(): def is_pkey(f: str) -> Path: try: load_pem_private_key(open(f, "rb").read(), password=None) @@ -114,41 +117,52 @@ def is_path(p): help='Path to the "unstream" binary. If not set, unstream must be in the PATH.', ) - return parser.parse_args() + return parser -def main(): - args = parse_args() - begin = datetime.now() - +def setup_logging( + log_file: Optional[str] = None, + log_level: Optional[str] = None, +) -> None: + """Do setup logging to redirect to log_file at DEBUG level.""" + if log_level is None: + log_level = "INFO" # Setup logging - log_level = getattr(logging, args.log_level) - if args.log_file: - # Send everything (DEBUG included) in the log file and keep only log_level messages on the console + if log_file: + # Send everything (DEBUG included) in the log file + # and keep only log_level messages on the console logging.basicConfig( level=logging.DEBUG, - format="[%(asctime)s] %(name)-12s - %(levelname)-8s - [%(ctx)s] %(message)s", - filename=args.log_file, + format="[%(asctime)s] %(levelname)-8s - %(name)s - %(ctx)s%(message)s", + filename=log_file, filemode="w", ) - # define a Handler which writes messages of log_level or higher to the sys.stderr + # define a Handler which writes messages of log_level + # or higher to the sys.stderr console = logging.StreamHandler() console.setLevel(log_level) # set a format which is simpler for console use formatter = logging.Formatter( - "[%(asctime)s] - %(levelname)-8s - [%(ctx)s] %(message)s" + "[%(asctime)s] %(levelname)-8s - %(ctx)s%(message)s", ) # tell the handler to use this format console.setFormatter(formatter) # add the handler to the root logger - logging.getLogger("").addHandler(console) + logging.root.addHandler(console) else: logging.basicConfig( - level=getattr(logging, args.log_level), - format="[%(asctime)s] - %(levelname)-8s - [%(ctx)s] %(message)s", + level=log_level, + format="[%(asctime)s] %(levelname)-8s - %(ctx)s%(message)s", ) - # set a filter to add context information - logging.getLogger("").addFilter(ContextFilter("main")) + for h in logging.root.handlers: + h.addFilter(ContextFilter()) + + +def entrypoint(argv: Optional[Sequence[str]] = None) -> int: + parser = get_parser() + args = parser.parse_args(argv) + setup_logging(args.log_file, args.log_level) + begin = datetime.now() # unstream should be installed in the PATH or specified on the CLI with --unstream-path unstream_cmd = which("unstream") @@ -157,7 +171,7 @@ def main(): logging.critical( 'Missing tool "unstream" in PATH, please provide the path to unstream binary with --unstream-path' ) - sys.exit(-1) + return -1 else: unstream_cmd = Path(unstream_cmd) # type: ignore[assignment] # args.unstream overrides unstream_cmd if it is a valid file @@ -170,18 +184,30 @@ def main(): ) args.method = "python" - # Build the list of encrypted archives from the input paths given on the CLI. Directories are recursively searched - archives_list = list() + # Build the list of encrypted archives and the corresponding output path from the input paths given on the CLI. + # Directories are recursively searched. + output_dir = Path(args.output_dir) + archives_list: List[Dict[str, Path]] = list() for path in args.input: if path.is_file(): - archives_list.append(path) + archives_list.append( + {"input": path, "output": (output_dir / path.stem).resolve()} + ) elif path.is_dir(): - archives_list += list(path.rglob("*.p7b")) + for subpath in path.rglob("*.p7b"): + archives_list.append( + { + "input": subpath, + "output": ( + output_dir / subpath.relative_to(path).parent / subpath.stem + ).resolve(), + } + ) if len(archives_list) == 0: logging.info( "No encrypted archives could be found in input path(s), nothing to do." ) - sys.exit(0) + return 0 logging.info( "Decrypting %d archives into %s using private key %s", len(archives_list), @@ -189,20 +215,20 @@ def main(): args.key, ) - Path(args.output_dir).mkdir(parents=True, exist_ok=True) + output_dir.mkdir(parents=True, exist_ok=True) with multiprocessing.Pool(args.jobs) as pool: results = pool.starmap( decrypt_archive, [ ( - f, + a["input"], Path(args.key), - (Path(args.output_dir) / f.stem).resolve(), + a["output"], unstream_cmd, args.method, args.force, ) - for f in archives_list + for a in archives_list ], ) @@ -214,3 +240,4 @@ def main(): results.count(False), results.count(True), ) + return 0 diff --git a/src/orcdecrypt/decrypt.py b/src/orcdecrypt/decrypt.py new file mode 100644 index 0000000..26e60a3 --- /dev/null +++ b/src/orcdecrypt/decrypt.py @@ -0,0 +1,456 @@ +#! /usr/bin/env python +# -*- coding:utf-8 -*- +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Copyright © 2011-2020 ANSSI. All Rights Reserved. +# +# Author(s): Ryad Benadjila (ANSSI), Sebastien Chapiron (ANSSI), Arnaud Ebalard (ANSSI) +# + +import logging +import os +import re +import struct +import subprocess # nosec B404 - checked +from pathlib import Path +from shutil import which +from tempfile import mkstemp +from typing import Optional +from typing import Tuple +from typing import Union + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey +from cryptography.hazmat.primitives.ciphers import Cipher +from cryptography.hazmat.primitives.ciphers import algorithms +from cryptography.hazmat.primitives.ciphers import modes +from cryptography.hazmat.primitives.hashes import HashAlgorithm +from cryptography.hazmat.primitives.serialization import load_pem_private_key + +rsa_oaep_oid = re.compile(re.escape(bytes.fromhex("06092a864886f70d010107"))) +rsa_encryption_oid = re.compile(re.escape(bytes.fromhex("06092a864886f70d0101010500"))) +pkcs7_data_oid = bytes.fromhex("06092a864886f70d010701") +logger = logging.getLogger("orcdecrypt.decrypt") + + +def decrypt_archive( + archive_path: Path, + private_key: Union[Path, bytes], + output_file: Path, + unstream_cmd: Path, + method: str = "auto", + force: bool = False, + password: Union[bytes, None] = None, +) -> bool: + logger.debug("Processing archive %s", archive_path) + log = logging.LoggerAdapter(logger, {"archive": archive_path.name}) + + if output_file.exists() and not force: + log.error( + "Output file %s already exists, skipping (use --force to overwrite)", + output_file, + ) + return False + + output_file.parent.mkdir(parents=True, exist_ok=True) + openssl_cmd = which("openssl") + + if archive_path.stat().st_size >= 2**32 / 2 - 1: + if method == "auto" or method == "python": + res = decrypt_archive_python( + archive_path, private_key, output_file, unstream_cmd, password=password + ) + else: + log.warning( + "Processing archive %s with OpenSSL will likely fail because file is too big (%d bytes) !", + archive_path, + archive_path.stat().st_size, + ) + res = decrypt_archive_openssl( + archive_path, + private_key, + output_file, + unstream_cmd, + openssl_cmd, + password=password, + ) + else: + if method == "auto": + if not openssl_cmd or not Path(openssl_cmd).exists(): + log.warning( + "OpenSSL binary could not be found in the PATH, falling back to python decryption." + ) + res = decrypt_archive_python( + archive_path, + private_key, + output_file, + unstream_cmd, + password=password, + ) + else: + res = decrypt_archive_openssl( + archive_path, + private_key, + output_file, + unstream_cmd, + openssl_cmd, + password=password, + ) + elif method == "openssl": + res = decrypt_archive_openssl( + archive_path, + private_key, + output_file, + unstream_cmd, + openssl_cmd, + password=password, + ) + else: + res = decrypt_archive_python( + archive_path, private_key, output_file, unstream_cmd, password=password + ) + + if res: + log.info("Successfully decrypted archive into %s", output_file) + else: + log.error("Failed to decrypt archive") + return res + + +def decrypt_archive_python( + archive_path: Path, + private_key: Union[Path, bytes], + output_file: Path, + unstream_cmd: Path, + hash_algo: HashAlgorithm = hashes.SHA1(), # nosec B303 - Not used for encryption, only decryption + password: Union[bytes, None] = None, +) -> bool: + log = logging.LoggerAdapter( + logger, {"archive": archive_path.name, "method": "python"} + ) + log.debug( + "Decrypting archive with private key %s using Python implementation", + private_key, + ) + + def get_cipher(oid: str, key: bytes, iv: bytes): + if oid == "2.16.840.1.101.3.4.1.42": # AES 256 CBC + log.debug("Using AES-256-CBC (OID %s) for decryption", oid) + ret = Cipher(algorithms.AES(key), modes.CBC(iv)) + elif oid == "2.16.840.1.101.3.4.1.2": # AES 128 CBC + log.debug("Using AES-128-CBC (OID %s) for decryption", oid) + ret = Cipher(algorithms.AES(key), modes.CBC(iv)) + elif oid == "1.2.840.113549.3.7": # dES-EDE3-CBC + log.debug("Using 3DES-CBC (OID %s) for decryption", oid) + ret = Cipher(algorithms.TripleDES(key), modes.CBC(iv)) + else: + raise NotImplementedError(f"Unknown encryption algorithm w/ OID {oid}") + return ret + + def decode_oid(oid: bytes) -> str: + if not (oid[0] == 6 and oid[1] == len(oid) - 2): + raise ValueError( + f"Not an OID: {oid.hex()}. Check first byte ({oid[0]}) and length {len(oid) - 2} with second byte ({oid[1]})." + ) + orig_oid = oid + oid = oid[2:] + + res = [] + res.append(int(oid[0] / 40)) + res.append(oid[0] - (40 * res[0])) + oid = oid[1:] + + cur = 0 + while oid: + tmp = oid[0] + cur <<= 7 + if tmp & 0x80: + cur |= tmp - 0x80 + else: + cur |= tmp + res.append(cur) + cur = 0 + oid = oid[1:] + + log.debug( + "Decoded bytes %s as OID %s", + orig_oid.hex(), + ".".join(map(lambda x: "%d" % x, res)), + ) + return ".".join(map(lambda x: "%d" % x, res)) + + # Load the private key and the base 64 ciphertext. + if isinstance(private_key, Path): + private_key = open(private_key, "rb").read() + pkey = load_pem_private_key(private_key, password=password) + if not isinstance(pkey, RSAPrivateKey): + raise ValueError(f"Unsupported private key type: {type(pkey)}") + + # We grab the beginning of the file so that we can extract the + # various information we need (RSA-encrypted symmetric key, + # encryption algorithm, etc). After having decrypted the key + # we open the file at the right offset (beginning of octet + # strings containing data) and decrypt the data. + s = open(archive_path, "rb").read(50 * 1024) + + # Next, we jump to pkcs7-data OID. It is followed by three bytes + # before the beginning of symmetric encryption method OID + pkcs7_offset = s.find(pkcs7_data_oid) + len(pkcs7_data_oid) + 2 + oidlen = s[pkcs7_offset + 1] + sym_enc_oid = decode_oid(s[pkcs7_offset : pkcs7_offset + oidlen + 2]) + + # Next elements is IV + iv_offset = pkcs7_offset + oidlen + 2 + ivlen = s[iv_offset + 1] + iv = s[iv_offset + 2 : iv_offset + ivlen + 2] + + symkey = None + cipher = None + match_rsaes_oaep = rsa_oaep_oid.findall(s) + match_rsa_encryption = rsa_encryption_oid.findall(s) + log.debug( + "Found %d potential recipients with RSAES-OAEP OID and %d potential recipients with rsaEncryption OID", + len(match_rsaes_oaep), + len(match_rsa_encryption), + ) + # First try with RSAES-OAEP OID + if len(match_rsaes_oaep) > 0: + for i, match in enumerate(rsa_oaep_oid.finditer(s), start=1): + # Next we should have 04|82|len|rsaencryptedsymkey + key_offset = match.end() + 4 + encsymkeylen = struct.unpack(">H", s[key_offset : key_offset + 2])[0] + encsymkey = s[key_offset + 2 : key_offset + 2 + encsymkeylen] + log.debug( + "Found potential RSAES-OAEP OID at offset 0x%x, with encrypted symmetric key length %d", + match.start(), + encsymkeylen, + ) + try: + symkey = pkey.decrypt( + encsymkey, + padding=padding.OAEP( + mgf=padding.MGF1(algorithm=hash_algo), + algorithm=hash_algo, + label=None, + ), + ) + cipher = get_cipher(sym_enc_oid, symkey, iv) + log.debug( + "Successfully decrypted symmetric key (length %d) for recipient n°%d found with RSAES-OAEP OID at offset 0x%x", + len(symkey), + i, + match.start(), + ) + break + except ValueError as e: + log.warning( + "Failed to decrypt symmetric key for recipient n°%d found with RSAES-OAEP OID at offset 0x%x: %s", + i, + match.start(), + e, + ) + else: + log.warning( + "Failed to decrypt any of the %d symmetric keys found with RSAES-OAEP OID", + len(match_rsaes_oaep), + ) + + if not symkey and len(match_rsa_encryption) > 0: + # Try with rsaEncryption OID + for i, match in enumerate(rsa_encryption_oid.finditer(s), start=1): + # Next we should have 04|82|len|rsaencryptedsymkey + key_offset = match.end() + 2 + encsymkeylen = struct.unpack(">H", s[key_offset : key_offset + 2])[0] + encsymkey = s[key_offset + 2 : key_offset + 2 + encsymkeylen] + log.debug( + "Found potential rsaEncryption OID at offset 0x%x, with encrypted symmetric key length %d", + match.start(), + encsymkeylen, + ) + try: + symkey = pkey.decrypt(encsymkey, padding=padding.PKCS1v15()) + cipher = get_cipher(sym_enc_oid, symkey, iv) + log.debug( + "Successfully decrypted symmetric key (length %d) for recipient n°%d, " + "found with rsaEncryption OID at offset 0x%x", + len(symkey), + i, + match.start(), + ) + break + except ValueError as e: + log.debug( + "Failed to decrypt symmetric key for recipient n°%d, " + "found with rsaEncryption OID at offset 0x%x: %s", + i, + match.start(), + e, + ) + else: + log.warning( + "Failed to decrypt any of the %d symmetric keys found with rsaEncryption OID", + len(rsa_encryption_oid.findall(s)), + ) + + if not symkey or not cipher: + return False + + # We should now have all our octet strings providing encrypted + # content after a A0 80 + content_offset = iv_offset + ivlen + 2 + if s[content_offset : content_offset + 2] != b"\xA0\x80": + log.error( + "File does not match what we expected (\\xA0\\x80) at offset %d: %s. Found at %d", + content_offset, + s[content_offset - 10 : content_offset + 20].hex(), + s[813:].find(b"\xA0\x80"), + ) + return False + + with open(archive_path, "rb") as f: + f.seek(content_offset + 2) + log.debug("Writing to %s", output_file) + # Remove output file if it exists so that unstream does not fail + if output_file.exists(): + output_file.unlink() + + decryptor = cipher.decryptor() + try: + p = subprocess.Popen( # nosec B603 - unstream_cmd is checked for type and existence, variables are quoted + [unstream_cmd.resolve(), "-", output_file.resolve()], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + assert p.stdin is not None # nosec B101 - only for type hinting + t, c = struct.unpack("BB", f.read(2)) + prev = bytes() + while t == 0x04: + if c & 0x80 == 0: + oslen = c + else: + oslen = int.from_bytes(f.read(c & 0x7F), byteorder="big") + # Revisit to deal with incomplete read + p.stdin.write(prev) + try: + prev = decryptor.update(f.read(oslen)) + except ValueError as e: + log.error( + "Failed to decrypt %d bytes at offset %d: %s", + oslen, + f.tell(), + e, + ) + h = f.read(2) + if not h: + break + t, c = struct.unpack("BB", h) + + # We need to remove possible padding from last decrypted chunk + if len(prev) > 1 and len(prev) > prev[-1]: + prev = prev[: -prev[-1]] + p.stdin.write(prev) + except BrokenPipeError: + pass + out, err = p.communicate() + if err: + for line in err.splitlines(): + log.error("[unstream] %s", line.decode("utf-8")) + if p.returncode != 0: + return False + return True + + +def decrypt_archive_openssl( + archive_path: Path, + private_key: Union[Path, bytes], + output_file: Path, + unstream_cmd: Path, + openssl_cmd: Optional[str], + password: Union[bytes, None] = None, +) -> bool: + if not openssl_cmd or not Path(openssl_cmd).exists(): + raise FileNotFoundError("OpenSSL binary could not be found in the PATH") + openssl_cmd = Path(openssl_cmd).resolve() # type: ignore[assignment] + + log = logging.LoggerAdapter( + logger, {"archive": archive_path.name, "method": "openssl"} + ) + log.debug("Decrypting archive with private key %s using OpenSSL", private_key) + + # Remove output file if it exists so that unstream does not fail + if output_file.exists(): + output_file.unlink() + + # Create a temporary file for the in-memory private key + if isinstance(private_key, bytes): + f_key, tmp_name = mkstemp() + os.write(f_key, private_key) + os.close(f_key) + private_key_path = Path(tmp_name) + else: + private_key_path = private_key + + args: Tuple = ( + openssl_cmd, + "cms", + "-decrypt", + "-in", + archive_path.resolve(), + "-inform", + "DER", + "-inkey", + private_key_path.resolve(), + "-binary", + ) + if password: + args += ( + "-passin", + f"pass:{password.decode('utf-8')}", + ) + openssl_p = subprocess.Popen( # nosec B603 - unstream_cmd is checked for type and existence, and variables are quoted + args=args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + unstream_p = subprocess.Popen( # nosec B603 - unstream_cmd is checked for type and existence, and variables are quoted + [unstream_cmd.resolve(), "-", output_file.resolve()], + stdin=openssl_p.stdout, + stderr=subprocess.PIPE, + ) + + _, err = unstream_p.communicate() + if err: + for line in err.splitlines(): + log.error("[unstream stderr] %s", line.decode("utf-8")) + + _, err = openssl_p.communicate() + if err: + for line in err.splitlines(): + log.error("[openssl stderr] %s", line.decode("utf-8")) + + # Remove temporary file + if isinstance(private_key, bytes): + private_key_path.unlink() + + if ( + openssl_p.returncode == 0 + and unstream_p.returncode == 0 + and output_file.exists() + and output_file.stat().st_size > 0 + ): + return True + else: + log.error( + "Decrypting archive %s with openssl (exit code: %d) and unstream (exit code: %d) failed. " + "Decrypted output size was: %d", + archive_path, + openssl_p.returncode, + unstream_p.returncode, + output_file.stat().st_size, + ) + if output_file.exists(): + output_file.unlink() + return False diff --git a/src/orcdecrypt/utils.py b/src/orcdecrypt/utils.py new file mode 100644 index 0000000..f4c2f5c --- /dev/null +++ b/src/orcdecrypt/utils.py @@ -0,0 +1,49 @@ +#! /usr/bin/env python +# -*- coding:utf-8 -*- +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Copyright © 2011-2020 ANSSI. All Rights Reserved. +# +# Author(s): Ryad Benadjila (ANSSI), Sebastien Chapiron (ANSSI), Arnaud Ebalard (ANSSI) +# +import logging +from typing import Literal + + +class ContextFilter(logging.Filter): + """Filter which injects contextual information into the log record.""" + + def filter(self, record: logging.LogRecord) -> Literal[True]: + """Add a ctx field to record based on extra fields.""" + base_keys = { + "name", + "msg", + "args", + "levelname", + "levelno", + "pathname", + "filename", + "message", + "module", + "exc_info", + "exc_text", + "stack_info", + "lineno", + "funcName", + "created", + "msecs", + "relativeCreated", + "thread", + "threadName", + "processName", + "process", + "asctime", + "ctx", + } + extra: str = " ".join( + f"{key}={str(getattr(record, key))!r}" + for key in sorted(set(record.__dict__) - base_keys) + ) + record.ctx = f"[{extra}] " if extra else "" + return True diff --git a/src/unstream.c b/src/unstream.c index f9998e9..d67fa24 100644 --- a/src/unstream.c +++ b/src/unstream.c @@ -172,7 +172,12 @@ int main( int argc, char **argv ) if ( !strcmp( argv[1], "-" ) ) { #if defined(_MSC_VER) - _setmode(_fileno(stdin), _O_BINARY); + int result; + result = _setmode( _fileno( stdin ), _O_BINARY ); + if( result == -1 ) { + perror( "Cannot set stdin in binary mode" ); + exit(EXIT_FAILURE); + } #endif f_in = stdin; } else { diff --git a/test_data/archive_aes.7z b/test_data/archive_aes.7z new file mode 100644 index 0000000000000000000000000000000000000000..514458d7f2513d759bbb9cf020d2bf5dc64e6b34 GIT binary patch literal 4544 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 literal 0 HcmV?d00001 diff --git a/test_data/archive_aes.7z.p7b b/test_data/archive_aes.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..9df51552bc8dc2db88345156a0e3a94be33222f4 GIT binary patch literal 5614 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 literal 0 HcmV?d00001 diff --git a/test_data/archive_aes_corrupted.7z.p7b b/test_data/archive_aes_corrupted.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..2077c72972d79dd6df90df8da18653b399cec787 GIT binary patch literal 2429 zcmb7^do&XaAIHaLZkuGAOD@T6S;l5~P#$8_$aN%HbD8Ek*T>}%au+HplKV9&mzC=z z!l+!L+FY7?ibqC9#p4o*xBhs~dH;CNdCz;!?~m{A`#tA(e&=(}?}teQi=xtr>03|% zVPHifCQ(oTfK3+M!6XaB{FE3kj1^W6BLfDW0RbVB_9P3mE=KoP;jp^8de{RP=&wi| z0=6KKoGnRM2@K?yAqEjTY3Jl*g%!t${_4RnK}(V#97V_oXf&D@U89(ye{6#KdlG~k zeopxJ1_Htu04P}i0DPBZ&d11Z z|HgXdpu&0Z3gHq?;T(jKX_x8~OuMMo9_r^`7 z?iZ!6bfxhDOzWjf`@y^9{9oI4a;lz{}=cpSF8G5-pC6GPs$2i zPVL6TFIDT~RCk@@`S=stgLn5#NGVDP)^Hcm_@Iz7VxVg85%P2P_XIBAaQ{7^+Kq2I z+7oU(`*`*2+hV9zi{#r+bA2R>RuRhsYdj7X(BB=npQ{)Ha)?-RN}}9OktUdDO7c0| zPw+)1<6m`76;CzHy1N0xT{aQ>r4~(Jdd8?zD^KN*HBIJ~hY<=frPwxQ+%!Gn51z6o zA3Bq)37bH809msmytGHSrsacUQJJAeW#c(l^&f=ljfeV}yfkiRm^*s%@|ACJwaFQr zu0};Uy{EQw7l=na$Fvx|yLSsY^^BnF`0s1YJl@{$Ma+L(gaAenEb5s~6rce?0)imP z!Bv|Or<#ki+Z8ahuN0`RA`z4f0?fI@d7^N*oMre!)(0MBGxe{sdR{Wv-LmFxZbSCk z=X3PG1^FY6q==$CtPww~&|*zON8@$-f?Zpxj&@wo(R&A0^V_0d7QPxNPCFNKZ4eP- zNk43sOSTqy9=*~l8utMWdUXy03rhUkq1?o;Lvo%ErTC0?8Z3IM63r~E=kB8OeHAo= zWlJYeI1!oZ33SQ#B2=|oH5w?hP|R>B#;B>k)Yf1&Saj!%99Y+QEUy5QS~b?C>q@)W zhN%@kjBU0|DlzObuu09xRZA(_N#1IWRg!^=VF^iaTmQ;4Qn@SPA zfroNi@$hf$hrwFH?lFXA=@Q#J`5ti`U*`KVT3eT4;+1m6_q#1?gPkUtxpJP{_q^9X z(j?5kU1Aup=ojp`9cWSbAw-fNtFR?%5Z-9KOBu&cwmtHY!d`zOArQvYqY@ww$G4hv zev^A)Q#8WdY!tqQ*n(<>&yeue9_>o| z-ip&0S2M%u9j4t+6zAI2zqlTgxxUlZ>#;sp+3`>*!hFUGexuGvmTek9k`SqY(V{0R zSIv9M3N8~UWG}H=BwNZN|5$lEz!z&{`#^7c$qsf;d#^})VXDqo(*@{Pg z|9*(SAkp7TeV&Jdy$f)q#5UPM0P_kYdout{IPcIAF3)A<_%UN}*q-UU)6QGhes`nR zfn}d|iFk7r3haZ-=Ts?9n`_;d!Xi}Bi=f6%HhrUI0698;nc1r|6}}J7siSH{`O|_L z;gx5;p&G|XZLj!+5x`0{_x;bI(PMQ0aFT_epb;7JeF2t-?{K?r!@_QZ$r{qqv@08n z{L|E#2bufqw&W+a{o-cKrEjQ!t_)Rz$9eBe)Hb&c=5_9r+`U(6c;1K908t68Yw#)! z^q*&I`Sk4Udr%BF+4#83Mfr4STYH9(cNO@GhrU9JR^!JPoQTS|%IwY)MWY)axra>l zUuqF)yRw;oUTWX0zeL`F7ZilIR86eDA!<3rUq8<#6B-WFyPYQsp3P`cAKwi3s3Su@ z-nTt^w1qRJLJ=!})U!?xy8+AC^c%Wu<~oU3r6ns6M#_uGi|9|RLvO3?tA&>pVVz2# z$V^e^-@TBZt z59kt`b-thTmE(B4)kVISYb37s59bJZkf5OKGP6Qo=m`hb26gLC+77Hd-SaBe2(fa; zBjts&JLbM{Q2=qa-K@*P-i#aVHW4Al=|L8Bi0h=UNDVXCL-M9NYnWH$8Q-npFENwUX+t6FYgELmE`7feF|m1Kf^E}zmSrTNTbozK9? zX&WsW_p}qCI|>Kr7iS&1bN#4&mt4YM8Q6IlKVmiPHU&@A5Lk=2xv27%Y9=pBwc6gG z{U32#MT-l0s--QCh8k)|tcF?MODi4v*G$Trw=0NTL7MnmvF5N8-;e~y!ap4Sq(#jt z5lHc-JB-r_-0eebLh<^^F{zKq6ZI$hFtR!^n`O1rI0kaEIKNTfo|t+V{kqf%_emoP z$iiz@FWFC4P=9zjGB2oPMm$v^-Eoog+PCYGqym>9-Cy60e-6*5x%Y$#$PS|ErzQ3Z z+O&y23Nc>>r`bYZ=NI9vR%zhO%dr%BmAqx~kjNvxZcN+9Dc^Nod(od(nf~ZO-2A4H zu5DPI@Iq=9d%R8&6|CEQWlA4Es=l#tN!d1*dX;yGm|bojow;>O3r>-r+!6dIl67lO zn(-zlD;TDW8yXBian`qSqdkLiAmsu+qW3DgKw-||ath=2b)bky1mpe5~` wH)ptfhdj8%02@$MamGSEJxeutDXbe+{uE?7F>c(^R7^Og=?p1Ky*AeTUp)Lsvj6}9 literal 0 HcmV?d00001 diff --git a/test_data/archive_aes_padded.7z.p7b b/test_data/archive_aes_padded.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..47892228e1875f97bc8285ab2179178af68a8467 GIT binary patch literal 5814 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 literal 0 HcmV?d00001 diff --git a/test_data/archive_aes_truncated.7z.p7b b/test_data/archive_aes_truncated.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..3e78af1ff601a9412cd04a817d6f1ce7bac47400 GIT binary patch literal 5000 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 M8(|&ugpZ~F2e52ex&QzG literal 0 HcmV?d00001 diff --git a/test_data/archive_des.7z.p7b b/test_data/archive_des.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..21253b16f839891c7c75cb157d22a76fa1ca0be1 GIT binary patch literal 5509 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 literal 0 HcmV?d00001 diff --git a/test_data/archive_des_corrupted.7z.p7b b/test_data/archive_des_corrupted.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..4574635e3dee4a1d94af07a3737ba8e86c8c0aad GIT binary patch literal 2429 zcmb7^cT5wC7RI411*BzDhJq9ju&lOhWkVSAPB;hp(15SS*^(4>uFwI^8R?q%S+B5=X@tQIr$~uhl~QVBjQctccILzKynl^ z3c$>SiUu4YqnX2gwaA-DGn6n=5Dc^f0U_L2oS_05iT+*6C^T9H#f60aUU5Lch8Uc! zF%HFvg!~rHKv?vxY;4R>97y)xGnfxxj05l^Fo{03niK3BF1iZGJD{$10?XB37ykVL zGcyYlD4Ll`Ry8Tz_tDKW-}&|=d#0^pww2`aMo_>@QVRnQW)NbJSk(D*6E%%5KpjD%9l+nEu?TnZtJ1gNjLI2l0o++4B> z+LvpI4jyA;=#bd;w3)R>{V7VM*OZqT*nIw{b!}U{|o#Q4d-izg=>MY2|Pw8?~{T3 z+?B0n5H~LS?&FTv@8RZ;6kxueME}s(2zbI>Ikn()0-v<=68i2Nu#v?{ZZ$=2h{3EayQc6@uIA7<1O9--a?jx^)-8Q57T2h3>GoSsMq{16WdQput0}&htbCS zow7&y=kpc91j{FZtFeCZFZ2z(ckQKzn1-vcF z6%HvRM}eY2OsuI9Wfd#igEuc(_6gW}uWJM(=$Pj}^FoB}vxZqxBUxq<>oRMh#m~>^ zrB6uHdH-U0pun7N zTrVL9Io4cSJJ3hJhKt%Pf_UVDPasmBRwi5a(&ggf!r3x2G6D$GdieJEfV(%2b2+Qe zRE==g0@UnaQ)M${536frs}{NCO>j6jyxlifl-{Mf0<|q*c^MM0NeWFMa|~xrY8ntb z+iKIqWs5|%e&E!Q*OwPLr@O7gtE<*kxsoy|j&RV=nSzA~8@ORCq(XSR*wjcjW9D1w zWbd5WZ5COyV=ue#E9rzhGok`{>XMQqFTA=|ff4H>E^EIg(`3?p&2D>=d&l*H%5d{& zp{f3&8WE$suMF*vQA|gm&VH$`-f0yz_7m1dX=j@x&X9>11Szn+aT=|K@!%02FmL{P zot_~zIKlbDAc(i-wYgVg_PYiW;`3_%#W*L>MW|ic08~{`g~!|RO_`x35gtoQol2Kr zr7gM68osS(b1*(!QAb`g(0dbnR4GOfoc-WJJl_L5by?fBCG?pLyA80f7@nKe*lmJ^ z_%L6fvpuNo?Yxw#E(jpAt@_7bGg}iF8;}noKJcUhLMw<n)qra{!j;LA5(_-r4U78wkAO&79?44jx=n9 zJ`qA&K9?IkO3PCTSyg;8Tlx6w!&Zsgy&_|e8s@j5R~wvT|0!^lvp#`(!*4HL z-mwR-Pu+Wj6dk?^Sz~jwsz+tt>|gOqR%`G0e(@>MaQ!u z2tcQkC8$%@MqSO4M%7T&Bjy+S!bv0ZsfA5<+9(!Bc%2NrmQwqB(IuI~jHAJ~U%h17 z*2e=M#uenp+GIHRsO`lWHW1f-}M7bM3}C^_^+YX{#do z*>$;8w+{E=`uOTaF`zh%rH0((FN^=vhV6jeL+SEKphQov2MJO7caHOM)g+@6gTXG~ znt(7*QT1AWb~9Y*T2Pfy(n{RlIm-8wg9RdQ9U4Q&34>Vsm_}TL?kTHpoLjU7iL!rW zWNB#9>*+)F*yAs~k~hYuD4FH;u^-ZG(QNWm3U-B)cikpX`_8fY?+hq8VC3)TL<%>MqkGRHj|9*-o>3A{fd(2`nggF*tos#NlG4nn-=d#uWApAEu*7 zSm&UBR9qtB>pBxJ7#?2~P(#To`j%gdsay2_bBXhI??9)-jR;l~`1I32Oa5qZ4}_^@ zT&^!f5aLF>E)tmcLtZvaZ^nk2*)Z#Lt>I4>p754?8{4B_vZu=j?myOX+4eDaMWK`W z>oOrvs@v|4>G$-0iKSP;-0y?G{(MS0*a>k_by81W>xl5X_2kRuy4t?&ULQJ!?0oD?W8#ni44{;KObm zB~ZT$vJH5qSlKa~{=tgYgKeRX&M5T;hmN%Jl{f4MWlzuyQhf6n21=q2%YB8C<)zz> wh91ZHAF8x<2NhAkmj0>{&e4**iglq@IqpK2gsv;Y7A literal 0 HcmV?d00001 diff --git a/test_data/archive_des_padded.7z.p7b b/test_data/archive_des_padded.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..3ac207a90c3b033c16dc4838e4e3f8a784d5b0b8 GIT binary patch literal 5709 zcmds%Wl$6hw1#1K0fD7U5NT;}caak5ZrBAx8WE6`Zdh`W?v|477L*PtDQRh>Ls(c^ z;(p)Uxik02ow;-W-ZSUNiJ3EJ-kJA-1mfazqm)pWMA&%1yg*1G02>P$47h;=V|)Ko zLd+q`P+ABLE>IT)1e0pQW%*zb*uNQ%gb@6Hdt3^D0vtfeE&tKBN^}AL)L4@5?gG@?iNw+R=fZz|fQ^lV1q#N-;uMTX za0oId_)T%e+P|tDaxTETQ_p>aeF8HZI=ZH52F>0GF#koc8B7vO3Yp~{WV7ZoonaYD zx5Yva#2Mom9i<_BZ0%DrR~aK3lhD) zyY4T8%&I|r)DDwR-;10&rIVJfY#ZJ)FX>u8{_WtjXW4!G%QMy2%osn ztoPfrA8c6k)tu?I`<|x!e&S$RBxlCYb#hH)y>*H8=%~h>s++xA?sV{?DBys)>JDUs z(SH6`dL?j!!KyxO83^yFl()0A%Ed52{+lDPA(Xg}{?i^nJkTQ&pxsjvKX6W7ASf7w zg%|5zRPyJ1)cmFT5S5P2iHK9UgzBd+{z#p}Y031iAY-s-Fk9kjvmcR@>XiC;&*fC-cy)LQpRRU(rn8zbVLbHK=@v zzLAD~VSb9(0g>^z--Fq#G!)PD*o&E%Xj3@WIH
    X^@GN8Z`MiT9F+l!TDZYM@qq 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 literal 0 HcmV?d00001 diff --git a/test_data/archive_des_truncated.7z.p7b b/test_data/archive_des_truncated.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..4e34c3ba879f62c46bfec47f2215cba52ad1fa25 GIT binary patch literal 5000 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 literal 0 HcmV?d00001 diff --git a/test_data/recipient1-key.enc.pem b/test_data/recipient1-key.enc.pem new file mode 100644 index 0000000..e1fb603 --- /dev/null +++ b/test_data/recipient1-key.enc.pem @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIc0EFr17/xiICAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBD7iRy82th8uUHSaEOgqEZNBIIE +0E3FPOVwJs1cjkj6gmzVMxLSDWlrkz7ndR3YFCTg2ktLuefvav1nRu5rDhq3YPJP +MzofybExvOTq3oXLtXh9P4yeMtpN7KcklvHJ+8UG1xEHSncTmFGyq8/U1B2spfx+ +m3lKTqE4L79j6H1PG4i9WL+KFauqL7xMPd1fKux7U3zMeqMLV8VaRIGbxThl9cJW +cQH4draXLezclhdFQRVU14Ga+T4eULfjge/B6iUWDkpyAFw2ymJQ6jspdq7Zq1FC +TrCDBHVQqu1Bt/7JWgCxZMDn1LDPsBwf4XPGCSpZ+kbU9uLPKUdT9qGqYWIVTVnb +sDpsGqOjbYBCpRqUCfMPKXGF683A8QLDFCdQhcdTubk/wUUjDP1aHg8on3lHokJ1 +29/Zs7zqocNCqTwHVbVXY2Bk3EOtrOy5nDnhqAo6+0sittION/F4dgI1DOZXYp5J +k6016Mn6XAUNhrvH1tVhXb9UWsdvNof7Sk/9e+H4Nhk1d0j8Hm3SBVL6SL29z8oX +aK51E3VzepaMmQ8cq7nHFXezXvWV6DR2hEd68W2o0h0FJBDIHQ6jenfPIc6/yi2K +TEY0plEjQaAVYuklaEjS5I37f3w4hOpvWNOQ0MlepZT2c4iWkk1hN3S00XVOW0As +FexG9Dp0RmYgyQPoQT8ifv/GxJNE8iPiksDjolah7+iZhKupxVMtcjNhK1LcqfrD +Lp+T+QZ+RqilfvqtyKGGDG/mm/E4CtgBC4Es/99hm6t9FHJbqX3Sd+vvCKrKMTZS +vY6q0s9kCIegMz4n9E1RY4vacDx/hRYK5/naDF4q8cCT46bYf50wwWdOFk/TphC2 +vVNVnngGHFM/r+2SrMYGZoZcfVqow5GxZ3SIHkrWgs77uj15W09WTS93fZIv53VX +LzkqmKMN9mBUx47uVGFqBBee9ObYXpIJaW4/C3kD6gj7LkVtMYypqf+9OSlG4WxD +MdfSBrW2DxpxpeAjXOvWEPGDrUZv9ky31AFXjL91MuQeopBVU82MCpnPRDuqTGEP +pURWl8zEyyF9ioMIrY28ph4rFGiC3YJC0Qf4VIBxTPDkdPSltExjEgrVYSX0qtg8 +ApBe/oKHUipQKQl9R8/Xc4FzUfeghfJA9YC3sxIu0MyG+V6pLwJqGvjp8n8miIC4 +flTaCdiLzQHcSsbi2LK1MLNFF+qx+8Efc3ih6czRNF6wuEuXZ1QM9kDv3+iHegFe +XPW2p9tkDinSUiOphZhhlIZRXYCBMESblj8f0DCrIBOY1dHWLvhn77CPxD08jXpj +5n9iutoE9UWL044GRQHvKdsYSvuOrQLZ1ddL3NQIx/z7qRKH0OcNzClH4GtQpFCQ +dhTXu1P4DzVc6j/vB8ltmnrZcJrH6wE8uvTdYtvq8HjGe6GXZngS5qJLXGhvNbvn +wt7TAhvMDBJiwcsjWWgw1iOKcF//FGL5R/hggKoHUwqdAjCoFfon1ffzRJKiGLO5 +GXGL4popLZBV1H0fNvuUQBwiv5kjxjQ2gfWWz3XsUSkxq543ZJS75s0JCHddciBc +em6iljmYewsL2NurEsaVGQyii8AVpGQPgdZ/ic3+Fd4RoJekrUhq74CLTQPMqWiF +q/gDpi4/PbRsbcyOLFaB4I6YqaazxJjSTMbUreoheVLq +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test_data/recipient1-key.pem b/test_data/recipient1-key.pem new file mode 100644 index 0000000..08e63c3 --- /dev/null +++ b/test_data/recipient1-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCjNBlJOHc8/SBX +bdGFj1Q0bBhsMzkzqP7lf7mAZovQRPaT1gwe69UU+1K800jAPqH4Rt9oNG3jC/cm +2+DLaNa41nnc6D2lRnL0+sVO6lbecmY9Rw9Sh6jMEnU4fzBAcCRc4KZkvmpIQsTH +ZHjivjHVHWeIeQJiSpEjolKDNJZL5SArmFokwq8RvGKa6kesOFX8f9jaa0ISfW3h +Dhx1QRWkYH2DV/2fqMNbU/2DPTECF+zxYCxfYFHT0WdDFCDu9QIh9mYSgB018sGi +ZDrwndSq9sHX2clCEJ3NnAnIGEe6JnQ/rEEZXT9wVlVRePD+EwQVJe1iOLEUTmN2 +yjg1ZtpJAgMBAAECggEBAKFeQiJD0qJbJj9MNn742Sl8OCnD/Cs4TdBeGez7eALW +LXi/i/yG8olsdsJ9ptFvHHeAnCVMsdptWlLx1bNKVgUtDBGBEHL61W+lBLKiwoHw +W2b7fAr+V8hv97eFCxCr0UiEWAIExNHuMuN0VJLdvCgciuJFxDWrxRaWyT8yH+mp +b2g4TPGZ7xH77lHSZP367gzlb2EiNKCW3QtZbmx3nZwccEwnWDvWbFXFVI2IqNnN +FQMSbjVU775uug+JzecvR2iaavQCt+KkWHloK3iemfzPY6sOZHTruzHwmO2ZoJ1t +a7Ju8Xrl8X0RtR+66XyMPxCgINBNqw6pUo0e3f7cFoECgYEA0hgHhegncvN1xI+f +a5oUoh+Xbo6xv2IDYVv13+pqWxobdFqpqDCITkbE+7HZ1lO/h7JWz9rz7ghG/kcc +ViErGCtr6L5MfKVBVQM8WpAlMNTLAV6tHlbEkMeqTnBepK3Aal8RXjyNfBiq0qgN +PHo2R9sXUNFvovbxTodBfdhywXcCgYEAxt0sv1afJrTMXkYzgh8pN7AUC3R9H0/5 +HC/s5iyh2Gx1R3Ms3NErrSmZ1qJe969FHwT7AfhWf8ijmOd5ANgqkT+auG+1CLbC +ov23Fz1pxn5mB4sOA0YApBqhaY5kP1atjl2nw/oswlG3MhBDV0BEq2zhiYmmDiET +X9Bds4ugMj8CgYAsUolzxJBd/eLAfxRA3RaxRTzrRAtXttPDvGTYwlmBsrZMC7xz +ERoQeXmhJ9ovDyf+9q691xFTDEf96P6fZQv0Y2S2iz8TpMFtr+sRqAtQi/Pv7AtV +tTRu3tCdD7PHxigryLafTOMEZSfUnUN9mMLO0ffPQv/sP3CVAo/cfsdm7QKBgQCn ++60Q89r8lz0LZcGc6TWoFNTZ2EzZZnTHmrRCuvD8IKHw+RmsbgS3Aa0x4XbXQvbg +fRSLVXu79YA8aUuNqwxKJbBMnBAQjFFd3XQL7ZSsV5lYRd5QZZGlDdnLkLydxFpX +KEXPBkVI4D4fzB0WVvOq2w6pX90lkksLZLfCMu/fgQKBgBtsEdRwQRgta44GtIIo +GuIZoIa+5INk1dj3stisQwoKqGHRUAAJACY/c/T0OwTlJjwKTMGb5b4J1au4Ntkg +mZSjym3ezKto7vGYGm6xaPugTO1EaXZ1gCUH2S4mM408H8sW2pwHj5Wpm+FpT6gM +EVPx2hANX64KC7SeRyAv07G6 +-----END PRIVATE KEY----- diff --git a/test_data/recipient2-key.pem b/test_data/recipient2-key.pem new file mode 100644 index 0000000..35e96bd --- /dev/null +++ b/test_data/recipient2-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD4/sbYj7gecdDy +xUxG4X9Rwv7XC0UY+75J4JHvPWp8R6aRboNIiMJOWvdxVAs5U9xucA1O9l8S9z07 +QTwUmfjJAZT6qz0Sm0ReUlb/Nl6wyMlpIPrJVOafWwRI5Mtfbzlc/uFnOg/WSzqW +A+K9XalBoBnKkNhc05FtW6TSVuxOuCX6PfOKLYQVIxoP1xQAidtU3rKbY9ED3f0O +3RV6mvJKKVXAExUC2AGzTYEFkG0v06je61M4UuSsNFw38VcbHl/C6fScGzXQuhS/ +/xWLppmlAgS0/cWpW2fLsvLGol5oGNDMb9LkNmbL+oRlRBrmObr7qKZ/4nU2qnKk +mnFC3pvPAgMBAAECggEAdusXhO5+/OtW49ue5mtkJ2R92a6kPZ5d72K10BEA/oIp +52NkiAvo4QtJCnMdOS/4IzaY5JF+LsyuDdLGIwC2dSmwQVfvPyewq2uf6RvKdZwv +tWnu3KET1B/gGABNsTKpruR8OpTcY3hibqtEazSEVbdlGf27l/njhtsyCNNC4QTY +1H7dE1BJTkfxpR7/hpmlLkgbzX0BA0gaSVg3Pt8dH60Is+aKPMoTjh9M5OabLGP6 +AYC1ylrKfmehpjXsHg5NEN/avCvC/W5tnaCVfn4ttBIBRJzSZC3EyVdQH9mtCYjL +ssJotL7MUg5tsHXONRVVrshvDuRnyJpTyPrJXw7SoQKBgQD9Xc6MmEsKN0Ub2klY +0tvNFVbsML+PL2yUNr0cuNZubnHfie7dP+qRF8KUD9joTJuNs3OD9U6uPdcZCGe7 +Kg4y1bXjYkdkAiAVCsPVKT/8vy79CE7Onvq3pZSb3QTZmAnuwfa7h7dn3lx1hzM1 +2jxQoBEupta2iVnr3D3s2IMm8wKBgQD7lVafn4+aKzJeBk6VrFhoacF36yzCVakk +6inY1XnTROE8jxGeKrtREW0x9JvO7mo0+GjMhXUKvvVTCagEAqT5Nbn5xMAZUE66 +WazQkd+5gWwg2Bzdct7ELZwNxGcjW/fO8EiDTBOuA/CRrHkjP5YTqQuHOs39Drl5 +ovqlPzQmtQKBgEhRSC1VIsYNtIoYqirSNK660o6j82lZLfK4FQ5nq7zlhVc6tZUt +MzSqxX0hcNYNV7EAcY5OrGCLgiPBBGqlvFvCKCJ44Muo16Np6S+kkIEByNWsU2Ii +eomEl4BNe57oCJ2/ez9Xo3/gNh/NsU5DtkeFCKJ6OHrthV8PBBXgTr8FAoGBANvW +JAp5kC8kPe/hTPwGzH34o4EZNFRgK9xA7e42pCyTjMmxLrDqICJ3utYjK58HwDCn +PFQy7hJEyPQVhQZ0FRpa8zao0T+NaC34PEVIoplj2Utd7OMpBDWisTFqhMFmFMV0 +O7jXWn79nFJ+xp+haY+mcJdrMCJHopUCcEjDUNvdAoGAc1K93YNfyQaBG/IvWIwd +hwoNAblyxy2Us8dye+LjGPExRi4N1Apg+9Rhwczz2CX5GqUUR9JXpX9Lg6cnQExb +cQRxzIXFMysfhhcGt/VHnCwZMJYnDR0xisjzfY0JwEZABSdY4Tx4836kOqnb5yYu +kZNvU6fp8r7fVl5Y0798QFc= +-----END PRIVATE KEY----- diff --git a/test_data/recipient3-key.pem b/test_data/recipient3-key.pem new file mode 100644 index 0000000..d02c119 --- /dev/null +++ b/test_data/recipient3-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDwfOkQ8kxK6ybk +Gglmuq0nKEdy66VfKdJlBMUS0oasZFlAtHeRgEX2dvswopgaeW5VJsLa45WmrQPB +i/nKJ4BfQZQDYObu5JxqV6YpjkgYNxjJ6PTE44lsJqe71vEFQWTtrwXcMWJ0uiVX +pLof9aZiD31gZcuZh+Xd9ZTvnp+bi6NOeIqbtPBJJeUn0KnF5qnD0OTJIvPkPsGH +oXjhQlOCOvGDQGT8XmDigIVxxnjAdl8qhqWuCNiFH8WNAv43fAaha9+Gs8w8N4Cb +iROceD4bC9XNt6IAzTM69JXEgydks/uBLElwspWy0NN5HTf5B4bVraom3WzMqyZq +OdpBcqCzAgMBAAECggEBAL4rCGJON+ZGbUqTDDwgAiykvVsy3GKUP7uCOhTYRYat +E6cHDkYQmUJ8c8XRzVWiEI1lSVCuBvj2d7HlbnFdKzYoNVM2nWbrgITXKp5R1NDR +QNjpTiUjiNfs+VagcZcmTxlk/c1Rf/mt+TmFGWmMZzXD6fEAji+qNyt9t3iEhtIH +4VoAv+5FWsz6ywk4Z5evzUM5Mtwuthjq6c9aVZmgSdPt9I+F47ekgfOqMNukEXCq +1iS1l4Gpo/m6ujIC3+D76tPmEzatXcXZrm5K9K4liNBlJkHpsI7k/R4IJHxxzcAT +0KWlXpEe9d1+YcoEcWk/rKE0GEo5Wt6/fUGBhZt+kQECgYEA/xzuSCq0/CjjbS+8 +dqPtPdpe/jHJYxzhJaR+UfMnTGKo6BaUL4x8ohY+qomFwX3jdpbNipHCpyi2lSh5 +EiRs+fycWwwDKF2+hvIiRkquR8xdzVxvln9DbFw4evjkYiVLtNtgqdVXsj+bt7Sw +r9XslevSgWnFv2rYnV0szG1+7FkCgYEA8VL2VS2OLMioyBetNfpC5rhUyQFAV3B/ +0YCjFozIQmFKYzkClHOOUGwWrb+ahBmSeswY6sh9VSXhx5sq6SGLFCc3fqWGaZpi +ljGxjlx8ybOKbv9Pio3/o+u/9wxR+bgCeRr6J8FODM3DOFZLhFrSHoNrv919Sxlz +3nwaiL6ro+sCgYEAgOu+4wtqANAs9j2ccRwwRQS44p6IViT/BoXVLFbDsk9dakQW +yNynE0ZIjugGhxy2OXTGFFPK2ayycDhOzsNHqyFkZoJwihKtuQZeGcWdwzzc3m3r +GlPf37/O7x4eVBbi5lfCxrDAq5yHddPDQmjKMY1GCQ5J140IQKYYgIqJDKkCgYBh +kZpYy+dcwfBDnhcA6OMdp09YSXI7KBf1m13U4yygcfeCcG1TmfjjGSB+NSaC3Ff1 +4Aj++/p4b61+Z4UM5uv1RPnR8ZiLn8jWUtcn6MrnPfjtcbo2Gb1PCCT//HI0Vapi +Tn7vjd9Bm/ufDnzP0Wx8u8PXufRLZcoMHP8ZZIW+6wKBgQCwFP4IFu+p2GuZ7ZPk +4ScSvSDCyZ4oenFjYSc6ZXODPY+k310oBkgyQqOTi/XOWGCfoT/HZ5q4OU72uXy0 +K/c7IK6yYUpgfqLBjo6urr6c/IXqr0scklp6F+iRAWZVJCl7A2DNUz0Qj5AyB4Ir +T+irzOIKKrwuAdsncx1Gn2iwxQ== +-----END PRIVATE KEY----- diff --git a/test_data/subdir_a/archive_aes.7z.p7b b/test_data/subdir_a/archive_aes.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..9df51552bc8dc2db88345156a0e3a94be33222f4 GIT binary patch literal 5614 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 literal 0 HcmV?d00001 diff --git a/test_data/subdir_a/subdir_1/archive_aes.7z.p7b b/test_data/subdir_a/subdir_1/archive_aes.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..9df51552bc8dc2db88345156a0e3a94be33222f4 GIT binary patch literal 5614 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 literal 0 HcmV?d00001 diff --git a/test_data/subdir_a/subdir_2/archive_aes.7z.p7b b/test_data/subdir_a/subdir_2/archive_aes.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..9df51552bc8dc2db88345156a0e3a94be33222f4 GIT binary patch literal 5614 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 literal 0 HcmV?d00001 diff --git a/test_data/subdir_b/archive_des.7z.p7b b/test_data/subdir_b/archive_des.7z.p7b new file mode 100644 index 0000000000000000000000000000000000000000..21253b16f839891c7c75cb157d22a76fa1ca0be1 GIT binary patch literal 5509 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 literal 0 HcmV?d00001 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..340bad8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,217 @@ +import sys +from pathlib import Path +from shutil import which +from typing import List +from typing import Union + +import pytest + +archive_files = [ + "archive_aes.7z.p7b", + "archive_aes_truncated.7z.p7b", + "archive_aes_padded.7z.p7b", + "archive_aes_corrupted.7z.p7b", + "archive_des.7z.p7b", + "archive_des_truncated.7z.p7b", + "archive_des_padded.7z.p7b", + "archive_des_corrupted.7z.p7b", +] +methods = ["python", "openssl"] +recipient1_key = b"""-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCjNBlJOHc8/SBX +bdGFj1Q0bBhsMzkzqP7lf7mAZovQRPaT1gwe69UU+1K800jAPqH4Rt9oNG3jC/cm +2+DLaNa41nnc6D2lRnL0+sVO6lbecmY9Rw9Sh6jMEnU4fzBAcCRc4KZkvmpIQsTH +ZHjivjHVHWeIeQJiSpEjolKDNJZL5SArmFokwq8RvGKa6kesOFX8f9jaa0ISfW3h +Dhx1QRWkYH2DV/2fqMNbU/2DPTECF+zxYCxfYFHT0WdDFCDu9QIh9mYSgB018sGi +ZDrwndSq9sHX2clCEJ3NnAnIGEe6JnQ/rEEZXT9wVlVRePD+EwQVJe1iOLEUTmN2 +yjg1ZtpJAgMBAAECggEBAKFeQiJD0qJbJj9MNn742Sl8OCnD/Cs4TdBeGez7eALW +LXi/i/yG8olsdsJ9ptFvHHeAnCVMsdptWlLx1bNKVgUtDBGBEHL61W+lBLKiwoHw +W2b7fAr+V8hv97eFCxCr0UiEWAIExNHuMuN0VJLdvCgciuJFxDWrxRaWyT8yH+mp +b2g4TPGZ7xH77lHSZP367gzlb2EiNKCW3QtZbmx3nZwccEwnWDvWbFXFVI2IqNnN +FQMSbjVU775uug+JzecvR2iaavQCt+KkWHloK3iemfzPY6sOZHTruzHwmO2ZoJ1t +a7Ju8Xrl8X0RtR+66XyMPxCgINBNqw6pUo0e3f7cFoECgYEA0hgHhegncvN1xI+f +a5oUoh+Xbo6xv2IDYVv13+pqWxobdFqpqDCITkbE+7HZ1lO/h7JWz9rz7ghG/kcc +ViErGCtr6L5MfKVBVQM8WpAlMNTLAV6tHlbEkMeqTnBepK3Aal8RXjyNfBiq0qgN +PHo2R9sXUNFvovbxTodBfdhywXcCgYEAxt0sv1afJrTMXkYzgh8pN7AUC3R9H0/5 +HC/s5iyh2Gx1R3Ms3NErrSmZ1qJe969FHwT7AfhWf8ijmOd5ANgqkT+auG+1CLbC +ov23Fz1pxn5mB4sOA0YApBqhaY5kP1atjl2nw/oswlG3MhBDV0BEq2zhiYmmDiET +X9Bds4ugMj8CgYAsUolzxJBd/eLAfxRA3RaxRTzrRAtXttPDvGTYwlmBsrZMC7xz +ERoQeXmhJ9ovDyf+9q691xFTDEf96P6fZQv0Y2S2iz8TpMFtr+sRqAtQi/Pv7AtV +tTRu3tCdD7PHxigryLafTOMEZSfUnUN9mMLO0ffPQv/sP3CVAo/cfsdm7QKBgQCn ++60Q89r8lz0LZcGc6TWoFNTZ2EzZZnTHmrRCuvD8IKHw+RmsbgS3Aa0x4XbXQvbg +fRSLVXu79YA8aUuNqwxKJbBMnBAQjFFd3XQL7ZSsV5lYRd5QZZGlDdnLkLydxFpX +KEXPBkVI4D4fzB0WVvOq2w6pX90lkksLZLfCMu/fgQKBgBtsEdRwQRgta44GtIIo +GuIZoIa+5INk1dj3stisQwoKqGHRUAAJACY/c/T0OwTlJjwKTMGb5b4J1au4Ntkg +mZSjym3ezKto7vGYGm6xaPugTO1EaXZ1gCUH2S4mM408H8sW2pwHj5Wpm+FpT6gM +EVPx2hANX64KC7SeRyAv07G6 +-----END PRIVATE KEY----- +""" +recipient1_enc_key = b"""-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIc0EFr17/xiICAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBD7iRy82th8uUHSaEOgqEZNBIIE +0E3FPOVwJs1cjkj6gmzVMxLSDWlrkz7ndR3YFCTg2ktLuefvav1nRu5rDhq3YPJP +MzofybExvOTq3oXLtXh9P4yeMtpN7KcklvHJ+8UG1xEHSncTmFGyq8/U1B2spfx+ +m3lKTqE4L79j6H1PG4i9WL+KFauqL7xMPd1fKux7U3zMeqMLV8VaRIGbxThl9cJW +cQH4draXLezclhdFQRVU14Ga+T4eULfjge/B6iUWDkpyAFw2ymJQ6jspdq7Zq1FC +TrCDBHVQqu1Bt/7JWgCxZMDn1LDPsBwf4XPGCSpZ+kbU9uLPKUdT9qGqYWIVTVnb +sDpsGqOjbYBCpRqUCfMPKXGF683A8QLDFCdQhcdTubk/wUUjDP1aHg8on3lHokJ1 +29/Zs7zqocNCqTwHVbVXY2Bk3EOtrOy5nDnhqAo6+0sittION/F4dgI1DOZXYp5J +k6016Mn6XAUNhrvH1tVhXb9UWsdvNof7Sk/9e+H4Nhk1d0j8Hm3SBVL6SL29z8oX +aK51E3VzepaMmQ8cq7nHFXezXvWV6DR2hEd68W2o0h0FJBDIHQ6jenfPIc6/yi2K +TEY0plEjQaAVYuklaEjS5I37f3w4hOpvWNOQ0MlepZT2c4iWkk1hN3S00XVOW0As +FexG9Dp0RmYgyQPoQT8ifv/GxJNE8iPiksDjolah7+iZhKupxVMtcjNhK1LcqfrD +Lp+T+QZ+RqilfvqtyKGGDG/mm/E4CtgBC4Es/99hm6t9FHJbqX3Sd+vvCKrKMTZS +vY6q0s9kCIegMz4n9E1RY4vacDx/hRYK5/naDF4q8cCT46bYf50wwWdOFk/TphC2 +vVNVnngGHFM/r+2SrMYGZoZcfVqow5GxZ3SIHkrWgs77uj15W09WTS93fZIv53VX +LzkqmKMN9mBUx47uVGFqBBee9ObYXpIJaW4/C3kD6gj7LkVtMYypqf+9OSlG4WxD +MdfSBrW2DxpxpeAjXOvWEPGDrUZv9ky31AFXjL91MuQeopBVU82MCpnPRDuqTGEP +pURWl8zEyyF9ioMIrY28ph4rFGiC3YJC0Qf4VIBxTPDkdPSltExjEgrVYSX0qtg8 +ApBe/oKHUipQKQl9R8/Xc4FzUfeghfJA9YC3sxIu0MyG+V6pLwJqGvjp8n8miIC4 +flTaCdiLzQHcSsbi2LK1MLNFF+qx+8Efc3ih6czRNF6wuEuXZ1QM9kDv3+iHegFe +XPW2p9tkDinSUiOphZhhlIZRXYCBMESblj8f0DCrIBOY1dHWLvhn77CPxD08jXpj +5n9iutoE9UWL044GRQHvKdsYSvuOrQLZ1ddL3NQIx/z7qRKH0OcNzClH4GtQpFCQ +dhTXu1P4DzVc6j/vB8ltmnrZcJrH6wE8uvTdYtvq8HjGe6GXZngS5qJLXGhvNbvn +wt7TAhvMDBJiwcsjWWgw1iOKcF//FGL5R/hggKoHUwqdAjCoFfon1ffzRJKiGLO5 +GXGL4popLZBV1H0fNvuUQBwiv5kjxjQ2gfWWz3XsUSkxq543ZJS75s0JCHddciBc +em6iljmYewsL2NurEsaVGQyii8AVpGQPgdZ/ic3+Fd4RoJekrUhq74CLTQPMqWiF +q/gDpi4/PbRsbcyOLFaB4I6YqaazxJjSTMbUreoheVLq +-----END ENCRYPTED PRIVATE KEY----- +""" +recipient3_key = b"""-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDwfOkQ8kxK6ybk +Gglmuq0nKEdy66VfKdJlBMUS0oasZFlAtHeRgEX2dvswopgaeW5VJsLa45WmrQPB +i/nKJ4BfQZQDYObu5JxqV6YpjkgYNxjJ6PTE44lsJqe71vEFQWTtrwXcMWJ0uiVX +pLof9aZiD31gZcuZh+Xd9ZTvnp+bi6NOeIqbtPBJJeUn0KnF5qnD0OTJIvPkPsGH +oXjhQlOCOvGDQGT8XmDigIVxxnjAdl8qhqWuCNiFH8WNAv43fAaha9+Gs8w8N4Cb +iROceD4bC9XNt6IAzTM69JXEgydks/uBLElwspWy0NN5HTf5B4bVraom3WzMqyZq +OdpBcqCzAgMBAAECggEBAL4rCGJON+ZGbUqTDDwgAiykvVsy3GKUP7uCOhTYRYat +E6cHDkYQmUJ8c8XRzVWiEI1lSVCuBvj2d7HlbnFdKzYoNVM2nWbrgITXKp5R1NDR +QNjpTiUjiNfs+VagcZcmTxlk/c1Rf/mt+TmFGWmMZzXD6fEAji+qNyt9t3iEhtIH +4VoAv+5FWsz6ywk4Z5evzUM5Mtwuthjq6c9aVZmgSdPt9I+F47ekgfOqMNukEXCq +1iS1l4Gpo/m6ujIC3+D76tPmEzatXcXZrm5K9K4liNBlJkHpsI7k/R4IJHxxzcAT +0KWlXpEe9d1+YcoEcWk/rKE0GEo5Wt6/fUGBhZt+kQECgYEA/xzuSCq0/CjjbS+8 +dqPtPdpe/jHJYxzhJaR+UfMnTGKo6BaUL4x8ohY+qomFwX3jdpbNipHCpyi2lSh5 +EiRs+fycWwwDKF2+hvIiRkquR8xdzVxvln9DbFw4evjkYiVLtNtgqdVXsj+bt7Sw +r9XslevSgWnFv2rYnV0szG1+7FkCgYEA8VL2VS2OLMioyBetNfpC5rhUyQFAV3B/ +0YCjFozIQmFKYzkClHOOUGwWrb+ahBmSeswY6sh9VSXhx5sq6SGLFCc3fqWGaZpi +ljGxjlx8ybOKbv9Pio3/o+u/9wxR+bgCeRr6J8FODM3DOFZLhFrSHoNrv919Sxlz +3nwaiL6ro+sCgYEAgOu+4wtqANAs9j2ccRwwRQS44p6IViT/BoXVLFbDsk9dakQW +yNynE0ZIjugGhxy2OXTGFFPK2ayycDhOzsNHqyFkZoJwihKtuQZeGcWdwzzc3m3r +GlPf37/O7x4eVBbi5lfCxrDAq5yHddPDQmjKMY1GCQ5J140IQKYYgIqJDKkCgYBh +kZpYy+dcwfBDnhcA6OMdp09YSXI7KBf1m13U4yygcfeCcG1TmfjjGSB+NSaC3Ff1 +4Aj++/p4b61+Z4UM5uv1RPnR8ZiLn8jWUtcn6MrnPfjtcbo2Gb1PCCT//HI0Vapi +Tn7vjd9Bm/ufDnzP0Wx8u8PXufRLZcoMHP8ZZIW+6wKBgQCwFP4IFu+p2GuZ7ZPk +4ScSvSDCyZ4oenFjYSc6ZXODPY+k310oBkgyQqOTi/XOWGCfoT/HZ5q4OU72uXy0 +K/c7IK6yYUpgfqLBjo6urr6c/IXqr0scklp6F+iRAWZVJCl7A2DNUz0Qj5AyB4Ir +T+irzOIKKrwuAdsncx1Gn2iwxQ== +-----END PRIVATE KEY----- +""" +pkey_password = b"123456789" + + +@pytest.fixture(scope="session") +def test_data_dir() -> Path: + return (Path(__file__).parent.parent / "test_data").resolve() + + +@pytest.fixture(scope="session") +def unstream_cmd() -> Path: + if sys.platform == "win32": + unstream_path = which("unstream.exe") + return ( + Path(unstream_path) + if unstream_path + else Path(__file__).parent.parent / "build" / "Release" / "unstream.exe" + ) + else: + unstream_path = which("unstream") + return ( + Path(unstream_path) + if unstream_path + else Path(__file__).parent.parent / "build" / "unstream" + ) + + +@pytest.fixture(scope="session", params=archive_files) +def input_file(test_data_dir, request) -> Path: + return test_data_dir / request.param + + +@pytest.fixture(scope="session", params=methods) +def method(request) -> str: + return request.param + + +@pytest.fixture(scope="session") +def valid_archive(test_data_dir) -> Path: + return test_data_dir / archive_files[0] + + +@pytest.fixture(scope="session") +def output_hash(archive: Path) -> str: + if archive.name.startswith("archive_aes"): + return "edcb3c7bf0901ba2d8581179bc8a69dc26bce571" + elif archive.name.startswith("archive_des"): + return "8ad404def35c4ed554e350cb36e331a779901052" + else: + return "deadbeef" + + +@pytest.fixture(scope="session") +def valid_recipient_keys(test_data_dir) -> List[Path]: + return [test_data_dir / "recipient1-key.pem", test_data_dir / "recipient2-key.pem"] + + +@pytest.fixture( + scope="session", + params=[ + "recipient1-key.pem", + "recipient2-key.pem", + recipient1_key, + ], +) +def valid_recipient_key(request, test_data_dir) -> Union[Path, bytes]: + if isinstance(request.param, str): + return test_data_dir / request.param + else: + return request.param + + +@pytest.fixture( + scope="session", + params=[ + "recipient1-key.enc.pem", + recipient1_enc_key, + ], +) +def valid_recipient_key_encrypted(request, test_data_dir) -> Union[Path, bytes]: + if isinstance(request.param, str): + return test_data_dir / request.param + else: + return request.param + + +@pytest.fixture(scope="session") +def key_password() -> bytes: + return pkey_password + + +@pytest.fixture(scope="session") +def invalid_recipient_keys(test_data_dir) -> List[Path]: + return [ + test_data_dir / "recipient3-key.pem", + ] + + +@pytest.fixture( + scope="session", + params=[ + "recipient3-key.pem", + recipient3_key, + ], +) +def invalid_recipient_key(request, test_data_dir) -> Union[Path, bytes]: + if isinstance(request.param, str): + return test_data_dir / request.param + else: + return request.param diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..54608ac --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,474 @@ +import hashlib +import os +from inspect import getfullargspec + +import pytest + +from src.orcdecrypt.cli import entrypoint + +test_results_valid_recipient = [ + ("archive_aes.7z.p7b", 1, "edcb3c7bf0901ba2d8581179bc8a69dc26bce571"), + ("archive_des.7z.p7b", 1, "8ad404def35c4ed554e350cb36e331a779901052"), + ("archive_aes_corrupted.7z.p7b", 0, None), + ("archive_des_corrupted.7z.p7b", 0, None), + ("archive_aes_truncated.7z.p7b", 0, None), + ("archive_des_truncated.7z.p7b", 0, None), + ("archive_aes_padded.7z.p7b", 1, "edcb3c7bf0901ba2d8581179bc8a69dc26bce571"), + ("archive_des_padded.7z.p7b", 1, "8ad404def35c4ed554e350cb36e331a779901052"), +] +test_results_invalid_recipient = [ + ("archive_aes.7z.p7b", 0, None), + ("archive_des.7z.p7b", 0, None), + ("archive_aes_corrupted.7z.p7b", 0, None), + ("archive_des_corrupted.7z.p7b", 0, None), + ("archive_aes_truncated.7z.p7b", 0, None), + ("archive_des_truncated.7z.p7b", 0, None), + ("archive_aes_padded.7z.p7b", 0, None), + ("archive_des_padded.7z.p7b", 0, None), +] + + +@pytest.mark.parametrize( + "input_file, expected_decrypted_archives, expected_hash", + test_results_valid_recipient, + indirect=["input_file"], +) +def test_cli_python_with_valid_recipient( + input_file, + expected_decrypted_archives, + expected_hash, + valid_recipient_keys, + unstream_cmd, + tmp_path, + caplog: pytest.LogCaptureFixture, +): + msg = ( + "Decryption process succeeded whereas it should have failed." + if expected_decrypted_archives == 0 + else "Failed to decrypt archive with Python implementation." + ) + for key in valid_recipient_keys: + ret = entrypoint( + [ + "-j", + "1", + "-k", + str(key), + "--unstream-path", + str(unstream_cmd), + "--output", + str(tmp_path), + "--method", + "python", + str(input_file), + ] + ) + assert ret == 0, "Unexpected script error" + assert ( + f"{int(not expected_decrypted_archives)} failed, {expected_decrypted_archives} succeeded" + in caplog.records[-1].message + ), msg + if expected_decrypted_archives > 0: + with open(tmp_path / input_file.stem, "rb") as f_archive: + if "usedforsecurity" in getfullargspec(hashlib.sha1).kwonlyargs: + sha1sum = hashlib.sha1( + f_archive.read(), usedforsecurity=False + ).hexdigest() + else: + sha1sum = hashlib.sha1(f_archive.read()).hexdigest() + assert ( + sha1sum == expected_hash + ), f"Decrypted archive has the wrong SHA1: {sha1sum}" + (tmp_path / input_file.stem).unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_decrypted_archives, expected_hash", + test_results_invalid_recipient, + indirect=["input_file"], +) +def test_cli_python_with_invalid_recipient( + input_file, + expected_decrypted_archives, + expected_hash, + invalid_recipient_keys, + unstream_cmd, + tmp_path, + caplog: pytest.LogCaptureFixture, +): + for key in invalid_recipient_keys: + ret = entrypoint( + [ + "-j", + "1", + "-k", + str(key), + "--unstream-path", + str(unstream_cmd), + "--output", + str(tmp_path), + "--method", + "python", + str(input_file), + ] + ) + assert ret == 0, "Unexpected script error" + assert ( + f"{int( not expected_decrypted_archives)} failed, {expected_decrypted_archives} succeeded" + in caplog.records[-1].message + ), "Decryption process succeeded whereas the key provided was invalid for this archive." + (tmp_path / input_file.stem).unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_decrypted_archives, expected_hash", + test_results_valid_recipient, + indirect=["input_file"], +) +def test_cli_openssl_with_valid_recipient( + input_file, + expected_decrypted_archives, + expected_hash, + valid_recipient_keys, + unstream_cmd, + tmp_path, + caplog: pytest.LogCaptureFixture, +): + msg = ( + "Decryption process succeeded whereas it should have failed." + if expected_decrypted_archives == 0 + else "Failed to decrypt archive with OpenSSL." + ) + for key in valid_recipient_keys: + ret = entrypoint( + [ + "-j", + "1", + "-k", + str(key), + "--unstream-path", + str(unstream_cmd), + "--output", + str(tmp_path), + "--method", + "openssl", + str(input_file), + ] + ) + assert ret == 0, "Unexpected script error" + assert ( + f"{int(not expected_decrypted_archives)} failed, {expected_decrypted_archives} succeeded" + in caplog.records[-1].message + ), msg + if expected_decrypted_archives > 0: + with open(tmp_path / input_file.stem, "rb") as f_archive: + if "usedforsecurity" in getfullargspec(hashlib.sha1).kwonlyargs: + sha1sum = hashlib.sha1( + f_archive.read(), usedforsecurity=False + ).hexdigest() + else: + sha1sum = hashlib.sha1(f_archive.read()).hexdigest() + assert ( + sha1sum == expected_hash + ), f"Decrypted archive has the wrong SHA1: {sha1sum}" + (tmp_path / input_file.stem).unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_decrypted_archives, expected_hash", + test_results_invalid_recipient, + indirect=["input_file"], +) +def test_cli_openssl_with_invalid_recipient( + input_file, + expected_decrypted_archives, + expected_hash, + invalid_recipient_keys, + unstream_cmd, + tmp_path, + caplog: pytest.LogCaptureFixture, +): + for key in invalid_recipient_keys: + ret = entrypoint( + [ + "-j", + "1", + "-k", + str(key), + "--unstream-path", + str(unstream_cmd), + "--output", + str(tmp_path), + "--method", + "openssl", + str(input_file), + ] + ) + assert ret == 0, "Unexpected script error" + assert ( + f"{int(not expected_decrypted_archives)} failed, {expected_decrypted_archives} succeeded" + in caplog.records[-1].message + ), "Decryption process succeeded whereas the key provided was invalid for this archive." + (tmp_path / input_file.stem).unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_decrypted_archives, expected_hash", + test_results_valid_recipient, + indirect=["input_file"], +) +def test_cli_auto_with_valid_recipient( + input_file, + expected_decrypted_archives, + expected_hash, + valid_recipient_keys, + unstream_cmd, + tmp_path, + caplog: pytest.LogCaptureFixture, +): + msg = ( + "Decryption process succeeded whereas it should have failed." + if expected_decrypted_archives == 0 + else "Failed to decrypt archive with OpenSSL implementation." + ) + for key in valid_recipient_keys: + ret = entrypoint( + [ + "-j", + "1", + "-k", + str(key), + "--log-level", + "DEBUG", + "--unstream-path", + str(unstream_cmd), + "--output", + str(tmp_path), + str(input_file), + ] + ) + assert ret == 0, "Unexpected script error" + assert ( + f"{int(not expected_decrypted_archives)} failed, {expected_decrypted_archives} succeeded" + in caplog.records[-1].message + ), msg + if expected_decrypted_archives > 0: + with open(tmp_path / input_file.stem, "rb") as f_archive: + if "usedforsecurity" in getfullargspec(hashlib.sha1).kwonlyargs: + sha1sum = hashlib.sha1( + f_archive.read(), usedforsecurity=False + ).hexdigest() + else: + sha1sum = hashlib.sha1(f_archive.read()).hexdigest() + assert ( + sha1sum == expected_hash + ), f"Decrypted archive has the wrong SHA1: {sha1sum}" + (tmp_path / input_file.stem).unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_decrypted_archives, expected_hash", + test_results_invalid_recipient, + indirect=["input_file"], +) +def test_cli_auto_with_invalid_recipient( + input_file, + expected_decrypted_archives, + expected_hash, + invalid_recipient_keys, + unstream_cmd, + tmp_path, + caplog: pytest.LogCaptureFixture, +): + for key in invalid_recipient_keys: + ret = entrypoint( + [ + "-j", + "1", + "-k", + str(key), + "--log-level", + "DEBUG", + "--unstream-path", + str(unstream_cmd), + "--output", + str(tmp_path), + str(input_file), + ] + ) + assert ret == 0, "Unexpected script error" + assert ( + f"{int(not expected_decrypted_archives)} failed, {expected_decrypted_archives} succeeded" + in caplog.records[-1].message + ), "Decryption process succeeded whereas the key provided was invalid for this archive." + (tmp_path / input_file.stem).unlink(missing_ok=True) + + +def test_cli_with_empty_input( + valid_recipient_keys, unstream_cmd, tmp_path, caplog: pytest.LogCaptureFixture +): + ret = entrypoint( + [ + "-j", + "1", + "-k", + str(valid_recipient_keys[0]), + "--log-level", + "DEBUG", + "--log-file", + str(tmp_path / "orcdecrypt.log"), + "--unstream-path", + str(unstream_cmd), + "--output", + str(tmp_path), + str(tmp_path), + ] + ) + assert ret == 0, "Unexpected script error" + assert ( + "No encrypted archives could be found in input path(s), nothing to do." + in caplog.records[-1].message + ) + + +def test_cli_without_unstream( + valid_recipient_keys, tmp_path, caplog: pytest.LogCaptureFixture +): + old_path = os.environ["PATH"] + os.environ.update(PATH="") + ret = entrypoint( + [ + "-j", + "1", + "-k", + str(valid_recipient_keys[0]), + "--log-level", + "DEBUG", + "--log-file", + str(tmp_path / "orcdecrypt.log"), + "--output", + str(tmp_path), + "test_data", + ] + ) + os.environ.update(PATH=old_path) + assert ( + ret == -1 + ), "Return code should have been -1 since unstream can not be found in this environment." + assert ( + 'Missing tool "unstream" in PATH, please provide the path to unstream binary with --unstream-path' + in caplog.records[-1].message + ) + + +def test_cli_with_invalid_unstream( + valid_recipient_keys, tmp_path, capsys: pytest.CaptureFixture +): + with pytest.raises(SystemExit) as e: + entrypoint( + [ + "-j", + "1", + "-k", + str(valid_recipient_keys[0]), + "--log-level", + "DEBUG", + "--log-file", + str(tmp_path / "orcdecrypt.log"), + "--output", + str(tmp_path), + "--unstream-path", + "/non/existent" "test_data", + ] + ) + assert ( + e.value.code == 2 + ), "Return code should have been 2 (incorrect usage) since unstream path does not exist." + assert "is not a valid file" in capsys.readouterr().err + + +def test_cli_with_invalid_key(tmp_path, capsys: pytest.CaptureFixture): + invalid_key = tmp_path / "invalid_key.pem" + invalid_key.touch() + with pytest.raises(SystemExit) as e: + entrypoint( + [ + "-j", + "1", + "-k", + str(invalid_key), + "--log-level", + "DEBUG", + "--log-file", + str(tmp_path / "orcdecrypt.log"), + "--output", + str(tmp_path), + "test_data", + ] + ) + assert ( + e.value.code == 2 + ), f"Return code should have been 2 (incorrect usage) since {invalid_key} is an invalid private key." + assert "is not a valid private key" in capsys.readouterr().err + + +def test_cli_with_inexistant_input( + valid_recipient_keys, + tmp_path, + capsys: pytest.CaptureFixture, +): + with pytest.raises(SystemExit) as e: + entrypoint( + [ + "-j", + "1", + "-k", + str(valid_recipient_keys[0]), + "--log-level", + "DEBUG", + "--log-file", + str(tmp_path / "orcdecrypt.log"), + "--output", + str(tmp_path), + "no_data", + ] + ) + assert ( + e.value.code == 2 + ), "Return code should have been 2 (incorrect usage) since the input does not exist." + assert "does not exist" in capsys.readouterr().err + + +def test_cli_with_input_directories( + valid_recipient_keys, + unstream_cmd, + tmp_path, + test_data_dir, + caplog: pytest.LogCaptureFixture, +): + ret = entrypoint( + [ + "-j", + "1", + "-k", + str(valid_recipient_keys[0]), + "--unstream-path", + str(unstream_cmd), + "--log-level", + "DEBUG", + "--log-file", + str(tmp_path / "orcdecrypt.log"), + "--output", + str(tmp_path), + str(test_data_dir / "subdir_a"), + str(test_data_dir / "subdir_b"), + ] + ) + assert ret == 0, "Unexpected script error" + assert ( + "0 failed, 4 succeeded" in caplog.records[-1].message + ), "Failed to decrypt archive with OpenSSL implementation." + assert (tmp_path / "subdir_1" / "archive_aes.7z").exists() + assert (tmp_path / "subdir_2" / "archive_aes.7z").exists() + assert (tmp_path / "archive_aes.7z").exists() + assert (tmp_path / "archive_des.7z").exists() diff --git a/tests/test_decrypt.py b/tests/test_decrypt.py new file mode 100644 index 0000000..35c6538 --- /dev/null +++ b/tests/test_decrypt.py @@ -0,0 +1,399 @@ +import hashlib +import logging +import os +from inspect import getfullargspec + +import pytest + +from src.orcdecrypt.decrypt import decrypt_archive + +test_results_valid_recipient = [ + ("archive_aes.7z.p7b", True, "edcb3c7bf0901ba2d8581179bc8a69dc26bce571"), + ("archive_des.7z.p7b", True, "8ad404def35c4ed554e350cb36e331a779901052"), + ("archive_aes_corrupted.7z.p7b", False, None), + ("archive_des_corrupted.7z.p7b", False, None), + ("archive_aes_truncated.7z.p7b", False, None), + ("archive_des_truncated.7z.p7b", False, None), + ("archive_aes_padded.7z.p7b", True, "edcb3c7bf0901ba2d8581179bc8a69dc26bce571"), + ("archive_des_padded.7z.p7b", True, "8ad404def35c4ed554e350cb36e331a779901052"), +] +test_results_invalid_recipient = [ + ("archive_aes.7z.p7b", False, None), + ("archive_des.7z.p7b", False, None), + ("archive_aes_corrupted.7z.p7b", False, None), + ("archive_des_corrupted.7z.p7b", False, None), + ("archive_aes_truncated.7z.p7b", False, None), + ("archive_des_truncated.7z.p7b", False, None), + ("archive_aes_padded.7z.p7b", False, None), + ("archive_des_padded.7z.p7b", False, None), +] + + +@pytest.mark.parametrize( + "input_file, expected_return, expected_hash", + test_results_valid_recipient, + indirect=["input_file"], +) +def test_decrypt_python_with_valid_recipient( + input_file, + expected_return, + expected_hash, + valid_recipient_key, + unstream_cmd, + tmp_path, +): + msg = ( + "Failed to decrypt archive with Python implementation." + if expected_return + else "Decryption process returned True whereas it should have failed" + ) + output_path = tmp_path / "archive.7z" + + assert ( + decrypt_archive( + input_file, valid_recipient_key, output_path, unstream_cmd, method="python" + ) + == expected_return + ), msg + if expected_return: + with open(output_path, "rb") as f_archive: + if "usedforsecurity" in getfullargspec(hashlib.sha1).kwonlyargs: + sha1sum = hashlib.sha1( + f_archive.read(), usedforsecurity=False + ).hexdigest() + else: + sha1sum = hashlib.sha1(f_archive.read()).hexdigest() + assert ( + sha1sum == expected_hash + ), f"Decrypted archive has the wrong SHA1: {sha1sum}" + output_path.unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_return, expected_hash", + test_results_valid_recipient, + indirect=["input_file"], +) +def test_decrypt_python_with_valid_recipient_encrypted( + input_file, + expected_return, + expected_hash, + valid_recipient_key_encrypted, + key_password, + unstream_cmd, + tmp_path, +): + msg = ( + "Failed to decrypt archive with Python implementation." + if expected_return + else "Decryption process returned True whereas it should have failed" + ) + output_path = tmp_path / "archive.7z" + + assert ( + decrypt_archive( + input_file, + valid_recipient_key_encrypted, + output_path, + unstream_cmd, + method="python", + password=key_password, + ) + == expected_return + ), msg + if expected_return: + with open(output_path, "rb") as f_archive: + if "usedforsecurity" in getfullargspec(hashlib.sha1).kwonlyargs: + sha1sum = hashlib.sha1( + f_archive.read(), usedforsecurity=False + ).hexdigest() + else: + sha1sum = hashlib.sha1(f_archive.read()).hexdigest() + assert ( + sha1sum == expected_hash + ), f"Decrypted archive has the wrong SHA1: {sha1sum}" + output_path.unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_return, expected_hash", + test_results_invalid_recipient, + indirect=["input_file"], +) +def test_decrypt_python_with_invalid_recipient( + input_file, + expected_return, + expected_hash, + invalid_recipient_key, + unstream_cmd, + tmp_path, +): + output_path = tmp_path / "archive.7z" + assert not decrypt_archive( + input_file, + invalid_recipient_key, + output_path, + unstream_cmd, + method="python", + ), "Decryption process returned True whereas the key provided was invalid for this archive." + output_path.unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_return, expected_hash", + test_results_valid_recipient, + indirect=["input_file"], +) +def test_decrypt_openssl_with_valid_recipient( + input_file, + expected_return, + expected_hash, + valid_recipient_key, + unstream_cmd, + tmp_path, +): + msg = ( + "Failed to decrypt archive with OpenSSL implementation." + if expected_return + else "Decryption process returned True whereas it should have failed" + ) + output_path = tmp_path / "archive.7z" + assert ( + decrypt_archive( + input_file, valid_recipient_key, output_path, unstream_cmd, method="openssl" + ) + == expected_return + ), msg + if expected_return: + with open(output_path, "rb") as f_archive: + if "usedforsecurity" in getfullargspec(hashlib.sha1).kwonlyargs: + sha1sum = hashlib.sha1( + f_archive.read(), usedforsecurity=False + ).hexdigest() + else: + sha1sum = hashlib.sha1(f_archive.read()).hexdigest() + assert ( + sha1sum == expected_hash + ), f"Decrypted archive has the wrong SHA1: {sha1sum}" + output_path.unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_return, expected_hash", + test_results_valid_recipient, + indirect=["input_file"], +) +def test_decrypt_openssl_with_valid_recipient_encrypted( + input_file, + expected_return, + expected_hash, + valid_recipient_key_encrypted, + key_password, + unstream_cmd, + tmp_path, +): + msg = ( + "Failed to decrypt archive with OpenSSL implementation." + if expected_return + else "Decryption process returned True whereas it should have failed" + ) + output_path = tmp_path / "archive.7z" + assert ( + decrypt_archive( + input_file, + valid_recipient_key_encrypted, + output_path, + unstream_cmd, + method="openssl", + password=key_password, + ) + == expected_return + ), msg + if expected_return: + with open(output_path, "rb") as f_archive: + if "usedforsecurity" in getfullargspec(hashlib.sha1).kwonlyargs: + sha1sum = hashlib.sha1( + f_archive.read(), usedforsecurity=False + ).hexdigest() + else: + sha1sum = hashlib.sha1(f_archive.read()).hexdigest() + assert ( + sha1sum == expected_hash + ), f"Decrypted archive has the wrong SHA1: {sha1sum}" + output_path.unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_return, expected_hash", + test_results_invalid_recipient, + indirect=["input_file"], +) +def test_decrypt_openssl_with_invalid_recipient( + input_file, + expected_return, + expected_hash, + invalid_recipient_key, + unstream_cmd, + tmp_path, +): + output_path = tmp_path / "archive.7z" + assert not decrypt_archive( + input_file, invalid_recipient_key, output_path, unstream_cmd, method="openssl" + ), "Decryption process returned True whereas the key provided was invalid for this archive." + output_path.unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_return, expected_hash", + test_results_valid_recipient, + indirect=["input_file"], +) +def test_decrypt_auto_with_valid_recipient( + input_file, + expected_return, + expected_hash, + valid_recipient_key, + unstream_cmd, + tmp_path, + caplog, +): + msg = ( + "Failed to decrypt archive with automatic method detection." + if expected_return + else "Decryption process returned True whereas it should have failed" + ) + caplog.set_level(logging.DEBUG) + output_path = tmp_path / "archive.7z" + assert ( + decrypt_archive(input_file, valid_recipient_key, output_path, unstream_cmd) + == expected_return + ), msg + assert any(" using OpenSSL" in record[2] for record in caplog.record_tuples) + if expected_return: + with open(output_path, "rb") as f_archive: + if "usedforsecurity" in getfullargspec(hashlib.sha1).kwonlyargs: + sha1sum = hashlib.sha1( + f_archive.read(), usedforsecurity=False + ).hexdigest() + else: + sha1sum = hashlib.sha1(f_archive.read()).hexdigest() + assert ( + sha1sum == expected_hash + ), f"Decrypted archive has the wrong SHA1: {sha1sum}" + output_path.unlink(missing_ok=True) + + +@pytest.mark.parametrize( + "input_file, expected_return, expected_hash", + test_results_invalid_recipient, + indirect=["input_file"], +) +def test_decrypt_auto_with_invalid_recipient( + input_file, + expected_return, + expected_hash, + invalid_recipient_key, + unstream_cmd, + tmp_path, + caplog, +): + output_path = tmp_path / "archive.7z" + caplog.set_level(logging.DEBUG) + assert not decrypt_archive( + input_file, invalid_recipient_key, output_path, unstream_cmd + ), "Decryption process returned True whereas the key provided was invalid for this archive." + assert any(" using OpenSSL" in record[2] for record in caplog.record_tuples) + output_path.unlink(missing_ok=True) + + +def test_decrypt_auto_without_openssl( + valid_archive, + valid_recipient_key, + unstream_cmd, + tmp_path, + caplog: pytest.LogCaptureFixture, +): + output_path = tmp_path / "archive.7z" + caplog.set_level(logging.DEBUG) + old_path = os.environ["PATH"] + os.environ.update(PATH="") + assert decrypt_archive( + valid_archive, valid_recipient_key, output_path, unstream_cmd + ), "Decryption process failed whereas it should have fell back to python decryption." + os.environ.update(PATH=old_path) + assert any( + " using Python implementation" in record.message for record in caplog.records + ) + assert any( + "OpenSSL binary could not be found in the PATH, falling back to python" + in record.message + for record in caplog.records + ) + output_path.unlink(missing_ok=True) + + +def test_decrypt_openssl_without_openssl( + valid_archive, + valid_recipient_key, + unstream_cmd, + tmp_path, + caplog: pytest.LogCaptureFixture, +): + output_path = tmp_path / "archive.7z" + caplog.set_level(logging.DEBUG) + old_path = os.environ["PATH"] + os.environ.update(PATH="") + with pytest.raises( + FileNotFoundError, match="OpenSSL binary could not be found in the PATH" + ): + decrypt_archive( + valid_archive, + valid_recipient_key, + output_path, + unstream_cmd, + method="openssl", + ) + os.environ.update(PATH=old_path) + output_path.unlink(missing_ok=True) + + +def test_decrypt_twice_without_force( + valid_archive, valid_recipient_key, unstream_cmd, tmp_path, caplog, method +): + output_path = tmp_path / "archive.7z" + caplog.set_level(logging.DEBUG) + assert decrypt_archive( + valid_archive, valid_recipient_key, output_path, unstream_cmd, method=method + ), "Failed to decrypt archive with OpenSSL." + assert not decrypt_archive( + valid_archive, valid_recipient_key, output_path, unstream_cmd, method=method + ), "Decryption process should have failed because of existing output file." + assert any( + " already exists, skipping (use --force to overwrite)" in record[2] + for record in caplog.record_tuples + ) + output_path.unlink(missing_ok=True) + + +def test_decrypt_twice_with_force( + valid_archive, + valid_recipient_key, + unstream_cmd, + tmp_path, + caplog, + method, +): + output_path = tmp_path / "archive.7z" + caplog.set_level(logging.DEBUG) + assert decrypt_archive( + valid_archive, valid_recipient_key, output_path, unstream_cmd, method=method + ), "Failed to decrypt archive with OpenSSL." + assert decrypt_archive( + valid_archive, + valid_recipient_key, + output_path, + unstream_cmd, + method=method, + force=True, + ), "Failed to decrypt archive with OpenSSL." + output_path.unlink(missing_ok=True)