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