#!/bin/bash # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2021 Ayush Agarwal # # tessen - a data selection interface for pass on Wayland # ------------------------------------------------------------------------------ # shell "strict" mode set -uo pipefail readonly PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin" export PATH umask 077 # don't leak password data if debug mode is enabled set +x # initialize the global variables readonly VERSION="1.1.2" readonly PREFIX="${PASSWORD_STORE_DIR:-$HOME/.password-store}" readonly CLIP_TIME="${PASSWORD_STORE_CLIP_TIME:-15}" BACKEND="${TESSEN_BACKEND:-bemenu}" # uses the value of TESSEN_BACKEND if set by user BACKEND_OPTS="" ACTION="${TESSEN_ACTION-}" # uses the value of TESSEN_ACTION if set by user PASSFILE="" declare -A PASSDATA_ARR USERNAME="" PASSWORD="" # display and get the shortened path of the password file get_pass_file() { local tmp_pass_1 tmp_pass_2 tmp_pass_3 # temporarily enable globbing to get the list of gpg files shopt -s nullglob globstar tmp_pass_1=("$PREFIX"/**/*.gpg) tmp_pass_2=("${tmp_pass_1[@]#"$PREFIX"/}") tmp_pass_3=("${tmp_pass_2[@]%.gpg}") shopt -u nullglob globstar PASSFILE="$(printf '%s\n' "${tmp_pass_3[@]}" | "$BACKEND" "$BACKEND_OPTS")" if ! [[ -e "$PREFIX/$PASSFILE".gpg ]]; then exit 1 fi } # get the password data including every key-value pair inside the encrypted file get_pass_data() { local passdata passdata_regex idx key val mapfile -t passdata < <(pass "$PASSFILE") # ASSUMPTION: the key can contain alphanumerics, spaces, hyphen, underscore # the value can contain anything but it has to follow after a space passdata_regex="^[[:alnum:][:blank:]_-]+:[[:blank:]].+$" # ASSUMPTION: the basename of the gpg file is the username although one can still # select a username field inside the file, if it exists USERNAME="${PASSFILE##*/}" # ASSUMPTION: the first line of $PASSFILE will contain the password PASSWORD="${passdata[0]}" # skip the password, validate each entry against $passdata_regex, store valid results # ASSUMPTION: each key is unique otherwise, the value of the last non-unique key will be used for idx in "${passdata[@]:1}"; do if [[ "${idx%%:*}" != "username" && "${idx%%:*}" != "password" && "$idx" =~ $passdata_regex ]]; then key="${idx%%:*}" val="${idx#*: }" PASSDATA_ARR["$key"]="$val" else continue fi done } # get the key that the user will choose to autotype or copy get_key() { local ch="" flag=false key_arr=() if [[ "${1-}" == "pass_key_list" ]]; then key_arr=("autotype username and password" "username" "password" "${!PASSDATA_ARR[@]}") shift elif [[ "${1-}" == "opt_key_list" ]]; then key_arr=("autotype" "copy") shift else exit 1 fi _KEY="$(printf '%s\n' "${key_arr[@]}" | "$BACKEND" "$BACKEND_OPTS")" # validate the chosen key name for ch in "${key_arr[@]}"; do if [[ "$_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 [[ "$_KEY" == "autotype username and password" ]]; then auto_type username_password exit 0 fi if [[ "${ACTION-}" == "autotype" ]]; then auto_type "$_KEY" exit 0 elif [[ "${ACTION-}" == "copy" ]]; then wld_copy "$_KEY" elif [[ "${ACTION-}" == "both" ]]; then auto_type "$_KEY" wld_copy "$_KEY" elif [[ -z "${ACTION-}" ]]; then tmp_key="$_KEY" get_key opt_key_list if [[ "$_KEY" == "autotype" ]]; then auto_type "$tmp_key" exit 0 elif [[ "$_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' "$USERNAME" | wtype -s 100 - wtype -s 100 -k Tab -- printf '%s' "$PASSWORD" | wtype -s 100 - shift elif [[ "${1-}" == "username" ]]; then printf '%s' "$USERNAME" | wtype -s 100 - shift elif [[ "${1-}" == "password" ]]; then printf '%s' "$PASSWORD" | wtype -s 100 - shift elif [[ -n "${PASSDATA_ARR[${1-}]}" ]]; then printf '%s' "${PASSDATA_ARR[${1-}]}" | wtype -s 100 - shift else exit 1 fi } wld_copy() { if [[ "${1-}" == "username" ]]; then printf '%s' "$USERNAME" | wl-copy notify-send -t $((CLIP_TIME * 1000)) "Copied username to clipboard. Will clear in $CLIP_TIME seconds." || true shift clean elif [[ "${1-}" == "password" ]]; then printf '%s' "$PASSWORD" | wl-copy notify-send -t $((CLIP_TIME * 1000)) "Copied password to clipboard. Will clear in $CLIP_TIME seconds." || true shift clean elif [[ -n "${PASSDATA_ARR[${1-}]}" ]]; then printf '%s' "${PASSDATA_ARR[${1-}]}" | wl-copy notify-send -t $((CLIP_TIME * 1000)) "Copied ${1-} to clipboard. Will clear in $CLIP_TIME 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 bmn_opt=() if [[ "$BACKEND" == "bemenu" ]]; then bmn_opt=("-i -l 10 -w --scrollbar=autohide -n") export BEMENU_OPTS="${BEMENU_OPTS:-${bmn_opt[*]}}" readonly BACKEND="bemenu" readonly BACKEND_OPTS="" elif [[ "$BACKEND" == "rofi" ]]; then readonly BACKEND="rofi" readonly BACKEND_OPTS="-dmenu" elif [[ "$BACKEND" == "wofi" ]]; then readonly BACKEND="wofi" readonly BACKEND_OPTS="-d" else printf '%s\n' "Please specify a backend: bemenu|rofi|wofi" >&2 exit 1 fi } validate_clip_time() { local clip_regex clip_regex="^[[:digit:]]+$" if [[ "$CLIP_TIME" =~ $clip_regex ]]; then return 0 else printf '%s\n' "invalid clipboard time provided" >&2 exit 1 fi } clean() { { sleep "$CLIP_TIME" || exit 1 wl-copy --clear } > /dev/null 2>&1 & disown unset -v PASSFILE USERNAME PASSWORD PASSDATA_ARR } die() { wl-copy --clear unset -v PASSFILE USERNAME PASSWORD PASSDATA_ARR } main() { local _opt # exit if the password store directory doesn't exist if ! [[ -d "$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 } BACKEND="${2-}" validate_backend shift ;; --backend=*) BACKEND="${_opt##--backend=}" validate_backend ;; -a | --action) [[ "$#" -lt 2 ]] && { printf '%s\n' "Please specify a valid option: autotype|copy|both" >&2 exit 1 } ACTION="${2-}" shift ;; --action=*) ACTION="${_opt##--action=}" ;; -h | --help) print_help exit 0 ;; -v | --version) printf '%s\n' "${0##*/} version $VERSION" exit 0 ;; --) shift break ;; *) printf '%s\n' "invalid argument detected" >&2 exit 1 ;; esac shift done unset -v _opt if unset -v BACKEND_OPTS 2> /dev/null; then validate_backend fi validate_clip_time readonly ACTION trap 'die' EXIT TERM get_pass_file get_pass_data key_menu trap - EXIT TERM } main "$@"