Compare commits
1
Commits
next
...
734a651be4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
734a651be4 |
@@ -4,33 +4,32 @@ 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
|
||||||
@@ -38,20 +37,19 @@ Version: 2025-06-08
|
|||||||
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
|
||||||
@@ -59,55 +57,57 @@ Version: 2025-06-08
|
|||||||
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
|
||||||
|
error.
|
||||||
|
|
||||||
**Good (modern Python 3.9+):**
|
**Good (modern Python 3.9+):**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def process_items(items: list[str]) -> dict[str, int]:
|
def process_items(items: list[str]) -> dict[str, int]:
|
||||||
counts: dict[str, int] = {}
|
counts: dict[str, int] = {}
|
||||||
@@ -115,6 +115,7 @@ Version: 2025-06-08
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Avoid (triggers UP006):**
|
**Avoid (triggers UP006):**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
@@ -124,6 +125,7 @@ Version: 2025-06-08
|
|||||||
```
|
```
|
||||||
|
|
||||||
For optional types, use the `|` operator instead of `Union`:
|
For optional types, use the `|` operator instead of `Union`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Good
|
# Good
|
||||||
def get_value(key: str) -> str | None:
|
def get_value(key: str) -> str | None:
|
||||||
@@ -144,14 +146,18 @@ Version: 2025-06-08
|
|||||||
|
|
||||||
## 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,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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -287,8 +280,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.
|
||||||
@@ -310,8 +310,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).
|
||||||
@@ -326,34 +324,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 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
|
||||||
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