|
6 | 6 | #include <test/fuzz/FuzzedDataProvider.h>
|
7 | 7 | #include <test/fuzz/fuzz.h>
|
8 | 8 | #include <test/fuzz/util.h>
|
| 9 | +#include <test/util/xoroshiro128plusplus.h> |
9 | 10 |
|
10 | 11 | #include <cstdint>
|
11 | 12 | #include <vector>
|
@@ -43,3 +44,110 @@ FUZZ_TARGET(crypto_chacha20)
|
43 | 44 | });
|
44 | 45 | }
|
45 | 46 | }
|
| 47 | + |
| 48 | +namespace |
| 49 | +{ |
| 50 | + |
| 51 | +/** Fuzzer that invokes ChaCha20::Crypt() or ChaCha20::Keystream multiple times: |
| 52 | + once for a large block at once, and then the same data in chunks, comparing |
| 53 | + the outcome. |
| 54 | +
|
| 55 | + If UseCrypt, seeded Xoroshiro128++ output is used as input to Crypt(). |
| 56 | + If not, Keystream() is used directly, or sequences of 0x00 are encrypted. |
| 57 | +*/ |
| 58 | +template<bool UseCrypt> |
| 59 | +void ChaCha20SplitFuzz(FuzzedDataProvider& provider) |
| 60 | +{ |
| 61 | + // Determine key, iv, start position, length. |
| 62 | + unsigned char key[32] = {0}; |
| 63 | + auto key_bytes = provider.ConsumeBytes<unsigned char>(32); |
| 64 | + std::copy(key_bytes.begin(), key_bytes.end(), key); |
| 65 | + uint64_t iv = provider.ConsumeIntegral<uint64_t>(); |
| 66 | + uint64_t total_bytes = provider.ConsumeIntegralInRange<uint64_t>(0, 1000000); |
| 67 | + /* ~x = 2^64 - 1 - x, so ~(total_bytes >> 6) is the maximal seek position. */ |
| 68 | + uint64_t seek = provider.ConsumeIntegralInRange<uint64_t>(0, ~(total_bytes >> 6)); |
| 69 | + |
| 70 | + // Initialize two ChaCha20 ciphers, with the same key/iv/position. |
| 71 | + ChaCha20 crypt1(key, 32); |
| 72 | + ChaCha20 crypt2(key, 32); |
| 73 | + crypt1.SetIV(iv); |
| 74 | + crypt1.Seek64(seek); |
| 75 | + crypt2.SetIV(iv); |
| 76 | + crypt2.Seek64(seek); |
| 77 | + |
| 78 | + // Construct vectors with data. |
| 79 | + std::vector<unsigned char> data1, data2; |
| 80 | + data1.resize(total_bytes); |
| 81 | + data2.resize(total_bytes); |
| 82 | + |
| 83 | + // If using Crypt(), initialize data1 and data2 with the same Xoroshiro128++ based |
| 84 | + // stream. |
| 85 | + if constexpr (UseCrypt) { |
| 86 | + uint64_t seed = provider.ConsumeIntegral<uint64_t>(); |
| 87 | + XoRoShiRo128PlusPlus rng(seed); |
| 88 | + uint64_t bytes = 0; |
| 89 | + while (bytes < (total_bytes & ~uint64_t{7})) { |
| 90 | + uint64_t val = rng(); |
| 91 | + WriteLE64(data1.data() + bytes, val); |
| 92 | + WriteLE64(data2.data() + bytes, val); |
| 93 | + bytes += 8; |
| 94 | + } |
| 95 | + if (bytes < total_bytes) { |
| 96 | + unsigned char valbytes[8]; |
| 97 | + uint64_t val = rng(); |
| 98 | + WriteLE64(valbytes, val); |
| 99 | + std::copy(valbytes, valbytes + (total_bytes - bytes), data1.data() + bytes); |
| 100 | + std::copy(valbytes, valbytes + (total_bytes - bytes), data2.data() + bytes); |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + // Whether UseCrypt is used or not, the two byte arrays must match. |
| 105 | + assert(data1 == data2); |
| 106 | + |
| 107 | + // Encrypt data1, the whole array at once. |
| 108 | + if constexpr (UseCrypt) { |
| 109 | + crypt1.Crypt(data1.data(), data1.data(), total_bytes); |
| 110 | + } else { |
| 111 | + crypt1.Keystream(data1.data(), total_bytes); |
| 112 | + } |
| 113 | + |
| 114 | + // Encrypt data2, in at most 256 chunks. |
| 115 | + uint64_t bytes2 = 0; |
| 116 | + int iter = 0; |
| 117 | + while (true) { |
| 118 | + bool is_last = (iter == 255) || (bytes2 == total_bytes) || provider.ConsumeBool(); |
| 119 | + ++iter; |
| 120 | + // Determine how many bytes to encrypt in this chunk: a fuzzer-determined |
| 121 | + // amount for all but the last chunk (which processes all remaining bytes). |
| 122 | + uint64_t now = is_last ? total_bytes - bytes2 : |
| 123 | + provider.ConsumeIntegralInRange<uint64_t>(0, total_bytes - bytes2); |
| 124 | + // For each chunk, consider using Crypt() even when UseCrypt is false. |
| 125 | + // This tests that Keystream() has the same behavior as Crypt() applied |
| 126 | + // to 0x00 input bytes. |
| 127 | + if (UseCrypt || provider.ConsumeBool()) { |
| 128 | + crypt2.Crypt(data2.data() + bytes2, data2.data() + bytes2, now); |
| 129 | + } else { |
| 130 | + crypt2.Keystream(data2.data() + bytes2, now); |
| 131 | + } |
| 132 | + bytes2 += now; |
| 133 | + if (is_last) break; |
| 134 | + } |
| 135 | + // We should have processed everything now. |
| 136 | + assert(bytes2 == total_bytes); |
| 137 | + // And the result should match. |
| 138 | + assert(data1 == data2); |
| 139 | +} |
| 140 | + |
| 141 | +} // namespace |
| 142 | + |
| 143 | +FUZZ_TARGET(chacha20_split_crypt) |
| 144 | +{ |
| 145 | + FuzzedDataProvider provider{buffer.data(), buffer.size()}; |
| 146 | + ChaCha20SplitFuzz<true>(provider); |
| 147 | +} |
| 148 | + |
| 149 | +FUZZ_TARGET(chacha20_split_keystream) |
| 150 | +{ |
| 151 | + FuzzedDataProvider provider{buffer.data(), buffer.size()}; |
| 152 | + ChaCha20SplitFuzz<false>(provider); |
| 153 | +} |
0 commit comments