# secret - Local Secret Manager ## Description `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. ## 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 ### Version Management Each secret maintains a history of versions, with each version having: - Its own encryption key pair - Metadata including creation time and validity period, encrypted to the version's key pair - Immutable value storage 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). ## Commands Reference ### Confirmation Before Removal `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. 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 script or a CI job, nobody is there to answer: the command fails at once, removes nothing, and says to pass `--force`. `--force` (`-f`) removes without asking, whatever the command removes: a vault that holds secrets and the last unlocker of a vault included. Scripts that remove things pass `--force`. ### Initialization #### `secret init` 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 ### Vault Management #### `secret vault list [--json]` / `secret vault ls` Lists all available vaults. The current vault is marked. #### `secret vault create ` Creates a new vault with the specified name. **Vault Name Format:** only lowercase ASCII letters, digits, `.`, `-` and `_` are allowed, and a name must not be empty, `.` or `..`. #### `secret vault select ` 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 [Confirmation Before Removal](#confirmation-before-removal)). The last vault cannot be removed. Removing the current vault makes another vault the current one. - `--force, -f`: Remove without asking, also a vault that contains secrets - **NO RECOVERY**: All secrets in the vault will be permanently deleted ### Secret Management #### `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. - 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. #### `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)). - `--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 #### `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 - 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. #### `secret version promote ` 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) ### Key Generation #### `secret generate mnemonic` 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 ### Unlocker Management #### `secret unlocker list [--json]` / `secret unlocker ls` Lists all unlockers in the current vault with their metadata. #### `secret unlocker add [options]` 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. #### `secret unlocker remove [--force]` / `secret unlocker rm` ⚠️ 🛑 **DANGER**: Permanently removes an unlocker. It first asks for confirmation, 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. - `--force, -f`: Remove without asking, even the last unlocker - **CRITICAL WARNING**: Without unlockers and without your mnemonic phrase, vault data will be PERMANENTLY INACCESSIBLE - **NO RECOVERY**: Removing all unlockers without having your mnemonic means losing access to all secrets forever #### `secret unlocker select ` Selects an unlocker as the current default for operations. ### Import Operations #### `secret import --source ` Imports a secret from a file and stores it in the current vault under the given name. #### `secret vault import [vault-name]` Imports a mnemonic phrase into the specified vault (defaults to "default"). ### Encryption Operations #### `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. #### `secret decrypt [--input=file] [--output=file]` Decrypts data using an Age key stored as a secret. ## Storage Architecture ### Directory Structure ``` ~/.local/share/secret/ ├── vaults.d/ │ ├── default/ │ │ ├── unlockers.d/ │ │ │ ├── passphrase-