checking in some stuff
This commit is contained in:
		
							parent
							
								
									69b6ec60d0
								
							
						
					
					
						commit
						a376bea0e9
					
				
							
								
								
									
										14
									
								
								TODO
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								TODO
									
									
									
									
									
								
							@ -1,11 +1,12 @@
 | 
				
			|||||||
- write classes/functions
 | 
					- write classes/functions
 | 
				
			||||||
- XML-based config
 | 
					- XML-based config
 | 
				
			||||||
-x XML syntax
 | 
					-x XML syntax
 | 
				
			||||||
--- xregex btags - case-insensitive? this can be represented in-pattern:
 | 
					--- x regex btags - case-insensitive? this can be represented in-pattern:
 | 
				
			||||||
    xhttps://stackoverflow.com/a/9655186/733214
 | 
					    x https://stackoverflow.com/a/9655186/733214
 | 
				
			||||||
 | 
					--- remove sources stuff - that should be in the guest definitions.
 | 
				
			||||||
-x configuration generator
 | 
					-x configuration generator
 | 
				
			||||||
--- xprint end result xml config to stderr for easier redirection? or print prompts to stderr and xml to stdout?
 | 
					--- x print end result xml config to stderr for easier redirection? or print prompts to stderr and xml to stdout?
 | 
				
			||||||
-- xXSD for validation
 | 
					-- x XSD for validation
 | 
				
			||||||
-- Flask app for generating config?
 | 
					-- Flask app for generating config?
 | 
				
			||||||
-- TKinter (or pygame?) GUI?
 | 
					-- TKinter (or pygame?) GUI?
 | 
				
			||||||
--- https://docs.python.org/3/faq/gui.html
 | 
					--- https://docs.python.org/3/faq/gui.html
 | 
				
			||||||
@ -15,9 +16,12 @@
 | 
				
			|||||||
  at the very least document all the functions and such so pydoc's happy.
 | 
					  at the very least document all the functions and such so pydoc's happy.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- locking
 | 
					- locking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- for docs, 3.x (as of 3.10) was 2.4M.
 | 
					- for docs, 3.x (as of 3.10) was 2.4M.
 | 
				
			||||||
- xNeed ability to write/parse mtree specs (or a similar equivalent) for applying ownerships/permissions to overlay files
 | 
					
 | 
				
			||||||
 | 
					- x Need ability to write/parse mtree specs (or a similar equivalent) for applying ownerships/permissions to overlay files
 | 
				
			||||||
-- parsing is done. writing may? come later.
 | 
					-- parsing is done. writing may? come later.
 | 
				
			||||||
 | 
					--- i think writing is mostly done/straightforward; still need to work on parsing mode octals for files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- package for PyPI:
 | 
					- package for PyPI:
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										528
									
								
								bdisk/GPG.py
									
									
									
									
									
								
							
							
						
						
									
										528
									
								
								bdisk/GPG.py
									
									
									
									
									
								
							@ -1,8 +1,71 @@
 | 
				
			|||||||
 | 
					import copy
 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
import gpg
 | 
					import gpg
 | 
				
			||||||
 | 
					import operator
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import psutil
 | 
					import re
 | 
				
			||||||
import gpg.errors
 | 
					import utils  # LOCAL
 | 
				
			||||||
 | 
					from functools import reduce
 | 
				
			||||||
 | 
					from gpg import gpgme
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Reference material.
 | 
				
			||||||
 | 
					# http://files.au.adversary.org/crypto/GPGMEpythonHOWTOen.html
 | 
				
			||||||
 | 
					# https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gpgme.git;a=tree;f=lang/python/examples;hb=HEAD
 | 
				
			||||||
 | 
					# https://www.gnupg.org/documentation/manuals/gpgme.pdf
 | 
				
			||||||
 | 
					# Support ECC? https://www.gnupg.org/faq/whats-new-in-2.1.html#ecc
 | 
				
			||||||
 | 
					# section 4.1, 4.2, 7.5.1, 7.5.5 in gpgme manual
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# These are static values. We include them in the parent so we don't define them every time a function is called.
 | 
				
			||||||
 | 
					# Key signature attributes.
 | 
				
			||||||
 | 
					_keysig_attrs = ('comment', 'email', 'expired', 'expires', 'exportable', 'invalid', 'keyid', 'name', 'notations',
 | 
				
			||||||
 | 
					                 'pubkey_algo', 'revoked', 'sig_class', 'status', 'timestamp', 'uid')
 | 
				
			||||||
 | 
					# Data signature attributes.
 | 
				
			||||||
 | 
					_sig_attrs = ('chain_model', 'exp_timestamp', 'fpr', 'hash_algo', 'is_de_vs', 'key', 'notations', 'pka_address',
 | 
				
			||||||
 | 
					              'pka_trust', 'pubkey_algo', 'status', 'summary', 'timestamp', 'validity', 'validity_reason',
 | 
				
			||||||
 | 
					              'wrong_key_usage')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# A regex that ignores signature verification validity errors we don't care about.
 | 
				
			||||||
 | 
					_valid_ignore = re.compile(('^('
 | 
				
			||||||
 | 
					                            #'CHECKSUM|'
 | 
				
			||||||
 | 
					                            'ELEMENT_NOT_FOUND|'
 | 
				
			||||||
 | 
					                            'MISSING_VALUE|'
 | 
				
			||||||
 | 
					                            #'UNKNOWN_PACKET|'
 | 
				
			||||||
 | 
					                            'UNSUPPORTED_CMS_OBJ|'
 | 
				
			||||||
 | 
					                            'WRONG_SECKEY|'
 | 
				
			||||||
 | 
					                                '('
 | 
				
			||||||
 | 
					                                    'DECRYPT|'
 | 
				
			||||||
 | 
					                                    'INV|'
 | 
				
			||||||
 | 
					                                    'NO|'
 | 
				
			||||||
 | 
					                                    'PIN|'
 | 
				
			||||||
 | 
					                                    'SOURCE'
 | 
				
			||||||
 | 
					                                ')_'
 | 
				
			||||||
 | 
					                            ')'))
 | 
				
			||||||
 | 
					# A function to build a list based on the above.
 | 
				
			||||||
 | 
					def _gen_valid_validities():
 | 
				
			||||||
 | 
					    # Strips out and minimizes the error output.
 | 
				
			||||||
 | 
					    v = {}
 | 
				
			||||||
 | 
					    for s in dir(gpg.constants.validity):
 | 
				
			||||||
 | 
					        if _valid_ignore.search(s):
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        val = getattr(gpg.constants.validity, s)
 | 
				
			||||||
 | 
					        if not isinstance(val, int):
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        v[s] = val
 | 
				
			||||||
 | 
					    return(v)
 | 
				
			||||||
 | 
					_valid_validities = _gen_valid_validities()
 | 
				
			||||||
 | 
					def _get_sigstatus(status):
 | 
				
			||||||
 | 
					    statuses = []
 | 
				
			||||||
 | 
					    for e in _valid_validities:
 | 
				
			||||||
 | 
					        if ((status & _valid_validities[e]) == _valid_validities[e]):
 | 
				
			||||||
 | 
					            statuses.append(e)
 | 
				
			||||||
 | 
					    return(statuses)
 | 
				
			||||||
 | 
					def _get_sig_isgood(sigstat):
 | 
				
			||||||
 | 
					    is_good = True
 | 
				
			||||||
 | 
					    if not ((sigstat & gpg.constants.sigsum.GREEN) == gpg.constants.sigsum.GREEN):
 | 
				
			||||||
 | 
					        is_good = False
 | 
				
			||||||
 | 
					    if not ((sigstat & gpg.constants.sigsum.VALID) == gpg.constants.sigsum.VALID):
 | 
				
			||||||
 | 
					        is_good = False
 | 
				
			||||||
 | 
					    return(is_good)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This helps translate the input name from the conf to a string compatible with the gpg module.
 | 
					# This helps translate the input name from the conf to a string compatible with the gpg module.
 | 
				
			||||||
@ -21,52 +84,94 @@ def _epoch_helper(epoch):
 | 
				
			|||||||
    return(abs(int(d.total_seconds())))  # Returns a positive integer even if negative...
 | 
					    return(abs(int(d.total_seconds())))  # Returns a positive integer even if negative...
 | 
				
			||||||
    #return(int(d.total_seconds()))
 | 
					    #return(int(d.total_seconds()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# http://files.au.adversary.org/crypto/GPGMEpythonHOWTOen.html
 | 
					# _KeyEditor and _getEditPrompt are used to interactively edit keys -- notably currently used for editing trusts
 | 
				
			||||||
# https://www.gnupg.org/documentation/manuals/gpgme.pdf
 | 
					# (since there's no way to edit trust otherwise).
 | 
				
			||||||
# Support ECC? https://www.gnupg.org/faq/whats-new-in-2.1.html#ecc
 | 
					# https://www.gnupg.org/documentation/manuals/gpgme/Advanced-Key-Editing.html
 | 
				
			||||||
