Skip to content

Commit

Permalink
v0.4.0 update - new sign(key=True) kwarg & code cleanups
Browse files Browse the repository at this point in the history
  • Loading branch information
rmlibre committed Dec 15, 2019
1 parent 0c12db2 commit c77e864
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 35 deletions.
9 changes: 5 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
tiny_gnupg.egg-info
.pytest_cache
__pycache__
openpgp-revocs.d
private-keys-v1.d
pubring.kbx
openpgp-revocs.d
pubring.kbx~
pubring.kbx
random_seed
trustdb.gpg
sshcontrol
tofu.db
venv
env
build
dist
venv
env

18 changes: 17 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
# Changes for version 0.3.9
# Changes for version 0.4.0
## Known Issues
- Because of Debian [bug #930665](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=930665), and related GnuPG [bug #T4393](https://dev.gnupg.org/T4393), importing keys from the default keyserver [keys.openpgp.org](https://keys.openpgp.org/) doesn't work automatically on all systems. Not without email confirmation, at least. That's because the keyserver will not publish uid information attached to a key before a user confirms access to the email address assigned to the uploaded key. And, because GnuPG folks are still holding up the merging, and back-porting, of patches that would allow GnuPG to automatically handle keys without uids gracefully. This effects the `network_import()` method specifically, but also the `text_import()` and `file_import()` methods, if they happen to be passed a key or filename argument which refers to a key without uid information. The gpg2 binary in this package can be replaced manually if a user's system has access to a patched version.
- This program may only be reliably compatible with keys that are also created with this program. That's because our terminal parsing is reliant on specific metadata to be similar across all encountered keys. It seems most keys have successfully been parsed with recent updates, though more testing is needed.
- Currently, the package is part synchronous, and part asynchronous. This is not ideal, so a decision has to be made: either to stay mixed style, or choose one consistent style.
- We're still in unstable and have to build out our test suite. Contributions welcome.
## Minor Changes
- Added keywords to `setup.py`
- Added copyright notice to LICENSE file.
- Code cleanups.
- Updated README.rst tutorials.
- Added new tests.
- Include .gitignore in MANIFEST.in for PyPI.
- Made all path manipulations more consistent by strictly using pathlib.Path for directory specifications.
- Added strict truthiness avoidance to `sign()` for the `key` boolean kwarg.
- Added strict truthiness avoidance to `text_export()` for the `secret` boolean kwarg.
## Major Changes
- Added `key` kwarg to the `sign(target="", key=False)` method to allow users to toggle between signing arbitrary data and signing a key in the package's local keyring.
- Changed the `message` kwarg in `sign(message="")` to `target` so it is also accurate when the method is used to sign keys instead of arbitrary data.


# Changes for version 0.3.9
## Minor Changes
- Added new tests.
## Major Changes
- Fixed new crash caused by `--batch` keyword in `encrypt()`. When a key being used to encrypt isn't ultimately trusted, gnupg raises an error, but this isn't a desired behavior. So, `--batch` is removed from the command sent from the method.
Expand Down
9 changes: 9 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
This file is part of tiny_gnupg, a small-as-possible solution for
handling GnuPG ECC keys.

Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
Copyright © 2019-2020 Gonzo Investigatory Journalism Agency, LLC
<[email protected]>
© 2019-2020 Richard Machado <[email protected]>
All rights reserved.


GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ recursive-include licenses *
include LICENSE
include CHANGES.md
include README.rst
include .gitignore
include tests/test_tiny_gnupg.py
include tiny_gnupg/__init__.py
include tiny_gnupg/tiny_gnupg.py
Expand Down
22 changes: 17 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ Networking Example
# Check your ip address for fun ->
ip_addr = run(read_url("https://icanhazip.com/"))
# There's a convenience function built into the class that
# basically mimics read_url() ->
ip_addr = run(gpg.get("https://icanhazip.com/"))
# POST requests can also be sent with the network_post() method.
# Let's use a POST request to send the keyserver a new key we
Expand All @@ -135,6 +139,11 @@ Networking Example
url = gpg.keyserver_export_api
payload = {"keytext": gpg.text_export(gpg.fingerprint)}
api_token_json = run(post(gpg, url, payload))
# There's also a convenience function built into the class that
# mimics post() ->
api_token_json = run(gpg.post(url, json=payload))
# And there we have it, it's super simple. And these requests have
# the added benefit of being completely routed through tor. The
# keyserver here also has a v3 onion address which we use to query,
Expand Down Expand Up @@ -166,6 +175,9 @@ Extras
# encrypting ->
signed_data = gpg.sign("maybe a hash of a file?")
# Or sign a key in the package's keyring ->
gpg.sign("[email protected]", key=True)
# And verify data as well ->
gpg.verify(signed_data) # throws if invalid
Expand All @@ -177,14 +189,14 @@ Extras
path_to_file = "/home/user/keyfiles/"
run(gpg.file_import(path=path_to_file + "alices_key.asc"))
# And exporting ->
# As well as exporting public keys ->
run(gpg.file_export(path=path_to_file, uid=gpg.email))
# And secret keys, but really, keep those safe! ->
run(gpg.file_export(path=path_to_file, uid=gpg.email, secret=True))
# When a user is done with a key, it can be deleted from the package
# keyring like this ->
gpg.delete("[email protected]") # You'll have to manually click
# the confirm button, though.
gpg.delete("[email protected]")
27 changes: 25 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
setup(
name="tiny_gnupg",
license="GPLv3",
version="0.3.9",
version="0.4.0",
description=description,
long_description=long_description,
url="https://github.com/rmlibre/tiny_gnupg",
Expand All @@ -46,13 +46,36 @@
"Intended Audience :: Information Technology",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
],
keywords=" ".join(
[
"gpg",
"gpg2",
"gnupg",
"gnupg2",
"await",
"async",
"asyncio",
"clean",
"code",
"tor",
"elliptic",
"curve",
"crypto",
"ed25519",
"wrapper",
"anonymous",
"anonymity",
"security",
"beta testing",
]
),
include_package_data=True,
install_requires=[
"pathlib",
"aiohttp",
"aiofiles",
"asyncio_contextmanager",
"aiohttp_socks",
"asyncio_contextmanager",
],
tests_require=["pytest"],
packages=find_packages(),
Expand Down
24 changes: 19 additions & 5 deletions tests/test_tiny_gnupg.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
from pathlib import Path
from aiohttp import ClientSession
from aiohttp_socks import SocksConnector
from multiprocessing import Process

PACKAGE_PATH = str(Path(__file__).parent.parent)
PACKAGE_PATH = str(Path(__file__).absolute().parent.parent)
sys.path.append(PACKAGE_PATH)
run = asyncio.get_event_loop().run_until_complete
new_task = asyncio.get_event_loop().create_task
Expand All @@ -31,7 +30,7 @@ def gpg():
username = "testing_user"
email = "[email protected]"
passphrase = "test_passphrase"
relative_gpg_path = PACKAGE_PATH + "/tiny_gnupg/gpghome"
relative_gpg_path = str(Path(PACKAGE_PATH).absolute() / "tiny_gnupg/gpghome")
gpg = GnuPG(username, email, passphrase)
gpg.set_homedir(relative_gpg_path)
gpg.reset_daemon()
Expand Down Expand Up @@ -123,8 +122,8 @@ def test_cipher(gpg):


def test_file_io(gpg):
path = gpg.home
file_path = f"{path}/{gpg.fingerprint}.asc"
path = Path(gpg.home).absolute()
file_path = str(path / f"{gpg.fingerprint}.asc")
key = gpg.text_export(gpg.fingerprint)
run(gpg.file_export(path, gpg.fingerprint))
run(gpg.file_import(file_path))
Expand Down Expand Up @@ -208,7 +207,21 @@ async def looper(gpg, uid):
assert url == link


def test_key_signing(gpg):
dev_email = "[email protected]"
dev_fingerprint = "31FDCC4F9961AFAC522A9D41AE2B47FA1EF44F0A"
command = gpg.command("--check-sigs")
keyring = gpg.read_output(command)
gpg.sign(dev_fingerprint, key=True)
signed_keying = gpg.read_output(command)
assert keyring != signed_keying
condensed_keyring = signed_keying.replace(" ", "")
fingerprint = gpg.fingerprint[-16:]
assert f"<{dev_email}>\nsig!{fingerprint}" in condensed_keyring


def test_delete(gpg):
dev_email = "[email protected]"
email = "[email protected]"
amount_of_test_keys = 0
for key_email in gpg.list_keys().values():
Expand All @@ -220,3 +233,4 @@ def test_delete(gpg):
if key_email == email:
amount_of_test_keys_after_delete += 1
assert amount_of_test_keys - 1 == amount_of_test_keys_after_delete
gpg.delete(dev_email)
2 changes: 1 addition & 1 deletion tiny_gnupg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
# All rights reserved.
#

__version__ = "0.3.9"
__version__ = "0.4.0"

from .tiny_gnupg import GnuPG, __all__
55 changes: 38 additions & 17 deletions tiny_gnupg/tiny_gnupg.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from aiohttp_socks import SocksConnector, SocksVer


HOME_PATH = Path(__file__).parent / "gpghome"
HOME_PATH = Path(__file__).absolute().parent / "gpghome"


class GnuPG:
Expand All @@ -34,13 +34,20 @@ def __init__(self, username="", email="", passphrase=""):

def set_homedir(self, path=HOME_PATH):
self.home = self.format_homedir(path)
self.executable = self.home + "/gpg2"
command = ["chmod", "-R", "700", self.home]
return self.read_output(command)
self.executable = str(Path(self.home).absolute() / "gpg2")
self.set_home_permissions(self.home)

def format_homedir(self, path=HOME_PATH):
return str(Path(path).absolute())

def set_home_permissions(self, home):
try:
home = str(Path(home).absolute())
command = ["chmod", "-R", "700", self.home]
return self.read_output(command)
except:
print(f"Invalid permission to modify home folder: {home}")

def set_fingerprint(self, uid=""):
try:
self.fingerprint = self.key_fingerprint(uid)
Expand Down Expand Up @@ -251,8 +258,8 @@ def encrypt(self, message="", uid="", sign=True, local_user=""):
uid,
with_passphrase=True,
)
command.remove("--batch") # avoid crash with untrusted keys
if self.key_trust(uid) != "ultimate":
command.remove("--batch") # avoid crash with untrusted keys
inputs = self.encode_inputs(self.passphrase, "y", message)
else:
inputs = self.encode_inputs(self.passphrase, message)
Expand All @@ -263,14 +270,26 @@ def decrypt(self, message=""):
inputs = self.encode_inputs(self.passphrase, message)
return self.read_output(command, inputs)

