Example #1
0
    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
Example #2
0
    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."
                ]))))
Example #3
0
    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__)
Example #4
0
    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
Example #5
0
    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))
Example #6
0
    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']))
Example #7
0
 def get_archive_file(self):
     return self.data.get(
         'archive_file',
         make_path(DEFAULT_SETTINGS_DIR, DEFAULT_ARCHIVE_FILENAME))
Example #8
0
 def get_log_file(self):
     return self.data.get(
         'log_file',
         make_path(DEFAULT_SETTINGS_DIR, DEFAULT_LOG_FILENAME))
Example #9
0
    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']
Example #10
0
    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