# section 4.1, 4.2, 7.5.1, 7.5.5 in gpgme manual
 | 
					# https://www.apt-browse.org/browse/debian/wheezy/main/amd64/python-pyme/1:0.8.1-2/file/usr/share/doc/python-pyme/examples/t-edit.py
 | 
				
			||||||
# Please select what kind of key you want:
 | 
					# https://searchcode.com/codesearch/view/20535820/
 | 
				
			||||||
#    (1) RSA and RSA (default) - 1024-4096 bits
 | 
					# https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS
 | 
				
			||||||
#    (2) DSA and Elgamal - 768-3072
 | 
					# You can get the prompt identifiers and status indicators without grokking the source
 | 
				
			||||||
#    (3) DSA (sign only) - 768-3072
 | 
					# by first interactively performing the type of edit(s) you want to do with this command:
 | 
				
			||||||
#    (4) RSA (sign only) - 1024-4096
 | 
					# gpg --expert --status-fd 2 --command-fd 2 --edit-key <KEY_ID>
 | 
				
			||||||
#    (7) DSA (set your own capabilities) - 768-3072
 | 
					# Per:
 | 
				
			||||||
#    (8) RSA (set your own capabilities) - 1024-4096
 | 
					# https://lists.gnupg.org/pipermail/gnupg-users/2002-April/012630.html
 | 
				
			||||||
#    (9) ECC and ECC - (see below)
 | 
					# https://lists.gt.net/gnupg/users/9544
 | 
				
			||||||
#   (10) ECC (sign only) - (see below)
 | 
					# https://raymii.org/s/articles/GPG_noninteractive_batch_sign_trust_and_send_gnupg_keys.html
 | 
				
			||||||
#   (11) ECC (set your own capabilities) - (see below)
 | 
					class _KeyEditor(object):
 | 
				
			||||||
# Your selection? 9
 | 
					    def __init__(self, optmap):
 | 
				
			||||||
# Please select which elliptic curve you want:
 | 
					        self.replied_once = False  # This is used to handle the first prompt vs. the last
 | 
				
			||||||
#    (2) NIST P-256
 | 
					        self.optmap = optmap
 | 
				
			||||||
#    (3) NIST P-384
 | 
					
 | 
				
			||||||
#    (4) NIST P-521
 | 
					    def editKey(self, status, args, out):
 | 
				
			||||||
#    (5) Brainpool P-256
 | 
					        result = None
 | 
				
			||||||
#    (6) Brainpool P-384
 | 
					        out.seek(0, 0)
 | 
				
			||||||
#    (7) Brainpool P-512
 | 
					        def mapDict(m, d):
 | 
				
			||||||
# Your selection? 10
 | 
					            return(reduce(operator.getitem, m, d))
 | 
				
			||||||
# Please select which elliptic curve you want:
 | 
					        if args == 'keyedit.prompt' and self.replied_once:
 | 
				
			||||||
#    (1) Curve 25519
 | 
					            result = 'quit'
 | 
				
			||||||
#    (3) NIST P-256
 | 
					        elif status == 'KEY_CONSIDERED':
 | 
				
			||||||
#    (4) NIST P-384
 | 
					            result = None
 | 
				
			||||||
#    (5) NIST P-521
 | 
					            self.replied_once = False
 | 
				
			||||||
#    (6) Brainpool P-256
 | 
					        elif status == 'GET_LINE':
 | 
				
			||||||
#    (7) Brainpool P-384
 | 
					            self.replied_once = True
 | 
				
			||||||
#    (8) Brainpool P-512
 | 
					            _ilist = args.split('.')
 | 
				
			||||||
#    (9) secp256k1
 | 
					            result = mapDict(_ilist, self.optmap['prompts'])
 | 
				
			||||||
# gpgme key creation:
 | 
					            if not result:
 | 
				
			||||||
#g = gpg.Context()
 | 
					                result = None
 | 
				
			||||||
#mainkey = g.create_key('test key via python <test2@test.com>', algorithm = 'rsa4096', expires = False,
 | 
					        return(result)
 | 
				
			||||||
#                        #certify = True,
 | 
					
 | 
				
			||||||
#                        certify = False,
 | 
					def _getEditPrompt(key, trust, cmd, uid = None):
 | 
				
			||||||
#                        sign = False,
 | 
					    if not uid:
 | 
				
			||||||
#                        authenticate = False,
 | 
					        uid = key.uids[0]
 | 
				
			||||||
#                        encrypt = False)
 | 
					    # This mapping defines the default "answers" to the gpgme key editing.
 | 
				
			||||||
#key = g.get_key(mainkey.fpr, secret = True)
 | 
					    # https://www.apt-browse.org/browse/debian/wheezy/main/amd64/python-pyme/1:0.8.1-2/file/usr/share/doc/python-pyme/examples/t-edit.py
 | 
				
			||||||
#subkey = g.create_subkey(key, algorithm = 'rsa4096', expires = False,
 | 
					    # https://searchcode.com/codesearch/view/20535820/
 | 
				
			||||||
#                         sign = True,
 | 
					    # https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS
 | 
				
			||||||
#                         #certify = False,
 | 
					    # You can get the prompt identifiers and status indicators without grokking the source
 | 
				
			||||||
#                         encrypt = False,
 | 
					    # by first interactively performing the type of edit(s) you want to do with this command:
 | 
				
			||||||
#                         authenticate = False)
 | 
					    # gpg --status-fd 2 --command-fd 2 --edit-key <KEY_ID>
 | 
				
			||||||
 | 
					    if trust >= gpg.constants.validity.FULL:  # For tsigning, it only prompts for two trust levels:
 | 
				
			||||||
 | 
					        _loctrust = 2  # "I trust fully"
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        _loctrust = 1  # "I trust marginally"
 | 
				
			||||||
 | 
					    # TODO: make the trust depth configurable. 1 is probably the safest, but we try to guess here.
 | 
				
			||||||
 | 
					    # "Full" trust is a pretty big thing.
 | 
				
			||||||
 | 
					    if trust >= gpg.constants.validity.FULL:
 | 
				
			||||||
 | 
					        _locdepth = 2  # Allow +1 level of trust extension
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        _locdepth = 1  # Only trust this key
 | 
				
			||||||
 | 
					    # The check level.
 | 
				
			||||||
 | 
					    # (0) I will not answer. (default)
 | 
				
			||||||
 | 
					    # (1) I have not checked at all.
 | 
				
			||||||
 | 
					    # (2) I have done casual checking.
 | 
				
			||||||
 | 
					    # (3) I have done very careful checking.
 | 
				
			||||||
 | 
					    # Since we're running this entirely non-interactively, we really should use 1.
 | 
				
			||||||
 | 
					    _chk_lvl = 1
 | 
				
			||||||
 | 
					    _map = {
 | 
				
			||||||
 | 
					        # Valid commands
 | 
				
			||||||
 | 
					        'cmds': ['trust', 'fpr', 'sign', 'tsign', 'lsign', 'nrsign', 'grip', 'list',
 | 
				
			||||||
 | 
					                 'uid', 'key', 'check', 'deluid', 'delkey', 'delsig', 'pref', 'showpref',
 | 
				
			||||||
 | 
					                 'revsig', 'enable', 'disable', 'showphoto', 'clean', 'minimize', 'save',
 | 
				
			||||||
 | 
					                 'quit'],
 | 
				
			||||||
 | 
					        # Prompts served by the interactive session, and a map of their responses.
 | 
				
			||||||
 | 
					        # It's expanded in the parent call, but the prompt is actually in the form of e.g.:
 | 
				
			||||||
 | 
					        # keyedit.save (we expand that to a list and use that list as a "path" in the below dict)
 | 
				
			||||||
 | 
					        # We *could* just use a flat dict of full prompt to constants, but this is a better visual segregation &
 | 
				
			||||||
 | 
					        # prevents unnecessary duplication.
 | 
				
			||||||
 | 
					        'prompts': {
 | 
				
			||||||
 | 
					            'edit_ownertrust': {'value': str(trust),  # Pulled at time of call
 | 
				
			||||||
 | 
					                                'set_ultimate': {'okay': 'yes'}},  # If confirming ultimate trust, we auto-answer yes
 | 
				
			||||||
 | 
					            'untrusted_key': {'override': 'yes'},  # We don't care if it's untrusted
 | 
				
			||||||
 | 
					            'pklist': {'user_id': {'enter': uid.uid}},  # Prompt for a user ID - can we use the full uid string? (tsign)
 | 
				
			||||||
 | 
					            'sign_uid': {'class': str(_chk_lvl),  # The certification/"check" level
 | 
				
			||||||
 | 
					                         'okay': 'yes'},  # Are you sure that you want to sign this key with your key..."
 | 
				
			||||||
 | 
					            'trustsig_prompt': {'trust_value': str(_loctrust),  # This requires some processing; see above
 | 
				
			||||||
 | 
					                                'trust_depth': str(_locdepth),  # The "depth" of the trust signature.
 | 
				
			||||||
 | 
					                                'trust_regexp': None},  # We can "Restrict" trust to certain domains if we wanted.
 | 
				
			||||||
 | 
					            'keyedit': {'prompt': cmd,  # Initiate trust editing (or whatever)
 | 
				
			||||||
 | 
					                        'save': {'okay': 'yes'}}}}  # Save if prompted
 | 
				
			||||||
 | 
					    return(_map)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GPGHandler(object):
 | 
					class GPGHandler(object):
 | 
				
			||||||
