diff --git a/bwmenu b/bwmenu index a9c342d..b6888f4 100755 --- a/bwmenu +++ b/bwmenu @@ -17,11 +17,11 @@ ITEMS= AUTOTYPE_MODE= # Stores which command will be used to deal with clipboards -CLIPBOARD_MODE= +CLIPBOARD_MODE=wayland # 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 +# Defaults to copy_password, can be changed to (auto_type all) or (auto_type password) or show_item_actions +ENTER_CMD=${ENTER_CMD:=copy_password} # Keyboard shortcuts KB_SYNC="Alt+r" @@ -48,26 +48,37 @@ DEDUP_MARK="(+)" DIR="$(dirname "$(readlink -f "$0")")" source "$DIR/lib-bwmenu" +ask_login() { + email=$(rofi -dmenu -p "User Email" -lines 0) || exit $? + mpw=$(rofi -dmenu -p "Master Password" -password -lines 0) || exit $? + echo "$mpw" | bw login "$email" 2>/dev/null | grep 'export' | sed -E 's/.*export BW_SESSION="(.*==)"$/\1/' || display_then_exit $? "Could not log in" +} + 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" + echo "$mpw" | bw unlock 2>/dev/null | grep 'export' | sed -E 's/.*export BW_SESSION="(.*==)"$/\1/' || display_then_exit $? "Could not unlock vault" } get_session_key() { - if [ $AUTO_LOCK -eq 0 ]; then + if [[ $AUTO_LOCK -eq 0 ]]; then keyctl purge user bw_session &>/dev/null - BW_HASH=$(ask_password) + fi + + if key_id=$(keyctl request user bw_session 2>/dev/null); then + BW_HASH=$(keyctl pipe "$key_id") + elif [[ "$(bw status | jq '.userEmail')" == null ]]; then + BW_HASH=$(ask_login) 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 + BW_HASH=$(ask_password) + fi + + [[ -z "$BW_HASH" ]] && display_then_exit 1 "Could not unlock vault" + + if [[ $AUTO_LOCK -ne 0 ]]; then + key_id=$(echo "$BW_HASH" | keyctl padd user bw_session @u) + if [[ $AUTO_LOCK -gt 0 ]]; then keyctl timeout "$key_id" $AUTO_LOCK fi - BW_HASH=$(keyctl pipe "$key_id") fi } @@ -75,11 +86,11 @@ get_session_key() { # 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" + display_then_exit $? "Could not load items" fi } -exit_error() { +display_then_exit() { local code="$1" local message="$2" @@ -189,7 +200,7 @@ show_folders() { # re-sync the BitWarden items with the server sync_bitwarden() { - bw sync --session "$BW_HASH" &>/dev/null || exit_error 1 "Failed to sync bitwarden" + bw sync --session "$BW_HASH" &>/dev/null || display_then_exit 1 "Failed to sync bitwarden" load_items show_items @@ -266,8 +277,12 @@ select_copy_command() { CLIPBOARD_MODE=xclip elif hash xsel 2>/dev/null; then CLIPBOARD_MODE=xsel + elif hash wl-copy 2>/dev/null; then + CLIPBOARD_MODE=wayland + else + display_then_exit 1 "No clipboard command found. Please install either xclip, xsel, or wl-clipboard." fi - [ -z "$CLIPBOARD_MODE" ] && exit_error 1 "No clipboard command found. Please install either xclip, xsel, or wl-clipboard." + [ -z "$CLIPBOARD_MODE" ] && display_then_exit 1 "No clipboard command found. Please install either xclip, xsel, or wl-clipboard." fi } @@ -275,50 +290,157 @@ clipboard-set() { clipboard-${CLIPBOARD_MODE}-set } +clipboard-set-primary() { + clipboard-${CLIPBOARD_MODE}-set-primary +} + clipboard-get() { clipboard-${CLIPBOARD_MODE}-get } +clipboard-get-primary() { + clipboard-${CLIPBOARD_MODE}-get-primary +} + clipboard-clear() { clipboard-${CLIPBOARD_MODE}-clear } +clipboard-clear-primary() { + clipboard-${CLIPBOARD_MODE}-clear-primary +} + clipboard-xclip-set() { xclip -selection clipboard -r } +clipboard-xclip-set-primary() { + xclip -selection primary -r +} + clipboard-xclip-get() { xclip -selection clipboard -o } +clipboard-xclip-get-primary() { + xclip -selection primary -o +} + clipboard-xclip-clear() { echo -n "" | xclip -selection clipboard -r } +clipboard-xclip-clear-primary() { + echo -n "" | xclip -selection primary -r +} + clipboard-xsel-set() { xsel --clipboard --input } +clipboard-xsel-set-primary() { + xsel --clipboard --input --primary +} + clipboard-xsel-get() { xsel --clipboard } +clipboard-xsel-get-primary() { + xsel --primary +} + clipboard-xsel-clear() { xsel --clipboard --delete } +clipboard-xsel-clear-primary() { + xsel --primary --delete +} + clipboard-wayland-set() { wl-copy } +clipboard-wayland-set-primary() { + wl-copy --primary +} + clipboard-wayland-get() { wl-paste } +clipboard-wayland-get-primary() { + wl-paste --primary +} + clipboard-wayland-clear() { wl-copy --clear } +clipboard-wayland-clear-primary() { + wl-copy --clear --primary +} + +show_item_actions() { + if not_unique "$1"; then + ITEMS="$1" + show_full_items + else + actions="$(actions_for_item "$1")" + if selection="$(cut -d : -f 1 <<< "$actions" | select_action)"; then + selected_action="$(sed "$(( $selection + 1 ))q;d" <<< "$actions")" + eval "$(cut -d : -f 3- <<< "$selected_action")" + show_copy_notification "$(cut -d : -f 2 <<< "$selected_action")" + fi + fi +} + +actions_for_item() { + actions=() + if username="$(jq -re ".[0].login.username" <<< "$1")"; then + actions+=("Copy username:Username copied to clipboard:copy_and_clear clipboard ${username@Q}") + [ -n "$AUTOTYPE_MODE" ] && actions+=("Type username::auto_type username ${1@Q}") + fi + if password="$(jq -re ".[0].login.password" <<< "$1")"; then + actions+=("Copy password:Password copied to clipboard:copy_and_clear clipboard ${password@Q}") + [ -n "$AUTOTYPE_MODE" ] && actions+=("Type password::auto_type password ${1@Q}") + fi + if [ -n "$password" ] && [ -n "$username" ] ; then + actions+=("Copy username and password:Username copied to clipboard<br>Password copied to primary selection: \ + copy_and_clear clipboard ${username@Q} \ + && copy_and_clear primary ${password@Q}") + [ -n "$AUTOTYPE_MODE" ] && actions+=("Type username and password::auto_type all ${1@Q}") + fi + if uri="$(jq -re 'map(.login.uris[0].uri | select(. != null)) | first' <<< "$1")" ; then + actions+=("Open URI::xdg-open ${uri@Q}") + if [ -n "$password" ] && [ -n "$username" ] ; then + actions+=("Copy username and password, open URI:Username copied to clipboard<br>Password copied to primary selection: \ + copy_and_clear clipboard ${username@Q} \ + && copy_and_clear primary ${password@Q} \ + && xdg-open ${uri@Q}") + elif [ -n "$password" ] ; then + actions+=("Copy password, open URI:Password copied to clipboard:copy_and_clear ${password@Q} && xdg-open ${uri@Q}") + fi + fi + if jq -e '.[0].login.totp' > /dev/null <<< "$1" ; then + actions+=("Copy TOTP:TOTP copied to clipboard:_copy_totp ${1@Q}") + fi + declare -a "custom_field_actions=($(jq -re '.[0].fields | map("Copy " + .name + ":" + .name + " copied to clipboard:copy_and_clear " + (.value | @sh)) | @sh' <<< "$1"))" + for action in "${custom_field_actions[@]}" ; do + actions+=("$action") + done + if notes="$(jq -re '.[0].notes' <<< "$1")" ; then + actions+=("Display notes::display_then_exit 0 ${notes@Q}") + fi + actions+=("Display everything::_show_everything ${1@Q}") + printf '%s\n' "${actions[@]}" +} + +select_action() { + rofi -dmenu -p "Action" "$1" -format i -i -no-custom "${ROFI_OPTIONS[@]}" +} + # Copy the password # copy to clipboard and give the user feedback that the password is copied # $1: json array of items @@ -330,15 +452,18 @@ copy_password() { pass="$(echo "$1" | jq -r '.[0].login.password')" show_copy_notification "$(echo "$1" | jq -r '.[0]')" - echo -n "$pass" | clipboard-set + copy_and_clear clipboard "$pass" + fi +} - if [[ $CLEAR -gt 0 ]]; then - sleep "$CLEAR" - if [[ "$(clipboard-get)" == "$pass" ]]; then - clipboard-clear - fi - fi +_copy_totp() { + id=$(echo "$1" | jq -r ".[0].id") + + if ! totp=$(bw --session "$BW_HASH" get totp "$id"); then + display_then_exit 1 "$totp" fi + + copy_and_clear clipboard "$totp" } # Copy the TOTP @@ -348,49 +473,65 @@ copy_totp() { ITEMS="$item_array" show_full_items else - id=$(echo "$1" | jq -r ".[0].id") + _copy_totp "$1" + notify-send "TOTP Copied" + fi +} - if ! totp=$(bw --session "$BW_HASH" get totp "$id"); then - exit_error 1 "$totp" +copy_and_clear() { + if [ "$1" = "primary" ]; then + clipboard-set-primary <<< "$2" + if [[ $CLEAR -gt 0 ]]; then + ( + sleep "$CLEAR" + if [[ "$(clipboard-get-primary)" == "$2" ]]; then + clipboard-clear-primary + fi + ) & + fi + else + clipboard-set <<< "$2" + if [[ $CLEAR -gt 0 ]]; then + ( + sleep "$CLEAR" + if [[ "$(clipboard-get)" == "$2" ]]; then + clipboard-clear + fi + ) & 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_everything() { + display_then_exit 0 "$( + jq -re '.[0] | + .URIs = (.login.uris | map(.uri)) | + .Username = .login.username | + .Password = .login.password | + reduce .fields[] as $field (.; .[$field.name] = $field.value) | + del(.id, .folderId, .object, .revisionDate, .type, .favorite, .login, .fields) | + reduce (tostream | select(length==2)) as $i ({}; .[[$i[0][] | tostring] | join(" ")] = $i[1]) | + to_entries | + map(if (.value | length) > 0 then .key + ": " + .value else empty end) | + join("\n") + ' <<< "$1" + )" } -# 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 + notify-send "$1" -i /usr/share/icons/hicolor/64x64/apps/bitwarden.png +} + +# Lock the vault by purging the key used to store the session hash +lock_vault() { + keyctl purge user bw_session &>/dev/null } 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" + display_then_exit 1 "Failed to parse command-line arguments" fi eval set -- "$ARGUMENTS" @@ -440,7 +581,7 @@ Quick Actions: $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: @@ -486,7 +627,7 @@ USAGE break ;; * ) - exit_error 1 "Unknown option $1" + display_then_exit 1 "Unknown option $1" esac done }