pashage

Yet Another Opinionated Re-engineering of the Unix Password Store
git clone https://git.instinctive.eu/pashage.git
Log | Files | Refs | README | LICENSE

commit 20dd3997907d82a14c455645bd5aaef08929bef0
parent 8fa27c9c253397f75172bed812f01abafd9efb67
Author: Natasha Kerensikova <natgh@instinctive.eu>
Date:   Sun, 17 Nov 2024 16:48:42 +0000

Generate command optionally appends extra input lines to the secret
Diffstat:
MREADME.md | 11++++++++---
Mspec/action_spec.sh | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mspec/pashage_extra_spec.sh | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mspec/usage_spec.sh | 38++++++++++++++++++++++++++++++++++++--
Msrc/pashage.sh | 46++++++++++++++++++++++++----------------------
5 files changed, 265 insertions(+), 33 deletions(-)

diff --git a/README.md b/README.md @@ -83,7 +83,10 @@ explicitly the character set. - The `generate` command optionally asks for confirmation before storing the generated secret (e.g. for iterative attempts against stupid password -rules) +rules). + +- The `generate` command optionally asks for extra lines to append after +the generated secret (e.g. for username, login page, or others comments). - The `init` command has new flags to control re-encryption (never or ask for each file). @@ -276,8 +279,8 @@ Environment: ``` pashage generate [--no-symbols,-n] [--clip,-c | --qrcode,-q] - [--in-place,-i | --force,-f] [--try,-t] - pass-name [pass-length [character-set]] + [--in-place,-i | --force,-f] [--multiline,-m] + [--try,-t] pass-name [pass-length [character-set]] ``` This subcommand generates a new secret from `/dev/urandom`, stores it in @@ -290,6 +293,8 @@ Flags: - `-f` or `--force`: replace existing secrets without asking - `-i` or `--in-place`: when the secret already exists, replace only its first line and re-use the following lines +- `-m` or `--multiline`: read lines from standard input append after the + generated data into the secret file - `-n` or `--no-symbols`: generate a secret using only alphanumeric characters - `-q` or `--qrcode`: display the secret as a QR-code instead of using the diff --git a/spec/action_spec.sh b/spec/action_spec.sh @@ -1033,6 +1033,7 @@ Describe 'Action Functions' Describe 'do_generate' DECISION=default + MULTILINE=no PREFIX="${SHELLSPEC_WORKDIR}/prefix" SHOW=none @@ -1198,7 +1199,6 @@ Describe 'Action Functions' It 'updates the first line of an existing file' MULTILINE=no OVERWRITE=reuse - mktemp() { %= "$1"; } do_decrypt() { mocklog do_decrypt "$@" %text @@ -1212,11 +1212,10 @@ Describe 'Action Functions' #|$ scm_begin #|$ mkdir -p -- ${PREFIX} #|$ do_decrypt ${PREFIX}/existing.age - #|$ do_encrypt existing-XXXXXXXXX.age + #|$ do_encrypt existing.age #|> 0123456789 #|> line 2 #|> line 3 - #|$ mv ${PREFIX}/existing-XXXXXXXXX.age ${PREFIX}/existing.age #|$ scm_add ${PREFIX}/existing.age #|$ scm_commit Replace generated password for existing. #|$ do_show existing @@ -1231,7 +1230,6 @@ Describe 'Action Functions' It 'updates the only line of an existing one-line file' MULTILINE=no OVERWRITE=reuse - mktemp() { %= "$1"; } do_decrypt() { mocklog do_decrypt "$@" %text @@ -1243,9 +1241,8 @@ Describe 'Action Functions' #|$ scm_begin #|$ mkdir -p -- ${PREFIX} #|$ do_decrypt ${PREFIX}/existing.age - #|$ do_encrypt existing-XXXXXXXXX.age + #|$ do_encrypt existing.age #|> 0123456789 - #|$ mv ${PREFIX}/existing-XXXXXXXXX.age ${PREFIX}/existing.age #|$ scm_add ${PREFIX}/existing.age #|$ scm_commit Replace generated password for existing. #|$ do_show existing @@ -1298,6 +1295,118 @@ Describe 'Action Functions' The output should be blank The error should equal "$(result)" End + + It 'accepts an extra line after the generated secret' + MULTILINE=yes + Data 'comment line' + When call do_generate sub/new 10 '[:alnum:]' + result(){ + %text:expand + #|$ scm_begin + #|$ mkdir -p -- ${PREFIX}/sub + #|$ do_encrypt sub/new.age + #|> 0123456789 + #|> comment line + #|$ scm_add ${PREFIX}/sub/new.age + #|$ scm_commit Add generated password for sub/new. + #|$ do_show sub/new + #|> 0123456789 + } + The status should be success + The output should be blank + The error should equal "$(result)" + End + + It 'accepts several lines after the generated secret' + MULTILINE=yes + OVERWRITE=no + Data + #|comment line + #|end of secret + End + yesno() { + mocklog yesno "$@" + ANSWER=y + } + When call do_generate existing 10 '[:alnum:]' + result(){ + %text:expand + #|$ scm_begin + #|$ mkdir -p -- ${PREFIX} + #|$ yesno An entry already exists for existing. Overwrite it? + #|$ do_encrypt existing.age + #|> 0123456789 + #|> comment line + #|> end of secret + #|$ scm_add ${PREFIX}/existing.age + #|$ scm_commit Add generated password for existing. + #|$ do_show existing + #|> 0123456789 + } + The status should be success + The output should be blank + The error should equal "$(result)" + End + + It 'does not asks for extra lines after refusing to overwrite' + MULTILINE=yes + OVERWRITE=no + Data 'n' + yesno() { + mocklog yesno "$@" + ANSWER=n + } + When call do_generate existing 10 '[:alnum:]' + result(){ + %text:expand + #|$ scm_begin + #|$ mkdir -p -- ${PREFIX} + #|$ yesno An entry already exists for existing. Overwrite it? + } + The status should be success + The output should be blank + The error should equal "$(result)" + End + + It 'inserts extra lines after the in-place secrets' + MULTILINE=yes + OVERWRITE=reuse + do_decrypt() { + mocklog do_decrypt "$@" + %text:expand + #|old password + #|old annotation + #|end of $* + } + Data + #|comment line + #|end of secret + End + yesno() { + mocklog yesno "$@" + ANSWER=y + } + When call do_generate existing 10 '[:alnum:]' + result(){ + %text:expand + #|$ scm_begin + #|$ mkdir -p -- ${PREFIX} + #|$ do_decrypt ${PREFIX}/existing.age + #|$ do_encrypt existing.age + #|> 0123456789 + #|> old annotation + #|> end of ${PREFIX}/existing.age + #|> comment line + #|> end of secret + #|$ scm_add ${PREFIX}/existing.age + #|$ scm_commit Replace generated password for existing. + #|$ do_show existing + #|> 0123456789 + } + The status should be success + The output should equal 'Decrypting previous secret for existing' + The error should equal "$(result)" + End End Describe 'do_grep' diff --git a/spec/pashage_extra_spec.sh b/spec/pashage_extra_spec.sh @@ -714,6 +714,88 @@ Describe 'Integrated Command Functions' equal 'Save generated password for new? [y/n]' The result of function check_git_log should be successful End + + It 'accepts an extra line after the generated secret' + Data 'extra comment line' + When call cmd_generate --multiline new 15 + The status should be success + The error should be blank + The lines of output should equal 2 + The line 1 of output should \ + equal '(B)The generated password for (U)new(!U) is:(N)' + The line 2 of output should match pattern '???????????????' + The lines of contents of file "${PREFIX}/new.age" should equal 3 + The line 3 of contents of file "${PREFIX}/new.age" should \ + equal 'age:extra comment line' + expected_log() { %text + #|Add generated password for new. + #| + #| new.age | 3 +++ + #| 1 file changed, 3 insertions(+) + setup_log + } + The result of function check_git_log should be successful + End + + It 'accepts extra lines after the generated secret when overwriting' + Data + #|Extra: line + #|Extra: end of input + End + When call cmd_generate --multiline --force fluff/three 5 + The status should be success + The error should be blank + The lines of output should equal 2 + The line 1 of output should \ + equal '(B)The generated password for (U)fluff/three(!U) is:(N)' + The line 2 of output should match pattern '?????' + The lines of contents of file "${PREFIX}/fluff/three.age" should equal 5 + The line 4 of contents of file "${PREFIX}/fluff/three.age" should \ + equal 'age:Extra: line' + The line 5 of contents of file "${PREFIX}/fluff/three.age" should \ + equal 'age:Extra: end of input' + expected_log() { %text + #|Add generated password for fluff/three. + #| + #| fluff/three.age | 6 +++--- + #| 1 file changed, 3 insertions(+), 3 deletions(-) + setup_log + } + The result of function check_git_log should be successful + End + + It 'accepts extra lines after the generated secret after in-place data' + Data + #|Extra: line + #|Extra: end of input + End + When call cmd_generate --multiline --in-place fluff/three 5 + The status should be success + The error should be blank + The lines of output should equal 3 + The line 1 of output should \ + equal 'Decrypting previous secret for fluff/three' + The line 2 of output should \ + equal '(B)The generated password for (U)fluff/three(!U) is:(N)' + The line 3 of output should match pattern '?????' + The lines of contents of file "${PREFIX}/fluff/three.age" should equal 7 + The line 4 of contents of file "${PREFIX}/fluff/three.age" should \ + equal 'age:Username: 3Jane' + The line 5 of contents of file "${PREFIX}/fluff/three.age" should \ + equal 'age:URL: https://example.com/login' + The line 6 of contents of file "${PREFIX}/fluff/three.age" should \ + equal 'age:Extra: line' + The line 7 of contents of file "${PREFIX}/fluff/three.age" should \ + equal 'age:Extra: end of input' + expected_log() { %text + #|Replace generated password for fluff/three. + #| + #| fluff/three.age | 4 +++- + #| 1 file changed, 3 insertions(+), 1 deletion(-) + setup_log + } + The result of function check_git_log should be successful + End End Describe 'cmd_git' diff --git a/spec/usage_spec.sh b/spec/usage_spec.sh @@ -620,8 +620,8 @@ Describe 'Command-Line Parsing' usage_text() { %text #|Usage: prg generate [--no-symbols,-n] [--clip,-c | --qrcode,-q] - #| [--in-place,-i | --force,-f] [--try,-t] - #| pass-name [pass-length [character-set]] + #| [--in-place,-i | --force,-f] [--multiline,-m] + #| [--try,-t] pass-name [pass-length [character-set]] } It 'generates a new entry with default length' @@ -930,6 +930,40 @@ Describe 'Command-Line Parsing' The error should equal "$(result)" End + It 'accepts extra lines after the generated secret (long)' + result() { + %text + #|$ check_sneaky_path secret + #|$ do_generate secret 25 [:punct:][:alnum:] + #|DECISION=default + #|MULTILINE=yes + #|OVERWRITE=no + #|SELECTED_LINE=1 + #|SHOW=text + } + When call cmd_generate --multiline secret + The status should be success + The output should be blank + The error should equal "$(result)" + End + + It 'accepts extra lines after the generated secret (short)' + result() { + %text + #|$ check_sneaky_path secret + #|$ do_generate secret 25 [:punct:][:alnum:] + #|DECISION=default + #|MULTILINE=yes + #|OVERWRITE=no + #|SELECTED_LINE=1 + #|SHOW=text + } + When call cmd_generate -m secret + The status should be success + The output should be blank + The error should equal "$(result)" + End + It 'reports incompatible generation long options' cat() { @cat; } When run cmd_generate --in-place --force secret diff --git a/src/pashage.sh b/src/pashage.sh @@ -644,6 +644,7 @@ do_generate() { do_generate_commit() { scm_begin mkdir -p -- "$(dirname "${PREFIX}/$1.age")" + EXTRA= if [ -d "${PREFIX}/$1.age" ]; then die "Cannot replace directory $1.age" @@ -652,23 +653,13 @@ do_generate_commit() { printf '%s\n' "Decrypting previous secret for $1" OLD_SECRET_FULL="$(do_decrypt "${PREFIX}/$1.age")" OLD_SECRET="${OLD_SECRET_FULL#*"${NL}"}" - WIP_FILE="$(mktemp "${PREFIX}/$1-XXXXXXXXX.age")" - OVERWRITE=once - if [ "${OLD_SECRET}" = "${OLD_SECRET_FULL}" ]; then - do_encrypt "${WIP_FILE#"${PREFIX}"/}" <<-EOF - ${NEW_PASS} - EOF - else - do_encrypt "${WIP_FILE#"${PREFIX}"/}" <<-EOF - ${NEW_PASS} - ${OLD_SECRET} - EOF + if ! [ "${OLD_SECRET}" = "${OLD_SECRET_FULL}" ]; then + EXTRA="${OLD_SECRET}" fi - mv "${WIP_FILE}" "${PREFIX}/$1.age" - VERB="Replace" - unset OLD_SECRET_FULL unset OLD_SECRET - unset WIP_FILE + unset OLD_SECRET_FULL + OVERWRITE=once + VERB="Replace" else if [ -e "${PREFIX}/$1.age" ] && ! [ "${OVERWRITE}" = yes ]; then @@ -678,13 +669,21 @@ do_generate_commit() { OVERWRITE=once fi - do_encrypt "$1.age" <<-EOF - ${NEW_PASS} - EOF - VERB="Add" fi + if [ "${MULTILINE}" = yes ]; then + while IFS='' read -r LINE; do + EXTRA="${EXTRA}${EXTRA:+${NL}}${LINE}" + done + fi + + do_encrypt "$1.age" <<-EOF + ${NEW_PASS}${EXTRA:+${NL}}${EXTRA} + EOF + + unset EXTRA + scm_add "${PREFIX}/$1.age" scm_commit "${VERB} generated password for $1." @@ -1241,6 +1240,9 @@ cmd_generate() { fi OVERWRITE=reuse shift ;; + -m|--multiline) + MULTILINE=yes + shift ;; -n|--no-symbols) CHARSET="${CHARACTER_SET_NO_SYMBOLS}" shift ;; @@ -1254,7 +1256,7 @@ cmd_generate() { -t|--try) DECISION=interactive shift ;; - -[cfinqt]?*) + -[cfimnqt]?*) REST="${1#-?}" ARG="${1%"${REST}"}" shift @@ -1699,8 +1701,8 @@ EOF generate) cat <<EOF ${F}${PROGRAM} generate [--no-symbols,-n] [--clip,-c | --qrcode,-q] -${I}${BLANKPG} [--in-place,-i | --force,-f] [--try,-t] -${I}${BLANKPG} pass-name [pass-length [character-set]] +${I}${BLANKPG} [--in-place,-i | --force,-f] [--multiline,-m] +${I}${BLANKPG} [--try,-t] pass-name [pass-length [character-set]] EOF [ "${VERBOSE}" = yes ] && cat <<EOF ${I} Generate a new password of pass-length (or ${GENERATED_LENGTH:-25} if unspecified)