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 08ed7dd12a284c4a05bcc1f8c345d1ffcc799d3a
parent 483fc8febac23f8015aaaf8850d399403a5d1459
Author: Natasha Kerensikova <natgh@instinctive.eu>
Date:   Sat,  9 Nov 2024 13:18:56 +0000

Success of piped expressions is corrected and tested
Diffstat:
Mspec/action_spec.sh | 46++++++++++++++++++++++++++++++++++++++++++++--
Mspec/pashage_extra_spec.sh | 81++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mspec/pass_spec.sh | 21+++++++++++++++++++++
Msrc/pashage.sh | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
4 files changed, 207 insertions(+), 21 deletions(-)

diff --git a/spec/action_spec.sh b/spec/action_spec.sh @@ -1139,7 +1139,6 @@ Describe 'Action Functions' It 'updates the first line of an existing file' OVERWRITE=yes mktemp() { %= "$1"; } - tail() { @tail "$@"; } do_decrypt() { mocklog do_decrypt "$@" %text @@ -1168,6 +1167,34 @@ Describe 'Action Functions' The output should equal 'Decrypting previous secret for existing' The error should equal "$(result)" End + + It 'updates the only line of an existing one-line file' + OVERWRITE=yes + mktemp() { %= "$1"; } + do_decrypt() { + mocklog do_decrypt "$@" + %text + #|old password + } + mv() { mocklog mv "$@"; } + result(){ + %text:expand + #|$ scm_begin + #|$ mkdir -p -- ${PREFIX} + #|$ do_decrypt ${PREFIX}/existing.age + #|$ do_encrypt existing-XXXXXXXXX.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 + #|> 0123456789 + } + When call do_generate existing 10 '[alnum:]' + The status should be success + The output should equal 'Decrypting previous secret for existing' + The error should equal "$(result)" + End End Describe 'do_grep' @@ -1208,6 +1235,21 @@ Describe 'Action Functions' The status should be success The output should equal "$(result)" End + + It 'outputs all the matching lines' + result(){ + %text + #|(B)subdir/(G)match(N): + #|other + #|suffix + } + start_do_grep(){ + ( cd "${PREFIX}" && do_grep '' "$@" ) + } + When call start_do_grep -vea + The status should be success + The output should equal "$(result)" + End End Describe 'do_init' @@ -1609,7 +1651,7 @@ Describe 'Action Functions' mocklog do_encrypt "$@" } - mktemp() { %putsn "$1"; } + mktemp() { %putsn "${2-$1}"; } mv() { mocklog mv "$@"; } scm_add() { mocklog scm_add "$@"; } scm_begin() { mocklog scm_begin "$@"; } diff --git a/spec/pashage_extra_spec.sh b/spec/pashage_extra_spec.sh @@ -344,6 +344,17 @@ Describe 'Integrated Command Functions' The result of function check_git_log should be successful End + It 'aborts on decryption failure even without pipefail' + Set 'pipefail:off' + mock-age() { false; } + When run cmd_move --reencrypt stale renamed + The status should equal 1 + The error should equal \ + "Fatal(1): mock-age -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age" + The output should be blank + The result of function check_git_log should be successful + End + It 'displays usage when called with incompatible reencryption arguments' PROGRAM=prg COMMAND=copy @@ -544,6 +555,18 @@ Describe 'Integrated Command Functions' The file "${PREFIX}/subdir/new.gpg" should not be exist The result of function check_git_log should be successful End + + It 'aborts on decryption failure even without pipefail' + Set 'pipefail:off' + mock-age() { false; } + tail() { @tail "$@"; } + When run cmd_edit stale + The status should equal 1 + The error should equal \ + "Fatal(1): mock-age -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age" + The output should be blank + The result of function check_git_log should be successful + End End Describe 'cmd_find' @@ -623,6 +646,18 @@ Describe 'Integrated Command Functions' The result of function git_log should be successful The contents of file "${GITLOG}" should equal "$(setup_log)" End + + It 'aborts on decryption failure even without pipefail' + Set 'pipefail:off' + mock-age() { false; } + tail() { @tail "$@"; } + When run cmd_generate --inplace stale + The status should equal 1 + The error should equal \ + "Fatal(1): mock-age -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age" + The output should equal 'Decrypting previous secret for stale' + The result of function check_git_log should be successful + End End Describe 'cmd_git' @@ -652,7 +687,19 @@ Describe 'Integrated Command Functions' End End -# Describe 'cmd_grep' is not needed (fully covered in pass_spec.sh) + Describe 'cmd_grep' + It 'aborts on decryption failure even without pipefail' + Set 'pipefail:off' + grep() { @grep "$@"; } + mock-age() { false; } + When run cmd_grep foo + The status should equal 1 + The error should equal \ + "Fatal(1): mock-age -d -i ${IDENTITIES_FILE} -- file.age" + The output should be blank + The result of function check_git_log should be successful + End + End Describe 'cmd_gitconfig' grep() { @grep "$@"; } @@ -1050,6 +1097,27 @@ Describe 'Integrated Command Functions' } The error should equal "$(expected_err)" End + + It 'aborts on age decryption failure even without pipefail' + Set 'pipefail:off' + mock-age() { false; } + When run cmd_list_or_show stale + The status should equal 1 + The error should equal \ + "Fatal(1): mock-age -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age" + The output should be blank + The result of function check_git_log should be successful + End + + It 'aborts on gpg decryption failure even without pipefail' + Set 'pipefail:off' + GPG=false + When run cmd_list_or_show old + The status should equal 1 + The error should equal "Fatal(1): false -d --quiet --yes --compress-algo=none --no-encrypt-to -- ${PREFIX}/old.gpg" + The output should be blank + The result of function check_git_log should be successful + End End # Describe 'cmd_move' is not needed (covered by 'cmd_copy_move') @@ -1237,6 +1305,17 @@ Describe 'Integrated Command Functions' The error should include 'sneaky' The result of function check_git_log should be successful End + + It 'aborts on age decryption failure even without pipefail' + Set 'pipefail:off' + mock-age() { false; } + When run cmd_reencrypt stale + The status should equal 1 + The error should equal \ + "Fatal(1): mock-age -d -i ${IDENTITIES_FILE} -- ${PREFIX}/stale.age" + The output should be blank + The result of function check_git_log should be successful + End End Describe 'cmd_usage' diff --git a/spec/pass_spec.sh b/spec/pass_spec.sh @@ -1554,6 +1554,27 @@ Describe 'Pass-like command' The contents of file "${GITLOG}" should equal "$(expected_log $3)" End + It 'replaces the line of an existing one-line file' + Skip if 'pass(age) needs bash' check_skip $2 + When run script $1 generate -ni fluff/one 4 + The status should be success + The output should include 'The generated password for' + The lines of contents of file "${PREFIX}/fluff/one.$3" should equal 3 + The line 3 of contents of file "${PREFIX}/fluff/one.$3" should \ + match pattern "$3:[0-9a-zA-z][0-9a-zA-z][0-9a-zA-z][0-9a-zA-z]" + The output should \ + include "$(@sed -n "3s/$3://p" "${PREFIX}/fluff/one.$3")" + expected_log() { %text:expand + #|Replace generated password for fluff/one. + #| + #| fluff/one.$1 | 2 +- + #| 1 file changed, 1 insertion(+), 1 deletion(-) + setup_log + } + The result of function git_log should be successful + The contents of file "${GITLOG}" should equal "$(expected_log $3)" + End + It 'pastes the generated password into the clipboard' DISPLAY=mock Skip if 'pass(age) needs bash' check_skip $2 diff --git a/src/pashage.sh b/src/pashage.sh @@ -69,6 +69,12 @@ glob_exists() { fi } +# Always-successful grep filter +# ... grep arguments +grep_filter() { + grep "$@" || true +} + # Generate random characters # $1: number of characters # $2: allowed character set @@ -626,14 +632,23 @@ do_generate() { elif [ -e "${PREFIX}/$1.age" ] && [ "${OVERWRITE}" = yes ]; then printf '%s\n' "Decrypting previous secret for $1" - OLD_SECRET="$(do_decrypt "${PREFIX}/$1.age" | tail -n +2)" + OLD_SECRET_FULL="$(do_decrypt "${PREFIX}/$1.age")" + OLD_SECRET="${OLD_SECRET_FULL#* +}" WIP_FILE="$(mktemp "${PREFIX}/$1-XXXXXXXXX.age")" - do_encrypt "${WIP_FILE#"${PREFIX}"/}" <<-EOF - ${NEW_PASS} - ${OLD_SECRET} - EOF + 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 + fi mv "${WIP_FILE}" "${PREFIX}/$1.age" VERB="Replace" + unset OLD_SECRET_FULL unset OLD_SECRET unset WIP_FILE @@ -691,15 +706,30 @@ do_grep() { if [ -d "${ARG}" ]; then ( cd "${ARG}" && do_grep "${SUBDIR}${ARG}/" "$@" ) elif [ "${ARG}" = "${ARG%.age}.age" ]; then - FOUND="$(do_decrypt "${ARG}" | (grep "$@" || true))" - if [ -n "${FOUND}" ]; then - printf '%s%s\n%s\n' \ - "${BLUE_TEXT}${SUBDIR}" \ - "${BOLD_TEXT}${ARG%.age}${NORMAL_TEXT}:" \ - "${FOUND}" - fi + HEADER="${BLUE_TEXT}${SUBDIR}${BOLD_TEXT}" + HEADER="${HEADER}${ARG%.age}${NORMAL_TEXT}:" + SECRET="$(do_decrypt "${ARG}")" + do_grep_filter "$@" <<-EOF + ${SECRET} + EOF fi done + + unset ARG + unset HEADER +} + +# Wrapper around grep filter to added a header when a match is found +# ... grep arguments +# HEADER header to print before matches, if any +do_grep_filter() { + unset SECRET + + grep_filter "$@" | while IFS= read -r LINE; do + [ -n "${HEADER}" ] && printf '%s\n' "${HEADER}" + printf '%s\n' "${LINE}" + HEADER='' + done } # Add identities to a subdirectory @@ -790,7 +820,9 @@ do_insert() { fi done - printf '%s\n' "${LINE1}" | do_encrypt "$1.age" + do_encrypt "$1.age" <<-EOF + ${LINE1} + EOF unset LINE1 LINE2 fi @@ -804,11 +836,19 @@ do_list_or_show() { if [ -z "$1" ]; then do_tree "${PREFIX}" "Password Store" elif [ -f "${PREFIX}/$1.age" ]; then - do_decrypt "${PREFIX}/$1.age" | do_show "$1" + SECRET="$(do_decrypt "${PREFIX}/$1.age")" + do_show "$1" <<-EOF + ${SECRET} + EOF + unset SECRET elif [ -d "${PREFIX}/$1" ]; then do_tree "${PREFIX}/$1" "$1" elif [ -f "${PREFIX}/$1.gpg" ]; then - do_decrypt_gpg "${PREFIX}/$1.gpg" | do_show "$1" + SECRET="$(do_decrypt_gpg "${PREFIX}/$1.gpg")" + do_show "$1" <<-EOF + ${SECRET} + EOF + unset SECRET else die "Error: $1 is not in the password store." fi @@ -872,9 +912,11 @@ do_reencrypt_file() { fi OVERWRITE=once - WIP_FILE="$(mktemp "${PREFIX}/$1-XXXXXXXXX.age")" - do_decrypt "${PREFIX}/$1.age" \ - | do_encrypt "${WIP_FILE#"${PREFIX}"/}" + WIP_FILE="$(mktemp -u "${PREFIX}/$1-XXXXXXXXX.age")" + SECRET="$(do_decrypt "${PREFIX}/$1.age")" + do_encrypt "${WIP_FILE#"${PREFIX}"/}" <<-EOF + ${SECRET} + EOF mv -f -- "${WIP_FILE}" "${PREFIX}/$1.age" unset WIP_FILE scm_add "$1.age" @@ -885,6 +927,8 @@ do_reencrypt_file() { # SELECTED_LINE: which line to paste or diplay as qr-code # SHOW: how to show the secret do_show() { + unset SECRET + case "${SHOW}" in text) cat