From bb415833568ae3b71857ff2b8c4fa19711a6e2b0 Mon Sep 17 00:00:00 2001 From: clawbot Date: Wed, 14 Oct 2026 19:17:01 +0000 Subject: [PATCH] Read the text form with blank lines in front, as age does (closes #77) age decrypt took the text form only when the file began exactly with -----BEGIN AGE ENCRYPTED FILE-----, so a blank line in front made it read the file as the binary form and refuse it. It now looks for that line past up to 1024 bytes of whitespace, as the age tool does, and the text form reader skips the blank lines. A test puts a blank line and a line of two spaces in front of a text-form file. The README's age decrypt section says so. Judgement call: that first line with spaces in front of it on the same line is still refused, as age refuses it. Model: opus-5-5 --- README.md | 4 +++- internal/agekey/agekey.go | 13 ++++++++++--- internal/agekey/agekey_test.go | 22 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1b741f3..abcc2c7 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,9 @@ or a device, such as `/dev/null`, is written to directly. Decrypts the file (or standard input) with the derived identity. Output goes to `-o`, which is treated as for `encrypt`, or standard output. If the identity is -not one of the recipients, the tool says so and exits with status 1. +not one of the recipients, the tool says so and exits with status 1. The text +form needs no flag, and up to 1024 bytes of blank lines in front of it are +skipped, as `age` skips them. ## Derived mnemonics: `keyfunc mnemonic` diff --git a/internal/agekey/agekey.go b/internal/agekey/agekey.go index f863d38..461b714 100644 --- a/internal/agekey/agekey.go +++ b/internal/agekey/agekey.go @@ -4,6 +4,7 @@ package agekey import ( "bufio" + "bytes" "errors" "fmt" "io" @@ -187,12 +188,18 @@ func (k *Key) Decrypt(dst io.Writer, src io.Reader) error { } // unarmored strips the text form when the input begins with the line -// that starts one, and leaves binary input alone. +// that starts one, and leaves binary input alone. Like age, it looks +// past up to 1024 bytes of whitespace for that line, and the text form +// reader skips blank lines in front of it. func unarmored(src io.Reader) io.Reader { + const maxWhitespace = 1024 + buffered := bufio.NewReader(src) - start, err := buffered.Peek(len(armor.Header)) - if err == nil && string(start) == armor.Header { + // A shorter input gives an error along with all of it, which is + // enough to look at. + start, _ := buffered.Peek(maxWhitespace + len(armor.Header)) + if bytes.HasPrefix(bytes.TrimSpace(start), []byte(armor.Header)) { return armor.NewReader(buffered) } diff --git a/internal/agekey/agekey_test.go b/internal/agekey/agekey_test.go index 4702d54..06c62e8 100644 --- a/internal/agekey/agekey_test.go +++ b/internal/agekey/agekey_test.go @@ -2,6 +2,7 @@ package agekey_test import ( "bytes" + "io" "strings" "testing" @@ -99,6 +100,27 @@ func TestTheArmoredFormIsText(t *testing.T) { )) } +func TestTheTextFormIsReadWithWhitespaceInFront(t *testing.T) { + t.Parallel() + + key := forIndex(t, 0) + + var sealed bytes.Buffer + + err := key.Encrypt(&sealed, strings.NewReader("hello"), nil, true) + require.NoError(t, err) + + // A blank line and a line of two spaces, as pasting can leave. age + // refuses spaces on the first line of the text form itself, and so + // does this. + pasted := io.MultiReader(strings.NewReader("\n \n"), &sealed) + + var opened bytes.Buffer + + require.NoError(t, key.Decrypt(&opened, pasted)) + require.Equal(t, "hello", opened.String()) +} + func TestAFileForSomebodyElseIsRefused(t *testing.T) { t.Parallel()