secrets/test/recipients.bats
Brian Majewski 4ba0234bd7 fix: harden recipients add --name validation (EGB-283)
Require a non-empty argument after --name (dies if it is the last token)
and reject whitespace-only labels that would write a blank comment line.
Two regression tests added to test/recipients.bats.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 12:53:27 -07:00

183 lines
6 KiB
Bash

#!/usr/bin/env bats
load test_helper
# A throwaway second identity for "another teammate".
make_second_identity() {
age-keygen -o "$TEST_TMPDIR/bob.txt" 2>/dev/null
BOB_PUB=$(age-keygen -y "$TEST_TMPDIR/bob.txt")
}
@test "push without recipients.txt stays single-key (legacy behavior)" {
init_with_remote
create_project_dir myproj
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
# No recipients.txt was created by push.
[ ! -e "$SECRETS_DIR/recipients.txt" ]
# Blob decrypts with the store's own key.
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@test "push with a hand-written recipients.txt encrypts to every listed key" {
init_with_remote
make_second_identity
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
printf '# self\n%s\n# bob\n%s\n' "$STORE_PUB" "$BOB_PUB" > "$SECRETS_DIR/recipients.txt"
create_project_dir myproj
run "$SECRETS_BIN" push
[ "$status" -eq 0 ]
# Bob (a recipient) can decrypt the pushed blob with HIS key.
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
# And the store key still can too.
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@test "push refuses a recipients.txt with an invalid key" {
init_with_remote
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
printf '%s\nnot-an-age-key\n' "$STORE_PUB" > "$SECRETS_DIR/recipients.txt"
create_project_dir myproj
run "$SECRETS_BIN" push
[ "$status" -ne 0 ]
[[ "$output" == *"Invalid recipient"* ]] || false
}
@test "push refuses a symlinked recipients.txt" {
init_with_remote
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
printf '%s\n' "$STORE_PUB" > "$TEST_TMPDIR/elsewhere.txt"
ln -s "$TEST_TMPDIR/elsewhere.txt" "$SECRETS_DIR/recipients.txt"
create_project_dir myproj
run "$SECRETS_BIN" push
[ "$status" -ne 0 ]
[[ "$output" == *"symlink"* ]] || false
}
@test "recipients list on a legacy store shows the single derived key" {
init_with_remote
run "$SECRETS_BIN" recipients list
[ "$status" -eq 0 ]
[[ "$output" == *"single-key"* ]] || false
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
[[ "$output" == *"$STORE_PUB"* ]] || false
}
@test "recipients list shows names and keys from recipients.txt" {
init_with_remote
make_second_identity
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
printf '# alice\n%s\n# bob\n%s\n' "$STORE_PUB" "$BOB_PUB" > "$SECRETS_DIR/recipients.txt"
run "$SECRETS_BIN" recipients list
[ "$status" -eq 0 ]
[[ "$output" == *"recipients: 2"* ]] || false
[[ "$output" == *"alice"* ]] || false
[[ "$output" == *"bob"* ]] || false
}
@test "reencrypt re-encrypts existing blobs to a newly added recipient line" {
init_with_remote
create_project_dir myproj
run "$SECRETS_BIN" push # single-key blob (project name = "myproj"; blob at $SECRETS_DIR/myproj/.env.age)
[ "$status" -eq 0 ]
make_second_identity
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
printf '%s\n%s\n' "$STORE_PUB" "$BOB_PUB" > "$SECRETS_DIR/recipients.txt"
# Bob cannot read the old single-key blob yet.
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -ne 0 ]
run "$SECRETS_BIN" reencrypt
[ "$status" -eq 0 ]
# Now he can.
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@test "rekey on a multi-recipient store keeps recipients and the same key" {
init_with_remote
create_project_dir myproj
run "$SECRETS_BIN" push
before=$(cat "$SECRETS_DIR/key.txt")
make_second_identity
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
printf '%s\n%s\n' "$STORE_PUB" "$BOB_PUB" > "$SECRETS_DIR/recipients.txt"
run "$SECRETS_BIN" rekey
[ "$status" -eq 0 ]
# No new keypair was generated.
[ "$(cat "$SECRETS_DIR/key.txt")" = "$before" ]
# Both recipients can decrypt.
run age -d -i "$TEST_TMPDIR/bob.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$status" -eq 0 ]
}
@test "rekey on a legacy store still rotates to a new key (unchanged)" {
init_with_remote
create_project_dir myproj
run "$SECRETS_BIN" push
before=$(cat "$SECRETS_DIR/key.txt")
run "$SECRETS_BIN" rekey
[ "$status" -eq 0 ]
[ "$(cat "$SECRETS_DIR/key.txt")" != "$before" ]
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
[ "$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
}
@test "recipients add rejects a whitespace-only --name" {
init_with_remote
make_second_identity
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name ' '
[ "$status" -ne 0 ]
[[ "$output" == *"Invalid --name"* ]] || false
}
@test "recipients add --name with no value errors" {
init_with_remote
make_second_identity
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name
[ "$status" -ne 0 ]
[[ "$output" == *"--name requires a value"* ]] || false
}