summaryrefslogtreecommitdiff
path: root/tessen
blob: 57877e63d49245bfc4f522133ced0b78629f41e4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2021 Ayush Agarwal <ayush at fastmail dot in>
#
# tessen - a data selection interface for pass on Wayland
# ------------------------------------------------------------------------------

# don't leak password data if debug mode is enabled
set +x

# GLOBAL VARIABLES
# variables which won't be changed and can be made readonly
readonly tsn_version="1.2.2"
readonly tsn_prefix="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
readonly tsn_cliptime="${PASSWORD_STORE_CLIP_TIME:-15}"
readonly tsn_delay="${TESSEN_DELAY:-200}"
# variables which hold data for possible actions and choices
tsn_backend="${TESSEN_BACKEND-}"
tsn_backend_opts=""
tsn_action="${TESSEN_ACTION-}"
tsn_userkey="${TESSEN_USERKEY:-user}"
tsn_urlkey="${TESSEN_URLKEY:-url}"
tsn_autokey="${TESSEN_AUTOKEY:-autotype}"
tsn_autotype=""
tsn_otp=false
# variables with sensitive data which will be manually unset using _clear
tsn_passfile=""
declare -A tsn_passdata
tsn_username=""
tsn_password=""

# FIRST MENU: generate a list of password store files, let the user select one
get_pass_file() {
  local -a tmp_list

  # temporarily enable globbing, get the list of all gpg files recursively,
  # remove PASSWORD_STORE_DIR from the file names, and remove the '.gpg' suffix
  shopt -s nullglob globstar
  tmp_list=("$tsn_prefix"/**/*.gpg)
  tmp_list=("${tmp_list[@]#"$tsn_prefix"/}")
  tmp_list=("${tmp_list[@]%.gpg}")
  shopt -u nullglob globstar

  tsn_passfile="$(printf "%s\n" "${tmp_list[@]}" | "$tsn_backend" "$tsn_backend_opts")"

  if ! [[ -s "$tsn_prefix/$tsn_passfile".gpg ]]; then
    _die
  fi
}

# parse the password store file for username, password, otp, custom autotype,
# and other key value pairs
get_pass_data() {
  local -a passdata
  local keyval_regex otp_regex idx key val

  mapfile -t passdata < <(pass "$tsn_passfile" 2> /dev/null)
  if [[ "${#passdata[@]}" -eq 0 ]]; then
    _die "$tsn_passfile is empty"
  fi

  # the key can contain
  # alphanumerics, spaces, hyphen, underscore, plus, at, and hash
  # the value can contain
  # anything but it should be separated with a space from 'key:'
  keyval_regex='^[[:alnum:][:blank:]+#@_-]+:[[:blank:]].+$'
  # parse the 'otpauth://' URI
  # this regex is borrowed from pass-otp at commit 0aadd4c
  otp_regex='^otpauth:\/\/(totp|hotp)(\/(([^:?]+)?(:([^:?]*))?))?\?(.+)$'

  # the first line should contain the only the password
  tsn_password="${passdata[0]}"
  # each key should be unique
  # if non-unique keys are present, the value of the last non-unique key will
  # be considered
  # in addition, the following keys should be case insensitive and unique
  # 'username', 'autotype'
  for idx in "${passdata[@]:1}"; do
    key="${idx%%:*}"
    val="${idx#*: }"
    # keys with the case insensitive name 'password' are ignored
    if [[ "${key,,}" == "password" ]]; then
      continue
    elif [[ "${key,,}" == "$tsn_userkey" ]]; then
      tsn_username="$val"
    elif [[ "${key,,}" == "$tsn_autokey" ]]; then
      tsn_autotype="$val"
    elif [[ "$idx" =~ $otp_regex ]]; then
      tsn_otp=true
    elif [[ "$idx" =~ $keyval_regex ]]; then
      tsn_passdata["$key"]="$val"
    fi
  done

  # if $tsn_userkey isn't found, use the basename of file as username
  if [[ -z "$tsn_username" ]]; then
    tsn_username="${tsn_passfile##*/}"
  fi
}

