The age commands: pub, priv, encrypt and decrypt #7
Reference in New Issue
Block a user
Delete Branch "issue-3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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