Make the README's storage and file format text match the code (closes #102)
check / check (push) Failing after 3s

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
This commit is contained in:
2026-10-04 18:09:03 +00:00
parent f2f89c8a06
commit 8d3e6da65c
2 changed files with 43 additions and 19 deletions
+32 -19
View File
@@ -180,8 +180,8 @@ period.
#### `secret version promote <secret-name> <version>` #### `secret version promote <secret-name> <version>`
Promotes a specific version to current by updating the symlink. Does not modify Promotes a specific version to current by rewriting the secret's `current` file
any timestamps, allowing for rollback scenarios. to name it. Does not modify any timestamps, allowing for rollback scenarios.
#### `secret version remove <secret-name> <version> [--force]` / `secret version rm` ⚠️ 🛑 #### `secret version remove <secret-name> <version> [--force]` / `secret version rm` ⚠️ 🛑
@@ -278,8 +278,13 @@ Decrypts data using an Age key stored as a secret.
### Directory Structure ### 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/ ├── vaults.d/
│ ├── default/ │ ├── default/
│ │ ├── unlockers.d/ │ │ ├── unlockers.d/
@@ -292,12 +297,12 @@ Decrypts data using an Age key stored as a secret.
│ │ │ │ │ │ ├── pub.age # Version public key │ │ │ │ │ │ ├── pub.age # Version public key
│ │ │ │ │ │ ├── priv.age # Version private key (encrypted) │ │ │ │ │ │ ├── priv.age # Version private key (encrypted)
│ │ │ │ │ │ ├── value.age # Encrypted value │ │ │ │ │ │ ├── value.age # Encrypted value
│ │ │ │ │ │ └── metadata.json # Unencrypted metadata │ │ │ │ │ │ └── metadata.age # Encrypted metadata
│ │ │ │ │ └── 20231216.001/ # Another version │ │ │ │ │ └── 20231216.001/ # Another version
│ │ │ │ └── current -> versions/20231216.001 │ │ │ │ └── current # Current version's name: 20231216.001
│ │ │ └── database%password/ # Secret: database/password │ │ │ └── database%password/ # Secret: database/password
│ │ │ ├── versions/ │ │ │ ├── versions/
│ │ │ └── current -> versions/20231215.001 │ │ │ └── current # Current version's name: 20231215.001
│ │ ├── vault-metadata.json # Vault metadata │ │ ├── vault-metadata.json # Vault metadata
│ │ ├── pub.age # Long-term public key │ │ ├── pub.age # Long-term public key
│ │ └── current-unlocker # Current unlocker's directory name │ │ └── current-unlocker # Current unlocker's directory name
@@ -307,9 +312,13 @@ Decrypts data using an Age key stored as a secret.
│ ├── vault-metadata.json │ ├── vault-metadata.json
│ ├── pub.age │ ├── pub.age
│ └── current-unlocker │ └── 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 ### Key Management and Encryption Flow
#### 1: Long-term Keys #### 1: Long-term Keys
@@ -336,7 +345,7 @@ Unlockers provide different authentication methods to access the long-term keys:
3. **Keychain Unlockers** (macOS only): 3. **Keychain Unlockers** (macOS only):
- Stores unlock keys in macOS Keychain - 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 - Automatic unlocking when Keychain is unlocked
- Cross-application integration - 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 - Hardware-backed key storage using Apple Secure Enclave
- Uses `sc_auth` / CryptoTokenKit for SE key management (no Apple Developer - Uses `sc_auth` / CryptoTokenKit for SE key management (no Apple Developer
Program required) Program required)
- ECIES encryption: vault long-term key encrypted directly by SE hardware - ECIES encryption: the vault long-term key is encrypted directly to the SE
- Protected by biometric authentication (Touch ID) or system password 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 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 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 - Each secret version has its own encryption key pair
- Private key encrypted to the vault's long-term key - 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 ### Environment Variables
@@ -505,17 +516,21 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
### File Formats ### File Formats
- **age Files**: Standard age encryption format (.age extension) - **age Files**: Standard age encryption format (.age extension), except
- **Metadata**: Unencrypted JSON format with timestamps and type information `pub.age`, which holds an age public key as text
- **Vault Metadata**: JSON containing vault name, creation time, derivation - **Metadata**: `vault-metadata.json` and `unlocker-metadata.json` are
index, and public key hash 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 ### Vault Management
- **Derivation Index**: Each vault uses a unique derivation index from the - **Derivation Index**: Each vault uses a unique derivation index from the
mnemonic, and thus a unique key pair mnemonic, and thus a unique key pair
- **Public Key Hash**: Double SHA-256 hash of the index-0 public key identifies - **Public Key Hash**: Double SHA-256 hash of the vault's public key; the same
vaults from the same mnemonic hash of the index-0 public key identifies vaults from the same mnemonic
- **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are - **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are
automatically derived automatically derived
@@ -566,8 +581,6 @@ The project includes comprehensive tests:
```bash ```bash
make test # Run all tests make test # Run all tests
go test ./... # Unit tests
go test -tags=integration -v ./internal/cli # Integration tests
``` ```
## Entrypoints ## Entrypoints
+11
View File
@@ -18,6 +18,17 @@ https://git.eeqj.de/sneak/secret/milestone/12
# Completed Steps # 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: A failed `secret unlocker add keychain` or - 2026-10-04: A failed `secret unlocker add keychain` or
`secret unlocker add secure-enclave` no longer leaves its keychain item or `secret unlocker add secure-enclave` no longer leaves its keychain item or
Secure Enclave key behind (https://git.eeqj.de/sneak/secret/issues/89). Secure Enclave key behind (https://git.eeqj.de/sneak/secret/issues/89).