internal/stream: disallow empty final chunks

A non-empty payload of length a multiple of the chunk size can be
encrypted in two ways: with the last chunk full, or with an extra empty
last chunk. This is mostly an oversight in the original spec.

Both age and rage generate full last chunks, so we should be still in
time to pick one of the two, and avoid the underspecification. It's not
the one I would have picked originally, maybe, because disallowing full
last chunks would have avoided the trial decryption, but oh well.
This commit is contained in:
Filippo Valsorda
2022-04-26 20:40:27 +02:00
parent 765400f0c1
commit 30d8e65e03
6 changed files with 20 additions and 3 deletions

View File

@@ -3,3 +3,4 @@
# who owns a contribution's copyright.
Google LLC
Filippo Valsorda

View File

@@ -65,7 +65,10 @@ func TestVectors(t *testing.T) {
r, err := age.Decrypt(in, identities...)
if expectFailure {
if err == nil {
t.Fatal("expected Decrypt failure")
_, err = io.ReadAll(r)
}
if err == nil {
t.Fatal("expected Decrypt or Read failure")
}
if e := new(age.NoIdentityMatchError); errors.As(err, &e) {
t.Errorf("got ErrIncorrectIdentity, expected more specific error")
@@ -87,7 +90,7 @@ func TestVectors(t *testing.T) {
}
t.Logf("%s", out)
} else {
t.Fatal("invalid test vector")
t.Fatal("invalid test vector: missing prefix")
}
})
}

BIN
cmd/age/testdata/fail_last_empty.age vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
age-encryption.org/v1
-> X25519 JRosIz2avWchP2qSL6wF6U7uzD6kDuJXDbZvN1MOGmo
KpIQxpkbBDHqp+JsHLiTy2d5RYRwp2qzvUrAe0aDOnk
--- orVjbqbzm8U3S9njAs53o4PFi1wK39fIQQ4gRj3i7IU
„Ïgñ¾<> Ô0‡µÆN¢'jûöao<61>¹&æT

BIN
cmd/age/testdata/good_last_full.age vendored Normal file

Binary file not shown.

View File

@@ -87,7 +87,11 @@ func (r *Reader) readChunk() (last bool, err error) {
// A message can't end without a marked chunk. This message is truncated.
return false, io.ErrUnexpectedEOF
case err == io.ErrUnexpectedEOF:
// The last chunk can be short.
// The last chunk can be short, but not empty unless it's the first and
// only chunk.
if !nonceIsZero(&r.nonce) && n == r.a.Overhead() {
return false, errors.New("last chunk is empty, try age v1.0.0, and please consider reporting this")
}
in = in[:n]
last = true
setLastChunkFlag(&r.nonce)
@@ -128,6 +132,10 @@ func setLastChunkFlag(nonce *[chacha20poly1305.NonceSize]byte) {
nonce[len(nonce)-1] = lastChunkFlag
}
func nonceIsZero(nonce *[chacha20poly1305.NonceSize]byte) bool {
return *nonce == [chacha20poly1305.NonceSize]byte{}
}
type Writer struct {
a cipher.AEAD
dst io.Writer