symmetric key encryption and decryption compatible with GPG
pip install tinygpgs==0.18
tinygpgs: symmetric key encryption compatible with GPG in Python ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ tinygpgs is a fast module and standalone Python script for doing symmetric key (passphrase-based) encryption and decryption using the OpenPGP file format compatible with GPG (GnuPG). Usage: $ ./tinygpgs -c FILE.bin # Encrypt. $ ./tinygpgs -d <FILE.bin.gpg >FILE2.bin # Decrypt. $ cmp FILE.bin FILE2.bin Features: * fast: decrypts <1.26 times slower than gpg(1), and encrypts <1.37 times slower than gpg(1), see section ``Speed'' below. * tiny: the full-featured standalone command-line tool is smaller than 50 KiB (minified and ZIP-compressed), and it contains all the GPG ciphers and hashes as Python code * all ciphers and hashes: supports all ciphers and hashes in the OpenPGP spec, including those which PyCrypto doesn't support. * small, constant memory usage: code + bzip2 decompression dictionary and output buffer (can be serveral MiBs) + <128 KiB buffers. * interoperability: both encryption and decryption works with files to and from gpg(1), checked versions 1.0.6, 1.4.1, 1.4.18, 2.1.18, 2.2.17. * tool compatibility: command-line flags compatible with gpg(1), mostly with the same defaults. * minimal dependencies: Works out-of-the-box with standard Python modules, but becomes much faster if PyCrypto is installed. * any Python: works with any Python >=2.4, including Python 3. * no regexps: the implementation doesn't use regexps, thus avoiding speed (and potential catastrophic speed) issues * binary files: always opens files in binary mode, doesn't do any coversion on the plaintext (not even when decrypting, this is a difference from GPG) Planned features: * docs: Add full documentation for the file class API. Explicit non-features: * asymmetric (public key) encryption * asymmetric (public key) signing * gpg-agent support (e.g. storing passphrases in the agent) * key management: ~/.gnupg/pubring.gpg and ~/.gnupg/secring.gpg * asymmetric key (keypair) generation * trust model Dependencies: * Python >=2.4. (Tested with: 2.4, 2.5, 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8.) If you use Debian or Ubuntu and you only have the minimal package installed (e.g. `sudo apt-get install python3.5-minimal', then all functionality except for bzip2 compression and decompression works). * Optionally, PyCrypto for fast encryption and decryption. If PyCrypto is not available, embedded fallback pure Python code is used instead, but that can be >400 times slower than PyCrypto. * Optionally, hashlib with OpenSSL for fast hashes (string-to-key and modification detection). If hashlib isn't available (or it doesn't support the hash needed), embedded fallback pure Python code is used instead, but that can be >400 times slower (~162 times for SHA-1) than hashlib with OpenSSL. * Only for zip and zlib (de)compression, the standard Python module zip. * Only dor bzip2 (de)compression, the standard Python module bzip2. * Only for interactive passphrase prompts, the standard Python module getpass. (Use --passphrase or similar to avoid the prompt.) tinygpgs is free software released under the MIT license. There is NO WARRANTY. Use at your risk. Default encryption settings: * (These settings are the same as GPG 1.0.6 (2001-06-01) ... 1.4.18, unless otherwise noted. Also checked GPG 2.2.17 (2019-07-09).) * --cipher-algo cast5 (Changed in GPG 2.1 (2017-08-09) to aes-128 (according to https://en.wikipedia.org/wiki/GNU_Privacy_Guard), and in GPG 2.2 (2017-09-19) to aes-256. tinygpgs doesn't reflect these changes.) * --digest-algo sha1 * --s2k-count 65536 (Changed in GPG 2.1 to 3014656 and then to even higher values. It makes dictionary attacks properotionally slower. tinygpgs doesn't reflect this change.) * --compress-algo zip * --compress-level 6 * --force-mdc (The default of GPG depends on other flags.) * --no-armor Installation as a standalone command-line tool ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you have Python 2 (preferably with PyCrypto) installed, you can download and use a the single-file, standalone command-line tool from https://raw.githubusercontent.com/pts/tinygpgs/master/tinygpgs.single , make it executable, and rename (or symlink) it to tinygpgs. FYI to install Python 2 with PyCrypto on Debian-based Linux systems: $ sudo apt-get install python-pycrypto Then on Linux you can download and use tinygpgs like this: $ wget -O tinygpgs.single https://raw.githubusercontent.com/pts/tinygpgs/master/tinygpgs.single $ chmod +x tinygpgs.single $ ln -s tinygpgs.single tinygpgs $ ./tinygpgs Alternatively, on macOS: $ curl -Lo tinygpgs.single https://raw.githubusercontent.com/pts/tinygpgs/master/tinygpgs.single $ chmod +x tinygpgs.single $ ln -s tinygpgs.single tinygpgs $ ./tinygpgs You can install tinygpgs by copying the file (and the symlink) to somewhere on your $PATH. You can also use tinygpgs.single on Windows. After renaming it to tinygpgs, run: $ python tinygpgs Installation as a Python module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you have Python 2 already installed, just run this command (maybe with sudo to install for all users): $ python -m pip install tinygpgs pycrypto To use the command-line tool, run it as `python -m tinygpgs.__main__' instead of `./tinygpgs', except for Python 2.4, which requires `python2.4 -m tinygpgs/__main'. For Python >=2.7, `python -m tinygpgs' also works. Installation instructions and usage on Windows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There is no installer, you need to run some commands in the command line (black Command Prompt window) to download and install. tinygpgs is a command-line only application, there is no GUI. Windows XP or newer (including Windows XP, Windows Vista, Windows 7, Windows 8, Windows 8.1 and Windows 10) is needed. 32-bit (i386, x86) or 64-bit (amd64, x86_64, x64) versions are both fine (tinygpgs contains a 32-bit Python 2.6 executable). Create an empty directory and download these files there: * https://github.com/pts/tinygpgs/releases/download/2019-12-10w/tinygpgs_win32exec.exe * https://github.com/pts/tinygpgs/raw/master/tinygpgs.cmd * https://github.com/pts/tinygpgs/raw/master/tinygpgs.single In a Command Prompt window, cd into this direcrytory and run tinygpgs_win32.exe. It will create some more files. Afterwards you can remove it (del tinygpgs_win32exec.exe). Now you can run tinygpgs as usual, e.g. `tinygpgs --help' (without the quotes) from that directory. If you want to run it from any directory, then add the directory containing tinygpgs.cmd to your PATH, or move all the files to C:\Windows\System32. To upgrade it, download a newer version of tinygpgs.single, and overwrite that file. Limitations: * It's very slow, because it doesn't have and doesn't use PyCrypto. * It doesn't work with filenames containing characters outside the CP-1252 encoding (https://en.wikipedia.org/wiki/CP-1252), which is ASCII, ISO-8859-1, plus some Windows-specific characters. (This is a limitation of the Python 2.6 executable used). Alteratively, if you already have Python installed (preferably with PyCrypto, for the huge speed boost) on your Windows system, tinygpgs can use that. The installation process is the same, but don't download or run tinygpgs_win32exec.exe. (If you've run it already, delete tinygpgs_python.exe, but better delete all the files and start downloading again.) After that, tinygpgs.cmd will run tinygpgs.single with the ``python'' command on your system. Using the file class API in the Python module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Install the Python module first, then see in https://github.com/pts/tinygpgs/blob/master/tinygpgs.rst how to use the file class API from your Python code. Tools for decrypting symmetric key GPG message ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * gpg1: gpg -d <FILE.bin.gpg >FILE.bin * gpg2: gpg -d --pinentry-mode loopback <FILE.bin.gpg >FILE.bin * pgpy: python -c 'import getpass, sys, pgpy; sys.stdout.write(pgpy.PGPMessage.from_file(sys.argv[1]).decrypt(getpass.getpass("Enter passphrase: ")).message)' FILE.bin.gpg >FILE.bin installation: pip install pgpy # Has lots of dependendes, use virtualenv. https://github.com/thenoviceoof/encryptedfile supports only encryption, --no-armor, --no-mdc and `--compress-algo none'. Some other Python PGP projects are listed here: https://pypi.org/project/py-pgp/ GPG symmetric key encryption steps ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ GPG (and OpenPGP) does the following for encryption: 1. Generates a random 8-byte salt1 and a random salt2 (of the same size as the cipher block size). 2. Computes the session key from the passphrase by computing a hash (typically SHA-1) of a (configurably) long repeat of the (salt1 + passphrase). This has similar purpose as of PBKDF2, but the actual algorithm is very different. 3. Optionally compresses the plaintext (with zip, zlib or bzip2). 4. Optionally computes the SHA-1 hash (20 bytes) of the compressed plaintext, and appends it. 5. Encrypts the result with a block cipher (CAST5 or AES by default) in CFB mode, with a random salt2 of 8 or 16 bytes (same as the cipher block size), repeating 2 bytes of the salt2 in the ciphertext, so that an incorrect passphrase can be detected at decryption time with high probability. 6. Splits the compressed output to packets with fixed payload size (typically 8192 bytes). 7. Adds a header. 8. Optionally converts the binary output to Base64 (ASCII), and adds a header (-----BEGIN PGP MESSAGE-----). Speed ~~~~~ For string-to-key conversion, data is grouped to chunks of almost 65536 bytes, and most of the time is spent in hash computation in C extensions, very little time is spend in Python code. For more details, see <FAST-HASH> in the source code. The Python code of critical path of tinygpgs decryption is optimized: it contains very little Python code, most of the heavly lifting is done by Crypto.Cipher._AES.new(...).encrypt and Crypto.Util.strxor.strxor with long strings (partial packet size in the encrypted input, typically 8192 bytes), both of which are implemented in C extensions. On average, the decryption part of `tinygpgs -d' is <1.26 slower than gpg(1), see the numbers in the ``Benchmarks'' section. For more details, see <FAST-DECRYPTION> in the source code. There is a similar speedup implemented for `tinygpgs -c', see <FAST-ENCRYPTION> in the source code for more details. The optimization there is a bit less effective, it uses the PyCrypto cipher objects in MODE_ECB, it's <1.37 times slower than GPG. The large strxor trick could make it just <1.21 times slower, but its output is incorrect: we need many calls to strxor in a feedback for MODE_ECB encryption. There is a class API (GpgSymmetricFile*), which is a <1.11 times slower than the function-based API (used for the command-line as well) for decryption, and about the same speed for encryption. Both APIs have the same, very small memory usage. They share about half of their code. In the benchmarks, the function-based API is measured, unless otherwise indicated. Benchmarks ~~~~~~~~~~ Encrypted input file (hellow5long.bin.gpg) parameters: * ~32.1 MiB compressed and encrypted. * Corresponding plaintext file is 32 MiB of random bytes (uncompressable). * No encrypted session key. * --cipher-algo aes-256 * --s2k-mode 3 # iterated-salted * --s2k-count 3014656 * --hash-algo sha1 * --compress-algo zip * --force-mdc * Partial packet are of size 8192. Unless otherwise indicated, benchmarks were run with Python 2.7.13 on a slow Linux amd64 system running Debian 9.4. Decryption (and decompression) benchmark measurements: gpg (GNUPG) 2.1.18. $ time gpg2 -d --pinentry-mode loopback <hellow5long.bin.gpg >hellow5long.out 3.168s user With standard OpenSSL hashlib + PyCrypto. $ time python2.7 tinygpgs -d --passphrase abc <hellow5long.bin.gpg >hellow5long.out 3.980s user With Python 3.5, standard OpenSSL hashlib + PyCrypto. $ time python3.5 tinygpgs -d --passphrase abc <hellow5long.bin.gpg >hellow5long.out 4.388s user With standard OpenSSL hashlib + PyCrypto, using the GpgSymmetricFileReader file class API. $ time python2.7 tinygpgs -d --passphrase abc <hellow5long.bin.gpg >hellow5long.out 4.388s user Old, slow tinygpgs, before the commit ``added speedup for the critical, common decryption path'': $ time python2.7 tinygpgs -d --passphrase abc <hellow5long.bin.gpg >hellow5long.out 109.692s user pgpy: This is very slow. $ python -c 'import getpass, sys, pgpy; sys.stdout.write(pgpy.PGPMessage.from_file(sys.argv[1]).decrypt(getpass.getpass("Enter passphrase: ")).message)' hellow5long.bin.gpg >hellow5long.out 282.272s Also it keeps the entire input and output files in memory, using more than 1.4 times memory than input_size + output_size. Encryption (and compression) benchmark measurements: <FAST-ENCRYPTION> with standard OpenSSL hashlib + PyCrypto, default. $ time python2.7 tinygpgs -c --cipher-algo aes-256 --passphrase abc <hellow5long.bin >hellowc5long.bin.gpg info: GPG symmetric encrypt cipher_algo=aes-256 is_py_cipher=0 s2k_mode=iterated-salted digest_algo=sha1 is_py_digest=0 count=65536 len(salt)=8 len(encrypted_session_key)=0 do_mdc=1 len(session_key)=32 8.828s user <FAST-ENCRYPTION>, with Python 3.5. $ time python3.5 tinygpgs -c --cipher-algo aes-256 --passphrase abc <hellow5long.bin >hellowc5long.bin.gpg 8.940s user <FAST-ENCRYPTION>, without MDC, without compression. To be compared with encryptedfile. $ time python2.7 tinygpgs -c --cipher-algo aes-256 --passphrase abc --disable-mdc --compress-algo none <hellow5long.bin >hellowc5long.bin.gpg info: GPG symmetric encrypt cipher_algo=aes-256(9) is_py_cipher=0 s2k_mode=iterated-salted(3) s2k_digest_algo=sha1(2) is_py_digest=0 s2k_count=65536 len(s2k_salt)=8 compress_algo=uncompressed(0) compress_level=6 len(encrypted_session_key)=0 do_mdc=0 len(session_key)=32 bufcap=8192 literal_type='b' plain_filename='' mtime=0 do_add_ascii_armor=0 0m2.336s user <FAST-ENCRYPTION>, with GpgSymmetricFileWriter file class API. $ time python2.7 tinygpgs -c --file-class --cipher-algo aes-256 --passphrase abc <hellow5long.bin >hellowc5long.bin.gpg info: GPG symmetric encrypt cipher_algo=aes-256 is_py_cipher=0 s2k_mode=iterated-salted digest_algo=sha1 is_py_digest=0 count=65536 len(salt)=8 len(encrypted_session_key)=0 do_mdc=1 len(session_key)=32 7.984s user <FAST-ENCRYPTION>, with ASCII armor. CRC24 calculation for the ASCII armor makes it very slow. Without the CRC24 update (resulting an invalid output file), it would take only 13.516s. $ time python2.7 tinygpgs -c -a --cipher-algo aes-256 --passphrase abc <hellow5long.bin >hellowc5long.bin.gpg info: GPG symmetric encrypt cipher_algo=cast5 is_py_cipher=0 s2k_mode=iterated-salted digest_algo=sha1 is_py_digest=0 count=65536 len(salt)=8 len(encrypted_session_key)=0 do_mdc=1 len(session_key)=16 63.236s user <FAST-ENCRYPTION>, with GpgSymmetricFileWriter file class API and ASCII armor. CRC24 calculation for the ASCII armor makes it very slow. Without the CRC24 update (resulting an invalid output file), it would take only 13.516s. $ time python2.7 tinygpgs -c -a --cipher-algo aes-256 --passphrase abc <hellow5long.bin >hellowc5long.bin.gpg info: GPG symmetric encrypt cipher_algo=cast5 is_py_cipher=0 s2k_mode=iterated-salted digest_algo=sha1 is_py_digest=0 count=65536 len(salt)=8 len(encrypted_session_key)=0 do_mdc=1 len(session_key)=16 63.520s user <MEDIUM-ENCRYPTION>. $ time python2.7 tinygpgs -c --cipher-algo aes-256 --passphrase abc <hellow5long.bin >hellowc5long.bin.gpg info: GPG symmetric encrypt cipher_algo=aes-256 is_py_cipher=0 s2k_mode=iterated-salted digest_algo=sha1 is_py_digest=0 count=65536 len(salt)=8 len(encrypted_session_key)=0 do_mdc=1 len(session_key)=32 18.856s user <SLOW-ENCRYPTION>, doesn't use a write buffer. $ time python2.7 tinygpgs -c --cipher-algo aes-256 --passphrase abc <hellow5long.bin >hellowc5long.bin.gpg info: GPG symmetric encrypt cipher_algo=aes-256 is_py_cipher=0 s2k_mode=iterated-salted digest_algo=sha1 is_py_digest=0 count=65536 len(salt)=8 len(encrypted_session_key)=0 do_mdc=1 len(session_key)=32 59.988s user # Settings equivalent to `tinygpgs -c --cipher-algo aes-256'. $ time gpg -c --pinentry-mode loopback --cipher-algo aes-256 --digest-algo sha1 --s2k-count 65536 --compress-algo zip --compress-level 6 --force-mdc --passphrase abc <hellow5long.bin >hellowc5long.bin.gpg 6.444s user encryptedfile in text mode: This is very-very slow. $ time python -c 'import encryptedfile; f = encryptedfile.EncryptedFile("hellow5longef.bin.gpg", "abc", encryption_algo=encryptedfile.EncryptedFile.ALGO_AES256); f.write(open("hellow5long.gpg", "rb").read()); f.close()' 1847.260s user encryptedfile in binary mode: This is very-very slow, possibly because of unnecessary string concatenations. $ time python -c 'import encryptedfile; f = encryptedfile.EncryptedFile("hellow5longef.bin.gpg", "abc", mode="wb", encryption_algo=encryptedfile.EncryptedFile.ALGO_AES256); f.write(open("hellow5long.gpg", "rb").read()); f.close()' 1814.316s encryptedfile in binary mode, no compression. $ time python -c 'import encryptedfile; f = encryptedfile.EncryptedFile("hellow5longef.bin.gpg", "abc", mode="wb", encryption_algo=encryptedfile.EncryptedFile.ALGO_AES256); inf = open("../hellow5long.bin", "rb") while 1: data = inf.read(8192) if not data: break f.write(data) f.close()' 0m4.200s user __END__