Add the README's required sections and clear stale TODO.md items (closes #46)
check / check (push) Failing after 2s
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. README and AGENTS.md are wrapped to prettier's settings. TODO.md: Workflow and Next Step point at the 1.0.0 milestone and the next branch, the old Next Step's 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:
@@ -4,154 +4,160 @@ Version: 2025-06-08
|
|||||||
|
|
||||||
# Instructions and Contextual Information
|
# Instructions and Contextual Information
|
||||||
|
|
||||||
* Be direct, robotic, expert, accurate, and professional.
|
- Be direct, robotic, expert, accurate, and professional.
|
||||||
|
|
||||||
* Do not butter me up or kiss my ass.
|
- Do not butter me up or kiss my ass.
|
||||||
|
|
||||||
* Come in hot with strong opinions, even if they are contrary to the
|
- Come in hot with strong opinions, even if they are contrary to the direction I
|
||||||
direction I am headed.
|
am headed.
|
||||||
|
|
||||||
* If either you or I are possibly wrong, say so and explain your point of
|
- If either you or I are possibly wrong, say so and explain your point of view.
|
||||||
view.
|
|
||||||
|
|
||||||
* Point out great alternatives I haven't thought of, even when I'm not
|
- Point out great alternatives I haven't thought of, even when I'm not asking
|
||||||
asking for them.
|
for them.
|
||||||
|
|
||||||
* Treat me like the world's leading expert in every situation and every
|
- Treat me like the world's leading expert in every situation and every
|
||||||
conversation, and deliver the absolute best recommendations.
|
conversation, and deliver the absolute best recommendations.
|
||||||
|
|
||||||
* I want excellence, so always be on the lookout for divergences from good
|
- I want excellence, so always be on the lookout for divergences from good data
|
||||||
data model design or best practices for object oriented development.
|
model design or best practices for object oriented development.
|
||||||
|
|
||||||
* IMPORTANT: This is production code, not a research or teaching exercise.
|
- IMPORTANT: This is production code, not a research or teaching exercise.
|
||||||
Deliver professional-level results, not prototypes.
|
Deliver professional-level results, not prototypes.
|
||||||
|
|
||||||
* Please read and understand the `README.md` file in the root of the repo
|
- Please read and understand the `README.md` file in the root of the repo for
|
||||||
for project-specific contextual information, including development
|
project-specific contextual information, including development policies,
|
||||||
policies, practices, and current implementation status.
|
practices, and current implementation status.
|
||||||
|
|
||||||
* Be proactive in suggesting improvements or refactorings in places where we
|
- Be proactive in suggesting improvements or refactorings in places where we
|
||||||
diverge from best practices for clean, modular, maintainable code.
|
diverge from best practices for clean, modular, maintainable code.
|
||||||
|
|
||||||
# Policies
|
# Policies
|
||||||
|
|
||||||
1. Before committing, tests must pass (`make test`), linting must pass
|
1. Before committing, tests must pass (`make test`), linting must pass
|
||||||
(`make lint`), and code must be formatted (`make fmt`). For go, those
|
(`make lint`), and code must be formatted (`make fmt`). For go, those
|
||||||
makefile targets should use `go fmt` and `go test -v ./...` and
|
makefile targets should use `go fmt` and `go test -v ./...` and
|
||||||
`golangci-lint run`. When you think your changes are complete, rather
|
`golangci-lint run`. When you think your changes are complete, rather than
|
||||||
than making three different tool calls to check, you can just run `make
|
making three different tool calls to check, you can just run
|
||||||
test && make fmt && make lint` as a single tool call which will save
|
`make test && make fmt && make lint` as a single tool call which will save
|
||||||
time.
|
time.
|
||||||
|
|
||||||
2. Always write a `Makefile` with the default target being `test`, and with
|
2. Always write a `Makefile` with the default target being `test`, and with a
|
||||||
a `fmt` target that formats the code. The `test` target should run all
|
`fmt` target that formats the code. The `test` target should run all tests in
|
||||||
tests in the project, and the `fmt` target should format the code.
|
the project, and the `fmt` target should format the code. `test` should also
|
||||||
`test` should also have a prerequisite target `lint` that should run any
|
have a prerequisite target `lint` that should run any linters that are
|
||||||
linters that are configured for the project.
|
configured for the project.
|
||||||
|
|
||||||
3. After each completed bugfix or feature, the code must be committed. Do
|
3. After each completed bugfix or feature, the code must be committed. Do all of
|
||||||
all of the pre-commit checks (test, lint, fmt) before committing, of
|
the pre-commit checks (test, lint, fmt) before committing, of course.
|
||||||
course.
|
|
||||||
|
|
||||||
4. When creating a very simple test script for testing out a new feature,
|
4. When creating a very simple test script for testing out a new feature,
|
||||||
instead of making a throwaway to be deleted after verification, write an
|
instead of making a throwaway to be deleted after verification, write an
|
||||||
actual test file into the test suite. It doesn't need to be very big or
|
actual test file into the test suite. It doesn't need to be very big or
|
||||||
complex, but it should be a real test that can be run.
|
complex, but it should be a real test that can be run.
|
||||||
|
|
||||||
5. When you are instructed to make the tests pass, DO NOT delete tests, skip
|
5. When you are instructed to make the tests pass, DO NOT delete tests, skip
|
||||||
tests, or change the tests specifically to make them pass (unless there
|
tests, or change the tests specifically to make them pass (unless there is a
|
||||||
is a bug in the test). This is cheating, and it is bad. You should only
|
bug in the test). This is cheating, and it is bad. You should only be
|
||||||
be modifying the test if it is incorrect or if the test is no longer
|
modifying the test if it is incorrect or if the test is no longer relevant.
|
||||||
relevant. In almost all cases, you should be fixing the code that is
|
In almost all cases, you should be fixing the code that is being tested, or
|
||||||
being tested, or updating the tests to match a refactored implementation.
|
updating the tests to match a refactored implementation.
|
||||||
|
|
||||||
6. When dealing with dates and times or timestamps, always use, display, and
|
6. When dealing with dates and times or timestamps, always use, display, and
|
||||||
store UTC. Set the local timezone to UTC on startup. If the user needs
|
store UTC. Set the local timezone to UTC on startup. If the user needs to see
|
||||||
to see the time in a different timezone, store the user's timezone in a
|
the time in a different timezone, store the user's timezone in a separate
|
||||||
separate field and convert the UTC time to the user's timezone when
|
field and convert the UTC time to the user's timezone when displaying it. For
|
||||||
displaying it. For internal use and internal applications and
|
internal use and internal applications and administrative purposes, always
|
||||||
administrative purposes, always display UTC.
|
display UTC.
|
||||||
|
|
||||||
7. Always write tests, even if they are extremely simple and just check for
|
7. Always write tests, even if they are extremely simple and just check for
|
||||||
correct syntax (ability to compile/import). If you are writing a new
|
correct syntax (ability to compile/import). If you are writing a new feature,
|
||||||
feature, write a test for it. You don't need to target complete
|
write a test for it. You don't need to target complete coverage, but you
|
||||||
coverage, but you should at least test any new functionality you add. If
|
should at least test any new functionality you add. If you are fixing a bug,
|
||||||
you are fixing a bug, write a test first that reproduces the bug, and
|
write a test first that reproduces the bug, and then fix the bug in the code.
|
||||||
then fix the bug in the code.
|
|
||||||
|
|
||||||
8. When implementing new features, be aware of potential side-effects (such
|
8. When implementing new features, be aware of potential side-effects (such as
|
||||||
as state files on disk, data in the database, etc.) and ensure that it is
|
state files on disk, data in the database, etc.) and ensure that it is
|
||||||
possible to mock or stub these side-effects in tests.
|
possible to mock or stub these side-effects in tests.
|
||||||
|
|
||||||
9. Always use structured logging. Log any relevant state/context with the
|
9. Always use structured logging. Log any relevant state/context with the
|
||||||
messages (but do not log secrets). If stdout is not a terminal, output
|
messages (but do not log secrets). If stdout is not a terminal, output the
|
||||||
the structured logs in jsonl format.
|
structured logs in jsonl format.
|
||||||
|
|
||||||
10. Avoid using bare strings or numbers in code, especially if they appear
|
10. Avoid using bare strings or numbers in code, especially if they appear
|
||||||
anywhere more than once. Always define a constant (usually at the top
|
anywhere more than once. Always define a constant (usually at the top of the
|
||||||
of the file) and give it a descriptive name, then use that constant in
|
file) and give it a descriptive name, then use that constant in the code
|
||||||
the code instead of the bare string or number.
|
instead of the bare string or number.
|
||||||
|
|
||||||
11. You do not need to summarize your changes in the chat after making them.
|
11. You do not need to summarize your changes in the chat after making them.
|
||||||
Making the changes and committing them is sufficient. If anything out
|
Making the changes and committing them is sufficient. If anything out of the
|
||||||
of the ordinary happened, please explain it, but in the normal case
|
ordinary happened, please explain it, but in the normal case where you found
|
||||||
where you found and fixed the bug, or implemented the feature, there is
|
and fixed the bug, or implemented the feature, there is no need for the
|
||||||
no need for the end-of-change summary.
|
end-of-change summary.
|
||||||
|
|
||||||
12. Do not create additional files in the root directory of the project
|
12. Do not create additional files in the root directory of the project without
|
||||||
without asking permission first. Configuration files, documentation, and
|
asking permission first. Configuration files, documentation, and build files
|
||||||
build files are acceptable in the root, but source code and other files
|
are acceptable in the root, but source code and other files should be
|
||||||
should be organized in appropriate subdirectories.
|
organized in appropriate subdirectories.
|
||||||
|
|
||||||
## Python-Specific Guidelines
|
## Python-Specific Guidelines
|
||||||
|
|
||||||
1. **Type Annotations (UP006)**: Use built-in collection types directly for type annotations instead of importing from `typing`. This avoids the UP006 linter error.
|
1. **Type Annotations (UP006)**: Use built-in collection types directly for type
|
||||||
|
annotations instead of importing from `typing`. This avoids the UP006 linter
|
||||||
**Good (modern Python 3.9+):**
|
error.
|
||||||
```python
|
|
||||||
def process_items(items: list[str]) -> dict[str, int]:
|
**Good (modern Python 3.9+):**
|
||||||
counts: dict[str, int] = {}
|
|
||||||
return counts
|
```python
|
||||||
```
|
def process_items(items: list[str]) -> dict[str, int]:
|
||||||
|
counts: dict[str, int] = {}
|
||||||
**Avoid (triggers UP006):**
|
return counts
|
||||||
```python
|
```
|
||||||
from typing import List, Dict
|
|
||||||
|
**Avoid (triggers UP006):**
|
||||||
def process_items(items: List[str]) -> Dict[str, int]:
|
|
||||||
counts: Dict[str, int] = {}
|
```python
|
||||||
return counts
|
from typing import List, Dict
|
||||||
```
|
|
||||||
|
def process_items(items: List[str]) -> Dict[str, int]:
|
||||||
For optional types, use the `|` operator instead of `Union`:
|
counts: Dict[str, int] = {}
|
||||||
```python
|
return counts
|
||||||
# Good
|
```
|
||||||
def get_value(key: str) -> str | None:
|
|
||||||
return None
|
For optional types, use the `|` operator instead of `Union`:
|
||||||
|
|
||||||
# Avoid
|
```python
|
||||||
from typing import Optional, Union
|
# Good
|
||||||
def get_value(key: str) -> Optional[str]:
|
def get_value(key: str) -> str | None:
|
||||||
return None
|
return None
|
||||||
```
|
|
||||||
|
# Avoid
|
||||||
|
from typing import Optional, Union
|
||||||
|
def get_value(key: str) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
2. **Import Organization**: Follow the standard Python import order:
|
2. **Import Organization**: Follow the standard Python import order:
|
||||||
- Standard library imports
|
- Standard library imports
|
||||||
- Third-party imports
|
- Third-party imports
|
||||||
- Local application imports
|
- Local application imports
|
||||||
|
|
||||||
Each group should be separated by a blank line.
|
Each group should be separated by a blank line.
|
||||||
|
|
||||||
## Go-Specific Guidelines
|
## Go-Specific Guidelines
|
||||||
|
|
||||||
1. **No `panic`, `log.Fatal`, or `os.Exit` in library code.** Always propagate errors via return values.
|
1. **No `panic`, `log.Fatal`, or `os.Exit` in library code.** Always propagate
|
||||||
|
errors via return values.
|
||||||
|
|
||||||
2. **Constructors return `(*T, error)`, not just `*T`.** Callers must handle errors, not crash.
|
2. **Constructors return `(*T, error)`, not just `*T`.** Callers must handle
|
||||||
|
errors, not crash.
|
||||||
|
|
||||||
3. **Wrap errors** with `fmt.Errorf("context: %w", err)` for debuggability.
|
3. **Wrap errors** with `fmt.Errorf("context: %w", err)` for debuggability.
|
||||||
|
|
||||||
4. **Never modify linter config** (`.golangci.yml`) to suppress findings. Fix the code.
|
4. **Never modify linter config** (`.golangci.yml`) to suppress findings. Fix
|
||||||
|
the code.
|
||||||
|
|
||||||
5. **All PRs must pass `make check` with zero failures.** No exceptions, no "pre-existing issue" excuses.
|
5. **All PRs must pass `make check` with zero failures.** No exceptions, no
|
||||||
|
"pre-existing issue" excuses.
|
||||||
|
|
||||||
6. **Pin external dependencies by commit hash**, not mutable tags.
|
6. **Pin external dependencies by commit hash**, not mutable tags.
|
||||||
|
|||||||
@@ -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,37 +313,43 @@ 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
|
||||||
|
|
||||||
Unlockers provide different authentication methods to access the long-term keys:
|
Unlockers provide different authentication methods to access the long-term keys:
|
||||||
|
|
||||||
1. **Passphrase Unlockers**:
|
1. **Passphrase Unlockers**:
|
||||||
- Encrypted with user-provided passphrase
|
- Encrypted with user-provided passphrase
|
||||||
- Stored as encrypted Age keys
|
- Stored as encrypted Age keys
|
||||||
- Cross-platform compatible
|
- Cross-platform compatible
|
||||||
|
|
||||||
2. **PGP Unlockers**:
|
2. **PGP Unlockers**:
|
||||||
- Uses existing GPG key infrastructure
|
- Uses existing GPG key infrastructure
|
||||||
- Leverages existing key management workflows
|
- Leverages existing key management workflows
|
||||||
- Strong authentication through GPG
|
- Strong authentication through GPG
|
||||||
|
|
||||||
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)
|
- Protected by system authentication (Touch ID, password)
|
||||||
- Automatic unlocking when Keychain is unlocked
|
- Automatic unlocking when Keychain is unlocked
|
||||||
- Cross-application integration
|
- Cross-application integration
|
||||||
|
|
||||||
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
|
||||||
- ECIES encryption: vault long-term key encrypted directly by SE hardware
|
Program required)
|
||||||
- Protected by biometric authentication (Touch ID) or system password
|
- 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
|
#### 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
|
||||||
|
|
||||||
@@ -527,6 +553,7 @@ to add or use them.
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make build # Build binary
|
make build # Build binary
|
||||||
make test # Run tests
|
make test # Run tests
|
||||||
@@ -534,7 +561,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
|
||||||
@@ -546,61 +575,68 @@ 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/lint-darwin` — run `go vet` and `golangci-lint` in docker on the code
|
||||||
- `script/lint-darwin` — run `go vet` and `golangci-lint` in docker on
|
as a macOS build compiles it (`GOOS=darwin`), which a Linux build never
|
||||||
the code as a macOS build compiles it (`GOOS=darwin`), which a Linux
|
compiles; cgo is off, so the keychain unlocker's calls into the keychain
|
||||||
build never compiles; cgo is off, so the keychain unlocker's calls into
|
(`internal/secret/keychainunlocker_cgo.go`, and `keychainunlocker_test.go`)
|
||||||
the keychain (`internal/secret/keychainunlocker_cgo.go`, and
|
and the Secure Enclave bindings (`internal/macse`) are not checked
|
||||||
`keychainunlocker_test.go`) and the Secure Enclave bindings
|
|
||||||
(`internal/macse`) are not checked
|
|
||||||
- `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`,
|
- `script/check` — run `script/test`, `script/lint`, `script/lint-darwin`, and
|
||||||
`script/lint-darwin`, 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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
# Workflow
|
# Workflow
|
||||||
|
|
||||||
* branch (from `main`)
|
- branch from `next`
|
||||||
* do the work in Next Step
|
- do the Next Step: the next open issue in the `1.0.0` milestone
|
||||||
* move Next Step to the top of Completed Steps
|
- log it at the top of Completed Steps
|
||||||
* move the top item of Future Steps into Next Step
|
- commit (`TODO.md` changes in the same commit as the work)
|
||||||
* commit (`TODO.md` changes in the same commit as the work)
|
- push, and open a PR against `next`
|
||||||
* merge to `main` if the branch is not protected, otherwise open a PR
|
|
||||||
* push
|
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
@@ -299,8 +292,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.
|
||||||
@@ -322,8 +322,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).
|
||||||
@@ -338,34 +336,18 @@ Bring the repo into policy compliance in one commit:
|
|||||||
never run on them, so it would likely find more there than the line
|
never run on them, so it would likely find more there than the line
|
||||||
lengths. No macOS test runs in CI. A macOS runner would cover all of it
|
lengths. No macOS test runs in CI. A macOS runner would cover all of it
|
||||||
(asked on https://git.eeqj.de/sneak/secret/issues/50).
|
(asked on https://git.eeqj.de/sneak/secret/issues/50).
|
||||||
- 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 writes an identity's private key out as a string in
|
||||||
(pgpunlocker.go:323-327); data.String() passed unescaped to the
|
ordinary memory, and the copies it makes on the way stay there
|
||||||
security command (keychainunlocker.go:472-476).
|
(`secret.IdentityToLockedBuffer` overwrites only the string itself).
|
||||||
- Memory security: age writes an identity's private key out as a
|
|
||||||
string in ordinary memory, and the copies it makes on the way stay
|
|
||||||
there (`secret.IdentityToLockedBuffer` overwrites only the string
|
|
||||||
itself); 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
|
||||||
suggestions.
|
suggestions.
|
||||||
- Validate GPG key existence before creating PGP unlock keys.
|
|
||||||
- Split oversized CLI functions.
|
- Split oversized CLI functions.
|
||||||
- mlock/munlock for sensitive allocations.
|
|
||||||
- Cleanups: read statedir from environment or default instead of
|
- Cleanups: read statedir from environment or default instead of
|
||||||
passing it around.
|
passing it around.
|
||||||
- Enhancements: help examples, shell completion, colored output,
|
- Enhancements: help examples, colored output, --quiet flag, name suggestions on
|
||||||
--quiet flag, name suggestions on miss, audit logging, hardware
|
miss, audit logging, hardware integration tests (Keychain, GPG), naming
|
||||||
integration tests (Keychain, GPG), naming consistency, vault
|
consistency, vault export/import, batch operations, search, secret metadata
|
||||||
export/import, batch operations, search, secret metadata
|
|
||||||
(descriptions, tags).
|
(descriptions, tags).
|
||||||
|
|||||||
Reference in New Issue
Block a user