mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 05:25:14 +00:00
internal/format: require the last line of stanzas to be short
We are going to reuse the stanza format for IPC in the plugin protocol, but in that context we need stanzas to be self-closing. Currently they almost are, but if the body is 0 modulo 48, there is no way to know if the stanza is over after the last line. Now, all stanzas have to end with a short line, even if empty. No ciphertexts generated by age in the past are affected, but 3% of the ciphertexts generated by rage will now stop working. They are still supported by rage going forward. If it turns out to be a common issue, we can add an exception.
This commit is contained in:
11
cmd/age/testdata/empty_recipient_body.age
vendored
11
cmd/age/testdata/empty_recipient_body.age
vendored
@@ -1,6 +1,7 @@
|
||||
age-encryption.org/v1
|
||||
-> ssh-ed25519 o1Hudg SZISkI5Qn8YgUBmTKG/Zp/QpFjXWvAivzvB+hOcN5W8
|
||||
dYfwGWYvCwpSU5EXIC1XqfXdsBvCi3kMypdqCVShrpk
|
||||
-> joint-oil-hw
|
||||
--- gC/27VAgqOEzAQMKHvBjih7sJ1oDKht+HNdguTIbjt8
|
||||
f<EFBFBD>tAe<EFBFBD>֨&8{<7B><><EFBFBD>νcat<61><1B><><16><><EFBFBD><13>˷}<17>=<3D>C<EFBFBD><43>u
|
||||
-> X25519 alRneDshIh43nwyD5+fhuTD5TReSn88f2us4hzZPyzU
|
||||
pGduNK5MUhnuzMxW0qbZnC2k7mRzz69bbJpKQrRc7uc
|
||||
-> A7)h-grease !,_
|
||||
|
||||
--- 5bA0uXjBxI6wuI5SseCRgD5/G8LkSVISRe/hnrQMb9s
|
||||
<EFBFBD><EFBFBD><06>1<EFBFBD><31><EFBFBD><EFBFBD><03>6_R<5F><08>څ<EFBFBD><DA85>U<<3C>1<EFBFBD>s<EFBFBD><73>?`<60>+<><7F>$<24>H<EFBFBD>W<EFBFBD>v?w8ZW
|
||||
@@ -1,7 +1 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACAwKgrb/LkvtI887QylSoUh5xUlKr1fb37euR6et5jHowAAAJgxqUx+MalM
|
||||
fgAAAAtzc2gtZWQyNTUxOQAAACAwKgrb/LkvtI887QylSoUh5xUlKr1fb37euR6et5jHow
|
||||
AAAEC7gKj74YIwaM1BT2tnODjfeZJvo8lcazvL6Uljv3+nIDAqCtv8uS+0jzztDKVKhSHn
|
||||
FSUqvV9vft65Hp63mMejAAAADnJ1bm5lckBmdi1hejMyAQIDBAUGBw==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
AGE-SECRET-KEY-1TRYTV7PQS5XPUYSTAQZCD7DQCWC7Q77YJD7UVFJRMW4J82Q6930QS70MRX
|
||||
|
||||
@@ -47,7 +47,8 @@ const BytesPerLine = ColumnsPerLine / 4 * 3
|
||||
|
||||
// NewlineWriter returns a Writer that writes to dst, inserting an LF character
|
||||
// every ColumnsPerLine bytes. It does not insert a newline neither at the
|
||||
// beginning nor at the end of the stream.
|
||||
// beginning nor at the end of the stream, but it ensures the last line is
|
||||
// shorter than ColumnsPerLine, which means it might be empty.
|
||||
func NewlineWriter(dst io.Writer) io.Writer {
|
||||
return &newlineWriter{dst: dst}
|
||||
}
|
||||
@@ -63,17 +64,16 @@ func (w *newlineWriter) Write(p []byte) (int, error) {
|
||||
panic("age: internal error: non-empty newlineWriter.buf")
|
||||
}
|
||||
for len(p) > 0 {
|
||||
remainingInLine := ColumnsPerLine - (w.written % ColumnsPerLine)
|
||||
if remainingInLine == ColumnsPerLine && w.written != 0 {
|
||||
w.buf.Write([]byte("\n"))
|
||||
}
|
||||
toWrite := remainingInLine
|
||||
toWrite := ColumnsPerLine - (w.written % ColumnsPerLine)
|
||||
if toWrite > len(p) {
|
||||
toWrite = len(p)
|
||||
}
|
||||
n, _ := w.buf.Write(p[:toWrite])
|
||||
w.written += n
|
||||
p = p[n:]
|
||||
if w.written%ColumnsPerLine == 0 {
|
||||
w.buf.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
if _, err := w.buf.WriteTo(w.dst); err != nil {
|
||||
// We always return n = 0 on error because it's hard to work back to the
|
||||
@@ -101,9 +101,6 @@ func (r *Stanza) Marshal(w io.Writer) error {
|
||||
if _, err := io.WriteString(w, "\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(r.Body) == 0 {
|
||||
return nil
|
||||
}
|
||||
ww := base64.NewEncoder(b64, NewlineWriter(w))
|
||||
if _, err := ww.Write(r.Body); err != nil {
|
||||
return err
|
||||
@@ -169,6 +166,9 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(line, footerPrefix) {
|
||||
if r != nil {
|
||||
return nil, nil, errorf("malformed body line %q: reached footer without previous stanza being closed\nNote: this might be a file encrypted with an old beta version of rage. Use rage to decrypt it.", line)
|
||||
}
|
||||
prefix, args := splitArgs(line)
|
||||
if prefix != string(footerPrefix) || len(args) != 1 {
|
||||
return nil, nil, errorf("malformed closing line: %q", line)
|
||||
@@ -180,6 +180,9 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
break
|
||||
|
||||
} else if bytes.HasPrefix(line, recipientPrefix) {
|
||||
if r != nil {
|
||||
return nil, nil, errorf("malformed body line %q: new stanza started without previous stanza being closed\nNote: this might be a file encrypted with an old beta version of rage. Use rage to decrypt it.", line)
|
||||
}
|
||||
r = &Stanza{}
|
||||
prefix, args := splitArgs(line)
|
||||
if prefix != string(recipientPrefix) || len(args) < 1 {
|
||||
@@ -202,9 +205,6 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
|
||||
if len(b) > BytesPerLine {
|
||||
return nil, nil, errorf("malformed body line %q: too long", line)
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil, nil, errorf("malformed body line %q: line is empty", line)
|
||||
}
|
||||
r.Body = append(r.Body, b...)
|
||||
if len(b) < BytesPerLine {
|
||||
// Only the last line of a body can be short.
|
||||
|
||||
41
internal/format/format_test.go
Normal file
41
internal/format/format_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2021 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
package format_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age/internal/format"
|
||||
)
|
||||
|
||||
func TestStanzaMarshal(t *testing.T) {
|
||||
s := &format.Stanza{
|
||||
Type: "test",
|
||||
Args: []string{"1", "2", "3"},
|
||||
Body: nil, // empty
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
s.Marshal(buf)
|
||||
if exp := "-> test 1 2 3\n\n"; buf.String() != exp {
|
||||
t.Errorf("wrong empty stanza encoding: expected %q, got %q", exp, buf.String())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
s.Body = []byte("AAA")
|
||||
s.Marshal(buf)
|
||||
if exp := "-> test 1 2 3\nQUFB\n"; buf.String() != exp {
|
||||
t.Errorf("wrong normal stanza encoding: expected %q, got %q", exp, buf.String())
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
s.Body = bytes.Repeat([]byte("A"), format.BytesPerLine)
|
||||
s.Marshal(buf)
|
||||
if exp := "-> test 1 2 3\nQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\n\n"; buf.String() != exp {
|
||||
t.Errorf("wrong 64 columns stanza encoding: expected %q, got %q", exp, buf.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user