-
Notifications
You must be signed in to change notification settings - Fork 1
/
bashpass
executable file
·354 lines (285 loc) · 10.8 KB
/
bashpass
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
#!/usr/bin/env bash
set -o errexit # Abort on nonzero exit code.
set -o noglob # Disable globbing.
set +o xtrace # Disable debug mode.
set -o pipefail # Don't hide errors within pipes.
# Restrict new password file permissions to only the current user.
umask 077
readonly VERSION="3.3"
readonly NAME="BashPass"
readonly SCRIPT_NAME="${0##*/}"
error() {
local error_msg="${1}"
local error_code="${2}"
printf 'Error: %s.\n' "${error_msg}" >&2
exit "${error_code}"
}
[[ "${UID}" -eq 0 ]] && error 'cannot run with superuser privileges' 1
readonly CONFIG_DIR="${HOME}/.config/bashpass"
readonly CONFIG_FILE="${CONFIG_DIR}/bashpass.conf"
[[ -d "${CONFIG_DIR}" ]] || mkdir -p "${CONFIG_DIR}"
[[ -f "${CONFIG_FILE}" ]] || :> "${CONFIG_FILE}"
# Since we can not use external tool like 'grep' and 'cut' to ensure compatibility,
# we need a way to retrieve the user's settings from their configuration file.
# This function will read the file, and return the value once the requested setting
# is found. It basically does the same as 'grep "setting" "${config}" | cut -d" " -f2',
# but then with pure bash instead.
get_setting() {
local setting="${1}"
while read -r; do
if [[ "${REPLY}" =~ ^${setting} ]]; then
printf '%s' "${REPLY#*: }"
return 0
fi
done < "${CONFIG_FILE}"
return 1
}
# A user can have multiple GnuPG keys located on their system, therefore is a good idea
# to ask for a specific key, by prompting the user to enter a key ID. Afterwards we can
# simpply append it to the configuration file to save it.
ask_for_key_id() {
local regex="^[0-9A-Fa-f]{8,8}$"
local key_id
read -rp 'Enter the key ID associated with your GPG key: ' key_id
if [[ "${key_id}" =~ ${regex} ]]; then
printf 'keyID: %s\n' "${key_id}" >> "${CONFIG_FILE}"
else
error 'invalid key ID' 2
fi
printf '%s' "${key_id}"
}
# The following variables will be used to store the user's settings. When no value is
# found in the configuration file, the user will either be prompted to enter the value,
# or a default value will be used.
CONFIGURED_KEY_ID="$(get_setting 'keyID' || ask_for_key_id)"
CONFIGURED_PASSWD_LENGTH="$(get_setting 'length' || printf '14')"
CONFIGURED_PASSWD_STORE="${HOME}/$(get_setting 'location' || printf '.local/share/bashpass')"
CONFIGURED_TIMER="$(get_setting 'timer' || printf '10')"
readonly CONFIGURED_KEY_ID
readonly CONFIGURED_PASSWD_LENGTH
readonly CONFIGURED_PASSWD_STORE
readonly CONFIGURED_TIMER
mkdir -p "${CONFIGURED_PASSWD_STORE}"
usage() {
printf '%s' "\
${NAME}, a password manager written in Bash.
Version: ${VERSION}
Usage: ${SCRIPT_NAME} [OPTION] [NAME | SYNC_COMMAND]
Options:
--help | -h Show this help message.
--version | -v Show the version number.
--add | -a [NAME] Add a password.
--copy | -c [NAME] Copy a password to the clipboard.
--delete | -d [NAME] Delete a password.
--show | -s [NAME] Show a password.
--update | -u [NAME] Update a password.
--list | -l List all passwords.
--sync | -S [SYNC_COMMAND] Synchronize password(s) with a git repository.
Synchronize commands:
upload Upload local password(s) to a remote repository.
download Download password(s) from a remote repository.
Note:
[NAME] is an optional argument. If not provided, the script will prompt you to enter it.
"
}
version() {
printf 'BashPass version: %s' "${VERSION}"
printf '\n'
}
# Surprisingly, 'sleep' is an external program and not a Bash built-in.
# So their is a small possibility that it is not installed on a system,
# although it is very unlikely. We can use 'read' to replace the 'sleep'
# command.
sleep() {
read -rt "${1}" <> <(:) || :
}
# Wrapper for 'command -v' to avoid spamming '> /dev/null'.
# It also protects against user aliasses and functions.
has() {
local command
command=$(command -v "${1}") 2> /dev/null || return 2
[[ -x ${command} ]] || return 1
}
set_passwd_name() {
local passwd_name="${1}"
local functionality="${2}"
[[ -z "${passwd_name}" ]] && \
read -rp "Enter the password name to ${functionality}: " passwd_name
printf '%s' "${passwd_name}"
}
set_passwd() {
local passwd_name="${1}"
local gen passwd passwd1 passwd2 passwd_length
read -rp 'Generate a password? [Y/n]: ' gen
case "${gen}" in
[Nn])
read -rsp 'Enter the password: ' passwd1
printf '\n'
[[ -z "${passwd1}" ]] && error 'password cannot be empty' 2
while [[ "${passwd1}" != "${passwd2}" ]]; do
read -rsp 'Re-enter the password: ' passwd2
printf '\n'
done
passwd="${passwd1}"
;;
*)
read -rp "Enter the password length (default ${CONFIGURED_PASSWD_LENGTH}): " \
passwd_length
[[ -z "${passwd_length}" ]] && passwd_length="${CONFIGURED_PASSWD_LENGTH}"
[[ ${passwd_length} =~ ^[1-9]([0-9])?+$ ]] || \
error "the password length must be a number." 1
passwd=$(LC_ALL=C \
tr -dc 'A-Za-z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~ ' < /dev/urandom | \
dd ibs=1 obs=1 count="${passwd_length}" 2> /dev/null) || :
;;
esac
[[ -z "${passwd}" ]] && error 'password cannot be empty' 2
printf '%s' "${passwd}" > "${CONFIGURED_PASSWD_STORE}/${passwd_name}"
}
check_passwd_exists() {
local passwd_name="${1}"
[[ -f "${CONFIGURED_PASSWD_STORE}/${passwd_name}.gpg" ]] || \
error "password '${passwd_name}' not found" 2
}
add_passwd() {
local passwd_name="${1}"
local passwd_file
local overwrite
passwd_name=$(set_passwd_name "${passwd_name}" 'add')
passwd_file="${CONFIGURED_PASSWD_STORE}/${passwd_name}.gpg"
while [[ -f "${CONFIGURED_PASSWD_STORE}/${passwd_name}.gpg" ]]; do
read -rp "Password '${passwd_name}' already exists. Overwrite? [y/N]: " overwrite
case "${overwrite}" in
[Yy])
rm -rf "${CONFIGURED_PASSWD_STORE}/${passwd_name}.gpg"
;;
*)
read -rp 'Enter a new password name: ' passwd_name
;;
esac
done
set_passwd "${passwd_name}"
${GPG_CMD} --encrypt --quiet --output "${passwd_file}" \
--recipient "${CONFIGURED_KEY_ID}" "${passwd_file%.gpg}"
rm -rf "${passwd_file%.gpg}"
printf 'Password added successfully.\n'
}
update_passwd() {
local passwd_name="${1}"
local passwd_file
passwd_name=$(set_passwd_name "${passwd_name}" 'update')
passwd_file="${CONFIGURED_PASSWD_STORE}/${passwd_name}.gpg"
check_passwd_exists "${passwd_name}"
rm -rf "${passwd_file}"
set_passwd "${passwd_name}"
${GPG_CMD} --encrypt --quiet --output "${passwd_file}" \
--recipient "${CONFIGURED_KEY_ID}" "${passwd_file%.gpg}"
rm -rf "${passwd_file%.gpg}"
printf 'Password updated successfully.\n'
}
delete_passwd() {
local passwd_name="${1}"
local passwd_file
passwd_name=$(set_passwd_name "${passwd_name}" 'delete')
passwd_file="${CONFIGURED_PASSWD_STORE}/${passwd_name}.gpg"
check_passwd_exists "${passwd_name}"
rm -rf "${passwd_file}"
printf 'Password deleted successfully.\n'
}
show_passwd() {
local passwd_name="${1}"
local passwd_file
passwd_name=$(set_passwd_name "${passwd_name}" 'show')
passwd_file="${CONFIGURED_PASSWD_STORE}/${passwd_name}.gpg"
check_passwd_exists "${passwd_name}"
printf 'Password: %s\n' "$(${GPG_CMD} --decrypt --quiet --output - "${passwd_file}")"
}
list_passwd() {
local passwd_name
printf 'The following passwords are stored:\n'
set +o noglob
for passwd_name in "${CONFIGURED_PASSWD_STORE:?}"/*.gpg; do
passwd_name="${passwd_name##*/}"
passwd_name="${passwd_name%%.gpg}"
printf '%s\n' "${passwd_name}"
done
set -o noglob
}
copy_passwd() {
local copy_clipboard_command clear_clipboard_command
local passwd_name="${1}"
local passwd_file
pass_name=$(set_passwd_name "${passwd_name}" 'copy')
passwd_file="${CONFIGURED_PASSWD_STORE}/${passwd_name}.gpg"
if has pbcopy; then
copy_clipboard_command="pbcopy"
clear_clipboard_command="pbcopy < /dev/null"
elif has xclip; then
copy_clipboard_command="xclip -selection clipboard"
clear_clipboard_command="xclip -selection clipboard < /dev/null"
elif has xsel; then
copy_clipboard_command="xsel --clipboard"
clear_clipboard_command="xsel --clipboard < /dev/null"
elif has wl-copy; then
copy_clipboard_command="wl-copy"
clear_clipboard_command="wl-copy --clear"
else
error 'no suitable clipboard manager found' 1
fi
check_passwd_exists "${pass_name}"
# Ignore terminal interrupts (CTRL+C).
# We do this to prevent the ability to kill the script while
# the password is still in the clipboard.
trap '' INT
${copy_clipboard_command} "$(${GPG_CMD} --decrypt --quiet --output - \
"${CONFIGURED_PASSWD_STORE}/${pass_name}.gpg")" || \
error 'failed to copy password to clipboard' 1
sleep "${CONFIGURED_TIMER}" || kill 0
${clear_clipboard_command}
printf 'Clipboard has been cleared to ensure it cannot be leaked.'
printf '\n'
}
sync_passwd() {
local sync_cmd="${1}"
has git || error 'git is not installed' 1
[[ -d "${CONFIGURED_PASSWD_STORE}/.git/" ]] || \
error "${CONFIGURED_PASSWD_STORE} is not a git repository" 128
case "${sync_cmd}" in
"upload")
git -C "${CONFIGURED_PASSWD_STORE}" add -A
git -C "${CONFIGURED_PASSWD_STORE}" commit \
-m "Upload: $(printf '%(%d/%m/%Y)T at %(%T)T')"
git -C "${CONFIGURED_PASSWD_STORE}" push
;;
"download")
git -C "${CONFIGURED_PASSWD_STORE}" pull
;;
*)
error "synchronize command '${sync_cmd}' not found" 2
;;
esac
printf 'Be aware you'\''ll need to import/export your GPG key.\n'
}
if has gpg2; then
readonly GPG_CMD="gpg2"
elif has gpg; then
readonly GPG_CMD="gpg"
else
error "GnuPG is not installed" 1
fi
case "${1}" in
"--add" | "-a") add_passwd "${2}";;
"--copy" | "-c") copy_passwd "${2}";;
"--delete" | "-d") delete_passwd "${2}";;
"--help" | "-h") usage;;
"--list" | "-l") list_passwd;;
"--show" | "-s") show_passwd "${2}";;
"--sync" | "-S") sync_passwd "${2}";;
"--update" | "-u") update_passwd "${2}";;
"--version"| "-v") version;;
*)
usage >&2
printf '\n' >&2
error "option '${1}' not found" 2
;;
esac