From 8583eba71c64747146320e5ef9c37a25b072d6c3 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 15 Oct 2026 06:10:06 +0000 Subject: [PATCH] Log the audit fixes in TODO.md (closes #173) Completed Steps gets one entry, dated 2026-10-15, for each fix merged into next since v1.0.0-rc2 that had none, and for https://git.eeqj.de/sneak/secret/issues/152 and https://git.eeqj.de/sneak/secret/issues/189, whose PRs are still in review. Each entry keeps to what its landing commit or PR says. The five fixes that logged themselves before this issue's decision keep their entries. Status no longer says there are no git tags. Unverified: the entries for 152 and 189 describe the PRs as they stand; neither had merged when this was pushed. Model: opus-5-5 --- TODO.md | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 2 deletions(-) diff --git a/TODO.md b/TODO.md index 42302ae..4b7e75d 100644 --- a/TODO.md +++ b/TODO.md @@ -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,176 @@ https://git.eeqj.de/sneak/secret/milestone/12 # Completed Steps +- 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). Unlocking can ask for a + passphrase, so while the move held the lock, every other command that changes + anything waited at that prompt. `moveSecretCrossVault` checks that the source + secret and the destination vault exist, unlocks both vaults, and only then + locks. Under the lock, `copySecretAndRemoveSource` checks again that the + source and the destination vault still exist, then copies the secret and + removes the source as before. `MoveSecret` takes no lock itself; a move within + a vault takes it in `moveSecretWithinVault`. +- 2026-10-15: `secret init`, `secret vault create`, `secret unlocker add`, and + `secret add`, `secret import` and `secret generate secret` with `--force` on + an existing secret ask for the mnemonic or passphrase and unlock the vault + before they take the state directory lock + (https://git.eeqj.de/sneak/secret/issues/152). While one of them waited at its + prompt, every other command that changes anything waited with it. What a + command checked before asking is checked again under the lock, since another + command may have changed it meanwhile. `secret unlocker add` fails if the + vault was removed, and `secret unlocker add pgp` refuses a GPG key added in + the meantime. A forced add of a secret that another command created meanwhile + lets go of the lock, gets the key and takes the lock again, so it still + replaces the secret. `secret.CreatePGPUnlocker` now takes the vault, already + unlocked; `SetMnemonic` and `SetUnlockPassphrase`, which only it used, are + gone. The README says which commands ask before they lock. +- 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, as 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 `. `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 "" for "secret "`, 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 [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-15: `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 an invalid name + also fails before the prompts. `secret init` prints the "Initialized" line + only once the vault is created. +- 2026-10-15: `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-15: `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-15: `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 ` (https://git.eeqj.de/sneak/secret/issues/157). It used to be age's "no