def archive_secrets(self): """ Archive secrets Save all secrets to the archive file. """ self.logger.log("Archive secrets.") try: import yaml except ImportError: self.logger.error( 'archive feature requires yaml, which is not available.') # Loop through accounts saving passwords and questions all_secrets = {} for account_id in self.all_accounts(): questions = [] account = self.get_account(account_id, quiet=True) password = self.generate_password(account) self.logger.debug(" Saving password.") 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) all_secrets[account_id] = { 'password': password, 'questions': questions } # Convert results to yaml archive unencrypted_secrets = yaml.dump(all_secrets) # Encrypt and save yaml archive gpg_id = self.accounts.get_gpg_id() encrypted_secrets = self.gpg.encrypt(unencrypted_secrets, gpg_id) filename = expand_path(self.accounts.get_archive_file()) try: with open(filename, 'w') as f: f.write(str(encrypted_secrets)) os.chmod(filename, 0o600) except IOError as err: self.logger.error('%s: %s.' % (err.filename, err.strerror))
def _terminate(self): if not self.logfile: return contents = '\n'.join(self.cache) + '\n' filename = expand_path(self.logfile) if get_extension(filename) in ['gpg', 'asc']: encrypted = self.gpg.encrypt( contents.encode('utf8', 'ignore'), self.gpg_id, always_trust=True, armor=True ) if not encrypted.ok: sys.stderr.write( "%s: unable to encrypt.\n%s" % (filename, encrypted.stderr) ) contents = str(encrypted) try: with open(filename, 'w') as file: file.write(contents) os.chmod(filename, 0o600) except IOError as err: sys.stderr.write('%s: %s.\n' % (err.filename, err.strerror))
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 print_changed_secrets(self): """ Identify updated secrets Inform the user of any secrets that have changed since they have been archived. """ self.logger.log("Print changed secrets.") try: import yaml except ImportError: self.logger.error( 'archive feature requires yaml, which is not available.') filename = expand_path(self.accounts.get_archive_file()) try: with open(filename, 'rb') as f: encrypted_secrets = f.read() except IOError as err: self.logger.error('%s: %s.' % (err.filename, err.strerror)) unencrypted_secrets = str(self.gpg.decrypt(encrypted_secrets)) archived_secrets = yaml.load(unencrypted_secrets) # Look for changes in the accounts archived_ids = set(archived_secrets.keys()) current_ids = set(list(self.all_accounts())) new_ids = current_ids - archived_ids deleted_ids = archived_ids - current_ids if new_ids: self.logger.display( "NEW ACCOUNTS:\n %s" % '\n '.join(new_ids)) else: self.logger.log("No new accounts.") if deleted_ids: self.logger.display( "DELETED ACCOUNTS:\n %s" % '\n '.join(deleted_ids)) else: self.logger.log("No deleted accounts.") # Loop through the accounts, and compare the secrets accounts_with_password_diffs = [] accounts_with_question_diffs = [] for account_id in self.all_accounts(): questions = [] account = self.get_account(account_id, quiet=True) password = self.generate_password(account) for i in account.get_security_questions(): questions += [list(self.generate_answer(i, account))] if account_id in archived_secrets: # check that password is unchanged if password != archived_secrets[account_id]['password']: accounts_with_password_diffs += [account_id] self.logger.display("PASSWORD DIFFERS: %s" % account_id) else: self.logger.debug(" Password matches.") # check that number of questions is unchanged archived_questions = archived_secrets[account_id]['questions'] if len(questions) != len(archived_questions): accounts_with_question_diffs += [account_id] self.logger.display( ' '.join([ "NUMBER OF SECURITY QUESTIONS CHANGED:", "%s (was %d, is now %d)" % ( account_id, len(archived_questions), len(questions))])) else: self.logger.debug( " Number of questions match (%d)." % ( len(questions))) # check that questions and answers are unchanged pairs = zip(archived_questions, questions) for i, (archived, new) in enumerate(pairs): if archived[0] != new[0]: self.logger.display( "QUESTION %d DIFFERS: %s (%s -> %s)." % ( i, account_id, archived[0], new[0])) else: self.logger.debug( " Question %d matches (%s)." % (i, new[0])) if archived[1] != new[1]: self.logger.display( "ANSWER TO QUESTION %d DIFFERS: %s (%s)." % ( i, account_id, new[0])) else: self.logger.debug( " Answer %d matches (%s)." % (i, new[0])) if accounts_with_password_diffs: self.logger.log( "Accounts with changed passwords:\n %s" % ',\n '.join( accounts_with_password_diffs)) else: self.logger.log("No accounts with changed passwords") if accounts_with_question_diffs: self.logger.log( "Accounts with changed questions:\n %s" % ',\n '.join( accounts_with_question_diffs)) else: self.logger.log("No accounts with changed questions")
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']))