@ -79,6 +184,16 @@ class GPGHandler(object):
 | 
				
			|||||||
        else:
 | 
					        else:
 | 
				
			||||||
            self._check_home()
 | 
					            self._check_home()
 | 
				
			||||||
        self.ctx = self.GetContext(home_dir = self.home)
 | 
					        self.ctx = self.GetContext(home_dir = self.home)
 | 
				
			||||||
 | 
					        self._orig_kl_mode = self.ctx.get_keylist_mode()
 | 
				
			||||||
 | 
					        self.mykey = None
 | 
				
			||||||
 | 
					        self.subkey = None
 | 
				
			||||||
 | 
					        if self.key_id:
 | 
				
			||||||
 | 
					            self.mykey = self.ctx.get_key(self.key_id, secret = True)
 | 
				
			||||||
 | 
					            for s in self.mykey.subkeys:
 | 
				
			||||||
 | 
					                if s.can_sign:
 | 
				
			||||||
 | 
					                    self.subkey = s
 | 
				
			||||||
 | 
					                    self.ctx.signers = [self.mykey]
 | 
				
			||||||
 | 
					                    break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _check_home(self, home = None):
 | 
					    def _check_home(self, home = None):
 | 
				
			||||||
        if not home:
 | 
					        if not home:
 | 
				
			||||||
