def get_all_pubkeys(fps, ldap, use_agent, gpg_path, gnupghome): """ Return dict {"email": ["public key in PEM format",...]} """ r = {} with GPG(use_agent, gpg_path, gnupghome) as gpg: for fp in fps: debug( 'Fetching public key with fingerprint {} (from configuration file).' .format(fp)) key = gpg.find_key(fp) if key is None: sys.exit('Key with fingerprint {} not found.'.format(fp)) uid = key['uids'][0] key_data = gpg.gpg.export_keys(fp) if uid not in r: r[uid] = [] r[uid].append(base64.b64encode(key_data.encode('utf-8'))) if ldap is not None: debug('Fetching public keys from LDAP') try: r.update( get_ldap_group_keys( ldap, ldap.get('bind_pw', getpass.getpass(prompt='LDAP password:')))) except RuntimeError as e: sys.exit(str(e)) return r
def _sync_pws(datapath, force, dec_gpg, enc_gpg): accounts = get_all_passwords(datapath) git = Git(datapath) with GitTransaction(git): num = 0 if git.has_origin(): git.rebase_origin_master() for (host, user, path) in accounts.iterate(): # There are three cases where the file needs to be reencrypted: # 1. The force flag is set # 2. The file is encrypted to a key that is no longer available, # or expected as a recipient # 3. The list of recipients do not match the expected recipient list (rec_fps, rec_not_found) = enc_gpg.get_file_recipients(path) if force or rec_not_found or sorted(rec_fps) != sorted( enc_gpg.get_recipient_fps()): debug('Need to reencrypt {}'.format(path)) encpw = enc_gpg.encrypt(dec_gpg.decrypt_file(path)) write_and_add(git, path, encpw, True) num += 1 if num > 0: uids = ''.join( [' - {}\n'.format(x) for x in enc_gpg.get_recipient_uids()]) git.commit( "Synchronized and reencrypted {} passwords to {} recipient{}{}\n\n{}\n\n{}" .format(num, enc_gpg.get_num_recipients(), 's' if enc_gpg.get_num_recipients() > 1 else '', ' (forced)' if force else '', uids, get_version())) if git.has_origin(): git.push_master() return num
def sync_pws(cfg, args): if 'ldap' not in cfg: ldap = None else: ldap = cfg['ldap'] keys = get_all_pubkeys(get_fps_from_conf(cfg), ldap, cfg['gnupg'].getboolean('use_agent'), cfg['gnupg']['gpg_path'], cfg['gnupg']['home']) with GPG(gpg_path=cfg['gnupg']['gpg_path'], use_agent=False) as enc_gpg: for email, fps in keys.items(): debug('Encrypting to {} ({} key{})'.format( email, len(fps), 's' if len(keys) > 1 else '')) for fp in fps: enc_gpg.add_recipient(fp) with GPG(use_agent=cfg['gnupg'].getboolean('use_agent'), gpg_path=cfg['gnupg']['gpg_path'], gnupghome=cfg['gnupg']['home']) as dec_gpg: if not cfg['gnupg'].getboolean('use_agent'): # gpg-agent should automatically popup a different password dialog so # we should only ask for the password if we're not using it dec_gpg.set_passphrase(args.gnupgpass) num = attempt_retry(_sync_pws, cfg['global']['datapath'], args.force, dec_gpg, enc_gpg) if num == 0: print("No synchronization necessary, recipient lists were correct.") else: print("Successfully reencrypted {} passwords to {} recipients.".format( num, enc_gpg.get_num_recipients()))
def _add_pw(host, user, password, datapath, keys, exist_ok, gpg_path, gnupghome): accounts = get_all_passwords(datapath) if not exist_ok and accounts.exists(host, user): sys.exit( "Account {} on {} already exists, use replace instead.".format( user, host)) if password is None: new_pass = getpass.getpass('Enter password to store:') if getpass.getpass('Enter it again to verify:') != new_pass: sys.exit("Passwords don't match.") else: new_pass = password # We are only encrypting and using different keyrings, so do not use the # users gpg-agent even if we were instructed to do so. with GPG(False, gpg_path, gnupghome) as gpg: for email, fp_list in keys.items(): debug('Encrypting to {} ({} key{})'.format( email, len(fp_list), 's' if len(fp_list) > 1 else '')) for fp in fp_list: gpg.add_recipient(fp) # Adding a trailing newline makes it prettier if someone decodes using # regular command line gnupg. encpw = gpg.encrypt('{}\n'.format(new_pass)) path = hostuser_to_path(datapath, host, user) attempt_retry(do_add, datapath, path, host, user, encpw, exist_ok) return gpg.get_num_recipients()
def __enter__(self): debug('Locking datastore at {}'.format(os.path.dirname(self.path))) try: f = open(self.path, 'x') except FileExistsError: f = open(self.path, 'r') fcntl.flock(f, fcntl.LOCK_EX) self.f = f
def __exit__(self, type, value, tb): if tb is not None: # An exception occured, roll back. The exception will be re-raised # once this handler is complete. debug('Exception occured, aborting git transaction to saved state') self.rollback() else: debug('Git transaction completed cleanly') self.git.rm_tag(self.tag)
def decrypt(self, data): debug('Decrypting using gnupg homedir {}'.format(self.gnupghome)) if not self.use_agent: if self.passphrase is None: raise RuntimeError("No GPG password set") r = self.gpg.decrypt(data, passphrase=self.passphrase) else: r = self.gpg.decrypt(data) if not r.ok: raise RuntimeError("Decryption failed ({})".format(r.status)) return str(r)
def get_dn_attribute(conn, dn, filtr, attr): """ Return an attribute for the LDAP entry specified by dn. """ debug("Getting LDAP attribute '{}' for DN={} with filter '{}'".format( attr, dn, filtr)) r = conn.search(dn, filtr, attributes=[attr]) if r is False or len(conn.entries) != 1: raise RuntimeError( "dn '{}' filter '{}' return did not return exactly one hit".format( dn, filtr)) if attr not in conn.entries[0]: return [] return [x for x in conn.entries[0][attr]]
def attempt_retry(fnc, *args, **kwargs): # This is useful because e.g. some git transactions will fail if multiple # people are committing and rebasing simultaneously. Retrying them a few # times make sense. attempt = 0 max_attempts = 5 while True: attempt += 1 debug('Attempting {}, attempt {}/{}'.format(getattr(fnc, '__name__'), attempt, max_attempts)) try: return fnc(*args, **kwargs) except Exception as e: debug('Attempt failed, caught {}: {}'.format(type(e), str(e))) if attempt >= 5: raise time.sleep(0.5)
def run_git(self, cmdl): cmdl.insert(0, self.git) stdout = [] stderr = [] debug("Running '{}' in {}".format(' '.join(cmdl).replace('\n', '\\n'), self.repo_path)) p = subprocess.Popen(cmdl, cwd=self.repo_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: (stdout, stderr) = p.communicate(timeout=60) except subprocess.TimeoutExpired: sys.exit("git commandline '{}' timed out".format(cmdl)) output = '{}{}'.format( stdout.decode('utf-8') if stdout else '', stderr.decode('utf-8') if stderr else '') if p.returncode != 0: if not self.silent: print(output) raise subprocess.CalledProcessError(cmd=cmdl, returncode=p.returncode, output=output) # The output from git tends to be very verbose so we make it a bit # more dense here from debugging purposes. debug('git returned success: {}'.format( '(no output)' if not output else '')) for line in output.split('\n'): if line == '' or line.isspace(): continue debug(' {}'.format(line)) return output
def _rm_pw(datapath, host, user, pwfile): def do_rm(pwfile, host, user): git = Git(datapath) with GitTransaction(git): if git.has_origin(): git.rebase_origin_master() git.rm(pwfile) git.commit("{}/{}: remove\n\n{}".format(host, user, get_version())) if git.has_origin(): git.push_master() debug("Removing password for account '{}/{}'".format(host, user)) attempt_retry(do_rm, pwfile, host, user) try: os.rmdir(os.path.dirname(pwfile)) except OSError as e: if e.errno == errno.ENOTEMPTY: debug("More accounts exist for '{}', not removing host.".format( host)) else: debug("No more accounts exist for '{}', host removed.".format(host))
def __exit__(self, type, value, tb): debug('Unlocking datastore at {}'.format(os.path.dirname(self.path))) # We can never remove the lock file or we might trigger a lock race self.f.close()
def set_passphrase(self, pw=None): if pw is None: pw = getpass.getpass('Enter GPG password:'******'Passphrase already supplied') self.passphrase = pw
def decrypt_file(self, path): debug('Trying to decrypt file {}'.format(path)) with open(path, 'rb') as f: data = f.read() return self.decrypt(data)
def __enter__(self): debug('Starting git transaction {}'.format(self.tag)) self.git.tag(self.tag)