diff --git a/CHANGES.rst b/CHANGES.rst index cb1f211..9152871 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -57,6 +57,32 @@ ============= +Changes for version 0.5.7 +========================= + +Minor Changes +------------- + +- Tests added to include checks for instance-isolated identities. + + +Major Changes +------------- + +- ``reset_daemon()`` calls added to ``decrypt()``, ``verify()``, ``sign()`` + & ``encrypt()``. This call kills the gpg-agent process & restarts it, + which in turn wipes the caching of secret keys available on the system + without a passphrase. This is crucial for users of applications with + multiple GnuPG objects that handle separate key identities. That's + because these methods will now throw ``PermissionError`` or ``LookupError`` + if a private key operation is needed from an instance which is already + assigned to another private key in the keyring. This gives some important + anonymity protections to users. +- More improvements to error reporting. + + + + Changes for version 0.5.6 ========================= diff --git a/README.rst b/README.rst index 9358c82..bdb7b07 100644 --- a/README.rst +++ b/README.rst @@ -382,6 +382,32 @@ After a user no longer considers a key useful, or wants to dissociate from the k ============= +Changes for version 0.5.7 +========================= + +Minor Changes +------------- + +- Tests added to include checks for instance-isolated identities. + + +Major Changes +------------- + +- ``reset_daemon()`` calls added to ``decrypt()``, ``verify()``, ``sign()`` + & ``encrypt()``. This call kills the gpg-agent process & restarts it, + which in turn wipes the caching of secret keys available on the system + without a passphrase. This is crucial for users of applications with + multiple GnuPG objects that handle separate key identities. That's + because these methods will now throw ``PermissionError`` or ``LookupError`` + if a private key operation is needed from an instance which is already + assigned to another private key in the keyring. This gives some important + anonymity protections to users. +- More improvements to error reporting. + + + + Changes for version 0.5.6 ========================= diff --git a/setup.py b/setup.py index a65b919..6e73541 100755 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup( name="tiny_gnupg", license="GPLv3", - version="0.5.6", + version="0.5.7", description=description, long_description=long_description, url="https://github.com/rmlibre/tiny_gnupg", diff --git a/tests/test_tiny_gnupg.py b/tests/test_tiny_gnupg.py index dc3fb2b..00ffd86 100644 --- a/tests/test_tiny_gnupg.py +++ b/tests/test_tiny_gnupg.py @@ -252,6 +252,57 @@ def test_export_import(gpg): """Successfully blocked non-boolean""" +def test_isolated_identities(gpg): + anon = GnuPG("anon_user", "anonymous@testing.testing", "rubbish_pw") + anon.gen_key() + anon_uid = anon.fingerprint + ### decrypting + enc_msg = gpg.encrypt("hi!", anon_uid) + msg = anon.decrypt(enc_msg) + try: + failed = False + gpg.decrypt(enc_msg) + except LookupError: + failed = True + """ + Same user succefully prevented from decrypting message sent to a + different identity. + """ + finally: + assert failed + ### signatures + sig_0 = anon.sign("message", local_user=anon_uid) + sig_1 = anon.sign("message") + try: + failed = False + gpg.sign("message", local_user=anon_uid) + except PermissionError: + failed = True + """ + Same user successfully prevented from signing message with the + secret key associated with another identity. + """ + finally: + assert failed + ### encrypting + try: + failed = False + gpg.encrypt("greetings", anon_uid, local_user=anon_uid) + except PermissionError: + failed = True + """ + Same user successfully prevented from signing an encrypted message + with the secret key associated with another identity. + """ + finally: + assert failed + while True: + try: + anon.delete(anon.email) + except: + break + + def test_cipher(gpg): test_email = "support@keys.openpgp.org" run(gpg.network_import(test_email)) diff --git a/tiny_gnupg.egg-info/PKG-INFO b/tiny_gnupg.egg-info/PKG-INFO index f70c9b1..2fd07ca 100644 --- a/tiny_gnupg.egg-info/PKG-INFO +++ b/tiny_gnupg.egg-info/PKG-INFO @@ -392,6 +392,32 @@ Description: tiny_gnupg - A small-as-possible solution for handling GnuPG ed2551 ============= + Changes for version 0.5.7 + ========================= + + Minor Changes + ------------- + + - Tests added to include checks for instance-isolated identities. + + + Major Changes + ------------- + + - ``reset_daemon()`` calls added to ``decrypt()``, ``verify()``, ``sign()`` + & ``encrypt()``. This call kills the gpg-agent process & restarts it, + which in turn wipes the caching of secret keys available on the system + without a passphrase. This is crucial for users of applications with + multiple GnuPG objects that handle separate key identities. That's + because these methods will now throw ``PermissionError`` or ``LookupError`` + if a private key operation is needed from an instance which is already + assigned to another private key in the keyring. This gives some important + anonymity protections to users. + - More improvements to error reporting. + + + + Changes for version 0.5.6 ========================= diff --git a/tiny_gnupg/__init__.py b/tiny_gnupg/__init__.py index 94578ca..3551c62 100644 --- a/tiny_gnupg/__init__.py +++ b/tiny_gnupg/__init__.py @@ -8,6 +8,6 @@ # All rights reserved. # -__version__ = "0.5.6" +__version__ = "0.5.7" from .tiny_gnupg import GnuPG, run, __all__ diff --git a/tiny_gnupg/tiny_gnupg.py b/tiny_gnupg/tiny_gnupg.py index 6d97d48..56ab996 100644 --- a/tiny_gnupg/tiny_gnupg.py +++ b/tiny_gnupg/tiny_gnupg.py @@ -186,9 +186,28 @@ def encode_inputs(self, *inputs): def read_output(self, command=(), inputs=b"", **kw): """Quotes terminal escape characters & runs user commands""" - return check_output( - [quote(part) for part in command], input=inputs, **kw - ).decode() + try: + return check_output( + [quote(part) for part in command], input=inputs, **kw + ).decode() + except Exception as source: + error = source + try: + kw.pop("stderr") if "stderr" in kw else 0 + check_output( + [quote(part) for part in command], + input=inputs, + stderr=STDOUT, + **kw, + ).decode() + except CalledProcessError as permissions_check: + notice = "Passphrase wrong, inexistent key, or invalid rights " + notice += "to access secret key." + warning = PermissionError(notice) + warning.inputs = inputs + warning.command = command + warning.output = permissions_check.output.decode() + raise warning if "Bad passphrase" in warning.output else error def gen_key(self): """ @@ -369,6 +388,7 @@ def encrypt(self, message="", uid="", sign=True, local_user=""): matching ``local_user`` or defaults to instance key. Optionally, if ``sign`` == False, ``message`` won't be signed. """ + self.reset_daemon() if sign else 0 uid = self.key_fingerprint(uid) # avoid wkd lookups command = self.command( "--command-fd", @@ -401,6 +421,7 @@ async def auto_encrypt( def decrypt(self, message=""): """Decrypts ``message`` autodetecting correct key from keyring""" + self.reset_daemon() fingerprint = self.packet_fingerprint(message) fingerprint = self.key_fingerprint(fingerprint) try: @@ -412,13 +433,13 @@ def decrypt(self, message=""): try: self.read_output(command, inputs, stderr=STDOUT) except CalledProcessError as error: - output = error.output - error_lines = output.decode().strip().split("\n") + error_lines = error.output.decode().strip().split("\n") sentinel = "gpg: using" uid = [line[-40:] for line in error_lines if sentinel in line] - notice = f"UID '{uid}' not in package keyring" + uid = uid[-1] if uid else "" + notice = f"UID '{uid}' not in the instance's keyring." warning = LookupError(notice) - warning.value = uid[-1] if uid else fingerprint + warning.value = uid if uid else fingerprint raise warning async def auto_decrypt(self, message=""): @@ -438,6 +459,7 @@ def sign(self, target="", local_user="", *, key=False): uid or the instance default. Optionally signs ``target`` message if ``key`` == False. """ + self.reset_daemon() if key == True: # avoid truthiness command = self.command( "--local-user", @@ -464,6 +486,7 @@ def verify(self, message=""): Verifies signed ``message`` if the corresponding public key is in the local keyring. """ + self.reset_daemon() fingerprint = self.packet_fingerprint(message) fingerprint = self.key_fingerprint(fingerprint) try: @@ -489,15 +512,15 @@ async def auto_verify(self, message=""): def raw_list_keys(self, uid="", secret=False): """Returns the terminal output of the --list-keys ``uid`` option""" - secret = "-secret" if secret else "" + secret = "secret-" if secret else "" if uid: - command = self.command(f"--list{secret}-keys", uid) + command = self.command(f"--list-{secret}keys", uid) else: - command = self.command(f"--list{secret}-keys") + command = self.command(f"--list-{secret}keys") try: return self.read_output(command) except CalledProcessError: - notice = f"UID '{uid}' not in package keyring" + notice = f"UID '{uid}' not in package {secret}keyring" warning = LookupError(notice) warning.value = uid raise warning