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