From 9e8efebad5094adee86954dd687115e2c62cc757 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 31 May 2023 12:17:06 +0100 Subject: [PATCH] Add support for SipHash short-input hashing. --- README.md | 19 ++ .../software/pando/crypto/nacl/Crypto.java | 72 +++++- .../software/pando/crypto/nacl/SipHash24.java | 210 ++++++++++++++++++ .../pando/crypto/nacl/CryptoTest.java | 40 +++- .../pando/crypto/nacl/SipHash24Test.java | 133 +++++++++++ 5 files changed, 462 insertions(+), 12 deletions(-) create mode 100644 src/main/java/software/pando/crypto/nacl/SipHash24.java create mode 100644 src/test/java/software/pando/crypto/nacl/SipHash24Test.java diff --git a/README.md b/README.md index f0b434b..d24c3a9 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,25 @@ byte[] hash = Crypto.hash(data); ``` This is SHA-512. +## Short-input hashing + +This function provides a fast pseudorandom function (PRF) suitable for hashing short inputs up to a few kB in size. +It is intended to be used as a hash-table or Bloom filter hash function replacing traditional functions such as Murmur3 +in cases where the risk of hash collision denial of service (DoS) attacks is high. So long as the key is kept secret, +an attacker will not be able to easily find inputs that produce the same hash value. However, if the key is known then +collisions can easily be created and the output size (64 bits) is insufficient to be collision-resistant in this case. + +This function can also be used as a MAC in cases where the overhead of `Crypto.auth` is too high (such as constrained +devices or low-level communication protocols), but the security level is much lower due to the small tag size. It is +recommended that additional rate-limiting techniques are used in this case to limit the risk of authentication forgeries. + +The implementation is based on SipHash-2-4 and is compatible with the same functionality in [libsodium](https://doc.libsodium.org/hashing/short-input_hashing). + +```java +SecretKey key = Crypto.shortHashKeyGen(); +byte[] hash = Crypto.shortHash(key, data); +``` + ## Random bytes ```java diff --git a/src/main/java/software/pando/crypto/nacl/Crypto.java b/src/main/java/software/pando/crypto/nacl/Crypto.java index 3956377..b3c5ba0 100644 --- a/src/main/java/software/pando/crypto/nacl/Crypto.java +++ b/src/main/java/software/pando/crypto/nacl/Crypto.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Neil Madden. + * Copyright 2019-2023 Neil Madden. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,16 @@ package software.pando.crypto.nacl; -import javax.crypto.SecretKey; -import javax.security.auth.Destroyable; +import static java.nio.charset.StandardCharsets.*; + import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.util.Arrays; import java.util.Iterator; -import static java.nio.charset.StandardCharsets.US_ASCII; +import javax.crypto.SecretKey; +import javax.security.auth.Destroyable; /** * The main interface to all cryptographic operations provided by this library. @@ -392,6 +393,69 @@ public static byte[] kdfDeriveFromInputKeyMaterial(byte[] salt, byte[] inputKeyM } } + /** + * Generates a fresh random key for use with the {@link #shortHash(SecretKey, byte[])} method. + * + * @return a fresh cryptographically-strong random key with at least 128 bits of entropy. + */ + public static SecretKey shortHashKeyGen() { + return shortHashKey(Bytes.secureRandom(16)); + } + + /** + * Imports the given byte slice as a secret key suitable for use with the {@link #shortHash(SecretKey, byte[])} + * method. The key material must be exactly 16 bytes in length. + * + * @param keyBytes the key material to import. + * @return the imported key material as a secret key object. + * @throws IllegalArgumentException if the key is not 16 bytes long. + */ + public static SecretKey shortHashKey(ByteSlice keyBytes) { + if (keyBytes.length() != 16) { + throw new IllegalArgumentException("Short hash key must be 16 bytes"); + } + return new CryptoSecretKey(keyBytes.array, keyBytes.offset, keyBytes.length, "SipHash"); + } + + /** + * Imports the given byte array as a secret key suitable for use with the {@link #shortHash(SecretKey, byte[])} + * method. The key material must be exactly 16 bytes in length. + * + * @param keyBytes the key material to import. + * @return the imported key material as a secret key object. + * @throws IllegalArgumentException if the key is not 16 bytes long. + */ + public static SecretKey shortHashKey(byte[] keyBytes) { + var slice = ByteSlice.of(keyBytes); + try { + return shortHashKey(slice); + } finally { + slice.wipe(); + } + } + + /** + * Hashes the given short input using a pseudorandom function (PRF) keyed with the given key. A 64-bit output + * tag is returned. The PRF makes it very hard for an attacker to discover hash collisions so long as the key + * remains secret and is therefore suitable for use in hash tables and other data structures to avoid hash + * collision denial of service (DoS) attacks. Use {@link #shortHashKeyGen()} to generate a random key for use with + * this function. + *

+ * Although this function is a secure PRF, the output size is too short to be collision resistant if the key is + * known to an attacker. This function can also be used as a secure MAC in cases where tag sizes must be + * minimised, data sizes are small, and other rate-limiting techniques are in place to limit tag forgeries. If + * these conditions are not satisfied then you should use a more general MAC like {@link #auth(SecretKey, byte[])}. + * + *

The implementation uses the SipHash-2-4 secure PRF. + * + * @param key the key, generated by {@link #shortHashKeyGen()}. + * @param data the data to hash, which should be at most a few kB in size. + * @return the 64-bit hash tag. + */ + public static byte[] shortHash(SecretKey key, byte[] data) { + return SipHash24.hash(key, data); + } + private Crypto() { throw new UnsupportedOperationException(); } diff --git a/src/main/java/software/pando/crypto/nacl/SipHash24.java b/src/main/java/software/pando/crypto/nacl/SipHash24.java new file mode 100644 index 0000000..4744967 --- /dev/null +++ b/src/main/java/software/pando/crypto/nacl/SipHash24.java @@ -0,0 +1,210 @@ +/* + * Copyright 2023 Neil Madden. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Portions copyright 2016 Pando Software Ltd. + */ + +package software.pando.crypto.nacl; + +import static java.util.Objects.requireNonNull; + +import java.util.Arrays; + +import javax.crypto.SecretKey; + +/** + * Implementation of the SipHash-2-4 fast, cryptographically strong pseudorandom function (PRF) designed to + * be used as a general purpose hash algorithm to avoid hash-flooding DoS attacks. This implementation is competitive + * in performance to other general-purpose Java hash algorithms such as MurmurHash, whilst having significantly + * stronger cryptographic properties. In particular, it is much more difficult to predict and manufacture hash + * collisions with SipHash so long as the key remains secret. + *

+ * SipHash can also be used as a Message Authentication Code (MAC) for short messages, but be aware that the output + * size (64 bits) is considered too small to be secure on its own in this usage. It is better to use a general-purpose + * MAC for those cases, such as BLAKE2 or SHA-256, which have significantly larger output tag sizes. SipHash can be used + * in cases where either the format precludes larger tag sizes (such as IP packet authentication) or where there are + * other mitigations (e.g., rate limiting if only online attacks are possible). + *

+ * A SipHash algorithm with c compression rounds and f finalization rounds is known as + * SipHash-c-f. For instance, SipHash-2-4 has 2 compression rounds and 4 finalization rounds. This + * is the default, as recommended by the SipHash authors and the only variant implemented here. + *

+ * The algorithm is designed to work well with short inputs, typically less than 1KiB in size. The interface is + * therefore designed to accept the input directly as a single byte array. It is not recommended to use it with + * significantly larger inputs, as other hash algorithms will likely be faster. + * + * @see SipHash Website + */ +final class SipHash24 { + + private SipHash24() {} + + /** + * Computes a PRF tag for the given input data and the configured secret key. + * + * @param key the SipHash key. + * @param input the input data. + * @return the computed SipHash tag for the data using the configured key. + */ + static byte[] hash(final SecretKey key, final byte[] input) { + requireNonNull(key, "key"); + if (!"SipHash".equalsIgnoreCase(key.getAlgorithm())) { + throw new IllegalArgumentException("Key is not intended for use with SipHash"); + } + if (!"raw".equalsIgnoreCase(key.getFormat())) { + throw new IllegalArgumentException("Only RAW format keys supported"); + } + final byte[] keyBytes = key.getEncoded(); + if (keyBytes.length != 16) { + throw new IllegalArgumentException("Key must be 16 bytes exactly"); + } + final long[] initialState = initialState(); + final long k0 = bytesToLong(keyBytes, 0); + final long k1 = bytesToLong(keyBytes, 8); + Arrays.fill(keyBytes, (byte) 0); + + initialState[3] ^= k1; + initialState[2] ^= k0; + initialState[1] ^= k1; + initialState[0] ^= k0; + + long[] state = Arrays.copyOf(initialState, 4); + + int len = input.length - (input.length % 8); + for (int offset = 0; offset < len; offset += 8) { + long m = bytesToLong(input, offset); + state[3] ^= m; + + // Compression rounds + sipround(state); + sipround(state); + + state[0] ^= m; + } + + long b = lastBits(input); + + state[3] ^= b; + // Last block compression rounds + sipround(state); + sipround(state); + + state[0] ^= b; + state[2] ^= 0xff; + // Finalization rounds + sipround(state); + sipround(state); + sipround(state); + sipround(state); + + b = state[0] ^ state[1] ^ state[2] ^ state[3]; + + byte[] out = new byte[8]; + longToBytes(out, b); + + return out; + } + + static long[] initialState() { + return new long[] { + 0x736f6d6570736575L, // "somepseu" + 0x646f72616e646f6dL, // "dorandom" + 0x6c7967656e657261L, // "lygenera" + 0x7465646279746573L // "tedbytes" + }; + } + + @SuppressWarnings("fallthrough") + static long lastBits(final byte[] input) { + final int left = input.length & 7; + final int len = input.length - (input.length % 8); + long b = (long) input.length << 56; + + switch (left) { + case 7: + b |= ((long) input[len + 6]) << 48; + case 6: + b |= ((long) input[len + 5]) << 40; + case 5: + b |= ((long) input[len + 4]) << 32; + case 4: + b |= ((long) input[len + 3]) << 24; + case 3: + b |= ((long) input[len + 2]) << 16; + case 2: + b |= ((long) input[len + 1]) << 8; + case 1: + b |= ((long) input[len]); + break; + case 0: + break; + } + return b; + } + + /** + * Implements a single round of the SipHash algorithm. + * + * @param state the internal state of the PRF. Must have exactly 4 elements. + */ + static void sipround(long[] state) { + long v0 = state[0], v1 = state[1], v2 = state[2], v3 = state[3]; + + v0 += v1; + v2 += v3; + v1 = Long.rotateLeft(v1, 13); + v3 = Long.rotateLeft(v3, 16); + v1 ^= v0; + v3 ^= v2; + + v0 = Long.rotateLeft(v0, 32); + + v2 += v1; + v0 += v3; + v1 = Long.rotateLeft(v1, 17); + v3 = Long.rotateLeft(v3, 21); + v1 ^= v2; + v3 ^= v0; + + v2 = Long.rotateLeft(v2, 32); + + state[0] = v0; + state[1] = v1; + state[2] = v2; + state[3] = v3; + } + + static void longToBytes(byte[] p, long v) { + assert p.length >= 8; + p[0] = (byte) v; + p[1] = (byte) (v >>> 8); + p[2] = (byte) (v >>> 16); + p[3] = (byte) (v >>> 24); + p[4] = (byte) (v >>> 32); + p[5] = (byte) (v >>> 40); + p[6] = (byte) (v >>> 48); + p[7] = (byte) (v >>> 56); + } + + static long bytesToLong(byte[] p, int offset) { + return l(p[offset]) | (l(p[offset + 1]) << 8) | (l(p[offset + 2]) << 16) | (l(p[offset + 3]) << 24) + | (l(p[offset + 4]) << 32) | (l(p[offset + 5]) << 40) | (l(p[offset + 6]) << 48) + | (l(p[offset + 7]) << 56); + } + + private static long l(byte b) { + return b & 0xffL; + } +} \ No newline at end of file diff --git a/src/test/java/software/pando/crypto/nacl/CryptoTest.java b/src/test/java/software/pando/crypto/nacl/CryptoTest.java index 27c421b..3c23a3c 100644 --- a/src/test/java/software/pando/crypto/nacl/CryptoTest.java +++ b/src/test/java/software/pando/crypto/nacl/CryptoTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 Neil Madden. + * Copyright 2019-2023 Neil Madden. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,10 @@ package software.pando.crypto.nacl; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; +import static java.nio.charset.StandardCharsets.*; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.internal.Digests.fromHex; -import javax.crypto.SecretKey; import java.security.KeyPair; import java.util.ArrayList; import java.util.Arrays; @@ -29,9 +28,11 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.internal.Digests.fromHex; +import javax.crypto.SecretKey; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; public class CryptoTest { @@ -225,4 +226,27 @@ private static byte[] mutate(byte[] input) { return output; } + @Test + public void shouldProduceValidSipHashKeys() { + SecretKey hashKey = Crypto.shortHashKeyGen(); + assertThat(hashKey) + .isNotNull() + .hasFieldOrPropertyWithValue("algorithm", "SipHash") + .hasFieldOrPropertyWithValue("format", "RAW"); + assertThat(hashKey.getEncoded()) + .isNotNull() + .hasSize(16) + .isNotEqualTo(new byte[16]); + } + + @Test + public void shouldProduceValidSipHashOutputs() { + SecretKey key = Crypto.shortHashKey(new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }); + byte[] input = new byte[] { 0 }; + byte[] expected = new byte[] { + (byte) 0xfd, 0x67, (byte) 0xdc, (byte) 0x93, (byte) 0xc5, 0x39, (byte) 0xf8, 0x74 }; + byte[] computed = Crypto.shortHash(key, input); + assertThat(computed).isEqualTo(expected); + } } \ No newline at end of file diff --git a/src/test/java/software/pando/crypto/nacl/SipHash24Test.java b/src/test/java/software/pando/crypto/nacl/SipHash24Test.java new file mode 100644 index 0000000..9bc59b1 --- /dev/null +++ b/src/test/java/software/pando/crypto/nacl/SipHash24Test.java @@ -0,0 +1,133 @@ +/* + * Copyright 2023 Neil Madden. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package software.pando.crypto.nacl; + +import static org.assertj.core.api.Assertions.*; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class SipHash24Test { + + // 128-bit fixed key + private static final SecretKey KEY = new SecretKeySpec( + new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }, "SipHash"); + + private static final int[][] EXPECTED_OUTPUT = { + { 0x31, 0x0e, 0x0e, 0xdd, 0x47, 0xdb, 0x6f, 0x72, }, + { 0xfd, 0x67, 0xdc, 0x93, 0xc5, 0x39, 0xf8, 0x74, }, + { 0x5a, 0x4f, 0xa9, 0xd9, 0x09, 0x80, 0x6c, 0x0d, }, + { 0x2d, 0x7e, 0xfb, 0xd7, 0x96, 0x66, 0x67, 0x85, }, + { 0xb7, 0x87, 0x71, 0x27, 0xe0, 0x94, 0x27, 0xcf, }, + { 0x8d, 0xa6, 0x99, 0xcd, 0x64, 0x55, 0x76, 0x18, }, + { 0xce, 0xe3, 0xfe, 0x58, 0x6e, 0x46, 0xc9, 0xcb, }, + { 0x37, 0xd1, 0x01, 0x8b, 0xf5, 0x00, 0x02, 0xab, }, + { 0x62, 0x24, 0x93, 0x9a, 0x79, 0xf5, 0xf5, 0x93, }, + { 0xb0, 0xe4, 0xa9, 0x0b, 0xdf, 0x82, 0x00, 0x9e, }, + { 0xf3, 0xb9, 0xdd, 0x94, 0xc5, 0xbb, 0x5d, 0x7a, }, + { 0xa7, 0xad, 0x6b, 0x22, 0x46, 0x2f, 0xb3, 0xf4, }, + { 0xfb, 0xe5, 0x0e, 0x86, 0xbc, 0x8f, 0x1e, 0x75, }, + { 0x90, 0x3d, 0x84, 0xc0, 0x27, 0x56, 0xea, 0x14, }, + { 0xee, 0xf2, 0x7a, 0x8e, 0x90, 0xca, 0x23, 0xf7, }, + { 0xe5, 0x45, 0xbe, 0x49, 0x61, 0xca, 0x29, 0xa1, }, + { 0xdb, 0x9b, 0xc2, 0x57, 0x7f, 0xcc, 0x2a, 0x3f, }, + { 0x94, 0x47, 0xbe, 0x2c, 0xf5, 0xe9, 0x9a, 0x69, }, + { 0x9c, 0xd3, 0x8d, 0x96, 0xf0, 0xb3, 0xc1, 0x4b, }, + { 0xbd, 0x61, 0x79, 0xa7, 0x1d, 0xc9, 0x6d, 0xbb, }, + { 0x98, 0xee, 0xa2, 0x1a, 0xf2, 0x5c, 0xd6, 0xbe, }, + { 0xc7, 0x67, 0x3b, 0x2e, 0xb0, 0xcb, 0xf2, 0xd0, }, + { 0x88, 0x3e, 0xa3, 0xe3, 0x95, 0x67, 0x53, 0x93, }, + { 0xc8, 0xce, 0x5c, 0xcd, 0x8c, 0x03, 0x0c, 0xa8, }, + { 0x94, 0xaf, 0x49, 0xf6, 0xc6, 0x50, 0xad, 0xb8, }, + { 0xea, 0xb8, 0x85, 0x8a, 0xde, 0x92, 0xe1, 0xbc, }, + { 0xf3, 0x15, 0xbb, 0x5b, 0xb8, 0x35, 0xd8, 0x17, }, + { 0xad, 0xcf, 0x6b, 0x07, 0x63, 0x61, 0x2e, 0x2f, }, + { 0xa5, 0xc9, 0x1d, 0xa7, 0xac, 0xaa, 0x4d, 0xde, }, + { 0x71, 0x65, 0x95, 0x87, 0x66, 0x50, 0xa2, 0xa6, }, + { 0x28, 0xef, 0x49, 0x5c, 0x53, 0xa3, 0x87, 0xad, }, + { 0x42, 0xc3, 0x41, 0xd8, 0xfa, 0x92, 0xd8, 0x32, }, + { 0xce, 0x7c, 0xf2, 0x72, 0x2f, 0x51, 0x27, 0x71, }, + { 0xe3, 0x78, 0x59, 0xf9, 0x46, 0x23, 0xf3, 0xa7, }, + { 0x38, 0x12, 0x05, 0xbb, 0x1a, 0xb0, 0xe0, 0x12, }, + { 0xae, 0x97, 0xa1, 0x0f, 0xd4, 0x34, 0xe0, 0x15, }, + { 0xb4, 0xa3, 0x15, 0x08, 0xbe, 0xff, 0x4d, 0x31, }, + { 0x81, 0x39, 0x62, 0x29, 0xf0, 0x90, 0x79, 0x02, }, + { 0x4d, 0x0c, 0xf4, 0x9e, 0xe5, 0xd4, 0xdc, 0xca, }, + { 0x5c, 0x73, 0x33, 0x6a, 0x76, 0xd8, 0xbf, 0x9a, }, + { 0xd0, 0xa7, 0x04, 0x53, 0x6b, 0xa9, 0x3e, 0x0e, }, + { 0x92, 0x59, 0x58, 0xfc, 0xd6, 0x42, 0x0c, 0xad, }, + { 0xa9, 0x15, 0xc2, 0x9b, 0xc8, 0x06, 0x73, 0x18, }, + { 0x95, 0x2b, 0x79, 0xf3, 0xbc, 0x0a, 0xa6, 0xd4, }, + { 0xf2, 0x1d, 0xf2, 0xe4, 0x1d, 0x45, 0x35, 0xf9, }, + { 0x87, 0x57, 0x75, 0x19, 0x04, 0x8f, 0x53, 0xa9, }, + { 0x10, 0xa5, 0x6c, 0xf5, 0xdf, 0xcd, 0x9a, 0xdb, }, + { 0xeb, 0x75, 0x09, 0x5c, 0xcd, 0x98, 0x6c, 0xd0, }, + { 0x51, 0xa9, 0xcb, 0x9e, 0xcb, 0xa3, 0x12, 0xe6, }, + { 0x96, 0xaf, 0xad, 0xfc, 0x2c, 0xe6, 0x66, 0xc7, }, + { 0x72, 0xfe, 0x52, 0x97, 0x5a, 0x43, 0x64, 0xee, }, + { 0x5a, 0x16, 0x45, 0xb2, 0x76, 0xd5, 0x92, 0xa1, }, + { 0xb2, 0x74, 0xcb, 0x8e, 0xbf, 0x87, 0x87, 0x0a, }, + { 0x6f, 0x9b, 0xb4, 0x20, 0x3d, 0xe7, 0xb3, 0x81, }, + { 0xea, 0xec, 0xb2, 0xa3, 0x0b, 0x22, 0xa8, 0x7f, }, + { 0x99, 0x24, 0xa4, 0x3c, 0xc1, 0x31, 0x57, 0x24, }, + { 0xbd, 0x83, 0x8d, 0x3a, 0xaf, 0xbf, 0x8d, 0xb7, }, + { 0x0b, 0x1a, 0x2a, 0x32, 0x65, 0xd5, 0x1a, 0xea, }, + { 0x13, 0x50, 0x79, 0xa3, 0x23, 0x1c, 0xe6, 0x60, }, + { 0x93, 0x2b, 0x28, 0x46, 0xe4, 0xd7, 0x06, 0x66, }, + { 0xe1, 0x91, 0x5f, 0x5c, 0xb1, 0xec, 0xa4, 0x6c, }, + { 0xf3, 0x25, 0x96, 0x5c, 0xa1, 0x6d, 0x62, 0x9f, }, + { 0x57, 0x5f, 0xf2, 0x8e, 0x60, 0x38, 0x1b, 0xe5, }, + { 0x72, 0x45, 0x06, 0xeb, 0x4c, 0x32, 0x8a, 0x95, } + }; + + @DataProvider + public static Object[][] expectedOutputs() { + Object[][] testCases = new Object[64][2]; + for (int i = 0; i < 64; ++i) { + testCases[i][0] = i; + testCases[i][1] = intArrayToByteArray(EXPECTED_OUTPUT[i]); + } + return testCases; + } + + @Test(dataProvider = "expectedOutputs") + public void shouldMatchOfficialTestCases(int inputSize, byte[] expectedOutput) { + // Given + byte[] input = new byte[inputSize]; + for (int i = 0; i < inputSize; ++i) { + input[i] = (byte) i; + } + + // When + byte[] result = SipHash24.hash(KEY, input); + + // Then + assertThat(result).isEqualTo(expectedOutput); + } + + private static byte[] intArrayToByteArray(int[] input) { + byte[] output = new byte[input.length]; + for (int i = 0; i < input.length; ++i) { + output[i] = (byte) (input[i]); + } + return output; + } +} \ No newline at end of file