internal/stream: reject trailing data (no EOF) after end of stream

This commit is contained in:
Filippo Valsorda 2022-06-19 23:11:14 +02:00
parent 2e090545df
commit 3f56ac13fb
45 changed files with 458 additions and 29 deletions

View file

@ -8,6 +8,7 @@ package stream
import (
"crypto/cipher"
"errors"
"fmt"
"io"
"golang.org/x/crypto/chacha20poly1305"
@ -66,7 +67,17 @@ func (r *Reader) Read(p []byte) (int, error) {
r.unread = r.unread[n:]
if last {
r.err = io.EOF
// Ensure there is an EOF after the last chunk as expected. In other
// words, check for trailing data after a full-length final chunk.
// Hopefully, the underlying reader supports returning EOF even if it
// had previously returned an EOF to ReadFull.
if _, err := r.src.Read(make([]byte, 1)); err == nil {
r.err = errors.New("trailing data after end of encrypted file")
} else if err != io.EOF {
r.err = fmt.Errorf("non-EOF error reading after end of encrypted file: %w", err)
} else {
r.err = io.EOF
}
}
return n, nil

View file

@ -9,6 +9,7 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"os"
@ -30,6 +31,16 @@ var _, TestX25519Identity, _ = bech32.Decode(
var TestX25519Recipient, _ = curve25519.X25519(TestX25519Identity, curve25519.Basepoint)
// These are the file key and nonce used to encrypt any full/multiple-chunk
// tests. They were generated by a previous iteration of this test suite.
// Reusing them across files and history makes the repository easier to pack and
// the test suite easier to compress.
var LargeTestFileKey, _ = hex.DecodeString("7aa5bdac0e6afeed3dd0a7eccb42af44")
var LargeTestNonce, _ = hex.DecodeString("c82f71eb82029b77136399e485e879f4")
var LargeTestFirstChunk = bytes.Repeat([]byte{0}, 64*1024)
var LargeTestSecondChunk = bytes.Repeat([]byte{1}, 64*1024)
var LargeTestThirdChunk = bytes.Repeat([]byte{2}, 64*1024)
func NotCanonicalBase64(s string) string {
// Assuming there are spare zero bits at the end of the encoded bitstring,
// the character immediately after in the alphabet compared to the last one
@ -222,6 +233,14 @@ func (f *TestFile) ExpectHeaderFailure() {
func (f *TestFile) ExpectPayloadFailure() {
f.expect = "payload failure"
f.payload.Reset()
}
func (f *TestFile) ExpectPartialPayload(goodBytes int) {
f.expect = "payload failure"
payload := f.payload.Bytes()
f.payload.Reset()
f.payload.Write(payload[:goodBytes])
}
func (f *TestFile) ExpectHMACFailure() {
@ -238,7 +257,7 @@ func (f *TestFile) Comment(c string) {
func (f *TestFile) Generate() {
fmt.Printf("expect: %s\n", f.expect)
if f.expect == "success" {
if f.expect == "success" || f.expect == "payload failure" {
fmt.Printf("payload: %x\n", sha256.Sum256(f.payload.Bytes()))
}
fmt.Printf("file key: %x\n", f.fileKey)

BIN
testdata/testkit/stream_bad_tag vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
testdata/testkit/stream_missing_tag vendored Normal file

Binary file not shown.

BIN
testdata/testkit/stream_no_chunks vendored Normal file

Binary file not shown.

BIN
testdata/testkit/stream_no_final vendored Normal file

Binary file not shown.

BIN
testdata/testkit/stream_no_final_full vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
testdata/testkit/stream_no_nonce vendored Normal file

Binary file not shown.

BIN
testdata/testkit/stream_short_chunk vendored Normal file

Binary file not shown.

BIN
testdata/testkit/stream_short_nonce vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
testdata/testkit/stream_three_chunks vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
testdata/testkit/stream_two_chunks vendored Normal file

Binary file not shown.

BIN
testdata/testkit/stream_two_final_chunks vendored Normal file

Binary file not shown.

View file

@ -155,6 +155,9 @@ func testVector(t *testing.T, test []byte) {
if err != nil {
if expect == "payload failure" {
t.Log(err)
if payloadHash != nil && sha256.Sum256(out) != *payloadHash {
t.Error("partial payload hash mismatch")
}
return
}
t.Fatalf("expected %s, got: %v", expect, err)

23
tests/stream_bad_tag.go Normal file
View file

@ -0,0 +1,23 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.X25519(testkit.TestX25519Recipient)
f.HMAC()
f.Payload("age")
file := f.Buf.Bytes()
f.Buf.Reset()
file[len(file)-1] ^= 0b0010_0000
f.Buf.Write(file)
f.ExpectPayloadFailure()
f.Generate()
}

View file

@ -0,0 +1,26 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.PayloadChunkFinal([]byte("age"))
file := f.Buf.Bytes()
f.Buf.Reset()
file[len(file)-1] ^= 0b0010_0000
f.Buf.Write(file)
f.ExpectPartialPayload(64 * 1024)
f.Generate()
}

View file

@ -0,0 +1,26 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.PayloadChunkFinal(testkit.LargeTestSecondChunk)
file := f.Buf.Bytes()
f.Buf.Reset()
file[len(file)-1] ^= 0b0010_0000
f.Buf.Write(file)
f.ExpectPartialPayload(64 * 1024)
f.Generate()
}

View file

@ -6,27 +6,18 @@
package main
import (
"bytes"
"encoding/hex"
"filippo.io/age/internal/testkit"
)
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
// Reuse the file key and nonce from a previous test vector to avoid
// bloating the git history with two versions that can't be compressed.
fileKey, _ := hex.DecodeString("7aa5bdac0e6afeed3dd0a7eccb42af44")
f.FileKey(fileKey)
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
nonce, _ := hex.DecodeString("c82f71eb82029b77136399e485e879f4")
f.Nonce(nonce)
f.PayloadChunk(bytes.Repeat([]byte{0}, 64*1024))
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.PayloadChunkFinal([]byte{})
f.Comment("final STREAM chunk can't be empty unless whole payload is empty")
f.ExpectPayloadFailure()
f.ExpectPartialPayload(64 * 1024)
f.Generate()
}

View file

@ -6,24 +6,15 @@
package main
import (
"bytes"
"encoding/hex"
"filippo.io/age/internal/testkit"
)
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
// Reuse the file key and nonce from a previous test vector to avoid
// bloating the git history with two versions that can't be compressed.
fileKey, _ := hex.DecodeString("5085919e0d59b19d6cbd00330f03861c")
f.FileKey(fileKey)
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
nonce, _ := hex.DecodeString("32521791a6f22e11637fb69ead3f2d5f")
f.Nonce(nonce)
f.PayloadChunkFinal(bytes.Repeat([]byte{0}, 64*1024))
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunkFinal(testkit.LargeTestFirstChunk)
f.Generate()
}

View file

@ -0,0 +1,21 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.PayloadChunkFinal(testkit.LargeTestSecondChunk)
f.Generate()
}

View file

@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.X25519(testkit.TestX25519Recipient)
f.HMAC()
f.Payload("age")
file := f.Buf.Bytes()
f.Buf.Reset()
f.Buf.Write(file[:len(file)-16])
f.ExpectPayloadFailure()
f.Generate()
}

19
tests/stream_no_chunks.go Normal file
View file

@ -0,0 +1,19 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.X25519(testkit.TestX25519Recipient)
f.HMAC()
f.Nonce(f.Rand(16))
f.ExpectPayloadFailure()
f.Generate()
}

20
tests/stream_no_final.go Normal file
View file

@ -0,0 +1,20 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.X25519(testkit.TestX25519Recipient)
f.HMAC()
f.Nonce(f.Rand(16))
f.PayloadChunk([]byte("age"))
f.ExpectPayloadFailure()
f.Generate()
}

View file

@ -0,0 +1,21 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.ExpectPartialPayload(64 * 1024)
f.Generate()
}

View file

@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.PayloadChunk([]byte("age"))
f.ExpectPartialPayload(64 * 1024)
f.Generate()
}

