Adds the age command group: pub, priv, encrypt and decrypt. The
application number is 657169, so the path is m/83696968'/657169'/<n>'.
The branch follows the shape the README lays out for a new key type: one
package under internal/ that turns the 32 derived bytes into a key, and one
command group under internal/cli/. The bytes are clamped the way X25519
requires and go through bech32 into an age identity, which is the only route
age offers from raw bytes to a key, and the same steps sneak/secret takes in
its agehd package. I read that package to confirm the match: bech32.EncodeFromBase256 is exactly the convert-then-encode pair it uses.
What the diff does not show:
Decrypting recognises the text form by the line it begins with, so there is
no armor flag on decrypt. That is what the age tool itself does.
--to accepts age1... recipients only. The README calls a recipient the age1... public key, so ssh recipients are outside this issue.
A file named with -o is created readable only by its owner. The README does
not say; a decrypted file is as secret as what went into it, and that is the
reading I took.
Unverified: this host has no age binary, so nothing was checked against the
reference implementation. The round trips in the tests go through this code
only.
make check is green.
Model: opus-5
Closes https://git.eeqj.de/sneak/keyfunc/issues/3
Adds the `age` command group: `pub`, `priv`, `encrypt` and `decrypt`. The
application number is 657169, so the path is `m/83696968'/657169'/<n>'`.
The branch follows the shape the README lays out for a new key type: one
package under `internal/` that turns the 32 derived bytes into a key, and one
command group under `internal/cli/`. The bytes are clamped the way X25519
requires and go through bech32 into an age identity, which is the only route
age offers from raw bytes to a key, and the same steps `sneak/secret` takes in
its `agehd` package. I read that package to confirm the match:
`bech32.EncodeFromBase256` is exactly the convert-then-encode pair it uses.
What the diff does not show:
- Decrypting recognises the text form by the line it begins with, so there is
no armor flag on `decrypt`. That is what the `age` tool itself does.
- `--to` accepts `age1...` recipients only. The README calls a recipient the
`age1...` public key, so ssh recipients are outside this issue.
- A file named with `-o` is created readable only by its owner. The README does
not say; a decrypted file is as secret as what went into it, and that is the
reading I took.
Unverified: this host has no `age` binary, so nothing was checked against the
reference implementation. The round trips in the tests go through this code
only.
`make check` is green.
Model: opus-5
clawbot
self-assigned this 2026-09-07 17:44:51 +02:00
1. internal/agekey/agekey.go:55 with internal/agekey/agekey_test.go:16-21: the clamping step is pinned by nothing.
Deleting clamp(scalar) leaves the whole suite green. X25519 clamps internally, so the recipient does not move — but keyfunc age priv then prints a different AGE-SECRET-KEY-1... line for the same mnemonic and index. Clamping is a step the specification states, and the identity is what makes a key interchangeable with the one sneak/secret derives, so the part of the derivation a later edit is most likely to break is unguarded. Acceptable: a fixed identity constant for the example mnemonic at index 0, beside the two recipient constants and asserted the same way.
2. internal/cli/age/age.go:192-194: a refused decryption destroys the file named by -o. output() opens the file with O_TRUNC before any work is attempted. Decrypting a file that was encrypted only to another key, with -o naming an existing file, prints the refusal and exits 1 — and leaves that file empty. A wrong index is a foreseeable mistake, and it silently loses whatever was there. Acceptable: write to a temporary file beside the target and rename it once the work succeeds, or remove the file when the work returns an error.
Verified
Rebased head against current next, which it merges cleanly; make check in a fresh clone; and the built binary for each item of the definition of done — the fixed recipient at indexes 0 and 1, byte and text round trips with and without --armor across both files and standard input, refusal with the stated message and status 1, and the derived recipient still able to read a file that --to addressed to somebody else. The identity derivation matches sneak/secret's agehd for the same 32 input bytes.
The branch's open item is closed: output from this branch decrypts under the reference age implementation in both the binary and the text form, that implementation's output decrypts here, and asking it for the public key of the printed identity returns the printed recipient.
Defects were planted in a scratch copy for the recipient list, the armor flag, the text-form detection and the refusal error; each was caught. Finding 1 is the one that was not.
Model: opus-5
**FAIL** — `needs-rework`.
**1. `internal/agekey/agekey.go:55` with `internal/agekey/agekey_test.go:16-21`: the clamping step is pinned by nothing.**
Deleting `clamp(scalar)` leaves the whole suite green. X25519 clamps internally, so the recipient does not move — but `keyfunc age priv` then prints a different `AGE-SECRET-KEY-1...` line for the same mnemonic and index. Clamping is a step the specification states, and the identity is what makes a key interchangeable with the one `sneak/secret` derives, so the part of the derivation a later edit is most likely to break is unguarded. Acceptable: a fixed identity constant for the example mnemonic at index 0, beside the two recipient constants and asserted the same way.
**2. `internal/cli/age/age.go:192-194`: a refused decryption destroys the file named by `-o`.**
`output()` opens the file with `O_TRUNC` before any work is attempted. Decrypting a file that was encrypted only to another key, with `-o` naming an existing file, prints the refusal and exits 1 — and leaves that file empty. A wrong index is a foreseeable mistake, and it silently loses whatever was there. Acceptable: write to a temporary file beside the target and rename it once the work succeeds, or remove the file when the work returns an error.
**Verified**
Rebased head against current `next`, which it merges cleanly; `make check` in a fresh clone; and the built binary for each item of the definition of done — the fixed recipient at indexes 0 and 1, byte and text round trips with and without `--armor` across both files and standard input, refusal with the stated message and status 1, and the derived recipient still able to read a file that `--to` addressed to somebody else. The identity derivation matches `sneak/secret`'s `agehd` for the same 32 input bytes.
The branch's open item is closed: output from this branch decrypts under the reference `age` implementation in both the binary and the text form, that implementation's output decrypts here, and asking it for the public key of the printed identity returns the printed recipient.
Defects were planted in a scratch copy for the recipient list, the armor flag, the text-form detection and the refusal error; each was caught. Finding 1 is the one that was not.
Model: opus-5
Rebased onto next (the child-mnemonic unit). The command-tree file conflicted; resolved by registering all three command groups. The rebase required a force-push.
The secret key the example mnemonic gives at index 0 is now a fixed constant beside the two recipients, asserted the same way, so removing the clamping step fails the suite.
age encrypt and age decrypt with -o now write a new file beside the target and rename it into place only once the work succeeds; on failure it is thrown away, so a refused decryption leaves an existing file untouched. The new file keeps the owner-only mode. A test covers the refusal case.
make check is green.
Model: opus-5
Rebased onto `next` (the child-mnemonic unit). The command-tree file conflicted; resolved by registering all three command groups. The rebase required a force-push.
1. The secret key the example mnemonic gives at index 0 is now a fixed constant beside the two recipients, asserted the same way, so removing the clamping step fails the suite.
2. `age encrypt` and `age decrypt` with `-o` now write a new file beside the target and rename it into place only once the work succeeds; on failure it is thrown away, so a refused decryption leaves an existing file untouched. The new file keeps the owner-only mode. A test covers the refusal case.
`make check` is green.
Model: opus-5
No findings. Both findings from the first review are fixed, and each fix is
guarded by a test that fails when the fix is taken back out.
Verified
The head already sits on current next and merges cleanly. make check in a
fresh clone exits 0, with the lint stage actually executing rather than replayed
from cache.
Finding 1: internal/agekey/agekey_test.go:24 now fixes the secret key for the
example mnemonic at index 0, beside the two recipients and asserted the same
way. Removing clamp(scalar) in a scratch copy fails that test and only that
test — the recipients still match, which is the gap the first review described.
The constant decodes to a 32-byte scalar clamped as RFC 7748 requires, checked
outside this code.
Finding 2: internal/cli/age/age.go:176-225 writes beside the target and
renames into place only once the work succeeds. From the built binary, a refused
decryption with -o naming an existing file exits 1 with the stated message and
leaves that file byte-identical, with no leftover partial file. Making finish
rename despite the failure fails the new test at internal/cli/age_test.go:75.
Also from the binary: encrypt then decrypt round-trips bytes and text, with and
without --armor, over both files and standard input; -o output is owner-only;
and a missing input, an unparseable --to, an unwritable output directory and a
truncated file each exit 1 without leaving anything behind.
Scope: the diff touches the age package, its command group, the command
registration and the filippo.io/age requirement. Nothing else.
Disclosures: the commit body runs slightly over the 120-word guide, which I read
as within its latitude rather than a finding; and the new test carries one lint
suppression at internal/cli/age_test.go:87, for a path it creates itself.
Model: opus-5
**PASS.**
No findings. Both findings from the first review are fixed, and each fix is
guarded by a test that fails when the fix is taken back out.
**Verified**
The head already sits on current `next` and merges cleanly. `make check` in a
fresh clone exits 0, with the lint stage actually executing rather than replayed
from cache.
Finding 1: `internal/agekey/agekey_test.go:24` now fixes the secret key for the
example mnemonic at index 0, beside the two recipients and asserted the same
way. Removing `clamp(scalar)` in a scratch copy fails that test and only that
test — the recipients still match, which is the gap the first review described.
The constant decodes to a 32-byte scalar clamped as RFC 7748 requires, checked
outside this code.
Finding 2: `internal/cli/age/age.go:176-225` writes beside the target and
renames into place only once the work succeeds. From the built binary, a refused
decryption with `-o` naming an existing file exits 1 with the stated message and
leaves that file byte-identical, with no leftover partial file. Making `finish`
rename despite the failure fails the new test at `internal/cli/age_test.go:75`.
Also from the binary: encrypt then decrypt round-trips bytes and text, with and
without `--armor`, over both files and standard input; `-o` output is owner-only;
and a missing input, an unparseable `--to`, an unwritable output directory and a
truncated file each exit 1 without leaving anything behind.
Scope: the diff touches the age package, its command group, the command
registration and the `filippo.io/age` requirement. Nothing else.
Disclosures: the commit body runs slightly over the 120-word guide, which I read
as within its latitude rather than a finding; and the new test carries one lint
suppression at `internal/cli/age_test.go:87`, for a path it creates itself.
Model: opus-5
clawbot
merged commit 5bbeec86d6 into next2026-09-07 18:35:11 +02:00
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.
Closes #3
Adds the
agecommand group:pub,priv,encryptanddecrypt. Theapplication number is 657169, so the path is
m/83696968'/657169'/<n>'.The branch follows the shape the README lays out for a new key type: one
package under
internal/that turns the 32 derived bytes into a key, and onecommand group under
internal/cli/. The bytes are clamped the way X25519requires and go through bech32 into an age identity, which is the only route
age offers from raw bytes to a key, and the same steps
sneak/secrettakes inits
agehdpackage. I read that package to confirm the match:bech32.EncodeFromBase256is exactly the convert-then-encode pair it uses.What the diff does not show:
no armor flag on
decrypt. That is what theagetool itself does.--toacceptsage1...recipients only. The README calls a recipient theage1...public key, so ssh recipients are outside this issue.-ois created readable only by its owner. The README doesnot say; a decrypted file is as secret as what went into it, and that is the
reading I took.
Unverified: this host has no
agebinary, so nothing was checked against thereference implementation. The round trips in the tests go through this code
only.
make checkis green.Model: opus-5
FAIL —
needs-rework.1.
internal/agekey/agekey.go:55withinternal/agekey/agekey_test.go:16-21: the clamping step is pinned by nothing.Deleting
clamp(scalar)leaves the whole suite green. X25519 clamps internally, so the recipient does not move — butkeyfunc age privthen prints a differentAGE-SECRET-KEY-1...line for the same mnemonic and index. Clamping is a step the specification states, and the identity is what makes a key interchangeable with the onesneak/secretderives, so the part of the derivation a later edit is most likely to break is unguarded. Acceptable: a fixed identity constant for the example mnemonic at index 0, beside the two recipient constants and asserted the same way.2.
internal/cli/age/age.go:192-194: a refused decryption destroys the file named by-o.output()opens the file withO_TRUNCbefore any work is attempted. Decrypting a file that was encrypted only to another key, with-onaming an existing file, prints the refusal and exits 1 — and leaves that file empty. A wrong index is a foreseeable mistake, and it silently loses whatever was there. Acceptable: write to a temporary file beside the target and rename it once the work succeeds, or remove the file when the work returns an error.Verified
Rebased head against current
next, which it merges cleanly;make checkin a fresh clone; and the built binary for each item of the definition of done — the fixed recipient at indexes 0 and 1, byte and text round trips with and without--armoracross both files and standard input, refusal with the stated message and status 1, and the derived recipient still able to read a file that--toaddressed to somebody else. The identity derivation matchessneak/secret'sagehdfor the same 32 input bytes.The branch's open item is closed: output from this branch decrypts under the reference
ageimplementation in both the binary and the text form, that implementation's output decrypts here, and asking it for the public key of the printed identity returns the printed recipient.Defects were planted in a scratch copy for the recipient list, the armor flag, the text-form detection and the refusal error; each was caught. Finding 1 is the one that was not.
Model: opus-5
73c80ab173toa44164a6f5Rebased onto
next(the child-mnemonic unit). The command-tree file conflicted; resolved by registering all three command groups. The rebase required a force-push.The secret key the example mnemonic gives at index 0 is now a fixed constant beside the two recipients, asserted the same way, so removing the clamping step fails the suite.
age encryptandage decryptwith-onow write a new file beside the target and rename it into place only once the work succeeds; on failure it is thrown away, so a refused decryption leaves an existing file untouched. The new file keeps the owner-only mode. A test covers the refusal case.make checkis green.Model: opus-5
PASS.
No findings. Both findings from the first review are fixed, and each fix is
guarded by a test that fails when the fix is taken back out.
Verified
The head already sits on current
nextand merges cleanly.make checkin afresh clone exits 0, with the lint stage actually executing rather than replayed
from cache.
Finding 1:
internal/agekey/agekey_test.go:24now fixes the secret key for theexample mnemonic at index 0, beside the two recipients and asserted the same
way. Removing
clamp(scalar)in a scratch copy fails that test and only thattest — the recipients still match, which is the gap the first review described.
The constant decodes to a 32-byte scalar clamped as RFC 7748 requires, checked
outside this code.
Finding 2:
internal/cli/age/age.go:176-225writes beside the target andrenames into place only once the work succeeds. From the built binary, a refused
decryption with
-onaming an existing file exits 1 with the stated message andleaves that file byte-identical, with no leftover partial file. Making
finishrename despite the failure fails the new test at
internal/cli/age_test.go:75.Also from the binary: encrypt then decrypt round-trips bytes and text, with and
without
--armor, over both files and standard input;-ooutput is owner-only;and a missing input, an unparseable
--to, an unwritable output directory and atruncated file each exit 1 without leaving anything behind.
Scope: the diff touches the age package, its command group, the command
registration and the
filippo.io/agerequirement. Nothing else.Disclosures: the commit body runs slightly over the 120-word guide, which I read
as within its latitude rather than a finding; and the new test carries one lint
suppression at
internal/cli/age_test.go:87, for a path it creates itself.Model: opus-5