# SECOND MENU: show a list of possible keys to choose from for auto typing or
# copying
# THIRD MENU: optional, this will show up if TESSEN_ACTION is blank
get_key() {
  local -a key_arr
  local ch flag=false

  # the second menu
  if [[ "$1" == "key_list" ]]; then
    if [[ "$tsn_otp" == "true" ]]; then
      key_arr=("$tsn_autokey" "$tsn_userkey" "password" "otp" "${!tsn_passdata[@]}")
    else
      key_arr=("$tsn_autokey" "$tsn_userkey" "password" "${!tsn_passdata[@]}")
    fi
  # the (optional) third menu, depends on $tsn_action
  elif [[ "$1" == "option" ]]; then
    key_arr=("$tsn_autokey" "copy")
  elif [[ "$1" == "$tsn_urlkey" ]]; then
    key_arr=("open" "copy")
  fi

  # a dynamically scoped variable to hold the selected key for key_menu
  chosen_key="$(printf "%s\n" "${key_arr[@]}" | "$tsn_backend" "$tsn_backend_opts")"

  # validate the chosen key, if it doesn't exist, exit
  for ch in "${key_arr[@]}"; do
    if [[ "$chosen_key" == "$ch" ]]; then
      flag=true
      break
    fi
  done
  if [[ "$flag" == "false" ]]; then
    _die
  fi
}

# SECOND MENU: use 'get_key()' to show a list of possible keys to choose from
key_menu() {
  get_key key_list

  case "$chosen_key" in
    "$tsn_autokey") auto_type_def ;;
    "$tsn_userkey") key_action "$tsn_username" ;;
    password) key_action "$tsn_password" ;;
    otp) key_otp ;;
    "$tsn_urlkey") key_url "${tsn_passdata["$tsn_urlkey"]}" ;;
    *) key_action "${tsn_passdata["$chosen_key"]}" ;;
  esac
}

# THIRD MENU: optional, use 'get_key()' and TESSEN_ACTION to show the option to
# either auto type or copy the selected key
key_action() {
  local arg="$1"

  # POTENTIAL IMPROVEMENT: used 'printf | wtype' instead of 'auto_type()'
  # because in all the other cases, 'auto_type()' is meant to exit but we don't
  # want to exit here
  case "$tsn_action" in
    autotype) auto_type "$arg" ;;
    copy) wld_copy "$arg" ;;
    both)
      printf "%s" "$arg" | wtype -s "$tsn_delay" -
      wld_copy "$arg"
      ;;
    "")
      get_key option
      if [[ "$chosen_key" == "$tsn_autokey" ]]; then
        auto_type "$arg"
      else
        wld_copy "$arg"
      fi
      ;;
  esac
}

# THIRD MENU: optional, this function is used if an 'otpauth://' URI is found
key_otp() {
  local tmp_otp

  if ! pass otp -h > /dev/null 2>&1; then
    _die "pass-otp is not installed"
  fi

  tmp_otp="$(pass otp "$tsn_passfile")"

  if [[ "$tmp_otp" =~ ^[[:digit:]]+$ ]]; then
    get_key option
    if [[ "$chosen_key" == "$tsn_autokey" ]]; then
      auto_type "$tmp_otp"
    else
      wld_copy "$tmp_otp"
    fi
  else
    _die "invalid OTP detected"
  fi
}

# THIRD MENU: optional, this function is used if TESSEN_URLKEY is found.
# Instead of showing 'autotype', it will show 'open'.
# This function could've been combined with 'key_action()' but it would've
# become a bit more complex than I like.
key_url() {
  local arg="$1"

  case "$tsn_action" in
    autotype)
      key_open_url "$arg" || _die
      _clear
      ;;
    copy) wld_copy "$arg" ;;
    both)
      key_open_url "$arg"
      wld_copy "$arg"
      ;;
    "")
      get_key "$tsn_urlkey"
      if [[ "$chosen_key" == "open" ]]; then
        key_open_url "$arg" || _die
        _clear
      else
        wld_copy "$arg"
      fi
      ;;
  esac
}

