skynet/bin/bwmenu

500 lines
12 KiB
Bash
Executable file

#!/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="<b>$KB_SYNC</b>: sync | <b>$KB_URLSEARCH</b>: urls | <b>$KB_NAMESEARCH</b>: names | <b>$KB_FOLDERSELECT</b>: folders | <b>$KB_TOTPCOPY</b>: totp | <b>$KB_LOCK</b>: lock"
[[ ! -z "$AUTOTYPE_MODE" ]] && {
actions+=(
-kb-custom-5 $KB_TYPEALL
-kb-custom-6 $KB_TYPEUSER
-kb-custom-7 $KB_TYPEPASS
)
msg+="
<b>$KB_TYPEALL</b>: Type all | <b>$KB_TYPEUSER</b>: Type user | <b>$KB_TYPEPASS</b>: 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="<b>$(echo "$1" | jq -r '.name')</b> 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<br>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 <SECONDS>
Automatically lock the Vault <SECONDS> seconds after last unlock.
Use 0 to lock immediatly.
Use -1 to disable.
Default: 900 (15 minutes)
-c <SECONDS>, --clear <SECONDS>, --clear=<SECONDS>
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