2424
2525
2626class 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 ("\n pub " )
333400 fingerprints = [
334401 part [part .find ("\n uid" ) - 40 : part .find ("\n uid" )]
@@ -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 ("\n uid" ):
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 ("\n uid[" ) + 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