From 4e2a8d67970c65562e2afc1f7b2da7a5446867d5 Mon Sep 17 00:00:00 2001 From: Rudis Muiznieks Date: Sat, 9 Apr 2022 17:03:58 -0500 Subject: [PATCH] added bwmenu script --- bin/bwmenu | 500 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100755 bin/bwmenu diff --git a/bin/bwmenu b/bin/bwmenu new file mode 100755 index 0000000..a9c342d --- /dev/null +++ b/bin/bwmenu @@ -0,0 +1,500 @@ +#!/usr/bin/env bash +# Rofi extension for BitWarden-cli +NAME="$(basename "$0")" +VERSION="0.4" +DEFAULT_CLEAR=5 +BW_HASH= + +# Options +CLEAR=$DEFAULT_CLEAR # Clear password after N seconds (0 to disable) +SHOW_PASSWORD=no # Show part of the password in the notification +AUTO_LOCK=900 # 15 minutes, default for bitwarden apps + +# Holds the available items in memory +ITEMS= + +# Stores which command will be used to emulate keyboard type +AUTOTYPE_MODE= + +# Stores which command will be used to deal with clipboards +CLIPBOARD_MODE= + +# Specify what happens when pressing Enter on an item. +# Defaults to copy_password, can be changed to (auto_type all) or (auto_type password) +ENTER_CMD=copy_password + +# Keyboard shortcuts +KB_SYNC="Alt+r" +KB_URLSEARCH="Alt+u" +KB_NAMESEARCH="Alt+n" +KB_FOLDERSELECT="Alt+c" +KB_TOTPCOPY="Alt+t" +KB_LOCK="Alt+L" +KB_TYPEALL="Alt+1" +KB_TYPEUSER="Alt+2" +KB_TYPEPASS="Alt+3" + +# Item type classification +TYPE_LOGIN=1 +TYPE_NOTE=2 +TYPE_CARD=3 +TYPE_IDENTITY=4 + +# Populated in parse_cli_arguments +ROFI_OPTIONS=() +DEDUP_MARK="(+)" + +# Source helper functions +DIR="$(dirname "$(readlink -f "$0")")" +source "$DIR/lib-bwmenu" + +ask_password() { + mpw=$(printf '' | rofi -dmenu -p "Master Password" -password -lines 0) || exit $? + echo "$mpw" | bw unlock 2>/dev/null | grep 'export' | sed -E 's/.*export BW_SESSION="(.*==)"$/\1/' || exit_error $? "Could not unlock vault" +} + +get_session_key() { + if [ $AUTO_LOCK -eq 0 ]; then + keyctl purge user bw_session &>/dev/null + BW_HASH=$(ask_password) + else + if ! key_id=$(keyctl request user bw_session 2>/dev/null); then + session=$(ask_password) + [[ -z "$session" ]] && exit_error 1 "Could not unlock vault" + key_id=$(echo "$session" | keyctl padd user bw_session @u) + fi + + if [ $AUTO_LOCK -gt 0 ]; then + keyctl timeout "$key_id" $AUTO_LOCK + fi + BW_HASH=$(keyctl pipe "$key_id") + fi +} + +# source the hash file to gain access to the BitWarden CLI +# Pre fetch all the items +load_items() { + if ! ITEMS=$(bw list items --session "$BW_HASH" 2>/dev/null); then + exit_error $? "Could not load items" + fi +} + +exit_error() { + local code="$1" + local message="$2" + + rofi -e "$message" + exit "$code" +} + +# Show the Rofi menu with options +# Reads items from stdin +rofi_menu() { + + actions=( + -kb-custom-1 $KB_SYNC + -kb-custom-2 $KB_NAMESEARCH + -kb-custom-3 $KB_URLSEARCH + -kb-custom-4 $KB_FOLDERSELECT + -kb-custom-8 $KB_TOTPCOPY + -kb-custom-9 $KB_LOCK + ) + + msg="$KB_SYNC: sync | $KB_URLSEARCH: urls | $KB_NAMESEARCH: names | $KB_FOLDERSELECT: folders | $KB_TOTPCOPY: totp | $KB_LOCK: lock" + + [[ ! -z "$AUTOTYPE_MODE" ]] && { + actions+=( + -kb-custom-5 $KB_TYPEALL + -kb-custom-6 $KB_TYPEUSER + -kb-custom-7 $KB_TYPEPASS + ) + msg+=" +$KB_TYPEALL: Type all | $KB_TYPEUSER: Type user | $KB_TYPEPASS: Type pass" + } + + rofi -dmenu -p 'Name' \ + -i -no-custom \ + -mesg "$msg" \ + "${actions[@]}" \ + "${ROFI_OPTIONS[@]}" +} + +# Show items in a rofi menu by name of the item +show_items() { + if item=$( + echo "$ITEMS" \ + | jq -r ".[] | select( has( \"login\" ) ) | \"\\(.name)\"" \ + | dedup_lines \ + | rofi_menu + ); then + item_array="$(array_from_name "$item")" + "${ENTER_CMD[@]}" "$item_array" + else + rofi_exit_code=$? + item_array="$(array_from_name "$item")" + on_rofi_exit "$rofi_exit_code" "$item_array" + fi +} + +# Similar to show_items() but using the item's ID for deduplication +show_full_items() { + if item=$( + echo "$ITEMS" \ + | jq -r ".[] | select( has( \"login\" )) | \"\\(.id): name: \\(.name), username: \\(.login.username)\"" \ + | rofi_menu + ); then + item_id="$(echo "$item" | cut -d ':' -f 1)" + item_array="$(array_from_id "$item_id")" + "${ENTER_CMD[@]}" "$item_array" + else + rofi_exit_code=$? + item_id="$(echo "$item" | cut -d ':' -f 1)" + item_array="$(array_from_id "$item_id")" + on_rofi_exit "$rofi_exit_code" "$item_array" + fi +} + +# Show items in a rofi menu by url of the item +# if url occurs in multiple items, show the menu again with those items only +show_urls() { + if url=$( + echo "$ITEMS" \ + | jq -r '.[] | select(has("login")) | .login | select(has("uris")).uris | .[].uri' \ + | rofi_menu + ); then + item_array="$(bw list items --url "$url" --session "$BW_HASH")" + "${ENTER_CMD[@]}" "$item_array" + else + rofi_exit_code="$?" + item_array="$(bw list items --url "$url" --session "$BW_HASH")" + on_rofi_exit "$rofi_exit_code" "$item_array" + fi +} + +show_folders() { + folders=$(bw list folders --session "$BW_HASH") + if folder=$(echo "$folders" | jq -r '.[] | .name' | rofi_menu); then + + folder_id=$(echo "$folders" | jq -r ".[] | select(.name == \"$folder\").id") + + ITEMS=$(bw list items --folderid "$folder_id" --session "$BW_HASH") + show_items + else + rofi_exit_code="$?" + folder_id=$(echo "$folders" | jq -r ".[] | select(.name == \"$folder\").id") + item_array=$(bw list items --folderid "$folder_id" --session "$BW_HASH") + on_rofi_exit "$rofi_exit_code" "$item_array" + fi +} + +# re-sync the BitWarden items with the server +sync_bitwarden() { + bw sync --session "$BW_HASH" &>/dev/null || exit_error 1 "Failed to sync bitwarden" + + load_items + show_items +} + +# Evaluate the rofi exit codes +on_rofi_exit() { + case "$1" in + 10) sync_bitwarden;; + 11) load_items; show_items;; + 12) show_urls;; + 13) show_folders;; + 17) copy_totp "$2";; + 18) lock_vault;; + 14) auto_type all "$2";; + 15) auto_type username "$2";; + 16) auto_type password "$2";; + *) exit "$1";; + esac +} + +# Auto type using xdotool/ydotool +# $1: what to type; all, username, password +# $2: item array +auto_type() { + if not_unique "$2"; then + ITEMS="$2" + show_full_items + else + sleep 0.3 + case "$1" in + all) + type_word "$(echo "$2" | jq -r '.[0].login.username')" + type_tab + type_word "$(echo "$2" | jq -r '.[0].login.password')" + ;; + username) + type_word "$(echo "$2" | jq -r '.[0].login.username')" + ;; + password) + type_word "$(echo "$2" | jq -r '.[0].login.password')" + ;; + esac + fi + +} + +# Set $AUTOTYPE_MODE to a command that will emulate keyboard input +select_autotype_command() { + if [[ -z "$AUTOTYPE_MODE" ]]; then + if [ "$XDG_SESSION_TYPE" = "wayland" ] && hash ydotool 2>/dev/null; then + AUTOTYPE_MODE=(sudo ydotool) + elif [ "$XDG_SESSION_TYPE" != "wayland" ] && hash xdotool 2>/dev/null; then + AUTOTYPE_MODE=xdotool + fi + fi +} + +type_word() { + "${AUTOTYPE_MODE[@]}" type "$1" +} + +type_tab() { + "${AUTOTYPE_MODE[@]}" key Tab +} + + +# Set $CLIPBOARD_MODE to a command that will put stdin into the clipboard. +select_copy_command() { + if [[ -z "$CLIPBOARD_MODE" ]]; then + if [ "$XDG_SESSION_TYPE" = "wayland" ]; then + hash wl-copy 2>/dev/null && CLIPBOARD_MODE=wayland + elif hash xclip 2>/dev/null; then + CLIPBOARD_MODE=xclip + elif hash xsel 2>/dev/null; then + CLIPBOARD_MODE=xsel + fi + [ -z "$CLIPBOARD_MODE" ] && exit_error 1 "No clipboard command found. Please install either xclip, xsel, or wl-clipboard." + fi +} + +clipboard-set() { + clipboard-${CLIPBOARD_MODE}-set +} + +clipboard-get() { + clipboard-${CLIPBOARD_MODE}-get +} + +clipboard-clear() { + clipboard-${CLIPBOARD_MODE}-clear +} + +clipboard-xclip-set() { + xclip -selection clipboard -r +} + +clipboard-xclip-get() { + xclip -selection clipboard -o +} + +clipboard-xclip-clear() { + echo -n "" | xclip -selection clipboard -r +} + +clipboard-xsel-set() { + xsel --clipboard --input +} + +clipboard-xsel-get() { + xsel --clipboard +} + +clipboard-xsel-clear() { + xsel --clipboard --delete +} + +clipboard-wayland-set() { + wl-copy +} + +clipboard-wayland-get() { + wl-paste +} + +clipboard-wayland-clear() { + wl-copy --clear +} + +# Copy the password +# copy to clipboard and give the user feedback that the password is copied +# $1: json array of items +copy_password() { + if not_unique "$1"; then + ITEMS="$1" + show_full_items + else + pass="$(echo "$1" | jq -r '.[0].login.password')" + + show_copy_notification "$(echo "$1" | jq -r '.[0]')" + echo -n "$pass" | clipboard-set + + if [[ $CLEAR -gt 0 ]]; then + sleep "$CLEAR" + if [[ "$(clipboard-get)" == "$pass" ]]; then + clipboard-clear + fi + fi + fi +} + +# Copy the TOTP +# $1: item array +copy_totp() { + if not_unique "$1"; then + ITEMS="$item_array" + show_full_items + else + id=$(echo "$1" | jq -r ".[0].id") + + if ! totp=$(bw --session "$BW_HASH" get totp "$id"); then + exit_error 1 "$totp" + fi + + echo -n "$totp" | clipboard-set + notify-send "TOTP Copied" + fi +} + +# Lock the vault by purging the key used to store the session hash +lock_vault() { + keyctl purge user bw_session &>/dev/null +} + +# Show notification about the password being copied. +# $1: json item +show_copy_notification() { + local title + local body="" + local extra_options=() + + title="$(echo "$1" | jq -r '.name') copied" + + if [[ $SHOW_PASSWORD == "yes" ]]; then + pass=$(echo "$1" | jq -r '.login.password') + body="${pass:0:4}****" + fi + + if [[ $CLEAR -gt 0 ]]; then + body="$body
Will be cleared in ${CLEAR} seconds." + # Keep notification visible while the clipboard contents are active. + extra_options+=("-t" "$((CLEAR * 1000))") + fi + # not sure if icon will be present everywhere, /usr/share/icons is default icon location + notify-send "$title" "$body" "${extra_options[@]}" -i /usr/share/icons/hicolor/64x64/apps/bitwarden.png +} + +parse_cli_arguments() { + # Use GNU getopt to parse command line arguments + if ! ARGUMENTS=$(getopt -o c:C --long auto-lock:,clear:,no-clear,show-password,state-path:,help,version -- "$@"); then + exit_error 1 "Failed to parse command-line arguments" + fi + eval set -- "$ARGUMENTS" + + while true; do + case "$1" in + --help ) + cat <<-USAGE +$NAME $VERSION + +Usage: + $NAME [options] -- [rofi options] + +Options: + --help + Show this help text and exit. + + --version + Show version information and exit. + + --auto-lock + Automatically lock the Vault seconds after last unlock. + Use 0 to lock immediatly. + Use -1 to disable. + Default: 900 (15 minutes) + + -c , --clear , --clear= + Clear password from clipboard after this many seconds. + Defaults: ${DEFAULT_CLEAR} seconds. + + -C, --no-clear + Don't automatically clear the password from the clipboard. This disables + the default --clear option. + + --show-password + Show the first 4 characters of the copied password in the notification. + +Quick Actions: + When hovering over an item in the rofi menu, you can make use of Quick Actions. + + $KB_SYNC Resync your vault + + $KB_URLSEARCH Search through urls + $KB_NAMESEARCH Search through names + $KB_FOLDERSELECT Search through folders + + $KB_TOTPCOPY Copy the TOTP + $KB_TYPEALL Autotype the username and password [needs xdotool or ydotool] + $KB_TYPEUSER Autotype the username [needs xdotool or ydotool] + $KB_TYPEPASS Autotype the password [needs xdotool or ydotool] + + $KB_LOCK Lock your vault + +Examples: + # Default options work well + $NAME + + # Immediatly lock the Vault after use + $NAME --auto-lock 0 + + # Never lock the Vault + $NAME --auto-lock -1 + + # Place rofi on top of screen, like a Quake console + $NAME -- -location 2 +USAGE + shift + exit 0 + ;; + --version ) + echo "$NAME $VERSION" + shift + exit 0 + ;; + --auto-lock ) + AUTO_LOCK=$2 + shift 2 + ;; + -c | --clear ) + CLEAR="$2" + shift 2 + ;; + -C | --no-clear ) + CLEAR=0 + shift + ;; + --show-password ) + SHOW_PASSWORD=yes + shift + ;; + -- ) + shift + ROFI_OPTIONS=("$@") + break + ;; + * ) + exit_error 1 "Unknown option $1" + esac + done +} + +parse_cli_arguments "$@" + +get_session_key +select_autotype_command +select_copy_command +load_items +show_items