blob: a1c3bd20d79d4ed58011bcd97dd4c2c17d3c04da [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cryptotest
import (
"bytes"
"crypto/cipher"
"testing"
)
type MakeBlock func(key []byte) (cipher.Block, error)
// TestBlock performs a set of tests on cipher.Block implementations, checking
// the documented requirements of BlockSize, Encrypt, and Decrypt.
func TestBlock(t *testing.T, keySize int, mb MakeBlock) {
// Generate random key
key := make([]byte, keySize)
newRandReader(t).Read(key)
t.Logf("Cipher key: 0x%x", key)
block, err := mb(key)
if err != nil {
t.Fatal(err)
}
blockSize := block.BlockSize()
t.Run("Encryption", func(t *testing.T) {
testCipher(t, block.Encrypt, blockSize)
})
t.Run("Decryption", func(t *testing.T) {
testCipher(t, block.Decrypt, blockSize)
})
// Checks baseline Encrypt/Decrypt functionality. More thorough
// implementation-specific characterization/golden tests should be done
// for each block cipher implementation.
t.Run("Roundtrip", func(t *testing.T) {
rng := newRandReader(t)
// Check Decrypt inverts Encrypt
before, ciphertext, after := make([]byte, blockSize), make([]byte, blockSize), make([]byte, blockSize)
rng.Read(before)
block.Encrypt(ciphertext, before)
block.Decrypt(after, ciphertext)
if !bytes.Equal(after, before) {
t.Errorf("plaintext is different after an encrypt/decrypt cycle; got %x, want %x", after, before)
}
// Check Encrypt inverts Decrypt (assumes block ciphers are deterministic)
before, plaintext, after := make([]byte, blockSize), make([]byte, blockSize), make([]byte, blockSize)
rng.Read(before)
block.Decrypt(plaintext, before)
block.Encrypt(after, plaintext)
if !bytes.Equal(after, before) {
t.Errorf("ciphertext is different after a decrypt/encrypt cycle; got %x, want %x", after, before)
}
})
}
func testCipher(t *testing.T, cipher func(dst, src []byte), blockSize int) {
t.Run("AlterInput", func(t *testing.T) {
rng := newRandReader(t)
// Make long src that shouldn't be modified at all, within block
// size scope or beyond it
src, before := make([]byte, blockSize*2), make([]byte, blockSize*2)
rng.Read(src)
copy(before, src)
dst := make([]byte, blockSize)
cipher(dst, src)
if !bytes.Equal(src, before) {
t.Errorf("block cipher modified src; got %x, want %x", src, before)
}
})
t.Run("Aliasing", func(t *testing.T) {
rng := newRandReader(t)
buff, expectedOutput := make([]byte, blockSize), make([]byte, blockSize)
// Record what output is when src and dst are different
rng.Read(buff)
cipher(expectedOutput, buff)
// Check that the same output is generated when src=dst alias to the same
// memory
cipher(buff, buff)
if !bytes.Equal(buff, expectedOutput) {
t.Errorf("block cipher produced different output when dst = src; got %x, want %x", buff, expectedOutput)
}
})
t.Run("OutOfBoundsWrite", func(t *testing.T) {
rng := newRandReader(t)
src := make([]byte, blockSize)
rng.Read(src)
// Make a buffer with dst in the middle and data on either end
buff := make([]byte, blockSize*3)
endOfPrefix, startOfSuffix := blockSize, blockSize*2
rng.Read(buff[:endOfPrefix])
rng.Read(buff[startOfSuffix:])
dst := buff[endOfPrefix:startOfSuffix]
// Record the prefix and suffix data to make sure they aren't written to
initPrefix, initSuffix := make([]byte, blockSize), make([]byte, blockSize)
copy(initPrefix, buff[:endOfPrefix])
copy(initSuffix, buff[startOfSuffix:])
// Write to dst (the middle of the buffer) and make sure it doesn't write
// beyond the dst slice
cipher(dst, src)
if !bytes.Equal(buff[startOfSuffix:], initSuffix) {
t.Errorf("block cipher did out of bounds write after end of dst slice; got %x, want %x", buff[startOfSuffix:], initSuffix)
}
if !bytes.Equal(buff[:endOfPrefix], initPrefix) {
t.Errorf("block cipher did out of bounds write before beginning of dst slice; got %x, want %x", buff[:endOfPrefix], initPrefix)
}
// Check that dst isn't written to beyond BlockSize even if there is room
// in the slice
dst = buff[endOfPrefix:] // Extend dst to include suffix
cipher(dst, src)
if !bytes.Equal(buff[startOfSuffix:], initSuffix) {
t.Errorf("block cipher modified dst past BlockSize bytes; got %x, want %x", buff[startOfSuffix:], initSuffix)
}
})
// Check that output of cipher isn't affected by adjacent data beyond input
// slice scope
// For encryption, this assumes block ciphers encrypt deterministically
t.Run("OutOfBoundsRead", func(t *testing.T) {
rng := newRandReader(t)
src := make([]byte, blockSize)
rng.Read(src)
expectedDst := make([]byte, blockSize)
cipher(expectedDst, src)
// Make a buffer with src in the middle and data on either end
buff := make([]byte, blockSize*3)
endOfPrefix, startOfSuffix := blockSize, blockSize*2
copy(buff[endOfPrefix:startOfSuffix], src)
rng.Read(buff[:endOfPrefix])
rng.Read(buff[startOfSuffix:])
testDst := make([]byte, blockSize)
cipher(testDst, buff[endOfPrefix:startOfSuffix])
if !bytes.Equal(testDst, expectedDst) {
t.Errorf("block cipher affected by data outside of src slice bounds; got %x, want %x", testDst, expectedDst)
}
// Check that src isn't read from beyond BlockSize even if the slice is
// longer and contains data in the suffix
cipher(testDst, buff[endOfPrefix:]) // Input long src
if !bytes.Equal(testDst, expectedDst) {
t.Errorf("block cipher affected by src data beyond BlockSize bytes; got %x, want %x", buff[startOfSuffix:], expectedDst)
}
})
t.Run("NonZeroDst", func(t *testing.T) {
rng := newRandReader(t)
// Record what the cipher writes into a destination of zeroes
src := make([]byte, blockSize)
rng.Read(src)
expectedDst := make([]byte, blockSize)
cipher(expectedDst, src)
// Make nonzero dst
dst := make([]byte, blockSize*2)
rng.Read(dst)
// Remember the random suffix which shouldn't be written to
expectedDst = append(expectedDst, dst[blockSize:]...)
cipher(dst, src)
if !bytes.Equal(dst, expectedDst) {
t.Errorf("block cipher behavior differs when given non-zero dst; got %x, want %x", dst, expectedDst)
}
})
t.Run("BufferOverlap", func(t *testing.T) {
rng := newRandReader(t)
buff := make([]byte, blockSize*2)
rng.Read((buff))
// Make src and dst slices point to same array with inexact overlap
src := buff[:blockSize]
dst := buff[1 : blockSize+1]
mustPanic(t, "invalid buffer overlap", func() { cipher(dst, src) })
// Only overlap on one byte
src = buff[:blockSize]
dst = buff[blockSize-1 : 2*blockSize-1]
mustPanic(t, "invalid buffer overlap", func() { cipher(dst, src) })
// src comes after dst with one byte overlap
src = buff[blockSize-1 : 2*blockSize-1]
dst = buff[:blockSize]
mustPanic(t, "invalid buffer overlap", func() { cipher(dst, src) })
})
// Test short input/output.
// Assembly used to not notice.
// See issue 7928.
t.Run("ShortBlock", func(t *testing.T) {
// Returns slice of n bytes of an n+1 length array. Lets us test that a
// slice is still considered too short even if the underlying array it
// points to is large enough
byteSlice := func(n int) []byte { return make([]byte, n+1)[0:n] }
// Off by one byte
mustPanic(t, "input not full block", func() { cipher(byteSlice(blockSize), byteSlice(blockSize-1)) })
mustPanic(t, "output not full block", func() { cipher(byteSlice(blockSize-1), byteSlice(blockSize)) })
// Small slices
mustPanic(t, "input not full block", func() { cipher(byteSlice(1), byteSlice(1)) })
mustPanic(t, "input not full block", func() { cipher(byteSlice(100), byteSlice(1)) })
mustPanic(t, "output not full block", func() { cipher(byteSlice(1), byteSlice(100)) })
})
}
func mustPanic(t *testing.T, msg string, f func()) {
t.Helper()
defer func() {
t.Helper()
err := recover()
if err == nil {
t.Errorf("function did not panic for %q", msg)
}
}()
f()
}