| // 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" | |
| "crypto/subtle" | |
| "fmt" | |
| "strings" | |
| "testing" | |
| ) | |
| // Each test is executed with each of the buffer lengths in bufLens. | |
| var ( | |
| bufLens = []int{0, 1, 3, 4, 8, 10, 15, 16, 20, 32, 50, 4096, 5000} | |
| bufCap = 10000 | |
| ) | |
| // MakeStream returns a cipher.Stream instance. | |
| // | |
| // Multiple calls to MakeStream must return equivalent instances, | |
| // so for example the key and/or IV must be fixed. | |
| type MakeStream func() cipher.Stream | |
| // TestStream performs a set of tests on cipher.Stream implementations, | |
| // checking the documented requirements of XORKeyStream. | |
| func TestStream(t *testing.T, ms MakeStream) { | |
| t.Run("XORSemantics", func(t *testing.T) { | |
| if strings.Contains(t.Name(), "TestCFBStream") { | |
| // This is ugly, but so is CFB's abuse of cipher.Stream. | |
| // Don't want to make it easier for anyone else to do that. | |
| t.Skip("CFB implements cipher.Stream but does not follow XOR semantics") | |
| } | |
| // Test that XORKeyStream inverts itself for encryption/decryption. | |
| t.Run("Roundtrip", func(t *testing.T) { | |
| for _, length := range bufLens { | |
| t.Run(fmt.Sprintf("BuffLength=%d", length), func(t *testing.T) { | |
| rng := newRandReader(t) | |
| plaintext := make([]byte, length) | |
| rng.Read(plaintext) | |
| ciphertext := make([]byte, length) | |
| decrypted := make([]byte, length) | |
| ms().XORKeyStream(ciphertext, plaintext) // Encrypt plaintext | |
| ms().XORKeyStream(decrypted, ciphertext) // Decrypt ciphertext | |
| if !bytes.Equal(decrypted, plaintext) { | |
| t.Errorf("plaintext is different after an encrypt/decrypt cycle; got %s, want %s", truncateHex(decrypted), truncateHex(plaintext)) | |
| } | |
| }) | |
| } | |
| }) | |
| // Test that XORKeyStream behaves the same as directly XORing | |
| // plaintext with the stream. | |
| t.Run("DirectXOR", func(t *testing.T) { | |
| for _, length := range bufLens { | |
| t.Run(fmt.Sprintf("BuffLength=%d", length), func(t *testing.T) { | |
| rng := newRandReader(t) | |
| plaintext := make([]byte, length) | |
| rng.Read(plaintext) | |
| // Encrypting all zeros should reveal the stream itself | |
| stream, directXOR := make([]byte, length), make([]byte, length) | |
| ms().XORKeyStream(stream, stream) | |
| // Encrypt plaintext by directly XORing the stream | |
| subtle.XORBytes(directXOR, stream, plaintext) | |
| // Encrypt plaintext with XORKeyStream | |
| ciphertext := make([]byte, length) | |
| ms().XORKeyStream(ciphertext, plaintext) | |
| if !bytes.Equal(ciphertext, directXOR) { | |
| t.Errorf("xor semantics were not preserved; got %s, want %s", truncateHex(ciphertext), truncateHex(directXOR)) | |
| } | |
| }) | |
| } | |
| }) | |
| }) | |
| t.Run("EmptyInput", func(t *testing.T) { | |
| rng := newRandReader(t) | |
| src, dst := make([]byte, 100), make([]byte, 100) | |
| rng.Read(dst) | |
| before := bytes.Clone(dst) | |
| ms().XORKeyStream(dst, src[:0]) | |
| if !bytes.Equal(dst, before) { | |
| t.Errorf("XORKeyStream modified dst on empty input; got %s, want %s", truncateHex(dst), truncateHex(before)) | |
| } | |
| }) | |
| t.Run("AlterInput", func(t *testing.T) { | |
| rng := newRandReader(t) | |
| src, dst, before := make([]byte, bufCap), make([]byte, bufCap), make([]byte, bufCap) | |
| rng.Read(src) | |
| for _, length := range bufLens { | |
| t.Run(fmt.Sprintf("BuffLength=%d", length), func(t *testing.T) { | |
| copy(before, src) | |
| ms().XORKeyStream(dst[:length], src[:length]) | |
| if !bytes.Equal(src, before) { | |
| t.Errorf("XORKeyStream modified src; got %s, want %s", truncateHex(src), truncateHex(before)) | |
| } | |
| }) | |
| } | |
| }) | |
| t.Run("Aliasing", func(t *testing.T) { | |
| rng := newRandReader(t) | |
| buff, expectedOutput := make([]byte, bufCap), make([]byte, bufCap) | |
| for _, length := range bufLens { | |
| // Record what output is when src and dst are different | |
| rng.Read(buff) | |
| ms().XORKeyStream(expectedOutput[:length], buff[:length]) | |
| // Check that the same output is generated when src=dst alias to the same | |
| // memory | |
| ms().XORKeyStream(buff[:length], buff[:length]) | |
| if !bytes.Equal(buff[:length], expectedOutput[:length]) { | |
| t.Errorf("block cipher produced different output when dst = src; got %x, want %x", buff[:length], expectedOutput[:length]) | |
| } | |
| } | |
| }) | |
| t.Run("OutOfBoundsWrite", func(t *testing.T) { // Issue 21104 | |
| rng := newRandReader(t) | |
| plaintext := make([]byte, bufCap) | |
| rng.Read(plaintext) | |
| ciphertext := make([]byte, bufCap) | |
| for _, length := range bufLens { | |
| copy(ciphertext, plaintext) // Reset ciphertext buffer | |
| t.Run(fmt.Sprintf("BuffLength=%d", length), func(t *testing.T) { | |
| mustPanic(t, "output smaller than input", func() { ms().XORKeyStream(ciphertext[:length], plaintext) }) | |
| if !bytes.Equal(ciphertext[length:], plaintext[length:]) { | |
| t.Errorf("XORKeyStream did out of bounds write; got %s, want %s", truncateHex(ciphertext[length:]), truncateHex(plaintext[length:])) | |
| } | |
| }) | |
| } | |
| }) | |
| t.Run("BufferOverlap", func(t *testing.T) { | |
| rng := newRandReader(t) | |
| buff := make([]byte, bufCap) | |
| rng.Read(buff) | |
| for _, length := range bufLens { | |
| if length == 0 || length == 1 { | |
| continue | |
| } | |
| t.Run(fmt.Sprintf("BuffLength=%d", length), func(t *testing.T) { | |
| // Make src and dst slices point to same array with inexact overlap | |
| src := buff[:length] | |
| dst := buff[1 : length+1] | |
| mustPanic(t, "invalid buffer overlap", func() { ms().XORKeyStream(dst, src) }) | |
| // Only overlap on one byte | |
| src = buff[:length] | |
| dst = buff[length-1 : 2*length-1] | |
| mustPanic(t, "invalid buffer overlap", func() { ms().XORKeyStream(dst, src) }) | |
| // src comes after dst with one byte overlap | |
| src = buff[length-1 : 2*length-1] | |
| dst = buff[:length] | |
| mustPanic(t, "invalid buffer overlap", func() { ms().XORKeyStream(dst, src) }) | |
| }) | |
| } | |
| }) | |
| t.Run("KeepState", func(t *testing.T) { | |
| rng := newRandReader(t) | |
| plaintext := make([]byte, bufCap) | |
| rng.Read(plaintext) | |
| ciphertext := make([]byte, bufCap) | |
| // Make one long call to XORKeyStream | |
| ms().XORKeyStream(ciphertext, plaintext) | |
| for _, step := range bufLens { | |
| if step == 0 { | |
| continue | |
| } | |
| stepMsg := fmt.Sprintf("step %d: ", step) | |
| dst := make([]byte, bufCap) | |
| // Make a bunch of small calls to (stateful) XORKeyStream | |
| stream := ms() | |
| i := 0 | |
| for i+step < len(plaintext) { | |
| stream.XORKeyStream(dst[i:], plaintext[i:i+step]) | |
| i += step | |
| } | |
| stream.XORKeyStream(dst[i:], plaintext[i:]) | |
| if !bytes.Equal(dst, ciphertext) { | |
| t.Errorf(stepMsg+"successive XORKeyStream calls returned a different result than a single one; got %s, want %s", truncateHex(dst), truncateHex(ciphertext)) | |
| } | |
| } | |
| }) | |
| } | |
| // TestStreamFromBlock creates a Stream from a cipher.Block used in a | |
| // cipher.BlockMode. It addresses Issue 68377 by checking for a panic when the | |
| // BlockMode uses an IV with incorrect length. | |
| // For a valid IV, it also runs all TestStream tests on the resulting stream. | |
| func TestStreamFromBlock(t *testing.T, block cipher.Block, blockMode func(b cipher.Block, iv []byte) cipher.Stream) { | |
| t.Run("WrongIVLen", func(t *testing.T) { | |
| t.Skip("see Issue 68377") | |
| rng := newRandReader(t) | |
| iv := make([]byte, block.BlockSize()+1) | |
| rng.Read(iv) | |
| mustPanic(t, "IV length must equal block size", func() { blockMode(block, iv) }) | |
| }) | |
| t.Run("BlockModeStream", func(t *testing.T) { | |
| rng := newRandReader(t) | |
| iv := make([]byte, block.BlockSize()) | |
| rng.Read(iv) | |
| TestStream(t, func() cipher.Stream { return blockMode(block, iv) }) | |
| }) | |
| } | |
| func truncateHex(b []byte) string { | |
| numVals := 50 | |
| if len(b) <= numVals { | |
| return fmt.Sprintf("%x", b) | |
| } | |
| return fmt.Sprintf("%x...", b[:numVals]) | |
| } |