This repository has been archived by the owner on Apr 26, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
/
pash
executable file
路241 lines (192 loc) 路 6.57 KB
/
pash
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
#!/bin/sh
#
# pash - simple password manager.
pw_add() {
name=$1
if yn "Generate a password?"; then
# Generate a password by reading '/dev/urandom' with the
# 'tr' command to translate the random bytes into a
# configurable character set.
#
# The 'dd' command is then used to read only the desired
# password length.
#
# Regarding usage of '/dev/urandom' instead of '/dev/random'.
# See: https://www.2uo.de/myths-about-urandom
pass=$(LC_ALL=C tr -dc "${PASH_PATTERN:-_A-Z-a-z-0-9}" < /dev/urandom |
dd ibs=1 obs=1 count="${PASH_LENGTH:-50}" 2>/dev/null)
else
# 'sread()' is a simple wrapper function around 'read'
# to prevent user input from being printed to the terminal.
sread pass "Enter password"
sread pass2 "Enter password (again)"
# Disable this check as we dynamically populate the two
# passwords using the 'sread()' function.
# shellcheck disable=2154
[ "$pass" = "$pass2" ] || die "Passwords do not match"
fi
[ "$pass" ] || die "Failed to generate a password"
# Mimic the use of an array for storing arguments by... using
# the function's argument list. This is very apt isn't it?
if [ "$PASH_KEYID" ]; then
set -- --trust-model always -aer "$PASH_KEYID"
else
set -- -c
fi
# Use 'gpg' to store the password in an encrypted file.
# A heredoc is used here instead of a 'printf' to avoid
# leaking the password through the '/proc' filesystem.
#
# Heredocs are sometimes implemented via temporary files,
# however this is typically done using 'mkstemp()' which
# is more secure than a leak in '/proc'.
"$gpg" "$@" -o "$name.gpg" <<-EOF &&
$pass
EOF
printf '%s\n' "Saved '$name' to the store."
}
pw_del() {
yn "Delete pass file '$1'?" && {
rm -f "$1.gpg"
# Remove empty parent directories of a password
# entry. It's fine if this fails as it means that
# another entry also lives in the same directory.
rmdir -p "${1%/*}" 2>/dev/null || :
}
}
pw_show() {
"$gpg" -dq "$1.gpg"
}
pw_copy() {
# Disable warning against word-splitting as it is safe
# and intentional (globbing is disabled).
# shellcheck disable=2086
: "${PASH_CLIP:=xclip -sel c}"
# Wait in the background for the password timeout and
# clear the clipboard when the timer runs out.
#
# If the 'sleep' fails, kill the script. This is the
# simplest method of aborting from a subshell.
[ "$PASH_TIMEOUT" != off ] && {
printf 'Clearing clipboard in "%s" seconds.\n' "${PASH_TIMEOUT:=15}"
sleep "$PASH_TIMEOUT" || kill 0
$PASH_CLIP </dev/null
} &
pw_show "$1" | $PASH_CLIP
}
pw_list() {
find . -type f -name \*.gpg | sed 's/..//;s/\.gpg$//'
}
pw_tree() {
command -v tree >/dev/null 2>&1 ||
die "'tree' command not found"
tree --noreport | sed 's/\.gpg$//'
}
yn() {
printf '%s [y/n]: ' "$1"
# Enable raw input to allow for a single byte to be read from
# stdin without needing to wait for the user to press Return.
stty -icanon
# Read a single byte from stdin using 'dd'. POSIX 'read' has
# no support for single/'N' byte based input from the user.
answer=$(dd ibs=1 count=1 2>/dev/null)
# Disable raw input, leaving the terminal how we *should*
# have found it.
stty icanon
printf '\n'
# Handle the answer here directly, enabling this function's
# return status to be used in place of checking for '[yY]'
# throughout this program.
glob "$answer" '[yY]'
}
sread() {
printf '%s: ' "$2"
# Disable terminal printing while the user inputs their
# password. POSIX 'read' has no '-s' flag which would
# effectively do the same thing.
stty -echo
read -r "$1"
stty echo
printf '\n'
}
glob() {
# This is a simple wrapper around a case statement to allow
# for simple string comparisons against globs.
#
# Example: if glob "Hello World" '* World'; then
#
# Disable this warning as it is the intended behavior.
# shellcheck disable=2254
case $1 in $2) return 0; esac; return 1
}
die() {
printf 'error: %s.\n' "$1" >&2
exit 1
}
usage() { printf %s "\
pash 2.3.0 - simple password manager.
=> [a]dd [name] - Create a new password entry.
=> [c]opy [name] - Copy entry to the clipboard.
=> [d]el [name] - Delete a password entry.
=> [l]ist - List all entries.
=> [s]how [name] - Show password for an entry.
=> [t]ree - List all entries in a tree.
Using a key pair: export PASH_KEYID=XXXXXXXX
Password length: export PASH_LENGTH=50
Password pattern: export PASH_PATTERN=_A-Z-a-z-0-9
Store location: export PASH_DIR=~/.local/share/pash
Clipboard tool: export PASH_CLIP='xclip -sel c'
Clipboard timeout: export PASH_TIMEOUT=15 ('off' to disable)
"
exit 0
}
main() {
: "${PASH_DIR:=${XDG_DATA_HOME:=$HOME/.local/share}/pash}"
# Look for both 'gpg' and 'gpg2',
# preferring 'gpg2' if it is available.
command -v gpg >/dev/null 2>&1 && gpg=gpg
command -v gpg2 >/dev/null 2>&1 && gpg=gpg2
[ "$gpg" ] ||
die "GPG not found"
mkdir -p "$PASH_DIR" ||
die "Couldn't create password directory"
cd "$PASH_DIR" ||
die "Can't access password directory"
glob "$1" '[acds]*' && [ -z "$2" ] &&
die "Missing [name] argument"
glob "$1" '[cds]*' && [ ! -f "$2.gpg" ] &&
die "Pass file '$2' doesn't exist"
glob "$1" 'a*' && [ -f "$2.gpg" ] &&
die "Pass file '$2' already exists"
glob "$2" '*/*' && glob "$2" '*../*' &&
die "Category went out of bounds"
glob "$2" '/*' &&
die "Category can't start with '/'"
glob "$2" '*/*' && { mkdir -p "${2%/*}" ||
die "Couldn't create category '${2%/*}'"; }
# Set 'GPG_TTY' to the current 'TTY' if it
# is unset. Fixes a somewhat rare `gpg` issue.
export GPG_TTY=${GPG_TTY:-$(tty)}
# Restrict permissions of any new files to
# only the current user.
umask 077
# Ensure that we leave the terminal in a usable
# state on exit or Ctrl+C.
[ -t 1 ] && trap 'stty echo icanon' INT EXIT
case $1 in
a*) pw_add "$2" ;;
c*) pw_copy "$2" ;;
d*) pw_del "$2" ;;
s*) pw_show "$2" ;;
l*) pw_list ;;
t*) pw_tree ;;
*) usage
esac
}
# Ensure that debug mode is never enabled to
# prevent the password from leaking.
set +x
# Ensure that globbing is globally disabled
# to avoid insecurities with word-splitting.
set -f
[ "$1" ] || usage && main "$@"