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 483fc8febac23f8015aaaf8850d399403a5d1459
parent 8e82720524ef8bb1272491f11f0e88e0c16e35c9
Author: Natasha Kerensikova <natgh@instinctive.eu>
Date:   Sat,  9 Nov 2024 09:08:02 +0000

Reencrypt command
Diffstat:
MREADME.md | 2++
Mspec/pashage_extra_spec.sh | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mspec/usage_spec.sh | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/pashage.sh | 49+++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/run.sh | 2++
5 files changed, 283 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md @@ -82,6 +82,8 @@ decrypt before `diff`. - The new `random` command leverages password generation without touching the password store. +- The new `reencrypt` command re-encrypts secrets in-place. + ## Manual TODO diff --git a/spec/pashage_extra_spec.sh b/spec/pashage_extra_spec.sh @@ -739,6 +739,7 @@ Describe 'Integrated Command Functions' The output should include ' prg gitconfig' The output should include ' prg move ' The output should include ' prg random ' + The output should include ' prg reencrypt ' End End @@ -1093,6 +1094,151 @@ Describe 'Integrated Command Functions' End End + Describe 'cmd_reencrypt' + usage_text() { %text + #|Usage: prg reencrypt [--interactive,-i] pass-name|subfolder ... + } + + It 'reencrypts a single file' + When call cmd_reencrypt stale + The status should be success + The error should be blank + The output should be blank + expected_file() { %text + #|ageRecipient:myself + #|age:0-password + } + The contents of file "${PREFIX}/stale.age" \ + should equal "$(expected_file)" + expected_log() { %text + #|Re-encrypt stale + #| + #| stale.age | 1 - + #| 1 file changed, 1 deletion(-) + setup_log + } + The result of function check_git_log should be successful + End + + It 'reencrypts a single file interactively' + Data 'y' + When call cmd_reencrypt -i stale + The status should be success + The error should be blank + The output should equal 'Re-encrypt stale? [y/n]' + expected_file() { %text + #|ageRecipient:myself + #|age:0-password + } + The contents of file "${PREFIX}/stale.age" \ + should equal "$(expected_file)" + expected_log() { %text + #|Re-encrypt stale + #| + #| stale.age | 1 - + #| 1 file changed, 1 deletion(-) + setup_log + } + The result of function check_git_log should be successful + End + + It 'does not reencrypt a single file when interactively refused' + Data 'n' + When call cmd_reencrypt --interactive stale + The status should be success + The error should be blank + The output should equal 'Re-encrypt stale? [y/n]' + expected_file() { %text + #|ageRecipient:master + #|ageRecipient:myself + #|age:0-password + } + The contents of file "${PREFIX}/stale.age" \ + should equal "$(expected_file)" + The result of function check_git_log should be successful + End + + It 'reencrypts a directory recursively' + When call cmd_reencrypt / + The status should be success + The error should be blank + The output should be blank + expected_file() { %text + #|ageRecipient:myself + #|age:0-password + } + The contents of file "${PREFIX}/stale.age" \ + should equal "$(expected_file)" + expected_log() { %text + #|Re-encrypt / + #| + #| stale.age | 1 - + #| 1 file changed, 1 deletion(-) + setup_log + } + The result of function check_git_log should be successful + End + + It 'reencrypts a directory recursively and interactively' + Data + #|n + #|y + #|n + End + When call cmd_reencrypt -i '' + The status should be success + The error should be blank + The output should equal 'Re-encrypt extra/subdir/file? [y/n]Re-encrypt stale? [y/n]Re-encrypt subdir/file? [y/n]' + expected_file() { %text + #|ageRecipient:myself + #|age:0-password + } + The contents of file "${PREFIX}/stale.age" \ + should equal "$(expected_file)" + expected_log() { %text + #|Re-encrypt / + #| + #| stale.age | 1 - + #| 1 file changed, 1 deletion(-) + setup_log + } + The result of function check_git_log should be successful + End + + It 'fails to reencrypt a file named like a flag without escape' + PROGRAM=prg + When run cmd_reencrypt -g + The status should equal 1 + The error should equal "$(usage_text)" + The output should be blank + The result of function check_git_log should be successful + End + + It 'fails to reencrypt a non-existent direcotry' + When run cmd_reencrypt -- -y/ + The status should equal 1 + The error should equal 'Error: -y/ is not in the password store.' + The output should be blank + The result of function check_git_log should be successful + End + + It 'fails to reencrypt a non-existent file' + When run cmd_reencrypt -- -y + The status should equal 1 + The error should equal 'Error: -y is not in the password store.' + The output should be blank + The result of function check_git_log should be successful + End + + It 'rejects a path containing ..' + When run cmd_reencrypt fluff/../stale + The status should equal 1 + The output should be blank + The error should include 'sneaky' + The result of function check_git_log should be successful + End + End + Describe 'cmd_usage' It 'defaults to four-space indentation' PROGRAM=prg diff --git a/spec/usage_spec.sh b/spec/usage_spec.sh @@ -121,6 +121,8 @@ Describe 'Command-Line Parsing' } do_reencrypt() { mocklog do_reencrypt "$@" + %text:expand >&2 + #|DECISION=${DECISION} } do_reencrypt_dir() { mocklog do_reencrypt_dir "$@" @@ -2052,6 +2054,89 @@ Describe 'Command-Line Parsing' End End + Describe 'cmd_reencrypt' + COMMAND=reencrypt + DECISION=default + + It 're-encrypts multiple files and directories' + result() { + %text + #|$ check_sneaky_path file-1 + #|$ check_sneaky_path dir/ + #|$ check_sneaky_path sub/file-2 + #|$ do_reencrypt file-1 + #|DECISION=default + #|$ do_reencrypt dir/ + #|DECISION=default + #|$ do_reencrypt sub/file-2 + #|DECISION=default + } + When call cmd_reencrypt file-1 dir/ sub/file-2 + The status should be success + The output should be blank + The error should equal "$(result)" + End + + It 'interactively re-encrypts with a long option' + result() { + %text + #|$ check_sneaky_path arg + #|$ do_reencrypt arg + #|DECISION=interactive + } + When call cmd_reencrypt --interactive arg + The status should be success + The output should be blank + The error should equal "$(result)" + End + + It 'interactively re-encrypts with a short option' + result() { + %text + #|$ check_sneaky_path arg + #|$ do_reencrypt arg + #|DECISION=interactive + } + When call cmd_reencrypt -i arg + The status should be success + The output should be blank + The error should equal "$(result)" + End + + It 're-encrypts a file named like a flag' + result() { + %text + #|$ check_sneaky_path -s + #|$ do_reencrypt -s + #|DECISION=default + } + When call cmd_reencrypt -- -s + The status should be success + The output should be blank + The error should equal "$(result)" + End + + usage_text() { %text + #|Usage: prg reencrypt [--interactive,-i] pass-name|subfolder ... + } + + It 'reports a bad option' + cat() { @cat; } + When run cmd_reencrypt -s arg + The status should equal 1 + The output should be blank + The error should equal "$(usage_text)" + End + + It 'reports a lack of argument' + cat() { @cat; } + When run cmd_reencrypt + The status should equal 1 + The output should be blank + The error should equal "$(usage_text)" + End + End + Describe 'cmd_usage' COMMAND=usage CLIP_TIME='$CLIP_TIME' @@ -2082,6 +2167,7 @@ Describe 'Command-Line Parsing' The output should include 'prg [list]' The output should include 'prg move' The output should include 'prg random' + The output should include 'prg reencrypt' The output should include 'prg [show]' The output should include 'prg version' End diff --git a/src/pashage.sh b/src/pashage.sh @@ -1495,6 +1495,42 @@ cmd_random() { random_chars "${1:-${GENERATED_LENGTH}}" "${2:-${CHARACTER_SET}}" } +cmd_reencrypt() { + DECISION=default + OVERWRITE=yes + PARSE_ERROR=no + + while [ $# -ge 1 ]; do + case "$1" in + -i|--interactive) + DECISION=interactive + shift ;; + --) + shift + break ;; + -*) + PARSE_ERROR=yes + break ;; + *) + break ;; + esac + done + + if [ "${PARSE_ERROR}" = yes ] || [ $# -eq 0 ]; then + cmd_usage 'Usage: ' reencrypt >&2 + exit 1 + fi + + unset PARSE_ERROR + + check_sneaky_paths "$@" + + for ARG in "$@"; do + do_reencrypt "${ARG}" + done + unset ARG +} + # Outputs the whole usage text # $1: indentation # ... commands to document @@ -1515,8 +1551,8 @@ cmd_usage(){ if [ $# -eq 0 ]; then echo 'Usage:' - set -- list show copy delete edit find generate \ - git gitconfig grep help init insert move random version + set -- list show copy delete edit find generate git gitconfig \ + grep help init insert move random reencrypt version VERBOSE=yes else VERBOSE=no @@ -1674,6 +1710,15 @@ ${I} using the given character set (or ${CHARACTER_SET} if unspecified) ${I} without recording it in the password store. EOF ;; + reencrypt) + cat <<EOF +${F}${PROGRAM} reencrypt [--interactive,-i] pass-name|subfolder ... +EOF + [ "${VERBOSE}" = yes ] && cat <<EOF +${I} Re-encrypt in-place a secret or all the secrets in a subfolder, +${I} optionally asking before each one. +EOF + ;; version) cat <<EOF ${F}${PROGRAM} version diff --git a/src/run.sh b/src/run.sh @@ -119,6 +119,8 @@ case "${COMMAND}" in ls) shift; cmd_list_or_show "$@" ;; move|mv) shift; cmd_move "$@" ;; random) shift; cmd_random "$@" ;; + re-encrypt) shift; cmd_reencrypt "$@" ;; + reencrypt) shift; cmd_reencrypt "$@" ;; remove) shift; cmd_delete "$@" ;; rm) shift; cmd_delete "$@" ;; show) shift; cmd_list_or_show "$@" ;;