diff --git a/crypto/src/main/java/org/tron/common/crypto/cryptohash/Digest.java b/crypto/src/main/java/org/tron/common/crypto/cryptohash/Digest.java index 62362d1d62d..6733d71d80a 100644 --- a/crypto/src/main/java/org/tron/common/crypto/cryptohash/Digest.java +++ b/crypto/src/main/java/org/tron/common/crypto/cryptohash/Digest.java @@ -18,6 +18,8 @@ package org.tron.common.crypto.cryptohash; +import java.security.DigestException; + /** * Copyright (c) 2007-2010 Project RNRT SAPHIR * @@ -57,9 +59,12 @@ public interface Digest { /** * Insert some more bytes. * + *

Invalid input is rejected before consuming any bytes, preserving the current hash state. + * * @param inbuf the data buffer * @param off the data offset in {@code inbuf} * @param len the data length (in bytes) + * @throws IllegalArgumentException if the input buffer is null or the input range is invalid */ void update(byte[] inbuf, int off, int len); @@ -82,17 +87,17 @@ public interface Digest { /** * Finalize the current hash computation and store the hash value in the provided output buffer. - * The {@code len} parameter contains the maximum number of bytes that should be written; no more - * bytes than the natural hash function output length will be produced. If {@code len} is smaller - * than the natural hash output length, the hash output is truncated to its first {@code len} - * bytes. The object is resetted. + * The {@code len} parameter contains the number of bytes available for the digest. The complete + * digest is written and the object is reset on success. Partial digests are not returned. * * @param outbuf the output buffer * @param off the output offset within {@code outbuf} - * @param len the requested hash output length (in bytes) + * @param len the available output length (in bytes) * @return the number of bytes actually written in {@code outbuf} + * @throws DigestException if {@code len} is smaller than the digest length + * @throws IllegalArgumentException if the output buffer is null or the output range is invalid */ - int digest(byte[] outbuf, int off, int len); + int digest(byte[] outbuf, int off, int len) throws DigestException; /** * Get the natural hash function output length (in bytes). diff --git a/crypto/src/main/java/org/tron/common/crypto/cryptohash/DigestEngine.java b/crypto/src/main/java/org/tron/common/crypto/cryptohash/DigestEngine.java index c7d013a0804..b13e1d14ea2 100644 --- a/crypto/src/main/java/org/tron/common/crypto/cryptohash/DigestEngine.java +++ b/crypto/src/main/java/org/tron/common/crypto/cryptohash/DigestEngine.java @@ -18,12 +18,13 @@ package org.tron.common.crypto.cryptohash; +import java.security.DigestException; import java.security.MessageDigest; public abstract class DigestEngine extends MessageDigest implements Digest { private int digestLen, blockLen, inputLen; - private byte[] inputBuf, outputBuf; + private byte[] inputBuf; private long blockCount; /** @@ -35,15 +36,18 @@ public DigestEngine(String alg) { digestLen = engineGetDigestLength(); blockLen = getInternalBlockLength(); inputBuf = new byte[blockLen]; - outputBuf = new byte[digestLen]; inputLen = 0; blockCount = 0; } /** - * Reset the hash algorithm state. + * Reset the buffered input state. Subclasses must call this method and reset their algorithm + * state. */ - protected abstract void engineReset(); + protected void engineReset() { + inputLen = 0; + blockCount = 0; + } /** * Process one block of data. @@ -72,7 +76,6 @@ public DigestEngine(String alg) { private void adjustDigestLen() { if (digestLen == 0) { digestLen = engineGetDigestLength(); - outputBuf = new byte[digestLen]; } } @@ -82,7 +85,8 @@ private void adjustDigestLen() { public byte[] digest() { adjustDigestLen(); byte[] result = new byte[digestLen]; - digest(result, 0, digestLen); + doPadding(result, 0); + reset(); return result; } @@ -97,18 +101,25 @@ public byte[] digest(byte[] input) { /** * @see Digest */ - public int digest(byte[] buf, int offset, int len) { + public int digest(byte[] buf, int offset, int len) throws DigestException { + if (buf == null) { + throw new IllegalArgumentException("Output buffer is null"); + } + if (offset < 0 || len < 0 || offset > buf.length || len > buf.length - offset) { + throw new IllegalArgumentException("Invalid output buffer range"); + } adjustDigestLen(); - if (len >= digestLen) { - doPadding(buf, offset); - reset(); - return digestLen; - } else { - doPadding(outputBuf, 0); - System.arraycopy(outputBuf, 0, buf, offset, len); - reset(); - return len; + if (len < digestLen) { + throw new DigestException("Partial digests are not returned"); } + doPadding(buf, offset); + reset(); + return digestLen; + } + + @Override + protected int engineDigest(byte[] buf, int offset, int len) throws DigestException { + return digest(buf, offset, len); } /** @@ -143,6 +154,12 @@ public void update(byte[] input) { * @see Digest */ public void update(byte[] input, int offset, int len) { + if (input == null) { + throw new IllegalArgumentException("Input buffer is null"); + } + if (offset < 0 || len < 0 || offset > input.length || len > input.length - offset) { + throw new IllegalArgumentException("Invalid input buffer range"); + } while (len > 0) { int copyLen = blockLen - inputLen; if (copyLen > len) { @@ -222,8 +239,6 @@ protected Digest copyState(DigestEngine dest) { inputBuf.length); adjustDigestLen(); dest.adjustDigestLen(); - System.arraycopy(outputBuf, 0, dest.outputBuf, 0, - outputBuf.length); return dest; } } diff --git a/crypto/src/main/java/org/tron/common/crypto/cryptohash/Keccak256.java b/crypto/src/main/java/org/tron/common/crypto/cryptohash/Keccak256.java index 83c0577c789..7ac282e84b5 100644 --- a/crypto/src/main/java/org/tron/common/crypto/cryptohash/Keccak256.java +++ b/crypto/src/main/java/org/tron/common/crypto/cryptohash/Keccak256.java @@ -37,14 +37,16 @@ public int engineGetDigestLength() { @Override protected byte[] engineDigest() { - return null; + return digest(); } @Override - protected void engineUpdate(byte arg0) { + protected void engineUpdate(byte input) { + update(input); } @Override - protected void engineUpdate(byte[] arg0, int arg1, int arg2) { + protected void engineUpdate(byte[] input, int offset, int len) { + update(input, offset, len); } } diff --git a/crypto/src/main/java/org/tron/common/crypto/cryptohash/Keccak512.java b/crypto/src/main/java/org/tron/common/crypto/cryptohash/Keccak512.java index 8283180370e..3846012ec46 100644 --- a/crypto/src/main/java/org/tron/common/crypto/cryptohash/Keccak512.java +++ b/crypto/src/main/java/org/tron/common/crypto/cryptohash/Keccak512.java @@ -37,14 +37,16 @@ public int engineGetDigestLength() { @Override protected byte[] engineDigest() { - return null; + return digest(); } @Override protected void engineUpdate(byte input) { + update(input); } @Override protected void engineUpdate(byte[] input, int offset, int len) { + update(input, offset, len); } } diff --git a/crypto/src/main/java/org/tron/common/crypto/cryptohash/KeccakCore.java b/crypto/src/main/java/org/tron/common/crypto/cryptohash/KeccakCore.java index 8e1bc558096..c8919f64904 100644 --- a/crypto/src/main/java/org/tron/common/crypto/cryptohash/KeccakCore.java +++ b/crypto/src/main/java/org/tron/common/crypto/cryptohash/KeccakCore.java @@ -79,6 +79,7 @@ private static long decodeLELong(byte[] buf, int off) { } protected void engineReset() { + super.engineReset(); doReset(); } diff --git a/framework/src/test/java/org/tron/common/crypto/cryptohash/KeccakTest.java b/framework/src/test/java/org/tron/common/crypto/cryptohash/KeccakTest.java new file mode 100644 index 00000000000..c3d1a96ba2e --- /dev/null +++ b/framework/src/test/java/org/tron/common/crypto/cryptohash/KeccakTest.java @@ -0,0 +1,357 @@ +package org.tron.common.crypto.cryptohash; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.nio.ByteBuffer; +import java.security.DigestException; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Collection; +import org.bouncycastle.crypto.digests.KeccakDigest; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.tron.common.crypto.jce.TronCastleProvider; + +@RunWith(Parameterized.class) +public class KeccakTest { + + private final int bits; + + public KeccakTest(int bits) { + this.bits = bits; + } + + @Parameterized.Parameters(name = "Keccak{0}") + public static Collection parameters() { + return Arrays.asList(new Object[][]{{256}, {512}}); + } + + @Test + public void testByteArrayDigest() throws Exception { + byte[] input = inputBytes(); + assertArrayEquals(referenceDigest(input), newDigest().digest(input)); + } + + @Test + public void testHeapByteBuffer() throws Exception { + checkBuffer(ByteBuffer.wrap(inputBytes())); + } + + @Test + public void testDirectByteBuffer() throws Exception { + ByteBuffer buffer = ByteBuffer.allocateDirect(inputBytes().length); + buffer.put(inputBytes()); + buffer.flip(); + checkBuffer(buffer); + } + + @Test + public void testReadOnlyByteBuffer() throws Exception { + checkBuffer(ByteBuffer.wrap(inputBytes()).asReadOnlyBuffer()); + } + + @Test + public void testByteBufferSlice() throws Exception { + byte[] input = inputBytes(); + ByteBuffer buffer = ByteBuffer.allocate(input.length + 10); + buffer.position(5); + buffer.put(input); + buffer.limit(5 + input.length); + buffer.position(5); + checkBuffer(buffer.slice()); + } + + @Test + public void testByteBufferWindow() throws Exception { + ByteBuffer buffer = ByteBuffer.wrap(inputBytes()); + buffer.position(7); + buffer.limit(buffer.limit() - 11); + checkBuffer(buffer); + } + + @Test + public void testEmptyByteBuffer() throws Exception { + ByteBuffer buffer = ByteBuffer.wrap(inputBytes()); + buffer.position(buffer.limit()); + checkBuffer(buffer); + } + + @Test + public void testMixedInput() throws Exception { + byte[] input = inputBytes(); + MessageDigest digest = newDigest(); + digest.update(input[0]); + digest.update(input, 1, 16); + ByteBuffer buffer = ByteBuffer.wrap(input, 17, input.length - 17); + digest.update(buffer); + assertEquals(buffer.limit(), buffer.position()); + assertArrayEquals(referenceDigest(input), digest.digest()); + } + + @Test + public void testInvalidUpdateRangePreservesState() throws Exception { + byte[] input = inputBytes(); + byte[] invalidInput = Arrays.copyOf(input, 200); + int[][] ranges = { + {-1, 1}, {-1, 0}, {0, -1}, {invalidInput.length + 1, 0}, + {invalidInput.length, 1}, {0, 300}, + {Integer.MAX_VALUE, 1}, {1, Integer.MAX_VALUE} + }; + for (int prefixLength : new int[]{0, 17, 200}) { + for (int[] range : ranges) { + MessageDigest digest = newDigest(); + digest.update(input, 0, prefixLength); + + assertThrows(IllegalArgumentException.class, + () -> digest.update(invalidInput, range[0], range[1])); + + digest.update(input, prefixLength, input.length - prefixLength); + assertArrayEquals(referenceDigest(input), digest.digest()); + } + } + } + + @Test + public void testNullUpdatePreservesState() throws Exception { + byte[] input = inputBytes(); + for (int length : new int[]{0, 1, -1}) { + MessageDigest digest = newDigest(); + digest.update(input, 0, 17); + + assertThrows(IllegalArgumentException.class, () -> digest.update(null, 0, length)); + + digest.update(input, 17, input.length - 17); + assertArrayEquals(referenceDigest(input), digest.digest()); + } + } + + @Test + public void testEmptyUpdateRangePreservesState() throws Exception { + byte[] input = inputBytes(); + MessageDigest digest = newDigest(); + digest.update(input, 0, 17); + + digest.update(input, input.length, 0); + digest.update(new byte[0], 0, 0); + + digest.update(input, 17, input.length - 17); + assertArrayEquals(referenceDigest(input), digest.digest()); + } + + @Test + public void testDigestOutputRange() throws Exception { + byte[] input = inputBytes(); + byte[] expected = referenceDigest(input); + for (int available : new int[]{expected.length, expected.length + 5}) { + MessageDigest digest = newDigest(); + digest.update(input); + byte[] output = new byte[expected.length + 12]; + Arrays.fill(output, (byte) 0x5a); + byte[] expectedOutput = output.clone(); + System.arraycopy(expected, 0, expectedOutput, 3, expected.length); + assertEquals(expected.length, digest.digest(output, 3, available)); + assertArrayEquals(expectedOutput, output); + assertArrayEquals(referenceDigest(new byte[0]), digest.digest()); + } + } + + @Test + public void testShortDigestPreservesState() throws Exception { + byte[] input = inputBytes(); + for (int available : new int[]{0, 1, bits / 8 - 1}) { + for (int capacity : new int[]{available, bits / 8}) { + MessageDigest digest = newDigest(); + digest.update(input, 0, 17); + byte[] output = new byte[capacity]; + Arrays.fill(output, (byte) 0x5a); + byte[] original = output.clone(); + assertThrows(DigestException.class, () -> digest.digest(output, 0, available)); + assertArrayEquals(original, output); + digest.update(input, 17, input.length - 17); + assertArrayEquals(referenceDigest(input), digest.digest()); + } + } + } + + @Test + public void testInvalidDigestRangePreservesState() throws Exception { + byte[] input = inputBytes(); + int length = bits / 8; + int[][] ranges = { + {-1, length}, {0, -1}, {length + 1, 0}, {1, length}, + {Integer.MAX_VALUE, length}, {1, Integer.MAX_VALUE} + }; + for (int[] range : ranges) { + MessageDigest digest = newDigest(); + digest.update(input); + byte[] output = new byte[length]; + Arrays.fill(output, (byte) 0x5a); + byte[] original = output.clone(); + assertThrows(IllegalArgumentException.class, + () -> digest.digest(output, range[0], range[1])); + assertArrayEquals(original, output); + assertArrayEquals(referenceDigest(input), digest.digest()); + } + MessageDigest digest = newDigest(); + digest.update(input); + assertThrows(IllegalArgumentException.class, () -> digest.digest(null, 0, length)); + assertArrayEquals(referenceDigest(input), digest.digest()); + } + + @Test + public void testDigestInterface() throws Exception { + Digest digest = (Digest) newDigest(); + byte[] input = inputBytes(); + digest.update(input); + assertThrows(DigestException.class, () -> digest.digest(new byte[1], 0, 1)); + byte[] output = new byte[bits / 8]; + assertEquals(output.length, digest.digest(output, 0, output.length)); + assertArrayEquals(referenceDigest(input), output); + } + + @Test + public void testSpiMethods() throws Exception { + byte[] input = inputBytes(); + byte[] actual; + byte[] afterReset; + if (bits == 256) { + Keccak256 digest = (Keccak256) newDigest(); + digest.engineUpdate(input[0]); + digest.engineUpdate(input, 1, input.length - 1); + actual = digest.engineDigest(); + afterReset = digest.engineDigest(); + } else { + Keccak512 digest = (Keccak512) newDigest(); + digest.engineUpdate(input[0]); + digest.engineUpdate(input, 1, input.length - 1); + actual = digest.engineDigest(); + afterReset = digest.engineDigest(); + } + assertArrayEquals(referenceDigest(input), actual); + assertArrayEquals(referenceDigest(new byte[0]), afterReset); + } + + @Test + public void testShortSpiDigestPreservesState() throws Exception { + for (int inputLength : new int[]{17, 8193}) { + byte[] input = Arrays.copyOf(inputBytes(), inputLength); + byte[] expected = referenceDigest(input); + for (int available : new int[]{0, 1, expected.length - 1}) { + DigestEngine digest = bits == 256 ? new TestKeccak256() : new TestKeccak512(); + digest.update(input); + byte[] output = new byte[expected.length + 6]; + Arrays.fill(output, (byte) 0x5a); + byte[] expectedOutput = output.clone(); + + assertThrows(DigestException.class, () -> spiDigest(digest, output, 3, available)); + assertArrayEquals(expectedOutput, output); + + assertEquals(expected.length, spiDigest(digest, output, 3, expected.length)); + System.arraycopy(expected, 0, expectedOutput, 3, expected.length); + assertArrayEquals(expectedOutput, output); + assertArrayEquals(referenceDigest(new byte[0]), digest.digest()); + } + } + } + + @Test + public void testSpiResetClearsBufferedState() throws Exception { + DigestEngine digest = (DigestEngine) newDigest(); + int blockLength = digest.getBlockLength(); + for (int length : new int[]{0, 1, blockLength - 1, blockLength, blockLength + 1, + 2 * blockLength + 3}) { + byte[] input = Arrays.copyOf(inputBytes(), length); + if (bits == 256) { + ((Keccak256) digest).engineUpdate(input, 0, input.length); + } else { + ((Keccak512) digest).engineUpdate(input, 0, input.length); + } + digest.engineReset(); + assertEquals("Buffered bytes after reset for length " + length, 0, digest.flush()); + assertEquals("Processed blocks after reset for length " + length, 0, digest.getBlockCount()); + byte[] afterReset = bits == 256 + ? ((Keccak256) digest).engineDigest() : ((Keccak512) digest).engineDigest(); + assertArrayEquals(referenceDigest(new byte[0]), afterReset); + } + } + + @Test + public void testSpiResetBeforeNewInput() throws Exception { + DigestEngine digest = (DigestEngine) newDigest(); + digest.update(inputBytes()); + digest.engineReset(); + digest.engineReset(); + byte[] nextInput = {1, 2, 3}; + digest.update(nextInput); + assertArrayEquals(referenceDigest(nextInput), digest.digest()); + } + + @Test + public void testCopy() throws Exception { + DigestEngine digest = (DigestEngine) newDigest(); + byte[] input = inputBytes(); + digest.update(ByteBuffer.wrap(input, 0, 151)); + Digest copy = digest.copy(); + digest.update(ByteBuffer.wrap(input, 151, input.length - 151)); + assertArrayEquals(referenceDigest(input), digest.digest()); + assertArrayEquals(referenceDigest(Arrays.copyOf(input, 151)), copy.digest()); + } + + private MessageDigest newDigest() throws Exception { + return MessageDigest.getInstance("TRON-KECCAK-" + bits, TronCastleProvider.getInstance()); + } + + private int spiDigest(DigestEngine digest, byte[] output, int offset, int len) + throws DigestException { + return bits == 256 + ? ((TestKeccak256) digest).engineDigest(output, offset, len) + : ((TestKeccak512) digest).engineDigest(output, offset, len); + } + + private static class TestKeccak256 extends Keccak256 { + + @Override + protected int engineDigest(byte[] output, int offset, int len) throws DigestException { + return super.engineDigest(output, offset, len); + } + } + + private static class TestKeccak512 extends Keccak512 { + + @Override + protected int engineDigest(byte[] output, int offset, int len) throws DigestException { + return super.engineDigest(output, offset, len); + } + } + + private byte[] referenceDigest(byte[] input) { + KeccakDigest digest = new KeccakDigest(bits); + digest.update(input, 0, input.length); + byte[] output = new byte[bits / 8]; + digest.doFinal(output, 0); + return output; + } + + private byte[] inputBytes() { + byte[] input = new byte[8193]; + for (int i = 0; i < input.length; i++) { + input[i] = (byte) i; + } + return input; + } + + private void checkBuffer(ByteBuffer buffer) throws Exception { + byte[] input = new byte[buffer.remaining()]; + buffer.duplicate().get(input); + int limit = buffer.limit(); + MessageDigest digest = newDigest(); + digest.update(buffer); + assertEquals(limit, buffer.position()); + assertEquals(limit, buffer.limit()); + assertArrayEquals(referenceDigest(input), digest.digest()); + assertArrayEquals(referenceDigest(new byte[0]), digest.digest()); + } +}