diff --git a/Bakefile b/Bakefile index a0f909f..2c7cd5e 100644 --- a/Bakefile +++ b/Bakefile @@ -19,25 +19,30 @@ install_useve() { py-format . useve qgpg pip install ./ + qgpg --help } test_encrypt() { + # Prepare cd ~/tmp/ . useve-runner useve qgpg - rm -fr key2* datadir sha1sum.txt - GPGPASS=secret @ qgpg --key key2 k + rm -fr key2* datadir datadir.encrypted sha1sum.txt @ mkdir -p datadir/folder{1,2} @ dd if=/dev/random of=datadir/testfile bs=3024 count=102400 for i in {1..10}; do dd if=/dev/random of=datadir/folder1/${i}testfile bs=1024 count=10240 &> /dev/null dd if=/dev/random of=datadir/folder2/${i}testfile bs=1024 count=10240 &> /dev/null done - @ qgpg --key key2.pub e datadir/folder1/1testfile - @ qgpg --key key2.pub e datadir/folder1/1testfile datadir/1testfile.encrypted.gpg - @ qgpg --key key2.pub -r e datadir - @ qgpg --key key2.pub -r e datadir - @ qgpg --key key2.pub -r e $(pwd)/datadir/ datadir.encrypted/ + # Run encryption code + set -e + GPGPASS=secret @ qgpg -k --key key2 + @ qgpg -e --key key2.pub datadir/folder1/1testfile + @ qgpg -e --key key2.pub datadir/folder1/1testfile datadir/1testfile.encrypted.gpg + @ qgpg -e --key key2.pub -r datadir + @ qgpg -e --key key2.pub -r datadir + @ qgpg -e --key key2.pub -r $(pwd)/datadir/ datadir.encrypted/ + GPGPASS=symmetric_password @ qgpg -e datadir/folder1/1testfile datadir/1testfile.encrypted.symmetric.gpg @ hash-update -t sha1 -f sha1sum.txt -r datadir } @@ -45,8 +50,10 @@ test_decrypt() { cd ~/tmp/ . useve-runner useve qgpg + set -e @ find datadir -type f -name '*testfile' | xargs -II rm -v I - GPGPASS=secret @ qgpg --key key2 -r d datadir + GPGPASS=secret @ qgpg -d --key key2 -r datadir + GPGPASS=symmetric_password @ qgpg -d --force datadir/1testfile.encrypted.symmetric.gpg datadir/folder1/1testfile @ hash-update -t sha1 -f sha1sum.txt -c } diff --git a/qgpg/__init__.py b/qgpg/__init__.py index 0f1df6f..cf24fa9 100644 --- a/qgpg/__init__.py +++ b/qgpg/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -__version__ = "1.1.1" +__version__ = "20240627.1" import argparse import os @@ -101,52 +101,106 @@ def get_opts(): """Returns command line arguments""" parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False, description="Wrapper to GPG, allowing recursive encryption.", - epilog="""Private key passphrase can be set to env variable GPGPASS, otherwise will prompt + epilog="""Private key passphrase can be set to env variable GPGPASS, otherwise program will prompt it. Usage ===== - Generate keys: - qgpg --key ./path/mykey --name "User Name" keygen + qgpg --keygen --key ./path/mykey --name "User Name" - This will create private and public keys, mykey and mykey.pub - Share the public key to someone who will encrypt data to you - Encrypt file: - qgpg --key ./mykey.pub encrypt ./plain_file.txt ./encrypted_file.txt.gpg -- Encrypt all files in a folder: - qgpg --key ./mykey.pub -r encrypt ./folder/ + qgpg --encrypt --key ./mykey.pub ./plain_file.txt ./encrypted_file.txt.gpg +- Encrypt all files recursively in a folder: + qgpg --encrypt --key ./mykey.pub --recursive ./in_folder/ + qgpg --encrypt --key ./mykey.pub --recursive ./from_folder/ ./to_another_folder/ - Decrypt all files with the private key: - qgpg --key ./mykey -r decrypt ./encrypted_folder/ + qgpg --decrypt --key ./mykey --recursive ./encrypted_folder/ - Using passhprase in a variable: - GPGPASS=mysecretpassword qgpg --key ./mykey decrypt file.gpg file.txt + GPGPASS=mysecretpassword qgpg --key ./mykey --decrypt file.gpg file.txt +- Symmetric encryption (Just leave out the --key): + qgpg --encrypt file.txt file.gpg """, ) - parser.add_argument("--width", help="Console width in characters. Defaults to auto detect.") - parser.add_argument("--no-progress", help="Disable progress meter.", default=False, action="store_true") - parser.add_argument( - "--key", help="path to recipient public key for encryption or private key for decryption", required=True + commands = parser.add_argument_group("Command", "Main command for the program") + commands_grp = commands.add_mutually_exclusive_group(required=True) + commands_grp.add_argument( + "--keygen", + "-k", + default=False, + action="store_true", + help="Create new key pair", ) - parser.add_argument("--name", help="Name of the owner of the key", required=False, default="recipient@address") - parser.add_argument( + commands_grp.add_argument( + "--encrypt", + "-e", + default=False, + action="store_true", + help="Encrypt files", + ) + commands_grp.add_argument( + "--decrypt", + "-d", + default=False, + action="store_true", + help="Decrypt files", + ) + crypt_grp = parser.add_argument_group("Encryption", "Encryption and Decryption options") + + crypt_grp.add_argument( + "--key", + help="Path to key file. For keygen: private key to write; encrypt: public key file, decrypt: private key file. If not defined, symmetric password encryption is used.", + ) + keygen_grp = parser.add_argument_group( + "Keygen", "Options for keygen. Note: --key is required for keygen, see above." + ) + keygen_grp.add_argument( + "--name", + help="Name of the owner of the key (usually email) Default: %(default)s", + required=False, + default="recipient@address", + ) + crypt_grp.add_argument( "--recursive", "-r", default=False, action="store_true", help="Recursive encryption or decryption. Processes full folder structures", ) - - parser.add_argument( - "command", - help="keygen/encrypt/decrypt. you can also use just the first letter.", - choices=["k", "e", "d", "keygen", "encrypt", "decrypt"], + crypt_grp.add_argument( + "--force", + default=False, + action="store_true", + help="Overwrite existing files", ) - parser.add_argument("path", help="path to file to encrypt/decrypt, or folder if recursive", nargs="?") - parser.add_argument("out_path", help="output file or folder when using recursive.", nargs="?") + misc_grp = parser.add_argument_group("Misc", "Other options") + misc_grp.add_argument("--width", help="Console width in characters. Defaults to auto detect.") + misc_grp.add_argument("--no-progress", help="Disable progress meter.", default=False, action="store_true") + misc_grp.add_argument("--version", help="Display version and exit", default=False, action="store_true") + misc_grp.add_argument("--help", "-h", action="help", help="Show this help message and exit") + + crypt_grp.add_argument("path", help="Path to source file to encrypt/decrypt, or folder if recursive", nargs="?") + crypt_grp.add_argument( + "out_path", + help="Path to target file or folder when using recursive. If not specified, use of .gpg extension is automatically used.", + nargs="?", + ) + parsed = parser.parse_args() - for cmd in ("keygen", "encrypt", "decrypt"): - if parsed.command == cmd[0]: - parsed.command = cmd + if parsed.version: + print(__version__) + sys.exit(0) + for cmd in zip(("keygen", "encrypt", "decrypt"), (parsed.keygen, parsed.encrypt, parsed.decrypt)): + if cmd[1]: + parsed.command = cmd[0] + if parsed.command == "keygen": + if parsed.key is None: + parser.error("--key required for keygen") + return parsed @@ -171,26 +225,33 @@ class Processor: self.homedir = tempfile.TemporaryDirectory() self.gpg = gnupg.GPG(gnupghome=self.homedir.name) self.gpg.encoding = "utf-8" + self.symmetric = self.opts.key is None self.suffix = ".gpg" + self.phrase = None if self.opts.command == "keygen": self.keygen() filelist = [] if self.opts.command in ("encrypt", "decrypt"): - import_result = self.gpg.import_keys_file(self.opts.key) + if not self.symmetric: + import_result = self.gpg.import_keys_file(self.opts.key) filelist = self.get_filelist(self.opts.path, self.opts.recursive, self.opts.command) if self.opts.command == "encrypt": - keyid = import_result.fingerprints[0] - public_keys = self.gpg.list_keys() - for key in public_keys: - if key["fingerprint"] == keyid: - self.uid = key["uids"][0] + if self.symmetric: + self.set_phrase(twice=True) + else: + keyid = import_result.fingerprints[0] + public_keys = self.gpg.list_keys() + for key in public_keys: + if key["fingerprint"] == keyid: + self.uid = key["uids"][0] if self.opts.command == "decrypt": self.set_phrase() + n_f = len(filelist) for i, f in enumerate(filelist): print(f"{i+1}/{n_f} {self.opts.command}ing {f} ", file=sys.stderr) @@ -200,16 +261,27 @@ class Processor: def set_phrase(self, twice=False): + if not self.phrase is None: + # phrase already set + return + self.phrase = os.environ.get("GPGPASS", None) if self.phrase is None: - print("Type private key passphrase [Empty if no passphrase]") + print( + "Type symmetric passphrase" + if self.symmetric + else "Type private key passphrase [Empty if no passphrase]" + ) self.phrase = getpass() - if twice: - print("Type private key passphrase again to confirm") + if twice and len(self.phrase) > 0: + print("Type passphrase again to confirm") phrase2 = getpass() if phrase2 != self.phrase: print("Passphrases do not match!") sys.exit(1) + if self.phrase == "" and self.symmetric: + print("Symmetric passphrase required!") + sys.exit(1) def get_filelist(self, root, recurse, direction): if not recurse: @@ -229,9 +301,12 @@ class Processor: return filelist def keygen(self): - if os.path.exists(self.opts.key): - print(f"File {self.opts.key} already exists") - sys.exit(1) + if not self.opts.force: + for f in (self.opts.key, self.opts.key + ".pub"): + if os.path.exists(f): + print(f"File {f} already exists") + sys.exit(1) + print(f"Generating keys for {self.opts.name}...", file=sys.stderr) self.set_phrase(twice=True) input_data = self.gpg.gen_key_input( @@ -264,9 +339,11 @@ class Processor: os.makedirs(os.path.dirname(out_file), exist_ok=True) else: out_file = self.opts.out_path if self.opts.out_path else auto_path - if os.path.exists(out_file): - print(f"{out_file} already exists", file=sys.stderr) - return + + if not self.opts.force: + if os.path.exists(out_file): + print(f"{out_file} already exists", file=sys.stderr) + return file_size = os.path.getsize(in_file) if not self.opts.no_progress: @@ -274,7 +351,12 @@ class Processor: try: with open(in_file, "rb") as fp: if self.opts.command == "encrypt": - d = self.gpg.encrypt_file(fp, self.uid, always_trust=True, armor=False) + if self.symmetric: + d = self.gpg.encrypt_file( + fp, None, symmetric="AES256", passphrase=self.phrase, always_trust=True, armor=False + ) + else: + d = self.gpg.encrypt_file(fp, self.uid, always_trust=True, armor=False) if self.opts.command == "decrypt": d = self.gpg.decrypt_file(fp, passphrase=self.phrase, always_trust=True) if not d.ok: