From 1a23fd31251d9a5dc4b8b525bf543d4fc99754f8 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Sun, 4 Oct 2026 20:58:42 +0200 Subject: [PATCH] Make the README's storage and file format text match the code (closes #102) The directory tree shows `current` and `currentvault` as plain files holding a name, a version's metadata as the encrypted `metadata.age`, the real state directory under the user's configuration directory, and the `lock` file. `version promote` rewrites `current`. File Formats tells unencrypted vault and unlocker metadata from encrypted version metadata; `pub.age` is plain text and vault metadata holds no vault name. Unlocker bullets lose Touch ID claims the code does not set up, and the Secure Enclave only decrypts. Per-version keys no longer claim forward secrecy. Testing lists only `make test`. Model: opus-5-5 --- README.md | 51 ++++++++++++++++++++++++++++++++------------------- TODO.md | 11 +++++++++++ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 7d3d10b..40ee8df 100644 --- a/README.md +++ b/README.md @@ -180,8 +180,8 @@ period. #### `secret version promote ` -Promotes a specific version to current by updating the symlink. Does not modify -any timestamps, allowing for rollback scenarios. +Promotes a specific version to current by rewriting the secret's `current` file +to name it. Does not modify any timestamps, allowing for rollback scenarios. #### `secret version remove [--force]` / `secret version rm` ⚠️ 🛑 @@ -278,8 +278,13 @@ Decrypts data using an Age key stored as a secret. ### Directory Structure +The state directory is `berlin.sneak.pkg.secret` in the user's configuration +directory: on Linux `$XDG_CONFIG_HOME`, or `~/.config` when that is unset; on +macOS `~/Library/Application Support`. When `SB_SECRET_STATE_DIR` is set, it is +the state directory instead. On Linux: + ``` -~/.local/share/secret/ +~/.config/berlin.sneak.pkg.secret/ ├── vaults.d/ │ ├── default/ │ │ ├── unlockers.d/ @@ -292,12 +297,12 @@ Decrypts data using an Age key stored as a secret. │ │ │ │ │ │ ├── pub.age # Version public key │ │ │ │ │ │ ├── priv.age # Version private key (encrypted) │ │ │ │ │ │ ├── value.age # Encrypted value -│ │ │ │ │ │ └── metadata.json # Unencrypted metadata +│ │ │ │ │ │ └── metadata.age # Encrypted metadata │ │ │ │ │ └── 20231216.001/ # Another version -│ │ │ │ └── current -> versions/20231216.001 +│ │ │ │ └── current # Current version's name: 20231216.001 │ │ │ └── database%password/ # Secret: database/password │ │ │ ├── versions/ -│ │ │ └── current -> versions/20231215.001 +│ │ │ └── current # Current version's name: 20231215.001 │ │ ├── vault-metadata.json # Vault metadata │ │ ├── pub.age # Long-term public key │ │ └── current-unlocker # Current unlocker's directory name @@ -307,9 +312,13 @@ Decrypts data using an Age key stored as a secret. │ ├── vault-metadata.json │ ├── pub.age │ └── current-unlocker -└── currentvault -> vaults.d/default +├── currentvault # Current vault's name: default +└── lock # Locked by each command that changes anything ``` +`current`, `currentvault` and `current-unlocker` are plain files that each hold +one name. Changing one replaces it in one rename, so it is never half-written. + ### Key Management and Encryption Flow #### 1: Long-term Keys @@ -336,7 +345,7 @@ Unlockers provide different authentication methods to access the long-term keys: 3. **Keychain Unlockers** (macOS only): - Stores unlock keys in macOS Keychain - - Protected by system authentication (Touch ID, password) + - Kept on this Mac only: the keychain item is never synced to other devices - Automatic unlocking when Keychain is unlocked - Cross-application integration @@ -344,8 +353,10 @@ Unlockers provide different authentication methods to access the long-term keys: - Hardware-backed key storage using Apple Secure Enclave - Uses `sc_auth` / CryptoTokenKit for SE key management (no Apple Developer Program required) - - ECIES encryption: vault long-term key encrypted directly by SE hardware - - Protected by biometric authentication (Touch ID) or system password + - ECIES encryption: the vault long-term key is encrypted directly to the SE + key, and only the SE can decrypt it + - The SE key cannot leave this Mac; using it asks for no Touch ID or + password Each vault maintains its own set of unlockers and one long-term key. The long-term key is encrypted to each unlocker, allowing any authorized unlocker to @@ -355,7 +366,7 @@ access vault secrets. - Each secret version has its own encryption key pair - Private key encrypted to the vault's long-term key -- Provides forward secrecy and granular access control +- A version's private key decrypts only that version's value and metadata ### Environment Variables @@ -505,17 +516,21 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt ### File Formats -- **age Files**: Standard age encryption format (.age extension) -- **Metadata**: Unencrypted JSON format with timestamps and type information -- **Vault Metadata**: JSON containing vault name, creation time, derivation - index, and public key hash +- **age Files**: Standard age encryption format (.age extension), except + `pub.age`, which holds an age public key as text +- **Metadata**: `vault-metadata.json` and `unlocker-metadata.json` are + unencrypted JSON with a creation time, and `unlocker-metadata.json` also + records the unlocker's type; a version's `metadata.age` is JSON encrypted to + the version's public key +- **Vault Metadata**: JSON containing creation time, derivation index, and the + public key hashes described below ### Vault Management - **Derivation Index**: Each vault uses a unique derivation index from the mnemonic, and thus a unique key pair -- **Public Key Hash**: Double SHA-256 hash of the index-0 public key identifies - vaults from the same mnemonic +- **Public Key Hash**: Double SHA-256 hash of the vault's public key; the same + hash of the index-0 public key identifies vaults from the same mnemonic - **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are automatically derived @@ -566,8 +581,6 @@ The project includes comprehensive tests: ```bash make test # Run all tests -go test ./... # Unit tests -go test -tags=integration -v ./internal/cli # Integration tests ``` ## Entrypoints diff --git a/TODO.md b/TODO.md index ff98eeb..e09672e 100644 --- a/TODO.md +++ b/TODO.md @@ -18,6 +18,17 @@ https://git.eeqj.de/sneak/secret/milestone/12 # Completed Steps +- 2026-10-04: README's Storage Architecture, `secret version promote`, + Technical Details and Testing text matches the code + (https://git.eeqj.de/sneak/secret/issues/102). `current` and + `currentvault` are plain files holding a name, not symbolic links; a + version's metadata is the encrypted `metadata.age`; the state directory is + `berlin.sneak.pkg.secret` in the user's configuration directory, not + `~/.local/share/secret`, and holds the `lock` file. Also corrected: the + code sets up no Touch ID for the keychain or Secure Enclave unlocker, and + the Secure Enclave only decrypts; per-version keys give no forward + secrecy; `pub.age` is not age-encrypted; vault metadata holds no vault + name. Testing lists only `make test`. - 2026-10-04: `secret init` and `secret vault create` create a vault whole or not at all (https://git.eeqj.de/sneak/secret/issues/105). `vault.CreateVault` now takes the unlocker passphrase too, writes the vault