Skip to content

Commit 0d8920d

Browse files
committed
v0.4.6 update - all the docstrings
1 parent 4ae9289 commit 0d8920d

1 file changed

Lines changed: 105 additions & 0 deletions

File tree

tiny_gnupg/tiny_gnupg.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,20 @@
2424

2525

2626
class GnuPG:
27+
"""
28+
GnuPG - A linux specific, small, simple & intuitive wrapper for
29+
creating, using and managing GnuPG's Ed-25519 curve keys. This class
30+
favors reducing code size & complexity with strong, bias defaults
31+
over flexibility in the api. It's designed to turn the complex,
32+
legacy, but powerful gnupg system into a fun tool to develop with.
33+
"""
2734
def __init__(
2835
self, username="", email="", passphrase="", torify=False
2936
):
37+
"""
38+
Initialize an instance intended to create, manage, or represent
39+
a single key in the local package gnupg keyring
40+
"""
3041
self.set_homedir()
3142
self.email = email
3243
self.username = username
@@ -36,14 +47,17 @@ def __init__(
3647
self.set_network_variables()
3748

3849
def set_homedir(self, path=HOME_PATH):
50+
"""Initialize a home directory to store gpg2 binary & data"""
3951
self.home = self.format_homedir(path)
4052
self.executable = str(Path(self.home).absolute() / "gpg2")
4153
self.set_home_permissions(self.home)
4254

4355
def format_homedir(self, path=HOME_PATH):
56+
"""Return an absolute path string for the home directory"""
4457
return str(Path(path).absolute())
4558

4659
def set_home_permissions(self, home):
60+
"""Set safer permissions on the home directory"""
4761
try:
4862
home = str(Path(home).absolute())
4963
command = ["chmod", "-R", "700", home]
@@ -52,6 +66,7 @@ def set_home_permissions(self, home):
5266
print(f"Invalid permission to modify home folder: {home}")
5367

5468
def set_base_command(self, torify=False):
69+
"""Contruct the default commands used to call gnupg2"""
5570
torify = ["torify"] if torify else []
5671
self.base_passphrase_command = torify + [
5772
self.executable,
@@ -79,6 +94,7 @@ def set_base_command(self, torify=False):
7994
]
8095

8196
def set_fingerprint(self, uid=""):
97+
"""Populate `fingerprint` attribute for persistent user"""
8298
try:
8399
self.fingerprint = self.key_fingerprint(uid)
84100
except:
@@ -91,6 +107,7 @@ def set_network_variables(
91107
keyserver="http://zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion",
92108
search="search?q=",
93109
):
110+
"""Set network variables for adaptable implementations"""
94111
self.port = port
95112
self.tor_port = tor_port
96113
self._keyserver = keyserver
@@ -100,22 +117,27 @@ def set_network_variables(
100117

101118
@property
102119
def keyserver(self):
120+
"""Autoconstruct keyserver URL with adaptable port number"""
103121
return f"{self._keyserver}:{self.port}/"
104122

105123
@property
106124
def keyserver_export_api(self):
125+
"""Autoconstruct specific keyserver key upload api URL"""
107126
return self.keyserver + "vks/v1/upload"
108127

109128
@property
110129
def keyserver_verify_api(self):
130+
"""Autoconstruct specific keyserver key verification api URL"""
111131
return self.keyserver + "vks/v1/request-verify"
112132

113133
@property
114134
def searchserver(self):
135+
"""Autoconstruct specific keyserver search URL"""
115136
return f"{self.keyserver}{self._search_string}"
116137

117138
@property
118139
def connector(self):
140+
"""Autoconstruct an aiohttp_socks.SocksConnector instance"""
119141
return self._connector(
120142
socks_ver=SocksVer.SOCKS5,
121143
host="127.0.0.1",
@@ -125,10 +147,12 @@ def connector(self):
125147

126148
@property
127149
def session(self):
150+
"""Autoconstruct an aiohttp.ClientSession instance"""
128151
return self._session(connector=self.connector)
129152

130153
@async_contextmanager
131154
async def network_get(self, url="", **kw):
155+
"""Opens a aiohttp.ClientSession.get context manager"""
132156
try:
133157
session = await self.session.__aenter__()
134158
yield await session.get(url, **kw)
@@ -137,37 +161,50 @@ async def network_get(self, url="", **kw):
137161

138162
@async_contextmanager
139163
async def network_post(self, url="", **kw):
164+
"""Opens a aiohttp.ClientSession.post context manager"""
140165
try:
141166
session = await self.session.__aenter__()
142167
yield await session.post(url, **kw)
143168
finally:
144169
await session.close()
145170

146171
async def get(self, url="", **kw):
172+
"""Returns text of an aiohttp.ClientSession.get request"""
147173
async with self.network_get(url, **kw) as response:
148174
return await response.text()
149175

150176
async def post(self, url="", **kw):
177+
"""Returns text of an aiohttp.ClientSession.post request"""
151178
async with self.network_post(url, **kw) as response:
152179
return await response.text()
153180

154181
def command(self, *options, with_passphrase=False):
182+
"""Autoformats gpg2 commands soley from additional options"""
155183
if with_passphrase:
156184
return self.base_passphrase_command + [*options]
157185
else:
158186
return self.base_command + [*options]
159187

160188
def encode_inputs(self, *inputs):
189+
"""Prepares inputs *X for subprocess.check_output(input=*X)"""
161190
return ("\n".join(inputs) + "\n").encode()
162191

163192
def read_output(self, command=(), inputs=b"", shell=False):
193+
"""Quotes terminal escape characters & runs user commands"""
164194
return check_output(
165195
[quote(part) for part in command],
166196
input=inputs,
167197
shell=shell,
168198
).decode()
169199

170200
def gen_key(self):
201+
"""
202+
Generates a set of ed25519 keys with isolated roles:
203+
Main Key - Certification
204+
Subkey - Signing
205+
Subkey - Authentication
206+
Subkey - Encryption
207+
"""
171208
command = [
172209
self.executable,
173210
"--yes",
@@ -204,6 +241,13 @@ def gen_key(self):
204241
return self.add_subkeys(self.fingerprint)
205242

206243
def add_subkeys(self, uid=""):
244+
"""
245+
Adds three subkeys with isolated roles to key matching `uid`:
246+
`uid` Key
247+
Subkey - Signing
248+
Subkey - Authentication
249+
Subkey - Encryption
250+
"""
207251
command = self.command(
208252
"--command-fd",
209253
"0",
@@ -234,6 +278,7 @@ def add_subkeys(self, uid=""):
234278
return self.read_output(command, inputs)
235279

236280
def delete(self, uid=""):
281+
"""Deletes secret & public key matching `uid` from keyring"""
237282
uid = self.key_fingerprint(uid) # avoid non-fingerprint uid crash
238283
try:
239284
command = self.command(
@@ -251,6 +296,7 @@ def delete(self, uid=""):
251296
return self.read_output(command, inputs)
252297

253298
def revoke(self, uid=""):
299+
"""Imports & generates revocation cert for key matching `uid`"""
254300
command = self.command(
255301
"--command-fd",
256302
"0",
@@ -264,6 +310,7 @@ def revoke(self, uid=""):
264310
return self.text_import(revoke_cert)
265311

266312
def trust(self, uid="", level=5):
313+
"""Sets trust `level` to key matching `uid` in the keyring"""
267314
level = str(int(level))
268315
if not 1 <= int(level) <= 5:
269316
raise ValueError("Trust levels must be between 1 and 5.")
@@ -272,6 +319,11 @@ def trust(self, uid="", level=5):
272319
return self.read_output(command, inputs)
273320

274321
def encrypt(self, message="", uid="", sign=True, local_user=""):
322+
"""
323+
Encrypts `message` to key matching `uid` & signs with key
324+
matching `local_user` or defaults to instance key. Optionally
325+
doesn't sign `message`.
326+
"""
275327
uid = self.key_fingerprint(uid) # avoid wkd lookups
276328
command = self.command(
277329
"--command-fd",
@@ -290,11 +342,17 @@ def encrypt(self, message="", uid="", sign=True, local_user=""):
290342
return self.read_output(command, inputs[:-1])
291343

292344
def decrypt(self, message=""):
345+
"""Decrypts `message` autodetecting correct key from keyring"""
293346
command = self.command("-d", with_passphrase=True)
294347
inputs = self.encode_inputs(self.passphrase, message)
295348
return self.read_output(command, inputs)
296349

297350
def sign(self, target="", local_user="", *, key=False):
351+
"""
352+
Signs key matching `target` uid with a key matching `local_user`
353+
uid or the instance default. Optionally signs `target` message
354+
if `key`==False.
355+
"""
298356
if key == True: # avoid truthiness
299357
command = self.command(
300358
"--local-user",
@@ -317,18 +375,27 @@ def sign(self, target="", local_user="", *, key=False):
317375
return self.read_output(command, inputs)
318376

319377
def verify(self, message=""):
378+
"""
379+
Verifies signed `message` if the corresponding public key is in
380+
the local keyring
381+
"""
320382
command = self.command("--verify")
321383
inputs = self.encode_inputs(message)
322384
return self.read_output(command, inputs)
323385

324386
def raw_list_keys(self, uid=""):
387+
"""Returns the terminal output of the --list-keys `uid` option"""
325388
if uid:
326389
command = self.command("--list-keys", uid)
327390
else:
328391
command = self.command("--list-keys")
329392
return self.read_output(command)
330393

331394
def format_list_keys(self, raw_list_keys_terminal_output):
395+
"""
396+
Returns a dict of fingerprints & email addresses scraped from
397+
the terminal output of the --list-keys option
398+
"""
332399
keys = raw_list_keys_terminal_output.split("\npub ")
333400
fingerprints = [
334401
part[part.find("\nuid") - 40 : part.find("\nuid")]
@@ -342,9 +409,14 @@ def format_list_keys(self, raw_list_keys_terminal_output):
342409
return dict(zip(fingerprints, emails))
343410

344411
def list_keys(self, uid=""):
412+
"""
413+
Returns a dict of fingerprints & email addresses of all keys in
414+
the local keyring, or optionally the key matching `uid`.
415+
"""
345416
return self.format_list_keys(self.raw_list_keys(uid))
346417

347418
def key_email(self, uid=""):
419+
"""Returns the email address on the key matching `uid`"""
348420
parts = self.raw_list_keys(uid).replace(" ", "")
349421
for part in parts.split("\nuid"):
350422
if "@" in part and "]" in part:
@@ -354,15 +426,18 @@ def key_email(self, uid=""):
354426
return part
355427

356428
def key_fingerprint(self, uid=""):
429+
"""Returns the fingerprint on the key matching `uid`"""
357430
key = self.list_keys(uid)
358431
return next(iter(key))
359432

360433
def key_trust(self, uid=""):
434+
"""Returns the current trust level on the key matching `uid`"""
361435
key = self.raw_list_keys(uid).replace(" ", "")
362436
trust = key[key.find("\nuid[") + 5 :]
363437
return trust[: trust.find("]")]
364438

365439
def reset_daemon(self):
440+
"""Resets the gpg-agent daemon"""
366441
command = [
367442
"gpgconf",
368443
"--homedir",
@@ -376,11 +451,13 @@ def reset_daemon(self):
376451
return kill_output, reset_output
377452

378453
async def raw_search(self, query=""):
454+
"""Returns HTML of keyserver key search matching `query` uid"""
379455
url = f"{self.searchserver}{query}"
380456
print(f"querying: {url}")
381457
return await self.get(url)
382458

383459
async def search(self, query=""):
460+
"""Returns keyserver URL of the key found from `query` uid"""
384461
query = query.replace("@", "%40")
385462
response = await self.raw_search(query)
386463
if "We found an entry" not in response:
@@ -389,6 +466,7 @@ async def search(self, query=""):
389466
return part[: part.find("</a>")]
390467

391468
async def network_import(self, uid=""):
469+
"""Imports the key matching `uid` from the keyserver."""
392470
key_url = await self.search(uid)
393471
if not key_url:
394472
raise FileNotFoundError("No key found on server.")
@@ -398,11 +476,13 @@ async def network_import(self, uid=""):
398476
return self.text_import(key)
399477

400478
async def file_import(self, path="", mode="r"):
479+
"""Imports a key from the file located at `path`"""
401480
async with aiofiles.open(path, mode) as keyfile:
402481
key = await keyfile.read()
403482
return self.text_import(key)
404483

405484
def text_import(self, key=""):
485+
"""Imports the `key` string into the local keyring"""
406486
command_bugfix = self.command(
407487
"--import-options", "import-drop-uids", "--import"
408488
)
@@ -416,6 +496,15 @@ def text_import(self, key=""):
416496
return self.read_output(command, inputs)
417497

418498
async def raw_api_export(self, uid=""):
499+
"""
500+
Uploads the key matching `uid` to the keyserver. Returns a json
501+
string
502+
'''{
503+
"key-fpr": self.fingerprint,
504+
"status": {self.email: "unpublished"},
505+
"token": api_token,
506+
}'''
507+
"""
419508
key = self.text_export(uid)
420509
url = self.keyserver_export_api
421510
print(f"contacting: {url}")
@@ -424,11 +513,18 @@ async def raw_api_export(self, uid=""):
424513
return await self.post(url, json=payload)
425514

426515
async def raw_api_verify(self, payload=""):
516+
"""
517+
Prompts the keyserver to verify the list of email addresses in
518+
`payload`["addresses"] with the api_token in `payload`["token"].
519+
The keyserver then sends a confirmation email asking for consent
520+
to publish the uid information with the key that was uploaded
521+
"""
427522
url = self.keyserver_verify_api
428523
print(f"sending verification to: {url}")
429524
return await self.post(url, json=payload)
430525

431526
async def network_export(self, uid=""):
527+
"""Exports the key matching `uid` to the keyserver"""
432528
response = json.loads(await self.raw_api_export(uid))
433529
payload = {
434530
"addresses": [self.key_email(uid)],
@@ -441,13 +537,22 @@ async def network_export(self, uid=""):
441537
async def file_export(
442538
self, path="", uid="", mode="w+", *, secret=False
443539
):
540+
"""
541+
Exports the public key matching `uid` to the `path` directory.
542+
If `secret`==True then exports the secret key that matches `uid`
543+
"""
444544
key = self.text_export(uid, secret=secret)
445545
fingerprint = self.key_fingerprint(uid)
446546
filename = Path(path).absolute() / (fingerprint + ".asc")
447547
async with aiofiles.open(filename, mode) as keyfile:
448548
return await keyfile.write(key)
449549

450550
def text_export(self, uid="", *, secret=False):
551+
"""
552+
Returns a public key string that matches `uid`. Optionally,
553+
returns the secret key as a string that matches `uid` if
554+
`secret`==True
555+
"""
451556
if secret == True: # avoid truthiness
452557
command = self.command(
453558
"-a", "--export-secret-keys", uid, with_passphrase=True

0 commit comments

Comments
 (0)