diff --git a/TODO.md b/TODO.md index 060a89d..9732040 100644 --- a/TODO.md +++ b/TODO.md @@ -2,7 +2,6 @@ - branch from `next` - do the Next Step: the next open issue in the `1.0.0` milestone -- log it at the top of Completed Steps - commit (`TODO.md` changes in the same commit as the work) - push, and open a PR against `next` @@ -15,779 +14,15 @@ pre-1.0. Open work is tracked on the issue tracker, which is authoritative. Take the next open issue in the `1.0.0` milestone: https://git.eeqj.de/sneak/secret/milestone/12 -# Completed Steps - -- 2026-10-15: The comment on `commandWait` in `internal/cli/integration_test.go` - no longer says `script/test` gives the package 30 seconds - (https://git.eeqj.de/sneak/secret/issues/195). `script/test` sets no time - limit; the test phase of the `Dockerfile` sets one with `go test -timeout`. - The comment now names that place instead of a number. -- 2026-10-15: The canonical files are re-vendored from `sneak/prompts` commit - `e3f79c4` (https://git.eeqj.de/sneak/secret/issues/129), so Go's build cache - survives between check builds and `make test` no longer compiles the standard - library and every dependency with `-race` on every run. `script/lint`, - `script/test`, `script/cibuild` and `script/docker` pass a new `CACHEBUST` - value instead of `--no-cache`, and every stage of the `Dockerfile` declares - `ARG CACHEBUST` first, so every `RUN` step still reruns on an unchanged tree. - The steps that compile mount Go's build cache, and both `go test` runs keep - `-count=1`, so no test result is reused. `script/lint` and `script/test` write - no image. The CI workflow cancels a branch's older run, stops after 20 - minutes, fetches the tags and leaves no token behind. `script/bootstrap` uses - the installed node only when its major version is the pinned one. -- 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 `. `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-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 ` - (https://git.eeqj.de/sneak/secret/issues/157). It used to be age's "no - identity matched any of the recipients", followed by advice to give the vault - a new unlocker from its mnemonic, which would have replaced an unlocker that - works. The error now wraps `secret.ErrWrongPassphrase` and carries no advice - to use the mnemonic. A `priv.age` that is not an age file still gets that - advice. -- 2026-10-14: The passphrase and mnemonic prompts ask on the controlling - terminal (`/dev/tty`) instead of stdin, so `echo v | secret add s1 --force`, - `cat file | secret encrypt key` and `cat file.age | secret decrypt key` ask - for the passphrase at a terminal instead of failing - (https://git.eeqj.de/sneak/secret/issues/144). Only a process with no - controlling terminal gets the error that names the variable to set. On SIGINT - or SIGTERM, `Entry()` restores the settings of that terminal, not of stdin, - since that is where the prompt turns echo off. `secret init` and - `secret vault create` no longer write a newline to stderr after the mnemonic - is typed, since the prompt ends that line on the terminal. -- 2026-10-14: `secret generate mnemonic`, `secret vault list` and - `secret unlocker list` print their output to stdout, as `secret list` does, so - it can be piped or captured (https://git.eeqj.de/sneak/secret/issues/138). - They printed with cobra's `cmd.Println` and `cmd.Printf`, which write to - stderr when no output writer is set, so `secret vault list --json | jq` - received nothing. "No versions found" from `secret version list` moved to - stdout too. Messages such as "Created vault", and the warning after the - mnemonic, stay on stderr. -- 2026-10-14: Removing the current unlocker no longer leaves the vault unable to - open through the unlockers it still has - (https://git.eeqj.de/sneak/secret/issues/137). `current-unlocker` went on - naming the removed unlocker, so every command that opened the vault failed, - with advice to use the mnemonic. `secret unlocker remove` of the current - unlocker now makes the first of the vault's other unlockers that - `secret unlocker select` accepts, in the order `secret unlocker list` shows, - current before removing it, and says so; when there is none, it also removes - `current-unlocker`. `vault.RemoveUnlocker` returns the ID of the unlocker it - made current. When `current-unlocker` names an unlocker that does not exist - and the vault has another that `secret unlocker select` accepts, the error - wraps `vault.ErrUnlockerNotFound` and says to run `secret unlocker select`, - without the advice to use the mnemonic. -- 2026-10-14: A `secret add --force` that cannot unlock the vault, given a wrong - passphrase or mnemonic, leaves the secret as it was - (https://git.eeqj.de/sneak/secret/issues/136). The new version used to be - written before the vault was unlocked to set `notAfter` on the version it - replaces, so each failed add left one more version that `secret version list` - showed. The vault is now unlocked, and that version's metadata read, before - anything is written. `generate secret --force` and `import --force` take the - same path. -- 2026-10-12: A mnemonic typed with extra spaces, tabs or line breaks gives the - same long-term key as its words separated by single spaces - (https://git.eeqj.de/sneak/secret/issues/135). The key was derived from the - text exactly as typed, so a vault made with a double or trailing space did not - open with the same words typed normally. `agehd.DeriveEntropy`, which every - use of the mnemonic goes through, now joins the words with single spaces and - puts them in Unicode NFKD, as BIP39 defines, before making the seed. -- 2026-10-12: The keychain and Secure Enclave unlockers are gone from the code - and the documents (https://git.eeqj.de/sneak/secret/issues/132). They were - research: neither works without macOS code signing and entitlements, which - `secret` does not have. The last commit that has them is `ef0ae90`, tagged - `v1.0.0-rc1`. `secret unlocker add` takes only `passphrase` and `pgp` on every - platform, `internal/macse` and those unlockers' files in `internal/secret` are - deleted, and `github.com/keybase/go-keychain` left `go.mod` and `go.sum`. - `secret` still builds and runs on macOS. -- 2026-10-07: The part of `github.com/tyler-smith/go-bip39` v1.1.0 that `secret` - uses is copied into `internal/bip39`, with upstream's `LICENSE` beside it, - because its repository no longer exists - (https://git.eeqj.de/sneak/secret/issues/122). Every import moved there, and - the module left `go.mod` and `go.sum`. No derived key or mnemonic changes. -- 2026-10-07: The canonical files are re-vendored from `sneak/prompts` commit - `dd4027b` (https://git.eeqj.de/sneak/secret/issues/121), with golangci-lint - v2.14.0 in the lint phase. Lint and test are phases of the `Dockerfile`, and - `script/lint` and `script/test` each build one with `--no-cache`; - `Dockerfile.lint` and `script/lint-darwin` are gone, and the lint phase runs - `go vet` and checks the macOS build too. The tests run on the Debian Go image - with cgo and the race detector, with the policy's 90-second timeout. - `script/cibuild` bootstraps, runs `script/check` and builds the image; - `CHECK_EPOCH` and the memlock ulimit are gone. `--no-cache` starts Go's build - cache mount empty, so `make test` compiles everything on every run. The rules - in `CLAUDE.md` that `AGENTS.md` lacked are in `AGENTS.md`, and `CLAUDE.md` is - deleted. -- 2026-10-06: `script/test` runs the tests with the race detector, a 30-second - timeout per package and coverage, as `REPO_POLICIES.md` requires - (https://git.eeqj.de/sneak/secret/issues/32). When they fail, it reruns them - verbosely for the details and then fails anyway, so a test that fails once and - passes on the retry no longer gives a green build. `go vet` still runs first, - and every `go test` keeps `-count=1`. The tests that gave the `secret` binary - a minute now give it 10 seconds, and the PGP unlocker test's 30-second timer - is 10 seconds, so a test that hangs fails with its own message before the - package's 30-second timeout ends every test in it. -- 2026-10-06: `make test` in `script/cibuild` no longer compiles the standard - library and every dependency from nothing on every build - (https://git.eeqj.de/sneak/secret/issues/124). The `Dockerfile` runs it and - `make build` with Go's build cache in a BuildKit cache mount, which docker - keeps between builds, so each compiles only what changed since the last build. - The mount has an id of its own, so other repositories' builds do not share it. - `script/test` passes `-count=1`, so every test runs on every build and no - result comes from Go's test cache. A build with an empty cache, such as the - first after docker's build cache is cleared, compiles everything in - `make test` as before. -- 2026-10-06: `TestRemoveIgnoresTerminalOnStdout` and - `TestRemoveAsksAtTerminalOnStdin` no longer wait until Go's test timeout - (https://git.eeqj.de/sneak/secret/issues/126). On Linux, `pty.Open` of - `github.com/creack/pty` v1.1.24 passed the address of a local variable to the - `ioctl` system call as a plain number, through a function call; when Go moved - the goroutine's stack in between, the kernel wrote the terminal's number to - the old place, and `pty.Open` opened `/dev/pts/0` instead of the terminal it - had created. `secret rm` then wrote to that other terminal, and the test read - a terminal no program had open, which never ends. `go.mod` now requires the - commit on that library's main branch that passes a pointer instead; no release - has it yet. Both tests stop reading the terminal when their one-minute context - ends and fail saying so. -- 2026-10-06: The tests run quickly with the race detector on - (https://git.eeqj.de/sneak/secret/issues/120). Most of their time went to - deriving keys from passphrases with scrypt, which is slow on purpose. The new - `secret.ScryptWorkFactor`, when not zero, replaces age's scrypt work factor - when a passphrase encrypts; the tests of `internal/secret`, `internal/vault` - and `internal/cli` set it to 1 before any test runs, and the program never - sets it; `TestGetCommandOutputsToStdout` checks that the passphrase unlocker - the built binary's `secret init` writes names age's work factor, 18. - `TestRemovalAsksWithoutHoldingLock` and `TestFailedCommandReleasesLock` no - longer run in parallel with other tests: each waits at most 10 seconds for the - in-memory lock that every test in the package shares, and other tests' - commands held it longer. `TestConcurrentAddsKeepEveryVersion`, which times - nothing, and `TestGetCommandOutputsToStdout`, which no longer sets an - environment variable its commands do not read, now run in parallel. The - `script/cibuild` comment no longer says that tests are skipped without its - memlock ulimit. -- 2026-10-05: No test stores a secret larger than 1 MiB - (https://git.eeqj.de/sneak/secret/issues/52). The size tests for `secret add`, - `secret import` and the stdin buffer no longer try 2 MB, 10 MB, 99 MB, 100 MB - or 101 MB secrets, and nothing tests that a secret over the 100 MB limit is - rejected; the limit itself is unchanged. With nothing large left, the size - tests no longer skip a case for want of locked memory. -- 2026-10-05: The Go module path is `sneak.berlin/go/secret`, as - `REPO_POLICIES.md` requires, not `git.eeqj.de/sneak/secret` - (https://git.eeqj.de/sneak/secret/issues/43). Every import uses it, as do the - `-X` flags in `script/build` that stamp the version and commit shown by - `secret info`, and the examples in `pkg/agehd/README.md` and - `pkg/bip85/README.md`. `go mod tidy` now lists `github.com/dustin/go-humanize` - and `github.com/fatih/color`, which `internal/cli` imports, as direct - requirements. Code that imported the old path must switch to the new one. -- 2026-10-04: `make fmt` formats every markdown file with prettier (4-space - tabs, `proseWrap: always`) as well as the Go code, and `make fmt-check` checks - both, as the model scripts in the `prompts` repo do - (https://git.eeqj.de/sneak/secret/issues/110). Prettier is pinned by hash in - `package.json` and `yarn.lock`, and `script/bootstrap` installs node, yarn and - prettier. The `Dockerfile` lint stage copies node and yarn from a node image - pinned by hash and runs `script/bootstrap`, so its `make fmt-check` fails the - build on an unformatted markdown file. Every markdown file was formatted once, - wording unchanged. -- 2026-10-04: A mnemonic that cannot be read, in `secret init` and - `secret vault create`, gives an error that names the mnemonic only - (https://git.eeqj.de/sneak/secret/issues/115). It is read with - `secret.ReadMnemonic`, whose every error wraps the new - `secret.ErrMnemonicNotRead`; before, it was read with `ReadPassphrase`, so the - message said "failed to read mnemonic: failed to read passphrase:" and advised - setting `SB_UNLOCK_PASSPHRASE`. Without a terminal it now says "failed to read - mnemonic: stdin is not a terminal (piped input or script). Please set the - SB_SECRET_MNEMONIC environment variable or run interactively". The passphrase - messages no longer repeat "cannot read passphrase" after "failed to read - passphrase:", and empty input gives "nothing was entered". -- 2026-10-04: A failure returns the same error value whichever command hits it - (https://git.eeqj.de/sneak/secret/issues/113). `internal/cli` no longer keeps - its own copies of `vault.ErrSecretNotFound`, `ErrVaultNotFound`, - `ErrVersionNotFound` and `ErrSecretExists`: `secret mv`, `rm`, `decrypt`, - `vault import`, `vault remove` and `version list`, `promote` and `rm` wrap the - `vault` errors. `errUnsupportedUnlockerType` is removed: `secret unlocker add` - gives `errInvalidUnlockerType` for an unknown type, whichever check rejects - it. Off macOS, adding a keychain or Secure Enclave unlocker returns the - `secret` package's error for it, not an `internal/cli` copy; on macOS, the - check that the system is macOS is gone, as it could never fail. - `secret vault import` gives `errInvalidMnemonicPhrase` for an invalid - mnemonic, as `init` and `vault create` do. `secret generate secret` gives - `errLengthTooSmall` for a length below 1 wherever it is checked, and - `errUnsupportedSecretType` for `--type mnemonic` too. `secret import` of a - file over 100MB wraps `errSecretTooLarge`, as `secret add` returns it. - `vault.ErrNilValueBuffer` is replaced by `secret.ErrNilValueBuffer`, which - `secret` already returned under another name. Messages are unchanged, except - that `secret decrypt` of a missing secret says "not found", as `secret get` - does, not "does not exist"; `vault import` of an invalid mnemonic says - "invalid BIP39 mnemonic phrase"; `--type mnemonic` says "unsupported type: - mnemonic (use 'secret generate mnemonic' instead)"; and a file too large to - import says - `failed to read secret from file : secret too large: exceeds 100MB limit`. - Every error of `secret.ReadPassphrase` wraps `secret.ErrPassphraseNotRead`, - which supplies the words "failed to read passphrase" that its callers used to - add themselves; so two passphrases that differ now give only "passphrases do - not match", the words now follow "failed to read mnemonic:" and "failed to - read passphrase confirmation:", and a terminal read error no longer repeats - them. A GPG key the keyring does not hold gives `secret.ErrGPGKeyNotFound`, - found by gpg's status line for "No public key"; before, the message repeated - "failed to resolve GPG key fingerprint" and ended in gpg's exit status. The - keychain unlocker returns `errNilDataBuffer` for nil data; this and its test - build only on macOS with cgo and were only read. `bip85.ErrPasswordTooShort` - and `ErrEncodedTooShort` are removed with their checks: 64 bytes of entropy - always give 86 Base64 or 80 Base85 characters, the most a password length may - ask for. Tests that matched these errors' text use `errors.Is`. -- 2026-10-04: Tests check which error a failure returns with `errors.Is`, not by - matching words of its message (https://git.eeqj.de/sneak/secret/issues/49). - Every exported error that can be returned has a test that the function returns - it, and errors wrapping a cause are checked through the wrapping. Checks that - still match text, because the error has no exported value the test can name, - are listed on the issue. -- 2026-10-04: When a vault cannot be opened through its current unlocker, - because a file the unlocker needs is missing or damaged, its keychain item or - Secure Enclave key is gone, or the passphrase is wrong, the error now ends by - naming the vault, saying that it still opens with its mnemonic, and that - `secret unlocker add passphrase`, run with `SB_SECRET_MNEMONIC` set to it, - gives the vault a new unlocker; for a vault that is not the current one, as in - `secret move` between vaults, it says to run `secret vault select` first - (https://git.eeqj.de/sneak/secret/issues/47). Before, it ended with the bare - cause. The advice is given only when the vault metadata records the key the - mnemonic derives, so not for a vault created without a mnemonic, and not when - the passphrase could not be read at all. `secret vault import` is not named: - it refuses a vault that has a long-term key. `secret encrypt` and - `secret decrypt` now read the key secret through `vault.GetSecret`, as - `secret get` does, so they give the same advice; `Secret.GetValue`, the other - way to get the long-term key, is removed. When a secret's `current` file - cannot be read, the error says that `secret version list` lists its versions - and `secret version promote` makes one current. The causes stay wrapped. -- 2026-10-04: An unlocker's ID is the name of its directory in `unlockers.d`, so - no two unlockers of a vault share one - (https://git.eeqj.de/sneak/secret/issues/98). Before, a keychain or Secure - Enclave unlocker's ID was its creation time to the minute and the host name, - and a passphrase unlocker's the time to the minute, so two created within a - minute shared an ID, and `unlocker select`, `unlocker remove` and the - selection `unlocker add` makes acted on the older one. A PGP unlocker's ID was - `pgp-` and its key's fingerprint; a second PGP unlocker for a key is still - refused, now by comparing the fingerprint in the other unlockers' metadata. - `unlocker list` and the shell completion of `unlocker select` and - `unlocker remove` take each ID from the directory the unlocker was read from, - no longer by matching metadata, so two unlockers with the same metadata are - listed apart; an unlocker of an unknown type is listed under its directory - name, and completion now offers Secure Enclave unlockers too. The keychain and - Secure Enclave code was type-checked by `script/lint-darwin`, never run; a - test on Linux lists, completes, selects and removes each of two passphrase - unlockers with the same metadata by its own ID. -- 2026-10-04: README's Storage Architecture, `secret version promote`, Technical - Details and Testing text matches the code - (https://git.eeqj.de/sneak/secret/issues/102). `current` and `currentvault` - are plain files holding a name, not symbolic links; a version's metadata is - the encrypted `metadata.age`; the state directory is `berlin.sneak.pkg.secret` - in the user's configuration directory, not `~/.local/share/secret`, and holds - the `lock` file. Also corrected: the code sets up no Touch ID for the keychain - or Secure Enclave unlocker, and the Secure Enclave only decrypts; per-version - keys give no forward secrecy; `pub.age` is not age-encrypted; vault metadata - holds no vault name. Testing lists only `make test`. -- 2026-10-04: `secret init` and `secret vault create` create a vault whole or - not at all (https://git.eeqj.de/sneak/secret/issues/105). `vault.CreateVault` - now takes the unlocker passphrase too, writes the vault directory with its - metadata, long-term public key and passphrase unlocker, `longterm.age` - included, into a temporary directory, renames that into `vaults.d` once it is - complete, and only then makes the vault current. Before, either command killed - after the passphrase prompt but before the unlocker was written left a vault - with no unlocker, which `vault create` had already made current and which - neither command would create again. Killed part-way now, it leaves no vault, - and the next command that takes the lock deletes the temporary directory; or, - killed between the rename and making the vault current, a complete vault that - is not current, which `secret vault select` makes current. -- 2026-10-04: A failed `secret unlocker add keychain` or - `secret unlocker add secure-enclave` no longer leaves its keychain item or - Secure Enclave key behind (https://git.eeqj.de/sneak/secret/issues/89). - `CreateSecureEnclaveUnlocker` gets the long-term key before it creates the - Secure Enclave key, so that a wrong passphrase creates none, and deletes the - key again if encrypting with it or writing the unlocker then fails. - `macse.CreateKey` finds the new key's hash right after `sc_auth` creates it, - and fails with an error naming the key's label if it cannot; it deletes the - key again if getting its public key then fails. The Objective-C was only read, - never compiled or run, and so was `macse_darwin.go`, which is cgo only. - `CreateKeychainUnlocker` writes all of the unlocker's files, the metadata - among them, before it stores the item in the keychain, and deletes the item - again if moving the unlocker into place then fails. A failure to delete is - reported along with the first error. The tests of this run only on macOS: the - Secure Enclave one in a build with cgo on a Mac with a Secure Enclave, the - keychain one in a build with cgo. -- 2026-10-04: What a command killed part-way left under a `.tmp-` name - (https://git.eeqj.de/sneak/secret/issues/75), the temporary directories of - `secret.TempDirFor` and the temporary files of `secret.WriteFileAtomic`, - encrypted keys included, is deleted by the next command that takes the state - directory lock. Before, it stayed until deleted by hand. A command writes - `finished` into the lock file just before it releases the lock; the next one - to take the lock searches only when it does not find that, so after a command - that finished nothing is searched, however many secrets and versions there - are. The search looks in the state directory, each vault, each secret and each - version, the only directories those helpers make them in. A command that only - reads takes no lock and deletes nothing. A failure to delete is warned about - and the command goes on. An unlocker directory with no metadata file was - already removed by `secret unlocker remove` given its directory name; a test - now shows it. -- 2026-10-04: An age identity's private key goes into a locked buffer through - `secret.IdentityToLockedBuffer` everywhere - (https://git.eeqj.de/sneak/secret/issues/38): the vault's long-term key when a - passphrase, PGP, keychain or Secure Enclave unlocker is created, the new - unlocker's own key, a new secret version's key, and the key `secret encrypt` - generates. Before, each place converted the string age returns to bytes and - left the string in ordinary memory. The function moves the string's own bytes - into the buffer, which overwrites them; the copies age makes while writing the - string remain, as its comment says. The 1.0 memory-security entry below no - longer lists these places, `internal/cli/crypto.go` among them, nor - `version.go:155`, which was `internal/secret/version.go`, not - `internal/cli/version.go`. -- 2026-10-04: `script/lint-darwin` (`make lint-darwin`) runs `go vet` and - `golangci-lint` in docker on the code as a macOS build compiles it - (`GOOS=darwin`), with cgo off (https://git.eeqj.de/sneak/secret/issues/50). - `script/check` runs it, and the `Dockerfile` lint stage runs its commands, so - `script/cibuild` does too. Before, CI on Linux never compiled the files built - only for macOS. Compiling cgo code for macOS needs Apple's SDK headers, and - both `internal/macse` and `github.com/keybase/go-keychain` are cgo on macOS. - So the three functions that call `go-keychain` moved from - `keychainunlocker.go` to `keychainunlocker_cgo.go`, built only with cgo on - macOS like `macse_darwin.go`. A macOS build without cgo, which before did not - compile, gets `keychainunlocker_nocgo.go` and the `macse` stub instead, whose - errors say the keychain or Secure Enclave needs a macOS build with cgo. The - check covers the rest of the keychain unlocker, the Secure Enclave unlocker - and the macOS-only tests other than `keychainunlocker_test.go`, whose lint - findings are fixed. For the length and complexity limits, parts of - `GetIdentity`, `getLongTermPrivateKey` and `CreateKeychainUnlocker` moved into - functions of their own, and the Secure Enclave unlocker derives the long-term - key from the mnemonic through the same function as the keychain unlocker - instead of a copy of it. Lines over 88 columns in the files the check cannot - see are wrapped. -- 2026-10-04: `secret rm`, `secret version rm`, `secret vault remove` and - `secret unlocker remove` ask `[y/N]` before removing anything - (https://git.eeqj.de/sneak/secret/issues/39), naming what they remove: the - secret, its vault and its version count; the version, secret and vault; the - vault and its secret count; the unlocker, its vault and whether it is the - last, and for the last the vault's secret count and that the vault then opens - only with its mnemonic. Only `y` or `yes` goes ahead. Without `--force`, a - command whose stdin is not a terminal fails at once. `--force` (now also on - `rm` and `version rm`) removes without asking; it replaces the old refusals to - remove a vault with secrets or the last unlocker of one without `--force`, - which the question now covers. The checks run, and the question is asked, - before the state directory lock is taken; under the lock the checks run again, - and if they would ask a different question, nothing is removed. `secret rm` - fails when it cannot count the versions. -- 2026-10-04: A crash while an unlocker is being replaced no longer leaves a - current unlocker that cannot open the vault - (https://git.eeqj.de/sneak/secret/issues/71). Every new unlocker gets a - directory of its own, named with the time to the nanosecond: - `passphrase-