• v0.2.0.0 e947cfde19

    brian released this 2026-05-26 15:02:55 -07:00 | 99 commits to main since this release

    Added

    • Sync designated keys from external files (Gradle properties). A new committed .secrets-files manifest lets secrets track specific keys from files that live outside the project root — the motivating case being ~/.gradle/gradle.properties, where Android builds read Clerk publishable keys (beaconClerkPkTest, beaconClerkPkLive) that Android Studio's GUI builds can only get from that persistent global file, not from terminal env vars. One entry per line: gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest beaconClerkPkLive. (EGB-531)
      • push extracts only the named keys and encrypts them under <project>/external/ in the store.
      • pull merges those keys into the target file, preserving every unrelated key, comment, and line order. An existing managed key is updated in place; the target is backed up to gradle.properties.secrets-bak before each merge.
      • secrets which reads back the parsed manifest; secrets list shows [external] entries; secrets rekey re-encrypts external blobs alongside dotenv ones.
      • Backward compatible: no .secrets-files → identical behavior to before.

    Security

    • The merge is pure bash with exact-string key matching — no sed/regex. This is deliberate: a sed-based substitution would corrupt values containing &, \, or / (common in API keys) and would false-match substring keys (beaconClerkPk vs beaconClerkPkTest). Values are treated as opaque literals and round-trip byte-exact.
    • External write targets are validated against an attacker-controlled path. Because the target path comes from a committed manifest, the writable target is locked down: basename must be gradle.properties, the path must resolve inside $HOME, .. traversal is rejected, and symlinked targets (file or parent dir) are refused — blocking a malicious manifest from appending decrypted keys to ~/.gitconfig, ~/.bashrc, etc. Manifest parsing rejects shell metacharacters and control characters in paths and keys, mirroring the .secrets-store posture. Writes are atomic (temp-in-same-dir + rename), mode-preserving, and default to 600 on create.
    • Storage isolation. External blobs live in a <project>/external/ subdir so the existing broad *.age globs (pull, list, rekey) structurally never decrypt them into the working directory or orphan them.
    • Note on plaintext. Merged Gradle keys are written as permanent plaintext into the target file (secrets clear does not remove them) — appropriate for publishable/low-secrecy values like Clerk publishable keys, by design.

    Fixed

    • secrets rekey was broken and never completed. Two latent bugs, exposed by the new rekey test: (1) age-keygen -o key.txt aborts because age-keygen refuses to overwrite an existing file — the new key is now generated into a temp dir and moved into place only on success, so the old key survives a failed rotation; (2) the EXIT trap referenced the function-local $tmpdir after the function returned, erroring under set -u and leaking the plaintext temp dir — the temp dir is now removed explicitly and the trap cleared on normal completion.

    Tests

    • 80 → 113 (+33). New coverage: manifest parse/read-back, key extraction across =/:/space separators, merge (preserve unrelated/comments/order, substring-key isolation, sed-metachar value round-trip, duplicate-key collapse, continuation-line safety, idempotency), path validation (wrong basename, outside $HOME, symlinked target, symlinked parent dir), first-create mode 600, manifest injection/symlink/unsafe-key rejection, rekey round-trip of external blobs, glob isolation (blob not leaked to cwd), list surfacing, pre-commit blocking plaintext gradle.properties, workspace (push -w/pull -w) external sync, multi-entry manifests, partial-key push warnings, missing-blob pull warnings, source-side comment/continuation skipping, and backward compatibility.
    Downloads