feat: secrets recipients add (EGB-283)
Add `secrets recipients add <age1...> [--name <label>]`: validates the age key and optional display name, bootstraps recipients.txt with the local pubkey on a legacy store (keeping the operator as a recipient), rejects duplicates, appends the new key (with optional name comment), then re-encrypts the entire store to the updated recipient list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a10f390519
commit
c262661030
2 changed files with 89 additions and 1 deletions
48
secrets
48
secrets
|
|
@ -117,6 +117,15 @@ _validate_age_recipient() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Recipient display names become "# <name>" comment lines in recipients.txt.
|
||||||
|
# Restrict to a safe charset so a name can't inject extra lines/metacharacters.
|
||||||
|
_validate_recipient_name() {
|
||||||
|
case "$1" in
|
||||||
|
*[!A-Za-z0-9\ ._-]*) return 1 ;;
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# Populate the global RECIPIENT_ARGS array with one "-r <key>" per store
|
# Populate the global RECIPIENT_ARGS array with one "-r <key>" per store
|
||||||
# recipient. recipients.txt present -> validated keys from the file (the store
|
# recipient. recipients.txt present -> validated keys from the file (the store
|
||||||
# is multi-recipient). Absent -> the single pubkey derived from key.txt (legacy
|
# is multi-recipient). Absent -> the single pubkey derived from key.txt (legacy
|
||||||
|
|
@ -2067,7 +2076,8 @@ cmd_recipients() {
|
||||||
[ $# -gt 0 ] && shift
|
[ $# -gt 0 ] && shift
|
||||||
case "$sub" in
|
case "$sub" in
|
||||||
list) _recipients_list ;;
|
list) _recipients_list ;;
|
||||||
*) die "Unknown recipients subcommand: '$sub'. Usage: secrets recipients [list]" ;;
|
add) _recipients_add "$@" ;;
|
||||||
|
*) die "Unknown recipients subcommand: '$sub'. Usage: secrets recipients [list|add <age1...> [--name N]]" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2088,6 +2098,42 @@ _recipients_list() {
|
||||||
done < <(_recipients_dump)
|
done < <(_recipients_dump)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_recipients_add() {
|
||||||
|
check_cmd age
|
||||||
|
check_cmd git
|
||||||
|
check_initialized
|
||||||
|
check_key
|
||||||
|
local key="" name=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--name) name="${2:-}"; shift 2 ;;
|
||||||
|
-*) die "Unknown flag: $1. Usage: secrets recipients add <age1...> [--name <label>]" ;;
|
||||||
|
*) if [ -z "$key" ]; then key="$1"; else die "Unexpected argument: $1"; fi; shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$key" ] || die "Usage: secrets recipients add <age1...> [--name <label>]"
|
||||||
|
_validate_age_recipient "$key" || die "Not a valid age recipient: '$key' (expected age1..., 62 chars; SSH keys unsupported)."
|
||||||
|
if [ -n "$name" ]; then
|
||||||
|
_validate_recipient_name "$name" || die "Invalid --name '$name' (allowed: letters, digits, space, . _ -)."
|
||||||
|
fi
|
||||||
|
if [ -L "$RECIPIENTS_FILE" ]; then
|
||||||
|
die "Refusing to write symlinked $RECIPIENTS_FILE_NAME."
|
||||||
|
fi
|
||||||
|
# Bootstrap a legacy store: seed this machine's key first so the operator
|
||||||
|
# stays a recipient (and can decrypt to re-encrypt).
|
||||||
|
if [ ! -e "$RECIPIENTS_FILE" ]; then
|
||||||
|
printf '# self\n%s\n' "$(get_pubkey)" > "$RECIPIENTS_FILE"
|
||||||
|
fi
|
||||||
|
local k _n
|
||||||
|
while IFS=$'\t' read -r k _n; do
|
||||||
|
[ "$k" = "$key" ] && die "Recipient already present: $key"
|
||||||
|
done < <(_recipients_dump)
|
||||||
|
{ [ -n "$name" ] && printf '# %s\n' "$name"; printf '%s\n' "$key"; } >> "$RECIPIENTS_FILE"
|
||||||
|
info "Added recipient${name:+ ($name)}: $key"
|
||||||
|
_load_recipients
|
||||||
|
_reencrypt_all "recipients: add ${name:-$key}; re-encrypt all"
|
||||||
|
}
|
||||||
|
|
||||||
cmd_which() {
|
cmd_which() {
|
||||||
resolve_store
|
resolve_store
|
||||||
echo "store: $SECRETS_DIR"
|
echo "store: $SECRETS_DIR"
|
||||||
|
|
|
||||||
|
|
@ -123,3 +123,45 @@ make_second_identity() {
|
||||||
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
|
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "recipients add bootstraps a legacy store and re-encrypts" {
|
||||||
|
init_with_remote
|
||||||
|
create_project_dir myproj
|
||||||
|
run "$SECRETS_BIN" push
|
||||||
|
make_second_identity
|
||||||
|
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ -e "$SECRETS_DIR/recipients.txt" ]
|
||||||
|
# recipients.txt now has self + bob (2 keys).
|
||||||
|
run "$SECRETS_BIN" recipients list
|
||||||
|
[[ "$output" == *"recipients: 2"* ]] || false
|
||||||
|
[[ "$output" == *"bob"* ]] || false
|
||||||
|
# Existing blob re-encrypted: bob can read it.
|
||||||
|
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "recipients add rejects a non-age key" {
|
||||||
|
init_with_remote
|
||||||
|
run "$SECRETS_BIN" recipients add "ssh-ed25519 AAAAfoo"
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"valid age recipient"* ]] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "recipients add rejects a duplicate" {
|
||||||
|
init_with_remote
|
||||||
|
make_second_identity
|
||||||
|
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
run "$SECRETS_BIN" recipients add "$BOB_PUB"
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"already present"* ]] || false
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "recipients add rejects an unsafe --name" {
|
||||||
|
init_with_remote
|
||||||
|
make_second_identity
|
||||||
|
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name 'bob; rm -rf ~'
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"Invalid --name"* ]] || false
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue