#!/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.1.2" 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 exit 1 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") # 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 } # get the key that the user will choose to autotype or copy get_key() { local -a key_arr local ch flag=false if [[ "$1" == "pass_key_list" ]]; then key_arr=("autotype username and password" "username" "password" "${!tsn_passdata[@]}") shift elif [[ "$1" == "opt_key_list" ]]; then key_arr=("autotype" "copy") shift else exit 1 fi # a dynamically scoped variable to hold the key selection for key_menu chosen_key="$(printf '%s\n' "${key_arr[@]}" | "$tsn_backend" "$tsn_backend_opts")" # validate the chosen key name for ch in "${key_arr[@]}"; do if [[ "$chosen_key" == "$ch" ]]; then flag=true break fi done if [[ "$flag" == "false" ]]; then exit 1 fi } # the 2nd, and possibly 3rd, stage of the menu key_menu() { local tmp_key get_key pass_key_list if [[ "$chosen_key" == "autotype username and password" ]]; then auto_type username_password exit 0 fi if [[ "$tsn_action" == "autotype" ]]; then auto_type "$chosen_key" exit 0 elif [[ "$tsn_action" == "copy" ]]; then wld_copy "$chosen_key" elif [[ "$tsn_action" == "both" ]]; then auto_type "$chosen_key" wld_copy "$chosen_key" elif [[ -z "$tsn_action" ]]; then tmp_key="$chosen_key" get_key opt_key_list if [[ "$chosen_key" == "autotype" ]]; then auto_type "$tmp_key" exit 0 elif [[ "$chosen_key" == "copy" ]]; then wld_copy "$tmp_key" else exit 1 fi else printf '%s\n' "Please specify a valid option: autotype|copy|both" >&2 exit 1 fi } auto_type() { if [[ "$1" == "username_password" ]]; then printf '%s' "$tsn_username" | wtype -s 100 - wtype -s 100 -k Tab -- printf '%s' "$tsn_password" | wtype -s 100 - shift elif [[ "$1" == "username" ]]; then printf '%s' "$tsn_username" | wtype -s 100 - shift elif [[ "$1" == "password" ]]; then printf '%s' "$tsn_password" | wtype -s 100 - shift elif [[ -n "${tsn_passdata[$1]}" ]]; then printf '%s' "${tsn_passdata[$1]}" | wtype -s 100 - shift else exit 1 fi } wld_copy() { if [[ "$1" == "username" ]]; then printf '%s' "$tsn_username" | wl-copy notify-send -t $((tsn_cliptime * 1000)) "Copied username to clipboard. Will clear in $tsn_cliptime seconds." || true shift clean elif [[ "$1" == "password" ]]; then printf '%s' "$tsn_password" | wl-copy notify-send -t $((tsn_cliptime * 1000)) "Copied password to clipboard. Will clear in $tsn_cliptime seconds." || true shift clean elif [[ -n "${tsn_passdata[$1]}" ]]; then printf '%s' "${tsn_passdata[$1]}" | wl-copy notify-send -t $((tsn_cliptime * 1000)) "Copied $1 to clipboard. Will clear in $tsn_cliptime seconds." || true shift clean else exit 1 fi } 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 bemenu 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 'bemenu', 'rofi', or 'wofi' as backend (default: bemenu)" printf '%s\n' " -a, --action, --action= choose '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, visit https://github.com/ayushnix/tessen" } validate_backend() { local -a bmn_opt if [[ "$tsn_backend" == "bemenu" ]]; then bmn_opt=("-i -l 10 -w --scrollbar=autohide -n") export BEMENU_OPTS="${BEMENU_OPTS:-${bmn_opt[*]}}" readonly tsn_backend="bemenu" readonly tsn_backend_opts="" elif [[ "$tsn_backend" == "rofi" ]]; then readonly tsn_backend="rofi" readonly tsn_backend_opts="-dmenu" elif [[ "$tsn_backend" == "wofi" ]]; then readonly tsn_backend="wofi" readonly tsn_backend_opts="-d" else printf '%s\n' "Please specify a backend: bemenu|rofi|wofi" >&2 exit 1 fi } validate_cliptime() { local clip_regex clip_regex="^[[:digit:]]+$" if [[ "$tsn_cliptime" =~ $clip_regex ]]; then return 0 else printf '%s\n' "invalid clipboard time provided" >&2 exit 1 fi } clean() { { sleep "$tsn_cliptime" || exit 1 wl-copy --clear } > /dev/null 2>&1 & disown unset -v tsn_passfile tsn_username tsn_password tsn_passdata } die() { wl-copy --clear unset -v tsn_passfile tsn_username tsn_password tsn_passdata } main() { local _opt # exit if the password store directory doesn't exist if ! [[ -d "$tsn_prefix" ]]; then printf '%s\n' "password store not found" >&2 exit 1 fi # parse any options given by the user while [[ "$#" -gt 0 ]]; do _opt="$1" case "$_opt" in -b | --backend) [[ "$#" -lt 2 ]] && { printf '%s\n' "Please specify a backend: bemenu|rofi|wofi" >&2 exit 1 } tsn_backend="$2" validate_backend shift ;; --backend=*) tsn_backend="${_opt##--backend=}" validate_backend ;; -a | --action) [[ "$#" -lt 2 ]] && { printf '%s\n' "Please specify a valid option: autotype|copy|both" >&2 exit 1 } tsn_action="$2" shift ;; --action=*) tsn_action="${_opt##--action=}" ;; -h | --help) print_help exit 0 ;; -v | --version) printf '%s\n' "${0##*/} version $tsn_version" exit 0 ;; --) shift break ;; *) printf '%s\n' "invalid argument detected" >&2 exit 1 ;; esac shift done unset -v _opt if unset -v tsn_backend_opts 2> /dev/null; then validate_backend fi validate_cliptime readonly tsn_action trap 'die' EXIT TERM get_pass_file get_pass_data key_menu trap - EXIT TERM } main "$@"