#!/bin/bash # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2021 Ayush Agarwal # # tessen - a data selection interface for pass on Wayland # ------------------------------------------------------------------------------ # don't leak password data if debug mode is enabled set +x # GLOBAL VARIABLES # variables which won't be changed and can be made readonly readonly tsn_version="1.2.3" readonly tsn_prefix="${PASSWORD_STORE_DIR:-$HOME/.password-store}" readonly tsn_cliptime="${PASSWORD_STORE_CLIP_TIME:-15}" readonly tsn_delay="${TESSEN_DELAY:-200}" # variables which hold data for possible actions and choices tsn_backend="${TESSEN_BACKEND-}" tsn_backend_opts="" tsn_action="${TESSEN_ACTION-}" tsn_userkey="${TESSEN_USERKEY:-user}" tsn_urlkey="${TESSEN_URLKEY:-url}" tsn_autokey="${TESSEN_AUTOKEY:-autotype}" tsn_autotype="" tsn_otp=false # variables with sensitive data which will be manually unset using _clear tsn_passfile="" declare -A tsn_passdata tsn_username="" tsn_password="" # FIRST MENU: generate a list of password store files, let the user select one get_pass_file() { local -a tmp_list # temporarily enable globbing, get the list of all gpg files recursively, # remove PASSWORD_STORE_DIR from the file names, and remove the '.gpg' suffix shopt -s nullglob globstar tmp_list=("$tsn_prefix"/**/*.gpg) tmp_list=("${tmp_list[@]#"$tsn_prefix"/}") tmp_list=("${tmp_list[@]%.gpg}") shopt -u nullglob globstar tsn_passfile="$(printf "%s\n" "${tmp_list[@]}" | "$tsn_backend" "$tsn_backend_opts")" if ! [[ -s "$tsn_prefix/$tsn_passfile".gpg ]]; then _die fi } # parse the password store file for username, password, otp, custom autotype, # and other key value pairs get_pass_data() { local -a passdata local keyval_regex otp_regex idx key val mapfile -t passdata < <(pass "$tsn_passfile" 2> /dev/null) if [[ "${#passdata[@]}" -eq 0 ]]; then _die "$tsn_passfile is empty" fi # the key can contain # alphanumerics, spaces, hyphen, underscore, plus, at, and hash # the value can contain # anything but it should be separated with a space from 'key:' keyval_regex='^[[:alnum:][:blank:]+#@_-]+:[[:blank:]].+$' # parse the 'otpauth://' URI # this regex is borrowed from pass-otp at commit 0aadd4c otp_regex='^otpauth:\/\/(totp|hotp)(\/(([^:?]+)?(:([^:?]*))?))?\?(.+)$' # the first line should contain the only the password tsn_password="${passdata[0]}" # each key should be unique # if non-unique keys are present, the value of the last non-unique key will # be considered # in addition, the following keys should be case insensitive and unique # 'username', 'autotype' for idx in "${passdata[@]:1}"; do key="${idx%%:*}" val="${idx#*: }" # keys with the case insensitive name 'password' are ignored if [[ "${key,,}" == "password" ]]; then continue elif [[ "${key,,}" == "$tsn_userkey" ]]; then tsn_username="$val" elif [[ "${key,,}" == "$tsn_autokey" ]]; then tsn_autotype="$val" elif [[ "$idx" =~ $otp_regex ]]; then tsn_otp=true elif [[ "$idx" =~ $keyval_regex ]]; then tsn_passdata["$key"]="$val" fi done # if $tsn_userkey isn't found, use the basename of file as username if [[ -z "$tsn_username" ]]; then tsn_username="${tsn_passfile##*/}" fi } # SECOND MENU: show a list of possible keys to choose from for auto typing or # copying # THIRD MENU: optional, this will show up if TESSEN_ACTION is blank get_key() { local -a key_arr local ch flag=false # the second menu if [[ "$1" == "key_list" ]]; then if [[ "$tsn_otp" == "true" ]]; then key_arr=("$tsn_autokey" "$tsn_userkey" "password" "otp" "${!tsn_passdata[@]}") else key_arr=("$tsn_autokey" "$tsn_userkey" "password" "${!tsn_passdata[@]}") fi # the (optional) third menu, depends on $tsn_action elif [[ "$1" == "option" ]]; then key_arr=("$tsn_autokey" "copy") elif [[ "$1" == "$tsn_urlkey" ]]; then key_arr=("open" "copy") fi # a dynamically scoped variable to hold the selected key for key_menu chosen_key="$(printf "%s\n" "${key_arr[@]}" | "$tsn_backend" "$tsn_backend_opts")" # validate the chosen key, if it doesn't exist, exit for ch in "${key_arr[@]}"; do if [[ "$chosen_key" == "$ch" ]]; then flag=true break fi done if [[ "$flag" == "false" ]]; then _die fi } # SECOND MENU: use 'get_key()' to show a list of possible keys to choose from key_menu() { get_key key_list case "$chosen_key" in "$tsn_autokey") auto_type_def ;; "$tsn_userkey") key_action "$tsn_username" ;; password) key_action "$tsn_password" ;; otp) key_otp ;; "$tsn_urlkey") key_url "${tsn_passdata["$tsn_urlkey"]}" ;; *) key_action "${tsn_passdata["$chosen_key"]}" ;; esac } # THIRD MENU: optional, use 'get_key()' and TESSEN_ACTION to show the option to # either auto type or copy the selected key key_action() { local arg="$1" # POTENTIAL IMPROVEMENT: used 'printf | wtype' instead of 'auto_type()' # because in all the other cases, 'auto_type()' is meant to exit but we don't # want to exit here case "$tsn_action" in autotype) auto_type "$arg" ;; copy) wld_copy "$arg" ;; both) printf "%s" "$arg" | wtype -s "$tsn_delay" - wld_copy "$arg" ;; "") get_key option if [[ "$chosen_key" == "$tsn_autokey" ]]; then auto_type "$arg" else wld_copy "$arg" fi ;; esac } # THIRD MENU: optional, this function is used if an 'otpauth://' URI is found key_otp() { local tmp_otp if ! pass otp -h > /dev/null 2>&1; then _die "pass-otp is not installed" fi tmp_otp="$(pass otp "$tsn_passfile")" if [[ "$tmp_otp" =~ ^[[:digit:]]+$ ]]; then get_key option if [[ "$chosen_key" == "$tsn_autokey" ]]; then auto_type "$tmp_otp" else wld_copy "$tmp_otp" fi else _die "invalid OTP detected" fi } # THIRD MENU: optional, this function is used if TESSEN_URLKEY is found. # Instead of showing 'autotype', it will show 'open'. # This function could've been combined with 'key_action()' but it would've # become a bit more complex than I like. key_url() { local arg="$1" case "$tsn_action" in autotype) key_open_url "$arg" || _die _clear ;; copy) wld_copy "$arg" ;; both) key_open_url "$arg" wld_copy "$arg" ;; "") get_key "$tsn_urlkey" if [[ "$chosen_key" == "open" ]]; then key_open_url "$arg" || _die _clear else wld_copy "$arg" fi ;; esac } # use either xdg-open or $BROWSER to open the selected URL key_open_url() { if is_installed xdg-open; then xdg-open "$1" 2> /dev/null || { printf "%s\n" "xdg-open was unable to open '$1'" >&2 return 1 } elif [[ -n "$BROWSER" ]] && is_installed "$BROWSER"; then "$BROWSER" "$1" > /dev/null 2>&1 || { printf "%s\n" "$BROWSER was unable to open '$1'" >&2 return 1 } else _die "Failed to open the $tsn_urlkey" fi } # SECOND MENU: the default autotype function, either autotype the username and # password or the custom autotype defined by the user # POTENTIAL IMPROVEMENT: Anything better than this ugly hack of # else..for..case..if..else..if? auto_type_def() { local word tmp_otp if [[ -z "$tsn_autotype" ]]; then printf "%s" "$tsn_username" | wtype -s "$tsn_delay" - wtype -s "$tsn_delay" -k Tab -- printf "%s" "$tsn_password" | wtype -s "$tsn_delay" - else for word in $tsn_autotype; do case "$word" in ":delay") sleep 1 ;; ":tab") wtype -s "$tsn_delay" -k Tab -- ;; ":space") wtype -s "$tsn_delay" -k space -- ;; ":enter") wtype -s "$tsn_delay" -k Return -- ;; ":otp") if ! pass otp -h > /dev/null 2>&1; then _die "pass-otp is not installed" else tmp_otp="$(pass otp "$tsn_passfile")" if [[ "$tmp_otp" =~ ^[[:digit:]]+$ ]]; then printf "%s" "$tmp_otp" | wtype -s "$tsn_delay" - else _die "invalid OTP detected" fi fi ;; path | basename | filename) printf "%s" "${tsn_passfile##*/}" | wtype -s "$tsn_delay" - ;; "$tsn_userkey") printf "%s" "$tsn_username" | wtype -s "$tsn_delay" - ;; pass | password) printf "%s" "$tsn_password" | wtype -s "$tsn_delay" - ;; *) if [[ -n "${tsn_passdata["$word"]}" ]]; then printf "%s" "${tsn_passdata["$word"]}" | wtype -s "$tsn_delay" - else wtype -s "$tsn_delay" -k space -- fi ;; esac done fi _clear exit 0 } auto_type() { printf "%s" "$1" | wtype -s "$tsn_delay" - _clear exit 0 } # POTENTIAL IMPROVEMENT: We could restore the clipboard as it was before pass # was used. This is done by default by pass. wld_copy() { printf "%s" "$1" | wl-copy if is_installed notify-send; then notify-send -t $((tsn_cliptime * 1000)) "Copied username to clipboard. Will clear in $tsn_cliptime seconds." fi { sleep "$tsn_cliptime" || exit 1 wl-copy --clear } > /dev/null 2>&1 & unset -v tsn_passfile tsn_username tsn_password tsn_passdata chosen_key } print_help() { printf "%s\n" "${0##*/} - autotype and copy data from password-store on wayland" "" printf "%s\n" "Usage: ${0##*/} [options]" "" printf "%s\n" " ${0##*/} use an available dmenu interface and either autotype OR copy data" printf "%s\n" " ${0##*/} -b rofi use rofi and either autotype OR copy data" printf "%s\n" " ${0##*/} -b rofi -a autotype use rofi and always autotype data" printf "%s\n" " ${0##*/} -b rofi -a copy use rofi and always copy data" printf "%s\n" " ${0##*/} -b rofi -a both use rofi and always autotype AND copy data" "" printf "%s\n" " -b, --backend, --backend= choose either 'bemenu', 'rofi', or 'wofi' as backend" printf "%s\n" " -a, --action, --action= choose either 'autotype', 'copy', or 'both'" printf "%s\n" " -h, --help print this help menu" printf "%s\n" " -v, --version print the version of ${0##*/}" "" printf "%s\n" "For more details and additional features, please read the man page of tessen(1)" printf "%s\n" "For reporting bugs or feedback, visit https://github.com/ayushnix/tessen" } is_installed() { if command -v "$1" > /dev/null 2>&1; then return 0 else return 1 fi } set_bemenu() { local -a bmn_opt=("-i -l 10 -w --scrollbar=autohide -n") readonly tsn_backend="bemenu" # use BEMENU_OPTS if set by the user, otherwise use $bmn_opt export BEMENU_OPTS="${BEMENU_OPTS:-${bmn_opt[*]}}" # manually using bemenu options doesn't seem to work readonly tsn_backend_opts="" } set_rofi() { readonly tsn_backend="rofi" tsn_backend_opts="-dmenu" } set_wofi() { readonly tsn_backend="wofi" tsn_backend_opts="-d" } assign_backend() { if is_installed "bemenu"; then set_bemenu elif is_installed "rofi"; then set_rofi elif is_installed "wofi"; then set_wofi else _die "Please install either 'bemenu', 'rofi', or 'wofi' to use ${0##*/}" fi } validate_backend() { case "$1" in bemenu) set_bemenu ;; rofi) set_rofi ;; wofi) set_wofi ;; *) _die "Please specify a valid backend: bemenu | rofi | wofi" ;; esac } validate_cliptime() { local clip_regex clip_regex="^[[:digit:]]+$" if [[ "$tsn_cliptime" =~ $clip_regex ]]; then return 0 else _die "Invalid clipboard time provided" fi } validate_action() { case "$1" in autotype) readonly tsn_action="autotype" ;; copy) readonly tsn_action="copy" ;; both) readonly tsn_action="both" ;; "") readonly tsn_action="" ;; *) _die "Please specify a valid action: autotype | copy | both" ;; esac } _clear() { wl-copy --clear unset -v tsn_passfile tsn_username tsn_password tsn_passdata chosen_key } _die() { if [[ -n "$1" ]]; then printf "%s\n" "$1" >&2 fi _clear exit 1 } main() { local _opt if ! [[ -d "$tsn_prefix" ]]; then _die "password store directory not found" fi while [[ "$#" -gt 0 ]]; do _opt="$1" case "$_opt" in -b | --backend) if [[ "$#" -lt 2 ]] || ! is_installed "$2"; then _die "Please specify a valid backend: bemenu | rofi | wofi" fi validate_backend "$2" shift ;; --backend=*) if ! is_installed "${_opt##--backend=}"; then _die "Please specify a valid backend: bemenu | rofi | wofi" fi validate_backend "${_opt##--backend=}" ;; -a | --action) if [[ "$#" -lt 2 ]]; then _die "Please specify a valid action: autotype | copy | both" fi validate_action "$2" shift ;; --action=*) validate_action "${_opt##--action=}" ;; -h | --help) print_help exit 0 ;; -v | --version) printf "%s\n" "${0##*/} version $tsn_version" exit 0 ;; --) shift break ;; *) _die "invalid argument detected" ;; esac shift done unset -v _opt if [[ -z "$tsn_backend" ]]; then assign_backend fi validate_cliptime readonly tsn_action trap '_clear' EXIT TERM get_pass_file get_pass_data key_menu trap - EXIT TERM } main "$@"