From cae35f2bfccccecb868e9f9722619ad915150373 Mon Sep 17 00:00:00 2001 From: "Manuel Amador (Rudd-O)" Date: Tue, 7 Dec 2021 19:54:48 +0000 Subject: [PATCH] Support clipboard and QR code functionality in pass get / generate / get-or-generate. --- bin/qvm-pass | 212 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 164 insertions(+), 48 deletions(-) diff --git a/bin/qvm-pass b/bin/qvm-pass index 812cbbc..5325165 100755 --- a/bin/qvm-pass +++ b/bin/qvm-pass @@ -8,6 +8,11 @@ import signal import subprocess import sys +try: + from pipes import quote +except ImportError: + from shlex import quote + signal.signal(signal.SIGINT, signal.SIG_DFL) @@ -18,11 +23,11 @@ usage = "\n".join( "", " qvm-pass [-d ] [subcommand] [arguments...]", "", - "subcommands:", + "subcommands (try qvm-pass subcommand --help for more):", "", " [ls|list|show]", " Retrieves the list of keys from the pass store.", - " [show] ", + " [show] [-c] ", " Retrieves a key from the pass store.", " generate [-n] [-f] [pass-length]", " Retrieves a key from the pass store; creates the key", @@ -52,6 +57,87 @@ usage = "\n".join( ) +# The following code was lifted from the pass program to support +# identical functionality. +shell_functions = """ +X_SELECTION="${PASSWORD_STORE_X_SELECTION:-clipboard}" +CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-45}" + +clip() { + if [[ -n $WAYLAND_DISPLAY ]]; then + local copy_cmd=( wl-copy ) + local paste_cmd=( wl-paste -n ) + if [[ $X_SELECTION == primary ]]; then + copy_cmd+=( --primary ) + paste_cmd+=( --primary ) + fi + local display_name="$WAYLAND_DISPLAY" + elif [[ -n $DISPLAY ]]; then + local copy_cmd=( xclip -selection "$X_SELECTION" ) + local paste_cmd=( xclip -o -selection "$X_SELECTION" ) + local display_name="$DISPLAY" + else + die "Error: No X11 or Wayland display detected" + fi + local sleep_argv0="password store sleep on display $display_name" + + # This base64 business is because bash cannot store binary data in a shell + # variable. Specifically, it cannot store nulls nor (non-trivally) store + # trailing new lines. + pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5 + local before="$("${paste_cmd[@]}" 2>/dev/null | $BASE64)" + echo -n "$1" | "${copy_cmd[@]}" || die "Error: Could not copy data to the clipboard" + ( + ( exec -a "$sleep_argv0" bash <<<"trap 'kill %1' TERM; sleep '$CLIP_TIME' & wait" ) + local now="$("${paste_cmd[@]}" | $BASE64)" + [[ $now != $(echo -n "$1" | $BASE64) ]] && before="$now" + + # It might be nice to programatically check to see if klipper exists, + # as well as checking for other common clipboard managers. But for now, + # this works fine -- if qdbus isn't there or if klipper isn't running, + # this essentially becomes a no-op. + # + # Clipboard managers frequently write their history out in plaintext, + # so we axe it here: + qdbus org.kde.klipper /klipper org.kde.klipper.klipper.clearClipboardHistory &>/dev/null + + echo "$before" | $BASE64 -d | "${copy_cmd[@]}" + ) >/dev/null 2>&1 & disown + echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds." +} + +qrcode() { + if [[ -n $DISPLAY || -n $WAYLAND_DISPLAY ]]; then + if type feh >/dev/null 2>&1; then + echo -n "$1" | qrencode --size 10 -o - | feh -x --title "pass: $2" -g +200+200 - + return + elif type gm >/dev/null 2>&1; then + echo -n "$1" | qrencode --size 10 -o - | gm display -title "pass: $2" -geometry +200+200 - + return + elif type display >/dev/null 2>&1; then + echo -n "$1" | qrencode --size 10 -o - | display -title "pass: $2" -geometry +200+200 - + return + fi + fi + echo -n "$1" | qrencode -t utf8 +} +""" + + +def pass_frontend_shell(cmd): + global shell_functions + quoted_cmd = shell_functions + "\n\n" + " ".join(quote(x) for x in cmd) + return subprocess.call(["bash", "-c", quoted_cmd]) + + +def clip(data, path): + return pass_frontend_shell(["clip", data, path]) + + +def qrcode(data, path): + return pass_frontend_shell(["qrcode", data, path]) + + parser_for_discrimination = argparse.ArgumentParser(description="(nobody sees this)") parser_for_discrimination.add_argument( "-d", @@ -141,6 +227,22 @@ for p in ["get-or-generate", "generate"]: help="no symbols in generated password", default=False, ) +for p in ["show", "get-or-generate", "generate"]: + _parsers[p].add_argument( + "-c", + "--clip", + action="store_true", + help="copy password to clipboard instead of displaying onscreen", + default=False, + ) + _parsers[p].add_argument( + "-q", + "--qrcode", + action="store_true", + help="display password as QR code", + default=False, + ) + for p in ["mv", "cp", "rm", "insert", "generate"]: _parsers[p].add_argument( "-f", @@ -226,11 +328,14 @@ def pass_manage(*args, **kwargs): arguments = sys.argv[1:] -global_opts, args = parser_for_discrimination.parse_known_args(arguments) -if len(global_opts.arguments) == 0: - arguments = ["ls"] + arguments -elif len(global_opts.arguments) == 1 and global_opts.arguments[0] not in subcommands: - arguments = ["show"] + arguments +if not "--help" in arguments and not "-h" in arguments and not "-?" in arguments: + global_opts, args = parser_for_discrimination.parse_known_args(arguments) + if len(global_opts.arguments) == 0: + arguments = ["ls"] + arguments + elif ( + len(global_opts.arguments) == 1 and global_opts.arguments[0] not in subcommands + ): + arguments = ["show"] + arguments opts = parser_for_subcommands.parse_args(arguments) @@ -253,7 +358,17 @@ if opts.subcommand == "ls" or (opts.subcommand == "show" and opts.key is None): sys.exit(pass_read("list")) elif opts.subcommand == "show": # User requested a password, or show with an argument. - sys.exit(pass_read("get", opts.key)) + if opts.clip or opts.qrcode: + ret, stdout = pass_read("get", opts.key, return_stdout=True) + if ret != 0: + sys.exit(ret) + stdout = stdout.decode("utf-8") + if opts.clip: + sys.exit(clip(stdout, opts.key)) + elif opts.qrcode: + sys.exit(qrcode(stdout, opts.key)) + else: + sys.exit(pass_read("get", opts.key)) elif opts.subcommand in ("mv", "cp"): if not opts.force and sys.stdin.isatty(): with open(os.devnull, "w") as null: @@ -278,57 +393,58 @@ elif opts.subcommand == "rm": else: sys.exit(1) sys.exit(pass_manage(opts.subcommand, opts.key)) -elif opts.subcommand == "get-or-generate": - with open(os.devnull, "w") as null: - ret, stdout = pass_read("get", opts.key, return_stdout=True, stderr=null) - if ret == 8: - # Not there. - with open(os.devnull, "w") as null: - ret = pass_manage( - "generate", - opts.key, - str(int(opts.no_symbols)), - str(int(opts.pass_length)), - stdout=null, - ) - if ret != 0: - sys.exit(ret) - sys.exit(pass_read("get", opts.key)) - elif ret == 0: - # There. - sys.stdout.buffer.write(stdout) - sys.exit(ret) - else: - # Woops. - sys.exit(ret) -elif opts.subcommand == "generate": - doit = lambda: sys.exit( - pass_manage( - opts.subcommand, +elif opts.subcommand in ("get-or-generate", "generate"): + + def doit(): + kwargs = {"return_stdout": True} if (opts.clip or opts.qrcode) else {} + ret = pass_manage( + "generate", opts.key, str(int(opts.no_symbols)), str(int(opts.pass_length)), + **kwargs, ) - ) + if not kwargs: + sys.exit(ret) + if opts.clip or opts.qrcode: + ret, stdout = pass_read("get", opts.key, **kwargs) + if ret != 0: + sys.exit(ret) + if opts.clip: + sys.exit(clip(stdout.decode("utf-8"), opts.key)) + elif opts.qrcode: + sys.exit(qrcode(stdout.decode("utf-8"), opts.key)) + sys.exit(ret) + with open(os.devnull, "w") as null: - ret = pass_read("get", opts.key, stdout=null, stderr=null) + ret, stdout = pass_read("get", opts.key, return_stdout=True, stderr=null) + if ret == 8: # Not there. doit() elif ret == 0: - # There: - if not opts.force and sys.stdin.isatty(): - sys.stderr.write( - "An entry already exists for %s. Overwrite it? [y/N] " % (opts.key,) - ) - ans = sys.stdin.readline().strip() - if ans and ans[0] in "yY": - doit() + if opts.subcommand == "get-or-generate": + if opts.clip: + sys.exit(clip(stdout.decode("utf-8"), opts.key)) + elif opts.qrcode: + sys.exit(qrcode(stdout.decode("utf-8"), opts.key)) else: - sys.exit(1) - else: - doit() + sys.stdout.buffer.write(stdout) + sys.exit(ret) + else: # generate + if not opts.force and sys.stdin.isatty(): + sys.stderr.write( + "An entry already exists for %s. Overwrite it? [y/N] " % (opts.key,) + ) + ans = sys.stdin.readline().strip() + if ans and ans[0] in "yY": + doit() + else: + sys.exit(1) + else: + doit() else: + # Woops. sys.exit(ret) elif opts.subcommand == "insert": if not opts.force and sys.stdin.isatty():