#!/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