#!/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 with only-self recipients.txt encrypts to the store key (born-multi)" { init_with_remote # init now seeds recipients.txt with self — born-multi store. [ -e "$SECRETS_DIR/recipients.txt" ] create_project_dir myproj run "$SECRETS_BIN" push [ "$status" -eq 0 ] # Blob must still decrypt 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" # Remove the born-multi recipients.txt so we can replace it with a symlink. rm -f "$SECRETS_DIR/recipients.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 # Simulate a legacy store by removing the born-multi recipients.txt. rm -f "$SECRETS_DIR/recipients.txt" 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") # Simulate a legacy store by removing the born-multi recipients.txt. rm -f "$SECRETS_DIR/recipients.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 # Simulate a legacy store (no recipients.txt) so recipients add triggers the # bootstrap branch (if [ ! -e "$RECIPIENTS_FILE" ]) rather than the append path. rm -f "$SECRETS_DIR/recipients.txt" 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 } @test "recipients rm removes a recipient and re-encrypts to the rest" { init_with_remote create_project_dir myproj run "$SECRETS_BIN" push make_second_identity run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob run "$SECRETS_BIN" recipients rm bob [ "$status" -eq 0 ] run "$SECRETS_BIN" recipients list [[ "$output" == *"recipients: 1"* ]] || false # Store key still reads its own blobs. run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age" [ "$status" -eq 0 ] } @test "recipients rm refuses to remove the last recipient" { init_with_remote make_second_identity run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob # store = self + bob STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt") run "$SECRETS_BIN" recipients rm bob # back to self only [ "$status" -eq 0 ] run "$SECRETS_BIN" recipients rm "$STORE_PUB" # would be the last [ "$status" -ne 0 ] [[ "$output" == *"last recipient"* ]] || false } @test "recipients rm of your own key requires --yes" { init_with_remote make_second_identity run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt") run "$SECRETS_BIN" recipients rm "$STORE_PUB" [ "$status" -ne 0 ] [[ "$output" == *"your own key"* ]] || false run "$SECRETS_BIN" recipients rm "$STORE_PUB" --yes [ "$status" -eq 0 ] } @test "recipients rm of a non-existent target errors" { init_with_remote make_second_identity run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob run "$SECRETS_BIN" recipients rm carol [ "$status" -ne 0 ] [[ "$output" == *"No recipient matches"* ]] || false } @test "init seeds recipients.txt with the new store key (born-multi)" { run "$SECRETS_BIN" init [ "$status" -eq 0 ] [ -e "$SECRETS_DIR/recipients.txt" ] STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt") run cat "$SECRETS_DIR/recipients.txt" [[ "$output" == *"$STORE_PUB"* ]] || false } @test "which reports the recipient count" { init_with_remote make_second_identity run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob create_project_dir myproj run "$SECRETS_BIN" which [ "$status" -eq 0 ] [[ "$output" == *"recipients: 2"* ]] || false [[ "$output" == *"bob"* ]] || false } @test "which reports single-key for a legacy store" { init_with_remote rm -f "$SECRETS_DIR/recipients.txt" create_project_dir myproj run "$SECRETS_BIN" which [ "$status" -eq 0 ] [[ "$output" == *"recipients: single-key"* ]] || false }