passrofi 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. #!/usr/bin/env bash
  2. #
  3. # passrofi is a password-store UI using rofi
  4. # Copyright (C) 2020 Distopico <distopico@riseup.net>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. #
  19. # Description:
  20. # --------------
  21. # Simple password-store aka 'pass' UI for rofi or dmenu
  22. #
  23. # Usage:
  24. # ---------------
  25. # * rofi mode: rofi -modi "pass:passrofi" -show
  26. # * rofi dmenu: /path/to/script/passrofi
  27. #
  28. shopt -s nullglob globstar
  29. # Options
  30. COPY_PASS="copy pass"
  31. COPY_OTP="copy otp"
  32. COPY_FIELD="copy field"
  33. SHOW_FIELD="show field"
  34. COPY_ACTION="copy"
  35. SHOW_ACTION="copy"
  36. QUIT_NAME="quit ›"
  37. RETURN_NAME="‹ return"
  38. OPTIONS=( "$COPY_PASS|*" "$COPY_OTP|•" "$COPY_FIELD|»" "$SHOW_FIELD|›" )
  39. ACTIONS=( "$COPY_ACTION|‣" "$SHOW_ACTION|▹" )
  40. # Setup
  41. BASE_PATH=$(dirname "$0")
  42. PASS_CMD=${PASSWORD_STORE_CMD:-"$BASE_PATH/passp"}
  43. PASS_CLIP_TIME=${PASSWORD_STORE_CLIP_TIME:-45}
  44. PASS_ROOT_DIR=${PASSWORD_STORE_DIR-~/.password-store}
  45. PASS_X_SELECTION=${PASSWORD_STORE_X_SELECTION:-clipboard}
  46. PASS_ROFI_FIELDS_SEPARATOR=${PASSWORD_STORE_ROFI_FIELDS_SEPARATOR:-":"}
  47. NOTIFY_CMD="notify-send"
  48. PROMPT="pass"
  49. BASE64="base64"
  50. # Check commands
  51. has_notify=0
  52. rofi_mode=0
  53. if [[ -x "$(command -v $NOTIFY_CMD)" ]]; then
  54. has_notify=1
  55. fi
  56. if ! [[ -x "$(command -v pgrep)" ]]; then
  57. # Set pgrep fallback
  58. function pgrep() {
  59. ps axf | grep $1 | grep -v grep | awk '{print $1}'
  60. }
  61. fi
  62. if ! [[ -z "$(pgrep "rofi")" ]]; then
  63. rofi_mode=1
  64. fi
  65. rofi_dmenu () {
  66. rofi -dmenu -p $PROMPT "$@"
  67. }
  68. # main menu
  69. main_menu () {
  70. for opt in "${OPTIONS[@]}"
  71. do
  72. IFS='|' read -ra val <<< "$opt"
  73. printf "%s\n" "${val[0]}"
  74. done
  75. }
  76. # Clip with the available command in X or Wayland
  77. clip() {
  78. if [[ -n $WAYLAND_DISPLAY ]]; then
  79. local copy_cmd=( wl-copy )
  80. local paste_cmd=( wl-paste -n )
  81. if [[ $PASS_X_SELECTION == primary ]]; then
  82. copy_cmd+=( --primary )
  83. paste_cmd+=( --primary )
  84. fi
  85. local display_name="$WAYLAND_DISPLAY"
  86. elif [[ -n $DISPLAY ]]; then
  87. local copy_cmd=( xclip -selection "$PASS_X_SELECTION" )
  88. local paste_cmd=( xclip -o -selection "$PASS_X_SELECTION" )
  89. local display_name="$DISPLAY"
  90. else
  91. die "Error: No X11 or Wayland display detected"
  92. fi
  93. local sleep_argv0="password store sleep on display $display_name"
  94. # This base64 is because bash cannot store binary data in a shell variable
  95. pkill -f "^$sleep_argv0" 2>/dev/null && sleep 0.5
  96. local before="$("${paste_cmd[@]}" 2>/dev/null | $BASE64)"
  97. echo -n "$1" | "${copy_cmd[@]}" >/dev/null 2>&1
  98. (
  99. ( exec -a "$sleep_argv0" bash <<<"trap 'kill %1' TERM; sleep '$PASS_CLIP_TIME' & wait" )
  100. local now="$("${paste_cmd[@]}" | $BASE64)"
  101. [[ $now != $(echo -n "$1" | $BASE64) ]] && before="$now"
  102. echo "$before" | $BASE64 -d | "${copy_cmd[@]}"
  103. ) >/dev/null 2>&1 & disown
  104. }
  105. # Get identifier type
  106. get_option_type () {
  107. local options=("${OPTIONS[@]}", "${ACTIONS[@]}")
  108. value=""
  109. for opt in "${options[@]}"
  110. do
  111. IFS='|' read -ra val <<< "$opt"
  112. if [[ "$1" == "${val[0]}" ]] || [[ "$1" == "${val[1]}" ]]; then
  113. if [[ "$2" == "identifie" ]]; then
  114. value=${val[1]}
  115. else
  116. value=${val[0]}
  117. fi
  118. break
  119. fi
  120. done
  121. echo "$value"
  122. }
  123. # Get passwords list
  124. get_pass_list () {
  125. password_files=( "$PASS_ROOT_DIR"/**/*.gpg )
  126. password_files=( "${password_files[@]#"$PASS_ROOT_DIR"/}" )
  127. password_files=( "${password_files[@]%.gpg}" )
  128. identifier=$(get_option_type "$@" "identifie")
  129. printf "$RETURN_NAME\n"
  130. printf "$identifier| %s\n" "${password_files[@]}"
  131. }
  132. # Call copy command
  133. copy_pass () {
  134. IFS='|' read -ra val <<< "$@"
  135. action=$(get_option_type "${val[0]}")
  136. password="$(echo -e "${val[1]}" | sed -e 's/^[[:space:]]*//')"
  137. should_notify=0
  138. response=1
  139. [[ -n $password ]] || exit
  140. case "${action}" in
  141. "$COPY_PASS")
  142. $PASS_CMD -c "$password" >/dev/null 2>&1
  143. should_notify=1
  144. response=$?
  145. ;;
  146. "$COPY_OTP")
  147. $PASS_CMD otp -c "$password" >/dev/null 2>&1
  148. should_notify=1
  149. response=$?
  150. ;;
  151. "$COPY_FIELD")
  152. pass_field=$($PASS_CMD show "$password")
  153. response=$?
  154. pass_key_value=$(printf '%s\n' "${pass_field}" | tail -n+2)
  155. return_opt="$RETURN_NAME"
  156. empty_msg="not have values"
  157. # Check if password not have additional fields
  158. if [[ -z "$pass_key_value" ]]; then
  159. if [[ $rofi_mode -eq 1 ]]; then
  160. echo -en "\0message\x1f<b>$password</b>: $empty_msg\n"
  161. printf "%s\n" "$return_opt"
  162. else
  163. printf "%s\n" "$password: $empty_msg | $RETURN_NAME"
  164. fi
  165. exit 0
  166. fi
  167. # Return opts
  168. printf "%s\n" "$return_opt"
  169. # Show pass fields
  170. local line=0
  171. while read -r LINE; do
  172. line_key="${LINE%%: *}"
  173. line_val="${LINE#* }"
  174. content="$line_key: $line_val"
  175. if [[ $line_key =~ "otpauth://" ]]; then
  176. # exclude OTP value/secret
  177. continue
  178. fi
  179. ((line++))
  180. if [[ $line_key = $line_val ]]; then
  181. content="$line_val"
  182. fi
  183. printf "‣| $line) %s [$password]\n" "$content"
  184. done < <(printf "%s\n" "${pass_key_value}")
  185. ;;
  186. "$COPY_ACTION")
  187. local data=$(echo "$password" | sed 's/\([[:digit:]]\+\))[[:space:]]\+\(.*\)[[:space:]]\+\[\([^]]*\)\]/\1,\2,\3/')
  188. IFS="," read -ra field_data <<< "$data"
  189. IFS="$PASS_ROFI_FIELDS_SEPARATOR" read -ra field_values <<< "${field_data[1]}"
  190. local line=$(("${field_data[0]}" + 1))
  191. local field_len=${#field_values[@]}
  192. password="${field_data[2]}"
  193. # Field name or line to show on notification
  194. field=" (<b>${field_values[0]}</b>)"
  195. if [[ $field_len -lt 2 ]]; then
  196. field=" (<b>line: $line</b>)"
  197. fi
  198. # Copy non-password entry without field type
  199. # e.g. "email: myname@email.com" -> "myname@email.com"
  200. if [[ $line -gt 1 ]]; then
  201. # trim leading/trailing spaces
  202. local copy_value=$(echo "${field_values[1]}" | sed 's,^ *,,; s, *$,,')
  203. clip echo "$copy_value"
  204. fi
  205. should_notify=1
  206. response=$?
  207. ;;
  208. *)
  209. response=1
  210. ;;
  211. esac
  212. if [[ $response -eq 1 ]]; then
  213. if [[ $rofi_mode -eq 1 ]]; then
  214. echo -en "\0message\x1f<b>$action</b>: error copying\n"
  215. printf "$QUIT_NAME\n"
  216. else
  217. printf "$action: error copying | $QUIT_NAME\n"
  218. fi
  219. elif [[ $has_notify -eq 1 ]] && [[ $should_notify -eq 1 ]]; then
  220. $NOTIFY_CMD "pass" "Copied <b>$password</b>$field to clipboard. Will clear in $PASS_CLIP_TIME seconds." >/dev/null 2>&1
  221. fi
  222. }
  223. call_action () {
  224. if [[ $rofi_mode -eq 1 ]]; then
  225. if ! [[ -z $(get_option_type "$@") ]]; then
  226. get_pass_list "$@"
  227. else
  228. copy_pass "$@"
  229. fi
  230. else
  231. call_dmenu "$@"
  232. fi
  233. }
  234. call_dmenu () {
  235. action=$(main_menu | rofi_dmenu "$@")
  236. [[ -n $action ]] || exit
  237. password=$(get_pass_list "$action" | rofi_dmenu "$@")
  238. [[ -n $password ]] || exit
  239. result=$(copy_pass "$password")
  240. if [[ "$result" ]]; then
  241. echo "$result" | rofi_dmenu "$@"
  242. fi
  243. }
  244. call_main () {
  245. # Show as rofi mode or dmenu
  246. if [[ $rofi_mode -eq 1 ]]; then
  247. echo -en "\0message\x1f\n" # reset message
  248. echo -en "\x00prompt\x1f$PROMPT\n"
  249. main_menu
  250. else
  251. call_dmenu
  252. fi
  253. }
  254. # Inputs
  255. if [[ x"$@" = x"$QUIT_NAME" ]]; then
  256. exit 0
  257. elif [[ x"$@" = x"$RETURN_NAME" ]]; then
  258. call_main
  259. elif [[ "$@" ]]; then
  260. call_action "$@"
  261. else
  262. call_main
  263. fi