View file

@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.PayloadChunk(testkit.LargeTestSecondChunk)
f.ExpectPartialPayload(64 * 1024 * 2)
f.Generate()
}

20
tests/stream_no_nonce.go Normal file
View file

@ -0,0 +1,20 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.X25519(testkit.TestX25519Recipient)
f.HMAC()
// Marked as header failure because we read the nonce while reading the
// header, before handing off to the STREAM implementation.
f.ExpectHeaderFailure()
f.Generate()
}

View file

@ -0,0 +1,20 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.X25519(testkit.TestX25519Recipient)
f.HMAC()
f.Nonce(f.Rand(16))
f.Nonce(f.Rand(12)) // less than the length of a Poly1305 tag
f.ExpectPayloadFailure()
f.Generate()
}

View file

@ -0,0 +1,21 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.VersionLine("v1")
f.X25519(testkit.TestX25519Recipient)
f.HMAC()
f.Nonce(f.Rand(12))
// Marked as header failure because we read the nonce while reading the
// header, before handing off to the STREAM implementation.
f.ExpectHeaderFailure()
f.Generate()
}

View file

@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.Nonce(f.Rand(12)) // less than the length of a Poly1305 tag
f.ExpectPartialPayload(64 * 1024)
f.Generate()
}

View file

@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.PayloadChunk(testkit.LargeTestSecondChunk)
f.PayloadChunkFinal(testkit.LargeTestThirdChunk)
f.Generate()
}

View file

@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunkFinal(testkit.LargeTestFirstChunk)
f.Buf.Write(f.Rand(20))
f.ExpectPartialPayload(64 * 1024)
f.Generate()
}

View file

@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunkFinal(testkit.LargeTestFirstChunk)
f.Buf.Write(f.Rand(12))
f.ExpectPartialPayload(64 * 1024)
f.Generate()
}

View file

@ -0,0 +1,21 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunk(testkit.LargeTestFirstChunk)
f.PayloadChunkFinal(testkit.LargeTestSecondChunk)
f.Generate()
}

View file

@ -0,0 +1,22 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "filippo.io/age/internal/testkit"
func main() {
f := testkit.NewTestFile()
f.FileKey(testkit.LargeTestFileKey)
f.VersionLine("v1")
f.X25519(testkit.TestX25519Identity)
f.HMAC()
f.Nonce(testkit.LargeTestNonce)
f.PayloadChunkFinal(testkit.LargeTestFirstChunk)
f.PayloadChunkFinal([]byte("age"))
f.ExpectPartialPayload(64 * 1024)
f.Generate()
}