def gather_public_keys(self): comment(' gathering public keys') keyname = self.keyname data = self.data clients = conjoin(self.data.get('clients', [])) default_purpose = fmt('This key allows access from {clients}.') purpose = self.data.get('purpose', default_purpose) servers = self.data.get('servers', []) prov = '.provisional' if self.trial_run else '' # read contents of public key try: pubkey = to_path(keyname + '.pub') key = pubkey.read_text().strip() except OSError as err: narrate('%s, skipping.' % os_error(err)) return # get fingerprint of public key try: keygen = Run(['ssh-keygen', '-l', '-f', pubkey], modes='wOeW') fields = keygen.stdout.strip().split() fingerprint = ' '.join([fields[0], fields[1], fields[-1]]) except OSError as err: error(os_error(err)) return # contribute commented and restricted public key to the authorized_key # file for each server for server in servers: if self.update and server not in self.update: continue if server in self.skip: continue server_data = servers[server] description = server_data.get('description', None) restrictions = server_data.get('restrictions', []) remarks = [ '# %s' % t for t in cull([purpose, description, self.warning, fingerprint]) if t ] include_file = server_data.get( 'remote-include-filename', data['remote-include-filename'] ) bypass = server_data.get('bypass') authkeys = AuthKeys(server, include_file, bypass, self.trial_run) authkeys.add_public_key(keyname, key, remarks, restrictions) if not servers: warn( 'no servers specified, you must update them manually.', culprit=keyname )
def run(cls, command, args, settings, options): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) mount_point = cmdline['<mount_point>'] archive = cmdline['--archive'] date = cmdline['--date'] mount_all = cmdline['--all'] include_external_archives = cmdline['--include-external'] # get the desired archive if not archive: if date: archive = get_name_of_nearest_archive(settings, date) elif not mount_all: archive = get_name_of_latest_archive(settings) # create mount point if it does not exist try: mkdir(mount_point) except OSError as e: raise Error(os_error(e)) # run borg borg = settings.run_borg( cmd='mount', args=[settings.destination(archive), mount_point], emborg_opts=options, strip_prefix=include_external_archives, ) out = borg.stdout if out: output(out.rstrip())
def main(): with Inform(notify_if_no_tty=True, version=version) as inform: try: # assure config and log directories exist to_path(CONFIG_DIR).mkdir(parents=True, exist_ok=True) to_path(DATA_DIR).mkdir(parents=True, exist_ok=True) inform.set_logfile(to_path(DATA_DIR, LOG_FILE)) # read command line cmdline = docopt(synopsis, options_first=True, version=version) command = cmdline["<command>"] args = cmdline["<args>"] if cmdline["--quiet"]: inform.quiet = True # find and run command settings = Settings(cmdline) cmd, cmd_name = Command.find(command) cmd.execute(cmd_name, args, settings, cmdline) except KeyboardInterrupt: display("Terminated by user.") except Error as e: e.terminate() except OSError as e: fatal(os_error(e)) done()
def run(cls, command, args, settings, options): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) fast = cmdline['--fast'] # report local information src_dirs = (str(d) for d in settings.src_dirs) output(f' config: {settings.config_name}') output(f' source: {", ".join(src_dirs)}') output(f' destination: {settings.destination()}') output(f' settings directory: {settings.config_dir}') output(f' logile: {settings.logfile}') try: backup_date = arrow.get(settings.date_file.read_text()) output( f' last backed up: {backup_date}, {backup_date.humanize()}' ) except FileNotFoundError as e: narrate(os_error(e)) except arrow.parser.ParserError as e: narrate(e, culprit=settings.date_file) if fast: return # now output the information from borg about the repository borg = settings.run_borg( cmd='info', args=[settings.destination()], emborg_opts=options, strip_prefix=True, ) out = borg.stdout if out: output() output(out.rstrip())
def main(): try: # read config file read_config() # read command line cmdline = docopt( __doc__.format(commands=Command.summarize()), version='avendesora {} ({})'.format(__version__, __released__), options_first=True, ) # start logging logfile = BufferedFile(get_setting('log_file'), True) Inform(logfile=logfile, hanging_indent=False, stream_policy='header', notify_if_no_tty=True) shlib.set_prefs(use_inform=True, log_cmd=True) # run the requested command Command.execute(cmdline['<command>'], cmdline['<args>']) done() except KeyboardInterrupt: output('\nTerminated by user.') terminate() except (PasswordError, Error) as e: e.terminate() except OSError as e: fatal(os_error(e)) done()
def run(self): global ActiveFile ActiveFile = self.path path = self.path self.encrypted = path.suffix in ['.gpg', '.asc'] log('reading.', culprit=path) try: self.code = self.read() # need to save the code for the new command except OSError as err: raise Error(os_error(err)) try: compiled = compile(self.code, str(path), 'exec') except SyntaxError as err: raise Error( err.msg + ':', err.text, (err.offset-1)*' ' + '^', culprit=(err.filename, err.lineno), sep='\n' ) # File "/home/ken/.config/avendesora/config", line 18 # 'g': 'google-chrome %s' # ^ contents = {} exec(compiled, contents) ActiveFile = None return contents
def run(cls, command, args, settings, options): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) mount_point = cmdline['<mount_point>'] if not mount_point: mount_point = settings.value('default_mount_point') if not mount_point: raise Error('must specify directory to use as mount point') mount_point = to_path(mount_point) # run borg try: settings.run_borg( cmd='umount', args=[mount_point], emborg_opts=options, ) try: to_path(mount_point).rmdir() except OSError as e: warn(os_error(e)) except Error as e: if 'busy' in str(e): e.reraise( codicil= f"Try running 'lsof +D {mount_point}' to find culprit.")
def test_archive(): try: result = subprocess.check_output("avendesora help archive".split()) except OSError as err: result = os_error(err) expected = dedent( """ Generates archive of all account information. Usage: avendesora archive avendesora A This command creates an encrypted archive that contains all the information in your accounts files, including the fully generated secrets. You should never need this file, but its presence protects you in case you lose access to Avendesora. To access your secrets without Avendesora, simply decrypt the archive file with GPG. The actual secrets will be hidden, but it easy to retrieve them even without Avendesora. When hidden, the secrets are encoded in base64. You can decode it by running 'base64 -d -' and pasting the encoded secret into the terminal. When you run this command it overwrites the existing archive. If you have accidentally deleted an account or changed a secret, then replacing the archive could cause the last copy of the original information to be lost. To prevent this from occurring it is a good practice to run the 'changed' command before regenerating the archive. It describes all of the changes that have occurred since the last time the archive was generated. You should only regenerate the archive once you have convinced yourself all of the changes are as expected. """ ).strip() assert result == bytes(expected, encoding="ascii")
def test_reveal(): try: result = subprocess.check_output("avendesora help reveal".split()) except OSError as err: result = os_error(err) expected = dedent( """ Reveal concealed text. Transform concealed text to reveal its original form. Usage: avendesora reveal [<text>] avendesora r [<text>] Options: -e <encoding>, --encoding <encoding> Encoding used when revealing information. Though available as an option for convenience, you should not pass the text to be revealed as an argument as it is possible for others to examine the commands you run and their argument list. For any sensitive secret, you should simply run 'avendesora reveal' and then enter the encoded text when prompted. """ ).strip() assert result == bytes(expected, encoding="ascii")
def main(): with Inform(error_status=2, flush=True, version=version) as inform: # read command line cmdline = docopt(expanded_synopsis, options_first=True, version=version) config = cmdline["--config"] command = cmdline["<command>"] args = cmdline["<args>"] if cmdline["--mute"]: inform.mute = True if cmdline["--quiet"]: inform.quiet = True emborg_opts = cull( [ "verbose" if cmdline["--verbose"] else "", "narrate" if cmdline["--narrate"] else "", "dry-run" if cmdline["--dry-run"] else "", "no-log" if cmdline["--no-log"] else "", ] ) if cmdline["--narrate"]: inform.narrate = True try: # find the command cmd, cmd_name = Command.find(command) # execute the command initialization exit_status = cmd.execute_early(cmd_name, args, None, emborg_opts) if exit_status is not None: terminate(exit_status) worst_exit_status = 0 try: while True: with Settings(config, cmd, emborg_opts) as settings: try: exit_status = cmd.execute( cmd_name, args, settings, emborg_opts ) except Error as e: settings.fail(e, cmd=' '.join(sys.argv)) e.terminate() if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status except NoMoreConfigs: pass # execute the command termination exit_status = cmd.execute_late(cmd_name, args, None, emborg_opts) if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status except KeyboardInterrupt: display("Terminated by user.") except Error as e: e.terminate() except OSError as e: fatal(os_error(e)) terminate(worst_exit_status)
def __new__(cls, server, include_file, bypass, trial_run): if server in AuthKeys.known: self = AuthKeys.known[server] if include_file != self.include_file: warn( 'inconsistent remote include file:', fmt('{include_file} != {self.include_file} in {server}.') ) return self self = super(AuthKeys, cls).__new__(cls) AuthKeys.known[server] = self self.server = server self.bypass = bypass self.trial_run = trial_run self.keys = {} self.comment = {} self.restrictions = {} self.include_file = include_file self.include = None # get remote include file if it exists if include_file and not bypass: narrate(fmt(' retrieving remote include file from {server}.')) try: try: run_sftp(self.server, [ fmt('get .ssh/{inc} {inc}.{server}', inc=include_file) ]) self.include = to_path(include_file + '.' + server).read_text() except OSError as err: comment(fmt(' sftp {server}: {include_file} not found.')) except OSError as err: error(os_error(err)) return self
def read_defaults(self): settings = {} try: from appdirs import user_config_dir config_file = to_path(user_config_dir('vdiff'), 'config') try: code = config_file.read_text() try: compiled = compile(code, str(config_file), 'exec') exec(compiled, settings) except Exception as e: error(e, culprit=config_file) except FileNotFoundError: pass except OSError as e: warn(os_error(e)) if self.useGUI is not None: settings['gui'] = self.useGUI except ImportError: pass if settings.get('gui', DEFAULT_GUI): if 'DISPLAY' not in os.environ: warn('$DISPLAY not set, ignoring request for gvim.') else: self.cmd = settings.get('gvimdiff', DEFAULT_GVIM) return self.cmd = settings.get('vimdiff', DEFAULT_VIM)
def test_new(): try: result = subprocess.check_output("avendesora help new".split()) except OSError as err: result = os_error(err) expected = dedent( """ Create new accounts file. Usage: avendesora new [--gpg-id <id>]... <name> avendesora N [--gpg-id <id>]... <name> Options: -g <id>, --gpg-id <id> Use this ID when creating any missing encrypted files. Creates a new accounts file. Accounts that share the same file share the same master password by default and, if the file is encrypted, can be decrypted by the same recipients. Generally you would create a new accounts file for each person or group with which you wish to share accounts. You would also use separate files for passwords with different security domains. For example, a high-value passwords might be placed in an encrypted file that would only be placed highly on protected computers. Conversely, low-value passwords might be contained in perhaps an unencrypted file that is found on many computers. Add a '.gpg' extension to <name> to encrypt the file. """ ).strip() assert result == bytes(expected, encoding="ascii")
def create(self, contents, gpg_ids=None): path = self.path try: # check to see if file already exists if path.exists(): # file creation (init) requested, but file already exists # don't overwrite the file, instead read it so the information # can be used to create any remaining files. display("%s: already exists." % path) return # create the file display('%s: creating.' % path) if path.suffix in ['.gpg', '.asc']: narrate('encrypting.', culprit=path) # encrypt it if not gpg_ids: raise PasswordError('gpg_ids missing.') self.save(contents, gpg_ids) else: narrate('not encrypting.', culprit=path) # file is not encrypted with path.open('wb') as f: f.write(contents.encode(get_setting('encoding'))) except OSError as e: raise PasswordError(os_error(e))
def run(cls, command, args, settings, options): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) mount_point = cmdline['<mount_point>'] archive = cmdline['--archive'] date = cmdline['--date'] # get the desired archive if date and not archive: archive = get_nearest_archive(settings, date) if not archive: raise Error('archive not available.', culprit=date) # create mount point if it does not exist try: mkdir(mount_point) except OSError as e: raise Error(os_error(e)) # run borg borg = settings.run_borg( cmd='mount', args=[settings.destination(archive), mount_point], emborg_opts=options, ) out = borg.stdout if out: output(out.rstrip())
def test_summary(): try: result = run('avendesora values -s mb') except OSError as err: result = os_error(err) expected = dedent("""\ names: mybank, mb accounts: checking: reveal with: avendesora value mybank accounts.checking savings: reveal with: avendesora value mybank accounts.savings creditcard: reveal with: avendesora value mybank accounts.creditcard birthdate: 1981-10-01 checking: {accounts.checking} comment: This is a multiline comment. It spans more than one line. customer service: 1-866-229-6633 email: [email protected] passcode: reveal with: avendesora value mybank passcode pin: reveal with: avendesora value mybank pin questions: 0: What city were you born in?, reveal with: avendesora value mybank questions.0 1: What street did you grow up on?, reveal with: avendesora value mybank questions.1 2: What was your childhood nickname?, reveal with: avendesora value mybank questions.2 urls: https://mb.com username: pizzaman verbal: reveal with: avendesora value mybank verbal """) assert result.decode('utf-8') == expected
def run(cls, command, args, settings, options): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) mount_point = cmdline["<mount_point>"] if mount_point: mount_point = settings.to_path(mount_point, resolve=False) else: mount_point = settings.as_path("default_mount_point") if not mount_point: raise Error("must specify directory to use as mount point") # run borg try: settings.run_borg( cmd="umount", args=[mount_point], emborg_opts=options, ) try: mount_point.rmdir() except OSError as e: warn(os_error(e)) except Error as e: if "busy" in str(e): e.reraise( codicil= f"Try running 'lsof +D {mount_point!s}' to find culprit.")
def main(): with Inform() as inform: # read command line cmdline = docopt( __doc__.format(commands=Command.summarize()), options_first=True ) config = cmdline['--config'] command = cmdline['<command>'] args = cmdline['<args>'] options = cull([ 'verbose' if cmdline['--verbose'] else '', 'narrate' if cmdline['--narrate'] else '', 'trial-run' if cmdline['--trial-run'] else '', ]) if cmdline['--narrate']: inform.narrate = True try: cmd, name = Command.find(command) with Settings(config, cmd.REQUIRES_EXCLUSIVITY) as settings: cmd.execute(name, args, settings, options) except KeyboardInterrupt: display('Terminated by user.') except Error as err: err.terminate() except OSError as err: fatal(os_error(err)) terminate()
def run(self): self.ActivePythonFile = self.path path = self.path narrate("reading:", path) try: self.code = self.read() # need to save the code for the new command except OSError as err: raise Error(os_error(err)) try: compiled = compile(self.code, str(path), "exec") except SyntaxError as err: culprit = (err.filename, err.lineno) if err.text is None or err.offset is None: raise Error(full_stop(err.msg), culprit=culprit) else: raise Error( err.msg + ":", err.text.rstrip(), (err.offset - 1) * " " + "^", culprit=culprit, sep="\n", ) contents = {} try: exec(compiled, contents) except Exception as err: from .utilities import error_source raise Error(full_stop(err), culprit=error_source()) self.ActivePythonFile = None # strip out keys that start with '__' and return them return {k: v for k, v in contents.items() if not k.startswith("__")}
def publish_private_key(self): keyname = self.keyname data = self.data clients = self.data.get('clients', []) prov = '.provisional' if self.trial_run else '' # copy key pair to remote client for client in sorted(clients): if self.update and client not in self.update: continue if client in self.skip: continue narrate(' publishing key pair to', client) client_data = clients[client] # delete any pre-existing provisional files # the goal here is to leave a clean directory when not trial-run try: run_sftp(client, [ fmt('rm .ssh/{keyname}.provisional'), fmt('rm .ssh/{keyname}.pub.provisional'), ]) except OSError as err: pass # now upload the new files try: run_sftp(client, [ fmt('put -p {keyname} .ssh/{keyname}{prov}'), fmt('put -p {keyname}.pub .ssh/{keyname}.pub{prov}'), ]) except OSError as err: error(os_error(err))
def test_changed(): try: result = subprocess.check_output("avendesora help changed".split()) except OSError as err: result = os_error(err) expected = dedent( """ Identify any changes that have occurred since the archive was created. Usage: avendesora changed avendesora C When you run the 'archive' command it overwrites the existing archive. If you have accidentally deleted an account or changed a secret, then replacing the archive could cause the last copy of the original information to be lost. To prevent this from occurring it is a good practice to run the 'changed' command before regenerating the archive. It describes all of the changes that have occurred since the last time the archive was generated. You should only regenerate the archive once you have convinced yourself all of the changes are as expected. """ ).strip() assert result == bytes(expected, encoding="ascii")
def read_manifests(self): # {{{2 if self.name_index: return cache_dir = get_setting('cache_dir') manifests_path = cache_dir / MANIFESTS_FILENAME try: encrypted = manifests_path.read_bytes() user_key = get_setting('user_key') if not user_key: raise Error('no user key.') key = base64.urlsafe_b64encode(sha256(user_key.encode('ascii')).digest()) fernet = Fernet(key) contents = fernet.decrypt(encrypted) try: cache = pickle.loads(contents, **PICKLE_ARGS) self.name_manifests = cache['names'] self.url_manifests = cache['urls'] self.title_manifests = cache['titles'] # build the name_index by inverting the name_manifests self.name_index = { h:n for n,l in self.name_manifests.items() for h in l } except (ValueError, pickle.UnpicklingError) as e: warn('garbled manifest.', culprit=manifests_path, codicil=str(e)) manifests_path.unlink() assert isinstance(self.name_index, dict) except OSErrors as e: comment(os_error(e)) except Exception as e: comment(e)
def start(self, stdin=None): """ Start the command, will not wait for it to terminate. If stdin is given, it should be a string. Otherwise, no connection is made to stdin of the command. """ self.stdin = None import subprocess if is_str(self.cmd): cmd = self.cmd if self.use_shell else split_cmd(self.cmd) else: cmd = self.cmd if _use_log(self.log): from inform import log log("running:", render_command(cmd, option_args=self.option_args)) if self.save_stdout or self.save_stderr: try: DEVNULL = subprocess.DEVNULL except AttributeError: DEVNULL = open(os.devnull, "wb") assert self.merge_stderr_into_stdout is False, "M not supported, use E" streams = {} if stdin is not None: streams["stdin"] = subprocess.PIPE if self.save_stdout: streams["stdout"] = DEVNULL if self.save_stderr: streams["stderr"] = DEVNULL # run the command try: process = subprocess.Popen(cmd, shell=self.use_shell, env=self.env, **streams) except OSError as e: if PREFERENCES["use_inform"]: from inform import Error, os_error raise Error(msg=os_error(e), cmd=render_command(self.cmd), template="{msg}") else: raise self.running = True # store needed information and wait for termination if desired self.pid = process.pid self.process = process # write to stdin if stdin is not None: process.stdin.write(stdin.encode(self.encoding)) process.stdin.close()
def test_alertscc_discovery(): try: result = run( 'avendesora value --title https://alertscc.bbcportal.com --stdout alertscc' ) except OSError as err: result = os_error(err) assert result == b'email is [email protected], password is R7ibHyPjWtG2\n'
def test_scc_browse(): try: result = run('avendesora browse --list scc') except OSError as err: result = os_error(err) assert sorted(result) == sorted( b' validation: https://alertscc.bbcportal.com/Validation\n' b' login: https://alertscc.bbcportal.com\n')
def run(self, stdin=None): """ Run the command, will wait for it to terminate. If stdin is given, it should be a string. Otherwise, no connection is made to stdin of the command. Returns exit status if wait_for_termination is True. If wait_for_termination is False, you must call wait(), otherwise stdin is not be applied. If you don't want to wait, call start() instead. """ self.stdin = stdin import subprocess if is_str(self.cmd): cmd = self.cmd if self.use_shell else split_cmd(self.cmd) else: # cannot use to_str() because it can change some arguments when not intended. # this is particularly problematic the duplicity arguments in embalm cmd = [str(c) for c in self.cmd] if _use_log(self.log): from inform import log log("running:", render_command(cmd, option_args=self.option_args)) # indicate streams to intercept streams = {} if stdin is not None: streams["stdin"] = subprocess.PIPE if self.save_stdout: streams["stdout"] = subprocess.PIPE if self.save_stderr: streams["stderr"] = subprocess.PIPE if self.merge_stderr_into_stdout: streams["stderr"] = subprocess.STDOUT # run the command try: process = subprocess.Popen(cmd, shell=self.use_shell, env=self.env, **streams) except OSError as e: if PREFERENCES["use_inform"]: from inform import Error, os_error raise Error(msg=os_error(e), cmd=render_command(self.cmd), template="{msg}") else: raise self.running = True # store needed information and wait for termination if desired self.pid = process.pid self.process = process if self.wait_for_termination: return self.wait()
def run(cls, command, args, settings, options): # read command line docopt(cls.USAGE, argv=[command] + args) try: prev_log = settings.prev_logfile.read_text() output(prev_log) except FileNotFoundError as e: narrate(os_error(e))
def now_playing(self): if self.now_playing_path: out = [each for each in [self.artist, self.title] if each] try: self.now_playing_path.write_text(' - '.join(out)) except OSError as e: if not self.warned: warn(os_error(e)) self.warned = True
def clean(host): try: narrate(fmt('Cleaning {host}.')) run_sftp(host, ['rm .ssh/*.provisional']) except OSError as err: if 'no such file or directory' in str(err).lower(): comment(os_error(err)) else: error('cannot connect.', culprit=host)
def main(): with Inform(error_status=2, flush=True, version=version) as inform: # read command line cmdline = docopt(expanded_synopsis, options_first=True, version=version) config = cmdline['--config'] command = cmdline['<command>'] args = cmdline['<args>'] if cmdline['--mute']: inform.mute = True if cmdline['--quiet']: inform.quiet = True options = cull([ 'verbose' if cmdline['--verbose'] else '', 'narrate' if cmdline['--narrate'] else '', 'trial-run' if cmdline['--trial-run'] else '', 'no-log' if cmdline['--no-log'] else '', ]) if cmdline['--narrate']: inform.narrate = True try: # find the command cmd, cmd_name = Command.find(command) # execute the command initialization exit_status = cmd.execute_early(cmd_name, args, None, options) if exit_status is not None: terminate(exit_status) worst_exit_status = 0 try: while True: with Settings(config, cmd, options) as settings: try: exit_status = cmd.execute(cmd_name, args, settings, options) except Error as e: settings.fail(e) e.terminate() if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status except NoMoreConfigs: pass # execute the command termination exit_status = cmd.execute_late(cmd_name, args, None, options) if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status except KeyboardInterrupt: display('Terminated by user.') except Error as e: e.terminate() except OSError as e: fatal(os_error(e)) terminate(worst_exit_status)
def test_find(): try: result = subprocess.check_output('avendesora find bank'.split()) except OSError as err: result = os_error(err) expected = dedent("""\ bank: mybank (mb) """) assert result == bytes(expected, encoding='ascii')
def test_conceal(): try: result = subprocess.check_output('avendesora conceal 12345678'.split()) except OSError as err: result = os_error(err) assert result == dedent(''' Hidden( "MTIzNDU2Nzg=" ) ''').lstrip().encode('ascii')
def test_find(): try: result = run('avendesora find bank') except OSError as err: result = os_error(err) expected = dedent("""\ bank: mybank (mb) """) assert result.decode('utf-8') == expected
def run(cls, command, args): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) archive_file = get_setting('archive_file') # first, save existing archive if it exists try: previous_archive = get_setting('previous_archive_file') if previous_archive and archive_file.is_file(): rm(previous_archive) mv(archive_file, previous_archive) except OSError as err: raise Error(os_error(err)) # run the generator generator = PasswordGenerator() # get dictionary that fully describes the contents of each account entries = [] for account in generator.all_accounts: entry = account.archive() if entry: entries.append(indent('%r: %s,' % ( account.get_name(), to_python(entry) ), ' ')) # build file contents from .preferences import ARCHIVE_FILE_CONTENTS import arrow contents = ARCHIVE_FILE_CONTENTS.format( encoding = get_setting('encoding'), date=str(arrow.now()), accounts = '\n\n'.join(entries) ) archive = GnuPG(archive_file) if not archive.will_encrypt(): warn('archive file is not encrypted.', culprit=archive_file) try: archive.save(contents) chmod(0o600, archive_file) except OSError as err: raise Error(os_error(err), culprit=archive_file)
def test_search(): try: result = subprocess.check_output('avendesora search pizza'.split()) except OSError as err: result = os_error(err) expected = dedent("""\ pizza: alertscc (scc) mybank (mb) """) assert result == bytes(expected, encoding='ascii')
def cleanup(self): if self.vim: self.vim.kill() for each in cull([self.file1, self.file2, self.file3, self.file4]): path = to_path(each) dn = path.parent fn = path.name swpfile = to_path(dn, '.' + fn + '.swp') try: rm(swpfile) except OSError as e: error(os_error(e))
def test_search(): try: result = run('avendesora search pizza') except OSError as err: result = os_error(err) expected = dedent("""\ pizza: alertscc (scc) margaritaville mybank (mb) """) assert result.decode('utf-8') == expected
def test_stealth(): try: avendesora = pexpect.spawn('avendesora', 'value -s xkcd'.split()) avendesora.expect('account name: ', timeout=4) avendesora.sendline('an-account-name') avendesora.expect(pexpect.EOF) avendesora.close() result = avendesora.before.decode('utf-8') except (pexpect.EOF, pexpect.TIMEOUT): result = avendesora.before.decode('utf8') except OSError as err: result = os_error(err) assert result.strip() == 'underdog crossword apron whinny'
def render(self): try: contents = self.contents.render() except AttributeError: contents = self.contents try: path = to_path(self.path) path.write_text(contents) path.chmod(self.mode) except OSError as e: raise PasswordError(os_error(e)) return 'Contents written to {}.'.format(str(path))
def differ(self): try: with open(self.file1) as f: lcontents = f.read() with open(self.file2) as f: rcontents = f.read() return lcontents != rcontents except OSError as e: raise Error(os_error(e)) except: # Any other errors, just assume files differ and move on. # Unicode errors can occur on old versions of CentOS. return True
def test_alertscc_seed(): try: avendesora = pexpect.spawn('avendesora', 'value -S -s alertscc password'.split()) avendesora.expect('seed for alertscc: ', timeout=4) avendesora.sendline('frozen-chaos') avendesora.expect(pexpect.EOF) avendesora.close() result = avendesora.before.decode('utf-8') except (pexpect.EOF, pexpect.TIMEOUT): result = avendesora.before.decode('utf8') except OSError as err: result = os_error(err) assert result.strip() == 'tRT7vXLeZrbz'
def test_version(): try: result = subprocess.check_output("avendesora help version".split()) except OSError as err: result = os_error(err) expected = dedent( """ Display Avendesora version. Usage: avendesora version """ ).strip() assert result == bytes(expected, encoding="ascii")
def publish(self): narrate('publishing authorized_keys to', self.server) prov = '.provisional' if self.trial_run else '' entries = [ fmt("# This file was generated by sshdeploy on {date}.") ] if self.include: entries += [ '\n'.join([ fmt('# Contents of {self.include_file}:'), self.include ]) ] for name in sorted(self.keys.keys()): key = self.keys[name] comment = self.comment[name] comment = [comment] if is_str(comment) else comment restrictions = self.restrictions[name] if not is_str(restrictions): restrictions = ','.join(restrictions) restricted_key = ' '.join(cull([restrictions, key])) entries.append('\n'.join(comment + [restricted_key])) # delete any pre-existing provisional files # the goal here is to leave a clean directory when not trial-run try: run_sftp(self.server, [ fmt('rm .ssh/authorized_keys.provisional') ]) except OSError as err: pass # now upload the new authorized_keys file try: authkey = to_path('authorized_keys.%s' % self.server) with authkey.open('w') as f: f.write('\n\n'.join(entries) + '\n') authkey.chmod(0o600) if self.bypass: warn( 'You must manually upload', fmt('<keydir>/authorized_keys.{self.server}.'), culprit=self.server ) else: run_sftp(self.server, [ fmt('put -p {authkey} .ssh/authorized_keys{prov}') ]) except OSError as err: error(os_error(err))
def get_psf_filename(psf_file): if not psf_file: try: with open(saved_psf_file_filename) as f: psf_file = f.read().strip() display('Using PSF file:', psf_file) except OSError: fatal('missing PSF file name.') try: with open(saved_psf_file_filename, 'w') as f: f.write(psf_file) except OSError as e: warn(os_error(e)) return psf_file
def main(): """ Construct, encrypt, and publish backup keys. As the primary entry point for the end user, this function is also responsible for integrating information from command-line arguments, configuration files, and `setuptools` plugins. """ set_shlib_prefs(use_inform=True, log_cmd=True) args = docopt.docopt(__doc__) if args['--verbose']: set_output_prefs(verbose=True, narrate=True) elif args['--quiet']: set_output_prefs(quiet=True) try: config_path, config = load_config() try: if args['plugins']: list_plugins(config) sys.exit() # Get the passcode before building the archive, so if something # goes wrong with the passcode, we don't need to worry about # cleaning up the unencrypted archive. passcode = query_passcode(config) batch = args['--yes'] or args['--quiet'] archive = build_archive(config, not batch) encrypt_archive(config, archive, passcode) publish_archive(config, archive) except ConfigError as e: e.reraise(culprit=config_path) finally: if 'archive' in locals(): delete_archive(config, archive) except KeyboardInterrupt: print() except Error as e: if args['--verbose']: raise else: e.report() except OSError as e: fatal(os_error(e)) terminate()
def test_help(): try: result = subprocess.check_output("avendesora help help".split()) except OSError as err: result = os_error(err) expected = dedent( """ Give information about commands or other topics. Usage: avendesora help [<topic>] avendesora h [<topic>] """ ).strip() assert result == bytes(expected, encoding="ascii")
def test_value(): try: result = subprocess.check_output("avendesora help value".split()) except OSError as err: result = os_error(err) expected = dedent( """ Show an account value. Produce an account value. If the value is secret, it is produced only temporarily unless --stdout is specified. Usage: avendesora value [options] [--stdout | --clipboard] [<account> [<field>]] avendesora val [options] [--stdout | --clipboard] [<account> [<field>]] avendesora v [options] [--stdout | --clipboard] [<account> [<field>]] Options: -c, --clipboard Write output to clipboard rather than stdout. -s, --stdout Write output to the standard output without any annotation or protections. -S, --seed Interactively request additional seed for generated secrets. -v, --verbose Add additional information to log file to help identify issues in account discovery. -t <title>, --title <title> Use account discovery on this title. You would request a scalar value by specifying its name after the account. For example: avendesora value pin If is a composite value, you should also specify a key that indicates which of the composite values you want. For example, if the 'accounts' field is a dictionary, you would specify accounts.checking or accounts[checking] to get information on your checking account. If the value is an array, you would give the index of the desired value. For example, questions.0 or questions[0]. If no value is requested, the passcode value is returned (this can be changed by specifying 'default_field' in the account or in the config file). If you only specify a number, then the name is assumed to be 'questions', as in the list of security questions (this can be changed by specifying the desired name as the 'default_vector_field' in the account or the config file). """ ).strip() assert result == bytes(expected, encoding="ascii")
def __init__(self, path): # find the dictionary, initially look in the settings directory if not path.exists(): # if not there look in install directory from pkg_resources import resource_filename path = to_path(resource_filename(__name__, 'words')) # open the dictionary try: contents= path.read_text() except OSError as err: error(os_error(err)) contents = '' self.hash = hashlib.md5(contents.encode('utf-8')).hexdigest() self.words = contents.split()
def create(self, contents): path = self.path try: if path.exists(): # file creation (init) requested, but file already exists # don't overwrite the file, instead read it so the information # can be used to create any remaining files. display("%s: already exists." % path) return # create the file display("%s: creating." % path) # file is not encrypted with path.open("wb") as f: f.write(contents.encode("utf-8")) except OSError as err: raise Error(os_error(err))
def test_abraxas(): try: result = subprocess.check_output("avendesora help abraxas".split()) except OSError as err: result = os_error(err) expected = dedent( """ Avendesora generalizes and replaces Abraxas, its predecessor. To transition from Abraxas to Avendesora, you will first need to upgrade Abraxas to version 1.8 or higher (use 'abraxas -v' to determine version). Then run: abraxas --export It will create a collection of Avendesora accounts files in ~/.config/abraxas/avendesora. You need to manually add these files to your list of accounts files in Avendesora. Say one such file in created: ~/.config/abraxas/avendesora/accounts.gpg. This could be added to Avendesora as follows: 1. create a symbolic link from ~/.config/avendesora/abraxas_accounts.gpg to ~/.config/abraxas/avendesora/accounts.gpg: cd ~/.config/avendesora ln -s ../abraxas/avendesora/accounts.gpg abraxas_accounts.gpg 2. add abraxas_accounts.gpg to account_files list in .accounts_files. Now all of the Abraxas accounts contained in abraxas_accounts.gpg should be available though Avendesora and the various features of the account should operate as expected. However, secrets in accounts exported by Abraxas are no longer generated secrets. Instead, the actual secrets are placed in a hidden form in the exported accounts files. If you would like to enhance the imported accounts to take advantage of the new features of Avendesora, it is recommended that you do not manually modify the imported files. Instead, copy the account information to one of your own account files before modifying it. To avoid conflict, you must then delete the account from the imported file. To do so, create ~/.config/abraxas/do-not-export if it does not exist, then add the account name to this file, and reexport your accounts from Abraxas. """ ).strip() assert result == bytes(expected, encoding="utf8")
def _validate_components(self): from pkg_resources import resource_filename # check permissions on the settings directory path = get_setting('settings_dir') mask = get_setting('config_dir_mask') try: permissions = getmod(path) except FileNotFoundError: raise PasswordError('missing, must run initialize.', culprit=path) violation = permissions & mask if violation: recommended = permissions & ~mask & 0o777 warn("directory permissions are too loose.", culprit=path) codicil("Recommend running: chmod {:o} {}".format( recommended, path)) # Check that files that are critical to the integrity of the generated # secrets have not changed for path, kind in [ (to_path(resource_filename(__name__, 'secrets.py')), 'secrets_hash'), (to_path(resource_filename(__name__, 'charsets.py')), 'charsets_hash'), ('default', 'dict_hash'), ('mnemonic', 'mnemonic_hash'), ]: try: contents = path.read_text() except AttributeError: contents = '\n'.join(Dictionary(path).get_words()) except OSErrors as e: raise PasswordError(os_error(e)) md5 = hashlib.md5(contents.encode('utf-8')).hexdigest() # Check that file has not changed. if md5 != get_setting(kind): warn("file contents have changed.", culprit=path) lines = wrap( dedent("""\ This could result in passwords that are inconsistent with those created in the past. Use 'avendesora changed' to assure that nothing has changed. Then, to suppress this message, change {hashes} to contain: """.format(hashes=get_setting('hashes_file')))) lines.append(" {kind} = '{md5}'".format(kind=kind, md5=md5)) codicil(*lines, sep='\n')
def test_search(): try: result = subprocess.check_output("avendesora help search".split()) except OSError as err: result = os_error(err) expected = dedent( """ Search accounts. Search for accounts whose values contain the search text. Usage: avendesora search <text> avendesora s <text> """ ).strip() assert result == bytes(expected, encoding="ascii")
def test_find(): try: result = subprocess.check_output("avendesora help find".split()) except OSError as err: result = os_error(err) expected = dedent( """ Find an account. Find accounts whose name contains the search text. Usage: avendesora find <text> avendesora f <text> """ ).strip() assert result == bytes(expected, encoding="ascii")
def test_conceal(): try: result = subprocess.check_output("avendesora help conceal".split()) except OSError as err: result = os_error(err) expected = dedent( """ Conceal text by encoding it. Usage: avendesora [options] conceal [<text>] avendesora [options] c [<text>] Options: -e <encoding>, --encoding <encoding> Encoding used when concealing information. -s, --symmetric Encrypt with a passphrase rather than using your GPG key (only appropriate for gpg encodings). Possible encodings include (default encoding is base64): base64: This encoding obscures but does not encrypt the text. It can protect text from observers that get a quick glance of the encoded text, but if they are able to capture it they can easily decode it. gpg: This encoding fully encrypts/decrypts the text with GPG key. By default your GPG key is used, but you can specify symmetric encryption, in which case a passphrase is used. scrypt: This encoding fully encrypts the text with your user key. Only you can decrypt it, secrets encoded with scrypt cannot be shared. Though available as an option for convenience, you should not pass the text to be hidden as an argument as it is possible for others to examine the commands you run and their argument list. For any sensitive secret, you should simply run 'avendesora conceal' and then enter the secret text when prompted. """ ).strip() assert result == bytes(expected, encoding="ascii")
def test_stealth(): try: result = subprocess.check_output("avendesora help stealth".split()) except OSError as err: result = os_error(err) expected = dedent( """ Normally Avendesora uses information from an account that is contained in an account file to generate the secrets for that account. In some cases, the presence of the account itself, even though it is contained within an encrypted file can be problematic. The mere presence of an encrypted file may result in you being compelled to open it. For the most damaging secrets, it is best if there is no evidence that the secret exists at all. This is the purpose of stealth accounts. (Misdirection is an alternative to stealth accounts; see 'avendesora help misdirection'). Generally one uses the predefined stealth accounts, which all have names that are descriptive of the form of the secret they generate, for example Word6 generates a 6-word pass phrase. The predefined accounts are kept in ~/.config/avendesora/stealth_accounts. Stealth accounts are subclasses of the StealthAccount class. These accounts differ from normal accounts in that they do not contribute the account name to the secrets generators for use as a seed. Instead, the user is requested to provide the account name every time the secret is generated. The secret depends strongly on this account name, so it is essential you give precisely the same name each time. The term 'account name' is being use here, but you can enter any text you like. Best to make this text very difficult to guess if you are concerned about being compelled to disclose your GPG keys. The secret generator will combine the account name with the master password before generating the secret. This allows you to use simple predictable account names and still get an unpredictable secret. The master password used is taken from master_password in the file that contains the stealth account if it exists, or the users key if it does not. By default the stealth accounts file does not contain a master password, which makes it difficult to share stealth accounts. You can create additional stealth account files that do contain master passwords that you can share with your associates. """ ).strip() assert result == bytes(expected, encoding="utf8")
def test_values(): try: result = subprocess.check_output("avendesora help values".split()) except OSError as err: result = os_error(err) expected = dedent( """ Display all account values. Show all account values. Usage: avendesora values <account> avendesora vals <account> avendesora V <account> """ ).strip() assert result == bytes(expected, encoding="ascii")
def test_misdirection(): try: result = subprocess.check_output("avendesora help misdirection".split()) except OSError as err: result = os_error(err) expected = dedent( """ One way to avoid being compelled to disclose a secret is to disavow any knowledge of the secret. However, the presence of an account in Avendesora that pertains to that secret undercuts this argument. This is the purpose of stealth accounts. They allow you to generate secrets for accounts for which Avendesora has no stored information. In this case Avendesora ask you for the minimal amount of information that it needs to generate the secret. However in some cases, the amount of information that must be retained is simply too much to keep in your head. In that case another approach, referred to as secret misdirection, can be used. With secret misdirection, you do not disavow any knowledge of the secret, instead you say your knowledge is out of date. So you would say something like "I changed the password and then forgot it", or "The account is closed". To support this ruse, you must use the --seed (or -S) option to 'avendsora value' when generating your secret (secrets misdirection only works with generated passwords, not stored passwords). This causes Avendesora to ask you for an additional seed at the time you request the secret. If you do not use --seed or you do and give the wrong seed, you will get a different value for your secret. In effect, using --seed when generating the original value of the secret cause Avendesora to generate the wrong secret by default, allowing you to say "See, I told you it would not work". But when you want it to work, you just interactively provide the right additional seed. You would typically only use misdirection for secrets you are worried about being compelled to disclose. So it behooves you to use an unpredictable additional seed for these secrets to reduce the chance someone could guess it. Be aware that when you employ misdirection on a secret, the value of the secret stored in in the archive will not be the true value, it will instead be the misdirected value. """ ).strip() assert result == bytes(expected, encoding="utf8")
def get_argv(): argv = sys.argv[1:] if argv: # save the command line arguments for next time try: with open(saved_arguments_filename, 'w') as f: args = [a for a in argv if a not in ['-c', '--no-cache']] f.write('\n'.join(args)) except OSError as e: warn(os_error(e)) else: # command line arguments not give, reuse previous ones try: with open(saved_arguments_filename) as f: argv = f.read().split('\n') display('Using command:', ' '.join(argv)) except OSError: done() return argv
def test_edit(): try: result = subprocess.check_output("avendesora help edit".split()) except OSError as err: result = os_error(err) expected = dedent( """ Edit an account. Usage: avendesora edit <account> avendesora e <account> Opens an existing account in your editor. You can specify the editor by changing the 'edit_account' setting in the config file (~/.config/avendesora/config). """ ).strip() assert result == bytes(expected, encoding="ascii")
def run(cls, command, args, settings, options): # read command line cmdline = docopt(cls.USAGE, argv=[command] + args) mount_point = cmdline['<mount_point>'] # run borg try: settings.run_borg( cmd='umount', args=[mount_point], emborg_opts=options, ) try: to_path(mount_point).rmdir() except OSError as e: warn(os_error(e)) except Error as e: if 'busy' in str(e): e.reraise( codicil= f"Try running 'lsof +D {mount_point}' to find culprit.")