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:
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)