Add the README's required sections and clear stale TODO.md items (closes #46)
check / check (push) Failing after 2s

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
This commit is contained in:
2026-10-04 15:51:33 +00:00
parent 1cc8653981
commit ec202eb07b
2 changed files with 191 additions and 168 deletions
+164 -128
View File
@@ -1,72 +1,69 @@
# secret - Local Secret Manager # secret - Local Secret Manager
secret is a command-line local secret manager that implements a hierarchical ## Description
key architecture for storing and managing sensitive data. It supports
multiple vaults, various unlock mechanisms, and provides secure storage
using the `age` encryption library.
It could be used as password manager, but was not designed as such. I `secret` is a WTFPL-licensed Go command-line local secret manager by
created it to scratch an itch for a secure key/value store for replacing a [@sneak](https://sneak.berlin) that implements a hierarchical key architecture
bunch of pgp-encrypted files in a directory structure. 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 ### Three-Layer Key Hierarchy
Secret implements a three-layer key architecture: Secret implements a three-layer key architecture:
1. **Long-term Keys**: Derived from BIP39 mnemonic phrases, these provide 1. **Long-term Keys**: Derived from BIP39 mnemonic phrases, these provide the
the foundation for all encryption foundation for all encryption
2. **Unlockers**: Short-term keys that encrypt the long-term keys, 2. **Unlockers**: Short-term keys that encrypt the long-term keys, supporting
supporting multiple authentication methods multiple authentication methods
3. **Version-specific Keys**: Per-version keys that encrypt individual 3. **Version-specific Keys**: Per-version keys that encrypt individual secret
secret values values
### Version Management ### Version Management
Each secret maintains a history of versions, with each version having: Each secret maintains a history of versions, with each version having:
- Its own encryption key pair - 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 - 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 ### Vault System
Vaults provide logical separation of secrets, each with its own long-term Vaults provide logical separation of secrets, each with its own long-term key
key and unlocker set. This allows for complete isolation between different and unlocker set. This allows for complete isolation between different contexts
contexts (work, personal, projects). (work, personal, projects).
## Installation
Build from source:
```bash
git clone <repository>
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
```
## Commands Reference ## Commands Reference
@@ -74,10 +71,10 @@ make build
`secret rm`, `secret version rm`, `secret vault remove` and `secret rm`, `secret version rm`, `secret vault remove` and
`secret unlocker remove` destroy data that exists nowhere else. On a terminal `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 each one first asks `[y/N]`, naming exactly what it is about to remove, and goes
goes ahead only on `y` or `yes`; any other answer, a bare Enter included, ahead only on `y` or `yes`; any other answer, a bare Enter included, cancels and
cancels and removes nothing. The question is asked only after the command's removes nothing. The question is asked only after the command's checks have
checks have passed, and before it changes anything. passed, and before it changes anything.
Whether to ask is decided by stdin, where the answer is read from, so 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 `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. mnemonic phrase and creates the initial directory structure.
**Environment Variables:** **Environment Variables:**
- `SB_SECRET_MNEMONIC`: Pre-set mnemonic phrase - `SB_SECRET_MNEMONIC`: Pre-set mnemonic phrase
- `SB_UNLOCK_PASSPHRASE`: Pre-set unlock passphrase - `SB_UNLOCK_PASSPHRASE`: Pre-set unlock passphrase
@@ -118,8 +116,8 @@ Switches to the specified vault for subsequent operations.
#### `secret vault remove <name> [--force]` / `secret vault rm` ⚠️ 🛑 #### `secret vault remove <name> [--force]` / `secret vault rm` ⚠️ 🛑
**DANGER**: Permanently removes a vault and all its secrets. It first asks **DANGER**: Permanently removes a vault and all its secrets. It first asks for
for confirmation, naming the vault and how many secrets it holds (see confirmation, naming the vault and how many secrets it holds (see
[Confirmation Before Removal](#confirmation-before-removal)). The last vault [Confirmation Before Removal](#confirmation-before-removal)). The last vault
cannot be removed. Removing the current vault makes another vault the current cannot be removed. Removing the current vault makes another vault the current
one. one.
@@ -132,58 +130,65 @@ one.
#### `secret add <secret-name> [--force]` #### `secret add <secret-name> [--force]`
Adds a secret to the current vault. Reads the secret value from stdin. Adds a secret to the current vault. Reads the secret value from stdin.
- `--force, -f`: Overwrite existing secret - `--force, -f`: Overwrite existing secret
**Secret Name Format:** only ASCII letters, digits, `.`, `-`, `_` and `/` **Secret Name Format:** only ASCII letters, digits, `.`, `-`, `_` and `/` are
are allowed, and a name must not be empty, start with `.` or `/`, end with allowed, and a name must not be empty, start with `.` or `/`, end with `/`,
`/`, contain `//`, or have `..` as a path segment. contain `//`, or have `..` as a path segment.
- Forward slashes (`/`) are converted to percent signs (`%`) for storage - Forward slashes (`/`) are converted to percent signs (`%`) for storage
- Examples: `database/password`, `api.key`, `ssh_private_key` - Examples: `database/password`, `api.key`, `ssh_private_key`
#### `secret get <secret-name> [--version <version>]` #### `secret get <secret-name> [--version <version>]`
Retrieves and outputs a secret value to stdout. Retrieves and outputs a secret value to stdout.
- `--version, -v`: Get a specific version (default: current) - `--version, -v`: Get a specific version (default: current)
#### `secret list [filter] [--json]` / `secret ls` #### `secret list [filter] [--json]` / `secret ls`
Lists all secrets in the current vault. Optional filter for substring Lists all secrets in the current vault. Optional filter for substring matching.
matching.
#### `secret remove <secret-name> [--force]` / `secret rm` ⚠️ 🛑 #### `secret remove <secret-name> [--force]` / `secret rm` ⚠️ 🛑
**DANGER**: Permanently removes a secret and ALL its versions. It first asks **DANGER**: Permanently removes a secret and ALL its versions. It first asks for
for confirmation, naming the secret, its vault and how many versions it has confirmation, naming the secret, its vault and how many versions it has (see
(see [Confirmation Before Removal](#confirmation-before-removal)). [Confirmation Before Removal](#confirmation-before-removal)).
- `--force, -f`: Remove without asking - `--force, -f`: Remove without asking
- **NO RECOVERY**: Once removed, the secret cannot be recovered - **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 <source> <destination>` / `secret mv` / `secret rename` #### `secret move <source> <destination>` / `secret mv` / `secret rename`
Moves or renames a secret within the current vault. Moves or renames a secret within the current vault.
- Fails if the destination already exists - Fails if the destination already exists
- Fails if the destination is the source under another name, such as `foo` - Fails if the destination is the source under another name, such as `foo` for
for `Foo` on a case-insensitive filesystem (the macOS default); there, to `Foo` on a case-insensitive filesystem (the macOS default); there, to change
change only the case of a name, move the secret to a third name first only the case of a name, move the secret to a third name first
- Preserves all versions and metadata - Preserves all versions and metadata
### Version Management ### Version Management
#### `secret version list <secret-name>` / `secret version ls` #### `secret version list <secret-name>` / `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 <secret-name> <version>` #### `secret version promote <secret-name> <version>`
Promotes a specific version to current by updating the symlink. Does not Promotes a specific version to current by updating the symlink. Does not modify
modify any timestamps, allowing for rollback scenarios. 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` ⚠️ 🛑
**DANGER**: Permanently removes a specific version of a secret. It first asks **DANGER**: Permanently removes a specific version of a secret. It first asks
for confirmation, naming the version, the secret and its vault (see for confirmation, naming the version, the secret and its vault (see
[Confirmation Before Removal](#confirmation-before-removal)). [Confirmation Before Removal](#confirmation-before-removal)).
- `--force, -f`: Remove without asking - `--force, -f`: Remove without asking
- **NO RECOVERY**: Once removed, this version cannot be recovered - **NO RECOVERY**: Once removed, this version cannot be recovered
- Cannot remove the current version (must promote another version first) - Cannot remove the current version (must promote another version first)
@@ -197,6 +202,7 @@ Generates a cryptographically secure BIP39 mnemonic phrase.
#### `secret generate secret <name> [--length=16] [--type=base58] [--force]` #### `secret generate secret <name> [--length=16] [--type=base58] [--force]`
Generates and stores a random secret. Generates and stores a random secret.
- `--length, -l`: Length of generated secret (default: 16) - `--length, -l`: Length of generated secret (default: 16)
- `--type, -t`: Type of secret (`base58`, `alnum`) - `--type, -t`: Type of secret (`base58`, `alnum`)
- `--force, -f`: Overwrite existing secret - `--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: Creates a new unlocker of the specified type:
**Types:** **Types:**
- `passphrase`: Traditional passphrase-protected unlocker - `passphrase`: Traditional passphrase-protected unlocker
- `pgp`: Uses an existing GPG key for encryption/decryption - `pgp`: Uses an existing GPG key for encryption/decryption
- `keychain`: macOS Keychain integration (macOS only) - `keychain`: macOS Keychain integration (macOS only)
- `secure-enclave`: Hardware-backed Secure Enclave protection (macOS only) - `secure-enclave`: Hardware-backed Secure Enclave protection (macOS only)
**Options:** **Options:**
- `--keyid <id>`: 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 - `--keyid <id>`: GPG key ID (optional for PGP type, uses default key if not
has, which is removed only once the new one is the current unlocker. 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 <unlocker-id> [--force]` / `secret unlocker rm` ⚠️ 🛑 #### `secret unlocker remove <unlocker-id> [--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 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 that the vault then opens only with its mnemonic (see
[Confirmation Before Removal](#confirmation-before-removal)). An unlocker [Confirmation Before Removal](#confirmation-before-removal)). An unlocker
directory that `secret unlocker list` skips with a warning, because its directory that `secret unlocker list` skips with a warning, because its metadata
metadata cannot be read or parsed, is removed by the directory name the cannot be read or parsed, is removed by the directory name the warning gives.
warning gives.
- `--force, -f`: Remove without asking, even the last unlocker - `--force, -f`: Remove without asking, even the last unlocker
- **CRITICAL WARNING**: Without unlockers and without your mnemonic phrase, - **CRITICAL WARNING**: Without unlockers and without your mnemonic phrase,
vault data will be PERMANENTLY INACCESSIBLE vault data will be PERMANENTLY INACCESSIBLE
@@ -247,7 +256,8 @@ Selects an unlocker as the current default for operations.
#### `secret import <secret-name> --source <filename>` #### `secret import <secret-name> --source <filename>`
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]` #### `secret vault import [vault-name]`
@@ -257,7 +267,8 @@ Imports a mnemonic phrase into the specified vault (defaults to "default").
#### `secret encrypt <secret-name> [--input=file] [--output=file]` #### `secret encrypt <secret-name> [--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 <secret-name> [--input=file] [--output=file]` #### `secret decrypt <secret-name> [--input=file] [--output=file]`
@@ -302,9 +313,12 @@ Decrypts data using an Age key stored as a secret.
### Key Management and Encryption Flow ### Key Management and Encryption Flow
#### 1: Long-term Keys #### 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 - **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 #### 2: Unlockers
@@ -328,11 +342,14 @@ Unlockers provide different authentication methods to access the long-term keys:
4. **Secure Enclave Unlockers** (macOS): 4. **Secure Enclave Unlockers** (macOS):
- 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 Program required) - Uses `sc_auth` / CryptoTokenKit for SE key management (no Apple Developer
Program required)
- ECIES encryption: vault long-term key encrypted directly by SE hardware - ECIES encryption: vault long-term key encrypted directly by SE hardware
- Protected by biometric authentication (Touch ID) or system password - 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 #### 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/<pid>/environ`). Every child process of the environment (on Linux, from `/proc/<pid>/environ`). Every child process of the
shell or script that sets them inherits them, `gpg` included. Set on a command 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 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 each one as soon as it has read it, so that the programs it runs itself, such as
as `gpg`, do not inherit it, but that erases nothing: the environment the `gpg`, do not inherit it, but that erases nothing: the environment the process
process started with, and its memory, still hold the value. The interactive started with, and its memory, still hold the value. The interactive prompt,
prompt, which every command except `secret vault import` offers when the which every command except `secret vault import` offers when the variable is not
variable is not set, is the safer default; `secret vault import` has no prompt set, is the safer default; `secret vault import` has no prompt and needs both
and needs both variables. variables.
## Security Features ## Security Features
### Encryption ### 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 - All private keys are encrypted at rest
- No plaintext secrets stored on disk - No plaintext secrets stored on disk
@@ -382,7 +400,8 @@ and needs both variables.
- Hardware token support via PGP/GPG integration - Hardware token support via PGP/GPG integration
- macOS Keychain integration for system-level security - 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 ## Examples
@@ -431,6 +450,7 @@ secret vault remove personal --force
``` ```
### Advanced Authentication ### Advanced Authentication
```bash ```bash
# Add multiple unlock methods # Add multiple unlock methods
secret unlocker add passphrase # Password-based secret unlocker add passphrase # Password-based
@@ -477,21 +497,27 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
## Technical Details ## Technical Details
### Cryptographic Primitives ### Cryptographic Primitives
- **Key Derivation**: BIP32/BIP39 hierarchical deterministic key derivation - **Key Derivation**: BIP32/BIP39 hierarchical deterministic key derivation
- **Encryption**: Age (X25519 + ChaCha20-Poly1305) - **Encryption**: Age (X25519 + ChaCha20-Poly1305)
- **Authentication**: Poly1305 MAC - **Authentication**: Poly1305 MAC
- **Hashing**: Double SHA-256 for public key identification - **Hashing**: Double SHA-256 for public key identification
### File Formats ### File Formats
- **age Files**: Standard age encryption format (.age extension) - **age Files**: Standard age encryption format (.age extension)
- **Metadata**: Unencrypted JSON format with timestamps and type information - **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 ### Vault Management
- **Derivation Index**: Each vault uses a unique derivation index from the mnemonic, and thus a unique key pair - **Derivation Index**: Each vault uses a unique derivation index from the
- **Public Key Hash**: Double SHA-256 hash of the index-0 public key identifies vaults from the same mnemonic mnemonic, and thus a unique key pair
- **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are automatically derived - **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 ### Cross-Platform Support
@@ -523,6 +549,7 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
## Development ## Development
### Building ### Building
```bash ```bash
make build # Build binary make build # Build binary
make test # Run tests make test # Run tests
@@ -530,7 +557,9 @@ make lint # Run linter
``` ```
### Testing ### Testing
The project includes comprehensive tests: The project includes comprehensive tests:
```bash ```bash
make test # Run all tests make test # Run all tests
go test ./... # Unit tests go test ./... # Unit tests
@@ -542,55 +571,62 @@ go test -tags=integration -v ./internal/cli # Integration tests
This repository adheres to the This repository adheres to the
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
standard: normalized scripts in `script/` are the entrypoints for the standard: normalized scripts in `script/` are the entrypoints for the
development workflow, and the Makefile targets are thin shims that call development workflow, and the Makefile targets are thin shims that call them. We
them. We provide: provide:
- `script/bootstrap` — install all dependencies (Go, Go module - `script/bootstrap` — install all dependencies (Go, Go module download),
download), idempotently; golangci-lint is not installed, it runs in idempotently; golangci-lint is not installed, it runs in docker
docker
- `script/setup` — make a fresh clone ready for development: runs - `script/setup` — make a fresh clone ready for development: runs
`script/bootstrap`, then `script/install-precommit` `script/bootstrap`, then `script/install-precommit`
- `script/projectname` — output the project name (`secret`); used by - `script/projectname` — output the project name (`secret`); used by other
other scripts such as `script/docker` scripts such as `script/docker`
- `script/build` — build the `secret` binary into the repo root, stamping - `script/build` — build the `secret` binary into the repo root, stamping the
the version (`VERSION` from the environment, else `git describe`) and version (`VERSION` from the environment, else `git describe`) and the git
the git commit commit
- `script/test` — run `go vet` and the test suite (verbose rerun on - `script/test` — run `go vet` and the test suite (verbose rerun on failure)
failure) - `script/lint` — run `golangci-lint` in docker only: builds `Dockerfile.lint`,
- `script/lint` — run `golangci-lint` in docker only: builds where the linter is a build step that runs on every call, also on an unchanged
`Dockerfile.lint`, where the linter is a build step that runs on every tree
call, also on an unchanged tree
- `script/fmt` — format all Go code (writes) - `script/fmt` — format all Go code (writes)
- `script/fmt-check` — check formatting without writing - `script/fmt-check` — check formatting without writing
- `script/check` — run `script/test`, `script/lint`, and - `script/check` — run `script/test`, `script/lint`, and `script/fmt-check`
`script/fmt-check`
- `script/docker` — build the Docker image tagged with the project name - `script/docker` — build the Docker image tagged with the project name
- `script/cibuild` — CI entrypoint: `docker build --ulimit - `script/cibuild` — CI entrypoint: `docker build --ulimit memlock=-1:-1 .`
memlock=-1:-1 .` (memguard needs mlock; the Dockerfile runs the (memguard needs mlock; the Dockerfile runs the checks), with a new
checks), with a new `CHECK_EPOCH` build argument on every run so the `CHECK_EPOCH` build argument on every run so the checks run again on an
checks run again on an unchanged tree unchanged tree
- `script/precommit` — pre-commit checks: `go mod tidy` verification, - `script/precommit` — pre-commit checks: `go mod tidy` verification, then
then `script/check` `script/check`
- `script/install-precommit` — install the git pre-commit hook that - `script/install-precommit` — install the git pre-commit hook that runs
runs `script/precommit` `script/precommit`
## Features ## 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 - **Vault Isolation**: Complete separation between different vaults
- **Per-Secret Encryption**: Each secret has its own encryption key - **Per-Secret Encryption**: Each secret has its own encryption key
- **BIP39 Mnemonic Support**: Keyless operation using mnemonic phrases - **BIP39 Mnemonic Support**: Keyless operation using mnemonic phrases
- **Cross-Platform**: Works on macOS, Linux, and other Unix-like systems - **Cross-Platform**: Works on macOS, Linux, and other Unix-like systems
# Author ## TODO
Made with love and lots of expensive SOTA AI by Open work is tracked on the
[sneak](https://sneak.berlin) in Berlin in the summer of 2025. [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 ## License
the [WTFPL](https://www.wtfpl.net/) 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) 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) [https://keys.openpgp.org/vks/v1/by-fingerprint/5539AD00DE4C42F3AFE11575052443F4DF2A55C2](https://keys.openpgp.org/vks/v1/by-fingerprint/5539AD00DE4C42F3AFE11575052443F4DF2A55C2)
+14 -27
View File
@@ -10,18 +10,13 @@
# Status # Status
pre-1.0. No git tags. TODO.md carries open 1.0 security blockers. Work in pre-1.0. No git tags. Open work is tracked on the issue tracker, which is
flight on branch secure-enclave-unlocker (clean tree as of 2026-07-06). authoritative.
# Next Step # Next Step
Bring the repo into policy compliance in one commit: Take the next open issue in the `1.0.0` milestone:
https://git.eeqj.de/sneak/secret/milestone/12
- 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.
# Completed Steps # Completed Steps
@@ -266,8 +261,15 @@ Bring the repo into policy compliance in one commit:
`findUnlockerIDByMetadata` now returns an error so `unlocker list` `findUnlockerIDByMetadata` now returns an error so `unlocker list`
skips an unreadable `unlockers.d` entry with a warning instead of skips an unreadable `unlockers.d` entry with a warning instead of
emitting a fabricated fallback ID. 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, - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
Makefile shims, README Entrypoints section 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 - 2026-03-11: Secure Enclave unlocker for hardware-backed secret
protection, plus review fixes (stub panics, derivation index, tests, protection, plus review fixes (stub panics, derivation index, tests,
README) on branch secure-enclave-unlocker. README) on branch secure-enclave-unlocker.
@@ -289,8 +291,6 @@ Bring the repo into policy compliance in one commit:
# Future Steps # 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 - Implement version-number shell completion for the second arg of
`secret version promote` and `secret version rm` `secret version promote` and `secret version rm`
(`internal/cli/version.go`; was an in-code TODO removed for godox). (`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 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 over the new 88-column limit; they will surface if lint ever runs on
macOS. macOS.
- Merge secure-enclave-unlocker to main once review is done.
- 1.0 critical security blockers (from repo TODO.md): - 1.0 critical security blockers (from repo TODO.md):
- Command injection: GPG key IDs passed unescaped to exec.Command - Memory security: age identity .String() creates unprotected copies of
(pgpunlocker.go:323-327); data.String() passed unescaped to the private keys; the call sites are listed in
security command (keychainunlocker.go:472-476). https://git.eeqj.de/sneak/secret/issues/38.
- 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.
- Medium priority: - Medium priority:
- Standardize error messages; stop leaking internals. - Standardize error messages; stop leaking internals.
- Graceful handling of corrupted or missing key files with recovery - Graceful handling of corrupted or missing key files with recovery