From ec202eb07beeb6d2bea544cc024cf898e788eb60 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 4 Oct 2026 15:51:33 +0000 Subject: [PATCH] Add the README's required sections and clear stale TODO.md items (closes #46) README gains Description, Getting Started, Rationale, Design, TODO and License sections; its first sentence names the licence and author. Installation and Quick Start become Getting Started; Core Architecture becomes Design, whose two false version bullets (symlink switching, unencrypted metadata) are corrected. The README is rewrapped to prettier's settings. TODO.md: Next Step points at the 1.0.0 milestone, its four finished items move to Completed Steps with their dates, and Future Steps loses the items already done. Model: opus-5-5 --- README.md | 318 ++++++++++++++++++++++++++++++------------------------ TODO.md | 41 +++---- 2 files changed, 191 insertions(+), 168 deletions(-) diff --git a/README.md b/README.md index 24dd19a..929dc86 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,69 @@ # secret - Local Secret Manager -secret is a command-line local secret manager that implements a hierarchical -key architecture for storing and managing sensitive data. It supports -multiple vaults, various unlock mechanisms, and provides secure storage -using the `age` encryption library. +## Description -It could be used as password manager, but was not designed as such. I -created it to scratch an itch for a secure key/value store for replacing a -bunch of pgp-encrypted files in a directory structure. +`secret` is a WTFPL-licensed Go command-line local secret manager by +[@sneak](https://sneak.berlin) that implements a hierarchical key architecture +for storing and managing sensitive data. It supports multiple vaults, various +unlock mechanisms, and provides secure storage using the `age` encryption +library. -## Core Architecture +## Getting Started + +Build from source, then install the binary as `~/bin/secret`: + +```bash +git clone https://git.eeqj.de/sneak/secret.git +cd secret +make build # writes the binary to ./secret +make install # builds it and copies it to ~/bin/secret +``` + +Generate a mnemonic, create the default vault, then store and read a secret: + +```bash +secret generate mnemonic # prints a new BIP39 mnemonic; write it down +secret init # asks for that mnemonic and an unlocker passphrase +echo "my-password" | secret add myservice/password +secret get myservice/password +``` + +## Rationale + +I created `secret` to scratch an itch: I wanted a secure key/value store to +replace a bunch of PGP-encrypted files in a directory structure. It could be +used as a password manager, but was not designed as one. + +## Design ### Three-Layer Key Hierarchy Secret implements a three-layer key architecture: -1. **Long-term Keys**: Derived from BIP39 mnemonic phrases, these provide - the foundation for all encryption -2. **Unlockers**: Short-term keys that encrypt the long-term keys, - supporting multiple authentication methods -3. **Version-specific Keys**: Per-version keys that encrypt individual - secret values +1. **Long-term Keys**: Derived from BIP39 mnemonic phrases, these provide the + foundation for all encryption +2. **Unlockers**: Short-term keys that encrypt the long-term keys, supporting + multiple authentication methods +3. **Version-specific Keys**: Per-version keys that encrypt individual secret + values ### Version Management Each secret maintains a history of versions, with each version having: + - Its own encryption key pair -- Metadata (unencrypted) including creation time and validity period +- Metadata including creation time and validity period, encrypted to the + version's key pair - Immutable value storage -- Atomic version switching via symlink updates + +The secret's `current` file names its current version. Switching versions +replaces that file in one rename, so it is never half-written. ### Vault System -Vaults provide logical separation of secrets, each with its own long-term -key and unlocker set. This allows for complete isolation between different -contexts (work, personal, projects). - -## Installation - -Build from source: -```bash -git clone -cd secret -make build -``` - -## Quick Start - -1. **Initialize the secret manager**: - ```bash - secret init - ``` - This creates the default vault and prompts for a BIP39 mnemonic phrase. - -2. **Generate a mnemonic** (if needed): - ```bash - secret generate mnemonic - ``` - -3. **Add a secret**: - ```bash - echo "my-password" | secret add myservice/password - ``` - -4. **Retrieve a secret**: - ```bash - secret get myservice/password - ``` +Vaults provide logical separation of secrets, each with its own long-term key +and unlocker set. This allows for complete isolation between different contexts +(work, personal, projects). ## Commands Reference @@ -74,10 +71,10 @@ make build `secret rm`, `secret version rm`, `secret vault remove` and `secret unlocker remove` destroy data that exists nowhere else. On a terminal -each one first asks `[y/N]`, naming exactly what it is about to remove, and -goes ahead only on `y` or `yes`; any other answer, a bare Enter included, -cancels and removes nothing. The question is asked only after the command's -checks have passed, and before it changes anything. +each one first asks `[y/N]`, naming exactly what it is about to remove, and goes +ahead only on `y` or `yes`; any other answer, a bare Enter included, cancels and +removes nothing. The question is asked only after the command's checks have +passed, and before it changes anything. Whether to ask is decided by stdin, where the answer is read from, so `secret rm foo | tee log` still asks. When stdin is not a terminal, as in a @@ -96,6 +93,7 @@ Initializes the secret manager with a default vault. Prompts for a BIP39 mnemonic phrase and creates the initial directory structure. **Environment Variables:** + - `SB_SECRET_MNEMONIC`: Pre-set mnemonic phrase - `SB_UNLOCK_PASSPHRASE`: Pre-set unlock passphrase @@ -118,8 +116,8 @@ Switches to the specified vault for subsequent operations. #### `secret vault remove [--force]` / `secret vault rm` ⚠️ 🛑 -**DANGER**: Permanently removes a vault and all its secrets. It first asks -for confirmation, naming the vault and how many secrets it holds (see +**DANGER**: Permanently removes a vault and all its secrets. It first asks for +confirmation, naming the vault and how many secrets it holds (see [Confirmation Before Removal](#confirmation-before-removal)). The last vault cannot be removed. Removing the current vault makes another vault the current one. @@ -132,58 +130,65 @@ one. #### `secret add [--force]` Adds a secret to the current vault. Reads the secret value from stdin. + - `--force, -f`: Overwrite existing secret -**Secret Name Format:** only ASCII letters, digits, `.`, `-`, `_` and `/` -are allowed, and a name must not be empty, start with `.` or `/`, end with -`/`, contain `//`, or have `..` as a path segment. +**Secret Name Format:** only ASCII letters, digits, `.`, `-`, `_` and `/` are +allowed, and a name must not be empty, start with `.` or `/`, end with `/`, +contain `//`, or have `..` as a path segment. + - Forward slashes (`/`) are converted to percent signs (`%`) for storage - Examples: `database/password`, `api.key`, `ssh_private_key` #### `secret get [--version ]` Retrieves and outputs a secret value to stdout. + - `--version, -v`: Get a specific version (default: current) #### `secret list [filter] [--json]` / `secret ls` -Lists all secrets in the current vault. Optional filter for substring -matching. +Lists all secrets in the current vault. Optional filter for substring matching. #### `secret remove [--force]` / `secret rm` ⚠️ 🛑 -**DANGER**: Permanently removes a secret and ALL its versions. It first asks -for confirmation, naming the secret, its vault and how many versions it has -(see [Confirmation Before Removal](#confirmation-before-removal)). +**DANGER**: Permanently removes a secret and ALL its versions. It first asks for +confirmation, naming the secret, its vault and how many versions it has (see +[Confirmation Before Removal](#confirmation-before-removal)). + - `--force, -f`: Remove without asking - **NO RECOVERY**: Once removed, the secret cannot be recovered -- **ALL VERSIONS DELETED**: Every version of the secret will be permanently deleted +- **ALL VERSIONS DELETED**: Every version of the secret will be permanently + deleted #### `secret move ` / `secret mv` / `secret rename` Moves or renames a secret within the current vault. + - Fails if the destination already exists -- Fails if the destination is the source under another name, such as `foo` - for `Foo` on a case-insensitive filesystem (the macOS default); there, to - change only the case of a name, move the secret to a third name first +- Fails if the destination is the source under another name, such as `foo` for + `Foo` on a case-insensitive filesystem (the macOS default); there, to change + only the case of a name, move the secret to a third name first - Preserves all versions and metadata ### Version Management #### `secret version list ` / `secret version ls` -Lists all versions of a secret showing creation time, status, and validity period. +Lists all versions of a secret showing creation time, status, and validity +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 updating the symlink. Does not modify +any timestamps, allowing for rollback scenarios. #### `secret version remove [--force]` / `secret version rm` ⚠️ 🛑 **DANGER**: Permanently removes a specific version of a secret. It first asks for confirmation, naming the version, the secret and its vault (see [Confirmation Before Removal](#confirmation-before-removal)). + - `--force, -f`: Remove without asking - **NO RECOVERY**: Once removed, this version cannot be recovered - Cannot remove the current version (must promote another version first) @@ -197,6 +202,7 @@ Generates a cryptographically secure BIP39 mnemonic phrase. #### `secret generate secret [--length=16] [--type=base58] [--force]` Generates and stores a random secret. + - `--length, -l`: Length of generated secret (default: 16) - `--type, -t`: Type of secret (`base58`, `alnum`) - `--force, -f`: Overwrite existing secret @@ -212,16 +218,19 @@ Lists all unlockers in the current vault with their metadata. Creates a new unlocker of the specified type: **Types:** + - `passphrase`: Traditional passphrase-protected unlocker - `pgp`: Uses an existing GPG key for encryption/decryption - `keychain`: macOS Keychain integration (macOS only) - `secure-enclave`: Hardware-backed Secure Enclave protection (macOS only) **Options:** -- `--keyid `: GPG key ID (optional for PGP type, uses default key if not specified) -A vault has one passphrase unlocker: adding one replaces the one the vault -has, which is removed only once the new one is the current unlocker. +- `--keyid `: GPG key ID (optional for PGP type, uses default key if not + specified) + +A vault has one passphrase unlocker: adding one replaces the one the vault has, +which is removed only once the new one is the current unlocker. #### `secret unlocker remove [--force]` / `secret unlocker rm` ⚠️ 🛑 @@ -230,9 +239,9 @@ naming the unlocker and its vault and saying whether it is the vault's last unlocker; for the last one it says how many secrets the vault holds and warns that the vault then opens only with its mnemonic (see [Confirmation Before Removal](#confirmation-before-removal)). An unlocker -directory that `secret unlocker list` skips with a warning, because its -metadata cannot be read or parsed, is removed by the directory name the -warning gives. +directory that `secret unlocker list` skips with a warning, because its metadata +cannot be read or parsed, is removed by the directory name the warning gives. + - `--force, -f`: Remove without asking, even the last unlocker - **CRITICAL WARNING**: Without unlockers and without your mnemonic phrase, vault data will be PERMANENTLY INACCESSIBLE @@ -247,7 +256,8 @@ Selects an unlocker as the current default for operations. #### `secret import --source ` -Imports a secret from a file and stores it in the current vault under the given name. +Imports a secret from a file and stores it in the current vault under the given +name. #### `secret vault import [vault-name]` @@ -257,7 +267,8 @@ Imports a mnemonic phrase into the specified vault (defaults to "default"). #### `secret encrypt [--input=file] [--output=file]` -Encrypts data using an Age key stored as a secret. If the secret doesn't exist, generates a new Age key. +Encrypts data using an Age key stored as a secret. If the secret doesn't exist, +generates a new Age key. #### `secret decrypt [--input=file] [--output=file]` @@ -302,37 +313,43 @@ Decrypts data using an Age key stored as a secret. ### Key Management and Encryption Flow #### 1: Long-term Keys -- **Source**: Derived from BIP39 mnemonic phrases using hierarchical deterministic (HD) key derivation + +- **Source**: Derived from BIP39 mnemonic phrases using hierarchical + deterministic (HD) key derivation - **Purpose**: Master keys for each vault, used to encrypt secret-specific keys -- **Storage**: Public key stored as `pub.age`, private key encrypted by unlockers +- **Storage**: Public key stored as `pub.age`, private key encrypted by + unlockers #### 2: Unlockers Unlockers provide different authentication methods to access the long-term keys: 1. **Passphrase Unlockers**: - - Encrypted with user-provided passphrase - - Stored as encrypted Age keys - - Cross-platform compatible + - Encrypted with user-provided passphrase + - Stored as encrypted Age keys + - Cross-platform compatible 2. **PGP Unlockers**: - - Uses existing GPG key infrastructure - - Leverages existing key management workflows - - Strong authentication through GPG + - Uses existing GPG key infrastructure + - Leverages existing key management workflows + - Strong authentication through GPG 3. **Keychain Unlockers** (macOS only): - - Stores unlock keys in macOS Keychain - - Protected by system authentication (Touch ID, password) - - Automatic unlocking when Keychain is unlocked - - Cross-application integration + - Stores unlock keys in macOS Keychain + - Protected by system authentication (Touch ID, password) + - Automatic unlocking when Keychain is unlocked + - Cross-application integration 4. **Secure Enclave Unlockers** (macOS): - - 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 + - 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 -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 access vault secrets. +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 +access vault secrets. #### 3: Secret-specific Keys @@ -352,18 +369,19 @@ they hold. Other processes running as the same user can read a process's environment (on Linux, from `/proc//environ`). Every child process of the shell or script that sets them inherits them, `gpg` included. Set on a command line or in a CI job, they end up in shell history and CI logs. `secret` unsets -each one as soon as it has read it, so that the programs it runs itself, such -as `gpg`, do not inherit it, but that erases nothing: the environment the -process started with, and its memory, still hold the value. The interactive -prompt, which every command except `secret vault import` offers when the -variable is not set, is the safer default; `secret vault import` has no prompt -and needs both variables. +each one as soon as it has read it, so that the programs it runs itself, such as +`gpg`, do not inherit it, but that erases nothing: the environment the process +started with, and its memory, still hold the value. The interactive prompt, +which every command except `secret vault import` offers when the variable is not +set, is the safer default; `secret vault import` has no prompt and needs both +variables. ## Security Features ### Encryption -- Uses the [age encryption library](https://age-encryption.org/) with X25519 keys +- Uses the [age encryption library](https://age-encryption.org/) with X25519 + keys - All private keys are encrypted at rest - No plaintext secrets stored on disk @@ -382,7 +400,8 @@ and needs both variables. - Hardware token support via PGP/GPG integration - macOS Keychain integration for system-level security -- Secure Enclave integration for hardware-backed key protection (macOS, via `sc_auth` / CryptoTokenKit) +- Secure Enclave integration for hardware-backed key protection (macOS, via + `sc_auth` / CryptoTokenKit) ## Examples @@ -431,6 +450,7 @@ secret vault remove personal --force ``` ### Advanced Authentication + ```bash # Add multiple unlock methods secret unlocker add passphrase # Password-based @@ -477,21 +497,27 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt ## Technical Details ### Cryptographic Primitives + - **Key Derivation**: BIP32/BIP39 hierarchical deterministic key derivation - **Encryption**: Age (X25519 + ChaCha20-Poly1305) - **Authentication**: Poly1305 MAC - **Hashing**: Double SHA-256 for public key identification ### 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 +- **Vault Metadata**: JSON containing vault name, creation time, derivation + index, and public key hash ### 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 -- **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are automatically derived +- **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 +- **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are + automatically derived ### Cross-Platform Support @@ -523,6 +549,7 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt ## Development ### Building + ```bash make build # Build binary make test # Run tests @@ -530,7 +557,9 @@ make lint # Run linter ``` ### Testing + The project includes comprehensive tests: + ```bash make test # Run all tests go test ./... # Unit tests @@ -542,55 +571,62 @@ go test -tags=integration -v ./internal/cli # Integration tests This repository adheres to the [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) standard: normalized scripts in `script/` are the entrypoints for the -development workflow, and the Makefile targets are thin shims that call -them. We provide: +development workflow, and the Makefile targets are thin shims that call them. We +provide: -- `script/bootstrap` — install all dependencies (Go, Go module - download), idempotently; golangci-lint is not installed, it runs in - docker +- `script/bootstrap` — install all dependencies (Go, Go module download), + idempotently; golangci-lint is not installed, it runs in docker - `script/setup` — make a fresh clone ready for development: runs `script/bootstrap`, then `script/install-precommit` -- `script/projectname` — output the project name (`secret`); used by - other scripts such as `script/docker` -- `script/build` — build the `secret` binary into the repo root, stamping - the version (`VERSION` from the environment, else `git describe`) and - the git commit -- `script/test` — run `go vet` and the test suite (verbose rerun on - failure) -- `script/lint` — run `golangci-lint` in docker only: builds - `Dockerfile.lint`, where the linter is a build step that runs on every - call, also on an unchanged tree +- `script/projectname` — output the project name (`secret`); used by other + scripts such as `script/docker` +- `script/build` — build the `secret` binary into the repo root, stamping the + version (`VERSION` from the environment, else `git describe`) and the git + commit +- `script/test` — run `go vet` and the test suite (verbose rerun on failure) +- `script/lint` — run `golangci-lint` in docker only: builds `Dockerfile.lint`, + where the linter is a build step that runs on every call, also on an unchanged + tree - `script/fmt` — format all Go code (writes) - `script/fmt-check` — check formatting without writing -- `script/check` — run `script/test`, `script/lint`, and - `script/fmt-check` +- `script/check` — run `script/test`, `script/lint`, and `script/fmt-check` - `script/docker` — build the Docker image tagged with the project name -- `script/cibuild` — CI entrypoint: `docker build --ulimit - memlock=-1:-1 .` (memguard needs mlock; the Dockerfile runs the - checks), with a new `CHECK_EPOCH` build argument on every run so the - checks run again on an unchanged tree -- `script/precommit` — pre-commit checks: `go mod tidy` verification, - then `script/check` -- `script/install-precommit` — install the git pre-commit hook that - runs `script/precommit` +- `script/cibuild` — CI entrypoint: `docker build --ulimit memlock=-1:-1 .` + (memguard needs mlock; the Dockerfile runs the checks), with a new + `CHECK_EPOCH` build argument on every run so the checks run again on an + unchanged tree +- `script/precommit` — pre-commit checks: `go mod tidy` verification, then + `script/check` +- `script/install-precommit` — install the git pre-commit hook that runs + `script/precommit` ## Features -- **Multiple Authentication Methods**: Supports passphrase, PGP, macOS Keychain, and Secure Enclave unlockers +- **Multiple Authentication Methods**: Supports passphrase, PGP, macOS Keychain, + and Secure Enclave unlockers - **Vault Isolation**: Complete separation between different vaults - **Per-Secret Encryption**: Each secret has its own encryption key - **BIP39 Mnemonic Support**: Keyless operation using mnemonic phrases - **Cross-Platform**: Works on macOS, Linux, and other Unix-like systems -# Author +## TODO -Made with love and lots of expensive SOTA AI by -[sneak](https://sneak.berlin) in Berlin in the summer of 2025. +Open work is tracked on the +[issue tracker](https://git.eeqj.de/sneak/secret/issues), which is +authoritative. The work to be done before 1.0 is the +[`1.0.0` milestone](https://git.eeqj.de/sneak/secret/milestone/12). `TODO.md` +records the steps completed so far. -Released as a free software gift to the world, no strings attached, under -the [WTFPL](https://www.wtfpl.net/) license. +## License + +Released as a free software gift to the world, no strings attached, under the +[WTFPL](https://www.wtfpl.net/) license; see [`LICENSE`](LICENSE). + +## Author + +Made with love and lots of expensive SOTA AI by [@sneak](https://sneak.berlin) +in Berlin in the summer of 2025. Contact: [sneak@sneak.berlin](mailto:sneak@sneak.berlin) [https://keys.openpgp.org/vks/v1/by-fingerprint/5539AD00DE4C42F3AFE11575052443F4DF2A55C2](https://keys.openpgp.org/vks/v1/by-fingerprint/5539AD00DE4C42F3AFE11575052443F4DF2A55C2) - diff --git a/TODO.md b/TODO.md index 401fbca..d1a4dad 100644 --- a/TODO.md +++ b/TODO.md @@ -10,18 +10,13 @@ # Status -pre-1.0. No git tags. TODO.md carries open 1.0 security blockers. Work in -flight on branch secure-enclave-unlocker (clean tree as of 2026-07-06). +pre-1.0. No git tags. Open work is tracked on the issue tracker, which is +authoritative. # Next Step -Bring the repo into policy compliance in one commit: - -- Add fmt-check and hooks targets to the Makefile (test/lint/fmt/check/ - docker already exist). -- Add REPO_POLICIES.md and .editorconfig. -- Add .gitea/workflows/check.yml running make check. -- Verify Dockerfile base images are pinned by sha256. +Take the next open issue in the `1.0.0` milestone: +https://git.eeqj.de/sneak/secret/milestone/12 # Completed Steps @@ -266,8 +261,15 @@ Bring the repo into policy compliance in one commit: `findUnlockerIDByMetadata` now returns an error so `unlocker list` skips an unreadable `unlockers.d` entry with a warning instead of emitting a fabricated fallback ID. +- 2026-08-07: Added `.editorconfig` + (https://git.eeqj.de/sneak/secret/issues/27). - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile shims, README Entrypoints section +- 2026-07-07: Added `REPO_POLICIES.md` and the `make hooks` target; + `.gitea/workflows/check.yml` now runs `script/cibuild`. +- 2026-03-30: Added the `make fmt-check` target and + `.gitea/workflows/check.yml`, which runs `docker build` on every push; the + `Dockerfile` base images are pinned by sha256. - 2026-03-11: Secure Enclave unlocker for hardware-backed secret protection, plus review fixes (stub panics, derivation index, tests, README) on branch secure-enclave-unlocker. @@ -289,8 +291,6 @@ Bring the repo into policy compliance in one commit: # Future Steps -- Compliance (after Next Step lands): keep main green under the new - .gitea workflow; run make check before every merge. - Implement version-number shell completion for the second arg of `secret version promote` and `secret version rm` (`internal/cli/version.go`; was an in-code TODO removed for godox). @@ -302,23 +302,10 @@ Bring the repo into policy compliance in one commit: tests) are not linted on the Linux CI runner and still contain lines over the new 88-column limit; they will surface if lint ever runs on macOS. -- Merge secure-enclave-unlocker to main once review is done. - 1.0 critical security blockers (from repo TODO.md): - - Command injection: GPG key IDs passed unescaped to exec.Command - (pgpunlocker.go:323-327); data.String() passed unescaped to the - security command (keychainunlocker.go:472-476). - - Memory security: age identity .String() creates unprotected - copies (keychainunlocker.go:356, pgpunlocker.go:256, - version.go:155); age secret key held in a plain string in - cli/crypto.go:86,91,113; private keys exposed via buffer.Bytes() - to GPGEncryptFunc and EncryptWithPassphrase. - - Input validation: no maximum secret size (DoS). - - Timing attacks: bytes.Equal passphrase compare (cli/init.go: - 209-216); non-constant-time public key compare (vault.go:95-100). -- High priority: - - Secure temporary file handling and cleanup. - - Initialize a default unlock key at vault creation. - - Add secret rm and vault deletion commands. + - Memory security: age identity .String() creates unprotected copies of + private keys; the call sites are listed in + https://git.eeqj.de/sneak/secret/issues/38. - Medium priority: - Standardize error messages; stop leaking internals. - Graceful handling of corrupted or missing key files with recovery