# use either xdg-open or $BROWSER to open the selected URL
key_open_url() {
  if is_installed xdg-open; then
    xdg-open "$1" 2> /dev/null || {
      printf "%s\n" "xdg-open was unable to open '$1'" >&2
      return 1
    }
  elif [[ -n "$BROWSER" ]] && is_installed "$BROWSER"; then
    "$BROWSER" "$1" > /dev/null 2>&1 || {
      printf "%s\n" "$BROWSER was unable to open '$1'" >&2
      return 1
    }
  else
    _die "Failed to open the $tsn_urlkey"
  fi
}

# SECOND MENU: the default autotype function, either autotype the username and
# password or the custom autotype defined by the user
# POTENTIAL IMPROVEMENT: Anything better than this ugly hack of
# else..for..case..if..else..if?
auto_type_def() {
  local word tmp_otp

  if [[ -z "$tsn_autotype" ]]; then
    printf "%s" "$tsn_username" | wtype -s "$tsn_delay" -
    wtype -s "$tsn_delay" -k Tab --
    printf "%s" "$tsn_password" | wtype -s "$tsn_delay" -
  else
    for word in $tsn_autotype; do
      case "$word" in
        ":delay") sleep 1 ;;
        ":tab") wtype -s "$tsn_delay" -k Tab -- ;;
        ":space") wtype -s "$tsn_delay" -k space -- ;;
        ":enter") wtype -s "$tsn_delay" -k Return -- ;;
        ":otp")
          if ! pass otp -h > /dev/null 2>&1; then
            _die "pass-otp is not installed"
          else
            tmp_otp="$(pass otp "$tsn_passfile")"
            if [[ "$tmp_otp" =~ ^[[:digit:]]+$ ]]; then
              printf "%s" "$tmp_otp" | wtype -s "$tsn_delay" -
            else
              _die "invalid OTP detected"
            fi
          fi
          ;;
        path | basename | filename) printf "%s" "${tsn_passfile##*/}" | wtype -s "$tsn_delay" - ;;
        "$tsn_userkey") printf "%s" "$tsn_username" | wtype -s "$tsn_delay" - ;;
        pass | password) printf "%s" "$tsn_password" | wtype -s "$tsn_delay" - ;;
        *)
          if [[ -n "${tsn_passdata["$word"]}" ]]; then
            printf "%s" "${tsn_passdata["$word"]}" | wtype -s "$tsn_delay" -
          else
            wtype -s "$tsn_delay" -k space --
          fi
          ;;
      esac
    done
  fi
  _clear
  exit 0
}

auto_type() {
  printf "%s" "$1" | wtype -s "$tsn_delay" -
  _clear
  exit 0
}

# POTENTIAL IMPROVEMENT: We could restore the clipboard as it was before pass
# was used. This is done by default by pass.
wld_copy() {
  printf "%s" "$1" | wl-copy
  if is_installed notify-send; then
    notify-send -t $((tsn_cliptime * 1000)) "Copied username to clipboard. Will clear in $tsn_cliptime seconds."
  fi
  {
    sleep "$tsn_cliptime" || exit 1
    wl-copy --clear
  } > /dev/null 2>&1 &
  unset -v tsn_passfile tsn_username tsn_password tsn_passdata chosen_key
}

print_help() {
  printf "%s\n" "${0##*/} - autotype and copy data from password-store on wayland" ""
  printf "%s\n" "Usage: ${0##*/} [options]" ""
  printf "%s\n" "  ${0##*/}                        use an available dmenu interface and either autotype OR copy data"
  printf "%s\n" "  ${0##*/} -b rofi                use rofi and either autotype OR copy data"
  printf "%s\n" "  ${0##*/} -b rofi -a autotype    use rofi and always autotype data"
  printf "%s\n" "  ${0##*/} -b rofi -a copy        use rofi and always copy data"
  printf "%s\n" "  ${0##*/} -b rofi -a both        use rofi and always autotype AND copy data" ""
  printf "%s\n" "  -b, --backend, --backend=     choose either 'bemenu', 'rofi', or 'wofi' as backend"
  printf "%s\n" "  -a, --action, --action=       choose either 'autotype', 'copy', or 'both'"
  printf "%s\n" "  -h, --help                    print this help menu"
  printf "%s\n" "  -v, --version                 print the version of ${0##*/}" ""
  printf "%s\n" "For more details and additional features, please read the man page of tessen(1)"
  printf "%s\n" "For reporting bugs or feedback, visit https://github.com/ayushnix/tessen"
}

