def _find_dictionary(self, filename, settings_dir): """Find Dictionary Finds the file that contains the dictionary of words used to construct pass phrases. Initially looks in the settings directory, if not there look in install directory. """ path = make_path(settings_dir, filename) #if not exists(path): # path = make_path(get_head(__file__), filename) if not exists(path): path = make_path(get_head(__file__), make_path('..', filename)) if not file_is_readable(path): self.logger.error("%s: cannot open dictionary." % path) return path
def _validate_assumptions(self): # Check that dictionary has not changed. # If the master password file exists, then self.data['dict_hash'] will # exist, and we will compare the current hash for the dictionary # against that stored in the master password file, otherwise we will # compare against the one present when the program was configured. self.dictionary.validate(self.data.get('dict_hash', DICTIONARY_SHA1)) # Check that secrets.py and charset.py have not changed for each, sha1 in [ ('secrets', SECRETS_SHA1), ('charsets', CHARSETS_SHA1) ]: path = make_path(get_head(__file__), each + '.py') try: with open(path) as f: contents = f.read() except IOError as err: path = make_path(get_head(__file__), '..', each + '.py') try: with open(path) as f: contents = f.read() except IOError as err: self.logger.error('%s: %s.' % (err.filename, err.strerror)) hash = hashlib.sha1(contents.encode('utf-8')).hexdigest() # Check that file has not changed. # If the master password file exists, then self.data['%s_hash'] # will exist, and we will compare the current hash for the file # against that stored in the master password file, otherwise we # will compare against the one present when the program was # configured. if hash != self.data.get('%s_hash' % each, sha1): self.logger.display("Warning: '%s' has changed." % path) self.logger.display(" " + "\n ".join(wrap(' '.join([ "This could result in passwords that are inconsistent", "with those created in the past.", 'Update the corresponding hash in %s/%s to "%s".' % ( DEFAULT_SETTINGS_DIR, MASTER_PASSWORD_FILENAME, hash), "Then use 'abraxas --changed' to assure that nothing has", "changed." ]))))
def __init__(self, argv): """Read the Command Line""" self.prog_name = get_tail(argv[0]) parser = argparse.ArgumentParser( add_help=False, description="Generate strong and unique password.") arguments = parser.add_argument_group('arguments') arguments.add_argument( 'account', nargs='?', default='', help="Generate password specific to this account.") parser.add_argument( '-P', '--password', action='store_true', help="Output the password (default if nothing else is requested).") parser.add_argument( '-N', '--username', action='store_true', help="Output the username.") parser.add_argument( '-Q', '--question', type=int, metavar='<N>', default=None, help="Output security question N.") parser.add_argument( '-A', '--account-number', action='store_true', help="Output the account number.") parser.add_argument( '-E', '--email', action='store_true', help="Output the email.") parser.add_argument( '-U', '--url', action='store_true', help="Output the URL.") parser.add_argument( '-R', '--remarks', action='store_true', help="Output remarks.") parser.add_argument( '-i', '--info', action='store_true', help="Output everything, except the password.") parser.add_argument( '-a', '--all', action='store_true', help="Output everything, including the password.") group = parser.add_mutually_exclusive_group() group.add_argument( '-q', '--quiet', action='store_true', help="Disable all non-essential output.") group.add_argument( '-c', '--clipboard', action='store_true', help="Write output to clipboard rather than stdout.") group.add_argument( '-t', '--autotype', action='store_true', help=(' '.join([ "Mimic keyboard to send output to the active window rather", "than stdout. In this case any command line arguments that", "specify what to output are ignored and the autotype entry", "scripts the output."]))) parser.add_argument( '-f', '--find', type=str, metavar='<str>', help=(' '.join([ "List any account that contains the given string", "in its ID or aliases."]))) parser.add_argument( '-s', '--search', type=str, metavar='<str>', help=(' '.join([ "List any account that contains the given string in", "%s, or its ID." % ', '.join(SEARCH_FIELDS)]))) parser.add_argument( '-S', '--stateless', action='store_true', help="Do not use master password or accounts file.") parser.add_argument( '-T', '--template', type=str, metavar='<template>', default=None, help="Template to use if account is not found.") parser.add_argument( '-b', '--default-browser', action='store_true', help="Open account in the default browser (%s)." % DEFAULT_BROWSER) browsers = [ '%s (%s)' % (k, BROWSERS[k].split()[0]) for k in sorted(BROWSERS) ] parser.add_argument( '-B', '--browser', type=str, metavar='<browser>', help="Open account in the specified browser (choose from %s)." % ( ', '.join(browsers))) parser.add_argument( '-n', '--notify', action='store_true', help="Output messages to notifier.") parser.add_argument( '-l', '--list', action='store_true', help=(' '.join([ "List available master passwords and templates (only pure", "templates are listed, not accounts, even though accounts", "can be used as templates)."]))) parser.add_argument( '-w', '--wait', type=float, default=60, metavar='<secs>', help=(' '.join([ "Wait this long before clearing the secret", "(use 0 to disable)."]))) parser.add_argument( '--archive', action='store_true', help=("Archive all the secrets to %s." % make_path( DEFAULT_SETTINGS_DIR, DEFAULT_ARCHIVE_FILENAME))) parser.add_argument( '-e', '--export', action='store_true', help=("Export to Avendesora.")) parser.add_argument( '--changed', action='store_true', help=( "Identify all secrets that have changed since last archived.")) parser.add_argument( '-I', '--init', type=str, metavar='<GPG ID>', help=(' '.join([ "Initialize the master password and account files in", DEFAULT_SETTINGS_DIR, "(but only if they do not already exist)."]))) parser.add_argument( '-v', '--version', action='store_true', help="Show Abraxas version number and exit.") parser.add_argument( '-h', '--help', action='store_true', help="Show this help message and exit.") args = parser.parse_args() # If requested, print help message and exit if args.help: parser.print_help() sys.exit() if args.version: print('Abraxas version %s (%s).' % (VERSION, DATE)) sys.exit() # Save all the command line arguments as attributes of self self.__dict__.update(args.__dict__)
def __init__( self, settings_dir=None, init=None, logger=None, gpg_home=None, stateless=False ): """ Arguments: settings_dir (string) Path to the settings directory. Generally only specified when testing. init (string) User's GPG ID. When present, the settings directory is assumed not to exist and so is created using the specified ID. logger (object) Instance of class that provides display(), log(), debug(), error(), terminate() and set_logfile() methods: display(msg) is called when a message is to be sent to the user. log(msg) is called when a message is only to be logged. debug(msg) is called for debugging messages. error(msg) is called when an error has occurred, should not return. terminate() is called to indicate program has terminated normally. set_logfile(logfile, gpg, gpg_id) is called to specify information about the logfile, in particular, the path to the logfile, a gnupg encryption object, and the GPG ID. The last two must be specified if the logfile has an encryption extension (.gpg or .asc). gpg_home (string) Path to desired home directory for gpg. stateless (bool) Boolean that indicates that Abraxas should operate without accessing the user's master password and accounts files. """ if not settings_dir: settings_dir = DEFAULT_SETTINGS_DIR self.settings_dir = expand_path(settings_dir) if not logger: logger = Logging() self.logger = logger self.stateless = stateless self.accounts_path = make_path( self.settings_dir, DEFAULT_ACCOUNTS_FILENAME) # Get the dictionary self.dictionary = Dictionary( DICTIONARY_FILENAME, self.settings_dir, logger) # Activate GPG gpg_args = {'gpgbinary': GPG_BINARY} if gpg_home: gpg_args.update({'gnupghome': gpg_home}) self.gpg = gnupg.GPG(**gpg_args) # Process master password file self.master_password_path = make_path( self.settings_dir, MASTER_PASSWORD_FILENAME) if init: self._create_initial_settings_files(gpg_id=init) self.master_password = _MasterPassword( self.master_password_path, self.dictionary, self.gpg, self.logger, stateless) try: path = self.master_password.data['accounts'] if path: self.accounts_path = make_path(self.settings_dir, path) except KeyError: pass
def avendesora_archive(self): """ Avendesora Archive Save all account information to Avendesora files. """ from binascii import b2a_base64, Error as BinasciiError self.logger.log("Archive secrets.") source_files = set() dest_files = {} gpg_ids = {} avendesora_dir = make_path(self.settings_dir, 'avendesora') mkdir(avendesora_dir) header = dedent('''\ # Translated Abraxas Accounts file (%s) # vim: filetype=python sw=4 sts=4 et ai ff=unix fileencoding='utf8' : # # It is recommended that you not modify this file directly. Instead, # if you wish to modify an account, copy it to an account file not # associated with Abraxas and modify it there. Then, to avoid # conflicts, add the account name to ~/.config/abraxas/do-not-export # and re-export the accounts using 'abraxas --export'. from avendesora import Account, Hidden, Question, RecognizeURL, RecognizeTitle ''') # read do-not-export file try: with open(make_path(self.settings_dir, 'do-not-export')) as f: do_not_export = set(f.read().split()) except IOError as err: do_not_export = set([]) def make_camel_case(text): text = text.translate(maketrans('@.-', ' ')) text = ''.join([e.title() for e in text.split()]) if text[0] in '0123456789': text = '_' + text return text def make_identifier(text): text = text.translate(maketrans('@.- ', '____')) if text[0] in '0123456789': text = '_' + text return text # Loop through accounts saving passwords and questions all_secrets = {} for account_id in self.all_accounts(): account = self.get_account(account_id, quiet=True) data = account.__dict__['data'] ID = account.__dict__['ID'] #aliases = data.get('aliases', []) #if set([ID] + aliases) & do_not_export: if ID in do_not_export: print('skipping', ID) continue class_name = make_camel_case(ID) output = [ 'class %s(Account): # %s' % (class_name, '{''{''{1') ] # TODO -- must make ID a valid class name: convert xxx-xxx to camelcase self.logger.debug(" Saving %s account." % ID) try: source_filepath = data['_source_file_'] dest_filepath = make_path( avendesora_dir, rel_path(source_filepath, self.settings_dir) ) if source_filepath not in source_files: source_files.add(source_filepath) # get recipient ids from existing file if get_extension(source_filepath) in ['gpg', 'asc']: try: gpg = Execute( ['gpg', '--list-packets', source_filepath], stdout=True, wait=True ) gpg_ids[dest_filepath] = [] for line in gpg.stdout.split('\n'): if line.startswith(':pubkey enc packet:'): words = line.split() assert words[7] == 'keyid' gpg_ids[dest_filepath].append(words[8]) except ExecuteError as err: print(str(err)) else: gpg_ids[dest_filepath] = None dest_files[dest_filepath] = {None: header % source_filepath} except KeyError: raise AssertionError('%s: SOURCE FILE MISSING.' % ID) except IOError as err: self.logger.error('%s: %s.' % (err.filename, err.strerror)) output.append(" NAME = %r" % ID) password = self.generate_password(account) output.append(" passcode = Hidden(%r)" % b2a_base64( password.encode('ascii')).strip().decode('ascii') ) questions = [] for question in account.get_security_questions(): # convert the result to a list rather than leaving it a tuple # because tuples are formatted oddly in yaml questions += [list(self.generate_answer(question, account))] self.logger.debug( " Saving question (%s) and its answer." % question) if questions: output.append(" questions = [") for question, answer in questions: output.append(" Question(%r, answer=Hidden(%r))," % ( question, b2a_base64(answer.encode('ascii')).strip().decode('ascii') )) output.append(" ]") if 'autotype' in data: autotype = data['autotype'].replace('{password}', '{passcode}') else: if 'username' in data: autotype = '{username}{tab}{passcode}{return}' else: autotype = '{email}{tab}{passcode}{return}' discovery = [] if 'url' in data: urls = [data['url']] if type(data['url']) == str else data['url'] discovery.append('RecognizeURL(%s, script=%r)' % ( ', '.join([repr(e) for e in urls]), autotype )) if 'window' in data: windows = [data['window']] if type(data['window']) == str else data['window'] discovery.append('RecognizeTitle(%s, script=%r)' % ( ', '.join([repr(e) for e in windows]), autotype )) if discovery: output.append(" discovery = [") for each in discovery: output.append(" %s," % each) output.append(" ]") for k, v in data.items(): if k in [ 'password', 'security questions', '_source_file_', 'password-type', 'master', 'num-words', 'num-chars', 'alphabet', 'template', 'url', 'version', 'autotype', 'window', ]: continue key = make_identifier(k) if type(v) == str and '\n' in v: output.append(' %s = """' % key) for line in dedent(v.strip('\n')).split('\n'): if line: output.append(' %s' % line.rstrip()) else: output.append('') output.append(' """') else: output.append(" %s = %r" % (key, v)) output.append('') output.append('') dest_files[dest_filepath][ID] = '\n'.join(output) # This version uses default gpg id to encrypt files. # Could also take gpg ids from actual files. # The gpg ids are gathered from files above, but code to use them is # currently commented out. for filepath, accounts in dest_files.items(): try: header = accounts.pop(None) contents = '\n'.join( [header] + [accounts[k] for k in sorted(accounts)] ) mkdir(get_head(filepath)) os.chmod(get_head(filepath), 0o700) print('%s: writing.' % filepath) # encrypt all files with default gpg ID #if gpg_ids[filepath]: # gpg_id = gpg_ids[filepath] if True: if get_extension(filepath) not in ['gpg', 'asc']: filepath += '.gpg' gpg_id = self.accounts.get_gpg_id() encrypted = self.gpg.encrypt( contents, gpg_id, always_trust=True, armor=True ) if not encrypted.ok: self.logger.error( "%s: unable to encrypt.\n%s" % ( filename, encrypted.stderr)) contents = str(encrypted) with open(filepath, 'w') as f: f.write(contents) os.chmod(filepath, 0o600) except IOError as err: self.logger.error('%s: %s.' % (err.filename, err.strerror))
def _create_initial_settings_files(self, gpg_id): """ Create initial version of settings files for the user (PRIVATE) Will create initial versions of the master password file and the accounts file, but only if they do not already exist. The master password file is encrypted with the GPG ID given on the command line, which should be the users. Arguments: Requires user's GPG ID (string) as the only argument. """ def create_file(filename, contents, encrypt=False): if encrypt: encrypted = self.gpg.encrypt( contents, gpg_id, always_trust=True, armor=True ) if not encrypted.ok: self.logger.error( "%s: unable to encrypt.\n%s" % ( filename, encrypted.stderr)) contents = str(encrypted) if is_file(filename): self.logger.display("%s: already exists." % filename) else: try: with open(filename, 'w') as file: file.write(contents) os.chmod(filename, 0o600) self.logger.display("%s: created." % filename) except IOError as err: self.logger.error('%s: %s.' % (err.filename, err.strerror)) def generate_random_string(): # Generate a random long string to act as the default password from string import ascii_letters, digits, punctuation import random # Create alphabet from letters, digits, and punctuation, but # replace double quote with a space so password can be safely # represented as a double-quoted string. alphabet = (ascii_letters + digits + punctuation).replace('"', ' ') rand = random.SystemRandom() password = '' for i in range(64): password += rand.choice(alphabet) return password mkdir(self.settings_dir) default_password = generate_random_string() if self.settings_dir != expand_path(DEFAULT_SETTINGS_DIR): # If settings_dir is not the DEFAULT_SETTINGS_DIR, then this is # probably a test, in which case we do not want to use a # random password as it would cause the test results to vary. # Still want to generate the random string so that code gets # tested. It has been the source of trouble in the past. default_password = '******' create_file( self.master_password_path, MASTER_PASSWORD_FILE_INITIAL_CONTENTS % ( self.dictionary.hash, SECRETS_SHA1, CHARSETS_SHA1, DEFAULT_ACCOUNTS_FILENAME, default_password), encrypt=True) create_file( self.accounts_path, ACCOUNTS_FILE_INITIAL_CONTENTS % ( make_path(self.settings_dir, DEFAULT_LOG_FILENAME), make_path(self.settings_dir, DEFAULT_ARCHIVE_FILENAME), gpg_id), encrypt=(get_extension(self.accounts_path) in ['gpg', 'asc']))
def get_archive_file(self): return self.data.get( 'archive_file', make_path(DEFAULT_SETTINGS_DIR, DEFAULT_ARCHIVE_FILENAME))
def get_log_file(self): return self.data.get( 'log_file', make_path(DEFAULT_SETTINGS_DIR, DEFAULT_LOG_FILENAME))
def _read_accounts_file(self): if not self.path: # There is no accounts file self.data = {} return self.data if not exists(self.path): # If file does not exist, look for encrypted versions for ext in ['gpg', 'asc']: new_path = '.'.join([self.path, ext]) if exists(new_path): self.path = new_path break logger = self.logger accounts_data = {} try: if get_extension(self.path) in ['gpg', 'asc']: # Accounts file is GPG encrypted, decrypt it before loading with open(self.path, 'rb') as f: decrypted = self.gpg.decrypt_file(f) if not decrypted.ok: logger.error("%s\n%s" % ( "%s: unable to decrypt." % (self.path), decrypted.stderr)) code = compile(decrypted.data, self.path, 'exec') exec(code, accounts_data) else: # Accounts file is not encrypted with open(self.path) as f: code = compile(f.read(), self.path, 'exec') exec(code, accounts_data) if 'accounts' not in accounts_data: logger.error( "%s: defective accounts file, 'accounts' not found." % self.path ) for account in accounts_data['accounts'].values(): account['_source_file_'] = self.path # Load additional accounts files additional_accounts = accounts_data.get('additional_accounts', []) if type(additional_accounts) == str: additional_accounts = [additional_accounts] for each in additional_accounts: more_accounts = {} path = make_path(get_head(self.path), each) try: if get_extension(path) in ['gpg', 'asc']: # Accounts file is GPG encrypted, decrypt it with open(path, 'rb') as f: decrypted = self.gpg.decrypt_file(f) if not decrypted.ok: logger.error("%s\n%s" % ( "%s: unable to decrypt." % (path), decrypted.stderr)) continue code = compile(decrypted.data, path, 'exec') exec(code, more_accounts) else: # Accounts file is not encrypted with open(path) as f: code = compile(f.read(), path, 'exec') exec(code, more_accounts) except IOError as err: logger.display('%s: %s. Ignored' % ( err.filename, err.strerror )) continue existing_names = set(accounts_data['accounts'].keys()) new_accounts = more_accounts.get('accounts', {}) new_names = set(new_accounts.keys()) names_in_common = sorted( existing_names.intersection(new_names)) if len(names_in_common) > 2: logger.display( "%s: overrides existing accounts:\n %s" % ( path, ',\n '.join(sorted(names_in_common)))) elif names_in_common: logger.display("%s: overrides existing account: %s" % ( path, names_in_common[0])) for account in new_accounts.values(): account['_source_file_'] = path accounts_data['accounts'].update(new_accounts) except IOError as err: logger.error('%s: %s.' % (err.filename, err.strerror)) except SyntaxError as err: traceback.print_exc(0) sys.exit() self.data = accounts_data return accounts_data['accounts']
def _read_master_password_file(self): data = { 'accounts': None, 'passwords': {}, 'default_password': None, 'password_overrides': {}, 'additional_master_password_files': [], } if not self.stateless: try: with open(self.path, 'rb') as f: decrypted = self.gpg.decrypt_file(f) if not decrypted.ok: self.logger.error("%s" % "%s: unable to decrypt." % (self.path), ) code = compile(decrypted.data, self.path, 'exec') exec(code, data) except IOError as err: self.logger.display( 'Warning: could not read master password file %s: %s.' % ( err.filename, err.strerror)) except SyntaxError as err: traceback.print_exc(0) sys.exit() # assure that the keys on the master passwords are strings for ID in data.get('passwords', {}): if type(ID) != str: self.logger.error( '%s: master password ID must be a string.' % ID) # Open additional master password files additional_password_files = data.get( 'additional_master_password_files', []) if type(additional_password_files) == str: additional_password_files = [additional_password_files] for each in additional_password_files: more_data = {} path = make_path(get_head(self.path), each) if get_extension(path) in ['gpg', 'asc']: # File is GPG encrypted, decrypt it try: with open(path, 'rb') as f: decrypted = self.gpg.decrypt_file(f) if not decrypted.ok: self.logger.error("%s" % "%s: unable to decrypt." % (path), ) continue code = compile(decrypted.data, path, 'exec') exec(code, more_data) except IOError as err: self.logger.display('%s: %s. Ignored.' % ( err.filename, err.strerror )) continue else: self.logger.error( "%s: must have .gpg or .asc extension" % (path)) # Check for duplicate master passwords existing_names = set(data.get('passwords', {}).keys()) new_passwords = more_data.get('passwords', {}) new_names = set(new_passwords.keys()) names_in_common = sorted( existing_names.intersection(new_names)) if names_in_common: self.logger.display( "%s: overrides existing password:\n %s" % ( path, ',\n '.join(sorted(names_in_common)))) data['passwords'].update(new_passwords) # Check for duplicate passwords overrides existing_names = set(data['password_overrides'].keys()) new_overrides = more_data.get('password_overrides', {}) new_names = set(new_overrides.keys()) names_in_common = sorted( existing_names.intersection(new_names)) if names_in_common: self.logger.display( "%s: overrides existing password overrides:\n %s" % ( path, ',\n '.join(sorted(names_in_common)))) data['password_overrides'].update(new_overrides) return data