Found by the security review #73. Severity: low (no key material leaks; wrong results and bad advice).
What is wrong
The private key is parsed in three places with different rules. extractAgeSecretKey (internal/config/config.go:390-402) runs only on VAULTIK_AGE_SECRET_KEY; it keeps the first identity of a multi-identity key file and drops the rest, and on a parse failure silently returns the trimmed raw input, so garbage becomes a non-empty key and CanDecrypt (internal/vaultik/vaultik.go:140-142) reports true. The YAML age_secret_key value skips the function. The key is parsed again as a single identity in internal/vaultik/restore.go:212 and internal/crypto/encryption.go:173.
A key file whose first identity is not a recipient fails with "no identity matched" although a later one would work.
A malformed key fails verify --deep only after the manifest has been fetched, under the prefix "failed to decrypt database" (internal/vaultik/verify.go:276).
README.md:78 and :81, the missing-key error (restore.go:29-33) and the snapshot restore help (internal/cli/snapshot_restore.go:34-35) show the private key typed literally, which leaves it in shell history on the restore machine. The variable already accepts the whole age-keygen file, but nothing says so.
Acceptable
One helper in internal/vaultik parses Config.AgeSecretKey with age.ParseIdentities and is the first step of snapshot restore and snapshot verify --deep, replacing restore.go:207-218 and vaultik.go:156-162. It passes all identities to age.Decrypt. Its error names the source (VAULTIK_AGE_SECRET_KEY or age_secret_key) and never echoes the value.
The check must not live in config.Load: backup, list and prune must keep working whatever the field holds.
extractAgeSecretKey and its silent fallback are removed; the config_test.go:143-151 cases that encode the fallback are replaced by tests of the new helper.
README, error text and help show VAULTIK_AGE_SECRET_KEY="$(cat vaultik_backup_private_key.txt)" and say the variable may hold the whole key file.
Definition of done
Tests: two identities where only the second matches decrypts; a malformed key errors before any download, and the error does not contain the input; a YAML value with a trailing newline works.
make fmt over the changed markdown; no existing assertion weakened; make check green.
Whether the YAML field stays at all is an owner question on #73 (also item 7 of #74).
Line numbers are as of next at 6fcd8e1.
model: fable-5-1
Found by the security review https://git.eeqj.de/sneak/vaultik/issues/73. Severity: **low** (no key material leaks; wrong results and bad advice).
## What is wrong
1. The private key is parsed in three places with different rules. `extractAgeSecretKey` (`internal/config/config.go:390-402`) runs only on `VAULTIK_AGE_SECRET_KEY`; it keeps the first identity of a multi-identity key file and drops the rest, and on a parse failure silently returns the trimmed raw input, so garbage becomes a non-empty key and `CanDecrypt` (`internal/vaultik/vaultik.go:140-142`) reports true. The YAML `age_secret_key` value skips the function. The key is parsed again as a single identity in `internal/vaultik/restore.go:212` and `internal/crypto/encryption.go:173`.
- A key file whose first identity is not a recipient fails with "no identity matched" although a later one would work.
- A malformed key fails `verify --deep` only after the manifest has been fetched, under the prefix "failed to decrypt database" (`internal/vaultik/verify.go:276`).
2. `README.md:78` and `:81`, the missing-key error (`restore.go:29-33`) and the `snapshot restore` help (`internal/cli/snapshot_restore.go:34-35`) show the private key typed literally, which leaves it in shell history on the restore machine. The variable already accepts the whole `age-keygen` file, but nothing says so.
## Acceptable
- One helper in `internal/vaultik` parses `Config.AgeSecretKey` with `age.ParseIdentities` and is the first step of `snapshot restore` and `snapshot verify --deep`, replacing `restore.go:207-218` and `vaultik.go:156-162`. It passes all identities to `age.Decrypt`. Its error names the source (`VAULTIK_AGE_SECRET_KEY` or `age_secret_key`) and never echoes the value.
- The check must not live in `config.Load`: backup, list and prune must keep working whatever the field holds.
- `extractAgeSecretKey` and its silent fallback are removed; the `config_test.go:143-151` cases that encode the fallback are replaced by tests of the new helper.
- README, error text and help show `VAULTIK_AGE_SECRET_KEY="$(cat vaultik_backup_private_key.txt)"` and say the variable may hold the whole key file.
## Definition of done
1. Tests: two identities where only the second matches decrypts; a malformed key errors before any download, and the error does not contain the input; a YAML value with a trailing newline works.
2. `make fmt` over the changed markdown; no existing assertion weakened; `make check` green.
Whether the YAML field stays at all is an owner question on https://git.eeqj.de/sneak/vaultik/issues/73 (also item 7 of https://git.eeqj.de/sneak/vaultik/issues/74).
Line numbers are as of `next` at `6fcd8e1`.
model: fable-5-1
A new internal/vaultik helper parses the age secret key once with age.ParseIdentities and passes every identity to decryption, so a multi-identity key file decrypts a blob encrypted to any of its recipients. It runs as the first step of restore and verify --deep, so a missing or unparseable key fails before any download; the error names the source (VAULTIK_AGE_SECRET_KEY or age_secret_key) and never echoes the value. extractAgeSecretKey and its silent fallback are gone; the key is parsed only where decryption happens, so backup, list and prune are unaffected. README, help, and the missing-key error now read the key from a file with $(cat ...) and note it may hold the whole key file.
Tests: second-identity-only decrypt; trailing-newline value; malformed key errors before any store access without echoing the input; restore exposes no key flag.
The YAML age_secret_key field is left in place; whether it stays is the owner question already open on #73.
Model: opus-4-8
Done in https://git.eeqj.de/sneak/vaultik/pulls/196 (base `next`).
A new `internal/vaultik` helper parses the age secret key once with `age.ParseIdentities` and passes every identity to decryption, so a multi-identity key file decrypts a blob encrypted to any of its recipients. It runs as the first step of restore and `verify --deep`, so a missing or unparseable key fails before any download; the error names the source (`VAULTIK_AGE_SECRET_KEY` or `age_secret_key`) and never echoes the value. `extractAgeSecretKey` and its silent fallback are gone; the key is parsed only where decryption happens, so backup, list and prune are unaffected. README, help, and the missing-key error now read the key from a file with `$(cat ...)` and note it may hold the whole key file.
Tests: second-identity-only decrypt; trailing-newline value; malformed key errors before any store access without echoing the input; restore exposes no key flag.
The YAML `age_secret_key` field is left in place; whether it stays is the owner question already open on https://git.eeqj.de/sneak/vaultik/issues/73.
Model: opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found by the security review #73. Severity: low (no key material leaks; wrong results and bad advice).
What is wrong
extractAgeSecretKey(internal/config/config.go:390-402) runs only onVAULTIK_AGE_SECRET_KEY; it keeps the first identity of a multi-identity key file and drops the rest, and on a parse failure silently returns the trimmed raw input, so garbage becomes a non-empty key andCanDecrypt(internal/vaultik/vaultik.go:140-142) reports true. The YAMLage_secret_keyvalue skips the function. The key is parsed again as a single identity ininternal/vaultik/restore.go:212andinternal/crypto/encryption.go:173.verify --deeponly after the manifest has been fetched, under the prefix "failed to decrypt database" (internal/vaultik/verify.go:276).README.md:78and:81, the missing-key error (restore.go:29-33) and thesnapshot restorehelp (internal/cli/snapshot_restore.go:34-35) show the private key typed literally, which leaves it in shell history on the restore machine. The variable already accepts the wholeage-keygenfile, but nothing says so.Acceptable
internal/vaultikparsesConfig.AgeSecretKeywithage.ParseIdentitiesand is the first step ofsnapshot restoreandsnapshot verify --deep, replacingrestore.go:207-218andvaultik.go:156-162. It passes all identities toage.Decrypt. Its error names the source (VAULTIK_AGE_SECRET_KEYorage_secret_key) and never echoes the value.config.Load: backup, list and prune must keep working whatever the field holds.extractAgeSecretKeyand its silent fallback are removed; theconfig_test.go:143-151cases that encode the fallback are replaced by tests of the new helper.VAULTIK_AGE_SECRET_KEY="$(cat vaultik_backup_private_key.txt)"and say the variable may hold the whole key file.Definition of done
make fmtover the changed markdown; no existing assertion weakened;make checkgreen.Whether the YAML field stays at all is an owner question on #73 (also item 7 of #74).
Line numbers are as of
nextat6fcd8e1.model: fable-5-1
Done in #196 (base
next).A new
internal/vaultikhelper parses the age secret key once withage.ParseIdentitiesand passes every identity to decryption, so a multi-identity key file decrypts a blob encrypted to any of its recipients. It runs as the first step of restore andverify --deep, so a missing or unparseable key fails before any download; the error names the source (VAULTIK_AGE_SECRET_KEYorage_secret_key) and never echoes the value.extractAgeSecretKeyand its silent fallback are gone; the key is parsed only where decryption happens, so backup, list and prune are unaffected. README, help, and the missing-key error now read the key from a file with$(cat ...)and note it may hold the whole key file.Tests: second-identity-only decrypt; trailing-newline value; malformed key errors before any store access without echoing the input; restore exposes no key flag.
The YAML
age_secret_keyfield is left in place; whether it stays is the owner question already open on #73.Model: opus-4-8