is_installed() {
  if command -v "$1" > /dev/null 2>&1; then
    return 0
  else
    return 1
  fi
}

set_bemenu() {
  local -a bmn_opt=("-i -l 10 -w --scrollbar=autohide -n")
  readonly tsn_backend="bemenu"
  # use BEMENU_OPTS if set by the user, otherwise use $bmn_opt
  export BEMENU_OPTS="${BEMENU_OPTS:-${bmn_opt[*]}}"
  # manually using bemenu options doesn't seem to work
  readonly tsn_backend_opts=""
}

set_rofi() {
  readonly tsn_backend="rofi" tsn_backend_opts="-dmenu"
}

set_wofi() {
  readonly tsn_backend="wofi" tsn_backend_opts="-d"
}

assign_backend() {
  if is_installed "bemenu"; then
    set_bemenu
  elif is_installed "rofi"; then
    set_rofi
  elif is_installed "wofi"; then
    set_wofi
  else
    _die "Please install either 'bemenu', 'rofi', or 'wofi' to use ${0##*/}"
  fi
}

validate_backend() {
  case "$1" in
    bemenu) set_bemenu ;;
    rofi) set_rofi ;;
    wofi) set_wofi ;;
    *) _die "Please specify a valid backend: bemenu | rofi | wofi" ;;
  esac
}

validate_cliptime() {
  local clip_regex

  clip_regex="^[[:digit:]]+$"

  if [[ "$tsn_cliptime" =~ $clip_regex ]]; then
    return 0
  else
    _die "Invalid clipboard time provided"
  fi
}

validate_action() {
  case "$1" in
    autotype) readonly tsn_action="autotype" ;;
    copy) readonly tsn_action="copy" ;;
    both) readonly tsn_action="both" ;;
    "") readonly tsn_action="" ;;
    *) _die "Please specify a valid action: autotype | copy | both" ;;
  esac
}

_clear() {
  wl-copy --clear
  unset -v tsn_passfile tsn_username tsn_password tsn_passdata chosen_key
}

_die() {
  if [[ -n "$1" ]]; then
    printf "%s\n" "$1" >&2
  fi
  _clear
  exit 1
}

main() {
  local _opt

  if ! [[ -d "$tsn_prefix" ]]; then
    _die "password store directory not found"
  fi

  while [[ "$#" -gt 0 ]]; do
    _opt="$1"
    case "$_opt" in
      -b | --backend)
        if [[ "$#" -lt 2 ]] || ! is_installed "$2"; then
          _die "Please specify a valid backend: bemenu | rofi | wofi"
        fi
        validate_backend "$2"
        shift
        ;;
      --backend=*)
        if ! is_installed "${_opt##--backend=}"; then
          _die "Please specify a valid backend: bemenu | rofi | wofi"
        fi
        validate_backend "${_opt##--backend=}"
        ;;
      -a | --action)
        if [[ "$#" -lt 2 ]]; then
          _die "Please specify a valid action: autotype | copy | both"
        fi
        validate_action "$2"
        shift
        ;;
      --action=*)
        validate_action "${_opt##--action=}"
        ;;
      -h | --help)
        print_help
        exit 0
        ;;
      -v | --version)
        printf "%s\n" "${0##*/} version $tsn_version"
        exit 0
        ;;
      --)
        shift
        break
        ;;
      *) _die "invalid argument detected" ;;
    esac
    shift
  done
  unset -v _opt

  if [[ -z "$tsn_backend" ]]; then
    assign_backend
  fi
  validate_cliptime
  readonly tsn_action

  trap '_clear' EXIT TERM
  get_pass_file
  get_pass_data
  key_menu
  trap - EXIT TERM
}

main "$@"