|
|
|
@@ -8,8 +8,7 @@
|
|
|
|
|
|
|
|
|
|
# Status
|
|
|
|
|
|
|
|
|
|
pre-1.0. No git tags. Open work is tracked on the issue tracker, which is
|
|
|
|
|
authoritative.
|
|
|
|
|
pre-1.0. Open work is tracked on the issue tracker, which is authoritative.
|
|
|
|
|
|
|
|
|
|
# Next Step
|
|
|
|
|
|
|
|
|
@@ -18,6 +17,183 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
|
|
|
|
|
|
|
|
|
# Completed Steps
|
|
|
|
|
|
|
|
|
|
- 2026-10-15: `make test` no longer fails on some runs in `internal/cli`
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/192). `cli.Entry()` set `time.Local`
|
|
|
|
|
to UTC and ended by wiping every memguard buffer, and a test ran it in the
|
|
|
|
|
same process as the other tests. A timer left by an earlier test could read
|
|
|
|
|
`time.Local` as it changed, which the race detector reported, and the wipe
|
|
|
|
|
could destroy a buffer still in use, the likeliest cause of a crash of the
|
|
|
|
|
test process on another run. `main()` in `cmd/secret` now sets `time.Local`
|
|
|
|
|
before it calls `cli.Entry()`, and that test runs `cli.Entry()` in a process
|
|
|
|
|
of its own.
|
|
|
|
|
- 2026-10-15: `secret mv` from one vault to another unlocks both vaults before
|
|
|
|
|
it takes the state directory lock
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/189). It unlocked them while holding
|
|
|
|
|
the lock, and unlocking can ask for a passphrase, so every other command that
|
|
|
|
|
changes anything waited at that prompt. `moveSecretCrossVault` now checks the
|
|
|
|
|
move, refusing an existing destination secret without `--force`, unlocks both
|
|
|
|
|
vaults, and only then takes the lock. Under the lock, it checks that the
|
|
|
|
|
destination vault still has the long-term key it unlocked, and
|
|
|
|
|
`copySecretAndRemoveSource` checks the move again before it copies. A move
|
|
|
|
|
within a vault takes the lock in `moveSecretWithinVault`, so `MoveSecret`
|
|
|
|
|
takes none itself. The error for a vault created again with another key, which
|
|
|
|
|
`secret unlocker add` shares, now ends "nothing was changed".
|
|
|
|
|
- 2026-10-15: `secret init`, `secret vault create`, `secret unlocker add`, and
|
|
|
|
|
`secret add`, `secret import` and `secret generate secret` with `--force` ask
|
|
|
|
|
for the mnemonic or passphrase, or unlock the vault, before they take the
|
|
|
|
|
state directory lock (https://git.eeqj.de/sneak/secret/issues/152). They did
|
|
|
|
|
so while holding the lock, so every other command that changes anything waited
|
|
|
|
|
at the prompt. Under the lock, `secret unlocker add` fails if the vault was
|
|
|
|
|
removed, or created again with another key, and refuses a GPG key added
|
|
|
|
|
meanwhile; a forced add of a secret created meanwhile lets go of the lock,
|
|
|
|
|
gets the key and takes the lock again. `secret.CreatePGPUnlocker` takes the
|
|
|
|
|
unlocked vault; `SetMnemonic` and `SetUnlockPassphrase` are gone.
|
|
|
|
|
- 2026-10-15: A `secret mv` to another vault that copies the secret but cannot
|
|
|
|
|
remove the source fails (https://git.eeqj.de/sneak/secret/issues/159). It
|
|
|
|
|
printed a warning and the usual "Moved" line and exited 0. If the source
|
|
|
|
|
secret's directory is still in place, the error says the copy holds all
|
|
|
|
|
versions and the source remains. If the source left its vault and only the
|
|
|
|
|
temporary directory it was moved into could not be deleted, the error says the
|
|
|
|
|
secret is moved and names that directory, which the error of `RemoveDirAtomic`
|
|
|
|
|
now does.
|
|
|
|
|
- 2026-10-15: `secret encrypt` and `secret decrypt` with `--output` hold the
|
|
|
|
|
result in memory and open the output file only once encryption or decryption
|
|
|
|
|
has succeeded (https://git.eeqj.de/sneak/secret/issues/143). They emptied that
|
|
|
|
|
file before reading any input, so a failed command left it empty, and
|
|
|
|
|
encrypting a file onto itself gave the encryption of nothing. A failure to
|
|
|
|
|
encrypt or decrypt now leaves `--output` untouched, and `--input` can name the
|
|
|
|
|
same file. A failure while writing `--output`, such as a full disk, can still
|
|
|
|
|
leave it empty or partly written; an error from closing it is reported.
|
|
|
|
|
Without `--output` the result still goes to stdout as it is produced.
|
|
|
|
|
- 2026-10-15: A command that uses the current vault fails when `currentvault` is
|
|
|
|
|
empty or names no vault (https://git.eeqj.de/sneak/secret/issues/156).
|
|
|
|
|
`secret list` reported an empty vault with exit status 0, and `secret add`
|
|
|
|
|
failed on a missing directory. `ResolveVaultSymlink` now refuses an empty or
|
|
|
|
|
invalid name, or one with no directory under `vaults.d`, with an error that
|
|
|
|
|
wraps `ErrVaultNotFound`, names the file and says to run
|
|
|
|
|
`secret vault select <name>`. `secret vault list` and `secret info` still run.
|
|
|
|
|
- 2026-10-15: `secret` prints every time in UTC, whatever the machine's time
|
|
|
|
|
zone (https://git.eeqj.de/sneak/secret/issues/169). `secret list`,
|
|
|
|
|
`secret version list`, `secret unlocker list` and `secret info` printed times
|
|
|
|
|
in the local time zone without naming it, so one vault showed different times
|
|
|
|
|
on two machines. `Entry()` now sets `time.Local` to UTC before anything else,
|
|
|
|
|
as policy 6 in `AGENTS.md` asks, so every time the program prints or writes is
|
|
|
|
|
in UTC. `--json` times end in `Z`, and a new version's name takes the UTC
|
|
|
|
|
date. The README says so.
|
|
|
|
|
- 2026-10-15: `secret vault`, `secret unlocker`, `secret version` and
|
|
|
|
|
`secret generate` fail when given a subcommand they do not have
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/183). They printed their help and
|
|
|
|
|
exited 0, so a mistyped command looked like one that worked. Each now exits 1
|
|
|
|
|
with `unknown command "<name>" for "secret <parent>"`, and with no arguments
|
|
|
|
|
still prints its help. Cobra checks a command's `Args` only when it has a
|
|
|
|
|
`RunE`, so each gets `Args: cobra.NoArgs` and a `RunE` that prints its help;
|
|
|
|
|
their help now shows a `secret <parent> [flags]` usage line.
|
|
|
|
|
`secret vault help` fails as an unknown subcommand, while `secret help vault`
|
|
|
|
|
and `secret vault --help` still work.
|
|
|
|
|
- 2026-10-15: With `SB_UNLOCK_PASSPHRASE` set, `secret unlocker add passphrase`
|
|
|
|
|
asks on the terminal for the new unlocker's passphrase and uses the variable
|
|
|
|
|
to unlock the vault (https://git.eeqj.de/sneak/secret/issues/154). It gave the
|
|
|
|
|
new unlocker the passphrase in the variable without asking. With no
|
|
|
|
|
controlling terminal, the new unlocker takes the variable's passphrase and the
|
|
|
|
|
output says so. `secret.ErrNoTerminal` is exported so that the command can
|
|
|
|
|
tell a missing terminal from other failures to read, such as nothing typed or
|
|
|
|
|
a confirmation that does not match, which still fail the command.
|
|
|
|
|
- 2026-10-15: `secret init` and `secret vault create` stop with an error naming
|
|
|
|
|
the file when a vault's `vault-metadata.json` cannot be read or parsed, and
|
|
|
|
|
leave the state directory as it was
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/140). `GetNextDerivationIndex` passed
|
|
|
|
|
over such a vault, so the next vault made from the same mnemonic could get its
|
|
|
|
|
derivation index and so the same long-term key. A vault directory with no
|
|
|
|
|
metadata file is still skipped.
|
|
|
|
|
- 2026-10-15: The README says that `secret add` drops one trailing newline from
|
|
|
|
|
stdin, that `secret import` stores a file's bytes unchanged, and that
|
|
|
|
|
`secret get` writes the value with no newline added
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/158). It said none of this, so a PEM
|
|
|
|
|
key piped into `secret add` came back one byte short with no hint why. A new
|
|
|
|
|
test stores `a\n\n` through `add` and through `import` and checks that `add`
|
|
|
|
|
kept two bytes and `import` three. No program code changes.
|
|
|
|
|
- 2026-10-15: When gpg fails to decrypt, to encrypt or to look up a key's
|
|
|
|
|
fingerprint, the error ends with what gpg wrote to stderr, trimmed
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/146). It gave only gpg's exit status,
|
|
|
|
|
so the user could not tell why. `cmd.Output` keeps gpg's stderr in the
|
|
|
|
|
`*exec.ExitError` it returns, and the three errors now use it. When gpg wrote
|
|
|
|
|
nothing to stderr the error is as before, and when gpg wrote several lines the
|
|
|
|
|
error has them all.
|
|
|
|
|
- 2026-10-15: `secret version list` no longer shows a version without
|
|
|
|
|
`NOT_AFTER` as expired (https://git.eeqj.de/sneak/secret/issues/150). STATUS
|
|
|
|
|
was `expired` for every version other than the current one, so after a promote
|
|
|
|
|
the newest version, which has no `NOT_AFTER`, read `expired`. STATUS is now
|
|
|
|
|
`current` for the current version, `expired` for another version whose
|
|
|
|
|
`NOT_AFTER` has passed, and `inactive` for any other version. The README
|
|
|
|
|
describes the three.
|
|
|
|
|
- 2026-10-15: `secret unlocker list --json` for a vault with no unlockers, and
|
|
|
|
|
`secret vault list --json` when `vaults.d` holds no vaults, print the empty
|
|
|
|
|
list as `[]` instead of `null` (https://git.eeqj.de/sneak/secret/issues/148).
|
|
|
|
|
Both lists were nil slices, which `encoding/json` writes as `null`; each is
|
|
|
|
|
now made as an empty slice before it is filled. `vault.ListVaults` therefore
|
|
|
|
|
returns an empty slice instead of nil for an empty `vaults.d`.
|
|
|
|
|
- 2026-10-15: `secret add`, `secret import` and `secret generate secret` fail
|
|
|
|
|
with an error, not memguard's panic, when a secret is too large for the
|
|
|
|
|
locked-memory limit (https://git.eeqj.de/sneak/secret/issues/145). The value
|
|
|
|
|
was held twice, so half the usual 8 MiB limit was enough to crash them.
|
|
|
|
|
`secret` now raises the limit to the hard limit at startup, and each of the
|
|
|
|
|
three commands refuses, before its first locked allocation, a limit that
|
|
|
|
|
leaves fewer than 8 pages for its keys and buffers. The value is read a page
|
|
|
|
|
at a time into a `memguard.Stream`, which keeps it encrypted, and copied once
|
|
|
|
|
into locked memory after its size is checked against 100 MiB and the limit. It
|
|
|
|
|
leaves the stream through `Next`, not `Read`, because `Read` re-encrypts a
|
|
|
|
|
leftover piece and memguard hangs if that runs out of locked memory.
|
|
|
|
|
- 2026-10-15: Total Size in `secret info` counts every file of each version: the
|
|
|
|
|
value, the metadata and the version's two keys
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/147). It added up each version's
|
|
|
|
|
`data.age` and `metadata.age`, but a version keeps its value in `value.age`,
|
|
|
|
|
so no secret's value was counted. A version directory that cannot be read adds
|
|
|
|
|
nothing and gets a warning naming it.
|
|
|
|
|
- 2026-10-15: `secret vault import` is gone, with its tests and its README entry
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/139). It took only a vault without a
|
|
|
|
|
long-term key, and `secret init` and `secret vault create` give every vault
|
|
|
|
|
one, so it always failed; a mnemonic enters through those two commands. The
|
|
|
|
|
help and the note of `secret generate mnemonic` named `secret import`, which
|
|
|
|
|
imports a file as a secret, and now name `secret init` and
|
|
|
|
|
`secret vault create`.
|
|
|
|
|
- 2026-10-15: `secret version promote --help` says promote rewrites the
|
|
|
|
|
`current` file, not a "current symlink"
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/153). `current` is a plain file
|
|
|
|
|
holding the version name, as the README already said, and the comment in
|
|
|
|
|
`PromoteVersion` now says so too.
|
|
|
|
|
- 2026-10-14: `secret init` and `secret vault create` refuse a vault that
|
|
|
|
|
already exists before they ask for the mnemonic or the unlocker passphrase
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/151). They asked first, and
|
|
|
|
|
`secret init` printed "Initialized secrets manager at" before refusing. The
|
|
|
|
|
check `vault.CreateVault` made before writing is now
|
|
|
|
|
`vault.CheckNewVaultName`, which both commands call before the first prompt
|
|
|
|
|
and `CreateVault` still calls. It validates the name too, so
|
|
|
|
|
`secret vault create` with an invalid name also fails before the prompts.
|
|
|
|
|
`secret init` prints the "Initialized" line only once the vault is created.
|
|
|
|
|
- 2026-10-14: `secret get` fails when it cannot write the value
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/155). `GetSecret` and
|
|
|
|
|
`GetSecretWithVersion` discarded the result of writing it to stdout, so a full
|
|
|
|
|
disk or a failing stdout left an empty or cut-short file and exit status 0.
|
|
|
|
|
The write error is now returned, so the command prints it and exits 1. A
|
|
|
|
|
stdout the shell closed before `secret` started still exits 0, because the Go
|
|
|
|
|
runtime opens `/dev/null` in its place and the write there succeeds.
|
|
|
|
|
- 2026-10-14: `secret list` and `secret list --json` give each secret's own
|
|
|
|
|
creation and last-update times (https://git.eeqj.de/sneak/secret/issues/142).
|
|
|
|
|
They gave the time of the listing for every secret, because
|
|
|
|
|
`Secret.LoadMetadata` set both to the current time. It now reads file
|
|
|
|
|
modification times, so listing still needs no passphrase. The last update is
|
|
|
|
|
when the secret's `current` file was written. The creation is when its oldest
|
|
|
|
|
version's `value.age` was written, not that version's directory, whose time
|
|
|
|
|
changes when the next add rewrites its `metadata.age`. A move to another vault
|
|
|
|
|
gives the copied files the times of the originals. `GetSecretObject` no longer
|
|
|
|
|
loads the times, so `secret get` keeps its error for a missing `current` file.
|
|
|
|
|
- 2026-10-14: `secret unlocker add pgp` without `--keyid` uses the `default-key`
|
|
|
|
|
in `gpg.conf` (https://git.eeqj.de/sneak/secret/issues/141).
|
|
|
|
|
`getDefaultGPGKey` took that value from `gpgconf --list-options gpg` as
|
|
|
|
|
printed, with a leading `"` and percent-escaped, so gpg rejected the key ID.
|
|
|
|
|
The new `parseDefaultGPGKey` drops the `"` and percent-decodes the rest with
|
|
|
|
|
`url.PathUnescape`, which leaves `+` alone. A value that does not decode is an
|
|
|
|
|
error, not a fallback to the first secret key.
|
|
|
|
|
- 2026-10-14: A wrong passphrase, from `SB_UNLOCK_PASSPHRASE` or typed at the
|
|
|
|
|
prompt, gives the error `wrong passphrase for unlocker <id>`
|
|
|
|
|
(https://git.eeqj.de/sneak/secret/issues/157). It used to be age's "no
|
|
|
|
|