| // 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() |
| } |