@ -110,30 +225,19 @@ class GPGHandler(object):
 | 
				
			|||||||
            if not _exists:
 | 
					            if not _exists:
 | 
				
			||||||
                raise PermissionError('We need a GnuPG home directory we can '
 | 
					                raise PermissionError('We need a GnuPG home directory we can '
 | 
				
			||||||
                                      'write to')
 | 
					                                      'write to')
 | 
				
			||||||
 | 
					        # TODO: write gpg.conf, parse existing one and write changes if needed.
 | 
				
			||||||
 | 
					        # Should use SHA512 etc. See:
 | 
				
			||||||
 | 
					        # https://spin.atomicobject.com/2013/11/24/secure-gpg-keys-guide/
 | 
				
			||||||
 | 
					        # https://github.com/BetterCrypto/Applied-Crypto-Hardening/blob/master/src/configuration/GPG/GnuPG/gpg.conf
 | 
				
			||||||
 | 
					        # https://riseup.net/en/security/message-security/openpgp/best-practices
 | 
				
			||||||
 | 
					        # And explicitly set keyservers if present in params.
 | 
				
			||||||
        return()
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def GetContext(self, **kwargs):
 | 
					    def GetContext(self, **kwargs):
 | 
				
			||||||
        ctx = gpg.Context(**kwargs)
 | 
					        ctx = gpg.Context(**kwargs)
 | 
				
			||||||
        return(ctx)
 | 
					        return(ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def KillStaleAgent(self):
 | 
					 | 
				
			||||||
        # Is this even necessary since I switched to the native gpg module instead of the gpgme one?
 | 
					 | 
				
			||||||
        _process_list = []
 | 
					 | 
				
			||||||
        # TODO: optimize; can I search by proc name?
 | 
					 | 
				
			||||||
        for p in psutil.process_iter():
 | 
					 | 
				
			||||||
            if (p.name() in ('gpg-agent', 'dirmngr') and \
 | 
					 | 
				
			||||||
                                                p.uids()[0] == os.getuid()):
 | 
					 | 
				
			||||||
                pd = psutil.Process(p.pid).as_dict()
 | 
					 | 
				
			||||||
                # TODO: convert these over
 | 
					 | 
				
			||||||
#                for d in (chrootdir, dlpath):
 | 
					 | 
				
			||||||
#                    if pd['cwd'].startswith('{0}'.format(d)):
 | 
					 | 
				
			||||||
#                        plst.append(p.pid)
 | 
					 | 
				
			||||||
#        if len(plst) >= 1:
 | 
					 | 
				
			||||||
#            for p in plst:
 | 
					 | 
				
			||||||
#                psutil.Process(p).terminate()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def CreateKey(self, name, algo, keysize, email = None, comment = None, passwd = None, key = None, expiry = None):
 | 
					    def CreateKey(self, name, algo, keysize, email = None, comment = None, passwd = None, key = None, expiry = None):
 | 
				
			||||||
        algo = _algmaps[algo].format(keysize = keysize)
 | 
					 | 
				
			||||||
        userid = name
 | 
					        userid = name
 | 
				
			||||||
        userid += ' ({0})'.format(comment) if comment else ''
 | 
					        userid += ' ({0})'.format(comment) if comment else ''
 | 
				
			||||||
        userid += ' <{0}>'.format(email) if email else ''
 | 
					        userid += ' <{0}>'.format(email) if email else ''
 | 
				
			||||||
@ -141,75 +245,249 @@ class GPGHandler(object):
 | 
				
			|||||||
            expires = False
 | 
					            expires = False
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            expires = True
 | 
					            expires = True
 | 
				
			||||||
        self.ctx.create_key(userid,
 | 
					        params = {'algorithm': _algmaps[algo].format(keysize = keysize),
 | 
				
			||||||
                            algorithm = algo,
 | 
					                  'expires': expires,
 | 
				
			||||||
                            expires = expires,
 | 
					                  'expires_in': (_epoch_helper(expiry) if expires else 0),
 | 
				
			||||||
                            expires_in = _epoch_helper(expiry),
 | 
					                  'sign': True,
 | 
				
			||||||
                            sign = True)
 | 
					                  'passphrase': passwd}
 | 
				
			||||||
        # Even if expires is False, it still parses the expiry...
 | 
					        if not key:
 | 
				
			||||||
        # except OverflowError:  # Only trips if expires is True and a negative expires occurred.
 | 
					            self.mykey = self.ctx.get_key(self.ctx.create_key(userid, **params).fpr)
 | 
				
			||||||
        #     raise ValueError(('Expiration epoch must be 0 (to disable) or a future time! '
 | 
					            self.subkey = self.mykey.subkeys[0]
 | 
				
			||||||
        #                       'The specified epoch ({0}, {1}) is in the past '
 | 
					 | 
				
			||||||
        #                       '(current time is {2}, {3}).').format(expiry,
 | 
					 | 
				
			||||||
        #                                                             str(datetime.datetime.utcfromtimestamp(expiry)),
 | 
					 | 
				
			||||||
        #                                                             datetime.datetime.utcnow().timestamp(),
 | 
					 | 
				
			||||||
        #                                                             str(datetime.datetime.utcnow())))
 | 
					 | 
				
			||||||
        return(k)
 | 
					 | 
				
			||||||
        # We can't use self.ctx.create_key; it's a little limiting.
 | 
					 | 
				
			||||||
        # It's a fairly thin wrapper to .op_createkey() (the C GPGME API gpgme_op_createkey) anyways.
 | 
					 | 
				
			||||||
        flags = (gpg.constants.create.SIGN |
 | 
					 | 
				
			||||||
                 gpg.constants.create.CERT)
 | 
					 | 
				
			||||||
        if not expiry:
 | 
					 | 
				
			||||||
            flags = (flags | gpg.constants.create.NOEXPIRE)
 | 
					 | 
				
			||||||
        if not passwd:
 | 
					 | 
				
			||||||
            flags = (flags | gpg.constants.create.NOPASSWD)
 | 
					 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            # Thanks, gpg/core.py#Context.create_key()!
 | 
					            if not self.mykey:
 | 
				
			||||||
            sys_pinentry = gpg.constants.PINENTRY_MODE_DEFAULT
 | 
					                self.mykey = self.ctx.get_key(self.ctx.create_key(userid, **params).fpr)
 | 
				
			||||||
            old_pass_cb = getattr(self, '_passphrase_cb', None)
 | 
					            self.subkey = self.ctx.get_key(self.ctx.create_subkey(self.mykey, **params).fpr)
 | 
				
			||||||
            self.ctx.pinentry_mode = gpg.constants.PINENTRY_MODE_LOOPBACK
 | 
					        self.ctx.signers = [self.subkey]
 | 
				
			||||||
            def passphrase_cb(hint, desc, prev_bad, hook = None):
 | 
					        return()
 | 
				
			||||||
                return(passwd)
 | 
					 | 
				
			||||||
            self.ctx.set_passphrase_cb(passphrase_cb)
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            if not key:
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    self.ctx.op_createkey(userid, algo, 0, 0, flags)
 | 
					 | 
				
			||||||
                    k = self.ctx.get_key(self.ctx.op_genkey_result().fpr, secret = True)
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                if not isinstance(key, gpg.gpgme._gpgme_key):
 | 
					 | 
				
			||||||
                    key = self.ctx.get_key(key)
 | 
					 | 
				
			||||||
                if not key:
 | 
					 | 
				
			||||||
                    raise ValueError('Key {0} does not exist'.format())
 | 
					 | 
				
			||||||
                #self.ctx.op_createsubkey(key, )
 | 
					 | 
				
			||||||
        finally:
 | 
					 | 
				
			||||||
            if not passwd:
 | 
					 | 
				
			||||||
                self.ctx.pinentry_mode = sys_pinentry
 | 
					 | 
				
			||||||
                if old_pass_cb:
 | 
					 | 
				
			||||||
                    self.ctx.set_passphrase_cb(*old_pass_cb[1:])
 | 
					 | 
				
			||||||
        return(k)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def GetSigs(self, data_in):
 | 
					    def ListSigs(self, sig_data):
 | 
				
			||||||
        key_ids = []
 | 
					        key_ids = []
 | 
				
			||||||
        # Currently as of May 13, 2018 there's no way using the GPGME API to do
 | 
					        # Currently as of May 13, 2018 there's no way using the GPGME API to do
 | 
				
			||||||
        # the equivalent of the CLI's --list-packets.
 | 
					        # the equivalent of the CLI's --list-packets. https://dev.gnupg.org/T3734
 | 
				
			||||||
        # https://lists.gnupg.org/pipermail/gnupg-users/2018-January/
 | 
					        # https://lists.gnupg.org/pipermail/gnupg-users/2018-January/059708.html
 | 
				
			||||||
        #                                                           059708.html
 | 
					        # https://lists.gnupg.org/pipermail/gnupg-users/2018-January/059715.html
 | 
				
			||||||
        # https://lists.gnupg.org/pipermail/gnupg-users/2018-January/
 | 
					        # We use the "workaround" in:
 | 
				
			||||||
        #                                                           059715.html
 | 
					        # https://lists.gnupg.org/pipermail/gnupg-users/2018-January/059711.html
 | 
				
			||||||
        # We use the "workaround in:
 | 
					 | 
				
			||||||
        # https://lists.gnupg.org/pipermail/gnupg-users/2018-January/
 | 
					 | 
				
			||||||
        #                                                           059711.html
 | 
					 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.ctx.verify(data_in)
 | 
					            self.ctx.verify(sig_data)
 | 
				
			||||||
        except gpg.errors.BadSignatures as sig_except:
 | 
					        except gpg.errors.BadSignatures as sig_except:
 | 
				
			||||||
            for line in [i.strip() for i in str(sig_except).splitlines()]:
 | 
					            for line in [i.strip() for i in str(sig_except).splitlines()]:
 | 
				
			||||||
                l = [i.strip() for i in line.split(':')]
 | 
					                l = [i.strip() for i in line.split(':')]
 | 
				
			||||||
                key_ids.append(l[0])
 | 
					                key_ids.append(l[0])
 | 
				
			||||||
        return(key_ids)
 | 
					        return(key_ids)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def CheckSigs(self, keys, sig_data):
 | 
					    def GetSigs(self, data_in, sig_data = None, verify_keys = None):
 | 
				
			||||||
 | 
					        signers = []
 | 
				
			||||||
 | 
					        if verify_keys:
 | 
				
			||||||
 | 
					            # Raises gpg.errors.BadSignatures if any are invalid.
 | 
				
			||||||
 | 
					            # Unlike Verify below, this will raise an exception.
 | 
				
			||||||
 | 
					            signers = verify_keys
 | 
				
			||||||
 | 
					        if sig_data:
 | 
				
			||||||
 | 
					            # Detached sig
 | 
				
			||||||
 | 
					            sig = self.ctx.verify(data_in, signature = sig_data, verify = signers)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Cleartext? or "normal" signatures (embedded)
 | 
				
			||||||
 | 
					            sig = self.ctx.verify(data_in, verify = signers)
 | 
				
			||||||
 | 
					        return(sig)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def GetKeysigs(self, pubkey):
 | 
				
			||||||
 | 
					        sigs = {}
 | 
				
			||||||
 | 
					        fpr = (pubkey if isinstance(pubkey, str) else pubkey.fpr)
 | 
				
			||||||
 | 
					        keys = list(self.ctx.keylist(fpr, mode = (gpg.constants.keylist.mode.LOCAL | gpg.constants.keylist.mode.SIGS)))
 | 
				
			||||||
 | 
					        for idx1, k in enumerate(keys):
 | 
				
			||||||
 | 
					            sigs[k.fpr] = {}
 | 
				
			||||||
 | 
					            for idx2, u in enumerate(k.uids):
 | 
				
			||||||
 | 
					                sigs[k.fpr][u.uid] = {}
 | 
				
			||||||
 | 
					                for idx3, sig in enumerate(u.signatures):
 | 
				
			||||||
 | 
					                    signer = getattr(sig, 'keyid')
 | 
				
			||||||
 | 
					                    sigs[k.fpr][u.uid][signer] = {}
 | 
				
			||||||
 | 
					                    for a in _keysig_attrs:
 | 
				
			||||||
 | 
					                        if a == 'keyid':
 | 
				
			||||||
 | 
					                            continue
 | 
				
			||||||
 | 
					                        sigs[k.fpr][u.uid][signer][a] = getattr(sig, a)
 | 
				
			||||||
 | 
					        return(sigs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def CheckSigs(self, sig, sigkeys = None):
 | 
				
			||||||
 | 
					        # sig should be a GetSigs result.
 | 
				
			||||||
 | 
					        is_valid = True
 | 
				
			||||||
 | 
					        # See self.CheckSigs().
 | 
				
			||||||
 | 
					        # https://www.gnupg.org/documentation/manuals/gpgme/Verify.html
 | 
				
			||||||
 | 
					        # https://github.com/micahflee/torbrowser-launcher/issues/262#issuecomment-284342876
 | 
				
			||||||
 | 
					        sig = sig[1]
 | 
				
			||||||
 | 
					        result = {}
 | 
				
			||||||
 | 
					        _keys = [s.fpr.upper() for s in sig.signatures]
 | 
				
			||||||
 | 
					        if sigkeys:
 | 
				
			||||||
 | 
					            if isinstance(sigkeys, str):
 | 
				
			||||||
 | 
					                sigkeys = [sigkeys.upper()]
 | 
				
			||||||
 | 
					            elif isinstance(sigkeys, list):
 | 
				
			||||||
 | 
					                _sigkeys = []
 | 
				
			||||||
 | 
					                for s in sigkeys[:]:
 | 
				
			||||||
 | 
					                    if isinstance(s, str):
 | 
				
			||||||
 | 
					                        _sigkeys.append(s.upper())
 | 
				
			||||||
 | 
					                    elif isinstance(s, gpgme._gpgme_key):
 | 
				
			||||||
 | 
					                        _sigkeys.append(s.fpr)
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        continue
 | 
				
			||||||
 | 
					                sigkeys = _sigkeys
 | 
				
			||||||
 | 
					            elif isinstance(sigkeys, gpgme._gpgme_key):
 | 
				
			||||||
 | 
					                sigkeys = [sigkeys.fpr]
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                raise ValueError('sigkeys must be a key fingerprint or a key object (or a list of those).')
 | 
				
			||||||
 | 
					            if not set(sigkeys).issubset(_keys):
 | 
				
			||||||
 | 
					                raise ValueError('All specified keys were not present in the signature.')
 | 
				
			||||||
 | 
					        for s in sig.signatures:
 | 
				
			||||||
 | 
					            fpr = getattr(s, 'fpr')
 | 
				
			||||||
 | 
					            result[fpr] = {}
 | 
				
			||||||
 | 
					            for a in _sig_attrs:
 | 
				
			||||||
 | 
					                if a == 'fpr':
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                result[fpr][a] = getattr(s, a)
 | 
				
			||||||
 | 
					            # Now we do some logic to determine if the sig is "valid".
 | 
				
			||||||
 | 
					            # Note that we can get confidence level by &'ing "validity" attr against gpg.constants.validity.*
 | 
				
			||||||
 | 
					            # Or just doing a <, >, <=, etc. operation since it's a sequential list of constants levels, not bitwise.
 | 
				
			||||||
 | 
					            # For now, we just check if it's valid or not, not "how valid" it is (how much we can trust it).
 | 
				
			||||||
 | 
					            _status = s.summary
 | 
				
			||||||
 | 
					            if not _get_sig_isgood(_status):
 | 
				
			||||||
 | 
					                result[fpr]['valid'] = False
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                result[fpr]['valid'] = True
 | 
				
			||||||
 | 
					        if sigkeys:
 | 
				
			||||||
 | 
					            for k in sigkeys:
 | 
				
			||||||
 | 
					                if (k not in result) or (not result[k]['valid']):
 | 
				
			||||||
 | 
					                    is_valid = False
 | 
				
			||||||
 | 
					                    break
 | 
				
			||||||
 | 
					        else:  # is_valid is satisfied by at LEAST one valid sig.
 | 
				
			||||||
 | 
					            is_valid = any([k[1]['valid'] for k in result])
 | 
				
			||||||
 | 
					        return(is_valid, result)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def Sign(self, data_in, ascii = True, mode = 'detached', notations = None):
 | 
				
			||||||
 | 
					        # notations is a list of dicts via notation format:
 | 
				
			||||||
 | 
					        # {<namespace>: {'value': 'some string', 'flags': BITWISE_OR_FLAGS}}
 | 
				
			||||||
 | 
					        # See RFC 4880 § 5.2.3.16 for valid user namespace format.
 | 
				
			||||||
 | 
					        if mode.startswith('d'):
 | 
				
			||||||
 | 
					            mode = gpg.constants.SIG_MODE_DETACH
 | 
				
			||||||
 | 
					        elif mode.startswith('c'):
 | 
				
			||||||
 | 
					            mode = gpg.constants.SIG_MODE_CLEAR
 | 
				
			||||||
 | 
					        elif mode.startswith('n'):
 | 
				
			||||||
 | 
					            mode = gpg.constants.SIG_MODE_NORMAL
 | 
				
			||||||
 | 
					        self.ctx.armor = ascii
 | 
				
			||||||
 | 
					        if not isinstance(data_in, bytes):
 | 
				
			||||||
 | 
					            if isinstance(data_in, str):
 | 
				
			||||||
 | 
					                data_in = data_in.encode('utf-8')
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # We COULD try serializing to JSON here, or converting to a pickle object,
 | 
				
			||||||
 | 
					                # or testing for other classes, etc. But we don't.
 | 
				
			||||||
 | 
					                # TODO?
 | 
				
			||||||
 | 
					                data_in = repr(data_in).encode('utf-8')
 | 
				
			||||||
 | 
					        data_in = gpg.Data(data_in)
 | 
				
			||||||
 | 
					        if notations:
 | 
				
			||||||
 | 
					            for n in notations:
 | 
				
			||||||
 | 
					                if not utils.valid().gpgsigNotation(n):
 | 
				
			||||||
 | 
					                    raise ValueError('Malformatted notation: {0}'.format(n))
 | 
				
			||||||
 | 
					                for ns in n:
 | 
				
			||||||
 | 
					                    self.ctx.sig_notation_add(ns, n[ns]['value'], n[ns]['flags'])
 | 
				
			||||||
 | 
					        # data_in *always* must be a bytes (or bytes-like?) object.
 | 
				
			||||||
 | 
					        # It will *always* return a bytes object.
 | 
				
			||||||
 | 
					        sig = self.ctx.sign(data_in, mode = mode)
 | 
				
			||||||
 | 
					        # And we need to clear the sig notations, otherwise they'll apply to the next signature this context makes.
 | 
				
			||||||
 | 
					        self.ctx.sig_notation_clear()
 | 
				
			||||||
 | 
					        return(sig)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def ImportPubkey(self, pubkey):
 | 
				
			||||||
 | 
					        fpr = (pubkey if isinstance(pubkey, str) else pubkey.fpr)
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.ctx.verify(sig_data)
 | 
					            self.ctx.get_key(fpr)
 | 
				
			||||||
        except:
 | 
					            return()  # already imported
 | 
				
			||||||
            pass  # TODO
 | 
					        except gpg.errors.KeyNotFound:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        _dflt_klm = self.ctx.get_keylist_mode()
 | 
				
			||||||
 | 
					        self.ctx.set_keylist_mode(gpg.constants.keylist.mode.EXTERN)
 | 
				
			||||||
 | 
					        if isinstance(pubkey, gpgme._gpgme_key):
 | 
				
			||||||
 | 
					            self.ctx.op_import_keys([pubkey])
 | 
				
			||||||
 | 
					        elif isinstance(pubkey, str):
 | 
				
			||||||
 | 
					            if not utils.valid().gpgkeyID(pubkey):
 | 
				
			||||||
 | 
					                raise ValueError('{0} is not a valid key or fingerprint'.format(pubkey))
 | 
				
			||||||
 | 
					            pubkey = self.ctx.get_key(fpr)
 | 
				
			||||||
 | 
					            self.ctx.op_import_keys([pubkey])
 | 
				
			||||||
 | 
					        self.ctx.set_keylist_mode(_dflt_klm)
 | 
				
			||||||
 | 
					        self.SignKey(pubkey)
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def ImportPubkeyFromFile(self, pubkey_data):
 | 
				
			||||||
 | 
					        _fpath = os.path.abspath(os.path.expanduser(pubkey_data))
 | 
				
			||||||
 | 
					        if os.path.isfile(_fpath):
 | 
				
			||||||
 | 
					            with open(_fpath, 'rb') as f:
 | 
				
			||||||
 | 
					                k = self.ctx.key_import(f.read())
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            k = self.ctx.key_import(pubkey_data)
 | 
				
			||||||
 | 
					        pubkey = self.ctx.get_key(k)
 | 
				
			||||||
 | 
					        self.SignKey(pubkey)
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def SignKey(self, pubkey, local = False, notations = None):
 | 
				
			||||||
 | 
					        # notations is a list of dicts via notation format:
 | 
				
			||||||
 | 
					        # {<namespace>: {'value': 'some string', 'flags': BITWISE_OR_FLAGS}}
 | 
				
			||||||
 | 
					        # See RFC 4880 § 5.2.3.16 for valid user namespace format.
 | 
				
			||||||
 | 
					        if isinstance(pubkey, gpgme._gpgme_key):
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        elif isinstance(pubkey, str):
 | 
				
			||||||
 | 
					            if not utils.valid().gpgkeyID(pubkey):
 | 
				
			||||||
 | 
					                raise ValueError('{0} is not a valid fingerprint'.format(pubkey))
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                pubkey = self.ctx.get_key(pubkey)
 | 
				
			||||||
 | 
					        if notations:
 | 
				
			||||||
 | 
					            for n in notations:
 | 
				
			||||||
 | 
					                if not utils.valid().gpgsigNotation(n):
 | 
				
			||||||
 | 
					                    raise ValueError('Malformatted notation: {0}'.format(n))
 | 
				
			||||||
 | 
					                for ns in n:
 | 
				
			||||||
 | 
					                    self.ctx.sig_notation_add(ns, n[ns]['value'], n[ns]['flags'])
 | 
				
			||||||
 | 
					        self.ctx.key_sign(pubkey, local = local)
 | 
				
			||||||
 | 
					        self.TrustKey(pubkey)
 | 
				
			||||||
 | 
					        # And we need to clear the sig notations, otherwise they'll apply to the next signature this context makes.
 | 
				
			||||||
 | 
					        self.ctx.sig_notation_clear()
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def TrustKey(self, pubkey, trust = gpg.constants.validity.FULL):
 | 
				
			||||||
 | 
					        # We use full as the default because signatures aren't considered valid otherwise.
 | 
				
			||||||
 | 
					        # TODO: we need a way of maybe reverting/rolling back any changes we do?
 | 
				
			||||||
 | 
					        output = gpg.Data()
 | 
				
			||||||
 | 
					        _map = _getEditPrompt(pubkey, trust, 'trust')
 | 
				
			||||||
 | 
					        self.ctx.interact(pubkey, _KeyEditor(_map).editKey, sink = output, fnc_value = output)
 | 
				
			||||||
 | 
					        output.seek(0, 0)
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def ExportPubkey(self, fpr, ascii = True, sigs = False):
 | 
				
			||||||
 | 
					        orig_armor = self.ctx.armor
 | 
				
			||||||
 | 
					        self.ctx.armor = ascii
 | 
				
			||||||
 | 
					        if sigs:
 | 
				
			||||||
 | 
					            export_mode = 0
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            export_mode = gpg.constants.EXPORT_MODE_MINIMAL  # default is 0; minimal strips signatures
 | 
				
			||||||
 | 
					        kb = gpg.Data()
 | 
				
			||||||
 | 
					        self.ctx.op_export_keys([self.ctx.get_key(fpr)], export_mode, kb)
 | 
				
			||||||
 | 
					        kb.seek(0, 0)
 | 
				
			||||||
 | 
					        self.ctx.armor = orig_armor
 | 
				
			||||||
 | 
					        return(kb.read())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def DeleteKey(self, pubkey):
 | 
				
			||||||
 | 
					        if isinstance(pubkey, gpgme._gpgme_key):
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					        elif isinstance(pubkey, str):
 | 
				
			||||||
 | 
					            if not utils.valid().gpgkeyID(pubkey):
 | 
				
			||||||
 | 
					                raise ValueError('{0} is not a valid fingerprint'.format(pubkey))
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                pubkey = self.ctx.get_key(pubkey)
 | 
				
			||||||
 | 
					        self.ctx.op_delete(pubkey, False)
 | 
				
			||||||
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def Verify(self, sig_data, data):
 | 
				
			||||||
 | 
					        # This is a more "flat" version of CheckSigs.
 | 
				
			||||||
 | 
					        # First we need to parse the sig(s) and import the key(s) to our keyring.
 | 
				
			||||||
 | 
					        signers = self.ListSigs(sig_data)
 | 
				
			||||||
 | 
					        for signer in signers:
 | 
				
			||||||
 | 
					            self.ImportPubkey(signer)
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.ctx.verify(data, signature = sig_data, verify = signers)
 | 
				
			||||||
 | 
					            return(True)
 | 
				
			||||||
 | 
					        except gpg.errors.BadSignatures as err:
 | 
				
			||||||
 | 
					            return(False)
 | 
				
			||||||
 | 
				
			|||||||
@ -3,13 +3,14 @@
 | 
				
			|||||||
# Ironically enough, I think building a GUI for this would be *cleaner*.
 | 
					# Ironically enough, I think building a GUI for this would be *cleaner*.
 | 
				
			||||||
# Go figure.
 | 
					# Go figure.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import confparse
 | 
					 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
import getpass
 | 
					import getpass
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import utils
 | 
					 | 
				
			||||||
import uuid
 | 
					import uuid
 | 
				
			||||||
import lxml.etree
 | 
					import lxml.etree
 | 
				
			||||||
 | 
					import utils  # LOCAL
 | 
				
			||||||
 | 
					import confparse  # LOCAL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
detect = utils.detect()
 | 
					detect = utils.detect()
 | 
				
			||||||
generate = utils.generate()
 | 
					generate = utils.generate()
 | 
				
			||||||
 | 
				
			|||||||
@ -2,9 +2,10 @@ import copy
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
import pprint
 | 
					import pprint
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
import utils
 | 
					 | 
				
			||||||
import lxml.etree
 | 
					import lxml.etree
 | 
				
			||||||
from urllib.parse import urlparse
 | 
					from urllib.parse import urlparse
 | 
				
			||||||
 | 
					import utils  # LOCAL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
etree = lxml.etree
 | 
					etree = lxml.etree
 | 
				
			||||||
detect = utils.detect()
 | 
					detect = utils.detect()
 | 
				
			||||||
@ -125,6 +126,7 @@ class Conf(object):
 | 
				
			|||||||
                ptrn = _item.format(**self.xml_suppl.btags['regex'])
 | 
					                ptrn = _item.format(**self.xml_suppl.btags['regex'])
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                ptrn = None
 | 
					                ptrn = None
 | 
				
			||||||
 | 
					            # TODO: remove all this shit! we switch to just a mirror url.
 | 
				
			||||||
            _source_item['fname'] = detect.remote_files(
 | 
					            _source_item['fname'] = detect.remote_files(
 | 
				
			||||||
                    '/'.join((_source['mirror'],
 | 
					                    '/'.join((_source['mirror'],
 | 
				
			||||||
                              _source['rootpath'])),
 | 
					                              _source['rootpath'])),
 | 
				
			||||||
@ -182,7 +184,7 @@ class Conf(object):
 | 
				
			|||||||
        self.cfg['build']['optimize'] = transform.xml2py(_optimize)
 | 
					        self.cfg['build']['optimize'] = transform.xml2py(_optimize)
 | 
				
			||||||
        for path in build.xpath('./paths/*'):
 | 
					        for path in build.xpath('./paths/*'):
 | 
				
			||||||
            self.cfg['build']['paths'][path.tag] = path.text
 | 
					            self.cfg['build']['paths'][path.tag] = path.text
 | 
				
			||||||
        self.cfg['build']['basedistro'] = build.get('basedistro', 'archlinux')
 | 
					        self.cfg['build']['guests'] = build.get('guests', 'archlinux')
 | 
				
			||||||
        # iso and ipxe are their own basic profile elements, but we group them
 | 
					        # iso and ipxe are their own basic profile elements, but we group them
 | 
				
			||||||
        # in here because 1.) they're related, and 2.) they're simple to
 | 
					        # in here because 1.) they're related, and 2.) they're simple to
 | 
				
			||||||
        # import. This may change in the future if they become more complex.
 | 
					        # import. This may change in the future if they become more complex.
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										48
									
								
								bdisk/download.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								bdisk/download.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Download(object):
 | 
				
			||||||
 | 
					    def __init__(self, url, progress = True, offset = None, chunksize = 1024):
 | 
				
			||||||
 | 
					        self.cnt_len = None
 | 
				
			||||||
 | 
					        self.head = requests.head(url, allow_redirects = True).headers
 | 
				
			||||||
 | 
					        self.req_headers = {}
 | 
				
			||||||
 | 
					        self.range = False
 | 
				
			||||||
 | 
					        self.url = url
 | 
				
			||||||
 | 
					        self.offset = offset
 | 
				
			||||||
 | 
					        self.chunksize = chunksize
 | 
				
			||||||
 | 
					        self.progress = progress
 | 
				
			||||||
 | 
					        if 'accept-ranges' in self.head:
 | 
				
			||||||
 | 
					            if self.head['accept-ranges'].lower() != 'none':
 | 
				
			||||||
 | 
					                self.range = True
 | 
				
			||||||
 | 
					            if 'content-length' in self.head:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    self.cnt_len = int(self.head['content-length'])
 | 
				
			||||||
 | 
					                except TypeError:
 | 
				
			||||||
 | 
					                    pass
 | 
				
			||||||
 | 
					            if self.cnt_len and self.offset and self.range:
 | 
				
			||||||
 | 
					                if not self.offset <= self.cnt_len:
 | 
				
			||||||
 | 
					                    raise ValueError(('The offset requested ({0}) is greater than '
 | 
				
			||||||
 | 
					                                      'the content-length value').format(self.offset, self.cnt_len))
 | 
				
			||||||
 | 
					                self.req_headers['range'] = 'bytes={0}-'.format(self.offset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def fetch(self):
 | 
				
			||||||
 | 
					        if not self.progress:
 | 
				
			||||||
 | 
					            self.req = requests.get(self.url, allow_redirects = True, headers = self.req_headers)
 | 
				
			||||||
 | 
					            self.bytes_obj = self.req.content
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self.req = requests.get(self.url, allow_redirects = True, stream = True, headers = self.req_headers)
 | 
				
			||||||
 | 
					            self.bytes_obj = bytes()
 | 
				
			||||||
 | 
					            _bytelen = 0
 | 
				
			||||||
 | 
					            # TODO: better handling for logging instead of print()s?
 | 
				
			||||||
 | 
					            for chunk in self.req.iter_content(chunk_size = self.chunksize):
 | 
				
			||||||
 | 
					                self.bytes_obj += chunk
 | 
				
			||||||
 | 
					                if self.cnt_len:
 | 
				
			||||||
 | 
					                    print('\033[F')
 | 
				
			||||||
 | 
					                    print('{0:.2f}'.format((_bytelen / float(self.head['content-length'])) * 100),
 | 
				
			||||||
 | 
					                          end = '%',
 | 
				
			||||||
 | 
					                          flush = True)
 | 
				
			||||||
 | 
					                    _bytelen += self.chunksize
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    print('.', end = '')
 | 
				
			||||||
 | 
					            print()
 | 
				
			||||||
 | 
					        return(self.bytes_obj)
 | 
				
			||||||
@ -1,14 +1,14 @@
 | 
				
			|||||||
import hashlib
 | 
					import hashlib
 | 
				
			||||||
import importlib  # needed for the guest-os-specific stuff...
 | 
					import importlib  # needed for the guest-os-specific stuff...
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
from . import utils
 | 
					import download  # LOCAL
 | 
				
			||||||
from urllib.parse import urljoin
 | 
					from urllib.parse import urljoin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def hashsum_downloader(url, filename = None):
 | 
					def hashsum_downloader(url, filename = None):
 | 
				
			||||||
    # TODO: support "latest" and "regex" flags? or remove from specs (since the tarball can be specified by these)?
 | 
					    # TODO: support "latest" and "regex" flags? or remove from specs (since the tarball can be specified by these)?
 | 
				
			||||||
    # move that to the utils.DOwnload() class?
 | 
					    # move that to the download.Download() class?
 | 
				
			||||||
    d = utils.Download(url, progress = False)
 | 
					    d = download.Download(url, progress = False)
 | 
				
			||||||
    hashes = {os.path.basename(k):v for (v, k) in [line.split() for line in d.fetch().decode('utf-8').splitlines()]}
 | 
					    hashes = {os.path.basename(k):v for (v, k) in [line.split() for line in d.fetch().decode('utf-8').splitlines()]}
 | 
				
			||||||
    if filename:
 | 
					    if filename:
 | 
				
			||||||
        if filename in hashes:
 | 
					        if filename in hashes:
 | 
				
			||||||
@ -19,19 +19,26 @@ def hashsum_downloader(url, filename = None):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Prepper(object):
 | 
					class Prepper(object):
 | 
				
			||||||
    def __init__(self, dirs, sources, gpg = None):
 | 
					    # Prepare sources, destinations, etc.
 | 
				
			||||||
        # dirs is a ConfParse.cfg['build']['paths'] dict of dirs
 | 
					    def __init__(self, cfg):
 | 
				
			||||||
        self.CreateDirs(dirs)
 | 
					        self.cfg = cfg
 | 
				
			||||||
        # TODO: set up GPG env here so we can use it to import sig key and verify sources
 | 
					        self.CreateDirs(self.cfg['build']['paths'])
 | 
				
			||||||
        for idx, s in enumerate(sources):
 | 
					        if 'handler' not in self.cfg['gpg'] or not self.cfg['gpg']['handler']:
 | 
				
			||||||
 | 
					            if self.cfg['gpg']['gnupghome']:
 | 
				
			||||||
 | 
					                os.environ['GNUPGHOME'] = self.cfg['gpg']['gnupghome']
 | 
				
			||||||
 | 
					            from . import GPG
 | 
				
			||||||
 | 
					            self.cfg['gpg']['handler'] = GPG.GPGHandler(gnupg_homedir = self.cfg['gpg']['gnupghome'],
 | 
				
			||||||
 | 
					                                                        key_id = self.cfg['gpg']['keyid'])
 | 
				
			||||||
 | 
					        self.gpg = self.cfg['gpg']['handler']
 | 
				
			||||||
 | 
					        for idx, s in enumerate(self.cfg['sources']):
 | 
				
			||||||
            self._download(idx)
 | 
					            self._download(idx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def CreateDirs(self, dirs):
 | 
					    def CreateDirs(self, dirs):
 | 
				
			||||||
        for d in dirs:
 | 
					        for d in dirs:
 | 
				
			||||||
            os.makedirs(d, exist_ok = True)
 | 
					            os.makedirs(d, exist_ok = True)
 | 
				
			||||||
 | 
					            os.chmod(d, 0o700)
 | 
				
			||||||
        return()
 | 
					        return()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _download(self, source_idx):
 | 
					    def _download(self, source_idx):
 | 
				
			||||||
        download = True
 | 
					        download = True
 | 
				
			||||||
        _source = self.cfg['sources'][source_idx]
 | 
					        _source = self.cfg['sources'][source_idx]
 | 
				
			||||||
@ -58,10 +65,12 @@ class Prepper(object):
 | 
				
			|||||||
                if _hash.hexdigest().lower() != _source['checksum']['value'].lower():
 | 
					                if _hash.hexdigest().lower() != _source['checksum']['value'].lower():
 | 
				
			||||||
                    return(False)
 | 
					                    return(False)
 | 
				
			||||||
            return(True)
 | 
					            return(True)
 | 
				
			||||||
        def _sig_verify(gpg_instance):  # TODO: move to utils.valid()? or just use as part of the bdisk.GPG module?
 | 
					        def _sig_verify():  # TODO: move to utils.valid()?
 | 
				
			||||||
            pass
 | 
					            if 'sig' in _source:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					            return(True)
 | 
				
			||||||
        if os.path.isfile(_tarball):
 | 
					        if os.path.isfile(_tarball):
 | 
				
			||||||
            download = _hash_verify()
 | 
					            download = _hash_verify()
 | 
				
			||||||
            download = _sig_verify()
 | 
					            download = _sig_verify()
 | 
				
			||||||
        if download:
 | 
					        if download:
 | 
				
			||||||
            d = utils.Download(_remote_tarball)
 | 
					            d = download.Download(_remote_tarball)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,47 @@
 | 
				
			|||||||
#!/usr/bin/env python3
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Supported initsys values:
 | 
					from .. import download  # LOCAL # do i need to escalate two levels up?
 | 
				
			||||||
# systemd
 | 
					import os
 | 
				
			||||||
# Possible future inclusions:
 | 
					from .. import utils
 | 
				
			||||||
# openrc
 | 
					
 | 
				
			||||||
# runit
 | 
					# TODO: can this be trimmed down?
 | 
				
			||||||
# sinit
 | 
					prereqs = ['arch-install-scripts', 'archiso', 'bzip2', 'coreutils', 'customizepkg-scripting', 'cronie', 'dhclient',
 | 
				
			||||||
# s6
 | 
					           'dhcp', 'dhcpcd', 'dosfstools', 'dropbear', 'efibootmgr', 'efitools', 'efivar', 'file', 'findutils',
 | 
				
			||||||
# shepherd
 | 
					           'iproute2', 'iputils', 'libisoburn', 'localepurge', 'lz4', 'lzo', 'lzop', 'mkinitcpio-nbd',
 | 
				
			||||||
initsys = 'systemd'
 | 
					           'mkinitcpio-nfs-utils', 'mkinitcpio-utils', 'nbd', 'ms-sys', 'mtools', 'net-tools', 'netctl',
 | 
				
			||||||
 | 
					           'networkmanager', 'pv', 'python', 'python-pyroute2', 'rsync', 'sed', 'shorewall', 'squashfs-tools',
 | 
				
			||||||
 | 
					           'sudo', 'sysfsutils', 'syslinux', 'traceroute', 'vi']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Manifest(object):
 | 
				
			||||||
 | 
					    def __init__(self, cfg):
 | 
				
			||||||
 | 
					        self.cfg = cfg
 | 
				
			||||||
 | 
					        self.name = 'archlinux'
 | 
				
			||||||
 | 
					        self.version = None  # rolling release
 | 
				
			||||||
 | 
					        self.release = None  # rolling release
 | 
				
			||||||
 | 
					        # https://www.archlinux.org/master-keys/
 | 
				
			||||||
 | 
					        # Pierre Schmitz. https://www.archlinux.org/people/developers/#pierre
 | 
				
			||||||
 | 
					        self.gpg_authorities = ['4AA4767BBC9C4B1D18AE28B77F2D434B9741E8AC']
 | 
				
			||||||
 | 
					        self.tarball = None
 | 
				
			||||||
 | 
					        self.sig = None
 | 
				
			||||||
 | 
					        self.checksum = {'sha1': None,
 | 
				
			||||||
 | 
					                         'md5': None}
 | 
				
			||||||
 | 
					        self._get_filename()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_filename(self):
 | 
				
			||||||
 | 
					        # TODO: cache this info
 | 
				
			||||||
 | 
					        webroot = 'iso/latest'
 | 
				
			||||||
 | 
					        for m in self.cfg['mirrors']:
 | 
				
			||||||
 | 
					            uri = os.path.join(m, webroot)
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                self.tarball = utils.detect().remote_files(uri, ptrn = ('archlinux-'
 | 
				
			||||||
 | 
					                                                                        'bootstrap-'
 | 
				
			||||||
 | 
					                                                                        '[0-9]{4}\.'
 | 
				
			||||||
 | 
					                                                                        '[0-9]{2}\.'
 | 
				
			||||||
 | 
					                                                                        '[0-9]{2}-'
 | 
				
			||||||
 | 
					                                                                        'x86_64\.tar\.gz$'))[0]
 | 
				
			||||||
 | 
					            except Exception as e:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def extern_prep(cfg, cur_arch = 'x86_64'):
 | 
					def extern_prep(cfg, cur_arch = 'x86_64'):
 | 
				
			||||||
    import os
 | 
					    import os
 | 
				
			||||||
@ -1 +1 @@
 | 
				
			|||||||
import GIT
 | 
					import GIT  # LOCAL
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
#!/usr/bin/env python3
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import argparse
 | 
					import argparse
 | 
				
			||||||
import confparse
 | 
					import confparse  # LOCAL
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""The primary user interface for BDisk. If we are running interactively,
 | 
					"""The primary user interface for BDisk. If we are running interactively,
 | 
				
			||||||
parse arguments first, then initiate a BDisk session."""
 | 
					parse arguments first, then initiate a BDisk session."""
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										100
									
								
								bdisk/utils.py
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								bdisk/utils.py
									
									
									
									
									
								
							@ -3,29 +3,29 @@
 | 
				
			|||||||
import _io
 | 
					import _io
 | 
				
			||||||
import copy
 | 
					import copy
 | 
				
			||||||
import crypt
 | 
					import crypt
 | 
				
			||||||
import GPG
 | 
					import GPG  # LOCAL
 | 
				
			||||||
import getpass
 | 
					import getpass
 | 
				
			||||||
import hashid
 | 
					import hashid
 | 
				
			||||||
import hashlib
 | 
					import hashlib
 | 
				
			||||||
import iso3166
 | 
					import iso3166
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import pprint
 | 
					import pprint
 | 
				
			||||||
import prompt_strings
 | 
					import prompt_strings  # LOCAL
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
import string
 | 
					import string
 | 
				
			||||||
import uuid
 | 
					import uuid
 | 
				
			||||||
import validators
 | 
					import validators
 | 
				
			||||||
import zlib
 | 
					import zlib
 | 
				
			||||||
import requests
 | 
					 | 
				
			||||||
import lxml.etree
 | 
					import lxml.etree
 | 
				
			||||||
import lxml.objectify
 | 
					import lxml.objectify
 | 
				
			||||||
from bs4 import BeautifulSoup
 | 
					from bs4 import BeautifulSoup
 | 
				
			||||||
from collections import OrderedDict
 | 
					from collections import OrderedDict
 | 
				
			||||||
from dns import resolver
 | 
					from dns import resolver
 | 
				
			||||||
 | 
					from download import Download  # LOCAL
 | 
				
			||||||
from email.utils import parseaddr as emailparse
 | 
					from email.utils import parseaddr as emailparse
 | 
				
			||||||
from passlib.context import CryptContext as cryptctx
 | 
					from passlib.context import CryptContext as cryptctx
 | 
				
			||||||
from urllib.parse import urlparse
 | 
					from urllib.parse import urlparse
 | 
				
			||||||
from urllib.request import urlopen
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Supported by all versions of GNU/Linux shadow
 | 
					# Supported by all versions of GNU/Linux shadow
 | 
				
			||||||
passlib_schemes = ['des_crypt', 'md5_crypt', 'sha256_crypt', 'sha512_crypt']
 | 
					passlib_schemes = ['des_crypt', 'md5_crypt', 'sha256_crypt', 'sha512_crypt']
 | 
				
			||||||
@ -43,53 +43,6 @@ crypt_map = {'sha512': crypt.METHOD_SHA512,
 | 
				
			|||||||
             'des': crypt.METHOD_CRYPT}
 | 
					             'des': crypt.METHOD_CRYPT}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Download(object):
 | 
					 | 
				
			||||||
    def __init__(self, url, progress = True, offset = None, chunksize = 1024):
 | 
					 | 
				
			||||||
        self.cnt_len = None
 | 
					 | 
				
			||||||
        self.head = requests.head(url, allow_redirects = True).headers
 | 
					 | 
				
			||||||
        self.req_headers = {}
 | 
					 | 
				
			||||||
        self.range = False
 | 
					 | 
				
			||||||
        self.url = url
 | 
					 | 
				
			||||||
        self.offset = offset
 | 
					 | 
				
			||||||
        self.chunksize = chunksize
 | 
					 | 
				
			||||||
        self.progress = progress
 | 
					 | 
				
			||||||
        if 'accept-ranges' in self.head:
 | 
					 | 
				
			||||||
            if self.head['accept-ranmges'].lower() != 'none':
 | 
					 | 
				
			||||||
                self.range = True
 | 
					 | 
				
			||||||
            if 'content-length' in self.head:
 | 
					 | 
				
			||||||
                try:
 | 
					 | 
				
			||||||
                    self.cnt_len = int(self.head['content-length'])
 | 
					 | 
				
			||||||
                except TypeError:
 | 
					 | 
				
			||||||
                    pass
 | 
					 | 
				
			||||||
            if self.cnt_len and self.offset and self.range:
 | 
					 | 
				
			||||||
                if not self.offset <= self.cnt_len:
 | 
					 | 
				
			||||||
                    raise ValueError(('The offset requested ({0}) is greater than '
 | 
					 | 
				
			||||||
                                      'the content-length value').format(self.offset, self.cnt_len))
 | 
					 | 
				
			||||||
                self.req_headers['range'] = 'bytes={0}-'.format(self.offset)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def fetch(self):
 | 
					 | 
				
			||||||
        if not self.progress:
 | 
					 | 
				
			||||||
            self.req = requests.get(self.url, allow_redirects = True, headers = self.req_headers)
 | 
					 | 
				
			||||||
            self.bytes_obj = self.req.content
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            self.req = requests.get(self.url, allow_redirects = True, stream = True, headers = self.req_headers)
 | 
					 | 
				
			||||||
            self.bytes_obj = bytes()
 | 
					 | 
				
			||||||
            _bytelen = 0
 | 
					 | 
				
			||||||
            # TODO: better handling for logging instead of print()s?
 | 
					 | 
				
			||||||
            for chunk in self.req.iter_content(chunk_size = self.chunksize):
 | 
					 | 
				
			||||||
                self.bytes_obj += chunk
 | 
					 | 
				
			||||||
                if self.cnt_len:
 | 
					 | 
				
			||||||
                    print('\033[F')
 | 
					 | 
				
			||||||
                    print('{0:.2f}'.format((_bytelen / float(self.head['content-length'])) * 100),
 | 
					 | 
				
			||||||
                          end = '%',
 | 
					 | 
				
			||||||
                          flush = True)
 | 
					 | 
				
			||||||
                    _bytelen += self.chunksize
 | 
					 | 
				
			||||||
                else:
 | 
					 | 
				
			||||||
                    print('.', end = '')
 | 
					 | 
				
			||||||
            print()
 | 
					 | 
				
			||||||
        return(self.bytes_obj)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class XPathFmt(string.Formatter):
 | 
					class XPathFmt(string.Formatter):
 | 
				
			||||||
    def get_field(self, field_name, args, kwargs):
 | 
					    def get_field(self, field_name, args, kwargs):
 | 
				
			||||||
        vals = self.get_value(field_name, args, kwargs), field_name
 | 
					        vals = self.get_value(field_name, args, kwargs), field_name
 | 
				
			||||||
@ -159,15 +112,15 @@ class detect(object):
 | 
				
			|||||||
        # But we CAN sort by filename.
 | 
					        # But we CAN sort by filename.
 | 
				
			||||||
        if 'latest' in flags:
 | 
					        if 'latest' in flags:
 | 
				
			||||||
            urls = sorted(list(set(urls)))
 | 
					            urls = sorted(list(set(urls)))
 | 
				
			||||||
            urls = urls[-1]
 | 
					        #     urls = urls[-1]
 | 
				
			||||||
        else:
 | 
					        # else:
 | 
				
			||||||
            urls = urls[0]
 | 
					        #     urls = urls[0]
 | 
				
			||||||
        return(urls)
 | 
					        return(urls)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def gpgkeyID_from_url(self, url):
 | 
					    def gpgkeyID_from_url(self, url):
 | 
				
			||||||
        data = Download(url, progress = False).bytes_obj
 | 
					        data = Download(url, progress = False).bytes_obj
 | 
				
			||||||
        g = GPG.GPGHandler()
 | 
					        g = GPG.GPGHandler()
 | 
				
			||||||
        key_ids = g.get_sigs(data)
 | 
					        key_ids = g.GetSigs(data)
 | 
				
			||||||
        del(g)
 | 
					        del(g)
 | 
				
			||||||
        return(key_ids)
 | 
					        return(key_ids)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -758,17 +711,17 @@ class valid(object):
 | 
				
			|||||||
            return(False)
 | 
					            return(False)
 | 
				
			||||||
        return(True)
 | 
					        return(True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dns(self, addr):
 | 
					    def dns(self, record):
 | 
				
			||||||
        pass
 | 
					        return (not isinstance(validators.domain(record)), validators.utils.ValidationFailure)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def connection(self, conninfo):
 | 
					    def connection(self, conninfo):
 | 
				
			||||||
        # conninfo should ideally be (host, port)
 | 
					        # conninfo should ideally be (host, port)
 | 
				
			||||||
        pass
 | 
					        pass  # TODO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def email(self, addr):
 | 
					    def email(self, addr):
 | 
				
			||||||
        return(
 | 
					        return (
 | 
				
			||||||
            not isinstance(validators.email(emailparse(addr)[1]),
 | 
					            not isinstance(validators.email(emailparse(addr)[1]),
 | 
				
			||||||
                      validators.utils.ValidationFailure))
 | 
					                           validators.utils.ValidationFailure))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def gpgkeyID(self, key_id):
 | 
					    def gpgkeyID(self, key_id):
 | 
				
			||||||
        # Condense fingerprints into normalized 40-char "full" key IDs.
 | 
					        # Condense fingerprints into normalized 40-char "full" key IDs.
 | 
				
			||||||
@ -783,6 +736,33 @@ class valid(object):
 | 
				
			|||||||
            return(False)
 | 
					            return(False)
 | 
				
			||||||
        return(True)
 | 
					        return(True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def gpgsigNotation(self, notations):
 | 
				
			||||||
 | 
					        # RFC 4880 § 5.2.3.16
 | 
				
			||||||
 | 
					        # A valid notation fmt: {'name@domain.tld': {'value': 'some str', 'flags': 1}}
 | 
				
			||||||
 | 
					        if not isinstance(notations, dict):
 | 
				
			||||||
 | 
					            return(False)
 | 
				
			||||||
 | 
					        for n in notations:
 | 
				
			||||||
 | 
					            # namespace
 | 
				
			||||||
 | 
					            s = n.split('@')
 | 
				
			||||||
 | 
					            if not len(s) != 2:
 | 
				
			||||||
 | 
					                return(False)  # IETF namespaces not supported by GPGME?
 | 
				
			||||||
 | 
					            dom = s[1]
 | 
				
			||||||
 | 
					            if not self.dns(dom):
 | 
				
			||||||
 | 
					                return(False)
 | 
				
			||||||
 | 
					            # flags
 | 
				
			||||||
 | 
					            # TODO: is there a better way to do this? basically confirm a value is a valid bitmask?
 | 
				
			||||||
 | 
					            flags = sorted([const for const in vars(GPG.gpg.constants.sig.notation).values() if isinstance(const, int)])
 | 
				
			||||||
 | 
					            if not isinstance(n['flags'], int):
 | 
				
			||||||
 | 
					                return(False)
 | 
				
			||||||
 | 
					            if not n['flags'] >= flags[0]: # at LEAST the lowest flag
 | 
				
			||||||
 | 
					                return(False)
 | 
				
			||||||
 | 
					            if not n['flags'] <= sum(flags):  # at MOST the highest sum of all flags
 | 
				
			||||||
 | 
					                return(False)
 | 
				
			||||||
 | 
					            # values
 | 
				
			||||||
 | 
					            if not isinstance(n['value'], str):  # per RFC, non-text data for values currently is not supported
 | 
				
			||||||
 | 
					                return(False)
 | 
				
			||||||
 | 
					        return(True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def integer(self, num):
 | 
					    def integer(self, num):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            int(num)
 | 
					            int(num)
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user