def sign(self, message="", local_user=""):
command = self.command(
"--local-user",
local_user if local_user else self.fingerprint,
"-as",
with_passphrase=True,
)
inputs = self.encode_inputs(self.passphrase, message)
def sign(self, target="", local_user="", *, key=False):
if key == True: # avoid truthiness
command = self.command(
"--local-user",
local_user if local_user else self.fingerprint,
"--sign-key",
target,
with_passphrase=True,
)
inputs = self.encode_inputs(self.passphrase)
elif key == False:
command = self.command(
"--local-user",
local_user if local_user else self.fingerprint,
"-as",
with_passphrase=True,
)
inputs = self.encode_inputs(self.passphrase, target)
else:
raise ValueError(f"key != boolean, {type(key)} given.")
return self.read_output(command, inputs)

def verify(self, message=""):
Expand Down Expand Up @@ -317,7 +336,7 @@ def key_fingerprint(self, uid=""):
def key_trust(self, uid=""):
key = self.raw_list_keys(uid).replace(" ", "")
trust = key[key.find("\nuid[") + 5 :]
return trust[: trust.find("]")].strip()
return trust[: trust.find("]")]

def reset_daemon(self):
command = [
Expand Down Expand Up @@ -400,17 +419,19 @@ async def file_export(
):
key = self.text_export(uid, secret=secret)
fingerprint = self.key_fingerprint(uid)
filename = Path(path) / (fingerprint + ".asc")
filename = Path(path).absolute() / (fingerprint + ".asc")
async with aiofiles.open(filename, mode) as keyfile:
return await keyfile.write(key)

def text_export(self, uid="", *, secret=False):
if secret == True: # make strictly True, not just truthy
if secret == True: # avoid truthiness
command = self.command(
"-a", "--export-secret-keys", uid, with_passphrase=True
)
inputs = self.encode_inputs(self.passphrase)
return self.read_output(command, inputs)
else:
elif secret == False:
command = self.command("-a", f"--export", uid)
return self.read_output(command)
else:
raise ValueError(f"secret != boolean, {type(secret)} given")

0 comments on commit c77e864

Please sign in to comment.