pax_global_header00006660000000000000000000000064146045433350014521gustar00rootroot0000000000000052 comment=430fa86c934bf2f96f6a630d4a54ee52c01d98a5 impass-0.13.1/000077500000000000000000000000001460454333500130775ustar00rootroot00000000000000impass-0.13.1/.gitignore000066400000000000000000000000301460454333500150600ustar00rootroot00000000000000*~ *.pyc build impass.1 impass-0.13.1/COPYING000066400000000000000000000012141460454333500141300ustar00rootroot00000000000000impass is free software. You can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program, (in the COPYING-GPL-3 file in this directory). If not, see https://www.gnu.org/licenses/ impass-0.13.1/Makefile000077500000000000000000000014771460454333500145530ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- VERSION:=$(shell git describe --tags | sed -e s/_/~/ -e s/-/+/ -e s/-/~/ -e 's|.*/||') .PHONY: all all: impass.1 .PHONY: test test: ./test/impass-test $(TEST_OPTS) rm -f test/gnupg/S.gpg-agent impass.1: impass PYTHONPATH=. python3 -m impass help \ | txt2man -t impass -r 'impass $(VERSION)' -s 1 \ > impass.1 version: echo "__version__ = '$(VERSION)'" >impass/version.py .PHONY: clean clean: rm -f impass.1 .PHONY: debian-snapshot debian-snapshot: rm -rf build/deb mkdir -p build/deb/debian git archive HEAD | tar -x -C build/deb/ git archive --format=tar debian/master:debian | tar -x -C build/deb/debian/ cd build/deb; dch -b -v $(VERSION) -D UNRELEASED 'test build, not for upload' cd build/deb; echo '3.0 (native)' > debian/source/format cd build/deb; debuild -us -uc impass-0.13.1/README.md000066400000000000000000000053071460454333500143630ustar00rootroot00000000000000impass - simple and secure password management system ====================================================== impass is a secure password manager that relies on your OpenPGP key for security and is designed to integrate in a minimal fashion into any X11 window manager. It also integrates into sway for a wayland session. Passwords and context strings are stored in a single OpenPGP-encrypted and signed file (meaning entry contexts are not exposed to the filesystem). Along with a simple command-line interface, there is a streamlined GUI meant for X11 window manager integration or sway when using Wayland. When invoked, the GUI produces a prompt to search stored contexts. New entries can also easily be created. Passwords are securely retrieved without displaying on the screen. Multiple retrieval methods are available, including auto-typing them directly into an X11 window (default), or inserting them into the X11 clipboard. impass was previously known as "assword". Contact ======= impass was written by: Jameson Graef Rollins Daniel Kahn Gillmor impass has a mailing list: assword@lists.mayfirst.org https://lists.mayfirst.org/mailman/listinfo/assword We also hang out on IRC: channel: #assword server: irc.oftc.net Getting impass ============== Source ------ Clone the repo: $ git clone https://salsa.debian.org/debian/impass.git Dependencies : * python3 * python3-gpg - Python bindings for the GPGME library * python3-setuptools - Python packaging and distribution utilities Recommends (for graphical UI): * python3-gi - Python bindings for GObject Introspection * gir1.2-gtk-3.0 - GObject Introspection for GTK 3+ (GUI toolkit) Recommends (for curses UI): * python3-xdo - Support for simulating X11 input (libxdo bindings) * xclip - Support for accessing X11 clipboard Recommends (for sway integration): * i3ipc - talk to sway * wtype - emulate keyboard input For sway integration, including the contents of the file swayconfig in ~/.config/sway/config is advised. Debian ------ impass is available in Debian: https://packages.qa.debian.org/impass Debian/Ubuntu snapshot packages can also be easily made from the git source. You can build the package from any branch but it requires an up-to-date local branch of origin/debian, e.g.: $ git branch debian/unstable origin/debian/unstable Then: $ sudo apt install build-essential devscripts pkg-config python3-all-dev python3-setuptools debhelper dpkg-dev fakeroot txt2man $ make debian-snapshot $ sudo apt install build/impass_*.deb Using impass ============ See the included impass(1) man page or built-in help string for detailed usage. impass-0.13.1/TODO000066400000000000000000000106051460454333500135710ustar00rootroot00000000000000 * ENHANCEMENT: advanced create gui. allows editing context, password and potentially expiration. password edit should provide statistics about password (number of characters/numbers/special characters/etc.). ctrl+e could be shortcut key. * ENHANCEMENT: entry edit gui. allows editing context, password and potentially expiration. password edit should provide statistics about password (number of characters/numbers/special characters/etc.). ctrl+e could be shortcut key. * ENHANCEMENT: add password expiration. User enters expiration in "natural language", which is resolved to future date expiration. Expiration date and specified delta are stored, so delta can be used to suggest expirations in the future. * ENHANCEMENT: store context history in separate encrypted file. gui should allow navigating history (via arrows and other command keys) when the context field is empty. Break out Database class into generic encrypted json store class the both Database and History classes can inherit from. * BUG: assword crashes when it gets EOF on stdin when prompting or when ASSWORD_PASSWORD=prompt. * ENHANCEMENT: ASSWORD_PASSWORD=stdin should just read the password from the first line of stdin (discarding trailing newlines). not sure how this should interact with the situation where no context is supplied. * ENHANCEMENT: xdo has two input methods -- one based on XSendEvent, and one based on injecting keystrokes into the X session as a whole (the difference is whether a window ID parameter is passed to "type"). We use the latter right now in gui despite its possibility of leakage because some X11 applications ignore XSendEvent input. Provide some hinting mechanism derived from the window in question to conditionally enable XSendEvent instead. * ENHANCEMENT: can we use python clipboard bindings instead of "xclip -i"? * ENHANCEMENT: preseed context in gui. use the target window title and/or pid to pre-seed the search box in "assword gui" (this should be pre-selected text so it is easy to start typing something else) if selected window is known browser, and it's possible to extract url, preseed context search with hostname of url. * ENHANCEMENT: gui create action should modify the database, move the user's focus back into the textentry (highlighting the whole entry text), disable the "Create" button, and *not* explicitly select and terminate. The user can then select in the usual way (pressing enter) or can continue without selecting. * ENHANCEMENT: review gui label texts and make sure they're saying reasonable things in different parts of the workflow. * ENHANCEMENT: ctrl+del from gui when a matching context is present should allow deletion of the indicated password. This should probably prompt for confirmation. * ENHANCEMENT: support multiple DB files. change current DB path to directory. app opens all available db files and presents a unified them in a single entry dict that can be searched/edited/etc. entries should store db source and present to user adjacent to context. Entry edits should go back to the appropriate db file. db files should store name and version in internal db metadata, and file name should include version number. * ENHANCEMENT: multi db syncronization. maybe start with a centralized, SVN-like-without-history system. pull remotes. db edits increment db version number. pushes succeed transparently if remote has same version as last sync. if remote version has been incremented initiate local-side merge. * ENHANCEMENT: associate default _XPASTE mechanisms with some contexts: if you know that certain passwords work with tools that prefer certain _XPASTE mechanisms, that ought to be something assword can figure out. see 526990F5.6050700@guerrier.com and following discussion. * ENHANCEMENT: import scripts from common password stores (e.g. ~/.mozilla/firefox/*.default/signons.sqlite) * ENHANCEMENT: guess about target window to determine default _XPASTE mechanism (e.g. we know iceweasel works with one _XPASTE mechanism, but rxvt works with another one) -- we can guess by looking at the process that controls the window and/or the window title or other things (we might need to expand python-xdo to get these guesses) * ENHANCEMENT: test for various PASSWORD values * ENHANCEMENT: gui error dialog impass-0.13.1/impass.1.additional000066400000000000000000000002771460454333500165720ustar00rootroot00000000000000[Signature validation] During decryption, OpenPGP signatures on the db file are checked for validity. If any of them are found to not be valid, a warning message will be written to stderr. impass-0.13.1/impass/000077500000000000000000000000001460454333500143735ustar00rootroot00000000000000impass-0.13.1/impass/__init__.py000066400000000000000000000002011460454333500164750ustar00rootroot00000000000000from .version import __version__ from .db import Database, DatabaseError __all__ = ["__version__", "Database", "DatabaseError"] impass-0.13.1/impass/__main__.py000077500000000000000000000565011460454333500164770ustar00rootroot00000000000000#!/usr/bin/env python3 import os import io import sys import json import gpg # type: ignore import getpass import argparse import textwrap import subprocess import collections from typing import Optional, NoReturn, List, Callable, Union, Sequence, Any, Dict from .db import Database, DatabaseError, DEFAULT_NEW_PASSWORD_OCTETS from .version import __version__ PROG = "impass" _swaymark = f"🔐{PROG}" ############################################################ IMPASS_DIR = os.path.join(os.path.expanduser("~"), ".impass") ############################################################ def xclip(text: str) -> None: p = subprocess.Popen(" ".join(["xclip", "-i"]), shell=True, stdin=subprocess.PIPE) p.communicate(text.encode("utf-8")) # FIXME: shouldn't we warn the user if xclip fails here? def log(*args: str) -> None: print(*args, file=sys.stderr) ############################################################ # Return codes: # 1 command/load line error # 2 context/password invalid # 5 db doesn't exist # 10 db error # 20 gpg/key error ############################################################ def error(code: int, msg: str = "") -> NoReturn: if msg: log(msg) sys.exit(code) def open_db(keyid: Optional[str] = None, create: bool = False) -> Database: db_path = os.getenv("IMPASS_DB", os.path.join(IMPASS_DIR, "db")) if not create and not os.path.exists(db_path): error( 5, """Impass database does not exist. To add an entry to the database use 'impass add'. See 'impass help' for more information.""", ) try: db = Database(db_path, keyid) except gpg.errors.GPGMEError as e: error(20, "Decryption error: {}".format(e)) except DatabaseError as e: error(10, "Impass database error: {}".format(e.msg)) if db.sigvalid is False: log("WARNING: could not validate OpenPGP signature on db file.") return db def get_keyid() -> str: keyid = os.getenv("IMPASS_KEYID") keyfile = os.getenv("IMPASS_KEYFILE", os.path.join(IMPASS_DIR, "keyid")) if not keyid and os.path.exists(keyfile): with open(keyfile, "r") as f: keyid = f.read().strip() save = False if keyid is None or keyid == "": log("OpenPGP key ID of encryption target not specified.") log("Please provide key ID in IMPASS_KEYID environment variable,") log("or specify key ID now to save in ~/.impass/keyid file.") keyid = input("OpenPGP key ID: ") if keyid == "": keyid = None else: save = True if keyid is None or keyid == "": error(20) try: gpgctx = gpg.Context() gpgctx.get_key(keyid, secret=False) except gpg.errors.GPGMEError as e: log("GPGME error for key ID {}:".format(keyid)) log(" {}".format(e)) error(20) if save: if not os.path.isdir(os.path.dirname(keyfile)): os.mkdir(os.path.dirname(keyfile)) with open(keyfile, "w") as f: f.write(keyid) return keyid class Completer: def __init__(self, completions: Optional[List[str]] = None): if completions is None: self.completions: List[str] = [] else: self.completions = completions def completer(self, text: str, index: int) -> Optional[str]: matching = [c for c in self.completions if c.startswith(text)] try: return matching[index] except IndexError: return None def input_complete( prompt: str, completions: Optional[List[str]] = None, default: Optional[str] = None ) -> str: try: # lifted from magic-wormhole/codes.py import readline c = Completer(completions) readline.set_startup_hook() if "libedit" in readline.__doc__: readline.parse_and_bind("bind ^I rl_complete") else: readline.parse_and_bind("tab: complete") readline.set_completer(c.completer) readline.set_completer_delims(" ") if default: readline.set_startup_hook(lambda: readline.insert_text(default)) except ImportError: pass try: return input(prompt) except KeyboardInterrupt: error(-1) def retrieve_context( arg: Optional[str], prompt: str = "context: ", default: Optional[str] = None, stdin: bool = True, db: Optional[Database] = None, ) -> str: if arg == "-" and stdin: context = sys.stdin.read() elif arg is None or arg == ":": if db: context = input_complete( prompt, completions=[c for c in db], default=default ) else: context = input_complete(prompt, default=default) else: context = arg return context.strip() class PasswordAction(argparse.Action): def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Optional[Union[str, Sequence[Any]]], option_string: Optional[str] = None, ) -> None: env_password = os.getenv("IMPASS_PASSWORD") if env_password is None or env_password == "": password: Optional[Union[str, int]] = None elif env_password in ["prompt", ":"]: password = ":" else: try: password = int() except ValueError: error( 1, "IMPASS_PASSWORD environment variable is neither int nor 'prompt'.", ) if values == ":": password = ":" elif isinstance(values, str): try: password = int(values) except ValueError: error(666, "Don't type your password on the command line!!!") setattr(namespace, self.dest, password) def retrieve_password(pwspec: str) -> str: if pwspec == ":": return input_password() else: log("Auto-generating password...") return pwspec def input_password() -> str: try: password0 = getpass.getpass("password: ") password1 = getpass.getpass("reenter password: ") if password0 != password1: error(2, "Passwords do not match. Aborting.") return password0 except KeyboardInterrupt: error(-1) ############################################################ # command functions def add(args: Optional[List[str]]) -> argparse.ArgumentParser: """Add new entry. If the context already exists in the database an error will be thrown. """ parser = argparse.ArgumentParser(prog=PROG + " add", description=add.__doc__) parser.add_argument( "context", nargs="?", help="existing database context, ':' for prompt, or '-' for stdin", ) parser.add_argument( "pwspec", nargs="?", action=PasswordAction, help="password spec: N octets or ':' for prompt", ) if args is None: return parser argsns = parser.parse_args(args) keyid = get_keyid() db = open_db(keyid, create=True) context = retrieve_context(argsns.context) if context in db: error(2, "Context '{}' already exists.".format(context)) password = retrieve_password(argsns.pwspec) try: db.add(context, password) db.save() except DatabaseError as e: error(10, "Impass database error: {}".format(e.msg)) log("New entry writen.") return parser def replace(args: Optional[List[str]]) -> argparse.ArgumentParser: """Replace password for entry. If the context does not already exist in the database an error will be thrown. """ parser = argparse.ArgumentParser( prog=PROG + " replace", description=replace.__doc__ ) parser.add_argument( "context", nargs="?", help="existing database context, ':' for prompt, or '-' for stdin", ) parser.add_argument( "pwspec", nargs="?", action=PasswordAction, help="password spec: N octets or ':' for prompt", ) if args is None: return parser argsns = parser.parse_args(args) keyid = get_keyid() db = open_db(keyid) context = retrieve_context(argsns.context, db=db) if context not in db: error(2, "Context '{}' not found.".format(context)) password = retrieve_password(argsns.pwspec) try: db.replace(context, password) db.save() except DatabaseError as e: error(10, "Impass database error: {}".format(e.msg)) log("Password replaced.") return parser def update(args: Optional[List[str]]) -> argparse.ArgumentParser: """Update context for existing entry, keeping password the same. Special context value of '-' can only be provided to the old context. """ parser = argparse.ArgumentParser(prog=PROG + " update", description=update.__doc__) parser.add_argument( "old_context", nargs="?", help="existing database context, ':' for prompt, or '-' for stdin", ) parser.add_argument( "new_context", nargs="?", help="new database context or ':' for prompt" ) if args is None: return parser argsns = parser.parse_args(args) keyid = get_keyid() db = open_db(keyid) old_context = retrieve_context(argsns.old_context, prompt="old context: ", db=db) if old_context not in db: error(2, "Context '{}' not found".format(old_context)) new_context = retrieve_context( argsns.new_context, prompt="new context: ", default=old_context, stdin=False ) if new_context in db: error(2, "Context '{}' already exists.".format(new_context)) try: db.update(old_context, new_context) db.save() except DatabaseError as e: error(10, "Impass database error: {}".format(e.msg)) log("Entry updated.") return parser def dump(args: Optional[List[str]]) -> argparse.ArgumentParser: """Dump password database to stdout as json. If a string is provide only entries whose context contains the string will be dumped. Otherwise all entries are returned. Passwords will not be displayed unless IMPASS_DUMP_PASSWORDS is set. """ parser = argparse.ArgumentParser(prog=PROG + " dump", description=dump.__doc__) parser.add_argument("string", nargs="?", help="substring match for contexts") if args is None: return parser argsns = parser.parse_args(args) keyid = get_keyid() db = open_db(keyid) results = db.search(argsns.string) output: Dict[str, Dict[str, str]] = {} for context in results: output[context] = {} output[context]["date"] = results[context]["date"] if os.getenv("IMPASS_DUMP_PASSWORDS"): output[context]["password"] = results[context]["password"] print(json.dumps(output, sort_keys=True, indent=2)) return parser def gui( args: Optional[List[str]], method: Optional[str] = os.getenv("IMPASS_XPASTE", None) ) -> argparse.ArgumentParser: """Launch minimal GUI. Good for X11 or Wayland-based window manager integration. Upon invocation the user will be prompted to decrypt the database, after which a graphical search prompt will be presented. If an additional string is provided, it will be added as the initial search string. All matching results for the query will be presented to the user. When a result is selected, the password will be retrieved according to the method specified by IMPASS_XPASTE. If no match is found, the user has the opportunity to generate and store a new password, which is then delivered via IMPASS_XPASTE. Note: contexts that have leading or trailing whitespace are not accessible through the GUI. """ parser = argparse.ArgumentParser(prog=PROG + " gui", description=gui.__doc__) parser.add_argument("string", nargs="?", help="substring match for contexts") if args is None: return parser argsns = parser.parse_args(args) from .gui import Gui if method is None: if os.getenv("SWAYSOCK", None) is not None: method = "sway" elif os.getenv("DISPLAY", None) is not None: method = "xdo" if method == "xdo": try: import xdo # type: ignore except ModuleNotFoundError: error( 1, """The xdo module is not found, so 'xdo' pasting is not available. Please install python3-xdo.""", ) # initialize xdo x = xdo.xdo() # get the id of the currently focused window win = x.get_focused_window() elif method == "xclip": pass elif method == "sway": try: import i3ipc # type: ignore except ModuleNotFoundError: error( 1, """The i3ipc module is not found, so 'sway' pasting is not available. Please install python3-i3ipc.""", ) i3conn = i3ipc.Connection() res = i3conn.command(f"mark {_swaymark}") if len(res) != 1 and not res[0]["success"]: error(1, "Failed to mark focused window") con_info = i3conn.get_tree().find_focused() if _swaymark not in con_info.marks: error(1, "The focused window was not marked") criteria = f"con_mark={_swaymark} con_id={con_info.id} pid={con_info.pid}" else: error(1, "Unknown X paste method '{}'.".format(method)) keyid = get_keyid() db = open_db(keyid) result = Gui(db, query=argsns.string).return_value() # type the password in the saved window if result: if method == "xdo": x.focus_window(win) x.wait_for_window_focus(win) x.type(result["password"]) elif method == "xclip": xclip(result["password"]) elif method == "sway": # pick the right element i3conn.command(f"[{criteria}] focus") proc = subprocess.Popen( ["wtype", "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) (stdout, stderr) = proc.communicate(result["password"].encode()) i3conn.command(f"[{criteria}] unmark") if proc.returncode: error(1, "failed to run wtype to inject keystrokes") else: error(1, f"Unknown X paste method '{method}'.") else: if method == "sway": i3conn.command(f"[{criteria}] unmark") return parser def remove(args: Optional[List[str]]) -> argparse.ArgumentParser: """Remove entry. If the context does not already exist in the database an error will be thrown. """ parser = argparse.ArgumentParser(prog=PROG + " remove", description=remove.__doc__) parser.add_argument( "context", nargs="?", help="existing database context, ':' for prompt, or '-' for stdin", ) if args is None: return parser argsns = parser.parse_args(args) keyid = get_keyid() db = open_db(keyid) context = retrieve_context(argsns.context, db=db) if context not in db: error(2, "Context '{}' not found.".format(context)) try: log("Really remove entry '{}'?".format(context)) response = input("Type 'yes' to remove: ") except KeyboardInterrupt: error(-1) if response != "yes": error(-1) try: db.remove(context) db.save() except DatabaseError as e: error(10, "Impass database error: {}".format(e.msg)) log("Entry removed.") return parser def print_help(args: Optional[List[str]]) -> argparse.ArgumentParser: """Full usage or command help (also '-h' after command).""" parser = argparse.ArgumentParser( prog=PROG + " help", description=print_help.__doc__ ) if args is None: return parser # if no argument is provided print the full man page try: cmd = args[0] except IndexError: print_manpage() return parser # otherwise assume the first argument is a command and print it's # help func = get_func(cmd) func(["-h"]) return parser def version(args: Optional[List[str]]) -> argparse.ArgumentParser: """Print version.""" parser = argparse.ArgumentParser( prog=PROG + " version", description=version.__doc__ ) if args is None: return parser print(__version__) return parser ############################################################ # main synopsis = f"{PROG} [...]" # NOTE: double spaces are interpreted by text2man to be paragraph # breaks. NO DOUBLE SPACES. Also two spaces at the end of a line # indicate an element in a tag list. def print_manpage() -> None: print( f""" NAME {PROG} - Simple and secure password management and retrieval system SYNOPSIS {synopsis} DESCRIPTION The password database is stored as a single json object, OpenPGP encrypted and signed, and written to local disk (see IMPASS_DB). The file is created upon addition of the first entry. Database entries are keyed by 'context'. During retrieval of passwords the database is decrypted and read into memory. Contexts are searched by sub-string match. Contexts can be any string. If a context string is not specified on the command line it can be provided at a prompt, which features tab completion for contexts already in the database. One may also specify a context of '-' to read the context from stdin, or ':' to force a prompt. Passwords are auto-generated by default with {DEFAULT_NEW_PASSWORD_OCTETS} bytes of entropy. The number of octets can be specified with the IMPASS_PASSWORD environment variable or via the 'pwspec' optional argument to relevant commands. The length of the actually generated password will sometimes be longer than the specified bytes due to base64 encoding. If pwspec is ':' the user will be prompted for the password. COMMANDS {format_commands(man=True)} SIGNATURES During decryption, OpenPGP signatures on the db file are checked for validity. If any of them are found to not be valid, a warning message will be written to stderr. ENVIRONMENT IMPASS_DB Path to impass database file. Default: ~/.impass/db IMPASS_KEYFILE File containing OpenPGP key ID of database encryption recipient. Default: ~/.impass/keyid IMPASS_KEYID OpenPGP key ID of database encryption recipient. This overrides IMPASS_KEYFILE if set. IMPASS_PASSWORD See Passwords above. IMPASS_DUMP_PASSWORDS Include passwords in dump when set. IMPASS_XPASTE Method for password retrieval from GUI. Options are: 'xdo', which attempts to type the password into the window that had focus on launch, 'xclip' which inserts the password in the X clipboard, and 'sway', which types the password into the focused wayland container. Default: xdo or sway, detected automatically. AUTHOR Jameson Graef Rollins Daniel Kahn Gillmor """.strip() # noqa: W291 (trailing whitespace) ) def format_commands(man: bool = False) -> str: prefix = " " * 8 wrapper = textwrap.TextWrapper( width=70, initial_indent=prefix, subsequent_indent=prefix, ) with io.StringIO("some initial text data") as f: for name, func in CMDS.items(): if man: parser = func(None) if parser is None: raise Exception( f"{name} yielded function {func} that did" "not return a parser (internal error)" ) startlen = len("usage: impass ") usage = parser.format_usage()[startlen:].strip() if parser.description is None: desc = "" else: desc = wrapper.fill( "\n".join( [ln.strip() for ln in parser.description.splitlines() if ln] ) ) f.write(" {} \n".format(usage)) f.write(desc + "\n") f.write("\n") else: if func.__doc__ is not None: desc = func.__doc__.splitlines()[0] f.write(" {:15}{}\n".format(name, desc)) output = f.getvalue() return output.rstrip() CMDS: collections.OrderedDict[ str, Callable[[Optional[List[str]]], argparse.ArgumentParser] ] = collections.OrderedDict( [ ("add", add), ("replace", replace), ("update", update), ("dump", dump), ("gui", gui), ("remove", remove), ("help", print_help), ("version", version), ] ) ALIAS = { "--version": "version", "--help": "help", "-h": "help", } def get_func(cmd: str) -> Callable[[Optional[List[str]]], argparse.ArgumentParser]: """Retrieve the appropriate function from the command argument.""" if cmd in ALIAS: cmd = ALIAS[cmd] try: return CMDS[cmd] except KeyError: log("Unknown command: {}".format(cmd)) log("See 'help' for usage.") error(1) def main() -> None: if len(sys.argv) < 2: log("Command not specified.") log("usage: {}".format(synopsis)) log() log(format_commands()) error(1) cmd = sys.argv[1] args = sys.argv[2:] func = get_func(cmd) # DEPRECATE: this is for the assword->impass transition if os.path.basename(sys.argv[0]) == "assword": log("WARNING: assword has been renamed impass. Please update your invocations.") vfound = [] for var in ["DB", "KEYFILE", "KEYID", "PASSWORD", "DUMP_PASSWORDS", "XPASTE"]: val = os.getenv("ASSWORD_" + var) if val: vfound.append(var) if not os.getenv("IMPASS_" + var): os.environ["IMPASS_" + var] = val if vfound: log( """WARNING: assword has been renamed impass. Please update your environment variables:""" ) for var in vfound: log(" ASSWORD_{var} -> IMPASS_{var}".format(var=var)) assword_dir = os.path.join(os.path.expanduser("~"), ".assword") if ( os.path.exists(assword_dir) and (not os.path.islink(assword_dir)) and os.path.isdir(assword_dir) and (not os.getenv("IMPASS_DB")) and cmd not in ["help", "version", "-h", "--help", "--version"] ): try: os.rename(assword_dir, IMPASS_DIR) linkerr: Optional[str] = None try: os.symlink(IMPASS_DIR, assword_dir) except Exception as e: linkerr = str(e) pass print("renamed ~/.assword -> ~/.impass", file=sys.stderr) if linkerr is not None: linkerr = f"(failed symlinking ~/.assword to ~/.impass ({linkerr}))" print(linkerr, file=sys.stderr) except Exception as e: sys.exit( f"""Could not rename old assword directory ~/.assword -> ~/.impass. Error: {e} Please check ~/.impass path.""" ) os.symlink(IMPASS_DIR, assword_dir) log("renamed ~/.assword -> ~/.impass") # DEPRECATE cmd = sys.argv[1] args = sys.argv[2:] func = get_func(cmd) func(args) if __name__ == "__main__": main() impass-0.13.1/impass/db.py000066400000000000000000000221061460454333500153330ustar00rootroot00000000000000import os import io import stat import json import gpg # type: ignore import codecs import datetime from typing import Optional, Dict, Iterator ############################################################ DEFAULT_NEW_PASSWORD_OCTETS = 18 def pwgen(nbytes: int) -> str: """Return *nbytes* bytes of random data, base64-encoded.""" s = os.urandom(nbytes) b = codecs.encode(s, "base64") b = bytes(filter(lambda x: x not in b"=\n", b)) return codecs.decode(b, "ascii") ############################################################ class DatabaseError(Exception): def __init__(self, msg: str) -> None: self.msg = msg def __str__(self) -> str: return repr(self.msg) class Database: """An impass database.""" def __init__( self, dbpath: Optional[str] = None, keyid: Optional[str] = None ) -> None: """Database at dbpath will be decrypted and loaded into memory. If dbpath is not specified, an empty database will be initialized. The sigvalid property is set False if any OpenPGP signatures on the db file are invalid. sigvalid is None for new databases. """ self._dbpath = dbpath self._keyid = keyid # default database information self._type = "impass" self._version = 1 self._entries: Dict[str, Dict[str, str]] = {} self._gpg = gpg.Context() self._gpg.armor = True self._sigvalid: Optional[bool] = None if self._dbpath and os.path.exists(self._dbpath): try: cleardata = self._decrypt_db(self._dbpath) # FIXME: trap exception if json corrupt jsondata = json.loads(cleardata.decode("utf-8")) except IOError as e: raise DatabaseError(str(e)) # unpack the json data # FIXME: we accept "assword" type for backwords compatibility if "type" not in jsondata or jsondata["type"] not in [ self._type, "assword", ]: raise DatabaseError("Database is not a proper impass database.") if "version" not in jsondata or jsondata["version"] != self._version: raise DatabaseError("Incompatible database.") self._entries = jsondata["entries"] @property def version(self) -> int: """Database version.""" return self._version @property def sigvalid(self) -> Optional[bool]: """Validity of OpenPGP signature on db file.""" return self._sigvalid def __str__(self) -> str: return '' % (self._dbpath) def __repr__(self) -> str: return 'impass.Database("%s")' % (self._dbpath) def __getitem__(self, context: str) -> Dict[str, str]: """Return database entry for exact context.""" return self._entries[context] def __contains__(self, context: str) -> bool: """True if context string in database.""" return context in self._entries def __iter__(self) -> Iterator[str]: """Iterator of all database contexts.""" return iter(self._entries) def _decrypt_db(self, path: str) -> bytes: data = None self._sigvalid = False with open(path, "rb") as f: try: data, _, vfy = self._gpg.decrypt(f, verify=True) for s in vfy.signatures: if s.validity >= gpg.constants.VALIDITY_FULL: self._sigvalid = True except gpg.errors.GPGMEError: # retry decryption without verification: with open(path, "rb") as try2: data, _, _ = self._gpg.decrypt(try2, verify=False) if not isinstance(data, bytes): raise DatabaseError( f"expected gpg.Context.decrypt() to return bytes, got {type(data)}" ) return data def _encrypt_db(self, data: io.BytesIO, keyid: Optional[str]) -> bytes: # The signer and the recipient are assumed to be the same. # FIXME: should these be separated? try: recipient = self._gpg.get_key(keyid or self._keyid, secret=False) signer = self._gpg.get_key(keyid or self._keyid, secret=False) except gpg.errors.GPGMEError: raise DatabaseError("Could not retrieve GPG encryption key.") self._gpg.signers = [signer] data.seek(0) encdata, _, _ = self._gpg.encrypt( data, [recipient], always_trust=True, compress=False ) if not isinstance(encdata, bytes): raise DatabaseError( f"expected gpg.Context.decrypt() to return bytes, got {type(data)}" ) return encdata def _set_entry( self, context: str, password: Optional[str] = None ) -> Dict[str, str]: if not isinstance(password, str): if password is None: bytes = DEFAULT_NEW_PASSWORD_OCTETS if isinstance(password, int): bytes = password password = pwgen(bytes) e = {"password": password, "date": datetime.datetime.utcnow().isoformat() + "Z"} self._entries[context] = e return e def add(self, context: str, password: Optional[str] = None) -> Dict[str, str]: """Add new entry. If password is None, one will be generated automatically. If password is an int it will be interpreted as the number of random bytes to use. If the context is already in the db a DatabaseError will be raised. Database changes are not saved to disk until the save() method is called. """ if context == "": raise DatabaseError("Can not add empty string context") if context in self: raise DatabaseError("Context already exists (see replace())") return self._set_entry(context, password) def replace(self, context: str, password: Optional[str] = None) -> Dict[str, str]: """Replace entry password. If password is None, one will be generated automatically. If password is an int it will be interpreted as the number of random bytes to use. If the context is not in the db a DatabaseError will be raised. Database changes are not saved to disk until the save() method is called. """ if context not in self: raise DatabaseError("Context not found (see add())") return self._set_entry(context, password) def update(self, old_context: str, new_context: str) -> None: """Update entry context. If the old context is not in the db a DatabaseError will be raised. Database changes are not saved to disk until the save() method is called. """ if old_context not in self: raise DatabaseError("Context '%s' not found." % old_context) password = self[old_context]["password"] self.add(new_context, password) self.remove(old_context) def remove(self, context: str) -> None: """Remove entry. Database changes are not saved to disk until the save() method is called. """ if context not in self: raise DatabaseError("Context '%s' not found" % context) del self._entries[context] def save(self, keyid: Optional[str] = None, path: Optional[str] = None) -> None: """Save database to disk. Key ID must either be specified here or at database initialization. If path not specified, database will be saved at original dbpath location. """ # FIXME: should check that recipient is not different than who # the db was originally encrypted for if not keyid: keyid = self._keyid if not keyid: raise DatabaseError("Key ID for decryption not specified.") if not path: path = self._dbpath if not path: raise DatabaseError("Save path not specified.") jsondata = { "type": self._type, "version": self._version, "entries": self._entries, } cleardata = io.BytesIO(json.dumps(jsondata, indent=2).encode("utf-8")) encdata = self._encrypt_db(cleardata, keyid) newpath = path + ".new" bakpath = path + ".bak" mode = stat.S_IRUSR | stat.S_IWUSR with open(newpath, "wb") as f: f.write(encdata) if os.path.exists(path): mode = os.stat(path)[stat.ST_MODE] os.rename(path, bakpath) # FIXME: shouldn't this be done when we're actually creating # the file? os.chmod(newpath, mode) os.rename(newpath, path) def search(self, string: Optional[str] = None) -> Dict[str, Dict[str, str]]: """Search for string in contexts. If query is None, all entries will be returned. """ mset = {} for context, entry in self._entries.items(): # simple substring match if not string or string in context: mset[context] = entry return mset impass-0.13.1/impass/gui.py000066400000000000000000000652061460454333500155420ustar00rootroot00000000000000from __future__ import annotations import os import gi # type: ignore from typing import Any, Optional, Dict, Callable from .db import pwgen, DEFAULT_NEW_PASSWORD_OCTETS, Database gi.require_version("Gtk", "3.0") from gi.repository import Gtk # type: ignore # noqa: E402 from gi.repository import GObject # noqa: E402 from gi.repository import Gdk # noqa: E402 ############################################################ # Assumes that the func_data is set to the number of the text column in the # model. def _match_func(completion: Any, key: str, iter: int, column: str) -> bool: model = completion.get_model() text = model[iter][column] if text.lower().find(key.lower()) > -1: return True return False _gui_layout = """ True False end gtk-delete True False True True True False end Create custom… True False True False 4 impass dialog-password True False vertical False False vertical True False WARNING: could not validate signature on db file! False True 0 True False False True 1 False True 0 True False Global state of impass gui True True 1 True False True True 50 Enter context… True True 0 Emit True True True False True 1 True True True False True 2 False True 2 False True False Context: False True 0 True False vertical True True Enter context… False True 0 True False True False This context already exists! True True 0 False True 1 True True 1 False True 2 False True False Password: False True 0 True False vertical True False True gtk-refresh gtk-find generate a new password show password You must enter a password! password False True 0 True False %d characters, %d lowercase, etc… False True 1 True True 1 Create and emit True True True False True 2 False True 3 """ class Gui: """Impass X-based query UI.""" def __init__(self, db: Database, query: Optional[str] = None) -> None: """ +--------------------- warning --------------------+ | notification | +--------------------------------------------------+ | description | +----------------- simplebox ----------------------+ | [_simplectxentry____] | +------------------- ctxbox -----------------------+ | | +----- ctxbox2 -----------------------+ | ctxlabel | | [_ctxentry________________________] | | | +----- ctxwarning --------------------+ (ctxwarning only shows when | | | ctxwarninglabel | ctxentry matches existing | | +-------------------------------------+ entry (createbtn is also +------------------- passbox ----------------------+ disabled in this case)) | +------ passbox2 --------+ | | passlabel | [_passentry__________] | | createbtn saves, emits, and | | passdescription | | closes +-----------+------------------------+-------------+ """ self.db = db self.selected: Optional[Dict[str, str]] = None self.window: Gtk.Widget self.entry: Gtk.Widget self.label: Gtk.Widget if query is not None: query = query.strip() if query: # If we have an intial query, directly do a search without # initializing any X objects. This will initialize the # database and potentially return entries. r = self.db.search(query) # If only a single entry is found, _search() will set the # result and attempt to close any X objects (of which # there are none). Since we don't need to initialize any # GUI, return the initialization immediately. # See .returnValue(). if len(r) == 1: self.selected = r[list(r.keys())[0]] return self.builder: Gtk.Builder = Gtk.Builder.new_from_string( _gui_layout, len(_gui_layout) ) self.window = self.builder.get_object("impass-gui") self.entry = self.builder.get_object("simplectxentry") self.simplebtn = self.builder.get_object("simplebtn") self.simplemenubtn = self.builder.get_object("simplemenubtn") self.emitmenu = self.builder.get_object("emitmenu") self.createmenu = self.builder.get_object("createmenu") self.warning = self.builder.get_object("warning") self.ctxentry = self.builder.get_object("ctxentry") self.ctxwarning = self.builder.get_object("ctxwarning") self.ctxwarninglabel = self.builder.get_object("ctxwarninglabel") self.createbtn = self.builder.get_object("createbtn") self.passentry = self.builder.get_object("passentry") self.passdescription = self.builder.get_object("passdescription") self.simplebox = self.builder.get_object("simplebox") self.ctxbox = self.builder.get_object("ctxbox") self.passbox = self.builder.get_object("passbox") if self.db.sigvalid is False: self.warning.show() completion = Gtk.EntryCompletion() self.entry.set_completion(completion) liststore = Gtk.ListStore(GObject.TYPE_STRING) completion.set_model(liststore) completion.set_text_column(0) completion.set_match_func(_match_func, 0) # 0 is column number context_len = 50 for context in sorted(filter(lambda x: x == x.strip(), self.db), key=str.lower): if len(context) > context_len: context_len = len(context) liststore.append([context]) self.window.connect("destroy", self.destroy) self.window.connect("key-press-event", self.keypress) self.entry.connect("activate", self.simpleclicked) self.entry.connect("changed", self.update_simple_context_entry) self.entry.connect("populate-popup", self.simple_ctx_popup) self.simplebtn.connect("clicked", self.simpleclicked) self.builder.get_object("deletemenuitem").connect( "activate", self.deleteclicked ) self.builder.get_object("custommenuitem").connect( "activate", self.customclicked ) self.ctxentry.connect("changed", self.update_ctxentry) self.ctxentry.connect("activate", self.customcreateclicked) self.passentry.connect("changed", self.update_passentry) self.passentry.connect("activate", self.customcreateclicked) self.passentry.connect("icon-press", self.passentry_icon_clicked) self.passentry.connect("populate-popup", self.passentry_popup) self.createbtn.connect("clicked", self.customcreateclicked) if query: self.entry.set_text(query) self.set_state("Enter context for desired password:") self.update_simple_context_entry(None) self.window.show() def set_state(self, state: str) -> None: self.builder.get_object("description").set_label(state) def update_simple_context_entry(self, widget: Optional[Gtk.Widget]) -> None: sctx = self.entry.get_text().strip() if sctx in self.db: self.simplebtn.set_label("Emit") self.simplemenubtn.set_popup(self.emitmenu) elif sctx is None or sctx == "": self.simplebtn.set_label("Create…") self.simplemenubtn.set_popup(None) else: self.simplebtn.set_label("Create") self.simplemenubtn.set_popup(self.createmenu) def add_to_menu( self, menu: Gtk.Widget, name: str, onclicked: Callable[[Gui], None], position: int, ) -> None: x = Gtk.MenuItem(label=name) x.connect("activate", onclicked) x.show() menu.insert(x, position) def simple_ctx_popup( self, entry: Gtk.Widget, widget: Gtk.Widget, data: Optional[Any] = None ) -> None: sctx = self.entry.get_text().strip() if sctx in self.db: self.add_to_menu( widget, "Emit password for '" + sctx + "'", self.simpleclicked, 0 ) self.add_to_menu( widget, "Delete password for '" + sctx + "'", self.deleteclicked, 1 ) pos = 2 elif sctx is None or sctx == "": self.add_to_menu(widget, "Create custom password…", self.customclicked, 0) pos = 1 else: self.add_to_menu( widget, "Create and emit password for '" + sctx + "'", self.create, 0 ) self.add_to_menu( widget, "Create custom password for '" + sctx + "'…", self.customclicked, 1, ) pos = 2 sep = Gtk.SeparatorMenuItem() sep.show() widget.insert(sep, pos) def simpleclicked(self, widget: Gtk.Widget) -> None: sctx = self.entry.get_text().strip() if sctx in self.db: self.selected = self.db[sctx] if self.selected is None: self.label.set_text( "weird -- no context found even though there should be one" ) else: Gtk.main_quit() elif sctx is None or sctx == "": self.customclicked(None) else: self.create(None) def keypress(self, widget: Gtk.Widget, event: Gdk.EventKey) -> None: if event.keyval == Gdk.KEY_Escape: Gtk.main_quit() def create(self, widget: Gtk.Widget, data: Optional[Any] = None) -> None: sctx = self.entry.get_text().strip() self.selected = self.db.add(sctx) self.db.save() Gtk.main_quit() def deleteclicked(self, widget: Gtk.Widget) -> None: sctx = self.entry.get_text().strip() confirmation = Gtk.MessageDialog( parent=self.window, modal=True, destroy_with_parent=True, buttons=Gtk.ButtonsType.OK_CANCEL, message_type=Gtk.MessageType.QUESTION, text="Are you sure you want to delete the password for '" + sctx + "'?", ) answer = confirmation.run() confirmation.destroy() if answer == Gtk.ResponseType.OK: self.selected = None self.db.remove(sctx) self.db.save() Gtk.main_quit() def customclicked(self, widget: Gtk.Widget) -> None: if self.ctxentry is None or self.entry is None: raise Exception("Gui is not initialized") self.simplebox.hide() self.ctxbox.show() self.passbox.show() self.ctxentry.set_text(self.entry.get_text()) selection = self.entry.get_selection_bounds() if selection: self.ctxentry.select_region(selection[0], selection[1]) else: self.ctxentry.set_position(self.entry.get_position()) self.ctxentry.grab_focus_without_selecting() self.set_state("Create new password (with custom settings):") self.refreshpass() self.update_ctxentry() def update_ctxentry( self, widget: Optional[Gtk.Widget] = None, data: Optional[Any] = None ) -> None: sctx = self.ctxentry.get_text().strip() if sctx in self.db: self.ctxwarning.show() self.ctxwarninglabel.set_text("The context '%s' already exists!" % (sctx)) self.createbtn.set_sensitive(False) elif sctx is None or sctx == "": self.ctxwarning.hide() self.createbtn.set_sensitive(False) else: self.ctxwarning.hide() self.createbtn.set_sensitive(self.passentry.get_text() != "") def update_passentry( self, widget: Optional[Gtk.Widget] = None, data: Optional[Any] = None ) -> None: newpass = self.passentry.get_text() sctx = self.ctxentry.get_text().strip() ln = len(newpass) # FIXME: should check (and warn) for non-ascii characters lcount = len("".join(filter(lambda x: x.islower(), newpass))) ucount = len("".join(filter(lambda x: x.isupper(), newpass))) ncount = len("".join(filter(lambda x: x.isnumeric(), newpass))) ocount = ln - (lcount + ucount + ncount) desc = "%d characters (%d lowercase, %d uppercase, %d number, %d other)" % ( ln, lcount, ucount, ncount, ocount, ) self.createbtn.set_sensitive( newpass != "" and sctx != "" and sctx not in self.db ) self.passdescription.set_text(desc) def passentry_icon_clicked( self, widget: Gtk.Widget, pos: Gtk.EntryIconPosition, event: Optional[Gdk.Event] = None, data: Optional[Any] = None, ) -> None: if pos == Gtk.EntryIconPosition.PRIMARY: self.refreshpass() elif pos == Gtk.EntryIconPosition.SECONDARY: newvis = not self.passentry.get_visibility() self.passentry.set_visibility(newvis) self.passentry.set_icon_tooltip_text( Gtk.EntryIconPosition.SECONDARY, "hide password" if newvis else "show password", ) def passentry_popup( self, entry: Gtk.Entry, widget: Gtk.Widget, data: Optional[Any] = None ) -> None: self.add_to_menu(widget, "Generate a new password", self.refreshpass, 0) self.add_to_menu( widget, "Hide password" if self.passentry.get_visibility() else "Show password", lambda x: self.passentry_icon_clicked( widget, Gtk.EntryIconPosition.SECONDARY ), 1, ) sep = Gtk.SeparatorMenuItem() sep.show() widget.insert(sep, 2) def refreshpass( self, widget: Optional[Gtk.Widget] = None, event: Optional[Gdk.Event] = None ) -> None: pwsize = os.environ.get("IMPASS_PASSWORD", DEFAULT_NEW_PASSWORD_OCTETS) try: pwsize = int(pwsize) except ValueError: pwsize = DEFAULT_NEW_PASSWORD_OCTETS newpw = pwgen(pwsize) self.passentry.set_text(newpw) # FIXME: should refocus self.passentry? def customcreateclicked( self, widget: Optional[Gtk.Widget] = None, event: Optional[Gdk.Event] = None ) -> None: newctx = self.ctxentry.get_text().strip() newpass = self.passentry.get_text() if newpass == "" or newctx == "" or newctx in self.db: # this button is not supposed to work under these conditions return self.selected = self.db.add(newctx, password=newpass) self.db.save() Gtk.main_quit() def destroy(self, widget: Gtk.Widget, data: Optional[Any] = None) -> None: Gtk.main_quit() def return_value(self) -> Optional[Dict[str, str]]: if self.selected is None: Gtk.main() return self.selected impass-0.13.1/impass/version.py000066400000000000000000000000251460454333500164270ustar00rootroot00000000000000__version__ = "0.12" impass-0.13.1/requirements.txt000066400000000000000000000000301460454333500163540ustar00rootroot00000000000000gpg PyGobject xdo i3ipc impass-0.13.1/setup.cfg000066400000000000000000000000361460454333500147170ustar00rootroot00000000000000[flake8] max-line-length = 88 impass-0.13.1/setup.py000077500000000000000000000034321460454333500146160ustar00rootroot00000000000000#!/usr/bin/env python3 # much of the structure here was cribbed from # https://github.com/pypa/sampleproject from setuptools import setup # To use a consistent encoding from codecs import open from os import path here = path.abspath(path.dirname(__file__)) # Get the long description from the README file with open(path.join(here, "README.md"), encoding="utf-8") as f: long_description = f.read() version = {} with open("impass/version.py") as f: exec(f.read(), version) setup( name="impass", version=version["__version__"], description="Simple and secure password management system.", long_description=long_description, author="Jameson Rollins", author_email="jrollins@finestructure.net", url="https://salsa.debian.org/debian/impass", license="GPLv3+", packages=["impass"], keywords=["passwords password-management"], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", # maybe extend this? "impass gui" won't work on anything but # X11, but the rest of it might still be useful. "Environment :: X11 Applications", "Operating System :: POSIX", "Programming Language :: Python :: 3 :: Only", ], install_requires=[ "gpg", "PyGobject", ], # You can install this optional dependency using the following syntax: # $ pip install -e .xdo extras_require={ "xdo": ["xdo"], }, # https://chriswarrick.com/blog/2014/09/15/python-apps-the-right-way-entry_points-and-scripts/ # should we have a 'gui_scripts' as well? entry_points={ "console_scripts": [ "impass = impass.__main__:main", ], }, ) impass-0.13.1/swayconfig000066400000000000000000000002451460454333500151740ustar00rootroot00000000000000# Ensure that sway shows the impass popup dialog appropriately for_window [app_id="^impass$" ] { title_format "🔐%title" floating enable sticky enable } impass-0.13.1/test/000077500000000000000000000000001460454333500140565ustar00rootroot00000000000000impass-0.13.1/test/basic000077500000000000000000000047321460454333500150730ustar00rootroot00000000000000#!/usr/bin/env bash # # Copyright (c) 2005 Junio C Hamano # test_description='the test framework itself' . lib/test-lib.sh ################################################################ test_expect_success 'success is reported like this' ' : ' test_set_prereq HAVEIT haveit=no test_expect_success HAVEIT 'test runs if prerequisite is satisfied' ' test_have_prereq HAVEIT && haveit=yes ' clean=no test_expect_success 'tests clean up after themselves' ' test_when_finished clean=yes ' cleaner=no test_expect_code 1 'tests clean up even after a failure' ' test_when_finished cleaner=yes && (exit 1) ' if test $clean$cleaner != yesyes then say "bug in test framework: cleanup commands do not work reliably" exit 1 fi test_expect_code 2 'failure to clean up causes the test to fail' ' test_when_finished "(exit 2)" ' # Ensure that all tests are being run test_begin_subtest 'Ensure that all available tests will be run' eval $(sed -n -e '/^TESTS="$/,/^"$/p' $TEST_DIRECTORY/impass-test) tests_in_suite=$(for i in $TESTS $TESTS_NET; do echo $i; done | sort) available=$(find "$TEST_DIRECTORY" -maxdepth 1 -type f -perm /111 \ ! -name '*~' \ ! -name 'lib/*' \ ! -name test-verbose \ ! -name impass-test \ | sed 's,.*/,,' | sort) test_expect_equal "$tests_in_suite" "$available" EXPECTED=$TEST_DIRECTORY/lib/test.expected-output suppress_diff_date() { sed -e 's/\(.*\-\-\- test-verbose\.4\.\expected\).*/\1/' \ -e 's/\(.*\+\+\+ test-verbose\.4\.\output\).*/\1/' } test_begin_subtest "Ensure that test output is suppressed unless the test fails" output=$(cd $TEST_DIRECTORY; ./test-verbose 2>&1 | suppress_diff_date) expected=$(cat $EXPECTED/test-verbose-no | suppress_diff_date) test_expect_equal "$output" "$expected" test_begin_subtest "Ensure that -v does not suppress test output" output=$(cd $TEST_DIRECTORY; ./test-verbose -v 2>&1 | suppress_diff_date) expected=$(cat $EXPECTED/test-verbose-yes | suppress_diff_date) # Do not include the results of test-verbose in totals rm $TEST_DIRECTORY/test-results/test-verbose-* rm -r $TEST_DIRECTORY/tmp.test-verbose test_expect_equal "$output" "$expected" test_begin_subtest "Ensure that we're looking at the correct library" output=$(python3 -c 'import impass; print(impass.__file__)') expected="${SRC_DIRECTORY}/impass/__init__.py" test_expect_equal "$output" "$expected" ################################################################ test_done impass-0.13.1/test/cli000077500000000000000000000041511460454333500145540ustar00rootroot00000000000000#!/usr/bin/env bash test_description='cli' . lib/test-lib.sh ################################################################ test_expect_code 5 'dump non-existant db' \ 'impass dump' test_expect_success 'add first entry' \ 'impass add foo@bar' test_expect_success 'add second entry' \ "impass add 'baz asdf Dokw okb 32438uoijdf'" test_expect_code 2 'add existing context' \ 'impass add foo@bar' test_begin_subtest "dump all entries" impass dump 2>&1 | sed 's/"date": ".*"/FOO/g' >OUTPUT cat <EXPECTED { "baz asdf Dokw okb 32438uoijdf": { FOO }, "foo@bar": { FOO } } EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "dump search 0" impass dump foo 2>&1 | sed 's/"date": ".*"/FOO/g' >OUTPUT cat <EXPECTED { "foo@bar": { FOO } } EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "dump search 1" impass dump asdf 2>&1 | sed 's/"date": ".*"/FOO/g' >OUTPUT cat <EXPECTED { "baz asdf Dokw okb 32438uoijdf": { FOO } } EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "dump search 2" impass dump ba 2>&1 | sed 's/"date": ".*"/FOO/g' >OUTPUT cat <EXPECTED { "baz asdf Dokw okb 32438uoijdf": { FOO }, "foo@bar": { FOO } } EOF test_expect_equal_file OUTPUT EXPECTED test_expect_code 2 'add existing context' 'impass add foo@bar' test_expect_code 2 'replace non-existing context' \ 'impass replace aaaa' # FIXME: add replacement test test_expect_code 2 'update non-existing context' \ 'impass update aaaa' test_begin_subtest "update entry" impass update foo@bar foo@example impass dump foo 2>&1 | sed 's/"date": ".*"/FOO/g' >OUTPUT cat <EXPECTED { "foo@example": { FOO } } EOF test_expect_equal_file OUTPUT EXPECTED test_expect_code 2 'remove non-existant entry' 'impass remove aaaa' test_begin_subtest "remove entry" echo yes | impass remove foo@example impass dump 2>&1 | sed 's/"date": ".*"/FOO/g' >OUTPUT cat <EXPECTED { "baz asdf Dokw okb 32438uoijdf": { FOO } } EOF test_expect_equal_file OUTPUT EXPECTED ################################################################ test_done impass-0.13.1/test/impass-test000077500000000000000000000022551460454333500162610ustar00rootroot00000000000000#!/usr/bin/env bash # Run tests # # Copyright (c) 2005 Junio C Hamano # # Adapted from a Makefile to a shell script by Carl Worth (2010) if [ ${BASH_VERSINFO[0]} -lt 4 ]; then echo "Error: The impass test suite requires a bash version >= 4.0" echo "due to use of associative arrays within the test suite." echo "Please try again with a newer bash (or help us fix the" echo "test suite to be more portable). Thanks." exit 1 fi cd $(dirname "$0") TESTS=" basic library cli " # setup TESTS=${IMPASS_TESTS:=$TESTS} # test for timeout utility if command -v timeout >/dev/null; then TEST_TIMEOUT_CMD="timeout 1m " echo "INFO: using 1 minute timeout for tests" else TEST_TIMEOUT_CMD="" fi # Prep rm -rf test-results mkdir -m 0700 -p ./gnupg GNUPGHOME=./gnupg gpg --import < openpgp-data/secring.gpg GNUPGHOME=./gnupg gpg --import-ownertrust < openpgp-data/ownertrust.txt trap 'e=$?; kill $!; exit $e' HUP INT TERM # Run the tests for test in $TESTS; do $TEST_TIMEOUT_CMD ./$test "$@" & wait $! done trap - HUP INT TERM # Report results echo ./lib/test-aggregate-results test-results/* ev=$? # Clean up rm -rf test-results rm -rf ./gnupg exit $ev impass-0.13.1/test/lib/000077500000000000000000000000001460454333500146245ustar00rootroot00000000000000impass-0.13.1/test/lib/test-aggregate-results000077500000000000000000000032221460454333500211530ustar00rootroot00000000000000#!/usr/bin/env bash fixed=0 success=0 failed=0 broken=0 total=0 for file do while read type value do case $type in '') continue ;; fixed) fixed=$(($fixed + $value)) ;; success) success=$(($success + $value)) ;; failed) failed=$(($failed + $value)) ;; broken) broken=$(($broken + $value)) ;; total) total=$(($total + $value)) ;; esac done <"$file" done pluralize () { case $2 in 1) case $1 in test) echo test ;; failure) echo failure ;; esac ;; *) case $1 in test) echo tests ;; failure) echo failures ;; esac ;; esac } echo "Test suite complete." if [ "$fixed" = "0" ] && [ "$failed" = "0" ]; then tests=$(pluralize "test" $total) printf "All $total $tests " if [ "$broken" = "0" ]; then echo "passed." else failures=$(pluralize "failure" $broken) echo "behaved as expected ($broken expected $failures)." fi; else echo "$success/$total tests passed." if [ "$broken" != "0" ]; then tests=$(pluralize "test" $broken) echo "$broken broken $tests failed as expected." fi if [ "$fixed" != "0" ]; then tests=$(pluralize "test" $fixed) echo "$fixed broken $tests now fixed." fi if [ "$failed" != "0" ]; then tests=$(pluralize "test" $failed) echo "$failed $tests failed." fi fi skipped=$(($total - $fixed - $success - $failed - $broken)) if [ "$skipped" != "0" ]; then tests=$(pluralize "test" $skipped) echo "$skipped $tests skipped." fi # Note that we currently do not consider skipped tests as failing the # build. if [ $success -gt 0 -a $fixed -eq 0 -a $failed -eq 0 ] then exit 0 else exit 1 fi impass-0.13.1/test/lib/test-lib.sh000066400000000000000000000435061460454333500167130ustar00rootroot00000000000000# # Copyright (c) 2005 Junio C Hamano # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see https://www.gnu.org/licenses/ . # # Modified 2012 Jameson Rollins # for use by impass # if --tee was passed, write the output not only to the terminal, but # additionally to the file test-results/$BASENAME.out, too. case "$GIT_TEST_TEE_STARTED, $* " in done,*) # do not redirect again ;; *' --tee '*|*' --va'*) mkdir -p test-results BASE=test-results/$(basename "$0" .sh) (GIT_TEST_TEE_STARTED=done ${SHELL-sh} "$0" "$@" 2>&1; echo $? > $BASE.exit) | tee $BASE.out test "$(cat $BASE.exit)" = 0 exit ;; esac # Keep the original TERM for say_color and test_emacs ORIGINAL_TERM=$TERM # For repeatability, reset the environment to known value. LANG=C LC_ALL=C PAGER=cat TZ=UTC TERM=dumb export LANG LC_ALL PAGER TERM TZ GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u} TEST_EMACS=${TEST_EMACS:-${EMACS:-emacs}} # Protect ourselves from common misconfiguration to export # CDPATH into the environment unset CDPATH unset GREP_OPTIONS # Convenience # # A regexp to match 5 and 40 hexdigits _x05='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' _x40="$_x05$_x05$_x05$_x05$_x05$_x05$_x05$_x05" _x04='[0-9a-f][0-9a-f][0-9a-f][0-9a-f]' _x32="$_x04$_x04$_x04$_x04$_x04$_x04$_x04$_x04" # Each test should start with something like this, after copyright notices: # # test_description='Description of this test... # This test checks if command xyzzy does the right thing... # ' # . ./test-lib.sh [ "x$ORIGINAL_TERM" != "xdumb" ] && ( TERM=$ORIGINAL_TERM && export TERM && [ -t 1 ] && tput bold >/dev/null 2>&1 && tput setaf 1 >/dev/null 2>&1 && tput sgr0 >/dev/null 2>&1 ) && color=t while test "$#" -ne 0 do case "$1" in -d|--d|--de|--deb|--debu|--debug) debug=t; shift ;; -i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate) immediate=t; shift ;; -l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests) GIT_TEST_LONG=t; export GIT_TEST_LONG; shift ;; -h|--h|--he|--hel|--help) help=t; shift ;; -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) verbose=t; shift ;; -q|--q|--qu|--qui|--quie|--quiet) quiet=t; shift ;; --with-dashes) with_dashes=t; shift ;; --no-color) color=; shift ;; --no-python) # noop now... shift ;; --va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind) valgrind=t; verbose=t; shift ;; --tee) shift ;; # was handled already --root=*) root=$(expr "z$1" : 'z[^=]*=\(.*\)') shift ;; *) echo "error: unknown test option '$1'" >&2; exit 1 ;; esac done if test -n "$debug"; then print_subtest () { printf " %-4s" "[$((test_count - 1))]" } else print_subtest () { true } fi if test -n "$color"; then say_color () { ( TERM=$ORIGINAL_TERM export TERM case "$1" in error) tput bold; tput setaf 1;; # bold red skip) tput bold; tput setaf 2;; # bold green pass) tput setaf 2;; # green info) tput setaf 3;; # brown *) test -n "$quiet" && return;; esac shift printf " " printf "$@" tput sgr0 print_subtest ) } else say_color() { test -z "$1" && test -n "$quiet" && return shift printf " " printf "$@" print_subtest } fi error () { say_color error "error: $*\n" GIT_EXIT_OK=t exit 1 } say () { say_color info "$*" } test "${test_description}" != "" || error "Test script did not set test_description." if test "$help" = "t" then echo "Tests ${test_description}" exit 0 fi echo $(basename "$0"): "Testing ${test_description}" exec 5>&1 test_failure=0 test_count=0 test_fixed=0 test_broken=0 test_success=0 die () { code=$? rm -rf "$TEST_TMPDIR" if test -n "$GIT_EXIT_OK" then exit $code else echo >&5 "FATAL: Unexpected exit with code $code" exit 1 fi } GIT_EXIT_OK= # Note: TEST_TMPDIR *NOT* exported! TEST_TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/test-$$.XXXXXX") trap 'die' EXIT test_decode_color () { sed -e 's/.\[1m//g' \ -e 's/.\[31m//g' \ -e 's/.\[32m//g' \ -e 's/.\[33m//g' \ -e 's/.\[34m//g' \ -e 's/.\[35m//g' \ -e 's/.\[36m//g' \ -e 's/.\[m//g' } q_to_nul () { perl -pe 'y/Q/\000/' } q_to_cr () { tr Q '\015' } append_cr () { sed -e 's/$/Q/' | tr Q '\015' } remove_cr () { tr '\015' Q | sed -e 's/Q$//' } test_begin_subtest () { if [ -n "$inside_subtest" ]; then exec 1>&6 2>&7 # Restore stdout and stderr error "bug in test script: Missing test_expect_equal in ${BASH_SOURCE[1]}:${BASH_LINENO[0]}" fi test_subtest_name="$1" test_reset_state_ # Remember stdout and stderr file descriptors and redirect test # output to the previously prepared file descriptors 3 and 4 (see # below) if test "$verbose" != "t"; then exec 4>test.output 3>&4; fi exec 6>&1 7>&2 >&3 2>&4 inside_subtest=t } # Pass test if two arguments match # # Note: Unlike all other test_expect_* functions, this function does # not accept a test name. Instead, the caller should call # test_begin_subtest before calling this function in order to set the # name. test_expect_equal () { exec 1>&6 2>&7 # Restore stdout and stderr inside_subtest= test "$#" = 3 && { prereq=$1; shift; } || prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_equal" output="$1" expected="$2" if ! test_skip "$test_subtest_name" then if [ "$output" = "$expected" ]; then test_ok_ "$test_subtest_name" else testname=$this_test.$test_count echo "$expected" > $testname.expected echo "$output" > $testname.output test_failure_ "$test_subtest_name" "$(diff -u $testname.expected $testname.output)" fi fi } # Like test_expect_equal, but takes two filenames. test_expect_equal_file () { exec 1>&6 2>&7 # Restore stdout and stderr inside_subtest= test "$#" = 3 && { prereq=$1; shift; } || prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_equal" output="$1" expected="$2" if ! test_skip "$test_subtest_name" then if diff -q "$expected" "$output" >/dev/null ; then test_ok_ "$test_subtest_name" else testname=$this_test.$test_count cp "$output" $testname.output cp "$expected" $testname.expected test_failure_ "$test_subtest_name" "$(diff -u $testname.expected $testname.output)" fi fi } # Like test_expect_equal, but arguments are JSON expressions to be # canonicalized before diff'ing. If an argument cannot be parsed, it # is used unchanged so that there's something to diff against. test_expect_equal_json () { output=$(echo "$1" | python3 -mjson.tool || echo "$1") expected=$(echo "$2" | python3 -mjson.tool || echo "$2") shift 2 test_expect_equal "$output" "$expected" "$@" } # Use test_set_prereq to tell that a particular prerequisite is available. # The prerequisite can later be checked for in two ways: # # - Explicitly using test_have_prereq. # # - Implicitly by specifying the prerequisite tag in the calls to # test_expect_{success,failure,code}. # # The single parameter is the prerequisite tag (a simple word, in all # capital letters by convention). test_set_prereq () { satisfied="$satisfied$1 " } satisfied=" " test_have_prereq () { case $satisfied in *" $1 "*) : yes, have it ;; *) ! : nope ;; esac } # declare prerequisite for the given external binary test_declare_external_prereq () { binary="$1" test "$#" = 2 && name=$2 || name="$binary(1)" hash $binary 2>/dev/null || eval " test_missing_external_prereq_${binary}_=t $binary () { echo -n \"\$test_subtest_missing_external_prereqs_ \" | grep -qe \" $name \" || test_subtest_missing_external_prereqs_=\"\$test_subtest_missing_external_prereqs_ $name\" false }" } # Explicitly require external prerequisite. Useful when binary is # called indirectly (e.g. from emacs). # Returns success if dependency is available, failure otherwise. test_require_external_prereq () { binary="$1" if [ "$(eval echo -n \$test_missing_external_prereq_${binary}_)" = t ]; then # dependency is missing, call the replacement function to note it eval "$binary" else true fi } # You are not expected to call test_ok_ and test_failure_ directly, use # the text_expect_* functions instead. test_ok_ () { if test "$test_subtest_known_broken_" = "t"; then test_known_broken_ok_ "$@" return fi test_success=$(($test_success + 1)) say_color pass "%-6s" "PASS" echo " $@" } test_failure_ () { if test "$test_subtest_known_broken_" = "t"; then test_known_broken_failure_ "$@" return fi test_failure=$(($test_failure + 1)) test_failure_message_ "FAIL" "$@" test "$immediate" = "" || { GIT_EXIT_OK=t; exit 1; } return 1 } test_failure_message_ () { say_color error "%-6s" "$1" echo " $2" shift 2 echo "$@" | sed -e 's/^/ /' if test "$verbose" != "t"; then cat test.output; fi } test_known_broken_ok_ () { test_reset_state_ test_fixed=$(($test_fixed+1)) say_color pass "%-6s" "FIXED" echo " $@" } test_known_broken_failure_ () { test_reset_state_ test_broken=$(($test_broken+1)) test_failure_message_ "BROKEN" "$@" return 1 } test_debug () { test "$debug" = "" || eval "$1" } test_run_ () { test_cleanup=: if test "$verbose" != "t"; then exec 4>test.output 3>&4; fi eval >&3 2>&4 "$1" eval_ret=$? eval >&3 2>&4 "$test_cleanup" return 0 } test_skip () { test_count=$(($test_count+1)) to_skip= for skp in $IMPASS_SKIP_TESTS do case $this_test.$test_count in $skp) to_skip=t esac done if test -z "$to_skip" && test -n "$prereq" && ! test_have_prereq "$prereq" then to_skip=t fi case "$to_skip" in t) test_report_skip_ "$@" ;; *) test_check_missing_external_prereqs_ "$@" ;; esac } test_check_missing_external_prereqs_ () { if test -n "$test_subtest_missing_external_prereqs_"; then say_color skip >&1 "missing prerequisites:" echo "$test_subtest_missing_external_prereqs_" >&1 test_report_skip_ "$@" else false fi } test_report_skip_ () { test_reset_state_ say_color skip >&3 "skipping test:" echo " $@" >&3 say_color skip "%-6s" "SKIP" echo " $1" } test_subtest_known_broken () { test_subtest_known_broken_=t } test_expect_success () { test "$#" = 3 && { prereq=$1; shift; } || prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test-expect-success" test_reset_state_ if ! test_skip "$@" then test_run_ "$2" run_ret="$?" # test_run_ may update missing external prerequisites test_check_missing_external_prereqs_ "$@" || if [ "$run_ret" = 0 -a "$eval_ret" = 0 ] then test_ok_ "$1" else test_failure_ "$@" fi fi } test_expect_code () { test "$#" = 4 && { prereq=$1; shift; } || prereq= test "$#" = 3 || error "bug in the test script: not 3 or 4 parameters to test-expect-code" test_reset_state_ if ! test_skip "$@" then test_run_ "$3" run_ret="$?" # test_run_ may update missing external prerequisites, test_check_missing_external_prereqs_ "$@" || if [ "$run_ret" = 0 -a "$eval_ret" = "$1" ] then test_ok_ "$2" else test_failure_ "$@" fi fi } # test_external runs external test scripts that provide continuous # test output about their progress, and succeeds/fails on # zero/non-zero exit code. It outputs the test output on stdout even # in non-verbose mode, and announces the external script with "* run # : ..." before running it. When providing relative paths, keep in # mind that all scripts run in "trash directory". # Usage: test_external description command arguments... # Example: test_external 'Perl API' perl ../path/to/test.pl test_external () { test "$#" = 4 && { prereq=$1; shift; } || prereq= test "$#" = 3 || error >&5 "bug in the test script: not 3 or 4 parameters to test_external" descr="$1" shift test_reset_state_ if ! test_skip "$descr" "$@" then # Announce the script to reduce confusion about the # test output that follows. say_color "" " run $test_count: $descr ($*)" # Run command; redirect its stderr to &4 as in # test_run_, but keep its stdout on our stdout even in # non-verbose mode. "$@" 2>&4 if [ "$?" = 0 ] then test_ok_ "$descr" else test_failure_ "$descr" "$@" fi fi } # Like test_external, but in addition tests that the command generated # no output on stderr. test_external_without_stderr () { # The temporary file has no (and must have no) security # implications. tmp="$TMPDIR"; if [ -z "$tmp" ]; then tmp=/tmp; fi stderr="$tmp/git-external-stderr.$$.tmp" test_external "$@" 4> "$stderr" [ -f "$stderr" ] || error "Internal error: $stderr disappeared." descr="no stderr: $1" shift if [ ! -s "$stderr" ]; then rm "$stderr" test_ok_ "$descr" else if [ "$verbose" = t ]; then output=`echo; echo Stderr is:; cat "$stderr"` else output= fi # rm first in case test_failure exits. rm "$stderr" test_failure_ "$descr" "$@" "$output" fi } # This is not among top-level (test_expect_success) # but is a prefix that can be used in the test script, like: # # test_expect_success 'complain and die' ' # do something && # do something else && # test_must_fail git checkout ../outerspace # ' # # Writing this as "! git checkout ../outerspace" is wrong, because # the failure could be due to a segv. We want a controlled failure. test_must_fail () { "$@" test $? -gt 0 -a $? -le 129 -o $? -gt 192 } # test_cmp is a helper function to compare actual and expected output. # You can use it like: # # test_expect_success 'foo works' ' # echo expected >expected && # foo >actual && # test_cmp expected actual # ' # # This could be written as either "cmp" or "diff -u", but: # - cmp's output is not nearly as easy to read as diff -u # - not all diff versions understand "-u" test_cmp() { $GIT_TEST_CMP "$@" } # This function can be used to schedule some commands to be run # unconditionally at the end of the test to restore sanity: # # test_expect_success 'test core.capslock' ' # git config core.capslock true && # test_when_finished "git config --unset core.capslock" && # hello world # ' # # That would be roughly equivalent to # # test_expect_success 'test core.capslock' ' # git config core.capslock true && # hello world # git config --unset core.capslock # ' # # except that the greeting and config --unset must both succeed for # the test to pass. test_when_finished () { test_cleanup="{ $* } && (exit \"\$eval_ret\"); eval_ret=\$?; $test_cleanup" } test_done () { GIT_EXIT_OK=t test_results_dir="$TEST_DIRECTORY/test-results" mkdir -p "$test_results_dir" test_results_path="$test_results_dir/${0%.sh}-$$" echo "total $test_count" >> $test_results_path echo "success $test_success" >> $test_results_path echo "fixed $test_fixed" >> $test_results_path echo "broken $test_broken" >> $test_results_path echo "failed $test_failure" >> $test_results_path echo "" >> $test_results_path echo [ -n "$EMACS_SERVER" ] && test_emacs '(kill-emacs)' if [ "$test_failure" = "0" ]; then if [ "$test_broken" = "0" ]; then # This seems to be unfortunately necessary when # running tests on NFS rm -rf "$remove_tmp" 2>/dev/null || true rm -rf "$remove_tmp" fi exit 0 else exit 1 fi } test_python() { (echo "import sys; _orig_stdout=sys.stdout; sys.stdout=open('OUTPUT', 'w')"; cat) \ | python3 - } test_reset_state_ () { test -z "$test_init_done_" && test_init_ test_subtest_known_broken_= test_subtest_missing_external_prereqs_= } # called once before the first subtest test_init_ () { test_init_done_=t # skip all tests if there were external prerequisites missing during init test_check_missing_external_prereqs_ "all tests in $this_test" && test_done } # Test the binaries we have just built. The tests are kept in # test/ subdirectory and are run in 'trash directory' subdirectory. TEST_DIRECTORY=$(pwd) export PATH # Test repository test="tmp.$(basename "$0" .sh)" test -n "$root" && test="$root/$test" case "$test" in /*) TMP_DIRECTORY="$test" ;; *) TMP_DIRECTORY="$TEST_DIRECTORY/$test" ;; esac test ! -z "$debug" || remove_tmp=$TMP_DIRECTORY rm -fr "$test" || { GIT_EXIT_OK=t echo >&5 "FATAL: Cannot prepare test area" exit 1 } mkdir -p "${test}" # load local test library . lib/test-local.sh # Use -P to resolve symlinks in our working directory so that the cwd # in subprocesses like git equals our $PWD (for pathname comparisons). cd -P "$test" || error "Cannot setup test environment" if test "$verbose" = "t" then exec 4>&2 3>&1 else exec 4>test.output 3>&4 fi this_test=${0##*/} for skp in $IMPASS_SKIP_TESTS do to_skip= for skp in $IMPASS_SKIP_TESTS do case "$this_test" in $skp) to_skip=t esac done case "$to_skip" in t) say_color skip >&3 "skipping test $this_test altogether" say_color skip "skip all tests in $this_test" test_done esac done # Provide an implementation of the 'yes' utility yes () { if test $# = 0 then y=y else y="$*" fi while echo "$y" do : done } # Fix some commands on Windows case $(uname -s) in *MINGW*) # Windows has its own (incompatible) sort and find sort () { /usr/bin/sort "$@" } find () { /usr/bin/find "$@" } sum () { md5sum "$@" } # git sees Windows-style pwd pwd () { builtin pwd -W } # no POSIX permissions # backslashes in pathspec are converted to '/' # exec does not inherit the PID ;; *) test_set_prereq POSIXPERM test_set_prereq BSLASHPSPEC test_set_prereq EXECKEEPSPID ;; esac test -z "$NO_PERL" && test_set_prereq PERL test -z "$NO_PYTHON" && test_set_prereq PYTHON # test whether the filesystem supports symbolic links ln -s x y 2>/dev/null && test -h y 2>/dev/null && test_set_prereq SYMLINKS rm -f y impass-0.13.1/test/lib/test-local.sh000066400000000000000000000006261460454333500172330ustar00rootroot00000000000000# declare prerequisites for external binaries used in tests # test_declare_external_prereq python3 export LC_ALL=C.UTF-8 export SRC_DIRECTORY=$(cd "$TEST_DIRECTORY"/.. && pwd) export PYTHONPATH="$SRC_DIRECTORY":"$PYTHONPATH" export IMPASS_DB="$TMP_DIRECTORY"/db export GNUPGHOME="$TEST_DIRECTORY"/gnupg export IMPASS_KEYID=84DCED32C1D6E9DDF52C65D1B2D1C2C1E7EEC6DC impass() { python3 -m impass "$@" } impass-0.13.1/test/lib/test.expected-output/000077500000000000000000000000001460454333500207415ustar00rootroot00000000000000impass-0.13.1/test/lib/test.expected-output/test-verbose-no000066400000000000000000000011451460454333500237210ustar00rootroot00000000000000test-verbose: Testing the verbosity options of the test framework itself. PASS print something in test_expect_success and pass FAIL print something in test_expect_success and fail echo "hello stdout" && echo "hello stderr" >&2 && false hello stdout hello stderr PASS print something between test_begin_subtest and test_expect_equal and pass FAIL print something test_begin_subtest and test_expect_equal and fail --- test-verbose.4.expected 2010-11-14 21:41:12.738189710 +0000 +++ test-verbose.4.output 2010-11-14 21:41:12.738189710 +0000 @@ -1 +1 @@ -b +a hello stdout hello stderr impass-0.13.1/test/lib/test.expected-output/test-verbose-yes000066400000000000000000000012311460454333500241010ustar00rootroot00000000000000test-verbose: Testing the verbosity options of the test framework itself. hello stdout hello stderr PASS print something in test_expect_success and pass hello stdout hello stderr FAIL print something in test_expect_success and fail echo "hello stdout" && echo "hello stderr" >&2 && false hello stdout hello stderr PASS print something between test_begin_subtest and test_expect_equal and pass hello stdout hello stderr FAIL print something test_begin_subtest and test_expect_equal and fail --- test-verbose.4.expected 2010-11-14 21:41:06.650023289 +0000 +++ test-verbose.4.output 2010-11-14 21:41:06.650023289 +0000 @@ -1 +1 @@ -b +a impass-0.13.1/test/library000077500000000000000000000066611460454333500154610ustar00rootroot00000000000000#!/usr/bin/env bash test_description='library interface' . lib/test-lib.sh ################################################################ test_begin_subtest "create db" python3 - <&1 | sed "s|$IMPASS_DB|IMPASS_DB|" >OUTPUT import os from stat import * import impass db = impass.Database("$IMPASS_DB") print(db) print(db.version) db.add('foo@bar') print('foo@bar' in db) db.save('$IMPASS_KEYID') mode = os.stat("$IMPASS_DB")[ST_MODE] print(mode&(S_IRWXU + S_IRWXG + S_IRWXO) == (S_IRUSR | S_IWUSR) + 0 + 0) EOF cat <EXPECTED 1 True True EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "decrypt db" python3 - <&1 | sed "s|$IMPASS_DB|IMPASS_DB|" >OUTPUT import impass db = impass.Database("$IMPASS_DB", '$IMPASS_KEYID') print(db) print(db.version) print('foo@bar' in db) EOF cat <EXPECTED 1 True EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "decrypt db with bad signature" python3 - <&1 | sed "s|$IMPASS_DB|IMPASS_DB|" >OUTPUT import impass db = impass.Database("$IMPASS_DB", 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') print(db) print(db.version) print('foo@bar' in db) EOF cat <EXPECTED 1 True EOF test_expect_equal_file OUTPUT EXPECTED # change permission to make sure permissions are preserved after # re-writes chmod 610 "$IMPASS_DB" test_begin_subtest "add second entry" python3 - <&1 | sed "s|$IMPASS_DB|IMPASS_DB|" >OUTPUT import os from stat import * import impass db = impass.Database("$IMPASS_DB", '$IMPASS_KEYID') db.add('aaaa') print('aaaa' in db) for e in sorted(db): print(e) db.save() mode = os.stat("$IMPASS_DB")[ST_MODE] print(mode&(S_IRWXU + S_IRWXG + S_IRWXO) == int('610',8)) EOF cat <EXPECTED True aaaa foo@bar True EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "replace entry" python3 - <&1 | sed "s|$IMPASS_DB|IMPASS_DB|" >OUTPUT import impass db = impass.Database("$IMPASS_DB", '$IMPASS_KEYID') p1 = db['aaaa']['password'] db.replace("aaaa") print('aaaa' in db) p2 = db['aaaa']['password'] print(p1 == p2) db.save() EOF cat <EXPECTED True False EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "update entry" python3 - <&1 | sed "s|$IMPASS_DB|IMPASS_DB|" >OUTPUT import impass db = impass.Database("$IMPASS_DB", '$IMPASS_KEYID') p1 = db['aaaa']['password'] db.update('aaaa', 'bbbb') print('aaaa' in db) print('bbbb' in db) p2 = db['bbbb']['password'] print(p1 == p2) db.save() EOF cat <EXPECTED False True True EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "remove entry" python3 - <&1 | sed "s|$IMPASS_DB|IMPASS_DB|" >OUTPUT import impass db = impass.Database("$IMPASS_DB", '$IMPASS_KEYID') print('foo@bar' in db) db.remove('foo@bar') print('foo@bar' in db) db.save() EOF python3 - <&1 | sed "s|$IMPASS_DB|IMPASS_DB|" >>OUTPUT import impass db = impass.Database("$IMPASS_DB", '$IMPASS_KEYID') for e in db: print(e) EOF cat <EXPECTED True False bbbb EOF test_expect_equal_file OUTPUT EXPECTED test_begin_subtest "add accented context" python3 - <&1 | sed "s|$IMPASS_DB|IMPASS_DB|" >OUTPUT import impass db = impass.Database("$IMPASS_DB", '$IMPASS_KEYID') db.add('això') print('això' in db) for e in sorted(db): print(e) db.save() EOF cat <EXPECTED True això bbbb EOF test_expect_equal_file OUTPUT EXPECTED ################################################################ test_done impass-0.13.1/test/openpgp-data/000077500000000000000000000000001460454333500164355ustar00rootroot00000000000000impass-0.13.1/test/openpgp-data/ownertrust.txt000066400000000000000000000002371460454333500214340ustar00rootroot00000000000000# List of assigned trustvalues, created 2024-03-26 23:02:28-0400 # (Use "gpg --import-ownertrust" to restore them) 84DCED32C1D6E9DDF52C65D1B2D1C2C1E7EEC6DC:6: impass-0.13.1/test/openpgp-data/secring.gpg000066400000000000000000000151231460454333500205700ustar00rootroot00000000000000Ęf Ma$0i;z(L;@.@lډRVGV"^? u҇6av'ȲLk%%ݱSH\/B|9\c=L\0pLd^ɽ_yq%vN>H J|=Aؼtۏҭ6-<0vuyftI.`k=KG g6uNR,M+\{"Ȋ {π xiNX\b^\Q?̀t&A'=dVp1Zݝ^F*812z*ڇ9J:jΡ|:iB lÊ܇" 5 ΝF̘$< oA="J߶$=K@_Ҷ#Z@nbh;ϭo7n #H?`2CN$YWf#žI|&osPQZZK=^BN{y-#FI@<{xB!TgV$Wp)TJ(E?)I$'mC)y<{= x|:ro\[q$aIB%$Fm;Ӈv-Ƕ=>Fh\v,\}=8p"8e/Cak{0?1B]hY`Hmbѩev x.w8c4oU'CZ 3K:'?Yh*{?}vw-$9Q4.jJ 4W -`72/vKq)}&TbQJkli7Aqr;U}8H:w6w"I+ľHKIV0u>hI gZSO#IsMT Q  $!AY}*=$m{xw6=8r5ƀ|YsQ q|UKEۼM(Gi7PM@S LF)9V4++/qq$ܡabK^&auhesy@. ߑKG1 )6k(W]ULJ) ;x8q4$<37I U WT{[^fڧ 5RG@a߿w?eיlxWkl& ({I }f  G salt@notations.sequoia-pgp.org=&kLP) cwSC6JiJ !2,eѲ c2D$ڧ?`PơͦM,fEe7@ٍBw<ղtn6?k njcHtsK~SbE/aJ>Y7ϝܠ-ߧ;DaA+HnU4c =ShQ3*;r͙ CY52pv$CtpT sL1/HHVD.;g:i8k.> J~eP3u$dW}!5Nv2((D V4.t9jL79֩8Q eDɹ'g7x % j*b;ʇ I!v!ꙎhI>W"YW(^iY3zO5f8"bՍ@օ⹩{%Impass Test User L f  G salt@notations.sequoia-pgp.org1~n=5"2?Cib{bu !2,eѲ {H?F޷wT[mQ#h|`U<໕$a^j `/.)mnjQpXRsf|lj+N Pל!nχ]P.8'-+}庚_0‚H|FDy K6NC^[-8l, :ViNh\+@+^TL" z@ݬA"BCs%oel߁YOm ?ި锸$?l'es po@WE>(cY߀=lsDvء26Œ|(_X\nn}>6Ęf ۿhcHaP'YrbI ̽WI0  % DR87Ecӽ([Pi 7'Xuu'6l1*#(P4ct֚In\>Y١ ;a^3~lHADΣ8b@st23n2N@!a.e Tob;7A6 "U6w+C9?>ܫ{./:1 UAƇ6!mCyE2Xj .[߅=9W zEfRXļWȔ}R*xl&Kϧ+%mLZoWEփ iHxA_Է'10ehqiK+M3XF\fTD)&z)(z5OP؛0t\ГH p767 7vw~U1vDsBTS%@ aYv >%b\xlл5fǁxw\ FVADPң0d >G"n - alo-9!ԇ[=%'ْ,̖dAf@Z{ƱoJZ$<|IG$)@$P+TwݤU!) SJѡzb,[ oRPҌ:Ѥq ۣeJvnp=P3+sˊVL2RxI? Pb:4R)[B}TT\!v΃F}bo2TMJ&sk" ag/ ֕ wg6̈́R"Qێ ªPPbi W#KE,Mۜ^I%7-xB kC'@h)Fyq{3D { T7't'n&Nfq0@pRq7%Z7( r ûP/bCw/@wz8jAX,Cde5H'ѸD".& ste E캵3> rf G salt@notations.sequoia-pgp.orgLWޘW~MYL>GԞkTxb !2,eѲb 5͵5RXE:Vc+шjح f5-}%˖}gp_f8fW1YgYk&D4A3B78,-6$|3|DGfZX MXCquCc8J -#H%80{h5ʈ#ڕA16q qEK^w{_žB+2=NBjNY v)jA_GoyT=BmK({l2)F>Z-pmF[bJ *f`$W }NjrDy%"U*l7(]Ryh_5.˫NʨDqa*0:PzĘf 9g8!2u%cwyC9>rwCGo!T .R2$(6BC}/x'nJ2Cϵ 'HK_]#[ьU×rm^,ֺ@EN'uD)zU(4֙8Px|NL 5Z`a;xu^2>]6{);,DfLf[8dK" #\BFC~?|`gXUzz'jyqN3-Pmq~Q%8S _֞ |py/uWL{q6jz+6޻$U މN|Ÿ!n$X̡}&V`/xMKPs]) KꝵtiN,کΠuәR cΜPfc$>! wZUuPyp>xR XHv6fK' %_ On-s^y(HgA(8Qf- &YH杖ayNo{,ܠ[Ksy]}6Q?RUu5ܬW\:糑"F}y,tFL)Sf%+ڰGyu}'T4&N C6jũfhD / !YׁVfBR&@);M8Z*ֆF5lf>{ @Qdw9I91z \k'p804F3v+oh4Wd-uv`zQ>F`RpԝeJ$ ѷiK{ցIAѠNauHqP5fĝ1}qVК/*({ϣOzAuµcm)l28є4*-pʀ7_ ?<[Ay<@;8&ti56>~[vr͐ߦgM1=˟~7dW;CaaTRT!sqت@,!2,eѲ# r"PP_isdACU\ W٢$Pi{5UNFG1פ.s'04eXso:]`1Sfp I_GP'}՟o&2 && true ' test_expect_success 'print something in test_expect_success and fail' ' echo "hello stdout" && echo "hello stderr" >&2 && false ' test_begin_subtest 'print something between test_begin_subtest and test_expect_equal and pass' echo "hello stdout" echo "hello stderr" >&2 test_expect_equal "a" "a" test_begin_subtest 'print something test_begin_subtest and test_expect_equal and fail' echo "hello stdout" echo "hello stderr" >&2 test_expect_equal "a" "b" test_done