feat: 'file' external type — whole-file sync for .secrets-files (EGB-652)
Built for the Beacon Android upload keystore: binary files outside the project root can now ride the encrypted store. - manifest: 'file <path>' (no keys; keys present = rejected loudly) - push: encrypts the file verbatim (age is binary-safe) - pull: restores next to target (atomic same-fs mv), mode 600, TOCTOU symlink recheck, divergent existing target backed up to .secrets-bak - _validate_external_target_path parameterized by type (basename restriction stays gradle-properties-only; $HOME/../symlink rails apply to both) - README/help/CLAUDE docs + 7 bats tests (133/133 pass)
This commit is contained in:
parent
ab45c4c94b
commit
e0474f3ce9
5 changed files with 226 additions and 40 deletions
|
|
@ -1657,3 +1657,104 @@ gradle_project() {
|
|||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"git clone git@example.com:me/secrets-work.git"* ]]
|
||||
}
|
||||
|
||||
# ─── EGB-652: `file` external type (whole-file sync, e.g. Android keystore) ──
|
||||
|
||||
# Helper: write a small binary source file under the sandboxed HOME.
|
||||
file_src() {
|
||||
mkdir -p "$HOME/keystores"
|
||||
printf 'KS\x00\x01\x02\xffDATA-%s\n' "${1:-v1}" > "$HOME/keystores/upload.keystore"
|
||||
}
|
||||
|
||||
# Helper: bind a project dir to a file entry via .secrets-files, cd into it.
|
||||
file_project() {
|
||||
local dir="$WORK_DIR/$1"
|
||||
mkdir -p "$dir"
|
||||
printf 'file ~/keystores/upload.keystore\n' > "$dir/.secrets-files"
|
||||
cd "$dir" || exit 1
|
||||
}
|
||||
|
||||
@test "EGB-652: which shows parsed file-type entry" {
|
||||
init_with_remote
|
||||
file_src
|
||||
file_project fproj
|
||||
run "$SECRETS_BIN" which
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"file"* ]]
|
||||
[[ "$output" == *"~/keystores/upload.keystore"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-652: push encrypts a file-type entry into external/ blob" {
|
||||
init_with_remote
|
||||
file_src
|
||||
file_project fproj
|
||||
run "$SECRETS_BIN" push fproj
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Encrypted file"* ]]
|
||||
run bash -c "ls $SECRETS_DIR/fproj/external/*.file.age"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "EGB-652: pull restores the file byte-identical with mode 600" {
|
||||
init_with_remote
|
||||
file_src v1
|
||||
file_project fproj
|
||||
"$SECRETS_BIN" push fproj >/dev/null 2>&1
|
||||
cp "$HOME/keystores/upload.keystore" "$TEST_TMPDIR/reference"
|
||||
rm -rf "$HOME/keystores"
|
||||
run "$SECRETS_BIN" pull fproj
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Restored file"* ]]
|
||||
cmp "$HOME/keystores/upload.keystore" "$TEST_TMPDIR/reference"
|
||||
mode=$(stat -f '%Lp' "$HOME/keystores/upload.keystore" 2>/dev/null || stat -c '%a' "$HOME/keystores/upload.keystore")
|
||||
[ "$mode" = "600" ]
|
||||
}
|
||||
|
||||
@test "EGB-652: pull backs up an existing divergent target before overwriting" {
|
||||
init_with_remote
|
||||
file_src v1
|
||||
file_project fproj
|
||||
"$SECRETS_BIN" push fproj >/dev/null 2>&1
|
||||
file_src v2-local-edit
|
||||
run "$SECRETS_BIN" pull fproj
|
||||
[ "$status" -eq 0 ]
|
||||
grep -q 'DATA-v1' "$HOME/keystores/upload.keystore"
|
||||
grep -q 'DATA-v2-local-edit' "$HOME/keystores/upload.keystore.secrets-bak"
|
||||
}
|
||||
|
||||
@test "EGB-652: file entry with trailing keys is rejected" {
|
||||
init_with_remote
|
||||
file_src
|
||||
local dir="$WORK_DIR/fbad"; mkdir -p "$dir"
|
||||
printf 'file ~/keystores/upload.keystore strayKey\n' > "$dir/.secrets-files"
|
||||
cd "$dir"
|
||||
run "$SECRETS_BIN" which
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"take no keys"* ]]
|
||||
# The rejected entry must not be listed as parsed (header only prints
|
||||
# when at least one entry parses).
|
||||
[[ "$output" != *"external files ("* ]]
|
||||
}
|
||||
|
||||
@test "EGB-652: file target outside HOME is refused on push" {
|
||||
init_with_remote
|
||||
local dir="$WORK_DIR/fout"; mkdir -p "$dir"
|
||||
printf 'file /etc/hosts\n' > "$dir/.secrets-files"
|
||||
cd "$dir"
|
||||
run "$SECRETS_BIN" push fout
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"inside \$HOME"* ]] || [[ "$output" == *"Refusing"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-652: gradle-properties entries still work alongside a file entry" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
file_src
|
||||
local dir="$WORK_DIR/fmix"; mkdir -p "$dir"
|
||||
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\nfile ~/keystores/upload.keystore\n' > "$dir/.secrets-files"
|
||||
cd "$dir"
|
||||
run "$SECRETS_BIN" push fmix
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Extracted 1 key"* ]]
|
||||
[[ "$output" == *"Encrypted file"* ]]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue