diff --git a/build.gradle b/build.gradle index b8023ff77..552b5a74d 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,7 @@ plugins { id 'se.patrikerdes.use-latest-versions' version '0.2.18' id 'com.github.ben-manes.versions' version '0.49.0' id 'org.jreleaser' version '1.16.0' + id 'me.champeau.jmh' version '0.7.3' apply false } description = 'A set of libraries and other tools to aid development of blockchain and other decentralized software in Java and other JVM languages' @@ -124,6 +125,39 @@ subprojects { } } + if (file('src/jmh').directory) { + apply plugin: 'me.champeau.jmh' + tasks.named("jmh") { + description = "Usage: gradle jmh -Pincludes=MyBench -PasyncProfiler= -PasyncProfilerOptions=\n" + + "\nRun JMH benchmarks in each of the projects. Allows for controlling JMH execution directly from the command line.\n" + + "\t-Pincludes=\tInclude pattern (regular expression) for benchmarks to be executed. Defaults to including all benchmarks.\n" + + "\t-PasyncProfiler=\tLibrary path to fetch the Async profiler from. Default is to disable profiling.\n" + + "\t-PasyncProfilerOptions=\tOptions to pass on to the Async profiler separated by ';'. Default is to produce a flamegraph with all other default profiler options.\n" + } + + // to pass compilation as the compiler doesn't like what jmh tool is doing + compileJmhJava { + options.compilerArgs << '-Xlint:none' + } + + jmh { + jmhVersion = '1.37' + fork = 3 + includes = _strListCmdArg('includes', ['']) + var asyncProfiler = _strCmdArg('asyncProfiler') + var asyncProfilerOptions = _strCmdArg('asyncProfilerOptions', 'output=flamegraph') + if (asyncProfiler != null) { + profilers = ['async:libPath=' + asyncProfiler + ';' + asyncProfilerOptions] + } + duplicateClassesStrategy = DuplicatesStrategy.WARN + jvmArgs = ['-XX:+EnableDynamicAgentLoading'] + } + + dependencies { + jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + } + } + plugins.withId('java', { _ -> sourceSets { integrationTest { @@ -567,3 +601,18 @@ tasks.register('checkNotice') { } throw new GradleException('NOTICE file is not up-to-date') } + +def _strListCmdArg(name, defaultValue) { + if (!project.hasProperty(name)) + return defaultValue + + return ((String) project.property(name)).tokenize(',') +} + +def _strCmdArg(name) { + return _strCmdArg(name, null) +} + +def _strCmdArg(name, defaultValue) { + return project.hasProperty(name) ? project.property(name) as String : defaultValue +} diff --git a/bytes/build.gradle b/bytes/build.gradle index aa2d575c1..973e68041 100644 --- a/bytes/build.gradle +++ b/bytes/build.gradle @@ -23,6 +23,7 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.junit.jupiter:junit-jupiter-params' testImplementation 'org.mockito:mockito-junit-jupiter' + testImplementation 'org.assertj:assertj-core' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' } diff --git a/bytes/src/jmh/java/org/benchmark/BytesMegamorphicBenchmarkV1.java b/bytes/src/jmh/java/org/benchmark/BytesMegamorphicBenchmarkV1.java new file mode 100644 index 000000000..c4c0cafc4 --- /dev/null +++ b/bytes/src/jmh/java/org/benchmark/BytesMegamorphicBenchmarkV1.java @@ -0,0 +1,62 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.benchmark; + +import org.apache.tuweni.bytes.Bytes; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@BenchmarkMode(value = Mode.AverageTime) +@State(Scope.Thread) +@OutputTimeUnit(value = TimeUnit.NANOSECONDS) +public class BytesMegamorphicBenchmarkV1 { + private static final int N = 4; + private static final int FACTOR = 1_000; + private static final Random RANDOM = new Random(23L); + Bytes[] bytesV1; + + @Param({"mono", "mega"}) + private String mode; + + @Setup + public void setup() { + bytesV1 = new Bytes[N * FACTOR]; + for (int i = 0; i < N * FACTOR; i += N) { + bytesV1[i] = Bytes.wrap(getBytes(32)); + bytesV1[i + 1] = "mega".equals(mode) ? Bytes.wrap(getBytes(48)) : Bytes.wrap(getBytes(32)); + bytesV1[i + 2] = + "mega".equals(mode) ? Bytes.repeat((byte) 0x09, 16) : Bytes.wrap(getBytes(32)); + bytesV1[i + 3] = + "mega".equals(mode) ? Bytes.wrap(bytesV1[i], bytesV1[i + 1]) : Bytes.wrap(getBytes(32)); + } + } + + private static byte[] getBytes(final int size) { + byte[] b = new byte[size]; + RANDOM.nextBytes(b); + return b; + } + + @Benchmark + @OperationsPerInvocation(N * FACTOR) + public void test() { + for (Bytes b : bytesV1) { + b.slice(1); + } + } +} diff --git a/bytes/src/jmh/java/org/benchmark/BytesMegamorphicBenchmarkV2.java b/bytes/src/jmh/java/org/benchmark/BytesMegamorphicBenchmarkV2.java new file mode 100644 index 000000000..b8315bc94 --- /dev/null +++ b/bytes/src/jmh/java/org/benchmark/BytesMegamorphicBenchmarkV2.java @@ -0,0 +1,62 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.benchmark; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OperationsPerInvocation; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@BenchmarkMode(value = Mode.AverageTime) +@State(Scope.Thread) +@OutputTimeUnit(value = TimeUnit.NANOSECONDS) +public class BytesMegamorphicBenchmarkV2 { + private static final int N = 4; + private static final int FACTOR = 1_000; + private static final Random RANDOM = new Random(23L); + Bytes[] bytesV2; + + @Param({"mono", "mega"}) + private String mode; + + @Setup + public void setup() { + bytesV2 = new Bytes[N * FACTOR]; + for (int i = 0; i < N * FACTOR; i += N) { + bytesV2[i] = Bytes.wrap(getBytes(32)); + bytesV2[i + 1] = "mega".equals(mode) ? Bytes.wrap(getBytes(48)) : Bytes.wrap(getBytes(32)); + bytesV2[i + 2] = + "mega".equals(mode) ? Bytes.repeat((byte) 0x09, 16) : Bytes.wrap(getBytes(32)); + bytesV2[i + 3] = + "mega".equals(mode) ? Bytes.wrap(bytesV2[i], bytesV2[i + 1]) : Bytes.wrap(getBytes(32)); + } + } + + private static byte[] getBytes(final int size) { + byte[] b = new byte[size]; + RANDOM.nextBytes(b); + return b; + } + + @Benchmark + @OperationsPerInvocation(N * FACTOR) + public void test() { + for (Bytes b : bytesV2) { + b.slice(1); + } + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/ArrayWrappingBytes.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ArrayWrappingBytes.java new file mode 100644 index 000000000..5a50c2011 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ArrayWrappingBytes.java @@ -0,0 +1,143 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkArgument; +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkLength; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.util.Arrays; + +import io.vertx.core.buffer.Buffer; + +class ArrayWrappingBytes extends Bytes { + + protected byte[] bytes; + protected final int offset; + + ArrayWrappingBytes(byte[] bytes) { + this(bytes, 0, bytes.length); + } + + ArrayWrappingBytes(byte[] bytes, int offset, int length) { + super(length); + this.bytes = bytes; + this.offset = offset; + } + + @Override + public byte get(int i) { + // Check bounds because while the array access would throw, the error message would be confusing + // for the caller. + checkElementIndex(offset + i, bytes.length); + checkElementIndex(i, size()); + return bytes[offset + i]; + } + + @Override + public Bytes slice(int i, int length) { + if (i == 0 && length == size()) { + return this; + } + + if (length == 0) { + return EMPTY; + } + + checkArgument(length > 0, "Invalid negative length"); + if (bytes.length > 0) { + checkElementIndex(offset + i, bytes.length); + } + checkLength(bytes.length, offset + i, length); + + return new ArrayWrappingBytes(bytes, offset + i, length); + } + + @Override + public int commonPrefixLength(Bytes other) { + if (!(other instanceof ArrayWrappingBytes o)) { + return super.commonPrefixLength(other); + } + int i = 0; + while (i < size() && i < o.size() && bytes[offset + i] == o.bytes[o.offset + i]) { + i++; + } + return i; + } + + @Override + public void update(MessageDigest digest) { + digest.update(bytes, offset, size()); + } + + @Override + public void appendTo(ByteBuffer byteBuffer) { + byteBuffer.put(bytes, offset, size()); + } + + @Override + public MutableBytes mutableCopy() { + return MutableBytes.fromArray(bytes, offset, size()); + } + + @Override + public void appendTo(Buffer buffer) { + buffer.appendBytes(bytes, offset, size()); + } + + @Override + public byte[] toArrayUnsafe() { + if (offset == 0 && size() == bytes.length) { + return bytes; + } + return Arrays.copyOfRange(bytes, offset, offset + size()); + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + Utils.and(this.bytes, this.offset, bytesArray, offset, length); + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + Utils.or(this.bytes, this.offset, bytesArray, offset, length); + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + Utils.xor(this.bytes, this.offset, bytesArray, offset, length); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Bytes other)) { + return false; + } + + if (this.size() != other.size()) { + return false; + } + + for (int i = 0; i < size(); i++) { + if (bytes[i + offset] != other.get(i)) { + return false; + } + } + + return true; + } + + @Override + protected int computeHashcode() { + int result = 1; + for (int i = 0; i < size(); i++) { + result = 31 * result + bytes[i + offset]; + } + return result; + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/BufferWrappingBytes.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/BufferWrappingBytes.java new file mode 100644 index 000000000..0d166d293 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/BufferWrappingBytes.java @@ -0,0 +1,124 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkLength; + +import io.vertx.core.buffer.Buffer; + +class BufferWrappingBytes extends Bytes { + protected final Buffer buffer; + + BufferWrappingBytes(Buffer buffer) { + super(buffer.length()); + this.buffer = buffer; + } + + BufferWrappingBytes(Buffer buffer, int offset, int length) { + super(length); + if (offset == 0 && length == buffer.length()) { + this.buffer = buffer; + } else { + this.buffer = buffer.slice(offset, offset + length); + } + } + + @Override + public byte get(int i) { + return buffer.getByte(i); + } + + @Override + public int getInt(int i) { + return buffer.getInt(i); + } + + @Override + public long getLong(int i) { + return buffer.getLong(i); + } + + @Override + public Bytes slice(int i, int length) { + int size = buffer.length(); + if (i == 0 && length == size) { + return this; + } + if (length == 0) { + return EMPTY; + } + + checkElementIndex(i, size); + checkLength(size, i, length); + + return new BufferWrappingBytes(buffer.slice(i, i + length)); + } + + @Override + public MutableBytes mutableCopy() { + return MutableBytes.fromArray(toArrayUnsafe()); + } + + @Override + public void appendTo(Buffer buffer) { + buffer.appendBuffer(this.buffer); + } + + @Override + public byte[] toArrayUnsafe() { + return buffer.getBytes(); + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (buffer.getByte(i) & bytesArray[offset + i]); + } + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (buffer.getByte(i) | bytesArray[offset + i]); + } + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (buffer.getByte(i) ^ bytesArray[offset + i]); + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Bytes other)) { + return false; + } + + if (this.size() != other.size()) { + return false; + } + + for (int i = 0; i < size(); i++) { + if (buffer.getByte(i) != other.get(i)) { + return false; + } + } + + return true; + } + + @Override + protected int computeHashcode() { + int result = 1; + for (int i = 0; i < size(); i++) { + result = 31 * result + buffer.getByte(i); + } + return result; + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/ByteBufWrappingBytes.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ByteBufWrappingBytes.java new file mode 100644 index 000000000..4d002a196 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ByteBufWrappingBytes.java @@ -0,0 +1,128 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkLength; + +import io.netty.buffer.ByteBuf; +import io.vertx.core.buffer.Buffer; + +class ByteBufWrappingBytes extends Bytes { + + protected final ByteBuf byteBuf; + + ByteBufWrappingBytes(ByteBuf byteBuf) { + super(byteBuf.capacity()); + this.byteBuf = byteBuf; + } + + ByteBufWrappingBytes(ByteBuf byteBuf, int offset, int length) { + super(length); + if (offset == 0 && length == byteBuf.capacity()) { + this.byteBuf = byteBuf; + } else { + this.byteBuf = byteBuf.slice(offset, length); + } + } + + @Override + public byte get(int i) { + return byteBuf.getByte(i); + } + + @Override + public int getInt(int i) { + return byteBuf.getInt(i); + } + + @Override + public long getLong(int i) { + return byteBuf.getLong(i); + } + + @Override + public Bytes slice(int i, int length) { + int size = byteBuf.capacity(); + if (i == 0 && length == size) { + return this; + } + if (length == 0) { + return EMPTY; + } + + checkElementIndex(i, size); + checkLength(size, i, length); + + return new ByteBufWrappingBytes(byteBuf.slice(i, length)); + } + + @Override + public MutableBytes mutableCopy() { + return MutableBytes.fromByteBuf(byteBuf, 0, byteBuf.capacity()); + } + + @Override + public void appendTo(Buffer buffer) { + buffer.appendBuffer(Buffer.buffer(this.byteBuf)); + } + + @Override + public byte[] toArrayUnsafe() { + byte[] array = new byte[byteBuf.capacity()]; + byteBuf.getBytes(0, array); + return array; + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (byteBuf.getByte(i) & bytesArray[offset + i]); + } + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (byteBuf.getByte(i) | bytesArray[offset + i]); + } + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (byteBuf.getByte(i) ^ bytesArray[offset + i]); + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Bytes other)) { + return false; + } + + if (this.size() != other.size()) { + return false; + } + + for (int i = 0; i < size(); i++) { + if (byteBuf.getByte(i) != other.get(i)) { + return false; + } + } + + return true; + } + + @Override + protected int computeHashcode() { + int result = 1; + for (int i = 0; i < size(); i++) { + result = 31 * result + byteBuf.getByte(i); + } + return result; + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/ByteBufferWrappingBytes.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ByteBufferWrappingBytes.java new file mode 100644 index 000000000..530acdfd1 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ByteBufferWrappingBytes.java @@ -0,0 +1,144 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkArgument; +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkLength; + +import java.nio.ByteBuffer; + +class ByteBufferWrappingBytes extends Bytes { + + protected final ByteBuffer byteBuffer; + protected final int offset; + + ByteBufferWrappingBytes(ByteBuffer byteBuffer) { + this(byteBuffer, 0, byteBuffer.limit()); + } + + ByteBufferWrappingBytes(ByteBuffer byteBuffer, int offset, int length) { + super(length); + this.byteBuffer = byteBuffer; + this.offset = offset; + } + + @Override + public int getInt(int i) { + return byteBuffer.getInt(offset + i); + } + + @Override + public long getLong(int i) { + return byteBuffer.getLong(offset + i); + } + + @Override + public byte get(int i) { + return byteBuffer.get(offset + i); + } + + @Override + public Bytes slice(int i, int length) { + if (i == 0 && length == size()) { + return this; + } + if (length == 0) { + return Bytes.EMPTY; + } + + checkArgument(length >= 0, "Invalid negative length"); + int bufferLength = byteBuffer.capacity(); + if (bufferLength > 0) { + checkElementIndex(offset + i, bufferLength); + } + checkLength(bufferLength, offset + i, length); + + return new ByteBufferWrappingBytes(byteBuffer, offset + i, length); + } + + @Override + public MutableBytes mutableCopy() { + return MutableBytes.fromByteBuffer(byteBuffer, offset, size()); + } + + @Override + public void appendTo(ByteBuffer byteBuffer) { + byteBuffer.put(this.byteBuffer); + } + + private byte[] toArray() { + byte[] array = new byte[size()]; + for (int i = 0; i < size(); i++) { + array[i] = byteBuffer.get(i + offset); + } + return array; + } + + @Override + public byte[] toArrayUnsafe() { + if (!byteBuffer.hasArray()) { + return toArray(); + } + byte[] array = byteBuffer.array(); + if (byteBuffer.limit() != size() || byteBuffer.arrayOffset() != offset) { + return toArray(); + } + return array; + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + // TODO: There is a chance for implementing with SIMD - see toArrayUnsafe() + bytesArray[offset + i] = (byte) (byteBuffer.get(this.offset + i) & bytesArray[offset + i]); + } + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + // TODO: There is a chance for implementing with SIMD - see toArrayUnsafe() + bytesArray[offset + i] = (byte) (byteBuffer.get(this.offset + i) | bytesArray[offset + i]); + } + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + // TODO: There is a chance for implementing with SIMD - see toArrayUnsafe() + bytesArray[offset + i] = (byte) (byteBuffer.get(this.offset + i) ^ bytesArray[offset + i]); + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Bytes other)) { + return false; + } + + if (this.size() != other.size()) { + return false; + } + + for (int i = 0; i < size(); i++) { + if (byteBuffer.get(offset + i) != other.get(i)) { + return false; + } + } + + return true; + } + + @Override + protected int computeHashcode() { + int result = 1; + for (int i = 0; i < size(); i++) { + result = 31 * result + byteBuffer.get(offset + i); + } + return result; + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes.java new file mode 100644 index 000000000..9c8eeb7e7 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes.java @@ -0,0 +1,1349 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static java.lang.String.format; +import static java.nio.ByteOrder.BIG_ENDIAN; +import static org.apache.tuweni.v2.bytes.Utils.checkArgument; +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkLength; +import static org.apache.tuweni.v2.bytes.Utils.checkNotNull; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.math.BigInteger; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.ReadOnlyBufferException; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.List; +import java.util.Random; + +import io.netty.buffer.ByteBuf; +import io.vertx.core.buffer.Buffer; + +/** + * A value made of bytes. + * + *

This class makes no thread-safety guarantee, and a {@link Bytes} value is generally not thread + * safe. However, specific implementations may be thread-safe, e.g., MutableBytes. + */ +public abstract class Bytes implements Comparable { + + public static final String HEX_CODE_AS_STRING = "0123456789abcdef"; + + /** The empty value (with 0 bytes). */ + public static Bytes EMPTY = wrap(new byte[0]); + + protected Integer hashCode; + protected int size; + + protected Bytes(final int size) { + this.size = size; + } + + /** + * Wrap the provided byte array as a {@link Bytes} value. + * + *

Note that value is not copied and thus any future update to {@code value} will be reflected + * in the returned value. + * + * @param value The value to wrap. + * @return A {@link Bytes} value wrapping {@code value}. + */ + public static Bytes wrap(byte[] value) { + return wrap(value, 0, value.length); + } + + /** + * Wrap a slice of a byte array as a {@link Bytes} value. + * + *

Note that value is not copied and thus any future update to {@code value} within the slice + * will be reflected in the returned value. + * + * @param value The value to wrap. + * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned + * value. In other words, you will have {@code wrap(value, o, l).get(0) == value[o]}. + * @param length The length of the resulting value. + * @return A {@link Bytes} value that expose the bytes of {@code value} from {@code offset} + * (inclusive) to {@code offset + length} (exclusive). + * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >= + * value.length)}. + * @throws IllegalArgumentException if {@code length < 0 || offset + length > value.length}. + */ + public static Bytes wrap(byte[] value, int offset, int length) { + checkNotNull(value); + checkArgument(length >= 0, "Invalid negative length"); + if (value.length > 0) { + checkElementIndex(offset, value.length); + } + checkLength(value.length, offset, length); + return new ArrayWrappingBytes(value, offset, length); + } + + /** + * Wrap a list of other values into a concatenated view. + * + *

Note that the values are not copied and thus any future update to the values will be + * reflected in the returned value. + * + * @param values The values to wrap. + * @return A value representing a view over the concatenation of all {@code values}. + * @throws IllegalArgumentException if the result overflows an int. + */ + public static Bytes wrap(Bytes... values) { + return ConcatenatedBytes.create(values); + } + + /** + * Wrap a list of other values into a concatenated view. + * + *

Note that the values are not copied and thus any future update to the values will be + * reflected in the returned value. + * + * @param values The values to wrap. + * @return A value representing a view over the concatenation of all {@code values}. + * @throws IllegalArgumentException if the result overflows an int. + */ + public static Bytes wrap(List values) { + return ConcatenatedBytes.create(values); + } + + /** + * Wrap a full Vert.x {@link Buffer} as a {@link Bytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value. + * + * @param buffer The buffer to wrap. + * @return A {@link Bytes} value. + */ + public static Bytes wrapBuffer(Buffer buffer) { + checkNotNull(buffer); + if (buffer.length() == 0) { + return EMPTY; + } + return new BufferWrappingBytes(buffer); + } + + /** + * Wrap a slice of a Vert.x {@link Buffer} as a {@link Bytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value. + * + * @param buffer The buffer to wrap. + * @param offset The offset in {@code buffer} from which to expose the bytes in the returned + * value. That is, {@code wrapBuffer(buffer, i, 1).get(0) == buffer.getByte(i)}. + * @param size The size of the returned value. + * @return A {@link Bytes} value. + * @throws IndexOutOfBoundsException if {@code offset < 0 || (buffer.length() > 0 && offset >= + * buffer.length())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + length > buffer.length()}. + */ + public static Bytes wrapBuffer(Buffer buffer, int offset, int size) { + checkNotNull(buffer); + if (size == 0) { + return EMPTY; + } + checkArgument(size >= 0, "Invalid negative length"); + int bufferLength = buffer.length(); + checkElementIndex(offset, bufferLength); + checkLength(bufferLength, offset, size); + return new BufferWrappingBytes(buffer, offset, size); + } + + /** + * Wrap a full Netty {@link ByteBuf} as a {@link Bytes} value. + * + *

Note that any change to the content of the byteBuf may be reflected in the returned value. + * + * @param byteBuf The {@link ByteBuf} to wrap. + * @return A {@link Bytes} value. + */ + public static Bytes wrapByteBuf(ByteBuf byteBuf) { + checkNotNull(byteBuf); + if (byteBuf.capacity() == 0) { + return EMPTY; + } + return new ByteBufWrappingBytes(byteBuf); + } + + /** + * Wrap a slice of a Netty {@link ByteBuf} as a {@link Bytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value. + * + * @param byteBuf The {@link ByteBuf} to wrap. + * @param offset The offset in {@code byteBuf} from which to expose the bytes in the returned + * value. That is, {@code wrapByteBuf(byteBuf, i, 1).get(0) == byteBuf.getByte(i)}. + * @param size The size of the returned value. + * @return A {@link Bytes} value. + * @throws IndexOutOfBoundsException if {@code offset < 0 || (byteBuf.capacity() > 0 && offset >= + * byteBuf.capacity())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + length > byteBuf.capacity()}. + */ + public static Bytes wrapByteBuf(ByteBuf byteBuf, int offset, int size) { + checkNotNull(byteBuf); + if (size == 0) { + return EMPTY; + } + checkArgument(size >= 0, "Invalid negative length"); + int bufferLength = byteBuf.capacity(); + checkElementIndex(offset, bufferLength); + checkLength(bufferLength, offset, size); + + return new ByteBufWrappingBytes(byteBuf, offset, size); + } + + /** + * Wrap a full Java NIO {@link ByteBuffer} as a {@link Bytes} value. + * + *

Note that any change to the content of the byteBuf may be reflected in the returned value. + * + * @param byteBuffer The {@link ByteBuffer} to wrap. + * @return A {@link Bytes} value. + */ + public static Bytes wrapByteBuffer(ByteBuffer byteBuffer) { + checkNotNull(byteBuffer); + if (byteBuffer.limit() == 0) { + return EMPTY; + } + return new ByteBufferWrappingBytes(byteBuffer); + } + + /** + * Wrap a slice of a Java NIO {@link ByteBuf} as a {@link Bytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value. + * + * @param byteBuffer The {@link ByteBuffer} to wrap. + * @param offset The offset in {@code byteBuffer} from which to expose the bytes in the returned + * value. That is, {@code wrapByteBuffer(byteBuffer, i, 1).get(0) == byteBuffer.getByte(i)}. + * @param size The size of the returned value. + * @return A {@link Bytes} value. + * @throws IndexOutOfBoundsException if {@code offset < 0 || (byteBuffer.limit() > 0 && offset >= + * byteBuf.limit())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + length > byteBuffer.limit()}. + */ + public static Bytes wrapByteBuffer(ByteBuffer byteBuffer, int offset, int size) { + checkNotNull(byteBuffer); + if (size == 0) { + return EMPTY; + } + checkArgument(size >= 0, "Invalid negative length"); + int bufferLength = byteBuffer.capacity(); + if (bufferLength > 0) { + checkElementIndex(offset, bufferLength); + } + checkLength(bufferLength, offset, size); + return new ByteBufferWrappingBytes(byteBuffer, offset, size); + } + + /** + * Create a value that contains the specified bytes in their specified order. + * + * @param bytes The bytes that must compose the returned value. + * @return A value containing the specified bytes. + */ + public static Bytes of(byte... bytes) { + return wrap(bytes); + } + + /** + * Create a value that contains the specified bytes in their specified order. + * + * @param bytes The bytes. + * @return A value containing bytes are the one from {@code bytes}. + * @throws IllegalArgumentException if any of the specified would be truncated when storing as a + * byte. + */ + public static Bytes of(int... bytes) { + byte[] result = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i]; + checkArgument(b == (((byte) b) & 0xff), "%sth value %s does not fit a byte", i + 1, b); + result[i] = (byte) b; + } + return Bytes.wrap(result); + } + + /** + * Return a 2-byte value corresponding to the provided value interpreted as an unsigned short. + * + * @param value The value, which must be no larger than an unsigned short. + * @return A 2 bytes value corresponding to {@code value}. + * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an + * unsigned 2-byte short (that is, if {@code value >= (1 << 16)}). + */ + public static Bytes ofUnsignedShort(int value) { + return ofUnsignedShort(value, BIG_ENDIAN); + } + + /** + * Return a 2-byte value corresponding to the provided value interpreted as an unsigned short. + * + * @param value The value, which must be no larger than an unsigned short. + * @param order The byte-order for the integer encoding. + * @return A 2 bytes value corresponding to {@code value}. + * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an + * unsigned 2-byte short (that is, if {@code value >= (1 << 16)}). + */ + public static Bytes ofUnsignedShort(int value, ByteOrder order) { + checkArgument( + value >= 0 && value <= BytesValues.MAX_UNSIGNED_SHORT, + "Value %s cannot be represented as an unsigned short (it is negative or too big)", + value); + byte[] res = new byte[2]; + if (order == BIG_ENDIAN) { + res[0] = (byte) ((value >> 8) & 0xFF); + res[1] = (byte) (value & 0xFF); + } else { + res[0] = (byte) (value & 0xFF); + res[1] = (byte) ((value >> 8) & 0xFF); + } + return Bytes.wrap(res); + } + + /** + * Return a 4-byte value corresponding to the provided value interpreted as an unsigned int. + * + * @param value The value, which must be no larger than an unsigned int. + * @return A 4 bytes value corresponding to {@code value}. + * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an + * unsigned 4-byte int (that is, if {@code value >= (1L << 32)}). + */ + public static Bytes ofUnsignedInt(long value) { + return ofUnsignedInt(value, BIG_ENDIAN); + } + + /** + * Return a 4-byte value corresponding to the provided value interpreted as an unsigned int. + * + * @param value The value, which must be no larger than an unsigned int. + * @param order The byte-order for the integer encoding. + * @return A 4 bytes value corresponding to the encoded {@code value}. + * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an + * unsigned 4-byte int (that is, if {@code value >= (1L << 32)}). + */ + public static Bytes ofUnsignedInt(long value, ByteOrder order) { + checkArgument( + value >= 0 && value <= BytesValues.MAX_UNSIGNED_INT, + "Value %s cannot be represented as an unsigned int (it is negative or too big)", + value); + byte[] res = new byte[4]; + if (order == BIG_ENDIAN) { + res[0] = (byte) ((value >> 24) & 0xFF); + res[1] = (byte) ((value >> 16) & 0xFF); + res[2] = (byte) ((value >> 8) & 0xFF); + res[3] = (byte) ((value) & 0xFF); + } else { + res[0] = (byte) ((value) & 0xFF); + res[1] = (byte) ((value >> 8) & 0xFF); + res[2] = (byte) ((value >> 16) & 0xFF); + res[3] = (byte) ((value >> 24) & 0xFF); + } + return Bytes.wrap(res); + } + + /** + * Return an 8-byte value corresponding to the provided value interpreted as an unsigned long. + * + * @param value The value, which will be interpreted as an unsigned long. + * @return A 8 bytes value corresponding to {@code value}. + * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an + * unsigned 8-byte int (that is, if {@code value >= (1L << 64)}). + */ + public static Bytes ofUnsignedLong(long value) { + return ofUnsignedLong(value, BIG_ENDIAN); + } + + /** + * Return an 8-byte value corresponding to the provided value interpreted as an unsigned long. + * + * @param value The value, which will be interpreted as an unsigned long. + * @param order The byte-order for the integer encoding. + * @return A 8 bytes value corresponding to {@code value}. + * @throws IllegalArgumentException if {@code value < 0} or {@code value} is too big to fit an + * unsigned 8-byte int (that is, if {@code value >= (1L << 64)}). + */ + public static Bytes ofUnsignedLong(long value, ByteOrder order) { + byte[] res = new byte[8]; + if (order == BIG_ENDIAN) { + res[0] = (byte) ((value >> 56) & 0xFF); + res[1] = (byte) ((value >> 48) & 0xFF); + res[2] = (byte) ((value >> 40) & 0xFF); + res[3] = (byte) ((value >> 32) & 0xFF); + res[4] = (byte) ((value >> 24) & 0xFF); + res[5] = (byte) ((value >> 16) & 0xFF); + res[6] = (byte) ((value >> 8) & 0xFF); + res[7] = (byte) (value & 0xFF); + } else { + res[0] = (byte) ((value) & 0xFF); + res[1] = (byte) ((value >> 8) & 0xFF); + res[2] = (byte) ((value >> 16) & 0xFF); + res[3] = (byte) ((value >> 24) & 0xFF); + res[4] = (byte) ((value >> 32) & 0xFF); + res[5] = (byte) ((value >> 40) & 0xFF); + res[6] = (byte) ((value >> 48) & 0xFF); + res[7] = (byte) ((value >> 56) & 0xFF); + } + return Bytes.wrap(res); + } + + /** + * Return the smallest bytes value whose bytes correspond to the provided long. That is, the + * returned value may be of size less than 8 if the provided long has leading zero bytes. + * + * @param value The long from which to create the bytes value. + * @return The minimal bytes representation corresponding to {@code l}. + */ + public static Bytes minimalBytes(long value) { + if (value == 0) { + return Bytes.EMPTY; + } + + int zeros = Long.numberOfLeadingZeros(value); + int resultBytes = 8 - (zeros / 8); + + byte[] result = new byte[resultBytes]; + int shift = 0; + for (int i = 0; i < resultBytes; i++) { + result[resultBytes - i - 1] = (byte) ((value >> shift) & 0xFF); + shift += 8; + } + return Bytes.wrap(result); + } + + /** + * Parse a hexadecimal string into a {@link Bytes} value. + * + *

This method is lenient in that {@code str} may of an odd length, in which case it will + * behave exactly as if it had an additional 0 in front. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation. + */ + public static Bytes fromHexStringLenient(CharSequence str) { + checkNotNull(str); + return BytesValues.fromHexString(str, -1, true); + } + + /** + * Parse a hexadecimal string into a {@link Bytes} value of the provided size. + * + *

This method allows for {@code str} to have an odd length, in which case it will behave + * exactly as if it had an additional 0 in front. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". + * @param destinationSize The size of the returned value, which must be big enough to hold the + * bytes represented by {@code str}. If it is strictly bigger those bytes from {@code str}, + * the returned value will be left padded with zeros. + * @return A value of size {@code destinationSize} corresponding to {@code str} potentially + * left-padded. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation, represents more bytes than {@code destinationSize} or {@code + * destinationSize < 0}. + */ + public static Bytes fromHexStringLenient(CharSequence str, int destinationSize) { + checkNotNull(str); + checkArgument(destinationSize >= 0, "Invalid negative destination size %s", destinationSize); + return BytesValues.fromHexString(str, destinationSize, true); + } + + /** + * Parse a hexadecimal string into a {@link Bytes} value. + * + *

This method requires that {@code str} have an even length. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation, or is of an odd length. + */ + public static Bytes fromHexString(CharSequence str) { + checkNotNull(str); + return BytesValues.fromHexString(str, -1, false); + } + + /** + * Parse a hexadecimal string into a {@link Bytes} value. + * + *

This method requires that {@code str} have an even length. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". + * @param destinationSize The size of the returned value, which must be big enough to hold the + * bytes represented by {@code str}. If it is strictly bigger those bytes from {@code str}, + * the returned value will be left padded with zeros. + * @return A value of size {@code destinationSize} corresponding to {@code str} potentially + * left-padded. + * @throws IllegalArgumentException if {@code str} does correspond to a valid hexadecimal + * representation, or is of an odd length. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation, or is of an odd length, or represents more bytes than {@code + * destinationSize} or {@code destinationSize < 0}. + */ + public static Bytes fromHexString(CharSequence str, int destinationSize) { + checkNotNull(str); + checkArgument(destinationSize >= 0, "Invalid negative destination size %s", destinationSize); + return BytesValues.fromHexString(str, destinationSize, false); + } + + /** + * Parse a base 64 string into a {@link Bytes} value. + * + * @param str The base 64 string to parse. + * @return The value corresponding to {@code str}. + */ + public static Bytes fromBase64String(CharSequence str) { + return Bytes.wrap(Base64.getDecoder().decode(str.toString())); + } + + /** + * Generate random bytes. + * + * @param size The number of bytes to generate. + * @return A value containing the desired number of random bytes. + */ + public static Bytes random(int size) { + return random(size, new SecureRandom()); + } + + /** + * Generate random bytes. + * + * @param size The number of bytes to generate. + * @param generator The generator for random bytes. + * @return A value containing the desired number of random bytes. + */ + public static Bytes random(int size, Random generator) { + byte[] array = new byte[size]; + generator.nextBytes(array); + return Bytes.wrap(array); + } + + /** + * Generate a bytes object filled with the same byte. + * + * @param b the byte to fill the Bytes with + * @param size the size of the object + * @return a value filled with a fixed byte + */ + public static Bytes repeat(byte b, int size) { + return new ConstantBytesValue(b, size); + } + + /** + * Provides the number of bytes this value represents. + * + * @return The number of bytes this value represents. + */ + public int size() { + return size; + } + + /** + * Retrieve a byte in this value. + * + * @param i The index of the byte to fetch within the value (0-indexed). + * @return The byte at index {@code i} in this value. + * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()}. + */ + public abstract byte get(int i); + + /** + * Retrieve the 4 bytes starting at the provided index in this value as an integer. + * + * @param i The index from which to get the int, which must less than or equal to {@code size() - + * 4}. + * @return An integer whose value is the 4 bytes from this value starting at index {@code i}. + * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 4}. + */ + public int getInt(int i) { + return getInt(i, BIG_ENDIAN); + } + + /** + * Retrieve the 4 bytes starting at the provided index in this value as an integer. + * + * @param i The index from which to get the int, which must less than or equal to {@code size() - + * 4}. + * @param order The byte-order for decoding the integer. + * @return An integer whose value is the 4 bytes from this value starting at index {@code i}. + * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 4}. + */ + public int getInt(int i, ByteOrder order) { + int size = size(); + checkElementIndex(i, size); + if (i > (size - 4)) { + throw new IndexOutOfBoundsException( + format( + "Value of size %s has not enough bytes to read a 4 bytes int from index %s", + size, i)); + } + + int value = 0; + if (order == BIG_ENDIAN) { + value |= ((int) get(i) & 0xFF) << 24; + value |= ((int) get(i + 1) & 0xFF) << 16; + value |= ((int) get(i + 2) & 0xFF) << 8; + value |= ((int) get(i + 3) & 0xFF); + } else { + value |= ((int) get(i + 3) & 0xFF) << 24; + value |= ((int) get(i + 2) & 0xFF) << 16; + value |= ((int) get(i + 1) & 0xFF) << 8; + value |= ((int) get(i) & 0xFF); + } + return value; + } + + /** + * The value corresponding to interpreting these bytes as an integer. + * + * @return An value corresponding to this value interpreted as an integer. + * @throws IllegalArgumentException if {@code size() > 4}. + */ + public int toInt() { + return toInt(BIG_ENDIAN); + } + + /** + * The value corresponding to interpreting these bytes as an integer. + * + * @param order The byte-order for decoding the integer. + * @return An value corresponding to this value interpreted as an integer. + * @throws IllegalArgumentException if {@code size() > 4}. + */ + public int toInt(ByteOrder order) { + int size = size(); + checkArgument(size <= 4, "Value of size %s has more than 4 bytes", size()); + if (size == 0) { + return 0; + } + if (order == BIG_ENDIAN) { + int i = size; + int value = ((int) get(--i) & 0xFF); + if (i == 0) { + return value; + } + value |= ((int) get(--i) & 0xFF) << 8; + if (i == 0) { + return value; + } + value |= ((int) get(--i) & 0xFF) << 16; + if (i == 0) { + return value; + } + return value | ((int) get(--i) & 0xFF) << 24; + } else { + int i = 0; + int value = ((int) get(i) & 0xFF); + if (++i == size) { + return value; + } + value |= ((int) get(i++) & 0xFF) << 8; + if (i == size) { + return value; + } + value |= ((int) get(i++) & 0xFF) << 16; + if (i == size) { + return value; + } + return value | ((int) get(i) & 0xFF) << 24; + } + } + + /** + * Whether this value contains no bytes. + * + * @return true if the value contains no bytes + */ + public boolean isEmpty() { + return size() == 0; + } + + /** + * Retrieves the 8 bytes starting at the provided index in this value as a long. + * + * @param i The index from which to get the long, which must less than or equal to {@code size() - + * 8}. + * @return A long whose value is the 8 bytes from this value starting at index {@code i}. + * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 8}. + */ + public long getLong(int i) { + return getLong(i, BIG_ENDIAN); + } + + /** + * Retrieves the 8 bytes starting at the provided index in this value as a long. + * + * @param i The index from which to get the long, which must less than or equal to {@code size() - + * 8}. + * @param order The byte-order for decoding the integer. + * @return A long whose value is the 8 bytes from this value starting at index {@code i}. + * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i > size() - 8}. + */ + public long getLong(int i, ByteOrder order) { + int size = size(); + checkElementIndex(i, size); + if (i > (size - 8)) { + throw new IndexOutOfBoundsException( + format( + "Value of size %s has not enough bytes to read a 8 bytes long from index %s", + size, i)); + } + + long value = 0; + if (order == BIG_ENDIAN) { + value |= ((long) get(i) & 0xFF) << 56; + value |= ((long) get(i + 1) & 0xFF) << 48; + value |= ((long) get(i + 2) & 0xFF) << 40; + value |= ((long) get(i + 3) & 0xFF) << 32; + value |= ((long) get(i + 4) & 0xFF) << 24; + value |= ((long) get(i + 5) & 0xFF) << 16; + value |= ((long) get(i + 6) & 0xFF) << 8; + value |= ((long) get(i + 7) & 0xFF); + } else { + value |= ((long) get(i + 7) & 0xFF) << 56; + value |= ((long) get(i + 6) & 0xFF) << 48; + value |= ((long) get(i + 5) & 0xFF) << 40; + value |= ((long) get(i + 4) & 0xFF) << 32; + value |= ((long) get(i + 3) & 0xFF) << 24; + value |= ((long) get(i + 2) & 0xFF) << 16; + value |= ((long) get(i + 1) & 0xFF) << 8; + value |= ((long) get(i) & 0xFF); + } + return value; + } + + /** + * The value corresponding to interpreting these bytes as a long. + * + * @return An value corresponding to this value interpreted as a long. + * @throws IllegalArgumentException if {@code size() > 8}. + */ + public long toLong() { + return toLong(BIG_ENDIAN); + } + + /** + * The value corresponding to interpreting these bytes as a long. + * + * @param order The byte-order for decoding the integer. + * @return An value corresponding to this value interpreted as a long. + * @throws IllegalArgumentException if {@code size() > 8}. + */ + public long toLong(ByteOrder order) { + int size = size(); + checkArgument(size <= 8, "Value of size %s has more than 8 bytes", size()); + if (size == 0) { + return 0; + } + if (order == BIG_ENDIAN) { + int i = size; + long value = ((long) get(--i) & 0xFF); + if (i == 0) { + return value; + } + value |= ((long) get(--i) & 0xFF) << 8; + if (i == 0) { + return value; + } + value |= ((long) get(--i) & 0xFF) << 16; + if (i == 0) { + return value; + } + value |= ((long) get(--i) & 0xFF) << 24; + if (i == 0) { + return value; + } + value |= ((long) get(--i) & 0xFF) << 32; + if (i == 0) { + return value; + } + value |= ((long) get(--i) & 0xFF) << 40; + if (i == 0) { + return value; + } + value |= ((long) get(--i) & 0xFF) << 48; + if (i == 0) { + return value; + } + return value | ((long) get(--i) & 0xFF) << 56; + } else { + int i = 0; + long value = ((long) get(i) & 0xFF); + if (++i == size) { + return value; + } + value |= ((long) get(i) & 0xFF) << 8; + if (++i == size) { + return value; + } + value |= ((long) get(i) & 0xFF) << 16; + if (++i == size) { + return value; + } + value |= ((long) get(i) & 0xFF) << 24; + if (++i == size) { + return value; + } + value |= ((long) get(i) & 0xFF) << 32; + if (++i == size) { + return value; + } + value |= ((long) get(i) & 0xFF) << 40; + if (++i == size) { + return value; + } + value |= ((long) get(i) & 0xFF) << 48; + if (++i == size) { + return value; + } + return value | ((long) get(i) & 0xFF) << 56; + } + } + + /** + * The BigInteger corresponding to interpreting these bytes as a two's-complement signed integer. + * + * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement + * signed integer. + */ + public BigInteger toSignedBigInteger() { + return toSignedBigInteger(BIG_ENDIAN); + } + + /** + * The BigInteger corresponding to interpreting these bytes as a two's-complement signed integer. + * + * @param order The byte-order for decoding the integer. + * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement + * signed integer. + */ + public BigInteger toSignedBigInteger(ByteOrder order) { + if (size() == 0) { + return BigInteger.ZERO; + } + return new BigInteger( + (order == BIG_ENDIAN) ? toArrayUnsafe() : mutableCopy().reverse().toArray()); + } + + /** + * The BigInteger corresponding to interpreting these bytes as a two's-complement signed integer. + * + * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement + * signed integer. + */ + public BigInteger toBigInteger() { + return toBigInteger(BIG_ENDIAN); + } + + /** + * The BigInteger corresponding to interpreting these bytes as a two's-complement signed integer. + * + * @param order The byte-order for decoding the integer. + * @return A {@link BigInteger} corresponding to interpreting these bytes as a two's-complement + * signed integer. + */ + public BigInteger toBigInteger(ByteOrder order) { + if (size() == 0) { + return BigInteger.ZERO; + } + return new BigInteger( + (order == BIG_ENDIAN) ? toArrayUnsafe() : mutableCopy().reverse().toArray()); + } + + /** + * The BigInteger corresponding to interpreting these bytes as an unsigned integer. + * + * @return A positive (or zero) {@link BigInteger} corresponding to interpreting these bytes as an + * unsigned integer. + */ + public BigInteger toUnsignedBigInteger() { + return toUnsignedBigInteger(BIG_ENDIAN); + } + + /** + * The BigInteger corresponding to interpreting these bytes as an unsigned integer. + * + * @param order The byte-order for decoding the integer. + * @return A positive (or zero) {@link BigInteger} corresponding to interpreting these bytes as an + * unsigned integer. + */ + public BigInteger toUnsignedBigInteger(ByteOrder order) { + if (size() == 0) { + return BigInteger.ZERO; + } + return new BigInteger( + 1, (order == BIG_ENDIAN) ? toArrayUnsafe() : mutableCopy().reverse().toArray()); + } + + /** + * Whether this value has only zero bytes. + * + * @return {@code true} if all the bits of this value are zeros. + */ + public boolean isZero() { + for (int i = size() - 1; i >= 0; --i) { + if (get(i) != 0) return false; + } + return true; + } + + /** + * Whether the bytes start with a zero bit value. + * + * @return true if the first bit equals zero + */ + public boolean hasLeadingZero() { + return size() > 0 && (get(0) & 0x80) == 0; + } + + /** + * Provides the number of zero bits preceding the highest-order ("leftmost") one-bit, or {@code + * size() * 8} if all bits * are zero. + * + * @return The number of zero bits preceding the highest-order ("leftmost") one-bit, or {@code + * size() * 8} if all bits are zero. + */ + public int numberOfLeadingZeros() { + int size = size(); + for (int i = 0; i < size; i++) { + byte b = get(i); + if (b == 0) { + continue; + } + + return (i * 8) + Integer.numberOfLeadingZeros(b & 0xFF) - 3 * 8; + } + return size * 8; + } + + /** + * Whether the bytes start with a zero byte value. + * + * @return true if the first byte equals zero + */ + public boolean hasLeadingZeroByte() { + return size() > 0 && get(0) == 0; + } + + /** + * Provides the number of leading zero bytes of the value + * + * @return The number of leading zero bytes of the value. + */ + public int numberOfLeadingZeroBytes() { + int size = size(); + for (int i = 0; i < size; i++) { + if (get(i) != 0) { + return i; + } + } + return size; + } + + /** + * Provides the number of trailing zero bytes of the value. + * + * @return The number of trailing zero bytes of the value. + */ + public int numberOfTrailingZeroBytes() { + int size = size(); + for (int i = size; i >= 1; i--) { + if (get(i - 1) != 0) { + return size - i; + } + } + return size; + } + + /** + * Provides the number of bits following and including the highest-order ("leftmost") one-bit, or + * zero if all bits are zero. + * + * @return The number of bits following and including the highest-order ("leftmost") one-bit, or + * zero if all bits are zero. + */ + public int bitLength() { + int size = size(); + for (int i = 0; i < size; i++) { + byte b = get(i); + if (b == 0) continue; + + return (size * 8) - (i * 8) - (Integer.numberOfLeadingZeros(b & 0xFF) - 3 * 8); + } + return 0; + } + + /** + * Create a new value representing (a view of) a slice of the bytes of this value. + * + *

Please note that the resulting slice is only a view and as such maintains a link to the + * underlying full value. So holding a reference to the returned slice may hold more memory than + * the slice represents. + * + * @param i The start index for the slice. + * @return A new value providing a view over the bytes from index {@code i} (included) to the end. + * @throws IndexOutOfBoundsException if {@code i < 0}. + */ + public Bytes slice(int i) { + if (i == 0) { + return this; + } + int size = size(); + if (i >= size) { + return EMPTY; + } + return slice(i, size - i); + } + + /** + * Create a new value representing (a view of) a slice of the bytes of this value. + * + *

Please note that the resulting slice is only a view and as such maintains a link to the + * underlying full value. So holding a reference to the returned slice may hold more memory than + * the slide represents. + * + * @param i The start index for the slice. + * @param length The length of the resulting value. + * @return A new value providing a view over the bytes from index {@code i} (included) to {@code i + * + length} (excluded). + * @throws IllegalArgumentException if {@code length < 0}. + * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()} or {i + length > size()} . + */ + public abstract Bytes slice(int i, int length); + + /** + * Return a new mutable value initialized with the content of this value. + * + * @return A mutable copy of this value. This will copy bytes, modifying the returned value will + * not modify this value. + */ + public abstract MutableBytes mutableCopy(); + + /** + * Append the bytes of this value to the {@link ByteBuffer}. + * + * @param byteBuffer The {@link ByteBuffer} to which to append this value. + * @throws BufferOverflowException If the writer attempts to write more than the provided buffer + * can hold. + * @throws ReadOnlyBufferException If the provided buffer is read-only. + */ + public void appendTo(ByteBuffer byteBuffer) { + checkNotNull(byteBuffer); + for (int i = 0; i < size(); i++) { + byteBuffer.put(get(i)); + } + } + + /** + * Append the bytes of this value to the provided Vert.x {@link Buffer}. + * + *

Note that since a Vert.x {@link Buffer} will grow as necessary, this method never fails. + * + * @param buffer The {@link Buffer} to which to append this value. + */ + public void appendTo(Buffer buffer) { + checkNotNull(buffer); + for (int i = 0; i < size(); i++) { + buffer.appendByte(get(i)); + } + } + + /** + * Append this value as a sequence of hexadecimal characters. + * + * @param appendable The appendable + * @param The appendable type. + * @return The appendable. + */ + public T appendHexTo(T appendable) { + try { + appendable.append(toFastHex(false)); + return appendable; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public String toFastHex(boolean prefix) { + + int offset = prefix ? 2 : 0; + + int resultSize = (size() * 2) + offset; + + char[] result = new char[resultSize]; + + if (prefix) { + result[0] = '0'; + result[1] = 'x'; + } + + for (int i = 0; i < size(); i++) { + byte b = get(i); + int pos = i * 2; + result[pos + offset] = Bytes.HEX_CODE_AS_STRING.charAt(b >> 4 & 15); + result[pos + offset + 1] = Bytes.HEX_CODE_AS_STRING.charAt(b & 15); + } + + return new String(result); + } + + /** + * Return the number of bytes in common between this set of bytes and another. + * + * @param other The bytes to compare to. + * @return The number of common bytes. + */ + public int commonPrefixLength(Bytes other) { + checkNotNull(other); + int ourSize = size(); + int otherSize = other.size(); + int i = 0; + while (i < ourSize && i < otherSize && get(i) == other.get(i)) { + i++; + } + return i; + } + + /** + * Return a slice over the common prefix between this set of bytes and another. + * + * @param other The bytes to compare to. + * @return A slice covering the common prefix. + */ + public Bytes commonPrefix(Bytes other) { + return slice(0, commonPrefixLength(other)); + } + + /** + * Return a slice of representing the same value but without any leading zero bytes. + * + * @return {@code value} if its left-most byte is non zero, or a slice that exclude any leading + * zero bytes. + */ + public Bytes trimLeadingZeros() { + int size = size(); + for (int i = 0; i < size; i++) { + if (get(i) != 0) { + return slice(i); + } + } + return Bytes.EMPTY; + } + + /** + * Return a slice of representing the same value but without any trailing zero bytes. + * + * @return {@code value} if its right-most byte is non zero, or a slice that exclude any trailing + * zero bytes. + */ + public Bytes trimTrailingZeros() { + int size = size(); + for (int i = size - 1; i >= 0; i--) { + if (get(i) != 0) { + return slice(0, i + 1); + } + } + return Bytes.EMPTY; + } + + /** + * Update the provided message digest with the bytes of this value. + * + * @param digest The digest to update. + */ + public void update(MessageDigest digest) { + checkNotNull(digest); + digest.update(toArrayUnsafe()); + } + + /** + * Get the bytes represented by this value as byte array. + * + *

This may avoid allocating a new array and directly return the backing array of this value if + * said value is array backed and doing so is possible. As such, modifications to the returned + * array may or may not impact this value. As such, this method should be used with care and hence + * the "unsafe" moniker. + * + * @return A byte array with the same content than this value, which may or may not be the direct + * backing of this value. + */ + public abstract byte[] toArrayUnsafe(); + + /** + * Provides this value represented as hexadecimal, starting with "0x". + * + * @return This value represented as hexadecimal, starting with "0x". + */ + public String toHexString() { + return toFastHex(true); + } + + /** + * Provides this value represented as hexadecimal, with no prefix + * + * @return This value represented as hexadecimal, with no prefix. + */ + public String toUnprefixedHexString() { + return toFastHex(false); + } + + public String toEllipsisHexString() { + int size = size(); + if (size < 6) { + return toHexString(); + } + char[] result = new char[12]; + result[0] = '0'; + result[1] = 'x'; + for (int i = 0; i < 2; i++) { + byte b = get(i); + int pos = (i * 2) + 2; + result[pos] = Bytes.HEX_CODE_AS_STRING.charAt(b >> 4 & 15); + result[pos + 1] = Bytes.HEX_CODE_AS_STRING.charAt(b & 15); + } + result[6] = '.'; + result[7] = '.'; + for (int i = 0; i < 2; i++) { + byte b = get(i + size - 2); + int pos = (i * 2) + 8; + result[pos] = Bytes.HEX_CODE_AS_STRING.charAt(b >> 4 & 15); + result[pos + 1] = Bytes.HEX_CODE_AS_STRING.charAt(b & 15); + } + return new String(result); + } + + /** + * Provides this value represented as a minimal hexadecimal string (without any leading zero) + * + * @return This value represented as a minimal hexadecimal string (without any leading zero). + */ + public String toShortHexString() { + String hex = toFastHex(false); + + int i = 0; + while (i < hex.length() && hex.charAt(i) == '0') { + i++; + } + return "0x" + hex.substring(i); + } + + /** + * Provides this value represented as a minimal hexadecimal string (without any leading zero, + * except if it's valued zero or empty, in which case it returns 0x0). + * + * @return This value represented as a minimal hexadecimal string (without any leading zero, + * except if it's valued zero or empty, in which case it returns 0x0). + */ + public String toQuantityHexString() { + if (Bytes.EMPTY.equals(this)) { + return "0x0"; + } + String hex = toFastHex(false); + + int i = 0; + while (i < hex.length() - 1 && hex.charAt(i) == '0') { + i++; + } + return "0x" + hex.substring(i); + } + + /** + * Provides this value represented as base 64 + * + * @return This value represented as base 64. + */ + public String toBase64String() { + return Base64.getEncoder().encodeToString(toArrayUnsafe()); + } + + @Override + public int compareTo(Bytes b) { + checkNotNull(b); + + int bitLength = bitLength(); + int sizeCmp = Integer.compare(bitLength, b.bitLength()); + if (sizeCmp != 0) { + return sizeCmp; + } + // same bitlength and is zeroes only, return 0. + if (bitLength == 0) { + return 0; + } + + for (int i = 0; i < size(); i++) { + int cmp = Integer.compare(get(i) & 0xff, b.get(i) & 0xff); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + /** + * Compare this value and the provided one for equality. + * + *

Two {@link Bytes} values are equal is they have contain the exact same bytes. + * + * @param obj The object to test for equality with. + * @return {@code true} if this value and {@code obj} are equal. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Bytes other)) { + return false; + } + + if (this.size() != other.size()) { + return false; + } + + for (int i = 0; i < size(); i++) { + if (this.get(i) != other.get(i)) { + return false; + } + } + + return true; + } + + protected int computeHashcode() { + int result = 1; + for (int i = 0; i < size(); i++) { + result = 31 * result + get(i); + } + return result; + } + + @Override + public int hashCode() { + if (this.hashCode == null) { + this.hashCode = computeHashcode(); + } + return this.hashCode; + } + + @Override + public String toString() { + return toHexString(); + } + + Bytes getImpl() { + return this; + } + + protected abstract void and(byte[] bytesArray, int offset, int length); + + protected abstract void or(byte[] bytesArray, int offset, int length); + + protected abstract void xor(byte[] bytesArray, int offset, int length); +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes32.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes32.java new file mode 100644 index 000000000..6a46f983a --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes32.java @@ -0,0 +1,235 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkArgument; +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkNotNull; + +import java.security.SecureRandom; +import java.util.Random; + +/** A {@link Bytes} value that is guaranteed to contain exactly 32 bytes. */ +public final class Bytes32 extends DelegatingBytes { + private static final int SIZE = 32; + + /** A {@code Bytes32} containing all zero bytes */ + public static final Bytes ZERO = fromByte((byte) 0); + + private Bytes32(Bytes delegate) { + super(delegate, SIZE); + } + + /** + * Generate a bytes object filled with the same byte. + * + * @param b the byte to fill the Bytes with + * @return a value filled with a fixed byte + */ + public static Bytes32 wrap(byte b) { + return new Bytes32(fromByte(b)); + } + + public static Bytes fromByte(byte b) { + return repeat(b, SIZE); + } + + /** + * Wrap the provided byte array, which must be of length 32, as a {@link Bytes32}. + * + *

Note that value is not copied, only wrapped, and thus any future update to {@code value} + * will be reflected in the returned value. + * + * @param bytes The bytes to wrap. + * @return A {@link Bytes32} wrapping {@code value}. + * @throws IllegalArgumentException if {@code value.length != 32}. + */ + public static Bytes32 wrap(byte[] bytes) { + return wrap(bytes, 0); + } + + public static Bytes fromArray(byte[] bytes) { + return fromArray(bytes, 0); + } + + /** + * Wrap a slice/sub-part of the provided array as a {@link Bytes32}. + * + *

Note that value is not copied, only wrapped, and thus any future update to {@code value} + * within the wrapped parts will be reflected in the returned value. + * + * @param bytes The bytes to wrap. + * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned + * value. In other words, you will have {@code wrap(value, i).get(0) == value[i]}. + * @return A {@link Bytes32} that exposes the bytes of {@code value} from {@code offset} + * (inclusive) to {@code offset + 32} (exclusive). + * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >= + * value.length)}. + * @throws IllegalArgumentException if {@code value.length - offset != 32}. + */ + public static Bytes32 wrap(byte[] bytes, int offset) { + return new Bytes32(fromArray(bytes, offset)); + } + + public static Bytes fromArray(byte[] bytes, int offset) { + checkNotNull(bytes); + if (bytes.length == 0) { + return EMPTY; + } + checkElementIndex(offset, bytes.length); + checkLength(bytes, offset); + return new ArrayWrappingBytes(bytes, offset, SIZE); + } + + /** + * Wrap a the provided value, which must be of size 32, as a {@link Bytes32}. + * + *

Note that value is not copied, only wrapped, and thus any future update to {@code value} + * will be reflected in the returned value. + * + * @param value The bytes to wrap. + * @return A {@link Bytes32} that exposes the bytes of {@code value}. + * @throws IllegalArgumentException if {@code value.size() != 32}. + */ + public static Bytes32 wrap(Bytes value) { + checkNotNull(value); + checkArgument(value.size() == SIZE, "Expected %s bytes but got %s", SIZE, value.size()); + if (value instanceof Bytes32 bytes32) { + return bytes32; + } + return new Bytes32(value.getImpl()); + } + + /** + * Wrap a slice/sub-part of the provided value as a {@link Bytes32}. + * + *

Note that value is not copied, only wrapped, and thus any future update to {@code value} + * within the wrapped parts will be reflected in the returned value. + * + * @param value The bytes to wrap. + * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned + * value. In other words, you will have {@code wrap(value, i).get(0) == value.get(i)}. + * @return A {@link Bytes32} that exposes the bytes of {@code value} from {@code offset} + * (inclusive) to {@code offset + 32} (exclusive). + * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.size() > 0 && offset >= + * value.size())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + 32 > value.size()}. + */ + public static Bytes32 wrap(Bytes value, int offset) { + checkNotNull(value); + Bytes slice = value.slice(offset, SIZE); + if (slice instanceof Bytes32 bytes32) { + return bytes32; + } + checkArgument(slice.size() == SIZE, "Expected %s bytes but got %s", SIZE, slice.size()); + return new Bytes32(slice.getImpl()); + } + + public static Bytes fromBytes(Bytes value, int offset) { + checkNotNull(value); + Bytes slice = value.slice(offset, SIZE); + checkArgument(slice.size() == SIZE, "Expected %s bytes but got %s", SIZE, slice.size()); + return slice.getImpl(); + } + + /** + * Parse a hexadecimal string into a {@link Bytes32}. + * + *

This method is lenient in that {@code str} may of an odd length, in which case it will + * behave exactly as if it had an additional 0 in front. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". That + * representation may contain less than 32 bytes, in which case the result is left padded with + * zeros (see {@link #fromHexStringStrict} if this is not what you want). + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation or contains more than 32 bytes. + */ + public static Bytes32 wrapHexStringLenient(CharSequence str) { + return new Bytes32(fromHexStringLenient(str)); + } + + public static Bytes fromHexStringLenient(CharSequence str) { + checkNotNull(str); + return fromArray(BytesValues.fromRawHexString(str, SIZE, true)); + } + + /** + * Parse a hexadecimal string into a {@link Bytes32}. + * + *

This method is strict in that {@code str} must of an even length. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". That + * representation may contain less than 32 bytes, in which case the result is left padded with + * zeros (see {@link #fromHexStringStrict} if this is not what you want). + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation, is of an odd length, or contains more than 32 bytes. + */ + public static Bytes32 wrapHexString(CharSequence str) { + return new Bytes32(fromHexString(str)); + } + + public static Bytes fromHexString(CharSequence str) { + checkNotNull(str); + return fromArray(BytesValues.fromRawHexString(str, SIZE, false)); + } + + /** + * Generate random bytes. + * + * @return A value containing random bytes. + */ + public static Bytes32 wrapRandom() { + return new Bytes32(fromRandom()); + } + + public static Bytes fromRandom() { + return fromRandom(new SecureRandom()); + } + + /** + * Generate random bytes. + * + * @param generator The generator for random bytes. + * @return A value containing random bytes. + */ + public static Bytes32 wrapRandom(Random generator) { + return new Bytes32(fromRandom(generator)); + } + + public static Bytes fromRandom(Random generator) { + byte[] array = new byte[32]; + generator.nextBytes(array); + return fromArray(array); + } + + /** + * Parse a hexadecimal string into a {@link Bytes32}. + * + *

This method is extra strict in that {@code str} must of an even length and the provided + * representation must have exactly 32 bytes. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation, is of an odd length or does not contain exactly 32 bytes. + */ + public static Bytes32 wrapHexStringStrict(CharSequence str) { + return new Bytes32(fromHexStringStrict(str)); + } + + public static Bytes fromHexStringStrict(CharSequence str) { + checkNotNull(str); + return fromArray(BytesValues.fromRawHexString(str, -1, false)); + } + + private static void checkLength(byte[] bytes, int offset) { + Utils.checkArgument( + bytes.length - offset == SIZE, + "Expected %s bytes from offset %s but got %s", + SIZE, + offset, + bytes.length - offset); + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes48.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes48.java new file mode 100644 index 000000000..2a7453c6e --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes48.java @@ -0,0 +1,236 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkArgument; +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkNotNull; + +import java.security.SecureRandom; +import java.util.Random; + +/** A {@link Bytes} value that is guaranteed to contain exactly 48 bytes. */ +public final class Bytes48 extends DelegatingBytes { + /** The number of bytes in this value - i.e. 48 */ + public static final int SIZE = 48; + + /** A {@code Bytes48} containing all zero bytes */ + public static final Bytes ZERO = fromByte((byte) 0); + + private Bytes48(Bytes delegate) { + super(delegate, SIZE); + } + + /** + * Generate a bytes object filled with the same byte. + * + * @param b the byte to fill the Bytes with + * @return a value filled with a fixed byte + */ + public static Bytes48 wrap(byte b) { + return new Bytes48(fromByte(b)); + } + + public static Bytes fromByte(byte b) { + return repeat(b, SIZE); + } + + /** + * Wrap the provided byte array, which must be of length 48, as a {@link Bytes48}. + * + *

Note that value is not copied, only wrapped, and thus any future update to {@code value} + * will be reflected in the returned value. + * + * @param bytes The bytes to wrap. + * @return A {@link Bytes48} wrapping {@code value}. + * @throws IllegalArgumentException if {@code value.length != 48}. + */ + public static Bytes48 wrap(byte[] bytes) { + return wrap(bytes, 0); + } + + public static Bytes fromArray(byte[] bytes) { + return fromArray(bytes, 0); + } + + /** + * Wrap a slice/sub-part of the provided array as a {@link Bytes48}. + * + *

Note that value is not copied, only wrapped, and thus any future update to {@code value} + * within the wrapped parts will be reflected in the returned value. + * + * @param bytes The bytes to wrap. + * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned + * value. In other words, you will have {@code wrap(value, i).get(0) == value[i]}. + * @return A {@link Bytes48} that exposes the bytes of {@code value} from {@code offset} + * (inclusive) to {@code offset + 48} (exclusive). + * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >= + * value.length)}. + * @throws IllegalArgumentException if {@code value.length - offset != 48}. + */ + public static Bytes48 wrap(byte[] bytes, int offset) { + return new Bytes48(fromArray(bytes, offset)); + } + + public static Bytes fromArray(byte[] bytes, int offset) { + checkNotNull(bytes); + if (bytes.length == 0) { + return EMPTY; + } + checkElementIndex(offset, bytes.length); + checkLength(bytes, offset); + return new ArrayWrappingBytes(bytes, offset, SIZE); + } + + /** + * Wrap a the provided value, which must be of size 48, as a {@link Bytes48}. + * + *

Note that value is not copied, only wrapped, and thus any future update to {@code value} + * will be reflected in the returned value. + * + * @param value The bytes to wrap. + * @return A {@link Bytes48} that exposes the bytes of {@code value}. + * @throws IllegalArgumentException if {@code value.size() != 48}. + */ + public static Bytes48 wrap(Bytes value) { + checkNotNull(value); + checkArgument(value.size() == SIZE, "Expected %s bytes but got %s", SIZE, value.size()); + if (value instanceof Bytes48 bytes48) { + return bytes48; + } + return new Bytes48(value.getImpl()); + } + + /** + * Wrap a slice/sub-part of the provided value as a {@link Bytes48}. + * + *

Note that value is not copied, only wrapped, and thus any future update to {@code value} + * within the wrapped parts will be reflected in the returned value. + * + * @param value The bytes to wrap. + * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned + * value. In other words, you will have {@code wrap(value, i).get(0) == value.get(i)}. + * @return A {@link Bytes48} that exposes the bytes of {@code value} from {@code offset} + * (inclusive) to {@code offset + 48} (exclusive). + * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.size() > 0 && offset >= + * value.size())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + 48 > value.size()}. + */ + public static Bytes48 wrap(Bytes value, int offset) { + checkNotNull(value); + Bytes slice = value.slice(offset, SIZE); + if (slice instanceof Bytes48 bytes48) { + return bytes48; + } + checkArgument(slice.size() == SIZE, "Expected %s bytes but got %s", SIZE, slice.size()); + return new Bytes48(slice.getImpl()); + } + + public static Bytes fromBytes(Bytes value, int offset) { + checkNotNull(value); + Bytes slice = value.slice(offset, SIZE); + checkArgument(slice.size() == SIZE, "Expected %s bytes but got %s", SIZE, slice.size()); + return slice.getImpl(); + } + + /** + * Parse a hexadecimal string into a {@link Bytes48}. + * + *

This method is lenient in that {@code str} may of an odd length, in which case it will + * behave exactly as if it had an additional 0 in front. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". That + * representation may contain less than 48 bytes, in which case the result is left padded with + * zeros (see {@link #fromHexStringStrict} if this is not what you want). + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation or contains more than 48 bytes. + */ + public static Bytes48 wrapHexStringLenient(CharSequence str) { + return new Bytes48(fromHexStringLenient(str)); + } + + public static Bytes fromHexStringLenient(CharSequence str) { + checkNotNull(str); + return fromArray(BytesValues.fromRawHexString(str, SIZE, true)); + } + + /** + * Parse a hexadecimal string into a {@link Bytes48}. + * + *

This method is strict in that {@code str} must of an even length. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". That + * representation may contain less than 48 bytes, in which case the result is left padded with + * zeros (see {@link #fromHexStringStrict} if this is not what you want). + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation, is of an odd length, or contains more than 48 bytes. + */ + public static Bytes48 wrapHexString(CharSequence str) { + return new Bytes48(fromHexString(str)); + } + + public static Bytes fromHexString(CharSequence str) { + checkNotNull(str); + return fromArray(BytesValues.fromRawHexString(str, SIZE, false)); + } + + /** + * Generate random bytes. + * + * @return A value containing random bytes. + */ + public static Bytes48 wrapRandom() { + return new Bytes48(fromRandom()); + } + + public static Bytes fromRandom() { + return fromRandom(new SecureRandom()); + } + + /** + * Generate random bytes. + * + * @param generator The generator for random bytes. + * @return A value containing random bytes. + */ + public static Bytes48 wrapRandom(Random generator) { + return new Bytes48(fromRandom(generator)); + } + + public static Bytes fromRandom(Random generator) { + byte[] array = new byte[48]; + generator.nextBytes(array); + return fromArray(array); + } + + /** + * Parse a hexadecimal string into a {@link Bytes48}. + * + *

This method is extra strict in that {@code str} must of an even length and the provided + * representation must have exactly 48 bytes. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation, is of an odd length or does not contain exactly 48 bytes. + */ + public static Bytes48 wrapHexStringStrict(CharSequence str) { + return new Bytes48(fromHexStringStrict(str)); + } + + public static Bytes fromHexStringStrict(CharSequence str) { + checkNotNull(str); + return fromArray(BytesValues.fromRawHexString(str, -1, false)); + } + + private static void checkLength(byte[] bytes, int offset) { + checkArgument( + bytes.length - offset == SIZE, + "Expected %s bytes from offset %s but got %s", + SIZE, + offset, + bytes.length - offset); + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/BytesValues.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/BytesValues.java new file mode 100644 index 000000000..4049110dd --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/BytesValues.java @@ -0,0 +1,74 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkArgument; +import static org.apache.tuweni.v2.bytes.Utils.checkNotNull; + +final class BytesValues { + private BytesValues() {} + + static final int MAX_UNSIGNED_SHORT = (1 << 16) - 1; + static final long MAX_UNSIGNED_INT = (1L << 32) - 1; + static final long MAX_UNSIGNED_LONG = Long.MAX_VALUE; + + static Bytes fromHexString(CharSequence str, int destSize, boolean lenient) { + return Bytes.wrap(fromRawHexString(str, destSize, lenient)); + } + + static byte[] fromRawHexString(CharSequence str, int destSize, boolean lenient) { + checkNotNull(str); + int len = str.length(); + CharSequence hex = str; + if (len >= 2 && str.charAt(0) == '0' && str.charAt(1) == 'x') { + hex = str.subSequence(2, len); + len -= 2; + } + + int idxShift = 0; + if ((len & 0x01) != 0) { + if (!lenient) { + throw new IllegalArgumentException("Invalid odd-length hex binary representation"); + } + + hex = "0" + hex; + len += 1; + idxShift = 1; + } + + int size = len >> 1; + if (destSize < 0) { + destSize = size; + } else { + checkArgument( + size <= destSize, + "Hex value is too large: expected at most %s bytes but got %s", + destSize, + size); + } + + byte[] out = new byte[destSize]; + + int destOffset = (destSize - size); + for (int i = destOffset, j = 0; j < len; i++) { + int h = Character.digit(hex.charAt(j), 16); + if (h == -1) { + throw new IllegalArgumentException( + String.format( + "Illegal character '%c' found at index %d in hex binary representation", + hex.charAt(j), j - idxShift)); + } + j++; + int l = Character.digit(hex.charAt(j), 16); + if (l == -1) { + throw new IllegalArgumentException( + String.format( + "Illegal character '%c' found at index %d in hex binary representation", + hex.charAt(j), j - idxShift)); + } + j++; + out[i] = (byte) ((h << 4) + l); + } + return out; + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/ConcatenatedBytes.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ConcatenatedBytes.java new file mode 100644 index 000000000..3ece172c5 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ConcatenatedBytes.java @@ -0,0 +1,273 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkLength; +import static org.apache.tuweni.v2.bytes.Utils.checkNotNull; + +import java.security.MessageDigest; +import java.util.List; + +final class ConcatenatedBytes extends Bytes { + + private final Bytes[] values; + + private ConcatenatedBytes(Bytes[] values, int totalSize) { + super(totalSize); + this.values = values; + } + + static Bytes create(Bytes... values) { + checkNotNull(values); + if (values.length == 0) { + return EMPTY; + } + if (values.length == 1) { + return values[0]; + } + + int count = 0; + int totalSize = 0; + + for (Bytes value : values) { + if (value == null) { + continue; + } + try { + totalSize = Math.addExact(totalSize, value.size()); + } catch (ArithmeticException e) { + throw new IllegalArgumentException( + "Combined length of values is too long (> Integer.MAX_VALUE)"); + } + if (value instanceof ConcatenatedBytes concatenatedBytes) { + count += concatenatedBytes.values.length; + } else if (!value.isEmpty()) { + count += 1; + } + } + + if (count == 0) { + return EMPTY; + } + if (count == values.length) { + return new ConcatenatedBytes(values, totalSize); + } + + Bytes[] concatenated = new Bytes[count]; + int i = 0; + for (Bytes value : values) { + if (value instanceof ConcatenatedBytes concatenatedBytes) { + Bytes[] subvalues = concatenatedBytes.values; + System.arraycopy(subvalues, 0, concatenated, i, subvalues.length); + i += subvalues.length; + } else if (value != null && !value.isEmpty()) { + concatenated[i] = value; + i++; + } + } + return new ConcatenatedBytes(concatenated, totalSize); + } + + static Bytes create(List values) { + if (values.isEmpty()) { + return EMPTY; + } + if (values.size() == 1) { + return values.getFirst(); + } + + int count = 0; + int totalSize = 0; + + for (Bytes value : values) { + int size = value.size(); + try { + totalSize = Math.addExact(totalSize, size); + } catch (ArithmeticException e) { + throw new IllegalArgumentException( + "Combined length of values is too long (> Integer.MAX_VALUE)"); + } + if (value instanceof ConcatenatedBytes) { + count += ((ConcatenatedBytes) value).values.length; + } else if (size != 0) { + count += 1; + } + } + + if (count == 0) { + return EMPTY; + } + if (count == values.size()) { + return new ConcatenatedBytes(values.toArray(new Bytes[0]), totalSize); + } + + Bytes[] concatenated = new Bytes[count]; + int i = 0; + for (Bytes value : values) { + if (value instanceof ConcatenatedBytes) { + Bytes[] subvalues = ((ConcatenatedBytes) value).values; + System.arraycopy(subvalues, 0, concatenated, i, subvalues.length); + i += subvalues.length; + } else if (!value.isEmpty()) { + concatenated[i++] = value; + } + } + return new ConcatenatedBytes(concatenated, totalSize); + } + + @Override + public byte get(int i) { + checkElementIndex(i, size()); + return getUnsafe(i); + } + + private byte getUnsafe(int i) { + for (Bytes value : values) { + int vSize = value.size(); + if (i < vSize) { + return value.get(i); + } + i -= vSize; + } + throw new IllegalStateException("element sizes do not match total size"); + } + + @Override + public Bytes slice(int offset, int length) { + if (offset == 0 && length == size()) { + return this; + } + if (length == 0) { + return EMPTY; + } + + checkElementIndex(offset, size()); + checkLength(size(), offset, length); + + int startIndex = 0; + for (int i = 0; i < values.length; i++) { + int currentValueSize = values[i].size(); + if (offset < currentValueSize) { + startIndex = i; + break; + } + offset -= currentValueSize; + } + + int endIndex = values.length - 1; + int lastElementLength = offset + length; + for (int i = startIndex; i < values.length; i++) { + int currentValueSize = values[i].size(); + if (lastElementLength <= currentValueSize) { + endIndex = i; + break; + } + lastElementLength -= currentValueSize; + } + + if (startIndex == endIndex) { + return values[startIndex].slice(offset, lastElementLength - offset); + } + + Bytes[] combined = new Bytes[endIndex - startIndex + 1]; + combined[0] = values[startIndex].slice(offset); + combined[combined.length - 1] = values[endIndex].slice(0, lastElementLength); + + if (endIndex >= startIndex + 2) { + System.arraycopy(values, startIndex + 1, combined, 1, endIndex - startIndex - 1); + } + + return new ConcatenatedBytes(combined, length); + } + + @Override + public MutableBytes mutableCopy() { + return MutableBytes.fromArray(toArrayUnsafe()); + } + + @Override + public void update(MessageDigest digest) { + for (Bytes value : values) { + value.update(digest); + } + } + + @Override + public byte[] toArrayUnsafe() { + byte[] bytesArray = new byte[size()]; + int offset = 0; + for (Bytes value : values) { + System.arraycopy(value.toArrayUnsafe(), 0, bytesArray, offset, value.size()); + offset += value.size(); + } + return bytesArray; + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + int resultOffset = offset; + for (Bytes value : values) { + value.and(bytesArray, resultOffset, value.size()); + resultOffset += value.size(); + if (resultOffset >= offset + length) { + return; + } + } + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + int resultOffset = offset; + for (Bytes value : values) { + value.or(bytesArray, resultOffset, value.size()); + resultOffset += value.size(); + if (resultOffset >= offset + length) { + return; + } + } + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + int resultOffset = offset; + for (Bytes value : values) { + value.xor(bytesArray, resultOffset, value.size()); + resultOffset += value.size(); + if (resultOffset >= offset + length) { + return; + } + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Bytes other)) { + return false; + } + + if (this.size() != other.size()) { + return false; + } + + for (int i = 0; i < size(); i++) { + if (getUnsafe(i) != other.get(i)) { + return false; + } + } + + return true; + } + + @Override + protected int computeHashcode() { + int result = 1; + for (int i = 0; i < size(); i++) { + result = 31 * result + getUnsafe(i); + } + return result; + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/ConstantBytesValue.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ConstantBytesValue.java new file mode 100644 index 000000000..945ebe6dc --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ConstantBytesValue.java @@ -0,0 +1,110 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkArgument; +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkLength; + +import java.util.Arrays; + +/** + * A Bytes value with just one constant value throughout. Ideal to avoid allocating large byte + * arrays filled with the same byte. + */ +class ConstantBytesValue extends Bytes { + + private final byte value; + + ConstantBytesValue(byte b, int size) { + super(size); + this.value = b; + } + + @Override + public byte get(int i) { + return this.value; + } + + @Override + public Bytes slice(int offset, int length) { + checkArgument(length >= 0, "Invalid negative length"); + if (size() > 0) { + checkElementIndex(offset, size()); + } + checkLength(size(), offset, length); + if (length == size()) { + return this; + } + return new ConstantBytesValue(this.value, length); + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + bytesArray[offset + i] = (byte) (value & bytesArray[offset + i]); + } + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + bytesArray[offset + i] = (byte) (value | bytesArray[offset + i]); + } + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + bytesArray[offset + i] = (byte) (value ^ bytesArray[offset + i]); + } + } + + @Override + public MutableBytes mutableCopy() { + MutableBytes mutableBytes = MutableBytes.create(size()); + mutableBytes.fill(value); + return mutableBytes; + } + + @Override + public byte[] toArrayUnsafe() { + byte[] array = new byte[size()]; + Arrays.fill(array, value); + return array; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Bytes other)) { + return false; + } + + if (this.size() != other.size()) { + return false; + } + + for (int i = 0; i < size(); i++) { + if (value != other.get(i)) { + return false; + } + } + + return true; + } + + @Override + protected int computeHashcode() { + int result = 1; + for (int i = 0; i < size(); i++) { + result = 31 * result + value; + } + return result; + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/DelegatingBytes.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/DelegatingBytes.java new file mode 100644 index 000000000..2cbd8581c --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/DelegatingBytes.java @@ -0,0 +1,260 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.apache.tuweni.v2.bytes.Utils.checkArgument; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; + +import io.vertx.core.buffer.Buffer; + +/** + * A class that holds and delegates all operations to its inner bytes field. + * + *

This class may be used to create more types that represent bytes, but need a different name + * for business logic. + */ +public class DelegatingBytes extends Bytes { + + final Bytes delegate; + + protected DelegatingBytes(Bytes delegate, int size) { + super(size); + this.delegate = delegate; + checkArgument(delegate.size() == size, "Expected %s bytes but got %s", size, delegate.size()); + } + + @Override + public byte get(int i) { + return delegate.get(i); + } + + @Override + public Bytes slice(int index, int length) { + return delegate.slice(index, length); + } + + @Override + public byte[] toArrayUnsafe() { + return delegate.toArrayUnsafe(); + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public Bytes getImpl() { + return delegate.getImpl(); + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + delegate.and(bytesArray, offset, length); + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + delegate.or(bytesArray, offset, length); + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + delegate.xor(bytesArray, offset, length); + } + + @Override + public int getInt(int i) { + return delegate.getInt(i); + } + + @Override + public int getInt(int i, ByteOrder order) { + return delegate.getInt(i, order); + } + + @Override + public int toInt() { + return delegate.toInt(); + } + + @Override + public int toInt(ByteOrder order) { + return delegate.toInt(order); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public long getLong(int i) { + return delegate.getLong(i); + } + + @Override + public long getLong(int i, ByteOrder order) { + return delegate.getLong(i, order); + } + + @Override + public long toLong() { + return delegate.toLong(); + } + + @Override + public long toLong(ByteOrder order) { + return delegate.toLong(order); + } + + @Override + public BigInteger toBigInteger() { + return delegate.toBigInteger(); + } + + @Override + public BigInteger toBigInteger(ByteOrder order) { + return delegate.toBigInteger(order); + } + + @Override + public BigInteger toUnsignedBigInteger() { + return delegate.toUnsignedBigInteger(); + } + + @Override + public BigInteger toUnsignedBigInteger(ByteOrder order) { + return delegate.toUnsignedBigInteger(order); + } + + @Override + public boolean isZero() { + return delegate.isZero(); + } + + @Override + public boolean hasLeadingZero() { + return delegate.hasLeadingZero(); + } + + @Override + public int numberOfLeadingZeros() { + return delegate.numberOfLeadingZeros(); + } + + @Override + public boolean hasLeadingZeroByte() { + return delegate.hasLeadingZeroByte(); + } + + @Override + public int numberOfLeadingZeroBytes() { + return delegate.numberOfLeadingZeroBytes(); + } + + @Override + public int numberOfTrailingZeroBytes() { + return delegate.numberOfTrailingZeroBytes(); + } + + @Override + public int bitLength() { + return delegate.bitLength(); + } + + @Override + public Bytes slice(int i) { + return delegate.slice(i); + } + + @Override + public void appendTo(ByteBuffer byteBuffer) { + delegate.appendTo(byteBuffer); + } + + @Override + public MutableBytes mutableCopy() { + return delegate.mutableCopy(); + } + + @Override + public void appendTo(Buffer buffer) { + delegate.appendTo(buffer); + } + + @Override + public T appendHexTo(T appendable) { + return delegate.appendHexTo(appendable); + } + + @Override + public int commonPrefixLength(Bytes other) { + return delegate.commonPrefixLength(other); + } + + @Override + public Bytes commonPrefix(Bytes other) { + return delegate.commonPrefix(other); + } + + @Override + public Bytes trimLeadingZeros() { + return delegate.trimLeadingZeros(); + } + + @Override + public void update(MessageDigest digest) { + delegate.update(digest); + } + + @Override + public String toHexString() { + return delegate.toHexString(); + } + + @Override + public String toUnprefixedHexString() { + return delegate.toUnprefixedHexString(); + } + + @Override + public String toEllipsisHexString() { + return delegate.toEllipsisHexString(); + } + + @Override + public String toShortHexString() { + return delegate.toShortHexString(); + } + + @Override + public String toQuantityHexString() { + return delegate.toQuantityHexString(); + } + + @Override + public String toBase64String() { + return delegate.toBase64String(); + } + + @Override + public int compareTo(Bytes b) { + return delegate.compareTo(b); + } + + @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") + public boolean equals(final Object o) { + return delegate.equals(o); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/MutableBytes.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/MutableBytes.java new file mode 100644 index 000000000..685aabf16 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/MutableBytes.java @@ -0,0 +1,675 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static java.lang.String.format; +import static org.apache.tuweni.v2.bytes.Utils.checkArgument; +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; +import static org.apache.tuweni.v2.bytes.Utils.checkLength; +import static org.apache.tuweni.v2.bytes.Utils.checkNotNull; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import io.netty.buffer.ByteBuf; +import io.vertx.core.buffer.Buffer; + +/** A class for doing modifications on a {@link Bytes} value without modifying the original. */ +public class MutableBytes extends ArrayWrappingBytes { + + MutableBytes(int size) { + super(new byte[size]); + } + + MutableBytes(byte[] bytesArray) { + super(new byte[bytesArray.length]); + System.arraycopy(bytesArray, 0, bytes, 0, bytesArray.length); + } + + MutableBytes(byte[] bytesArray, int offset, int length) { + super(new byte[length]); + System.arraycopy(bytesArray, offset, bytes, 0, length); + } + + /** + * Create a new mutable bytes value. + * + * @param size The size of the returned value. + * @return A {@link MutableBytes} value. + */ + public static MutableBytes create(int size) { + return new MutableBytes(size); + } + + /** + * Create a {@link MutableBytes} value from a byte array. + * + * @param value The value to wrap. + * @return A {@link MutableBytes} value wrapping {@code value}. + */ + public static MutableBytes fromArray(byte[] value) { + checkNotNull(value); + if (value.length == 0) { + return MutableBytes.create(0); + } + return new MutableBytes(value); + } + + /** + * Wrap a slice of a byte array as a {@link MutableBytes} value. + * + *

Note that value is not copied and thus any future update to {@code value} within the slice + * will be reflected in the returned value. + * + * @param value The value to wrap. + * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned + * value. In other words, you will have {@code wrap(value, o, l).get(0) == value[o]}. + * @param length The length of the resulting value. + * @return A {@link Bytes} value that expose the bytes of {@code value} from {@code offset} + * (inclusive) to {@code offset + length} (exclusive). + * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >= + * value.length)}. + * @throws IllegalArgumentException if {@code length < 0 || offset + length > value.length}. + */ + public static MutableBytes fromArray(byte[] value, int offset, int length) { + checkNotNull(value); + checkArgument(length >= 0, "Invalid negative length"); + if (value.length > 0) { + checkElementIndex(offset, value.length); + } + checkLength(value.length, offset, length); + if (length == 0) { + return MutableBytes.create(0); + } + return new MutableBytes(value, offset, length); + } + + /** + * Wrap a full Vert.x {@link Buffer} as a {@link MutableBytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value. + * + * @param buffer The buffer to wrap. + * @return A {@link MutableBytes} value. + */ + public static MutableBytes fromBuffer(Buffer buffer) { + checkNotNull(buffer); + if (buffer.length() == 0) { + return MutableBytes.create(0); + } + return new MutableBytes(buffer.getBytes()); + } + + /** + * Wrap a slice of a Vert.x {@link Buffer} as a {@link MutableBytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value, + * and any change to the returned value will be reflected in the buffer. + * + * @param buffer The buffer to wrap. + * @param offset The offset in {@code buffer} from which to expose the bytes in the returned + * value. That is, {@code wrapBuffer(buffer, i, 1).get(0) == buffer.getByte(i)}. + * @param length The size of the returned value. + * @return A {@link MutableBytes} value. + * @throws IndexOutOfBoundsException if {@code offset < 0 || (buffer.length() > 0 && offset >= + * buffer.length())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + length > buffer.length()}. + */ + public static MutableBytes fromBuffer(Buffer buffer, int offset, int length) { + checkNotNull(buffer); + checkArgument(length >= 0, "Invalid negative length"); + if (buffer.length() > 0) { + checkElementIndex(offset, buffer.length()); + } + checkLength(buffer.length(), offset, length); + if (length == 0) { + return MutableBytes.create(0); + } + return new MutableBytes(buffer.getBytes(), offset, length); + } + + /** + * Wrap a full Netty {@link ByteBuf} as a {@link MutableBytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value. + * + * @param byteBuf The {@link ByteBuf} to wrap. + * @return A {@link MutableBytes} value. + */ + public static MutableBytes fromByteBuf(ByteBuf byteBuf) { + checkNotNull(byteBuf); + if (byteBuf.capacity() == 0) { + return MutableBytes.create(0); + } + MutableBytes mutableBytes = MutableBytes.create(byteBuf.capacity()); + byteBuf.getBytes(0, mutableBytes.bytes); + return mutableBytes; + } + + /** + * Wrap a slice of a Netty {@link ByteBuf} as a {@link MutableBytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value, + * and any change to the returned value will be reflected in the buffer. + * + * @param byteBuf The {@link ByteBuf} to wrap. + * @param offset The offset in {@code byteBuf} from which to expose the bytes in the returned + * value. That is, {@code wrapByteBuf(byteBuf, i, 1).get(0) == byteBuf.getByte(i)}. + * @param length The size of the returned value. + * @return A {@link MutableBytes} value. + * @throws IndexOutOfBoundsException if {@code offset < 0 || (byteBuf.capacity() > 0 && offset >= + * byteBuf.capacity())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + length > byteBuf.capacity()}. + */ + public static MutableBytes fromByteBuf(ByteBuf byteBuf, int offset, int length) { + checkNotNull(byteBuf); + checkArgument(length >= 0, "Invalid negative length"); + final int byteBufLength = byteBuf.capacity(); + if (byteBufLength > 0) { + checkElementIndex(offset, byteBuf.capacity()); + } + checkLength(byteBufLength, offset, length); + if (length == 0) { + return MutableBytes.create(0); + } + MutableBytes mutableBytes = MutableBytes.create(length); + byteBuf.getBytes(offset, mutableBytes.bytes); + return mutableBytes; + } + + /** + * Wrap a full Java NIO {@link ByteBuffer} as a {@link MutableBytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value. + * + * @param byteBuffer The {@link ByteBuffer} to wrap. + * @return A {@link MutableBytes} value. + */ + public static MutableBytes fromByteBuffer(ByteBuffer byteBuffer) { + checkNotNull(byteBuffer); + if (byteBuffer.limit() == 0) { + return MutableBytes.create(0); + } + MutableBytes mutableBytes = MutableBytes.create(byteBuffer.limit()); + byteBuffer.get(0, mutableBytes.bytes); + return mutableBytes; + } + + /** + * Wrap a slice of a Java NIO {@link ByteBuffer} as a {@link MutableBytes} value. + * + *

Note that any change to the content of the buffer may be reflected in the returned value, + * and any change to the returned value will be reflected in the buffer. + * + * @param byteBuffer The {@link ByteBuffer} to wrap. + * @param offset The offset in {@code byteBuffer} from which to expose the bytes in the returned + * value. That is, {@code wrapByteBuffer(byteBuffer, i, 1).get(0) == byteBuffer.getByte(i)}. + * @param length The size of the returned value. + * @return A {@link MutableBytes} value. + * @throws IndexOutOfBoundsException if {@code offset < 0 || (byteBuffer.limit() > 0 && offset >= + * byteBuffer.limit())}. + * @throws IllegalArgumentException if {@code length < 0 || offset + length > byteBuffer.limit()}. + */ + public static MutableBytes fromByteBuffer(ByteBuffer byteBuffer, int offset, int length) { + checkNotNull(byteBuffer); + checkArgument(length >= 0, "Invalid negative length"); + final int byteBufferLength = byteBuffer.limit(); + if (byteBufferLength > 0) { + checkElementIndex(offset, byteBuffer.limit()); + } + checkLength(byteBufferLength, offset, length); + if (length == 0) { + return MutableBytes.create(0); + } + MutableBytes mutableBytes = MutableBytes.create(length); + byteBuffer.get(offset, mutableBytes.bytes); + return mutableBytes; + } + + /** + * Create a value that contains the specified bytes in their specified order. + * + * @param bytes The bytes that must compose the returned value. + * @return A value containing the specified bytes. + */ + public static MutableBytes of(byte... bytes) { + return fromArray(bytes); + } + + /** + * Create a value that contains the specified bytes in their specified order. + * + * @param bytes The bytes. + * @return A value containing bytes are the one from {@code bytes}. + * @throws IllegalArgumentException if any of the specified would be truncated when storing as a + * byte. + */ + public static MutableBytes of(int... bytes) { + checkNotNull(bytes); + byte[] result = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + int b = bytes[i]; + checkArgument(b == (((byte) b) & 0xff), "%sth value %s does not fit a byte", i + 1, b); + result[i] = (byte) b; + } + return fromArray(result); + } + + /** + * Set a byte in this value. + * + * @param index The offset of the bytes to set. + * @param bytes The value to set bytes to. + * @throws IndexOutOfBoundsException if {@code offset < 0} or {offset >= size()}. + * @throws IllegalArgumentException if {@code offset + bytes.size() > this.length}. + */ + public void set(int index, Bytes bytes) { + checkNotNull(bytes); + if (bytes.isEmpty()) { + return; + } + checkElementIndex(index, size); + checkLength(this.bytes.length, index, bytes.size()); + for (int i = 0; i < bytes.size(); i++) { + set(i + index, bytes.get(i)); + } + } + + /** + * Set a byte array in this value. + * + * @param index The offset of the bytes to set. + * @param bytes The value to set bytes to. + * @throws IndexOutOfBoundsException if {@code offset < 0} or {offset >= bytes.length}. + * @throws IllegalArgumentException if {@code offset + bytes.length > this.length}. + */ + public void set(int index, byte[] bytes) { + checkNotNull(bytes); + if (bytes.length == 0) { + return; + } + checkElementIndex(index, size); + checkLength(this.bytes.length, index, bytes.length); + for (int i = 0; i < bytes.length; i++) { + set(i + index, bytes[i]); + } + } + + /** + * Set the 4 bytes starting at the specified index to the specified integer value. + * + * @param index The index, which must less than or equal to {@code size() - 4}. + * @param value The integer value. + * @throws IndexOutOfBoundsException if {@code index < 0} or {@code index > size() - 4}. + */ + public void setInt(int index, int value) { + checkElementIndex(index, size); + if (index > (size - 4)) { + throw new IndexOutOfBoundsException( + format( + "Value of size %s has not enough bytes to write a 4 bytes int from index %s", + size, index)); + } + + set(index++, (byte) (value >>> 24)); + set(index++, (byte) ((value >>> 16) & 0xFF)); + set(index++, (byte) ((value >>> 8) & 0xFF)); + set(index, (byte) (value & 0xFF)); + } + + /** + * Set the 8 bytes starting at the specified index to the specified long value. + * + * @param index The index, which must less than or equal to {@code size() - 8}. + * @param value The long value. + * @throws IndexOutOfBoundsException if {@code index < 0} or {@code index > size() - 8}. + */ + public void setLong(int index, long value) { + checkElementIndex(index, size); + if (index > (size - 8)) { + throw new IndexOutOfBoundsException( + format( + "Value of length %s has not enough bytes to write a 8 bytes long from index %s", + size, index)); + } + + set(index++, (byte) (value >>> 56)); + set(index++, (byte) ((value >>> 48) & 0xFF)); + set(index++, (byte) ((value >>> 40) & 0xFF)); + set(index++, (byte) ((value >>> 32) & 0xFF)); + set(index++, (byte) ((value >>> 24) & 0xFF)); + set(index++, (byte) ((value >>> 16) & 0xFF)); + set(index++, (byte) ((value >>> 8) & 0xFF)); + set(index, (byte) (value & 0xFF)); + } + + /** + * Set a byte in this value. + * + * @param index The index of the byte to set. + * @param b The value to set that byte to. + * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()}. + */ + public void set(int index, byte b) { + checkElementIndex(index, size); + bytes[index] = b; + hashCode = null; + } + + /** + * Increments the value of the bytes by 1, treating the value as big endian. + * + *

If incrementing overflows the value then all bits flip, i.e. incrementing 0xFFFF will return + * 0x0000. + * + * @return This mutable bytes instance. + */ + public MutableBytes increment() { + for (int i = size - 1; i >= 0; --i) { + if (bytes[i] == (byte) 0xFF) { + bytes[i] = (byte) 0x00; + } else { + byte currentValue = bytes[i]; + bytes[i] = ++currentValue; + break; + } + } + hashCode = null; + return this; + } + + /** + * Decrements the value of the bytes by 1, treating the value as big endian. + * + *

If decrementing underflows the value then all bits flip, i.e. decrementing 0x0000 will + * return 0xFFFF. + * + * @return This mutable bytes instance. + */ + public MutableBytes decrement() { + for (int i = size - 1; i >= 0; --i) { + if (bytes[i] == (byte) 0x00) { + bytes[i] = (byte) 0xFF; + } else { + byte currentValue = bytes[i]; + bytes[i] = --currentValue; + break; + } + } + hashCode = null; + return this; + } + + /** + * Fill all the bytes of this value with the specified byte. + * + * @param b The byte to use to fill the value. + * @return This mutable bytes instance. + */ + public MutableBytes fill(byte b) { + for (int i = 0; i < size; i++) { + bytes[i] = b; + } + hashCode = null; + return this; + } + + /** + * Set all bytes in this value to 0. + * + * @return This mutable bytes instance. + */ + public MutableBytes clear() { + fill((byte) 0); + return this; + } + + /** + * Computes the reverse array of bytes of the current bytes. + * + * @return This mutable bytes instance. + */ + public MutableBytes reverse() { + byte[] reverse = new byte[size]; + for (int i = 0; i < size; i++) { + reverse[size - 1 - i] = bytes[i]; + } + bytes = reverse; + hashCode = null; + return this; + } + + /** + * Calculate a bit-wise AND of these bytes and the supplied bytes. + * + * @param other The bytes to perform the operation with. + * @return This mutable bytes instance. + */ + public MutableBytes and(Bytes other) { + checkNotNull(other); + int otherSize = other.size(); + if (size == otherSize) { + other.and(bytes, 0, size); + hashCode = null; + return this; + } + + int otherOffset = 0; + if (size < otherSize) { + byte[] newBytesArray = new byte[otherSize]; + System.arraycopy(bytes, 0, newBytesArray, otherSize - size, size); + bytes = newBytesArray; + size = otherSize; + } else { + Arrays.fill(bytes, 0, size - otherSize, (byte) 0); + otherOffset = size - otherSize; + } + other.and(bytes, otherOffset, otherSize); + hashCode = null; + return this; + } + + /** + * Calculate a bit-wise OR of these bytes and the supplied bytes. + * + * @param other The bytes to perform the operation with. + * @return This mutable bytes instance. + */ + public MutableBytes or(Bytes other) { + checkNotNull(other); + int otherSize = other.size(); + if (size == otherSize) { + other.or(bytes, 0, size); + hashCode = null; + return this; + } + + int otherOffset = 0; + if (size < otherSize) { + byte[] newBytesArray = new byte[otherSize]; + System.arraycopy(bytes, 0, newBytesArray, otherSize - size, size); + bytes = newBytesArray; + size = otherSize; + } else { + otherOffset = size - otherSize; + } + other.or(bytes, otherOffset, otherSize); + hashCode = null; + return this; + } + + /** + * Calculate a bit-wise XOR of these bytes and the supplied bytes. + * + * @param other The bytes to perform the operation with. + * @return This mutable bytes instance. + */ + public MutableBytes xor(Bytes other) { + checkNotNull(other); + int otherSize = other.size(); + if (size == otherSize) { + other.xor(bytes, 0, size); + hashCode = null; + return this; + } + + int otherOffset = 0; + if (size < otherSize) { + byte[] newBytesArray = new byte[otherSize]; + System.arraycopy(bytes, 0, newBytesArray, otherSize - size, size); + bytes = newBytesArray; + size = otherSize; + } else { + otherOffset = size - otherSize; + } + other.xor(bytes, otherOffset, otherSize); + hashCode = null; + return this; + } + + /** + * Calculate a bit-wise NOT of these bytes. + * + * @return This mutable bytes instance. + */ + public MutableBytes not() { + for (int i = 0; i < size; i++) { + bytes[i] = (byte) ~bytes[i]; + } + hashCode = null; + return this; + } + + /** + * Shift all bits in this value to the right. + * + * @param distance The number of bits to shift by. + * @return This mutable bytes instance. + */ + public MutableBytes shiftRight(int distance) { + checkArgument(distance >= 0, "Invalid negative distance"); + if (distance == 0 || size() == 0) { + return this; + } + distance = Math.min(distance, size * 8); + int byteShift = distance / 8; + int bitShift = distance % 8; + + if (byteShift > 0) { + for (int i = size - 1; i >= 0; i--) { + byte previousByte = (i < byteShift) ? 0 : bytes[i - byteShift]; + bytes[i] = previousByte; + } + } + + if (bitShift > 0) { + for (int i = size - 1; i >= 0; i--) { + byte currentByte = bytes[i]; + byte previousByte = (i == 0) ? 0 : bytes[i - 1]; + int rightSide = (currentByte & 0XFF) >>> bitShift; + int leftSide = previousByte << (8 - bitShift); + bytes[i] = (byte) (leftSide | rightSide); + } + } + hashCode = null; + return this; + } + + /** + * Shift all bits in this value to the left. + * + * @param distance The number of bits to shift by. + * @return This mutable bytes instance. + */ + public MutableBytes shiftLeft(int distance) { + checkArgument(distance >= 0, "Invalid negative distance"); + if (distance == 0 || size() == 0) { + return this; + } + distance = Math.min(distance, size * 8); + int byteShift = distance / 8; + int bitShift = distance % 8; + + if (byteShift > 0) { + for (int i = 0; i < size; i++) { + byte nextByte = (i + byteShift < size) ? bytes[i + byteShift] : 0; + bytes[i] = nextByte; + } + } + + if (bitShift > 0) { + for (int i = 0; i < size; i++) { + byte currentByte = bytes[i]; + byte nextByte = (i == size - 1) ? 0 : bytes[i + 1]; + int leftSide = currentByte << bitShift; + int rightSide = (nextByte & 0XFF) >>> (8 - bitShift); + bytes[i] = (byte) (leftSide | rightSide); + } + } + hashCode = null; + return this; + } + + /** + * Left pad these mutable values with zero bytes up to the specified length. Resulting bytes are + * guaranteed to have at least {@code length} bytes in length but not necessarily that exact + * amount. If length already exceeds {@code length} then bytes are not modified. + * + * @param length The new length of the bytes. + * @throws IllegalArgumentException if {@code length} is negative. + * @return This mutable bytes instance. + */ + public MutableBytes leftPad(int length) { + checkArgument(length >= 0, "Invalid negative length"); + if (length <= size) { + return this; + } + byte[] newBytesArray = new byte[length]; + System.arraycopy(bytes, 0, newBytesArray, length - size, size); + bytes = newBytesArray; + size = length; + hashCode = null; + return this; + } + + /** + * Right pad these mutable values with zero bytes up to the specified length. Resulting bytes are + * guaranteed to have at least {@code length} bytes in length but not necessarily that exact + * amount. If length already exceeds {@code length} then bytes are not modified. + * + * @param length The new length of the bytes. + * @throws IllegalArgumentException if {@code length} is negative. + * @return This mutable bytes instance. + */ + public MutableBytes rightPad(int length) { + checkArgument(length >= 0, "Invalid negative length"); + if (length <= size) { + return this; + } + byte[] newBytesArray = new byte[length]; + System.arraycopy(bytes, 0, newBytesArray, 0, size); + bytes = newBytesArray; + size = length; + hashCode = null; + return this; + } + + public byte[] toArray() { + return toArrayUnsafe(); + } + + /** + * Parse a hexadecimal string into a {@link MutableBytes} value. + * + *

This method requires that {@code str} have an even length. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation, or is of an odd length. + */ + public static MutableBytes fromHexString(CharSequence str) { + checkNotNull(str); + return MutableBytes.fromArray(BytesValues.fromRawHexString(str, -1, false)); + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/Utils.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/Utils.java new file mode 100644 index 000000000..7fe049660 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/Utils.java @@ -0,0 +1,113 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import javax.annotation.Nullable; + +import com.google.errorprone.annotations.FormatMethod; + +public final class Utils { + + static void checkNotNull(@Nullable Object object) { + if (object == null) { + throw new NullPointerException("argument cannot be null"); + } + } + + public static void checkElementIndex(int index, int size) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException("index is out of bounds"); + } + } + + @FormatMethod + static void checkArgument(boolean condition, String message) { + if (!condition) { + throw new IllegalArgumentException(message); + } + } + + @FormatMethod + static void checkArgument(boolean condition, String message, int arg1) { + if (!condition) { + throw new IllegalArgumentException(String.format(message, arg1)); + } + } + + @FormatMethod + static void checkArgument(boolean condition, String message, int arg1, int arg2) { + if (!condition) { + throw new IllegalArgumentException(String.format(message, arg1, arg2)); + } + } + + @FormatMethod + static void checkArgument(boolean condition, String message, int arg1, int arg2, int arg3) { + if (!condition) { + throw new IllegalArgumentException(String.format(message, arg1, arg2, arg3)); + } + } + + @FormatMethod + static void checkArgument( + boolean condition, String message, int arg1, int arg2, int arg3, int arg4) { + if (!condition) { + throw new IllegalArgumentException(String.format(message, arg1, arg2, arg3, arg4)); + } + } + + @FormatMethod + static void checkArgument(boolean condition, String message, long arg1) { + if (!condition) { + throw new IllegalArgumentException(String.format(message, arg1)); + } + } + + static void checkLength(int arrayLength, int offset, int length) { + checkArgument( + offset + length <= arrayLength, + "Provided length %s is too big: the value has only %s bytes from offset %s", + length, + arrayLength - offset, + offset); + } + + static void and( + byte[] sourceBytesArray, + int sourceOffset, + byte[] destBytesArray, + int destOffset, + int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + destBytesArray[destOffset + i] = + (byte) (sourceBytesArray[sourceOffset + i] & destBytesArray[destOffset + i]); + } + } + + static void or( + byte[] sourceBytesArray, + int sourceOffset, + byte[] destBytesArray, + int destOffset, + int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + destBytesArray[destOffset + i] = + (byte) (sourceBytesArray[sourceOffset + i] | destBytesArray[destOffset + i]); + } + } + + static void xor( + byte[] sourceBytesArray, + int sourceOffset, + byte[] destBytesArray, + int destOffset, + int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + destBytesArray[destOffset + i] = + (byte) (sourceBytesArray[sourceOffset + i] ^ destBytesArray[destOffset + i]); + } + } +} diff --git a/bytes/src/main/java/org/apache/tuweni/v2/bytes/package-info.java b/bytes/src/main/java/org/apache/tuweni/v2/bytes/package-info.java new file mode 100644 index 000000000..35259f8db --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/package-info.java @@ -0,0 +1,23 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You 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. + * + *

Classes and utilities for working with byte arrays. + * + *

These classes are included in the standard Tuweni distribution, or separately when using the + * gradle dependency 'org.apache.tuweni:tuweni-bytes' (tuweni-bytes.jar). + */ +@ParametersAreNonnullByDefault +package org.apache.tuweni.v2.bytes; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/bytes/src/test/java/org/apache/tuweni/bytes/ConcatenatedBytesTest.java b/bytes/src/test/java/org/apache/tuweni/bytes/ConcatenatedBytesTest.java index 03056df39..0450efa55 100644 --- a/bytes/src/test/java/org/apache/tuweni/bytes/ConcatenatedBytesTest.java +++ b/bytes/src/test/java/org/apache/tuweni/bytes/ConcatenatedBytesTest.java @@ -72,6 +72,7 @@ void shouldSliceConcatenatedValue() { fromHexString("0x89ABCDEF")); assertEquals("0x", bytes.slice(4, 0).toHexString()); assertEquals("0x0123456789abcdef0123456789abcdef", bytes.slice(0, 16).toHexString()); + assertEquals("0x0123456789abcdef0123456789ab", bytes.slice(0, 14).toHexString()); assertEquals("0x01234567", bytes.slice(0, 4).toHexString()); assertEquals("0x0123", bytes.slice(0, 2).toHexString()); assertEquals("0x6789", bytes.slice(3, 2).toHexString()); diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/BufferBytesTest.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/BufferBytesTest.java new file mode 100644 index 000000000..d8eae113c --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/BufferBytesTest.java @@ -0,0 +1,28 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import io.vertx.core.buffer.Buffer; + +class BufferBytesTest extends CommonBytesTests { + + @Override + Bytes h(String hex) { + return Bytes.wrapBuffer(Buffer.buffer(Bytes.fromHexString(hex).toArrayUnsafe())); + } + + @Override + MutableBytes m(int size) { + return MutableBytes.fromBuffer(Buffer.buffer(new byte[size])); + } + + @Override + Bytes w(byte[] bytes) { + return Bytes.wrapBuffer(Buffer.buffer(Bytes.of(bytes).toArrayUnsafe())); + } + + @Override + Bytes of(int... bytes) { + return Bytes.wrapBuffer(Buffer.buffer(Bytes.of(bytes).toArrayUnsafe())); + } +} diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/ByteBufBytesTest.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/ByteBufBytesTest.java new file mode 100644 index 000000000..b5e10b011 --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/ByteBufBytesTest.java @@ -0,0 +1,28 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import io.netty.buffer.Unpooled; + +class ByteBufBytesTest extends CommonBytesTests { + + @Override + Bytes h(String hex) { + return Bytes.wrapByteBuf(Unpooled.copiedBuffer(Bytes.fromHexString(hex).toArrayUnsafe())); + } + + @Override + MutableBytes m(int size) { + return MutableBytes.fromByteBuf(Unpooled.copiedBuffer(new byte[size])); + } + + @Override + Bytes w(byte[] bytes) { + return Bytes.wrapByteBuf(Unpooled.copiedBuffer(Bytes.of(bytes).toArrayUnsafe())); + } + + @Override + Bytes of(int... bytes) { + return Bytes.wrapByteBuf(Unpooled.copiedBuffer(Bytes.of(bytes).toArrayUnsafe())); + } +} diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/ByteBufferBytesTest.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/ByteBufferBytesTest.java new file mode 100644 index 000000000..b1bcd8cc9 --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/ByteBufferBytesTest.java @@ -0,0 +1,28 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import java.nio.ByteBuffer; + +class ByteBufferBytesTest extends CommonBytesTests { + + @Override + Bytes h(String hex) { + return Bytes.wrapByteBuffer(ByteBuffer.wrap(Bytes.fromHexString(hex).toArrayUnsafe())); + } + + @Override + MutableBytes m(int size) { + return MutableBytes.fromByteBuffer(ByteBuffer.allocate(size)); + } + + @Override + Bytes w(byte[] bytes) { + return Bytes.wrapByteBuffer(ByteBuffer.wrap(Bytes.of(bytes).toArrayUnsafe())); + } + + @Override + Bytes of(int... bytes) { + return Bytes.wrapByteBuffer(ByteBuffer.wrap(Bytes.of(bytes).toArrayUnsafe())); + } +} diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/Bytes32Test.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/Bytes32Test.java new file mode 100644 index 000000000..0e64e1c3e --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/Bytes32Test.java @@ -0,0 +1,160 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class Bytes32Test { + + @Test + void testConcatenation() { + Bytes wrapped = Bytes.wrap(Bytes32.wrap(new byte[32]), Bytes32.wrap(new byte[32])); + assertEquals(64, wrapped.size()); + wrapped = wrapped.slice(0, 32); + assertEquals(32, wrapped.size()); + wrapped = wrapped.slice(31, 0); + assertEquals(0, wrapped.size()); + } + + @Test + void constantBytes32Slice() { + assertEquals(Bytes32.ZERO.slice(12, 20).size(), 20); + } + + @Test + void constantBytesslice() { + assertEquals(Bytes.repeat((byte) 1, 63).slice(12, 20).size(), 20); + } + + @Test + void testMutableBytes32WrapWithOffset() { + Bytes bytes = + Bytes.fromHexString( + "0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"); + MutableBytes mutableBytes = bytes.mutableCopy(); + assertEquals( + "0x112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + Bytes32.wrap(mutableBytes, 1).toHexString()); + } + + @Test + void testBytes32SliceWithOffset() { + Bytes bytes = + Bytes.fromHexString( + "0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"); + assertEquals( + "0x112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00", + Bytes32.wrap(bytes.slice(1, 32)).toHexString()); + assertEquals( + "0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899", + Bytes32.wrap(bytes.slice(10, 32)).toHexString()); + } + + @Test + void failsWhenWrappingArraySmallerThan32() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes32.wrap(new byte[31])); + assertEquals("Expected 32 bytes from offset 0 but got 31", exception.getMessage()); + } + + @Test + void failsWhenWrappingArrayLargerThan32() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes32.wrap(new byte[33])); + assertEquals("Expected 32 bytes from offset 0 but got 33", exception.getMessage()); + } + + @Test + void wrapReturnsInstanceBytes32() { + assertThat(Bytes32.wrapHexString("0x")).isExactlyInstanceOf(Bytes32.class); + } + + @Test + void leftPadAValueToBytes32() { + Bytes32 b32 = Bytes32.wrap(Bytes.of(1, 2, 3).mutableCopy().leftPad(32)); + assertEquals(32, b32.size()); + for (int i = 0; i < 28; ++i) { + assertEquals((byte) 0, b32.get(i)); + } + assertEquals((byte) 1, b32.get(29)); + assertEquals((byte) 2, b32.get(30)); + assertEquals((byte) 3, b32.get(31)); + } + + @Test + void rightPadAValueToBytes32() { + Bytes32 b32 = Bytes32.wrap(Bytes.of(1, 2, 3).mutableCopy().rightPad(32)); + assertEquals(32, b32.size()); + for (int i = 3; i < 32; ++i) { + assertEquals((byte) 0, b32.get(i)); + } + assertEquals((byte) 1, b32.get(0)); + assertEquals((byte) 2, b32.get(1)); + assertEquals((byte) 3, b32.get(2)); + } + + @Test + void failsWhenLeftPaddingValueLargerThan32() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, + () -> Bytes32.wrap(Bytes.EMPTY.mutableCopy().leftPad(33))); + assertEquals("Expected 32 bytes but got 33", exception.getMessage()); + } + + @Test + void failsWhenRightPaddingValueLargerThan32() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, + () -> Bytes32.wrap(Bytes.EMPTY.mutableCopy().rightPad(33))); + assertEquals("Expected 32 bytes but got 33", exception.getMessage()); + } + + @Test + void testWrapSlicesCorrectly() { + Bytes input = + Bytes.fromHexString( + "0xA99A76ED7796F7BE22D5B7E85DEEB7C5677E88E511E0B337618F8C4EB61349B4BF2D153F649F7B53359FE8B94A38E44C00000000000000000000000000000000"); + Bytes32 value = Bytes32.wrap(input, 0); + assertEquals( + Bytes.fromHexString("0xA99A76ED7796F7BE22D5B7E85DEEB7C5677E88E511E0B337618F8C4EB61349B4"), + value); + + Bytes32 secondValue = Bytes32.wrap(input, 32); + assertEquals( + Bytes.fromHexString("0xBF2D153F649F7B53359FE8B94A38E44C00000000000000000000000000000000"), + secondValue); + } + + @Test + void wrap() { + Bytes source = Bytes.random(96); + Bytes32 value = Bytes32.wrap(source, 2); + assertEquals(source.slice(2, 32), value); + } + + @Test + void hexString() { + Bytes initial = Bytes32.fromRandom(); + assertEquals(initial, Bytes32.fromHexStringLenient(initial.toHexString())); + assertEquals(initial, Bytes32.fromHexString(initial.toHexString())); + assertEquals(initial, Bytes32.fromHexStringStrict(initial.toHexString())); + } + + @Test + void size() { + assertEquals(32, Bytes32.fromRandom().size()); + } + + @Test + void padding() { + Bytes source = Bytes32.fromRandom(); + assertEquals(source, Bytes32.wrap(source.mutableCopy().leftPad(32))); + assertEquals(source, Bytes32.wrap(source.mutableCopy().rightPad(32))); + } +} diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/Bytes48Test.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/Bytes48Test.java new file mode 100644 index 000000000..4cabc71c5 --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/Bytes48Test.java @@ -0,0 +1,100 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class Bytes48Test { + + @Test + void failsWhenWrappingArraySmallerThan48() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes48.wrap(new byte[47])); + assertEquals("Expected 48 bytes from offset 0 but got 47", exception.getMessage()); + } + + @Test + void failsWhenWrappingArrayLargerThan48() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes48.wrap(new byte[49])); + assertEquals("Expected 48 bytes from offset 0 but got 49", exception.getMessage()); + } + + @Test + void wrapReturnsInstanceBytes48() { + assertThat(Bytes48.wrapHexString("0x")).isExactlyInstanceOf(Bytes48.class); + } + + @Test + void rightPadAValueToBytes48() { + Bytes48 b48 = Bytes48.wrap(Bytes.of(1, 2, 3).mutableCopy().rightPad(48)); + assertEquals(48, b48.size()); + for (int i = 3; i < 48; ++i) { + assertEquals((byte) 0, b48.get(i)); + } + assertEquals((byte) 1, b48.get(0)); + assertEquals((byte) 2, b48.get(1)); + assertEquals((byte) 3, b48.get(2)); + } + + @Test + void leftPadAValueToBytes48() { + Bytes48 b48 = Bytes48.wrap(Bytes.of(1, 2, 3).mutableCopy().leftPad(48)); + assertEquals(48, b48.size()); + for (int i = 0; i < 28; ++i) { + assertEquals((byte) 0, b48.get(i)); + } + assertEquals((byte) 1, b48.get(45)); + assertEquals((byte) 2, b48.get(46)); + assertEquals((byte) 3, b48.get(47)); + } + + @Test + void failsWhenLeftPaddingValueLargerThan48() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, + () -> Bytes48.wrap(Bytes.EMPTY.mutableCopy().leftPad(49))); + assertEquals("Expected 48 bytes but got 49", exception.getMessage()); + } + + @Test + void failsWhenRightPaddingValueLargerThan48() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, + () -> Bytes48.wrap(Bytes.EMPTY.mutableCopy().rightPad(49))); + assertEquals("Expected 48 bytes but got 49", exception.getMessage()); + } + + @Test + void hexString() { + Bytes initial = Bytes48.fromRandom(); + assertEquals(initial, Bytes48.fromHexStringLenient(initial.toHexString())); + assertEquals(initial, Bytes48.fromHexString(initial.toHexString())); + assertEquals(initial, Bytes48.fromHexStringStrict(initial.toHexString())); + } + + @Test + void size() { + assertEquals(48, Bytes48.fromRandom().size()); + } + + @Test + void wrap() { + Bytes source = Bytes.random(96); + Bytes48 value = Bytes48.wrap(source, 2); + assertEquals(source.slice(2, 48), value); + } + + @Test + void padding() { + Bytes source = Bytes48.fromRandom(); + assertEquals(source, Bytes48.wrap(source.mutableCopy().leftPad(48))); + assertEquals(source, Bytes48.wrap(source.mutableCopy().rightPad(48))); + } +} diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/BytesTest.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/BytesTest.java new file mode 100644 index 000000000..ac61ce200 --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/BytesTest.java @@ -0,0 +1,569 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static java.nio.ByteOrder.LITTLE_ENDIAN; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class BytesTest extends CommonBytesTests { + + @Override + Bytes h(String hex) { + return Bytes.fromHexString(hex); + } + + @Override + MutableBytes m(int size) { + return MutableBytes.create(size); + } + + @Override + Bytes w(byte[] bytes) { + return Bytes.wrap(bytes); + } + + @Override + Bytes of(int... bytes) { + return Bytes.of(bytes); + } + + @Test + void wrapEmpty() { + Bytes wrap = Bytes.wrap(new byte[0]); + assertEquals(Bytes.EMPTY, wrap); + } + + @ParameterizedTest + @MethodSource("wrapProvider") + void wrap(List arr) { + final byte[] bytes = new byte[arr.size()]; + arr.forEach(byteValue -> bytes[arr.indexOf(byteValue)] = byteValue); + Bytes value = Bytes.wrap(bytes); + assertEquals(bytes.length, value.size()); + assertArrayEquals(value.toArrayUnsafe(), bytes); + } + + @SuppressWarnings("UnusedMethod") + private static Stream wrapProvider() { + return Stream.of( + Arguments.of(new ArrayList(10)), + Arguments.of(toList(new byte[] {1})), + Arguments.of(toList(new byte[] {1, 2, 3, 4})), + Arguments.of(toList(new byte[] {-1, 127, -128}))); + } + + private static List toList(final byte[] array) { + return IntStream.range(0, array.length).mapToObj(i -> array[i]).toList(); + } + + @Test + void wrapNull() { + assertThrows(NullPointerException.class, () -> Bytes.wrap((byte[]) null)); + } + + /** Checks that modifying a wrapped array modifies the value itself. */ + @Test + void wrapReflectsUpdates() { + byte[] bytes = new byte[] {1, 2, 3, 4, 5}; + Bytes value = Bytes.wrap(bytes); + + assertEquals(bytes.length, value.size()); + assertArrayEquals(value.toArrayUnsafe(), bytes); + + bytes[1] = 127; + bytes[3] = 127; + + assertEquals(bytes.length, value.size()); + assertArrayEquals(value.toArrayUnsafe(), bytes); + } + + @Test + void wrapSliceEmpty() { + assertEquals(Bytes.EMPTY, Bytes.wrap(new byte[0], 0, 0)); + assertEquals(Bytes.EMPTY, Bytes.wrap(new byte[] {1, 2, 3}, 0, 0)); + assertEquals(Bytes.EMPTY, Bytes.wrap(new byte[] {1, 2, 3}, 2, 0)); + } + + @ParameterizedTest + @MethodSource("wrapSliceProvider") + void wrapSlice(Object arr, int offset, int length) { + assertWrapSlice((byte[]) arr, offset, length); + } + + @SuppressWarnings("UnusedMethod") + private static Stream wrapSliceProvider() { + return Stream.of( + Arguments.of(new byte[] {1, 2, 3, 4}, 0, 4), + Arguments.of(new byte[] {1, 2, 3, 4}, 0, 2), + Arguments.of(new byte[] {1, 2, 3, 4}, 2, 1), + Arguments.of(new byte[] {1, 2, 3, 4}, 2, 2)); + } + + private void assertWrapSlice(byte[] bytes, int offset, int length) { + Bytes value = Bytes.wrap(bytes, offset, length); + assertEquals(length, value.size()); + assertArrayEquals(value.toArrayUnsafe(), Arrays.copyOfRange(bytes, offset, offset + length)); + } + + @Test + void wrapSliceNull() { + assertThrows(NullPointerException.class, () -> Bytes.wrap(null, 0, 2)); + } + + @Test + void wrapSliceNegativeOffset() { + assertThrows( + IndexOutOfBoundsException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, -1, 4)); + } + + @Test + void wrapSliceOutOfBoundOffset() { + assertThrows( + IndexOutOfBoundsException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, 5, 1)); + } + + @Test + void wrapSliceNegativeLength() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, 0, -2)); + assertEquals("Invalid negative length", exception.getMessage()); + } + + @Test + void wrapSliceTooBig() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, 2, 3)); + assertEquals( + "Provided length 3 is too big: the value has only 2 bytes from offset 2", + exception.getMessage()); + } + + /** + * Checks that modifying a wrapped array modifies the value itself, but only if within the wrapped + * slice. + */ + @Test + void wrapSliceReflectsUpdates() { + byte[] bytes = new byte[] {1, 2, 3, 4, 5}; + assertWrapSlice(bytes, 2, 2); + bytes[2] = 127; + bytes[3] = 127; + assertWrapSlice(bytes, 2, 2); + + Bytes wrapped = Bytes.wrap(bytes, 2, 2); + Bytes copy = wrapped.mutableCopy(); + + // Modify the bytes outside of the wrapped slice and check this doesn't affect the value (that + // it is still equal to the copy from before the updates) + bytes[0] = 127; + assertEquals(copy, wrapped); + + // Sanity check for copy(): modify within the wrapped slice and check the copy differs now. + bytes[2] = 42; + assertEquals("0x2a7f", wrapped.toHexString()); + assertEquals(Bytes.fromHexString("0x7f7f"), copy); + } + + @Test + void ofBytes() { + assertArrayEquals(Bytes.of().toArrayUnsafe(), new byte[] {}); + assertArrayEquals(Bytes.of((byte) 1, (byte) 2).toArrayUnsafe(), new byte[] {1, 2}); + assertArrayEquals( + Bytes.of((byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5).toArrayUnsafe(), + new byte[] {1, 2, 3, 4, 5}); + assertArrayEquals( + Bytes.of((byte) -1, (byte) 2, (byte) -3).toArrayUnsafe(), new byte[] {-1, 2, -3}); + } + + @Test + void ofInts() { + assertArrayEquals(Bytes.of(1, 2).toArrayUnsafe(), new byte[] {1, 2}); + assertArrayEquals(Bytes.of(1, 2, 3, 4, 5).toArrayUnsafe(), new byte[] {1, 2, 3, 4, 5}); + assertArrayEquals(Bytes.of(0xff, 0x7f, 0x80).toArrayUnsafe(), new byte[] {-1, 127, -128}); + } + + @Test + void ofIntsTooBig() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.of(2, 3, 256)); + assertEquals("3th value 256 does not fit a byte", exception.getMessage()); + } + + @Test + void ofIntsTooLow() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.of(2, -1, 3)); + assertEquals("2th value -1 does not fit a byte", exception.getMessage()); + } + + @Test + void minimalBytes() { + assertEquals(h("0x"), Bytes.minimalBytes(0)); + assertEquals(h("0x01"), Bytes.minimalBytes(1)); + assertEquals(h("0x04"), Bytes.minimalBytes(4)); + assertEquals(h("0x10"), Bytes.minimalBytes(16)); + assertEquals(h("0xFF"), Bytes.minimalBytes(255)); + assertEquals(h("0x0100"), Bytes.minimalBytes(256)); + assertEquals(h("0x0200"), Bytes.minimalBytes(512)); + assertEquals(h("0x010000"), Bytes.minimalBytes(1L << 16)); + assertEquals(h("0x01000000"), Bytes.minimalBytes(1L << 24)); + assertEquals(h("0x0100000000"), Bytes.minimalBytes(1L << 32)); + assertEquals(h("0x010000000000"), Bytes.minimalBytes(1L << 40)); + assertEquals(h("0x01000000000000"), Bytes.minimalBytes(1L << 48)); + assertEquals(h("0x0100000000000000"), Bytes.minimalBytes(1L << 56)); + assertEquals(h("0xFFFFFFFFFFFFFFFF"), Bytes.minimalBytes(-1L)); + } + + @Test + void ofUnsignedShort() { + assertEquals(h("0x0000"), Bytes.ofUnsignedShort(0)); + assertEquals(h("0x0001"), Bytes.ofUnsignedShort(1)); + assertEquals(h("0x0100"), Bytes.ofUnsignedShort(256)); + assertEquals(h("0xFFFF"), Bytes.ofUnsignedShort(65535)); + } + + @Test + void ofUnsignedShortLittleEndian() { + assertEquals(h("0x0000"), Bytes.ofUnsignedShort(0, LITTLE_ENDIAN)); + assertEquals(h("0x0100"), Bytes.ofUnsignedShort(1, LITTLE_ENDIAN)); + assertEquals(h("0x0001"), Bytes.ofUnsignedShort(256, LITTLE_ENDIAN)); + assertEquals(h("0xFFFF"), Bytes.ofUnsignedShort(65535, LITTLE_ENDIAN)); + } + + @Test + void ofUnsignedShortNegative() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes.ofUnsignedShort(-1)); + assertEquals( + "Value -1 cannot be represented as an unsigned short (it is negative or too big)", + exception.getMessage()); + } + + @Test + void ofUnsignedShortTooBig() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes.ofUnsignedShort(65536)); + assertEquals( + "Value 65536 cannot be represented as an unsigned short (it is negative or too big)", + exception.getMessage()); + } + + @Test + void asUnsignedBigIntegerConstants() { + assertEquals(bi("0"), Bytes.EMPTY.toUnsignedBigInteger()); + assertEquals(bi("1"), Bytes.of(1).toUnsignedBigInteger()); + } + + @Test + void asSignedBigIntegerConstants() { + assertEquals(bi("0"), Bytes.EMPTY.toBigInteger()); + assertEquals(bi("1"), Bytes.of(1).toBigInteger()); + } + + @Test + void fromHexStringLenient() { + assertEquals(Bytes.of(), Bytes.fromHexStringLenient("")); + assertEquals(Bytes.of(), Bytes.fromHexStringLenient("0x")); + assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("0")); + assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("0x0")); + assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("00")); + assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("0x00")); + assertEquals(Bytes.of(1), Bytes.fromHexStringLenient("0x1")); + assertEquals(Bytes.of(1), Bytes.fromHexStringLenient("0x01")); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("1FF2A")); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1FF2A")); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1ff2a")); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1fF2a")); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("01FF2A")); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01FF2A")); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01ff2A")); + } + + @Test + void compareTo() { + assertEquals(1, Bytes.of(0x05).compareTo(Bytes.of(0x01))); + assertEquals(1, Bytes.of(0x05).compareTo(Bytes.of(0x01))); + assertEquals(1, Bytes.of(0xef).compareTo(Bytes.of(0x01))); + assertEquals(1, Bytes.of(0xef).compareTo(Bytes.of(0x00, 0x01))); + assertEquals(1, Bytes.of(0x00, 0x00, 0xef).compareTo(Bytes.of(0x00, 0x01))); + assertEquals(1, Bytes.of(0x00, 0xef).compareTo(Bytes.of(0x00, 0x00, 0x01))); + assertEquals(1, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0xff))); + assertEquals(1, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0x01))); + assertEquals(1, Bytes.of(0xef, 0xf1).compareTo(Bytes.of(0xef, 0xf0))); + assertEquals(1, Bytes.of(0x00, 0x00, 0x01).compareTo(Bytes.of(0x00, 0x00))); + assertEquals(0, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0xef, 0xf0))); + assertEquals(-1, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0xef, 0xf5))); + assertEquals(-1, Bytes.of(0xef).compareTo(Bytes.of(0xff))); + assertEquals(-1, Bytes.of(0x01).compareTo(Bytes.of(0xff))); + assertEquals(-1, Bytes.of(0x01).compareTo(Bytes.of(0x01, 0xff))); + assertEquals(-1, Bytes.of(0x00, 0x00, 0x01).compareTo(Bytes.of(0x00, 0x02))); + assertEquals(-1, Bytes.of(0x00, 0x01).compareTo(Bytes.of(0x00, 0x00, 0x05))); + assertEquals(0, Bytes.fromHexString("0x0000").compareTo(Bytes.fromHexString("0x00"))); + assertEquals(0, Bytes.fromHexString("0x00").compareTo(Bytes.fromHexString("0x0000"))); + assertEquals(0, Bytes.fromHexString("0x000000").compareTo(Bytes.fromHexString("0x000000"))); + assertEquals(-1, Bytes.fromHexString("0x000001").compareTo(Bytes.fromHexString("0x0001"))); + } + + @Test + void fromHexStringLenientInvalidInput() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("foo")); + assertEquals( + "Illegal character 'o' found at index 1 in hex binary representation", + exception.getMessage()); + } + + @Test + void fromHexStringLenientLeftPadding() { + assertEquals(Bytes.of(), Bytes.fromHexStringLenient("", 0)); + assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("", 1)); + assertEquals(Bytes.of(0, 0), Bytes.fromHexStringLenient("", 2)); + assertEquals(Bytes.of(0, 0), Bytes.fromHexStringLenient("0x", 2)); + assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("0", 3)); + assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("0x0", 3)); + assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("00", 3)); + assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("0x00", 3)); + assertEquals(Bytes.of(0, 0, 1), Bytes.fromHexStringLenient("0x1", 3)); + assertEquals(Bytes.of(0, 0, 1), Bytes.fromHexStringLenient("0x01", 3)); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("1FF2A", 3)); + assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1FF2A", 4)); + assertEquals(Bytes.of(0x00, 0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1ff2a", 5)); + assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1fF2a", 4)); + assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("01FF2A", 4)); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01FF2A", 3)); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01ff2A", 3)); + } + + @Test + void fromHexStringLenientLeftPaddingInvalidInput() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("foo", 10)); + assertEquals( + "Illegal character 'o' found at index 1 in hex binary representation", + exception.getMessage()); + } + + @Test + void fromHexStringLenientLeftPaddingInvalidSize() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("0x001F34", 2)); + assertEquals( + "Hex value is too large: expected at most 2 bytes but got 3", exception.getMessage()); + } + + @Test + void fromHexString() { + assertEquals(Bytes.of(), Bytes.fromHexString("0x")); + assertEquals(Bytes.of(0), Bytes.fromHexString("00")); + assertEquals(Bytes.of(0), Bytes.fromHexString("0x00")); + assertEquals(Bytes.of(1), Bytes.fromHexString("0x01")); + assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("01FF2A")); + assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("0x01FF2A")); + assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("0x01ff2a")); + assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("0x01fF2a")); + } + + @Test + void fromHexStringInvalidInput() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("fooo")); + assertEquals( + "Illegal character 'o' found at index 1 in hex binary representation", + exception.getMessage()); + } + + @Test + void fromHexStringNotLenient() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("0x100")); + assertEquals("Invalid odd-length hex binary representation", exception.getMessage()); + } + + @Test + void fromHexStringLeftPadding() { + assertEquals(Bytes.of(), Bytes.fromHexString("0x", 0)); + assertEquals(Bytes.of(0, 0), Bytes.fromHexString("0x", 2)); + assertEquals(Bytes.of(0, 0, 0, 0), Bytes.fromHexString("0x", 4)); + assertEquals(Bytes.of(0, 0), Bytes.fromHexString("00", 2)); + assertEquals(Bytes.of(0, 0), Bytes.fromHexString("0x00", 2)); + assertEquals(Bytes.of(0, 0, 1), Bytes.fromHexString("0x01", 3)); + assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexString("01FF2A", 4)); + assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexString("0x01FF2A", 3)); + assertEquals(Bytes.of(0x00, 0x00, 0x01, 0xff, 0x2a), Bytes.fromHexString("0x01ff2a", 5)); + assertEquals(Bytes.of(0x00, 0x00, 0x01, 0xff, 0x2a), Bytes.fromHexString("0x01fF2a", 5)); + } + + @Test + void fromHexStringLeftPaddingInvalidInput() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("fooo", 4)); + assertEquals( + "Illegal character 'o' found at index 1 in hex binary representation", + exception.getMessage()); + } + + @Test + void fromHexStringLeftPaddingNotLenient() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("0x100", 4)); + assertEquals("Invalid odd-length hex binary representation", exception.getMessage()); + } + + @Test + void fromHexStringLeftPaddingInvalidSize() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("0x001F34", 2)); + assertEquals( + "Hex value is too large: expected at most 2 bytes but got 3", exception.getMessage()); + } + + @Test + void fromBase64Roundtrip() { + Bytes value = Bytes.fromBase64String("deadbeefISDAbest"); + assertEquals("deadbeefISDAbest", value.toBase64String()); + } + + @Test + void littleEndianRoundtrip() { + int val = Integer.MAX_VALUE - 5; + Bytes littleEndianEncoded = Bytes.ofUnsignedInt(val, LITTLE_ENDIAN); + assertEquals(4, littleEndianEncoded.size()); + Bytes bigEndianEncoded = Bytes.ofUnsignedInt(val); + assertEquals(bigEndianEncoded.get(0), littleEndianEncoded.get(3)); + assertEquals(bigEndianEncoded.get(1), littleEndianEncoded.get(2)); + assertEquals(bigEndianEncoded.get(2), littleEndianEncoded.get(1)); + assertEquals(bigEndianEncoded.get(3), littleEndianEncoded.get(0)); + + int read = littleEndianEncoded.toInt(LITTLE_ENDIAN); + assertEquals(val, read); + } + + @Test + void littleEndianLongRoundtrip() { + long val = 1L << 46; + Bytes littleEndianEncoded = Bytes.ofUnsignedLong(val, LITTLE_ENDIAN); + assertEquals(8, littleEndianEncoded.size()); + Bytes bigEndianEncoded = Bytes.ofUnsignedLong(val); + assertEquals(bigEndianEncoded.get(0), littleEndianEncoded.get(7)); + assertEquals(bigEndianEncoded.get(1), littleEndianEncoded.get(6)); + assertEquals(bigEndianEncoded.get(2), littleEndianEncoded.get(5)); + assertEquals(bigEndianEncoded.get(3), littleEndianEncoded.get(4)); + assertEquals(bigEndianEncoded.get(4), littleEndianEncoded.get(3)); + assertEquals(bigEndianEncoded.get(5), littleEndianEncoded.get(2)); + assertEquals(bigEndianEncoded.get(6), littleEndianEncoded.get(1)); + assertEquals(bigEndianEncoded.get(7), littleEndianEncoded.get(0)); + + long read = littleEndianEncoded.toLong(LITTLE_ENDIAN); + assertEquals(val, read); + } + + @Test + void wrapModifyNoChange() { + Bytes value1 = Bytes.fromHexString("deadbeef"); + Bytes result = Bytes.wrap(value1, value1); + assertEquals(Bytes.fromHexString("deadbeefdeadbeef"), result); + MutableBytes value1Mutable = value1.mutableCopy(); + value1Mutable.set(0, (byte) 0); + assertEquals(Bytes.fromHexString("deadbeefdeadbeef"), result); + } + + @Test + void random() { + Bytes value = Bytes.random(20); + assertNotEquals(value, Bytes.random(20)); + assertEquals(20, value.size()); + } + + @Test + void getInt() { + Bytes value = Bytes.fromHexString("0x00000001"); + assertEquals(1, value.getInt(0)); + assertEquals(16777216, value.getInt(0, LITTLE_ENDIAN)); + assertEquals(1, value.toInt()); + assertEquals(16777216, value.toInt(LITTLE_ENDIAN)); + } + + @Test + void getLong() { + Bytes value = Bytes.fromHexString("0x0000000000000001"); + assertEquals(1, value.getLong(0)); + assertEquals(72057594037927936L, value.getLong(0, LITTLE_ENDIAN)); + assertEquals(1, value.toLong()); + assertEquals(72057594037927936L, value.toLong(LITTLE_ENDIAN)); + } + + @Test + void numberOfLeadingZeros() { + Bytes value = Bytes.fromHexString("0x00000001"); + assertEquals(31, value.numberOfLeadingZeros()); + } + + @Test + void commonPrefix() { + Bytes value = Bytes.fromHexString("0x01234567"); + Bytes value2 = Bytes.fromHexString("0x01236789"); + assertEquals(2, value.commonPrefixLength(value2)); + assertEquals(Bytes.fromHexString("0x0123"), value.commonPrefix(value2)); + } + + @Test + void testWrapByteBufEmpty() { + ByteBuf buffer = Unpooled.buffer(0); + assertSame(Bytes.EMPTY, Bytes.wrapByteBuf(buffer)); + } + + @Test + void testWrapByteBufWithIndexEmpty() { + ByteBuf buffer = Unpooled.buffer(3); + assertSame(Bytes.EMPTY, Bytes.wrapByteBuf(buffer, 3, 0)); + } + + @Test + void testWrapByteBufSizeWithOffset() { + ByteBuf buffer = Unpooled.buffer(10); + assertEquals(1, Bytes.wrapByteBuf(buffer, 1, 1).size()); + } + + @Test + void testWrapByteBufSize() { + ByteBuf buffer = Unpooled.buffer(20); + assertEquals(20, Bytes.wrapByteBuf(buffer).size()); + } + + @Test + void testWrapByteBufReadableBytes() { + ByteBuf buffer = Unpooled.buffer(20).writeByte(3); + assertEquals(1, Bytes.wrapByteBuf(buffer, 0, buffer.readableBytes()).size()); + } + + @Test + void testTrimLeadingZeros() { + Bytes b = Bytes.fromHexString("0x000000f300567800"); + assertEquals(Bytes.fromHexString("0xf300567800"), b.trimLeadingZeros()); + } + + @Test + void testTrimTrailingZeros() { + Bytes b = Bytes.fromHexString("0x000000f300567800"); + assertEquals(Bytes.fromHexString("0x000000f3005678"), b.trimTrailingZeros()); + } +} diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/CommonBytesTest.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/CommonBytesTest.java new file mode 100644 index 000000000..bfdef1df6 --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/CommonBytesTest.java @@ -0,0 +1,624 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.function.Function; + +import io.netty.buffer.Unpooled; +import io.vertx.core.buffer.Buffer; +import org.junit.jupiter.api.Test; + +abstract class CommonBytesTests { + + abstract Bytes h(String hex); + + abstract MutableBytes m(int size); + + abstract Bytes w(byte[] bytes); + + abstract Bytes of(int... bytes); + + BigInteger bi(String decimal) { + return new BigInteger(decimal); + } + + @Test + void asUnsignedBigInteger() { + // Make sure things are interpreted unsigned. + assertEquals(bi("255"), h("0xFF").toUnsignedBigInteger()); + + // Try 2^100 + Long.MAX_VALUE, as an easy to define a big not too special big integer. + BigInteger expected = BigInteger.valueOf(2).pow(100).add(BigInteger.valueOf(Long.MAX_VALUE)); + + // 2^100 is a one followed by 100 zeros, that's 12 bytes of zeros (=96) plus 4 more zeros (so + // 0x10 == 16). + MutableBytes v = m(13); + v.set(0, (byte) 16); + v.setLong(v.size() - 8, Long.MAX_VALUE); + assertEquals(expected, v.toUnsignedBigInteger()); + } + + @Test + void testAsSignedBigInteger() { + // Make sure things are interpreted signed. + assertEquals(bi("-1"), h("0xFF").toBigInteger()); + + // Try 2^100 + Long.MAX_VALUE, as an easy to define a big but not too special big integer. + BigInteger expected = BigInteger.valueOf(2).pow(100).add(BigInteger.valueOf(Long.MAX_VALUE)); + + // 2^100 is a one followed by 100 zeros, that's 12 bytes of zeros (=96) plus 4 more zeros (so + // 0x10 == 16). + MutableBytes v = m(13); + v.set(0, (byte) 16); + v.setLong(v.size() - 8, Long.MAX_VALUE); + assertEquals(expected, v.toBigInteger()); + + // And for a large negative one, we use -(2^100 + Long.MAX_VALUE), which is: + // 2^100 + Long.MAX_VALUE = 0x10(4 bytes of 0)7F( 7 bytes of 1) + // inverse = 0xEF(4 bytes of 1)80( 7 bytes of 0) + // +1 = 0xEF(4 bytes of 1)80(6 bytes of 0)01 + expected = expected.negate(); + v = m(13); + v.set(0, (byte) 0xEF); + for (int i = 1; i < 5; i++) { + v.set(i, (byte) 0xFF); + } + v.set(5, (byte) 0x80); + // 6 bytes of 0 + v.set(12, (byte) 1); + assertEquals(expected, v.toBigInteger()); + } + + @Test + void testSize() { + assertEquals(0, w(new byte[0]).size()); + assertEquals(1, w(new byte[1]).size()); + assertEquals(10, w(new byte[10]).size()); + } + + @Test + void testGet() { + Bytes v = w(new byte[] {1, 2, 3, 4}); + assertEquals((int) (byte) 1, (int) v.get(0)); + assertEquals((int) (byte) 2, (int) v.get(1)); + assertEquals((int) (byte) 3, (int) v.get(2)); + assertEquals((int) (byte) 4, (int) v.get(3)); + } + + @Test + void testGetNegativeIndex() { + assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).get(-1)); + } + + @Test + void testGetOutOfBound() { + assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).get(4)); + } + + @Test + void testGetInt() { + Bytes value = w(new byte[] {0, 0, 1, 0, -1, -1, -1, -1}); + + // 0x00000100 = 256 + assertEquals(256, value.getInt(0)); + // 0x000100FF = 65536 + 255 = 65791 + assertEquals(65791, value.getInt(1)); + // 0x0100FFFF = 16777216 (2^24) + (65536 - 1) = 16842751 + assertEquals(16842751, value.getInt(2)); + // 0xFFFFFFFF = -1 + assertEquals(-1, value.getInt(4)); + } + + @Test + void testGetIntNegativeIndex() { + assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).getInt(-1)); + } + + @Test + void testGetIntOutOfBound() { + assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).getInt(4)); + } + + @Test + void testGetIntNotEnoughBytes() { + assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).getInt(1)); + } + + @Test + void testAsInt() { + assertEquals(0, Bytes.EMPTY.toInt()); + Bytes value1 = w(new byte[] {0, 0, 1, 0}); + // 0x00000100 = 256 + assertEquals(256, value1.toInt()); + assertEquals(256, value1.slice(2).toInt()); + + Bytes value2 = w(new byte[] {0, 1, 0, -1}); + // 0x000100FF = 65536 + 255 = 65791 + assertEquals(65791, value2.toInt()); + assertEquals(65791, value2.slice(1).toInt()); + + Bytes value3 = w(new byte[] {1, 0, -1, -1}); + // 0x0100FFFF = 16777216 (2^24) + (65536 - 1) = 16842751 + assertEquals(16842751, value3.toInt()); + + Bytes value4 = w(new byte[] {-1, -1, -1, -1}); + // 0xFFFFFFFF = -1 + assertEquals(-1, value4.toInt()); + } + + @Test + void testAsIntTooManyBytes() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> w(new byte[] {1, 2, 3, 4, 5}).toInt()); + assertEquals("Value of size 5 has more than 4 bytes", exception.getMessage()); + } + + @Test + void testGetLong() { + Bytes value1 = w(new byte[] {0, 0, 1, 0, -1, -1, -1, -1, 0, 0}); + // 0x00000100FFFFFFFF = (2^40) + (2^32) - 1 = 1103806595071 + assertEquals(1103806595071L, value1.getLong(0)); + // 0x 000100FFFFFFFF00 = (2^48) + (2^40) - 1 - 255 = 282574488338176 + assertEquals(282574488338176L, value1.getLong(1)); + + Bytes value2 = w(new byte[] {-1, -1, -1, -1, -1, -1, -1, -1}); + assertEquals(-1L, value2.getLong(0)); + } + + @Test + void testGetLongNegativeIndex() { + assertThrows( + IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}).getLong(-1)); + } + + @Test + void testGetLongOutOfBound() { + assertThrows( + IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}).getLong(8)); + } + + @Test + void testGetLongNotEnoughBytes() { + assertThrows(IndexOutOfBoundsException.class, () -> w(new byte[] {1, 2, 3, 4}).getLong(0)); + } + + @Test + void testAsLong() { + assertEquals(0, Bytes.EMPTY.toLong()); + Bytes value1 = w(new byte[] {0, 0, 1, 0, -1, -1, -1, -1}); + // 0x00000100FFFFFFFF = (2^40) + (2^32) - 1 = 1103806595071 + assertEquals(1103806595071L, value1.toLong()); + assertEquals(1103806595071L, value1.slice(2).toLong()); + Bytes value2 = w(new byte[] {0, 1, 0, -1, -1, -1, -1, 0}); + // 0x000100FFFFFFFF00 = (2^48) + (2^40) - 1 - 255 = 282574488338176 + assertEquals(282574488338176L, value2.toLong()); + assertEquals(282574488338176L, value2.slice(1).toLong()); + + Bytes value3 = w(new byte[] {-1, -1, -1, -1, -1, -1, -1, -1}); + assertEquals(-1L, value3.toLong()); + } + + @Test + void testAsLongTooManyBytes() { + Throwable exception = + assertThrows( + IllegalArgumentException.class, + () -> w(new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9}).toLong()); + assertEquals("Value of size 9 has more than 8 bytes", exception.getMessage()); + } + + @Test + void testSlice() { + assertEquals(h("0x"), h("0x0123456789").slice(0, 0)); + assertEquals(h("0x"), h("0x0123456789").slice(2, 0)); + assertEquals(h("0x01"), h("0x0123456789").slice(0, 1)); + assertEquals(h("0x0123"), h("0x0123456789").slice(0, 2)); + + assertEquals(h("0x4567"), h("0x0123456789").slice(2, 2)); + assertEquals(h("0x23456789"), h("0x0123456789").slice(1, 4)); + } + + @Test + void testSliceNegativeOffset() { + assertThrows(IndexOutOfBoundsException.class, () -> h("0x012345").slice(-1, 2)); + } + + @Test + void testSliceOffsetOutOfBound() { + assertThrows(IndexOutOfBoundsException.class, () -> h("0x012345").slice(3, 2)); + } + + @Test + void testSliceTooLong() { + Throwable exception = + assertThrows(IllegalArgumentException.class, () -> h("0x012345").slice(1, 3)); + assertEquals( + "Provided length 3 is too big: the value has only 2 bytes from offset 1", + exception.getMessage()); + } + + @Test + void testMutableCopy() { + Bytes v = h("0x012345"); + + MutableBytes dest = v.mutableCopy(); + + // Initially, copy must be equal. + assertEquals(dest, v); + + // Upon modification, original should not have been modified. + dest.set(0, (byte) -1); + assertNotEquals(dest, v); + assertEquals(h("0x012345"), v); + assertEquals(h("0xFF2345"), dest); + + // The follow does nothing, but simply making sure it doesn't throw. + dest = Bytes.EMPTY.mutableCopy(); + assertEquals(Bytes.EMPTY, dest); + + dest = of(1).mutableCopy(); + assertEquals(h("0x01"), dest); + + dest = of(10).mutableCopy(); + assertEquals(h("0x0A"), dest); + + dest = of(0xff, 0x03).mutableCopy(); + assertEquals(h("0xFF03"), dest); + + dest = of(0xff, 0x03).mutableCopy(); + Bytes destView = dest.slice(1, 1); + assertEquals(h("0x03"), destView); + } + + @Test + void testAppendTo() { + testAppendTo(Bytes.EMPTY, Buffer.buffer(), Bytes.EMPTY); + testAppendTo(Bytes.EMPTY, Buffer.buffer(h("0x1234").toArrayUnsafe()), h("0x1234")); + testAppendTo(h("0x1234"), Buffer.buffer(), h("0x1234")); + testAppendTo(h("0x5678"), Buffer.buffer(h("0x1234").toArrayUnsafe()), h("0x12345678")); + } + + private void testAppendTo(Bytes toAppend, Buffer buffer, Bytes expected) { + toAppend.appendTo(buffer); + assertEquals(expected, Bytes.wrap(buffer.getBytes())); + } + + @Test + void testIsZero() { + assertTrue(Bytes.EMPTY.isZero()); + assertTrue(Bytes.of(0).isZero()); + assertTrue(Bytes.of(0, 0, 0).isZero()); + + assertFalse(Bytes.of(1).isZero()); + assertFalse(Bytes.of(1, 0, 0).isZero()); + assertFalse(Bytes.of(0, 0, 1).isZero()); + assertFalse(Bytes.of(0, 0, 1, 0, 0).isZero()); + } + + @Test + void testIsEmpty() { + assertTrue(Bytes.EMPTY.isEmpty()); + + assertFalse(Bytes.of(0).isEmpty()); + assertFalse(Bytes.of(0, 0, 0).isEmpty()); + assertFalse(Bytes.of(1).isEmpty()); + } + + @Test + void findsCommonPrefix() { + Bytes v = Bytes.of(1, 2, 3, 4, 5, 6, 7); + Bytes o = Bytes.of(1, 2, 3, 4, 4, 3, 2); + assertEquals(4, v.commonPrefixLength(o)); + assertEquals(Bytes.of(1, 2, 3, 4), v.commonPrefix(o)); + } + + @Test + void findsCommonPrefixOfShorter() { + Bytes v = Bytes.of(1, 2, 3, 4, 5, 6, 7); + Bytes o = Bytes.of(1, 2, 3, 4); + assertEquals(4, v.commonPrefixLength(o)); + assertEquals(Bytes.of(1, 2, 3, 4), v.commonPrefix(o)); + } + + @Test + void findsCommonPrefixOfLonger() { + Bytes v = Bytes.of(1, 2, 3, 4); + Bytes o = Bytes.of(1, 2, 3, 4, 4, 3, 2); + assertEquals(4, v.commonPrefixLength(o)); + assertEquals(Bytes.of(1, 2, 3, 4), v.commonPrefix(o)); + } + + @Test + void findsCommonPrefixOfSliced() { + Bytes v = Bytes.of(1, 2, 3, 4).slice(2, 2); + Bytes o = Bytes.of(3, 4, 3, 3, 2).slice(3, 2); + assertEquals(1, v.commonPrefixLength(o)); + assertEquals(Bytes.of(3), v.commonPrefix(o)); + } + + @Test + void testTrimLeadingZeroes() { + assertEquals(h("0x"), h("0x").trimLeadingZeros()); + assertEquals(h("0x"), h("0x00").trimLeadingZeros()); + assertEquals(h("0x"), h("0x00000000").trimLeadingZeros()); + + assertEquals(h("0x01"), h("0x01").trimLeadingZeros()); + assertEquals(h("0x01"), h("0x00000001").trimLeadingZeros()); + + assertEquals(h("0x3010"), h("0x3010").trimLeadingZeros()); + assertEquals(h("0x3010"), h("0x00003010").trimLeadingZeros()); + + assertEquals(h("0xFFFFFFFF"), h("0xFFFFFFFF").trimLeadingZeros()); + assertEquals(h("0xFFFFFFFF"), h("0x000000000000FFFFFFFF").trimLeadingZeros()); + } + + @Test + void testQuantityHexString() { + assertEquals("0x0", h("0x").toQuantityHexString()); + assertEquals("0x0", h("0x0000").toQuantityHexString()); + assertEquals("0x1000001", h("0x01000001").toQuantityHexString()); + } + + @Test + void testHexString() { + assertEquals("0x", h("0x").toShortHexString()); + assertEquals("0x", h("0x0000").toShortHexString()); + assertEquals("0x1000001", h("0x01000001").toShortHexString()); + + assertEquals("0000", h("0x0000").toUnprefixedHexString()); + assertEquals("1234", h("0x1234").toUnprefixedHexString()); + assertEquals("0022", h("0x0022").toUnprefixedHexString()); + } + + @Test + void testEllipsisHexString() { + assertEquals("0x", h("0x").toEllipsisHexString()); + assertEquals("0x0000", h("0x0000").toEllipsisHexString()); + assertEquals("0x01000001", h("0x01000001").toEllipsisHexString()); + assertEquals("0x0100000001", h("0x0100000001").toEllipsisHexString()); + assertEquals("0x0100..0001", h("0x010000000001").toEllipsisHexString()); + assertEquals("0x1234..5678", h("0x123456789abcdef012345678").toEllipsisHexString()); + assertEquals("0x1234..789a", h("0x123456789abcdef0123456789a").toEllipsisHexString()); + assertEquals("0x1234..9abc", h("0x123456789abcdef0123456789abc").toEllipsisHexString()); + assertEquals("0x1234..bcde", h("0x123456789abcdef0123456789abcde").toEllipsisHexString()); + assertEquals("0x1234..def0", h("0x123456789abcdef0123456789abcdef0").toEllipsisHexString()); + assertEquals("0x1234..def0", h("0x123456789abcdef0123456789abcdef0").toEllipsisHexString()); + } + + @Test + void slideToEnd() { + assertEquals(Bytes.of(1, 2, 3, 4), Bytes.of(1, 2, 3, 4).slice(0)); + assertEquals(Bytes.of(2, 3, 4), Bytes.of(1, 2, 3, 4).slice(1)); + assertEquals(Bytes.of(3, 4), Bytes.of(1, 2, 3, 4).slice(2)); + assertEquals(Bytes.of(4), Bytes.of(1, 2, 3, 4).slice(3)); + } + + @Test + void slicePastEndReturnsEmpty() { + assertEquals(Bytes.EMPTY, Bytes.of(1, 2, 3, 4).slice(4)); + assertEquals(Bytes.EMPTY, Bytes.of(1, 2, 3, 4).slice(5)); + } + + @Test + void testUpdate() throws NoSuchAlgorithmException { + // Digest the same byte array in 4 ways: + // 1) directly from the array + // 2) after wrapped using the update() method + // 3) after wrapped and copied using the update() method + // 4) after wrapped but getting the byte manually + // and check all compute the same digest. + MessageDigest md1 = MessageDigest.getInstance("SHA-1"); + MessageDigest md2 = MessageDigest.getInstance("SHA-1"); + MessageDigest md3 = MessageDigest.getInstance("SHA-1"); + MessageDigest md4 = MessageDigest.getInstance("SHA-1"); + + byte[] toDigest = new BigInteger("12324029423415041783577517238472017314").toByteArray(); + Bytes wrapped = w(toDigest); + + byte[] digest1 = md1.digest(toDigest); + + wrapped.update(md2); + byte[] digest2 = md2.digest(); + + wrapped.mutableCopy().update(md3); + byte[] digest3 = md3.digest(); + + for (int i = 0; i < wrapped.size(); i++) md4.update(wrapped.get(i)); + byte[] digest4 = md4.digest(); + + assertArrayEquals(digest2, digest1); + assertArrayEquals(digest3, digest1); + assertArrayEquals(digest4, digest1); + } + + @Test + void testArrayExtraction() { + // extractArray() and getArrayUnsafe() have essentially the same contract... + testArrayExtraction(Bytes::toArrayUnsafe); + + // But on top of the basic, extractArray() guarantees modifying the returned array is safe from + // impacting the original value (not that getArrayUnsafe makes no guarantees here one way or + // another, so there is nothing to test). + byte[] orig = new byte[] {1, 2, 3, 4}; + Bytes value = w(orig); + byte[] extracted = value.mutableCopy().toArray(); + assertArrayEquals(orig, extracted); + Arrays.fill(extracted, (byte) -1); + assertArrayEquals(extracted, new byte[] {-1, -1, -1, -1}); + assertArrayEquals(orig, new byte[] {1, 2, 3, 4}); + assertEquals(of(1, 2, 3, 4), value); + } + + private void testArrayExtraction(Function extractor) { + byte[] bytes = new byte[0]; + assertArrayEquals(extractor.apply(Bytes.EMPTY), bytes); + + byte[][] toTest = + new byte[][] {new byte[] {1}, new byte[] {1, 2, 3, 4, 5, 6}, new byte[] {-1, -1, 0, -1}}; + for (byte[] array : toTest) { + assertArrayEquals(extractor.apply(w(array)), array); + } + + // Test slightly more complex interactions + assertArrayEquals( + extractor.apply(w(new byte[] {1, 2, 3, 4, 5}).slice(2, 2)), new byte[] {3, 4}); + assertArrayEquals(extractor.apply(w(new byte[] {1, 2, 3, 4, 5}).slice(2, 0)), new byte[] {}); + } + + @Test + void testToString() { + assertEquals("0x", Bytes.EMPTY.toString()); + + assertEquals("0x01", of(1).toString()); + assertEquals("0x0aff03", of(0x0a, 0xff, 0x03).toString()); + } + + @Test + void testHasLeadingZeroByte() { + assertFalse(Bytes.fromHexString("0x").hasLeadingZeroByte()); + assertTrue(Bytes.fromHexString("0x0012").hasLeadingZeroByte()); + assertFalse(Bytes.fromHexString("0x120012").hasLeadingZeroByte()); + } + + @Test + void testNumberOfLeadingZeroBytes() { + assertEquals(0, Bytes.fromHexString("0x12").numberOfLeadingZeroBytes()); + assertEquals(1, Bytes.fromHexString("0x0012").numberOfLeadingZeroBytes()); + assertEquals(2, Bytes.fromHexString("0x000012").numberOfLeadingZeroBytes()); + assertEquals(0, Bytes.fromHexString("0x").numberOfLeadingZeroBytes()); + assertEquals(1, Bytes.fromHexString("0x00").numberOfLeadingZeroBytes()); + assertEquals(2, Bytes.fromHexString("0x0000").numberOfLeadingZeroBytes()); + assertEquals(3, Bytes.fromHexString("0x000000").numberOfLeadingZeroBytes()); + } + + @Test + void testNumberOfTrailingZeroBytes() { + assertEquals(0, Bytes.fromHexString("0x12").numberOfTrailingZeroBytes()); + assertEquals(1, Bytes.fromHexString("0x1200").numberOfTrailingZeroBytes()); + assertEquals(2, Bytes.fromHexString("0x120000").numberOfTrailingZeroBytes()); + assertEquals(0, Bytes.fromHexString("0x").numberOfTrailingZeroBytes()); + assertEquals(1, Bytes.fromHexString("0x00").numberOfTrailingZeroBytes()); + assertEquals(2, Bytes.fromHexString("0x0000").numberOfTrailingZeroBytes()); + assertEquals(3, Bytes.fromHexString("0x000000").numberOfTrailingZeroBytes()); + } + + @Test + void testHasLeadingZeroBit() { + assertFalse(Bytes.fromHexString("0x").hasLeadingZero()); + assertTrue(Bytes.fromHexString("0x01").hasLeadingZero()); + assertFalse(Bytes.fromHexString("0xFF0012").hasLeadingZero()); + } + + @Test + void testEquals() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key); + Bytes b2 = w(key); + assertEquals(b.hashCode(), b2.hashCode()); + } + + @Test + void testEqualsWithOffset() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key).slice(16, 4); + Bytes b2 = w(key).slice(16, 8).slice(0, 4); + assertEquals(b, b2); + } + + @Test + void testHashCode() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key); + Bytes b2 = w(key); + assertEquals(b.hashCode(), b2.hashCode()); + } + + @Test + void testHashCodeWithOffset() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key).slice(16, 16); + Bytes b2 = w(key).slice(16, 16); + assertEquals(b.hashCode(), b2.hashCode()); + } + + @Test + void testHashCodeWithByteBufferWrappingBytes() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key); + Bytes other = Bytes.wrapByteBuffer(ByteBuffer.wrap(key)); + assertEquals(b.hashCode(), other.hashCode()); + } + + @Test + void testEqualsWithByteBufferWrappingBytes() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key); + Bytes other = Bytes.wrapByteBuffer(ByteBuffer.wrap(key)); + assertEquals(b, other); + } + + @Test + void testHashCodeWithBufferWrappingBytes() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key); + Bytes other = Bytes.wrapBuffer(Buffer.buffer(key)); + assertEquals(b.hashCode(), other.hashCode()); + } + + @Test + void testEqualsWithBufferWrappingBytes() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key); + Bytes other = Bytes.wrapBuffer(Buffer.buffer(key)); + assertEquals(b, other); + } + + @Test + void testHashCodeWithByteBufWrappingBytes() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key); + Bytes other = Bytes.wrapByteBuf(Unpooled.copiedBuffer(key)); + assertEquals(b.hashCode(), other.hashCode()); + } + + @Test + void testEqualsWithByteBufWrappingBytes() { + SecureRandom random = new SecureRandom(); + byte[] key = new byte[32]; + random.nextBytes(key); + Bytes b = w(key); + Bytes other = Bytes.wrapByteBuf(Unpooled.copiedBuffer(key)); + assertEquals(b, other); + } +} diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/ConcatenatedBytesTest.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/ConcatenatedBytesTest.java new file mode 100644 index 000000000..bb36b12f7 --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/ConcatenatedBytesTest.java @@ -0,0 +1,181 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; + +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InOrder; + +class ConcatenatedBytesTest { + + @ParameterizedTest + @MethodSource("concatenatedWrapProvider") + void concatenatedWrap(Object arr1, Object arr2) { + byte[] first = (byte[]) arr1; + byte[] second = (byte[]) arr2; + byte[] res = Bytes.wrap(Bytes.wrap(first), Bytes.wrap(second)).toArrayUnsafe(); + assertArrayEquals(Arrays.copyOfRange(res, 0, first.length), first); + assertArrayEquals(Arrays.copyOfRange(res, first.length, res.length), second); + } + + @SuppressWarnings("UnusedMethod") + private static Stream concatenatedWrapProvider() { + return Stream.of( + Arguments.of(new byte[] {}, new byte[] {}), + Arguments.of(new byte[] {}, new byte[] {1, 2, 3}), + Arguments.of(new byte[] {1, 2, 3}, new byte[] {}), + Arguments.of(new byte[] {1, 2, 3}, new byte[] {4, 5})); + } + + @Test + void testConcatenatedWrapReflectsUpdates() { + byte[] first = new byte[] {1, 2, 3}; + byte[] second = new byte[] {4, 5}; + byte[] expected1 = new byte[] {1, 2, 3, 4, 5}; + Bytes res = Bytes.wrap(Bytes.wrap(first), Bytes.wrap(second)); + assertArrayEquals(res.toArrayUnsafe(), expected1); + + first[1] = 42; + second[0] = 42; + byte[] expected2 = new byte[] {1, 42, 3, 42, 5}; + assertArrayEquals(res.toArrayUnsafe(), expected2); + } + + @Test + void shouldReadConcatenatedValue() { + Bytes bytes = Bytes.wrap(Bytes.fromHexString("0x01234567"), Bytes.fromHexString("0x89ABCDEF")); + assertEquals(8, bytes.size()); + assertEquals("0x0123456789abcdef", bytes.toHexString()); + } + + static Stream sliceProvider() { + return Stream.of( + Arguments.of("0x", 4, 0), + Arguments.of("0x01234567", 0, 4), + Arguments.of("0x0123", 0, 2), + Arguments.of("0x2345", 1, 2), + Arguments.of("0x6789", 3, 2), + Arguments.of("0x89abcdef", 4, 4), + Arguments.of("0xabcd", 5, 2), + Arguments.of("0xef012345", 7, 4), + Arguments.of("0x01234567", 8, 4), + Arguments.of("0x456789abcdef", 10, 6), + Arguments.of("0x89abcdef", 12, 4), + Arguments.of("0xabcd", 13, 2), + Arguments.of("0x0123456789abcdef0123456789ab", 0, 14), + Arguments.of("0x0123456789abcdef0123456789abcd", 0, 15), + Arguments.of("0x0123456789abcdef0123456789abcdef", 0, 16)); + } + + @ParameterizedTest + @MethodSource("sliceProvider") + void sliceValue(String expectedHex, int sliceOffset, int sliceLength) { + Bytes bytes = + Bytes.wrap( + Bytes.fromHexString("0x01234567"), + Bytes.fromHexString("0x89ABCDEF"), + Bytes.fromHexString("0x01234567"), + Bytes.fromHexString("0x89ABCDEF")); + assertEquals(expectedHex, bytes.slice(sliceOffset, sliceLength).toHexString()); + } + + @ParameterizedTest + @MethodSource("sliceProvider") + void slicedValueToArrayUnsafe(String expectedHex, int sliceOffset, int sliceLength) { + Bytes bytes = + Bytes.wrap( + Bytes.fromHexString("0x01234567"), + Bytes.fromHexString("0x89ABCDEF"), + Bytes.fromHexString("0x01234567"), + Bytes.fromHexString("0x89ABCDEF")); + assertThat(Bytes.fromHexString(expectedHex).toArrayUnsafe()) + .containsExactly(bytes.slice(sliceOffset, sliceLength).toArrayUnsafe()); + } + + @Test + void shouldReadDeepConcatenatedValue() { + Bytes bytes = + Bytes.wrap( + Bytes.wrap(Bytes.fromHexString("0x01234567"), Bytes.fromHexString("0x89ABCDEF")), + Bytes.wrap(Bytes.fromHexString("0x01234567"), Bytes.fromHexString("0x89ABCDEF")), + Bytes.fromHexString("0x01234567"), + Bytes.fromHexString("0x89ABCDEF")); + assertEquals(24, bytes.size()); + assertEquals("0x0123456789abcdef0123456789abcdef0123456789abcdef", bytes.toHexString()); + } + + @Test + void testMutableCopy() { + Bytes bytes = Bytes.wrap(Bytes.fromHexString("0x01234567"), Bytes.fromHexString("0x89ABCDEF")); + assertEquals(bytes, bytes.mutableCopy()); + } + + @Test + void testHashcodeUpdates() { + MutableBytes dest = MutableBytes.create(32); + Bytes bytes = Bytes.wrap(dest, Bytes.fromHexString("0x4567")); + int hashCode = bytes.hashCode(); + dest.set(1, (byte) 123); + assertEquals(hashCode, bytes.hashCode()); + } + + @Test + void shouldUpdateMessageDigest() { + Bytes value1 = Bytes.fromHexString("0x01234567"); + Bytes value2 = Bytes.fromHexString("0x89ABCDEF"); + Bytes value3 = Bytes.fromHexString("0x01234567"); + Bytes bytes = Bytes.wrap(value1, value2, value3); + MessageDigest digest = mock(MessageDigest.class); + bytes.update(digest); + + final InOrder inOrder = inOrder(digest); + inOrder.verify(digest).update(value1.toArrayUnsafe(), 0, 4); + inOrder.verify(digest).update(value2.toArrayUnsafe(), 0, 4); + inOrder.verify(digest).update(value3.toArrayUnsafe(), 0, 4); + } + + @Test + void and() { + Bytes part1 = Bytes.of(0xaa, 0xaa); + Bytes part2 = Bytes.of(0xbb, 0xbb); + Bytes expected = Bytes.fromHexString("0x000000000000aaaabbbb"); + + Bytes concat = ConcatenatedBytes.create(part1, part2); + MutableBytes mutableBytes = MutableBytes.fromHexString("0xffffffffffffffffffff"); + assertThat(mutableBytes.and(concat)).isEqualTo(expected); + } + + @Test + void or() { + Bytes part1 = Bytes.of(0xaa, 0xaa); + Bytes part2 = Bytes.of(0xbb, 0xbb); + Bytes expected = Bytes.fromHexString("0xffffffffffffffffbbbb"); + + Bytes concat = ConcatenatedBytes.create(part1, part2); + MutableBytes mutableBytes = MutableBytes.fromHexString("0xffffffffffffffff0000"); + assertThat(mutableBytes.or(concat)).isEqualTo(expected); + } + + @Test + void xor() { + Bytes part1 = Bytes.of(0xaa, 0xaa); + Bytes part2 = Bytes.of(0xbb, 0xbb); + Bytes expected = Bytes.fromHexString("0xffffffffffff55554444"); + + Bytes concat = ConcatenatedBytes.create(part1, part2); + MutableBytes mutableBytes = MutableBytes.fromHexString("0xffffffffffffffffffff"); + assertThat(mutableBytes.xor(concat)).isEqualTo(expected); + } +} diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/DelegateBytesTest.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/DelegateBytesTest.java new file mode 100644 index 000000000..34c9b2bef --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/DelegateBytesTest.java @@ -0,0 +1,30 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +class DelegateBytesTest extends CommonBytesTests { + + @Override + Bytes h(String hex) { + Bytes bytes = Bytes.fromHexString(hex); + return new DelegatingBytes(bytes, bytes.size()); + } + + @Override + MutableBytes m(int size) { + // no-op + return MutableBytes.create(size); + } + + @Override + Bytes w(byte[] bytes) { + Bytes bytesValue = Bytes.wrap(bytes); + return new DelegatingBytes(bytesValue, bytesValue.size()); + } + + @Override + Bytes of(int... bytes) { + Bytes bytesValue = Bytes.of(bytes); + return new DelegatingBytes(bytesValue, bytesValue.size()); + } +} diff --git a/bytes/src/test/java/org/apache/tuweni/v2/bytes/MutableBytesTest.java b/bytes/src/test/java/org/apache/tuweni/v2/bytes/MutableBytesTest.java new file mode 100644 index 000000000..afe34b1ce --- /dev/null +++ b/bytes/src/test/java/org/apache/tuweni/v2/bytes/MutableBytesTest.java @@ -0,0 +1,421 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.bytes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.nio.ByteBuffer; + +import io.netty.buffer.Unpooled; +import io.vertx.core.buffer.Buffer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class MutableBytesTest { + + @Test + void testMutableBytesWrap() { + MutableBytes b = MutableBytes.fromArray(Bytes.fromHexString("deadbeef").toArrayUnsafe(), 1, 3); + assertEquals(Bytes.fromHexString("adbeef"), b); + } + + @Test + void testClear() { + MutableBytes b = MutableBytes.fromHexString("deadbeef"); + assertEquals(Bytes.fromHexString("00000000"), b.clear()); + } + + @Test + void testFill() { + MutableBytes b = MutableBytes.create(2); + assertEquals(Bytes.fromHexString("0x2222"), b.fill((byte) 34)); + } + + @Test + void testDecrementAndIncrement() { + MutableBytes b = MutableBytes.create(2); + assertEquals(Bytes.fromHexString("0x0001"), b.increment()); + assertEquals(Bytes.fromHexString("0x0000"), b.decrement()); + + assertEquals(Bytes.fromHexString("0xFFFE"), b.fill((byte) 0xFF).decrement()); + + b = MutableBytes.fromHexString("0x00FF"); + assertEquals(Bytes.fromHexString("0x0100"), b.increment()); + } + + @Test + void setLong() { + MutableBytes b = MutableBytes.create(8); + b.setLong(0, 256); + assertEquals(Bytes.fromHexString("0x0000000000000100"), b); + } + + @Test + void setInt() { + MutableBytes b = MutableBytes.create(4); + b.setInt(0, 256); + assertEquals(Bytes.fromHexString("0x00000100"), b); + } + + @Test + void setIntOverflow() { + MutableBytes b = MutableBytes.create(2); + assertThrows(IndexOutOfBoundsException.class, () -> b.setInt(0, 18096)); + } + + @Test + void setLongOverflow() { + MutableBytes b = MutableBytes.create(6); + assertThrows(IndexOutOfBoundsException.class, () -> b.setLong(0, Long.MAX_VALUE)); + } + + @Test + void wrapEmpty() { + MutableBytes b = MutableBytes.fromBuffer(Buffer.buffer()); + assertEquals(b.size(), 0); + b = MutableBytes.fromByteBuf(Unpooled.buffer(0)); + assertEquals(b.size(), 0); + } + + @Test + void testHashcodeUpdates() { + MutableBytes dest = MutableBytes.create(32); + int hashCode = dest.hashCode(); + dest.set(1, (byte) 123); + assertNotEquals(hashCode, dest.hashCode()); + } + + @Test + void testHashcodeUpdatesBuffer() { + MutableBytes dest = MutableBytes.fromBuffer(Buffer.buffer(new byte[4])); + int hashCode = dest.hashCode(); + dest.set(1, (byte) 123); + assertNotEquals(hashCode, dest.hashCode()); + } + + @Test + void testHashcodeUpdatesByteBuffer() { + MutableBytes dest = MutableBytes.fromByteBuffer(ByteBuffer.wrap(new byte[4])); + int hashCode = dest.hashCode(); + dest.set(1, (byte) 123); + assertNotEquals(hashCode, dest.hashCode()); + } + + @Test + void testHashcodeUpdatesByteBuf() { + MutableBytes dest = MutableBytes.fromByteBuf(Unpooled.buffer(4)); + int hashCode = dest.hashCode(); + dest.set(1, (byte) 123); + assertNotEquals(hashCode, dest.hashCode()); + } + + @ParameterizedTest + @CsvSource({ + "0x01, 0x00, 1", + "0x02, 0x01, 1", + "0x04, 0x01, 2", + "0x08, 0x01, 3", + "0x10, 0x01, 4", + "0xFF, 0x0F, 4", + "0xFFFF, 0x00FF, 8", + "0x1234, 0x0123, 4", + "0x8000, 0x0001, 15", + "0x321243, 0x000000, 25", + "0x213211AD, 0x00000213, 20", + "0x7FFFFFFF, 0x3FFFFFFF, 1", + "0xFFFFFFFF, 0x0FFFFFFF, 4", + "0xABCDEF, 0x00ABCD, 8", + "0x12345678, 0x01234567, 4", + "0x00, 0x00, 1", + "0x01, 0x01, 0", + "0xAA55, 0x0552, 5", + "0x01000001, 0x00400000, 2" + }) + void shiftRight(String bytesValue, String expected, int shiftBits) { + MutableBytes value = MutableBytes.fromHexString(bytesValue); + assertEquals(Bytes.fromHexString(expected), value.shiftRight(shiftBits)); + } + + @ParameterizedTest + @CsvSource({ + "0x80, 0x00, 1", + "0x40, 0x80, 1", + "0x20, 0x80, 2", + "0x10, 0x80, 3", + "0x08, 0x80, 4", + "0xFF, 0xF0, 4", + "0xFFFF, 0xFF00, 8", + "0x1234, 0x2340, 4", + "0x0001, 0x8000, 15", + "0x321243, 0x000000, 25", + "0x213211AD, 0x1AD00000, 20", + "0xFFFFFFFE, 0xFFFFFFFC, 1", + "0xFFFFFFFF, 0xFFFFFFF0, 4", + "0xABCDEF, 0xCDEF00, 8", + "0x12345678, 0x23456780, 4", + "0x00, 0x00, 1", + "0x80, 0x80, 0", + "0xAA55, 0x4AA0, 5", + "0xAA, 0x40, 5", + "0x01000001, 0x04000004, 2" + }) + void shiftLeft(String bytesValue, String expected, int shiftBits) { + MutableBytes value = MutableBytes.fromHexString(bytesValue); + assertEquals(Bytes.fromHexString(expected), value.shiftLeft(shiftBits)); + } + + @ParameterizedTest + @CsvSource({ + "0x0102, 0x00000102, 4", + "0x0102, 0x0102, 2", + "0x0102, 0x0102, 0", + "0x, 0x000000, 3", + "0x, 0x, 0", + "0x01, 0x000000000001, 6", + "0xFF, 0x00FF, 2", + "0x0000, 0x00000000, 4", + "0x000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000, 29", + "0xE000000000E000000000E000000000, 0x0000000000000000000000000000000000E000000000E000000000E000000000, 32", + "0x123456789ABCDEF0, 0x123456789ABCDEF0, 4" + }) + void leftPad(String bytesValue, String expected, int size) { + MutableBytes value = MutableBytes.fromHexString(bytesValue); + assertEquals(Bytes.fromHexString(expected), value.leftPad(size)); + } + + @ParameterizedTest + @CsvSource({ + "0x0102, 0x01020000, 4", + "0x0102, 0x0102, 2", + "0x0102, 0x0102, 0", + "0x, 0x000000, 3", + "0x, 0x, 0", + "0x01, 0x010000000000, 6", + "0xFF, 0xFF00, 2", + "0x0000, 0x00000000, 4", + "0x000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000, 29", + "0xE000000000E000000000E000000000, 0xE000000000E000000000E0000000000000000000000000000000000000000000, 32", + "0x123456789ABCDEF0, 0x123456789ABCDEF0, 4" + }) + void rightPad(String bytesValue, String expected, int size) { + MutableBytes value = MutableBytes.fromHexString(bytesValue); + assertEquals(Bytes.fromHexString(expected), value.rightPad(size)); + } + + @ParameterizedTest + @CsvSource({"0x000102030405, 0x050403020100", "0x, 0x"}) + void reverse(String bytesValue, String expectedValue) { + MutableBytes bytes = MutableBytes.fromHexString(bytesValue); + assertEquals(MutableBytes.fromHexString(expectedValue), bytes.reverse()); + } + + @ParameterizedTest + @CsvSource({"0x01, 0x02", "0x01FF, 0x0200", "0xFFFFFF, 0x000000"}) + void increment(String bytesValue, String expectedBytes) { + MutableBytes one = MutableBytes.fromHexString(bytesValue); + one.increment(); + assertEquals(MutableBytes.fromHexString(expectedBytes), one); + } + + @ParameterizedTest + @CsvSource({"0x02, 0x01", "0x0100, 0x00FF", "0x000000, 0xFFFFFF"}) + void decrement(String bytesValue, String expectedBytes) { + MutableBytes one = MutableBytes.fromHexString(bytesValue); + one.decrement(); + assertEquals(MutableBytes.fromHexString(expectedBytes), one); + } + + @ParameterizedTest + @CsvSource({ + "0x0F, 0xF0", + "0xA5, 0x5A", + "0xFF, 0x00", + "0xAA, 0x55", + "0x, 0x", + "0x123456, 0xEDCBA9", + "0xABCDEF, 0x543210", + "DEADBEEF, 21524110", + "0x000000, 0xFFFFFF", + "0x0100, 0xFEFF", + "0x01000001, 0xfefffffe" + }) + void not(String bytesValue, String expectedBytes) { + MutableBytes value = MutableBytes.fromHexString(bytesValue).not(); + assertEquals(MutableBytes.fromHexString(expectedBytes), value); + } + + @ParameterizedTest + @CsvSource({ + "0x0F, 0xF0, 0x00", + "0xA5, 0x5A, 0x00", + "0xFF, 0xFF, 0xFF", + "0x00, 0x00, 0x00", + "0xAA, 0x55, 0x00", + "0x, 0x, 0x", + "0x123456, 0x654321, 0x000000", + "0xABCDEF, 0x123456, 0x020446", + "0xDEADBEEF, 0xCAFEBABE, 0xCAACBAAE", + "0x01000001, 0x01000000, 0x01000000", + "0xFF, 0xF012, 0x0012", + "0xF012, 0xFF, 0x0012", + "0xFF, 0x, 0x00", + "0x, 0xFF, 0x00", + "0xDEADBEEF, 0x0000BABE, 0x0000BAAE", + "0x0000BEEF, 0xCAFEBABE, 0x0000BAAE" + }) + void and(String bytes1, String bytes2, String expectedResult) { + MutableBytes value = MutableBytes.fromHexString(bytes1).and(Bytes.fromHexString(bytes2)); + assertEquals(Bytes.fromHexString(expectedResult), value); + } + + @ParameterizedTest + @CsvSource({ + "0, 4, 0xCAACBAAE", + "1, 3, 0x00ACBAAE", + "0, 3, 0x00DEA8BE", + "0, 2, 0x00009AAC", + "2, 1, 0x000000BE", + "2, 2, 0x0000BAAE" + }) + void andOffsetLengthFirstOperand(int offset, int length, String expectedResult) { + Bytes firstOperand = Bytes.fromHexString("0xDEADBEEF"); + Bytes secondOperand = Bytes.fromHexString("0xCAFEBABE"); + firstOperand = firstOperand.slice(offset, length); + assertEquals( + Bytes.fromHexString(expectedResult), firstOperand.mutableCopy().and(secondOperand)); + } + + @ParameterizedTest + @CsvSource({ + "0, 4, 0xCAACBAAE", + "1, 3, 0x00ACBAAE", + "0, 3, 0x0088BEAA", + "0, 2, 0x00008AEE", + "2, 1, 0x000000AA", + "2, 2, 0x0000BAAE" + }) + void andOffsetLengthSecondOperand(int offset, int length, String expectedResult) { + Bytes firstOperand = Bytes.fromHexString("0xDEADBEEF"); + Bytes secondOperand = Bytes.fromHexString("0xCAFEBABE"); + secondOperand = secondOperand.slice(offset, length); + assertEquals( + Bytes.fromHexString(expectedResult), firstOperand.mutableCopy().and(secondOperand)); + } + + @ParameterizedTest + @CsvSource({ + "0x0F, 0xF0, 0xFF", + "0xA5, 0x5A, 0xFF", + "0xFF, 0xFF, 0xFF", + "0x00, 0x00, 0x00", + "0xAA, 0x55, 0xFF", + "0x, 0x, 0x", + "0x123456, 0x654321, 0x777777", + "0xABCDEF, 0x123456, 0xBBFDFF", + "0xDEADBEEF, 0xCAFEBABE, 0xDEFFBEFF", + "0x01000001, 0x01000000, 0x01000001", + "0x0F, 0xF0F0, 0xF0FF", + "0xF0F0, 0x0F, 0xF0FF", + "0xFF, 0x, 0xFF", + "0x, 0xFF, 0xFF", + "0xDEADBEEF, 0x0000BABE, 0xDEADBEFF", + "0x0000BEEF, 0xCAFEBABE, 0xCAFEBEFF" + }) + void or(String bytes1, String bytes2, String expectedResult) { + MutableBytes value = MutableBytes.fromHexString(bytes1).or(Bytes.fromHexString(bytes2)); + assertEquals(Bytes.fromHexString(expectedResult), value); + } + + @ParameterizedTest + @CsvSource({ + "0, 4, 0xDEFFBEFF", + "1, 3, 0xCAFFBEFF", + "0, 3, 0xCAFEBFBE", + "0, 2, 0xCAFEFEBF", + "2, 1, 0xCAFEBABE", + "2, 2, 0xCAFEBEFF" + }) + void orOffsetLengthFirstOperand(int offset, int length, String expectedResult) { + Bytes firstOperand = Bytes.fromHexString("0xDEADBEEF"); + Bytes secondOperand = Bytes.fromHexString("0xCAFEBABE"); + firstOperand = firstOperand.slice(offset, length); + assertEquals(Bytes.fromHexString(expectedResult), firstOperand.mutableCopy().or(secondOperand)); + } + + @ParameterizedTest + @CsvSource({ + "0, 4, 0xDEFFBEFF", + "1, 3, 0xDEFFBEFF", + "0, 3, 0xDEEFFEFF", + "0, 2, 0xDEADFEFF", + "2, 1, 0xDEADBEFF", + "2, 2, 0xDEADBEFF" + }) + void orOffsetLengthSecondOperand(int offset, int length, String expectedResult) { + Bytes firstOperand = Bytes.fromHexString("0xDEADBEEF"); + Bytes secondOperand = Bytes.fromHexString("0xCAFEBABE"); + secondOperand = secondOperand.slice(offset, length); + assertEquals(Bytes.fromHexString(expectedResult), firstOperand.mutableCopy().or(secondOperand)); + } + + @ParameterizedTest + @CsvSource({ + "0x0F, 0xF0, 0xFF", + "0xA5, 0x5A, 0xFF", + "0xFF, 0xFF, 0x00", + "0x00, 0x00, 0x00", + "0xAA, 0x55, 0xFF", + "0x, 0x, 0x", + "0x123456, 0x654321, 0x777777", + "0xABCDEF, 0x123456, 0xB9F9B9", + "0xDEADBEEF, 0xCAFEBABE, 0x14530451", + "0x01000001, 0x01000000, 0x00000001", + "0x0F, 0xF0F0, 0xF0FF", + "0xF0F0, 0x0F, 0xF0FF", + "0xFF, 0x, 0xFF", + "0x, 0xFF, 0xFF", + "0x0000BEEF, 0xCAFEBABE, 0xCAFE0451", + "0xDEADBEEF, 0x0000BABE, 0xDEAD0451" + }) + void xor(String bytes1, String bytes2, String expectedResult) { + MutableBytes value = MutableBytes.fromHexString(bytes1).xor(Bytes.fromHexString(bytes2)); + assertEquals(Bytes.fromHexString(expectedResult), value); + } + + @ParameterizedTest + @CsvSource({ + "0, 4, 0x14530451", + "1, 3, 0xCA530451", + "0, 3, 0xCA201700", + "0, 2, 0xCAFE6413", + "2, 1, 0xCAFEBA00", + "2, 2, 0xCAFE0451" + }) + void xorOffsetLengthFirstOperand(int offset, int length, String expectedResult) { + Bytes firstOperand = Bytes.fromHexString("0xDEADBEEF"); + Bytes secondOperand = Bytes.fromHexString("0xCAFEBABE"); + firstOperand = firstOperand.slice(offset, length); + assertEquals( + Bytes.fromHexString(expectedResult), firstOperand.mutableCopy().xor(secondOperand)); + } + + @ParameterizedTest + @CsvSource({ + "0, 4, 0x14530451", + "1, 3, 0xDE530451", + "0, 3, 0xDE674055", + "0, 2, 0xDEAD7411", + "2, 1, 0xDEADBE55", + "2, 2, 0xDEAD0451" + }) + void xorOffsetLengthSecondOperand(int offset, int length, String expectedResult) { + Bytes firstOperand = Bytes.fromHexString("0xDEADBEEF"); + Bytes secondOperand = Bytes.fromHexString("0xCAFEBABE"); + secondOperand = secondOperand.slice(offset, length); + assertEquals( + Bytes.fromHexString(expectedResult), firstOperand.mutableCopy().xor(secondOperand)); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/Hash.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/Hash.java new file mode 100644 index 000000000..7bfa5ceda --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/Hash.java @@ -0,0 +1,296 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto; + +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.crypto.sodium.SHA256Hash; +import org.apache.tuweni.v2.crypto.sodium.Sodium; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Various utilities for providing hashes (digests) of arbitrary data. + * + *

Requires the BouncyCastleProvider to be loaded and available. See + * https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation for detail. + */ +public final class Hash { + static boolean USE_SODIUM = + Boolean.parseBoolean(System.getProperty("org.apache.tuweni.crypto.useSodium", "true")); + + private Hash() {} + + // SHA-2 + private static final String SHA2_256 = "SHA-256"; + private static final String SHA2_512_256 = "SHA-512/256"; + + // Keccak + private static final String KECCAK_256 = "KECCAK-256"; + private static final String KECCAK_512 = "KECCAK-512"; + + static final ThreadLocal> cachedDigests = + ThreadLocal.withInitial(ConcurrentHashMap::new); + + // SHA-3 + private static final String SHA3_256 = "SHA3-256"; + private static final String SHA3_512 = "SHA3-512"; + + /** + * Helper method to generate a digest using the provided algorithm. + * + * @param input The input bytes to produce the digest for. + * @param alg The name of the digest algorithm to use. + * @return A digest. + * @throws NoSuchAlgorithmException If no Provider supports a MessageDigestSpi implementation for + * the specified algorithm. + */ + public static byte[] digestUsingAlgorithm(byte[] input, String alg) + throws NoSuchAlgorithmException { + requireNonNull(input); + requireNonNull(alg); + try { + MessageDigest digest = + cachedDigests + .get() + .computeIfAbsent( + alg, + (key) -> { + try { + return MessageDigest.getInstance(key); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + }); + digest.update(input); + return digest.digest(); + } catch (RuntimeException e) { + if (e.getCause() instanceof NoSuchAlgorithmException) { + throw (NoSuchAlgorithmException) e.getCause(); + } else { + throw e; + } + } + } + + /** + * Helper method to generate a digest using the provided algorithm. + * + * @param input The input bytes to produce the digest for. + * @param alg The name of the digest algorithm to use. + * @return A digest. + * @throws NoSuchAlgorithmException If no Provider supports a MessageDigestSpi implementation for + * the specified algorithm. + */ + public static Bytes digestUsingAlgorithm(Bytes input, String alg) + throws NoSuchAlgorithmException { + requireNonNull(input); + return Bytes.wrap(digestUsingAlgorithm(input.toArrayUnsafe(), alg)); + } + + /** + * Digest using SHA2-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static byte[] sha2_256(byte[] input) { + if (isSodiumAvailable()) { + SHA256Hash.Input shaInput = SHA256Hash.Input.fromBytes(input); + try { + SHA256Hash.Hash result = SHA256Hash.hash(shaInput); + try { + return result.bytesArray(); + } finally { + result.destroy(); + } + } finally { + shaInput.destroy(); + } + } + try { + return digestUsingAlgorithm(input, SHA2_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA2-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static Bytes sha2_256(Bytes input) { + if (isSodiumAvailable()) { + SHA256Hash.Input shaInput = SHA256Hash.Input.fromBytes(input); + try { + SHA256Hash.Hash result = SHA256Hash.hash(shaInput); + try { + return result.bytes(); + } finally { + result.destroy(); + } + } finally { + shaInput.destroy(); + } + } + try { + return digestUsingAlgorithm(input, SHA2_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + private static boolean isSodiumAvailable() { + if (!USE_SODIUM) { + return false; + } + USE_SODIUM = Sodium.isAvailable(); + return USE_SODIUM; + } + + /** + * Digest using SHA2-512/256. + * + * @param input The value to encode. + * @return A digest. + */ + public static byte[] sha2_512_256(byte[] input) { + try { + return digestUsingAlgorithm(input, SHA2_512_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA-512/256. + * + * @param input The value to encode. + * @return A digest. + */ + public static Bytes sha2_512_256(Bytes input) { + try { + return digestUsingAlgorithm(input, SHA2_512_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using keccak-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static byte[] keccak256(byte[] input) { + try { + return digestUsingAlgorithm(input, KECCAK_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using keccak-256. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static Bytes keccak256(Bytes input) { + try { + return digestUsingAlgorithm(input, KECCAK_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using keccak-512. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static byte[] keccak512(byte[] input) { + try { + return digestUsingAlgorithm(input, KECCAK_512); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using keccak-512. + * + * @param input The input bytes to produce the digest for. + * @return A digest. + */ + public static Bytes keccak512(Bytes input) { + try { + return digestUsingAlgorithm(input, KECCAK_512); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA3-256. + * + * @param input The value to encode. + * @return A digest. + */ + public static byte[] sha3_256(byte[] input) { + try { + return digestUsingAlgorithm(input, SHA3_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA3-256. + * + * @param input The value to encode. + * @return A digest. + */ + public static Bytes sha3_256(Bytes input) { + try { + return digestUsingAlgorithm(input, SHA3_256); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA3-512. + * + * @param input The value to encode. + * @return A digest. + */ + public static byte[] sha3_512(byte[] input) { + try { + return digestUsingAlgorithm(input, SHA3_512); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } + + /** + * Digest using SHA3-512. + * + * @param input The value to encode. + * @return A digest. + */ + public static Bytes sha3_512(Bytes input) { + try { + return digestUsingAlgorithm(input, SHA3_512); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/SECP256K1.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/SECP256K1.java new file mode 100644 index 000000000..c19922395 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/SECP256K1.java @@ -0,0 +1,1045 @@ +/* + * Copyright 2011 Google Inc. + * Copyright 2014 Andreas Schildbach + * Copyright 2014-2016 the libsecp256k1 contributors + * + * 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 org.apache.tuweni.v2.crypto; + +import static java.nio.file.StandardOpenOption.READ; +import static org.apache.tuweni.io.file.Files.atomicReplace; +import static org.apache.tuweni.v2.crypto.Hash.keccak256; + +import org.apache.tuweni.crypto.DecryptionException; +import org.apache.tuweni.crypto.EncryptionException; +import org.apache.tuweni.crypto.InvalidSEC256K1SecretKeyStoreException; +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.MutableBytes; +import org.apache.tuweni.v2.units.bigints.UInt256; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.Objects; +import javax.crypto.Cipher; +import javax.security.auth.Destroyable; + +import org.bouncycastle.asn1.sec.SECNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.asn1.x9.X9IntegerConverter; +import org.bouncycastle.crypto.agreement.ECDHBasicAgreement; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.HMacDSAKCalculator; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey; +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.spec.ECParameterSpec; +import org.bouncycastle.jce.spec.ECPrivateKeySpec; +import org.bouncycastle.jce.spec.ECPublicKeySpec; +import org.bouncycastle.math.ec.ECAlgorithms; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.math.ec.custom.sec.SecP256K1Curve; +import org.jetbrains.annotations.Nullable; + +/* + * Adapted from the BitcoinJ ECKey (Apache 2 License) implementation: + * https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/core/ECKey.java + * + */ + +/** + * An Elliptic Curve Digital Signature using parameters as used by Bitcoin, and defined in Standards for Efficient + * Cryptography (SEC) (Certicom Research, http://www.secg.org/sec2-v2.pdf). + * + *

+ * This class depends upon the BouncyCastle library being available and added as a {@link java.security.Provider}. See + * https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation. + * + *

+ * BouncyCastle can be included using the gradle dependency 'org.bouncycastle:bcprov-jdk15on'. + */ +public final class SECP256K1 { + private SECP256K1() {} + + private static final String ALGORITHM = "ECDSA"; + private static final String CURVE_NAME = "secp256k1"; + private static final String PROVIDER = "BC"; + + // Lazily initialize parameters by using java initialization on demand + public static final class Parameters { + public static final ECDomainParameters CURVE; + static final ECParameterSpec PARAMETER_SPEC; + static final BigInteger CURVE_ORDER; + static final BigInteger HALF_CURVE_ORDER; + static final KeyPairGenerator KEY_PAIR_GENERATOR; + static final X9IntegerConverter X_9_INTEGER_CONVERTER; + + static { + try { + Class.forName("org.bouncycastle.asn1.sec.SECNamedCurves"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException( + "BouncyCastle is not available on the classpath, see https://www.bouncycastle.org/latest_releases.html"); + } + X9ECParameters params = SECNamedCurves.getByName(CURVE_NAME); + CURVE = new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH()); + PARAMETER_SPEC = new ECParameterSpec( + params.getCurve(), + CURVE.getG(), + CURVE.getN(), + CURVE.getH()); + CURVE_ORDER = CURVE.getN(); + HALF_CURVE_ORDER = CURVE_ORDER.shiftRight(1); + if (CURVE_ORDER.compareTo(SecP256K1Curve.q) >= 0) { + throw new IllegalStateException("secp256k1.n should be smaller than secp256k1.q, but is not"); + } + try { + KEY_PAIR_GENERATOR = KeyPairGenerator.getInstance(ALGORITHM, PROVIDER); + } catch (NoSuchProviderException e) { + throw new IllegalStateException( + "BouncyCastleProvider is not available, see https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation", + e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Algorithm should be available but was not", e); + } + ECGenParameterSpec ecGenParameterSpec = new ECGenParameterSpec(CURVE_NAME); + try { + KEY_PAIR_GENERATOR.initialize(ecGenParameterSpec, new SecureRandom()); + } catch (InvalidAlgorithmParameterException e) { + throw new IllegalStateException("Algorithm parameter should be available but was not", e); + } + + X_9_INTEGER_CONVERTER = new X9IntegerConverter(); + } + } + + // Decompress a compressed public key (x co-ord and low-bit of y-coord). + @Nullable + private static ECPoint decompressKey(BigInteger xBN, boolean yBit) { + byte[] compEnc = Parameters.X_9_INTEGER_CONVERTER + .integerToBytes(xBN, 1 + Parameters.X_9_INTEGER_CONVERTER.getByteLength(Parameters.CURVE.getCurve())); + compEnc[0] = (byte) (yBit ? 0x03 : 0x02); + try { + return Parameters.CURVE.getCurve().decodePoint(compEnc); + } catch (IllegalArgumentException e) { + // the compressed key was invalid + return null; + } + } + + /** + * Given the components of a signature and a selector value, recover and return the public key that generated the + * signature according to the algorithm in SEC1v2 section 4.1.6. + * + *

+ * The recovery id is an index from 0 to 3 which indicates which of the 4 possible keys is the correct one. Because + * the key recovery operation yields multiple potential keys, the correct key must either be stored alongside the + * signature, or you must be willing to try each recovery id in turn until you find one that outputs the key you are + * expecting. + * + *

+ * If this method returns null it means recovery was not possible and recovery id should be iterated. + * + *

+ * Given the above two points, a correct usage of this method is inside a for loop from 0 to 3, and if the output is + * null OR a key that is not the one you expect, you try again with the next recovery id. + * + * @param v Which possible key to recover - can be null if either key can be attempted. + * @param r The R component of the signature. + * @param s The S component of the signature. + * @param messageHash Hash of the data that was signed. + * @return A ECKey containing only the public part, or {@code null} if recovery wasn't possible. + */ + @Nullable + private static BigInteger recoverFromSignature(int v, BigInteger r, BigInteger s, Bytes messageHash) { + assert (v == 0 || v == 1); + assert (r.signum() >= 0); + assert (s.signum() >= 0); + assert (messageHash != null); + + // Compressed keys require you to know an extra bit of data about the y-coord as there are two possibilities. + // So it's encoded in the recovery id (v). + ECPoint R = decompressKey(r, (v & 1) == 1); + // 1.4. If nR != point at infinity, then do another iteration of Step 1 (callers responsibility). + if (R == null || !R.multiply(Parameters.CURVE_ORDER).isInfinity()) { + return null; + } + + // 1.5. Compute e from M using Steps 2 and 3 of ECDSA signature verification. + BigInteger e = messageHash.toUnsignedBigInteger(); + // 1.6. For k from 1 to 2 do the following. (loop is outside this function via iterating v) + // 1.6.1. Compute a candidate public key as: + // Q = mi(r) * (sR - eG) + // + // Where mi(x) is the modular multiplicative inverse. We transform this into the following: + // Q = (mi(r) * s ** R) + (mi(r) * -e ** G) + // Where -e is the modular additive inverse of e, that is z such that z + e = 0 (mod n). + // In the above equation ** is point multiplication and + is point addition (the EC group + // operator). + // + // We can find the additive inverse by subtracting e from zero then taking the mod. For example the additive + // inverse of 3 modulo 11 is 8 because 3 + 8 mod 11 = 0, and -3 mod 11 = 8. + BigInteger eInv = BigInteger.ZERO.subtract(e).mod(Parameters.CURVE_ORDER); + BigInteger rInv = r.modInverse(Parameters.CURVE_ORDER); + BigInteger srInv = rInv.multiply(s).mod(Parameters.CURVE_ORDER); + BigInteger eInvrInv = rInv.multiply(eInv).mod(Parameters.CURVE_ORDER); + ECPoint q = ECAlgorithms.sumOfTwoMultiplies(Parameters.CURVE.getG(), eInvrInv, R, srInv); + + if (q.isInfinity()) { + return null; + } + + byte[] qBytes = q.getEncoded(false); + // We remove the prefix + return new BigInteger(1, Arrays.copyOfRange(qBytes, 1, qBytes.length)); + } + + /** + * Encrypts bytes using a public key. + * @param publicKey the public key for encryption + * @param payload the payload to encrypt + * @return the encrypted data + */ + public static Bytes encrypt(SECP256K1.PublicKey publicKey, Bytes payload) { + try { + ECPoint ecPoint = publicKey.asEcPoint(); + ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint, Parameters.PARAMETER_SPEC); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + java.security.PublicKey bcKey = keyFactory.generatePublic(keySpec); + + Cipher iesCipher = Cipher.getInstance("ECIES", "BC"); + iesCipher.init(Cipher.ENCRYPT_MODE, bcKey); + byte[] output = iesCipher.doFinal(payload.toArrayUnsafe()); + return Bytes.wrap(output); + } catch(Exception e) { + throw new EncryptionException(e); + } + } + + public static Bytes decrypt(SECP256K1.SecretKey secretKey, Bytes encrypted) { + try { + ECPrivateKeySpec keySpec = new ECPrivateKeySpec(secretKey.bytes().toUnsignedBigInteger(), Parameters.PARAMETER_SPEC); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + java.security.PrivateKey bcKey = keyFactory.generatePrivate(keySpec); + Cipher iesCipher = Cipher.getInstance("ECIES", "BC"); + iesCipher.init(Cipher.DECRYPT_MODE, bcKey); + byte[] output = iesCipher.doFinal(encrypted.toArrayUnsafe()); + return Bytes.wrap(output); + } catch (Exception e) { + throw new DecryptionException(e); + } + } + + /** + * Generates an ECDSA signature. + * + * @param data The data to sign. + * @param keyPair The keypair to sign using. + * @return The signature. + */ + public static Signature sign(byte[] data, KeyPair keyPair) { + return signHashed(keccak256(data), keyPair); + } + + /** + * Generates an ECDSA signature. + * + * @param data The data to sign. + * @param keyPair The keypair to sign using. + * @return The signature. + */ + public static Signature sign(Bytes data, KeyPair keyPair) { + return signHashed(keccak256(data), keyPair); + } + + /** + * Generates an ECDSA signature. + * + * @param hash The keccak256 hash of the data to sign. + * @param keyPair The keypair to sign using. + * @return The signature. + */ + public static Signature signHashed(byte[] hash, KeyPair keyPair) { + return signHashed(Bytes.wrap(hash), keyPair); + } + + /** + * Generates an ECDSA signature. + * + * @param hash The keccak256 hash of the data to sign. + * @param keyPair The keypair to sign using. + * @return The signature. + */ + public static Signature signHashed(Bytes hash, KeyPair keyPair) { + ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest())); + + ECPrivateKeyParameters privKey = + new ECPrivateKeyParameters(keyPair.secretKey().bytes().toUnsignedBigInteger(), Parameters.CURVE); + signer.init(true, privKey); + + BigInteger[] components = signer.generateSignature(hash.toArrayUnsafe()); + BigInteger r = components[0]; + BigInteger s = components[1]; + + // Automatically adjust the S component to be less than or equal to half the curve + // order, if necessary. This is required because for every signature (r,s) the signature + // (r, -s (mod N)) is a valid signature of the same message. However, we dislike the + // ability to modify the bits of a Bitcoin transaction after it's been signed, as that + // violates various assumed invariants. Thus in future only one of those forms will be + // considered legal and the other will be banned. + if (s.compareTo(Parameters.HALF_CURVE_ORDER) > 0) { + // The order of the curve is the number of valid points that exist on that curve. + // If S is in the upper half of the number of valid points, then bring it back to + // the lower half. Otherwise, imagine that: + // N = 10 + // s = 8, so (-8 % 10 == 2) thus both (r, 8) and (r, 2) are valid solutions. + // 10 - 8 == 2, giving us always the latter solution, which is canonical. + s = Parameters.CURVE_ORDER.subtract(s); + } + + // Now we have to work backwards to figure out the recovery id needed to recover the signature. + // On this curve, there are only two possible values for the recovery id. + int recId = -1; + BigInteger publicKeyBI = keyPair.publicKey().bytes().toUnsignedBigInteger(); + for (int i = 0; i < 2; i++) { + BigInteger k = recoverFromSignature(i, r, s, hash); + if (k != null && k.equals(publicKeyBI)) { + recId = i; + break; + } + } + if (recId == -1) { + // this should never happen + throw new RuntimeException("Unexpected error - could not construct a recoverable key."); + } + + byte v = (byte) recId; + return new Signature(v, r, s); + } + + /** + * Verifies the given ECDSA signature against the message bytes using the public key bytes. + * + * @param data The data to verify. + * @param signature The signature. + * @param publicKey The public key. + * @return True if the verification is successful. + */ + public static boolean verify(byte[] data, Signature signature, PublicKey publicKey) { + return verifyHashed(keccak256(data), signature, publicKey); + } + + /** + * Verifies the given ECDSA signature against the message bytes using the public key bytes. + * + * @param data The data to verify. + * @param signature The signature. + * @param publicKey The public key. + * @return True if the verification is successful. + */ + public static boolean verify(Bytes data, Signature signature, PublicKey publicKey) { + return verifyHashed(keccak256(data), signature, publicKey); + } + + /** + * Verifies the given ECDSA signature against the message bytes using the public key bytes. + * + * @param hash The keccak256 hash of the data to verify. + * @param signature The signature. + * @param publicKey The public key. + * @return True if the verification is successful. + */ + public static boolean verifyHashed(Bytes hash, Signature signature, PublicKey publicKey) { + return verifyHashed(hash.toArrayUnsafe(), signature, publicKey); + } + + /** + * Verifies the given ECDSA signature against the message bytes using the public key bytes. + * + * @param hash The keccak256 hash of the data to verify. + * @param signature The signature. + * @param publicKey The public key. + * @return True if the verification is successful. + */ + public static boolean verifyHashed(byte[] hash, Signature signature, PublicKey publicKey) { + ECDSASigner signer = new ECDSASigner(); + Bytes toDecode = Bytes.wrap(Bytes.of((byte) 4), publicKey.bytes()); + ECPublicKeyParameters params = + new ECPublicKeyParameters(Parameters.CURVE.getCurve().decodePoint(toDecode.mutableCopy().toArray()), Parameters.CURVE); + signer.init(false, params); + try { + return signer.verifySignature(hash, signature.r, signature.s); + } catch (NullPointerException e) { + // Bouncy Castle contains a bug that can cause NPEs given specially crafted signatures. Those signatures + // are inherently invalid/attack sigs so we just fail them here rather than crash the thread. + return false; + } + } + + /** + * Calculates an ECDH key agreement between the private and the public key of another party, formatted as a 32 bytes + * array. + * + * @param privKey the private key + * @param theirPubKey the public key + * @return shared secret as 32 bytes + */ + public static UInt256 calculateKeyAgreement(SecretKey privKey, PublicKey theirPubKey) { + if (privKey == null) { + throw new NullPointerException("missing private key"); + } + if (theirPubKey == null) { + throw new NullPointerException("missing remote public key"); + } + + ECPrivateKeyParameters privKeyP = + new ECPrivateKeyParameters(privKey.bytes().toUnsignedBigInteger(), Parameters.CURVE); + ECPublicKeyParameters pubKeyP = new ECPublicKeyParameters(theirPubKey.asEcPoint(), Parameters.CURVE); + + ECDHBasicAgreement agreement = new ECDHBasicAgreement(); + agreement.init(privKeyP); + return UInt256.valueOf(agreement.calculateAgreement(pubKeyP)); + } + + public static Bytes deriveECDHKeyAgreement(Bytes srcPrivKey, Bytes destPubKey) { + ECPoint pudDestPoint = SECP256K1.PublicKey.fromBytes(destPubKey).asEcPoint(); + ECPoint mult = pudDestPoint.multiply(srcPrivKey.toUnsignedBigInteger()); + return Bytes.wrap(mult.getEncoded(true)); + } + + /** + * A SECP256K1 private key. + */ + public static class SecretKey implements Destroyable { + + private Bytes keyBytes; + + @Override + public void destroy() { + if (keyBytes != null) { + byte[] b = keyBytes.toArrayUnsafe(); + keyBytes = null; + Arrays.fill(b, (byte) 0); + } + } + + /** + * Create the private key from a {@link BigInteger}. + * + * @param key The integer describing the key. + * @return The private key. + * @throws IllegalArgumentException If the integer would overflow 32 bytes. + */ + public static SecretKey fromInteger(BigInteger key) { + if (key == null) { + throw new NullPointerException("key cannot be null"); + } + byte[] bytes = key.toByteArray(); + int offset = 0; + while (bytes[offset] == 0) { + ++offset; + } + if ((bytes.length - offset) > 32) { + throw new IllegalArgumentException("key integer is too large"); + } + return fromBytes(MutableBytes.fromArray(bytes, offset, bytes.length - offset).leftPad(32)); + } + + /** + * Create the private key from bytes. + * + * @param bytes The key bytes. + * @return The private key. + */ + public static SecretKey fromBytes(Bytes bytes) { + return new SecretKey(bytes.mutableCopy()); + } + + /** + * Load a private key from a file. + * + * @param file The file to read the key from. + * @return The private key. + * @throws IOException On a filesystem error. + * @throws InvalidSEC256K1SecretKeyStoreException If the file does not contain a valid key. + */ + public static SecretKey load(Path file) throws IOException, InvalidSEC256K1SecretKeyStoreException { + // use buffers for all secret key data transfer, so they can be overwritten on completion + ByteBuffer byteBuffer = ByteBuffer.allocate(65); + CharBuffer charBuffer = CharBuffer.allocate(64); + try { + FileChannel channel = FileChannel.open(file, READ); + while (byteBuffer.hasRemaining() && channel.read(byteBuffer) > 0) { + // no body + } + channel.close(); + if (byteBuffer.remaining() > 1) { + throw new InvalidSEC256K1SecretKeyStoreException(); + } + byteBuffer.flip(); + for (int i = 0; i < 64; ++i) { + charBuffer.put((char) byteBuffer.get()); + } + if (byteBuffer.limit() == 65 && byteBuffer.get(64) != '\n' && byteBuffer.get(64) != '\r') { + throw new InvalidSEC256K1SecretKeyStoreException(); + } + charBuffer.flip(); + return SecretKey.fromBytes(Bytes.fromHexString(charBuffer)); + } catch (IllegalArgumentException ex) { + throw new InvalidSEC256K1SecretKeyStoreException(); + } finally { + Arrays.fill(byteBuffer.array(), (byte) 0); + Arrays.fill(charBuffer.array(), (char) 0); + } + } + + private SecretKey(Bytes bytes) { + if (bytes == null) { + throw new NullPointerException("bytes cannot be null"); + } + this.keyBytes = bytes; + } + + /** + * Write the secret key to a file. + * + * @param file The file to write to. + * @throws IOException On a filesystem error. + */ + public void store(Path file) throws IOException { + if (keyBytes == null) { + throw new NullPointerException("SecretKey has been destroyed"); + } + // use buffers for all secret key data transfer, so they can be overwritten on completion + byte[] bytes = new byte[64]; + CharBuffer hexChars = keyBytes.appendHexTo(CharBuffer.allocate(64)); + try { + hexChars.flip(); + for (int i = 0; i < 64; ++i) { + bytes[i] = (byte) hexChars.get(); + } + atomicReplace(file, bytes); + } finally { + Arrays.fill(bytes, (byte) 0); + Arrays.fill(hexChars.array(), (char) 0); + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SecretKey)) { + return false; + } + if (keyBytes == null) { + throw new NullPointerException("SecretKey has been destroyed"); + } + SecretKey other = (SecretKey) obj; + return this.keyBytes.equals(other.keyBytes); + } + + @Override + public int hashCode() { + if (keyBytes == null) { + throw new NullPointerException("SecretKey has been destroyed"); + } + return keyBytes.hashCode(); + } + + /** + * Provides the bytes of the key. + * @return The bytes of the key. + */ + public Bytes bytes() { + if (keyBytes == null) { + throw new NullPointerException("SecretKey has been destroyed"); + } + return keyBytes; + } + + /** + * Provides the bytes of the key. + * @return The bytes of the key. + */ + public byte[] bytesArray() { + if (keyBytes == null) { + throw new NullPointerException("SecretKey has been destroyed"); + } + return keyBytes.toArrayUnsafe(); + } + } + + /** + * A SECP256K1 public key. + */ + public static class PublicKey { + + private static final int BYTE_LENGTH = 64; + + private final Bytes keyBytes; + + /** + * Create the public key from a secret key. + * + * @param secretKey The secret key. + * @return The associated public key. + */ + public static PublicKey fromSecretKey(SecretKey secretKey) { + BigInteger privKey = secretKey.bytes().toUnsignedBigInteger(); + + /* + * TODO: FixedPointCombMultiplier currently doesn't support scalars longer than the group + * order, but that could change in future versions. + */ + if (privKey.bitLength() > Parameters.CURVE_ORDER.bitLength()) { + privKey = privKey.mod(Parameters.CURVE_ORDER); + } + + ECPoint point = new FixedPointCombMultiplier().multiply(Parameters.CURVE.getG(), privKey); + return PublicKey.fromBytes(Bytes.wrap(Arrays.copyOfRange(point.getEncoded(false), 1, 65))); + } + + private static Bytes toBytes64(byte[] backing) { + if (backing.length == BYTE_LENGTH) { + return Bytes.wrap(backing); + } else if (backing.length > BYTE_LENGTH) { + return Bytes.wrap(backing, backing.length - BYTE_LENGTH, BYTE_LENGTH); + } else { + MutableBytes res = MutableBytes.create(BYTE_LENGTH); + res.set(BYTE_LENGTH - backing.length, backing); + return res; + } + } + + /** + * Create the public key from a secret key. + * + * @param privateKey The secret key. + * @return The associated public key. + */ + public static PublicKey fromInteger(BigInteger privateKey) { + if (privateKey == null) { + throw new NullPointerException("privateKey cannot be null"); + } + return fromBytes(toBytes64(privateKey.toByteArray())); + } + + /** + * Create the public key from bytes. + * + * @param bytes The key bytes. + * @return The public key. + */ + public static PublicKey fromBytes(Bytes bytes) { + return new PublicKey(bytes); + } + + /** + * Create the public key from a hex string. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". + * @return The public key. + */ + public static PublicKey fromHexString(CharSequence str) { + return new PublicKey(Bytes.fromHexString(str)); + } + + /** + * Recover a public key using a digital signature and the data it signs. + * + * @param data The signed data. + * @param signature The digital signature. + * @return The associated public key, or {@code null} if recovery wasn't possible. + */ + @Nullable + public static PublicKey recoverFromSignature(byte[] data, Signature signature) { + return recoverFromHashAndSignature(keccak256(data), signature); + } + + /** + * Recover a public key using a digital signature and the data it signs. + * + * @param data The signed data. + * @param signature The digital signature. + * @return The associated public key, or {@code null} if recovery wasn't possible. + */ + @Nullable + public static PublicKey recoverFromSignature(Bytes data, Signature signature) { + return recoverFromHashAndSignature(keccak256(data), signature); + } + + /** + * Recover a public key using a digital signature and a keccak256 hash of the data it signs. + * + * @param hash The keccak256 hash of the signed data. + * @param signature The digital signature. + * @return The associated public key, or {@code null} if recovery wasn't possible. + */ + @Nullable + public static PublicKey recoverFromHashAndSignature(byte[] hash, Signature signature) { + return recoverFromHashAndSignature(Bytes.wrap(hash), signature); + } + + /** + * Recover a public key using a digital signature and a keccak256 hash of the data it signs. + * + * @param hash The keccak256 hash of the signed data. + * @param signature The digital signature. + * @return The associated public key, or {@code null} if recovery wasn't possible. + */ + @Nullable + public static PublicKey recoverFromHashAndSignature(Bytes hash, Signature signature) { + BigInteger publicKeyBI = SECP256K1.recoverFromSignature(signature.v(), signature.r(), signature.s(), hash); + return (publicKeyBI != null) ? fromInteger(publicKeyBI) : null; + } + + private PublicKey(Bytes bytes) { + if (bytes == null) { + throw new NullPointerException("bytes cannot be null"); + } + if (bytes.size() != BYTE_LENGTH) { + throw new IllegalArgumentException(String.format("Key must be %s bytes long, got %s", BYTE_LENGTH, bytes.size())); + } + this.keyBytes = bytes; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof PublicKey)) { + return false; + } + + PublicKey that = (PublicKey) other; + return this.keyBytes.equals(that.keyBytes); + } + + @Override + public int hashCode() { + return keyBytes.hashCode(); + } + + /** + * Provides the bytes of the key. + * @return The bytes of the key. + */ + public Bytes bytes() { + return keyBytes; + } + + /** + * Provides the bytes of the key. + * + * @return The bytes of the key. + */ + public byte[] bytesArray() { + return keyBytes.toArrayUnsafe(); + } + + /** + * Computes the public key as a point on the elliptic curve. + * + * @return the public key as a BouncyCastle elliptic curve point + */ + public ECPoint asEcPoint() { + // 0x04 is the prefix for uncompressed keys. + Bytes val = Bytes.wrap(Bytes.of(0x04), keyBytes); + return Parameters.CURVE.getCurve().decodePoint(val.toArrayUnsafe()); + } + + @Override + public String toString() { + return keyBytes.toString(); + } + + /** + * Provides this key represented as hexadecimal, starting with "0x". + * @return This key represented as hexadecimal, starting with "0x". + */ + public String toHexString() { + return keyBytes.toHexString(); + } + } + + /** + * A SECP256K1 key pair. + */ + public static class KeyPair { + + private final SecretKey secretKey; + private final PublicKey publicKey; + + /** + * Create a keypair from a private and public key. + * + * @param secretKey The private key. + * @param publicKey The public key. + * @return The key pair. + */ + public static KeyPair create(SecretKey secretKey, PublicKey publicKey) { + return new KeyPair(secretKey, publicKey); + } + + /** + * Create a keypair using only a private key. + * + * @param secretKey The private key. + * @return The key pair. + */ + public static KeyPair fromSecretKey(SecretKey secretKey) { + return new KeyPair(secretKey, PublicKey.fromSecretKey(secretKey)); + } + + /** + * Generate a new keypair. + * + * Entropy for the generation is drawn from {@link SecureRandom}. + * + * @return A new keypair. + */ + public static KeyPair random() { + java.security.KeyPair rawKeyPair = Parameters.KEY_PAIR_GENERATOR.generateKeyPair(); + BCECPrivateKey privateKey = (BCECPrivateKey) rawKeyPair.getPrivate(); + BCECPublicKey publicKey = (BCECPublicKey) rawKeyPair.getPublic(); + + BigInteger privateKeyValue = privateKey.getD(); + + // Ethereum does not use encoded public keys like bitcoin - see + // https://en.bitcoin.it/wiki/Elliptic_Curve_Digital_Signature_Algorithm for details + // Additionally, as the first bit is a constant prefix (0x04) we ignore this value + byte[] publicKeyBytes = publicKey.getQ().getEncoded(false); + BigInteger publicKeyValue = new BigInteger(1, Arrays.copyOfRange(publicKeyBytes, 1, publicKeyBytes.length)); + + return new KeyPair(SecretKey.fromInteger(privateKeyValue), PublicKey.fromInteger(publicKeyValue)); + } + + /** + * Load a key pair from a path. + * + * @param file The file containing a private key. + * @return The key pair. + * @throws IOException On a filesystem error. + * @throws InvalidSEC256K1SecretKeyStoreException If the file does not contain a valid key. + */ + public static KeyPair load(Path file) throws IOException, InvalidSEC256K1SecretKeyStoreException { + return fromSecretKey(SecretKey.load(file)); + } + + private KeyPair(SecretKey secretKey, PublicKey publicKey) { + if (secretKey == null) { + throw new NullPointerException("secretKey cannot be null"); + } + if (publicKey == null) { + throw new NullPointerException("publicKey cannot be null"); + } + this.secretKey = secretKey; + this.publicKey = publicKey; + } + + @Override + public int hashCode() { + return Objects.hash(secretKey, publicKey); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof KeyPair)) { + return false; + } + + KeyPair that = (KeyPair) other; + return this.secretKey.equals(that.secretKey) && this.publicKey.equals(that.publicKey); + } + + /** + * Provides the secret key + * @return The secret key. + */ + public SecretKey secretKey() { + return secretKey; + } + + /** + * Provides the public key + * @return The public key. + */ + public PublicKey publicKey() { + return publicKey; + } + + /** + * Write the key pair to a file. + * + * @param file The file to write to. + * @throws IOException On a filesystem error. + */ + public void store(Path file) throws IOException { + secretKey.store(file); + } + } + + /** + * A SECP256K1 digital signature. + */ + public static class Signature { + /* + * Parameter v is the recovery id to reconstruct the public key used to create the signature. It must be in + * the range 0 to 3 and indicates which of the 4 possible keys is the correct one. Because the key recovery + * operation yields multiple potential keys, the correct key must either be stored alongside the signature, + * or you must be willing to try each recovery id in turn until you find one that outputs the key you are + * expecting. + */ + private final byte v; + private final BigInteger r; + private final BigInteger s; + + /** + * Create a signature from bytes. + * + * @param bytes The signature bytes. + * @return The signature. + */ + public static Signature fromBytes(Bytes bytes) { + if (bytes == null) { + throw new NullPointerException("bytes cannot be null"); + } + if (bytes.size() != 65) { + throw new IllegalArgumentException(String.format("Signature must be 65 bytes, but got %s instead", bytes.size())); + } + BigInteger r = bytes.slice(0, 32).toUnsignedBigInteger(); + BigInteger s = bytes.slice(32, 32).toUnsignedBigInteger(); + return new Signature(bytes.get(64), r, s); + } + + /** + * Create a signature from parameters. + * + * @param v The v-value (recovery id). + * @param r The r-value. + * @param s The s-value. + * @return The signature. + * @throws IllegalArgumentException If any argument has an invalid range. + */ + public static Signature create(byte v, BigInteger r, BigInteger s) { + return new Signature(v, r, s); + } + + Signature(byte v, BigInteger r, BigInteger s) { + if (v != 0 && v != 1) { + throw new IllegalArgumentException(String.format("Invalid v-value, should be 0 or 1, got %s", v)); + } + if (r == null) { + throw new NullPointerException("r cannot be null"); + } + if (s == null) { + throw new NullPointerException("s cannot be null"); + } + if (r.compareTo(BigInteger.ONE) < 0 || r.compareTo(Parameters.CURVE_ORDER) > 0) { + throw new IllegalArgumentException(String.format("Invalid r-value, should be >= 1 and < %s, got %s", + Parameters.CURVE_ORDER, + r)); + } + if (s.compareTo(BigInteger.ONE) < 0 || s.compareTo(Parameters.CURVE_ORDER) > 0) { + throw new IllegalArgumentException(String.format("Invalid s-value, should be >= 1 and < %s, got %s", + Parameters.CURVE_ORDER, + s)); + } + this.v = v; + this.r = r; + this.s = s; + } + + /** + * Provides the v-value of the signature. + * @return The v-value (recovery id) of the signature. + */ + public byte v() { + return v; + } + + /** + * Provides the r-value of the signature. + * @return The r-value of the signature. + */ + public BigInteger r() { + return r; + } + + /** + * Provides the s-value of the signature. + * @return The s-value of the signature. + */ + public BigInteger s() { + return s; + } + + /** + * Check if the signature is canonical. + * + * Every signature (r,s) has an equivalent signature (r, -s (mod N)) that is also valid for the same message. The + * canonical signature is considered the signature with the s-value less than or equal to half the curve order. + * + * @return {@code true} if this is the canonical form of the signature, and {@code false} otherwise. + */ + public boolean isCanonical() { + return s.compareTo(Parameters.HALF_CURVE_ORDER) <= 0; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Signature)) { + return false; + } + + Signature that = (Signature) other; + return this.r.equals(that.r) && this.s.equals(that.s) && this.v == that.v; + } + + /** + * Provides the bytes of the signature. + * + * @return The bytes of the signature. + */ + public Bytes bytes() { + MutableBytes signature = MutableBytes.create(65); + signature.set(0, UInt256.valueOf(r)); + signature.set(32, UInt256.valueOf(s)); + signature.set(64, v); + return signature; + } + + @Override + public int hashCode() { + return Objects.hash(r, s, v); + } + + @Override + public String toString() { + return "Signature{" + "r=" + r + ", s=" + s + ", v=" + v + '}'; + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigest.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigest.java new file mode 100644 index 000000000..2295e914f --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigest.java @@ -0,0 +1,233 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.blake2bf; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.MutableBytes; + +import java.nio.ByteOrder; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.jcajce.provider.digest.BCMessageDigest; +import org.bouncycastle.util.Pack; + +public class Blake2bfMessageDigest extends BCMessageDigest implements Cloneable { + + public Blake2bfMessageDigest() { + super(new Blake2bfDigest()); + } + + /** + * Implementation of the `F` compression function of the Blake2b cryptographic hash function. + * + *

RFC - https://tools.ietf.org/html/rfc7693 + * + *

Copied from - + * https://github.com/hyperledger/besu/blob/main/crypto/src/main/java/org/hyperledger/besu/crypto/Blake2bfMessageDigest.java + * + *

Optimized for 64-bit platforms + */ + public static class Blake2bfDigest implements Digest { + + public static final int MESSAGE_LENGTH_BYTES = 213; + + private static final long[] IV = { + 0x6a09e667f3bcc908L, + 0xbb67ae8584caa73bL, + 0x3c6ef372fe94f82bL, + 0xa54ff53a5f1d36f1L, + 0x510e527fade682d1L, + 0x9b05688c2b3e6c1fL, + 0x1f83d9abfb41bd6bL, + 0x5be0cd19137e2179L + }; + + private static final byte[][] PRECOMPUTED = { + {0, 2, 4, 6, 1, 3, 5, 7, 8, 10, 12, 14, 9, 11, 13, 15}, + {14, 4, 9, 13, 10, 8, 15, 6, 1, 0, 11, 5, 12, 2, 7, 3}, + {11, 12, 5, 15, 8, 0, 2, 13, 10, 3, 7, 9, 14, 6, 1, 4}, + {7, 3, 13, 11, 9, 1, 12, 14, 2, 5, 4, 15, 6, 10, 0, 8}, + {9, 5, 2, 10, 0, 7, 4, 15, 14, 11, 6, 3, 1, 12, 8, 13}, + {2, 6, 0, 8, 12, 10, 11, 3, 4, 7, 15, 1, 13, 5, 14, 9}, + {12, 1, 14, 4, 5, 15, 13, 10, 0, 6, 9, 8, 7, 3, 2, 11}, + {13, 7, 12, 3, 11, 14, 1, 9, 5, 15, 8, 2, 0, 4, 6, 10}, + {6, 14, 11, 0, 15, 9, 3, 8, 12, 13, 1, 10, 2, 7, 4, 5}, + {10, 8, 7, 1, 2, 4, 6, 5, 15, 9, 3, 13, 11, 14, 12, 0} + }; + + private static final int DIGEST_LENGTH = 64; + + // buffer which holds serialized input for this compression function + // [ 4 bytes for rounds ][ 64 bytes for h ][ 128 bytes for m ] + // [ 8 bytes for t_0 ][ 8 bytes for t_1 ][ 1 byte for f ] + private Bytes buffer = Bytes.EMPTY; + + Blake2bfDigest() {} + + // for tests + Blake2bfDigest( + final long[] h, final long[] m, final long[] t, final boolean f, final long rounds) { + assert rounds <= 4294967295L; // uint max value + } + + @Override + public String getAlgorithmName() { + return "BLAKE2f"; + } + + @Override + public int getDigestSize() { + return DIGEST_LENGTH; + } + + /** + * update the message digest with a single byte. + * + * @param in the input byte to be entered. + */ + @Override + public void update(final byte in) { + + if (buffer.size() == MESSAGE_LENGTH_BYTES) { // full buffer + throw new IllegalArgumentException(); + } else { + buffer = Bytes.wrap(buffer, Bytes.of(in)); + } + } + + /** + * update the message digest with a block of bytes. + * + * @param in the byte array containing the data. + * @param offset the offset into the byte array where the data starts. + * @param len the length of the data. + */ + @Override + public void update(final byte[] in, final int offset, final int len) { + Bytes value = Bytes.wrap(in, offset, len); + if (value.size() > MESSAGE_LENGTH_BYTES - buffer.size()) { + throw new IllegalArgumentException( + "Attempting to update buffer with " + + value.size() + + " byte(s) but there is " + + (MESSAGE_LENGTH_BYTES - value.size()) + + " byte(s) left to fill"); + } + + MutableBytes mutable = MutableBytes.create(buffer.size() + value.size()); + mutable.set(0, buffer); + mutable.set(buffer.size(), value); + buffer = mutable; + } + + /** + * close the digest, producing the final digest value. The doFinal call leaves the digest reset. + * + * @param out the array the digest is to be copied into. + * @param offset the offset into the out array the digest is to start at. + */ + @Override + public int doFinal(final byte[] out, final int offset) { + if (buffer.size() != 213) { + throw new IllegalStateException("The buffer must be filled with 213 bytes"); + } + + compress(out); + + reset(); + + return 0; + } + + /** Reset the digest back to its initial state. */ + @Override + public void reset() { + buffer = Bytes.EMPTY; + } + + /** + * F is a compression function for BLAKE2b. It takes as an argument the state vector `h`, + * message block vector `m`, offset counter `t`, final block indicator flag `f`, and number of + * rounds `rounds`. The state vector provided as the first parameter is modified by the + * function. + */ + private void compress(byte[] out) { + long rounds = Integer.toUnsignedLong(buffer.getInt(0)); + + long[] h = new long[8]; + long[] m = new long[16]; + long[] t = new long[2]; + + long[] v = new long[16]; + + for (int i = 0; i < h.length; i++) { + final int offset = 4 + i * 8; + h[i] = buffer.getLong(offset, ByteOrder.LITTLE_ENDIAN); + } + + for (int i = 0; i < 16; i++) { + final int offset = 68 + i * 8; + m[i] = buffer.getLong(offset, ByteOrder.LITTLE_ENDIAN); + } + + t[0] = buffer.getLong(196, ByteOrder.LITTLE_ENDIAN); + t[1] = buffer.getLong(204, ByteOrder.LITTLE_ENDIAN); + + boolean f = buffer.get(212) != 0; + + long t0 = t[0]; + long t1 = t[1]; + + System.arraycopy(h, 0, v, 0, 8); + System.arraycopy(IV, 0, v, 8, 8); + + v[12] ^= t0; + v[13] ^= t1; + + if (f) { + v[14] ^= 0xffffffffffffffffL; + } + + for (long j = 0; j < rounds; ++j) { + byte[] s = PRECOMPUTED[(int) (j % 10)]; + + mix(v, m[s[0]], m[s[4]], 0, 4, 8, 12); + mix(v, m[s[1]], m[s[5]], 1, 5, 9, 13); + mix(v, m[s[2]], m[s[6]], 2, 6, 10, 14); + mix(v, m[s[3]], m[s[7]], 3, 7, 11, 15); + mix(v, m[s[8]], m[s[12]], 0, 5, 10, 15); + mix(v, m[s[9]], m[s[13]], 1, 6, 11, 12); + mix(v, m[s[10]], m[s[14]], 2, 7, 8, 13); + mix(v, m[s[11]], m[s[15]], 3, 4, 9, 14); + } + + // update h: + for (int offset = 0; offset < h.length; offset++) { + h[offset] ^= v[offset] ^ v[offset + 8]; + } + + for (int i = 0; i < h.length; i++) { + System.arraycopy(Pack.longToLittleEndian(h[i]), 0, out, i * 8, 8); + } + } + + private void mix( + final long[] v, + final long a, + final long b, + final int i, + final int j, + final int k, + final int l) { + v[i] += a + v[j]; + v[l] = Long.rotateLeft(v[l] ^ v[i], -32); + v[k] += v[l]; + v[j] = Long.rotateLeft(v[j] ^ v[k], -24); + + v[i] += b + v[j]; + v[l] = Long.rotateLeft(v[l] ^ v[i], -16); + v[k] += v[l]; + v[j] = Long.rotateLeft(v[j] ^ v[k], -63); + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/AtePairing.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/AtePairing.java new file mode 100644 index 000000000..3e6dc4222 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/AtePairing.java @@ -0,0 +1,22 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import org.apache.milagro.amcl.BLS381.FP12; +import org.apache.milagro.amcl.BLS381.PAIR; + +/** Function that maps 2 points on an elliptic curve to a number. */ +final class AtePairing { + + /** + * Pair of points on the curve + * + * @param p1 the point in Group1, not null + * @param p2 the point in Group2, not null + * @return GTPoint + */ + static GTPoint pair(G1Point p1, G2Point p2) { + FP12 e = PAIR.ate(p2.ecp2Point(), p1.ecpPoint()); + return new GTPoint(PAIR.fexp(e)); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/BLS12381.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/BLS12381.java new file mode 100644 index 000000000..8dd7f0349 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/BLS12381.java @@ -0,0 +1,122 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import org.apache.milagro.amcl.BLS381.ECP; +import org.apache.milagro.amcl.BLS381.ECP2; +import org.apache.milagro.amcl.BLS381.MPIN; +import org.apache.tuweni.v2.bytes.Bytes; + +/* + * Adapted from the ConsenSys/mikuli (Apache 2 License) implementation: + * https://github.com/ConsenSys/mikuli/blob/master/src/main/java/net/consensys/mikuli/crypto/*.java + */ + +/** + * This Boneh-Lynn-Shacham (BLS) signature implementation is constructed from a pairing friendly + * elliptic curve, the BLS12-381 curve. It uses parameters as defined in + * https://z.cash/blog/new-snark-curve and the points in groups G1 and G2 are defined + * https://github.com/zkcrypto/pairing/blob/master/src/bls12_381/README.md + * + *

This class depends upon the Apache Milagro library being available. See + * https://milagro.apache.org. + * + *

Apache Milagro can be included using the gradle dependency + * 'org.miracl.milagro.amcl:milagro-crypto-java'. + */ +public final class BLS12381 { + + private BLS12381() {} + + /** + * Generates a SignatureAndPublicKey. + * + * @param keyPair The public and private key pair, not null + * @param message The message to sign, not null + * @param domain The domain value added to the message + * @return The SignatureAndPublicKey, not null + */ + public static SignatureAndPublicKey sign(KeyPair keyPair, byte[] message, int domain) { + G2Point hashInGroup2 = hashFunction(message, domain); + /* + * The signature is hash point in G2 multiplied by the private key. + */ + G2Point sig = keyPair.secretKey().sign(hashInGroup2); + return new SignatureAndPublicKey(new Signature(sig), keyPair.publicKey()); + } + + /** + * Generates a SignatureAndPublicKey. + * + * @param keyPair The public and private key pair, not null + * @param message The message to sign, not null + * @param domain The domain value added to the message + * @return The SignatureAndPublicKey, not null + */ + public static SignatureAndPublicKey sign(KeyPair keyPair, Bytes message, int domain) { + return sign(keyPair, message.toArrayUnsafe(), domain); + } + + /** + * Verifies the given BLS signature against the message bytes using the public key. + * + * @param publicKey The public key, not null + * @param signature The signature, not null + * @param message The message data to verify, not null + * @param domain The domain value added to the message + * @return True if the verification is successful. + */ + public static boolean verify( + PublicKey publicKey, Signature signature, byte[] message, int domain) { + G1Point g1Generator = KeyPair.g1Generator; + + G2Point hashInGroup2 = hashFunction(message, domain); + GTPoint e1 = AtePairing.pair(publicKey.g1Point(), hashInGroup2); + GTPoint e2 = AtePairing.pair(g1Generator, signature.g2Point()); + + return e1.equals(e2); + } + + /** + * Verifies the given BLS signature against the message bytes using the public key. + * + * @param publicKey The public key, not null + * @param signature The signature, not null + * @param message The message data to verify, not null + * @param domain The domain value added to the message + * @return True if the verification is successful. + */ + public static boolean verify( + PublicKey publicKey, Signature signature, Bytes message, int domain) { + return verify(publicKey, signature, message.toArrayUnsafe(), domain); + } + + /** + * Verifies the given BLS signature against the message bytes using the public key. + * + * @param sigAndPubKey The signature and public key, not null + * @param message The message data to verify, not null + * @param domain The domain value added to the message + * @return True if the verification is successful, not null + */ + public static boolean verify(SignatureAndPublicKey sigAndPubKey, byte[] message, int domain) { + return verify(sigAndPubKey.publicKey(), sigAndPubKey.signature(), message, domain); + } + + /** + * Verifies the given BLS signature against the message bytes using the public key. + * + * @param sigAndPubKey The public key, not null + * @param message The message data to verify, not null + * @param domain The domain value added to the message + * @return True if the verification is successful. + */ + public static boolean verify(SignatureAndPublicKey sigAndPubKey, Bytes message, int domain) { + return verify(sigAndPubKey.publicKey(), sigAndPubKey.signature(), message, domain); + } + + private static G2Point hashFunction(byte[] message, int domain) { + byte[] hashByte = MPIN.HASH_ID(ECP.SHA256, message, domain); + return new G2Point(ECP2.mapit(hashByte)); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G1Point.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G1Point.java new file mode 100644 index 000000000..6f1cb3368 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G1Point.java @@ -0,0 +1,88 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import org.apache.milagro.amcl.BLS381.BIG; +import org.apache.milagro.amcl.BLS381.ECP; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Objects; + +/** + * G1 is a subgroup of an elliptic curve whose points are elements of the finite field Fp - simple + * numbers mod some prime p. The curve is defined by: y^2 = x^3 + 4 + */ +final class G1Point implements Group { + + private static final int fpPointSize = BIG.MODBYTES; + + static G1Point fromBytes(Bytes bytes) { + return new G1Point(ECP.fromBytes(bytes.toArrayUnsafe())); + } + + private final ECP point; + + G1Point(ECP point) { + this.point = point; + } + + @Override + public G1Point add(G1Point other) { + ECP sum = new ECP(); + sum.add(point); + sum.add(other.point); + sum.affine(); + return new G1Point(sum); + } + + @Override + public G1Point mul(Scalar scalar) { + ECP newPoint = point.mul(scalar.value()); + return new G1Point(newPoint); + } + + Bytes toBytes() { + // Size of the byte array representing compressed ECP point for BLS12-381 is + // 49 bytes in milagro + // size of the point = 48 bytes + // meta information (parity bit, curve type etc) = 1 byte + byte[] bytes = new byte[fpPointSize + 1]; + point.toBytes(bytes, true); + return Bytes.wrap(bytes); + } + + ECP ecpPoint() { + return point; + } + + @Override + public String toString() { + return point.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long x = point.getX().norm(); + long y = point.getY().norm(); + result = prime * result + (int) (x ^ (x >>> 32)); + result = prime * result + (int) (y ^ (y >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (Objects.isNull(obj)) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof G1Point)) { + return false; + } + G1Point other = (G1Point) obj; + return point.equals(other.point); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G2Point.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G2Point.java new file mode 100644 index 000000000..ccb0c84cc --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G2Point.java @@ -0,0 +1,85 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import org.apache.milagro.amcl.BLS381.BIG; +import org.apache.milagro.amcl.BLS381.ECP2; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Objects; + +/** + * G2 is the subgroup of elliptic curve similar to G1 and the points are identical except for where + * they are elements of the extension field Fq12. + */ +final class G2Point implements Group { + private final ECP2 point; + private static final int fpPointSize = BIG.MODBYTES; + + G2Point(ECP2 point) { + this.point = point; + } + + @Override + public G2Point add(G2Point other) { + ECP2 sum = new ECP2(); + sum.add(point); + sum.add(other.point); + sum.affine(); + return new G2Point(sum); + } + + @Override + public G2Point mul(Scalar scalar) { + ECP2 newPoint = point.mul(scalar.value()); + return new G2Point(newPoint); + } + + Bytes toBytes() { + byte[] bytes = new byte[4 * fpPointSize]; + point.toBytes(bytes); + return Bytes.wrap(bytes); + } + + static G2Point fromBytes(Bytes bytes) { + return new G2Point(ECP2.fromBytes(bytes.toArrayUnsafe())); + } + + ECP2 ecp2Point() { + return point; + } + + @Override + public String toString() { + return point.toString(); + } + + @Override + public boolean equals(Object obj) { + if (Objects.isNull(obj)) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof G2Point other)) { + return false; + } + return point.equals(other.point); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long xa = point.getX().getA().norm(); + long ya = point.getY().getA().norm(); + long xb = point.getX().getB().norm(); + long yb = point.getY().getB().norm(); + result = prime * result + (int) (xa ^ (xa >>> 32)); + result = prime * result + (int) (ya ^ (ya >>> 32)); + result = prime * result + (int) (xb ^ (xb >>> 32)); + result = prime * result + (int) (yb ^ (yb >>> 32)); + return result; + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/GTPoint.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/GTPoint.java new file mode 100644 index 000000000..1108f8a9b --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/GTPoint.java @@ -0,0 +1,33 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import org.apache.milagro.amcl.BLS381.FP12; + +import java.util.Objects; + +/** + * GT is the object that holds the result of the pairing operation. Points in GT are elements of + * Fq12. + */ +final class GTPoint { + + private final FP12 point; + + GTPoint(FP12 point) { + this.point = point; + } + + @Override + public int hashCode() { + return Objects.hash(point); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GTPoint gtPoint = (GTPoint) o; + return (point != null && gtPoint.point == null) || point.equals(gtPoint.point); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Group.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Group.java new file mode 100644 index 000000000..45dda1907 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Group.java @@ -0,0 +1,11 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +/** Group is an interface that define the allowed mathematical operators */ +interface Group { + + G add(G g); + + G mul(Scalar scalar); +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/KeyPair.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/KeyPair.java new file mode 100644 index 000000000..20eed1cc5 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/KeyPair.java @@ -0,0 +1,46 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import org.apache.milagro.amcl.BLS381.BIG; +import org.apache.milagro.amcl.BLS381.ECP; +import org.apache.milagro.amcl.BLS381.ROM; +import org.apache.milagro.amcl.RAND; + +/** KeyPair represents a public and private key. */ +public final class KeyPair { + + private static final BIG curveOrder = new BIG(ROM.CURVE_Order); + static final G1Point g1Generator = new G1Point(ECP.generator()); + + /** + * Generate a new random key pair + * + * @return a new random key pair + */ + public static KeyPair random() { + RAND rng = new RAND(); + Scalar secret = new Scalar(BIG.randomnum(curveOrder, rng)); + + SecretKey secretKey = new SecretKey(secret); + G1Point g1Point = g1Generator.mul(secret); + PublicKey publicKey = new PublicKey(g1Point); + return new KeyPair(secretKey, publicKey); + } + + private final SecretKey secretKey; + private final PublicKey publicKey; + + private KeyPair(SecretKey secretKey, PublicKey publicKey) { + this.secretKey = secretKey; + this.publicKey = publicKey; + } + + public PublicKey publicKey() { + return publicKey; + } + + public SecretKey secretKey() { + return secretKey; + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/PublicKey.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/PublicKey.java new file mode 100644 index 000000000..56872efcb --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/PublicKey.java @@ -0,0 +1,101 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.List; +import java.util.Objects; + +/** This class represents a BLS12-381 public key. */ +public final class PublicKey { + + /** + * Aggregates list of PublicKey pairs + * + * @param keys The list of public keys to aggregate, not null + * @return PublicKey The public key, not null + * @throws IllegalArgumentException if parameter list is empty + */ + public static PublicKey aggregate(List keys) { + if (keys.isEmpty()) { + throw new IllegalArgumentException("Parameter list is empty"); + } + return keys.stream().reduce((a, b) -> a.combine(b)).get(); + } + + /** + * Create a PublicKey from byte array + * + * @param bytes the bytes to read the public key from + * @return a valid public key + */ + public static PublicKey fromBytes(byte[] bytes) { + return fromBytes(Bytes.wrap(bytes)); + } + + /** + * Create a PublicKey from bytes + * + * @param bytes the bytes to read the public key from + * @return a valid public key + */ + public static PublicKey fromBytes(Bytes bytes) { + G1Point point = G1Point.fromBytes(bytes); + return new PublicKey(point); + } + + private final G1Point point; + + PublicKey(G1Point point) { + this.point = point; + } + + PublicKey combine(PublicKey pk) { + return new PublicKey(point.add(pk.point)); + } + + /** + * Public key serialization + * + * @return byte array representation of the public key + */ + public byte[] toByteArray() { + return point.toBytes().toArrayUnsafe(); + } + + /** + * Public key serialization + * + * @return byte array representation of the public key + */ + public Bytes toBytes() { + return point.toBytes(); + } + + G1Point g1Point() { + return point; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Objects.hashCode(point); + return result; + } + + @Override + public boolean equals(Object obj) { + if (Objects.isNull(obj)) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof PublicKey other)) { + return false; + } + return point.equals(other.point); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Scalar.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Scalar.java new file mode 100644 index 000000000..3f0c7edfc --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Scalar.java @@ -0,0 +1,34 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import org.apache.milagro.amcl.BLS381.BIG; + +import java.util.Objects; + +/** This class represents an ordinary scalar value. */ +final class Scalar { + + private final BIG value; + + Scalar(BIG value) { + this.value = value; + } + + BIG value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Scalar scalar = (Scalar) o; + return Objects.equals(value.toString(), scalar.value.toString()); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SecretKey.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SecretKey.java new file mode 100644 index 000000000..e5d290807 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SecretKey.java @@ -0,0 +1,63 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import static org.apache.milagro.amcl.BLS381.BIG.MODBYTES; + +import org.apache.milagro.amcl.BLS381.BIG; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Objects; + +/** This class represents a BLS12-381 private key. */ +public final class SecretKey { + + /** + * Create a private key from a byte array + * + * @param bytes the bytes of the private key + * @return a new SecretKey object + */ + public static SecretKey fromBytes(byte[] bytes) { + return fromBytes(Bytes.wrap(bytes)); + } + + /** + * Create a private key from bytes + * + * @param bytes the bytes of the private key + * @return a new SecretKey object + */ + public static SecretKey fromBytes(Bytes bytes) { + return new SecretKey(new Scalar(BIG.fromBytes(bytes.toArrayUnsafe()))); + } + + private final Scalar scalarValue; + + SecretKey(Scalar value) { + this.scalarValue = value; + } + + G2Point sign(G2Point message) { + return message.mul(scalarValue); + } + + public Bytes toBytes() { + byte[] bytea = new byte[MODBYTES]; + scalarValue.value().toBytes(bytea); + return Bytes.wrap(bytea); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SecretKey secretKey = (SecretKey) o; + return Objects.equals(scalarValue, secretKey.scalarValue); + } + + @Override + public int hashCode() { + return Objects.hash(scalarValue); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Signature.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Signature.java new file mode 100644 index 000000000..1fbfd951c --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Signature.java @@ -0,0 +1,94 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.List; +import java.util.Objects; + +/** This class represents a Signature on G2 */ +public final class Signature { + + /** + * Aggregates list of Signature pairs + * + * @param signatures The list of signatures to aggregate, not null + * @throws IllegalArgumentException if parameter list is empty + * @return Signature, not null + */ + public static Signature aggregate(List signatures) { + if (signatures.isEmpty()) { + throw new IllegalArgumentException("Parameter list is empty"); + } + return signatures.stream().reduce(Signature::combine).get(); + } + + /** + * Decode a signature from its serialized representation. + * + * @param bytes the bytes of the signature + * @return the signature + */ + public static Signature decode(Bytes bytes) { + G2Point point = G2Point.fromBytes(bytes); + return new Signature(point); + } + + private final G2Point point; + + Signature(G2Point point) { + this.point = point; + } + + /** + * Combines this signature with another signature, creating a new signature. + * + * @param signature the signature to combine with + * @return a new signature as combination of both signatures. + */ + public Signature combine(Signature signature) { + return new Signature(point.add(signature.point)); + } + + /** + * Signature serialization + * + * @return byte array representation of the signature, not null + */ + public Bytes encode() { + return point.toBytes(); + } + + @Override + public String toString() { + return "Signature [ecpPoint=" + point.toString() + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((point == null) ? 0 : point.hashCode()); + return result; + } + + G2Point g2Point() { + return point; + } + + @Override + public boolean equals(Object obj) { + if (Objects.isNull(obj)) { + return false; + } + if (this == obj) { + return true; + } + if (!(obj instanceof Signature)) { + return false; + } + Signature other = (Signature) obj; + return point.equals(other.point); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SignatureAndPublicKey.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SignatureAndPublicKey.java new file mode 100644 index 000000000..20adab496 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SignatureAndPublicKey.java @@ -0,0 +1,62 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import java.util.List; + +/** This class represents a signature and a public key */ +public final class SignatureAndPublicKey { + + /** + * Aggregates list of Signature and PublicKey pairs + * + * @param sigAndPubKeys The list of Signatures and corresponding Public keys to aggregate, not + * null + * @return SignatureAndPublicKey, not null + * @throws IllegalArgumentException if parameter list is empty + */ + public static SignatureAndPublicKey aggregate(List sigAndPubKeys) { + if (sigAndPubKeys.isEmpty()) { + throw new IllegalArgumentException("Parameter list is empty"); + } + return sigAndPubKeys.stream().reduce((a, b) -> a.combine(b)).get(); + } + + private final Signature signature; + private final PublicKey publicKey; + + SignatureAndPublicKey(Signature signature, PublicKey pubKey) { + this.signature = signature; + this.publicKey = pubKey; + } + + /** + * Provides the public key. + * + * @return the public key of the pair + */ + public PublicKey publicKey() { + return publicKey; + } + + /** + * Provides the signature. + * + * @return the signature of the pair + */ + public Signature signature() { + return signature; + } + + /** + * Combine the signature and public key provided to form a new signature and public key pair + * + * @param sigAndPubKey the signature and public key pair + * @return a new signature and public key pair as a combination of both elements. + */ + public SignatureAndPublicKey combine(SignatureAndPublicKey sigAndPubKey) { + Signature newSignature = signature.combine(sigAndPubKey.signature); + PublicKey newPubKey = publicKey.combine(sigAndPubKey.publicKey); + return new SignatureAndPublicKey(newSignature, newPubKey); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/package-info.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/package-info.java new file mode 100644 index 000000000..397b33ca7 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/package-info.java @@ -0,0 +1,23 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You 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. + * + *

Classes and utilities for working with cryptography. + * + *

These classes are included in the standard Tuweni distribution, or separately when using the + * gradle dependency 'org.apache.tuweni:tuweni-crypto' (tuweni-crypto.jar). + */ +@ParametersAreNonnullByDefault +package org.apache.tuweni.v2.crypto.mikuli; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/package-info.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/package-info.java new file mode 100644 index 000000000..1a01bdd6a --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/package-info.java @@ -0,0 +1,20 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You 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. + * + *

Classes and utilities for working with cryptography. + * + *

These classes are included in the standard Tuweni distribution, or separately when using the + * gradle dependency 'org.apache.tuweni:tuweni-crypto' (tuweni-crypto.jar). + */ +package org.apache.tuweni.v2.crypto; diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/AES256GCM.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/AES256GCM.java new file mode 100644 index 000000000..2bdb9833f --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/AES256GCM.java @@ -0,0 +1,1046 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Arrays; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; +import jnr.ffi.byref.LongLongByReference; +import org.jetbrains.annotations.Nullable; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/secret-key_cryptography/aes-256-gcm.md + +/** + * Authenticated Encryption with Additional Data using AES-GCM. + * + *

WARNING: Despite being the most popular AEAD construction due to its use in TLS, safely using + * AES-GCM in a different context is tricky. + * + *

No more than ~350 GB of input data should be encrypted with a given key. This is for ~16 KB + * messages -- Actual figures vary according to message sizes. + * + *

In addition, nonces are short and repeated nonces would totally destroy the security of this + * scheme. Nonces should thus come from atomic counters, which can be difficult to set up in a + * distributed environment. + * + *

Unless you absolutely need AES-GCM, use {@link XChaCha20Poly1305} instead. It doesn't have any + * of these limitations. Or, if you don't need to authenticate additional data, just stick to {@link + * Sodium#crypto_box(byte[], byte[], long, byte[], byte[], byte[])}. + * + *

This class depends upon the JNR-FFI library being available on the classpath, along with its + * dependencies. See https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle + * dependency 'com.github.jnr:jnr-ffi'. + */ +public final class AES256GCM implements AutoCloseable { + + private static final byte[] EMPTY_BYTES = new byte[0]; + + /** + * Check if Sodium and the AES256-GCM algorithm is available. + * + * @return {@code true} if Sodium and the AES256-GCM algorithm is available. + */ + public static boolean isAvailable() { + try { + return Sodium.crypto_aead_aes256gcm_is_available() != 0; + } catch (LinkageError e) { + return false; + } + } + + private static void assertAvailable() { + if (!isAvailable()) { + throw new UnsupportedOperationException("Sodium AES256-GCM is not available"); + } + } + + /** An AES256-GSM key. */ + public static final class Key implements Destroyable { + final Allocated value; + + private Key(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + public static Key fromBytes(byte[] bytes) { + assertAvailable(); + if (bytes.length != Sodium.crypto_aead_aes256gcm_keybytes()) { + throw new IllegalArgumentException( + "key must be " + + Sodium.crypto_aead_aes256gcm_keybytes() + + " bytes, got " + + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + public static int length() { + assertAvailable(); + long keybytes = Sodium.crypto_aead_aes256gcm_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_aead_aes256gcm_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + public static Key random() { + assertAvailable(); + int length = length(); + Pointer ptr = Sodium.malloc(length); + try { + // When support for 10.0.11 is dropped, use this instead + // Sodium.crypto_aead_aes256gcm_keygen(ptr); + Sodium.randombytes_buf(ptr, length); + return new Key(ptr, length); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Key)) { + return false; + } + Key other = (Key) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** An AES256-GSM nonce. */ + public static final class Nonce { + final Allocated value; + + private Nonce(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + public static Nonce fromBytes(byte[] bytes) { + assertAvailable(); + if (bytes.length != Sodium.crypto_aead_aes256gcm_npubbytes()) { + throw new IllegalArgumentException( + "nonce must be " + + Sodium.crypto_aead_aes256gcm_npubbytes() + + " bytes, got " + + bytes.length); + } + return Sodium.dup(bytes, Nonce::new); + } + + /** + * Obtain the length of the nonce in bytes (12). + * + * @return The length of the nonce in bytes (12). + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + public static int length() { + assertAvailable(); + long npubbytes = Sodium.crypto_aead_aes256gcm_npubbytes(); + if (npubbytes > Integer.MAX_VALUE) { + throw new SodiumException( + "crypto_aead_aes256gcm_npubbytes: " + npubbytes + " is too large"); + } + return (int) npubbytes; + } + + /** + * Create a zero {@link Nonce}. + * + * @return A zero nonce. + */ + public static Nonce zero() { + int length = length(); + Pointer ptr = Sodium.malloc(length); + try { + Sodium.sodium_memzero(ptr, length); + return new Nonce(ptr, length); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + /** + * Generate a random {@link Nonce}. + * + * @return A randomly generated nonce. + */ + public static Nonce random() { + return Sodium.randomBytes(length(), Nonce::new); + } + + /** + * Increment this nonce. + * + *

Note that this is not synchronized. If multiple threads are creating encrypted messages + * and incrementing this nonce, then external synchronization is required to ensure no two + * encrypt operations use the same nonce. + * + * @return A new {@link Nonce}. + */ + public Nonce increment() { + return Sodium.dupAndIncrement(value.pointer(), value.length(), Nonce::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Nonce)) { + return false; + } + Nonce other = (Nonce) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this nonce. + * + * @return The bytes of this nonce. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this nonce. + * + * @return The bytes of this nonce. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + private Pointer ctx; + + private AES256GCM(Key key) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + ctx = Sodium.malloc(Sodium.crypto_aead_aes256gcm_statebytes()); + try { + int rc = Sodium.crypto_aead_aes256gcm_beforenm(ctx, key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_beforenm: failed with result " + rc); + } + } catch (Throwable e) { + Sodium.sodium_free(ctx); + ctx = null; + throw e; + } + } + + /** + * Pre-compute the expansion for the key. + * + *

Note that the returned instance of {@link AES256GCM} should be closed using {@link #close()} + * (or try-with-resources) to ensure timely release of the expanded key, which is held in native + * memory. + * + * @param key The key to precompute an expansion for. + * @return A {@link AES256GCM} instance. + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + public static AES256GCM forKey(Key key) { + requireNonNull(key); + assertAvailable(); + return new AES256GCM(key); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, Key key, Nonce nonce) { + return encrypt(message, EMPTY_BYTES, key, nonce); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Bytes data, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + public static byte[] encrypt(byte[] message, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + + byte[] cipherText = new byte[maxCombinedCypherTextLength(message)]; + + LongLongByReference cipherTextLen = new LongLongByReference(); + int rc = + Sodium.crypto_aead_aes256gcm_encrypt( + cipherText, + cipherTextLen, + message, + message.length, + data, + data.length, + null, + nonce.value.pointer(), + key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt: failed with result " + rc); + } + + return maybeSliceResult(cipherText, cipherTextLen, "crypto_aead_aes256gcm_encrypt"); + } + + /** + * Encrypt a message. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public Bytes encrypt(Bytes message, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), nonce)); + } + + /** + * Encrypt a message. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public byte[] encrypt(byte[] message, Nonce nonce) { + return encrypt(message, EMPTY_BYTES, nonce); + } + + /** + * Encrypt a message. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public Bytes encrypt(Bytes message, Bytes data, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), data.toArrayUnsafe(), nonce)); + } + + /** + * Encrypt a message. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public byte[] encrypt(byte[] message, byte[] data, Nonce nonce) { + assertOpen(); + + byte[] cipherText = new byte[maxCombinedCypherTextLength(message)]; + + LongLongByReference cipherTextLen = new LongLongByReference(); + int rc = + Sodium.crypto_aead_aes256gcm_encrypt_afternm( + cipherText, + cipherTextLen, + message, + message.length, + data, + data.length, + null, + nonce.value.pointer(), + ctx); + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt_afternm: failed with result " + rc); + } + + return maybeSliceResult(cipherText, cipherTextLen, "crypto_aead_aes256gcm_encrypt_afternm"); + } + + private static int maxCombinedCypherTextLength(byte[] message) { + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + return (int) abytes + message.length; + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(Bytes message, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(byte[] message, Key key, Nonce nonce) { + return encryptDetached(message, EMPTY_BYTES, key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached( + Bytes message, Bytes data, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + public static DetachedEncryptionResult encryptDetached( + byte[] message, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + + byte[] cipherText = new byte[message.length]; + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + byte[] mac = new byte[(int) abytes]; + + LongLongByReference macLen = new LongLongByReference(); + int rc = + Sodium.crypto_aead_aes256gcm_encrypt_detached( + cipherText, + mac, + macLen, + message, + message.length, + data, + data.length, + null, + nonce.value.pointer(), + key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt_detached: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult( + cipherText, maybeSliceResult(mac, macLen, "crypto_aead_aes256gcm_encrypt_detached")); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(Bytes message, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), nonce); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(byte[] message, Nonce nonce) { + return encryptDetached(message, EMPTY_BYTES, nonce); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(Bytes message, Bytes data, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), data.toArrayUnsafe(), nonce); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(byte[] message, byte[] data, Nonce nonce) { + assertOpen(); + + byte[] cipherText = new byte[message.length]; + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + byte[] mac = new byte[(int) abytes]; + + LongLongByReference macLen = new LongLongByReference(); + int rc = + Sodium.crypto_aead_aes256gcm_encrypt_detached_afternm( + cipherText, + mac, + macLen, + message, + message.length, + data, + data.length, + null, + nonce.value.pointer(), + ctx); + if (rc != 0) { + throw new SodiumException( + "crypto_aead_aes256gcm_encrypt_detached_afternm: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult( + cipherText, + maybeSliceResult(mac, macLen, "crypto_aead_aes256gcm_encrypt_detached_afternm")); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce to use when decrypting. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce to use when decrypting. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, Key key, Nonce nonce) { + return decrypt(cipherText, EMPTY_BYTES, key, nonce); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Bytes data, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + + byte[] clearText = new byte[maxClearTextLength(cipherText)]; + + LongLongByReference clearTextLen = new LongLongByReference(); + int rc = + Sodium.crypto_aead_aes256gcm_decrypt( + clearText, + clearTextLen, + null, + cipherText, + cipherText.length, + data, + data.length, + nonce.value.pointer(), + key.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt: failed with result " + rc); + } + + return maybeSliceResult(clearText, clearTextLen, "crypto_aead_aes256gcm_decrypt"); + } + + /** + * Decrypt a message. + * + * @param cipherText The cipher text to decrypt. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public Bytes decrypt(Bytes cipherText, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message. + * + * @param cipherText The cipher text to decrypt. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public byte[] decrypt(byte[] cipherText, Nonce nonce) { + return decrypt(cipherText, EMPTY_BYTES, nonce); + } + + /** + * Decrypt a message. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public Bytes decrypt(Bytes cipherText, Bytes data, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), data.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public byte[] decrypt(byte[] cipherText, byte[] data, Nonce nonce) { + assertOpen(); + + byte[] clearText = new byte[maxClearTextLength(cipherText)]; + + LongLongByReference clearTextLen = new LongLongByReference(); + int rc = + Sodium.crypto_aead_aes256gcm_decrypt_afternm( + clearText, + clearTextLen, + null, + cipherText, + cipherText.length, + data, + data.length, + nonce.value.pointer(), + ctx); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_decrypt_afternm: failed with result " + rc); + } + + return maybeSliceResult(clearText, clearTextLen, "crypto_aead_aes256gcm_decrypt_afternm"); + } + + private static int maxClearTextLength(byte[] cipherText) { + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + if (abytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + return cipherText.length - ((int) abytes); + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, Key key, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptDetached(byte[] cipherText, byte[] mac, Key key, Nonce nonce) { + return decryptDetached(cipherText, mac, EMPTY_BYTES, key, nonce); + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptDetached( + Bytes cipherText, Bytes mac, Bytes data, Key key, Nonce nonce) { + byte[] bytes = + decryptDetached( + cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + @Nullable + public static byte[] decryptDetached( + byte[] cipherText, byte[] mac, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + if (mac.length != abytes) { + throw new IllegalArgumentException("mac must be " + abytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = + Sodium.crypto_aead_aes256gcm_decrypt_detached( + clearText, + null, + cipherText, + cipherText.length, + mac, + data, + data.length, + nonce.value.pointer(), + key.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_aead_aes256gcm_encrypt: failed with result " + rc); + } + + return clearText; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public Bytes decryptDetached(Bytes cipherText, Bytes mac, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public byte[] decryptDetached(byte[] cipherText, byte[] mac, Nonce nonce) { + return decryptDetached(cipherText, mac, EMPTY_BYTES, nonce); + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public Bytes decryptDetached(Bytes cipherText, Bytes mac, Bytes data, Nonce nonce) { + byte[] bytes = + decryptDetached( + cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), data.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + * @throws UnsupportedOperationException If AES256-GSM support is not available. + */ + @Nullable + public byte[] decryptDetached(byte[] cipherText, byte[] mac, byte[] data, Nonce nonce) { + assertAvailable(); + + long abytes = Sodium.crypto_aead_aes256gcm_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_aead_aes256gcm_abytes: " + abytes + " is too large"); + } + if (mac.length != abytes) { + throw new IllegalArgumentException("mac must be " + abytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = + Sodium.crypto_aead_aes256gcm_decrypt_detached_afternm( + clearText, + null, + cipherText, + cipherText.length, + mac, + data, + data.length, + nonce.value.pointer(), + ctx); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException( + "crypto_aead_aes256gcm_decrypt_detached_afternm: failed with result " + rc); + } + + return clearText; + } + + private void assertOpen() { + if (ctx == null) { + throw new IllegalStateException(getClass().getName() + ": already closed"); + } + } + + private static byte[] maybeSliceResult( + byte[] bytes, LongLongByReference actualLength, String methodName) { + if (actualLength.longValue() == bytes.length) { + return bytes; + } + if (actualLength.longValue() > Integer.MAX_VALUE) { + throw new SodiumException( + methodName + ": result of length " + actualLength.longValue() + " is too large"); + } + return Arrays.copyOfRange(bytes, 0, actualLength.intValue()); + } + + @Override + public void close() { + if (ctx != null) { + Sodium.sodium_free(ctx); + ctx = null; + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Allocated.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Allocated.java new file mode 100644 index 000000000..2013b4f11 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Allocated.java @@ -0,0 +1,127 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.v2.bytes.Bytes; + +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; +import org.jetbrains.annotations.Nullable; + +/** + * Allocated objects track allocation of memory using Sodium. + * + * @see Secure memory + */ +public final class Allocated implements Destroyable { + + /** + * Assign bytes using Sodium memory allocation + * + * @param bytes the bytes to assign + * @return a new allocated value filled with the bytes + */ + public static Allocated fromBytes(Bytes bytes) { + Allocated allocated = Allocated.allocate(bytes.size()); + allocated.pointer().put(0, bytes.toArrayUnsafe(), 0, bytes.size()); + return allocated; + } + + /** + * Allocate bytes using Sodium memory allocation + * + * @param length the length of the memory allocation, in bytes + * @return a new allocated value + */ + static Allocated allocate(long length) { + Pointer ptr = Sodium.malloc(length); + return new Allocated(ptr, (int) length); + } + + @Nullable private Pointer ptr; + private final int length; + + Allocated(Pointer ptr, int length) { + this.ptr = ptr; + this.length = length; + } + + Pointer pointer() { + if (isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + return ptr; + } + + int length() { + return length; + } + + /** Destroys the value from memory. */ + @Override + public void destroy() { + if (!isDestroyed()) { + Pointer p = ptr; + ptr = null; + Sodium.sodium_free(p); + } + } + + /** + * Returns true if the value is destroyed. + * + * @return true if the allocated value is destroyed + */ + @Override + public boolean isDestroyed() { + return ptr == null; + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + if (isDestroyed()) { + throw new IllegalStateException("allocated value has been destroyed"); + } + return Sodium.reify(ptr, length); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Allocated)) { + return false; + } + Allocated other = (Allocated) obj; + if (isDestroyed()) { + throw new IllegalStateException("allocated value has been destroyed"); + } + if (other.isDestroyed()) { + throw new IllegalStateException("allocated value has been destroyed"); + } + return Sodium.sodium_memcmp(this.ptr, other.ptr, length) == 0; + } + + @Override + public int hashCode() { + if (isDestroyed()) { + throw new IllegalStateException("allocated value has been destroyed"); + } + return Sodium.hashCode(ptr, length); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Auth.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Auth.java new file mode 100644 index 000000000..b2bd3aca3 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Auth.java @@ -0,0 +1,228 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/secret-key_cryptography/secret-key_authentication.md + +/** + * Secret-key authentication. + * + *

These operations computes an authentication tag for a message and a secret key, and provides a + * way to verify that a given tag is valid for a given message and a key. + * + *

The function computing the tag is deterministic: the same (message, key) tuple will always + * produce the same output. + * + *

However, even if the message is public, knowing the key is required in order to be able to + * compute a valid tag. Therefore, the key should remain confidential. The tag, however, can be + * public. + * + *

A typical use case is: + * + *

    + *
  • {@code A} prepares a message, add an authentication tag, sends it to {@code B} + *
  • {@code A} doesn't store the message + *
  • Later on, {@code B} sends the message and the authentication tag to {@code A} + *
  • {@code A} uses the authentication tag to verify that it created this message. + *
+ * + *

This operation does not encrypt the message. It only computes and verifies an authentication + * tag. + */ +public final class Auth { + private Auth() {} + + /** An Auth key. */ + public static final class Key implements Destroyable { + final Allocated value; + + private Key(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_auth_keybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_auth_keybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_auth_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_auth_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + */ + public static Key random() { + int length = length(); + Pointer ptr = Sodium.malloc(length); + try { + // When support for 10.0.11 is dropped, use this instead + // Sodium.crypto_auth_keygen(ptr); + Sodium.randombytes_buf(ptr, length); + return new Key(ptr, length); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Key)) { + return false; + } + Key other = (Key) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** + * Create an authentication tag for a given input. + * + * @param input The input to generate an authentication tag for. + * @param key A confidential key. + * @return The authentication tag. + */ + public static Bytes auth(Bytes input, Key key) { + return Bytes.wrap(auth(input.toArrayUnsafe(), key)); + } + + /** + * Create an authentication tag for a given input. + * + * @param input The input to generate an authentication tag for. + * @param key A confidential key. + * @return The authentication tag. + */ + public static byte[] auth(byte[] input, Key key) { + if (key.isDestroyed()) { + throw new IllegalStateException("Key has been destroyed"); + } + long abytes = Sodium.crypto_auth_bytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_auth_bytes: " + abytes + " is too large"); + } + byte[] tag = new byte[(int) abytes]; + + int rc = Sodium.crypto_auth(tag, input, input.length, key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_auth_bytes: failed with result " + rc); + } + return tag; + } + + /** + * Verify an input using an authentication tag. + * + * @param tag The authentication tag for the input. + * @param input The input. + * @param key A confidential key that was used for tag creation. + * @return {@code true} if the tag correction authenticates the input (using the specified key). + */ + public static boolean verify(Bytes tag, Bytes input, Key key) { + return verify(tag.toArrayUnsafe(), input.toArrayUnsafe(), key); + } + + /** + * Verify an input using an authentication tag. + * + * @param tag The authentication tag for the input. + * @param input The input. + * @param key A confidential key that was used for tag creation. + * @return {@code true} if the tag correction authenticates the input (using the specified key). + */ + public static boolean verify(byte[] tag, byte[] input, Key key) { + if (key.isDestroyed()) { + throw new IllegalStateException("Key has been destroyed"); + } + long abytes = Sodium.crypto_auth_bytes(); + if (tag.length != abytes) { + throw new IllegalArgumentException("tag must be " + abytes + " bytes, got " + tag.length); + } + int rc = Sodium.crypto_auth_verify(tag, input, input.length, key.value.pointer()); + return (rc == 0); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Box.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Box.java new file mode 100644 index 000000000..3ff9b82e2 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Box.java @@ -0,0 +1,1225 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Objects; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; +import org.jetbrains.annotations.Nullable; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/public-key_cryptography/authenticated_encryption.md + +/** + * Public-key authenticated encryption. + * + *

Using public-key authenticated encryption, Bob can encrypt a confidential message specifically + * for Alice, using Alice's public key. + * + *

Using Bob's public key, Alice can compute a shared secret key. Using Alice's public key and + * his secret key, Bob can compute the exact same shared secret key. That shared secret key can be + * used to verify that the encrypted message was not tampered with, before eventually decrypting it. + * + *

Alice only needs Bob's public key, the nonce and the ciphertext. Bob should never ever share + * his secret key, even with Alice. + * + *

And in order to send messages to Alice, Bob only needs Alice's public key. Alice should never + * ever share her secret key either, even with Bob. + * + *

Alice can reply to Bob using the same system, without having to generate a distinct key pair. + * + *

The nonce doesn't have to be confidential, but it should be used with just one encryption for + * a particular pair of public and secret keys. + * + *

One easy way to generate a nonce is to use {@link Nonce#random()}, considering the size of the + * nonces the risk of any random collisions is negligible. For some applications, if you wish to use + * nonces to detect missing messages or to ignore replayed messages, it is also acceptable to use an + * incrementing counter as a nonce. + * + *

When doing so you must ensure that the same value can never be re-used (for example you may + * have multiple threads or even hosts generating messages using the same key pairs). + * + *

As stated above, senders can decrypt their own messages, and compute a valid authentication + * tag for any messages encrypted with a given shared secret key. This is generally not an issue for + * online protocols. + * + *

This class depends upon the JNR-FFI library being available on the classpath, along with its + * dependencies. See https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle + * dependency 'com.github.jnr:jnr-ffi'. + */ +public final class Box implements AutoCloseable { + + /** A Box public key. */ + public static final class PublicKey implements Destroyable { + final Allocated value; + + private PublicKey(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_box_publickeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_box_publickeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, PublicKey::new); + } + + /** + * Transforms the Ed25519 signature public key to a Curve25519 public key. See + * https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519 + * + * @param publicKey the signature public key + * @return the public key as a Curve25519 public key + */ + public static PublicKey forSignaturePublicKey(Signature.PublicKey publicKey) { + Pointer publicKeyPtr = null; + try { + int publicKeyLength = PublicKey.length(); + publicKeyPtr = Sodium.malloc(publicKeyLength); + int rc = + Sodium.crypto_sign_ed25519_pk_to_curve25519(publicKeyPtr, publicKey.value.pointer()); + if (rc != 0) { + throw new SodiumException( + "crypto_sign_ed25519_pk_to_curve25519: failed with results " + rc); + } + PublicKey pk = new PublicKey(publicKeyPtr, publicKeyLength); + publicKeyPtr = null; + return pk; + } catch (Throwable e) { + if (publicKeyPtr != null) { + Sodium.sodium_free(publicKeyPtr); + } + throw e; + } + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_box_publickeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_box_publickeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof PublicKey)) { + return false; + } + PublicKey other = (PublicKey) obj; + return this.value.equals(other.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + } + + /** A Box secret key. */ + public static final class SecretKey implements Destroyable { + + final Allocated value; + + private SecretKey(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_box_secretkeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_box_secretkeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, SecretKey::new); + } + + /** + * Transforms the Ed25519 secret key to a Curve25519 secret key. See + * https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519 + * + * @param secretKey the signature secret key + * @return the secret key as a Curve25519 secret key + */ + public static SecretKey forSignatureSecretKey(Signature.SecretKey secretKey) { + if (secretKey.value.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + Pointer secretKeyPtr = null; + try { + int secretKeyLength = SecretKey.length(); + secretKeyPtr = Sodium.malloc(secretKeyLength); + int rc = + Sodium.crypto_sign_ed25519_sk_to_curve25519(secretKeyPtr, secretKey.value.pointer()); + if (rc != 0) { + throw new SodiumException( + "crypto_sign_ed25519_sk_to_curve25519: failed with results " + rc); + } + SecretKey sk = new SecretKey(secretKeyPtr, secretKeyLength); + secretKeyPtr = null; + return sk; + } catch (Throwable e) { + if (secretKeyPtr != null) { + Sodium.sodium_free(secretKeyPtr); + } + throw e; + } + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_box_secretkeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_box_secretkeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SecretKey)) { + return false; + } + SecretKey other = (SecretKey) obj; + return other.value.equals(this.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A Box key pair seed. */ + public static final class Seed { + final Allocated value; + + private Seed(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_box_seedbytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_box_seedbytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Seed::new); + } + + /** + * Obtain the length of the seed in bytes (32). + * + * @return The length of the seed in bytes (32). + */ + public static int length() { + long seedbytes = Sodium.crypto_box_seedbytes(); + if (seedbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_box_seedbytes: " + seedbytes + " is too large"); + } + return (int) seedbytes; + } + + /** + * Generate a new {@link Seed} using a random generator. + * + * @return A randomly generated seed. + */ + public static Seed random() { + return Sodium.randomBytes(length(), Seed::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Seed)) { + return false; + } + Seed other = (Seed) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this seed + * + * @return The bytes of this seed. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this seed + * + * @return The bytes of this seed. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A Box key pair. */ + public static final class KeyPair { + + private final PublicKey publicKey; + private final SecretKey secretKey; + + /** + * Create a {@link KeyPair} from pair of keys. + * + * @param publicKey The bytes for the public key. + * @param secretKey The bytes for the secret key. + */ + public KeyPair(PublicKey publicKey, SecretKey secretKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + } + + /** + * Create a {@link KeyPair} from an array of secret key bytes. + * + * @param secretKey The secret key. + * @return A {@link KeyPair}. + */ + public static KeyPair forSecretKey(SecretKey secretKey) { + if (secretKey.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + return Sodium.scalarMultBase( + secretKey.value.pointer(), + SecretKey.length(), + (ptr, len) -> { + int publicKeyLength = PublicKey.length(); + if (len != publicKeyLength) { + throw new IllegalStateException( + "Public key length " + + publicKeyLength + + " is not same as generated key length " + + len); + } + return new KeyPair(new PublicKey(ptr, publicKeyLength), secretKey); + }); + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key pair. + */ + public static KeyPair random() { + int publicKeyLength = PublicKey.length(); + Pointer publicKey = Sodium.malloc(publicKeyLength); + Pointer secretKey = null; + try { + int secretKeyLength = SecretKey.length(); + secretKey = Sodium.malloc(secretKeyLength); + int rc = Sodium.crypto_box_keypair(publicKey, secretKey); + if (rc != 0) { + throw new SodiumException("crypto_box_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey, publicKeyLength); + publicKey = null; + SecretKey sk = new SecretKey(secretKey, secretKeyLength); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * Generate a new key using a seed. + * + * @param seed A seed. + * @return The generated key pair. + */ + public static KeyPair fromSeed(Seed seed) { + int publicKeyLength = PublicKey.length(); + Pointer publicKey = Sodium.malloc(publicKeyLength); + Pointer secretKey = null; + try { + int secretKeyLength = SecretKey.length(); + secretKey = Sodium.malloc(secretKeyLength); + int rc = Sodium.crypto_box_seed_keypair(publicKey, secretKey, seed.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_box_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey, publicKeyLength); + publicKey = null; + SecretKey sk = new SecretKey(secretKey, secretKeyLength); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * Converts signature key pair (Ed25519) to a box key pair (Curve25519) so that the same key + * pair can be used both for authenticated encryption and for signatures. See + * https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519 + * + * @param keyPair A {@link Signature.KeyPair}. + * @return A {@link KeyPair}. + */ + public static KeyPair forSignatureKeyPair(Signature.KeyPair keyPair) { + return forSecretKey(SecretKey.forSignatureSecretKey(keyPair.secretKey())); + } + + /** + * Provides the public key + * + * @return The public key of the key pair. + */ + public PublicKey publicKey() { + return publicKey; + } + + /** + * Provides the secret key + * + * @return The secret key of the key pair. + */ + public SecretKey secretKey() { + return secretKey; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof KeyPair)) { + return false; + } + KeyPair other = (KeyPair) obj; + return this.publicKey.equals(other.publicKey) && this.secretKey.equals(other.secretKey); + } + + @Override + public int hashCode() { + return Objects.hash(publicKey, secretKey); + } + } + + /** A Box nonce. */ + public static final class Nonce { + final Allocated value; + + private Nonce(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_box_noncebytes()) { + throw new IllegalArgumentException( + "nonce must be " + Sodium.crypto_box_noncebytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Nonce::new); + } + + /** + * Obtain the length of the nonce in bytes (24). + * + * @return The length of the nonce in bytes (24). + */ + public static int length() { + long npubbytes = Sodium.crypto_box_noncebytes(); + if (npubbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_box_noncebytes: " + npubbytes + " is too large"); + } + return (int) npubbytes; + } + + /** + * Create a zero {@link Nonce}. + * + * @return A zero nonce. + */ + public static Nonce zero() { + int length = length(); + Pointer ptr = Sodium.malloc(length); + try { + Sodium.sodium_memzero(ptr, length); + return new Nonce(ptr, length); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + /** + * Generate a random {@link Nonce}. + * + * @return A randomly generated nonce. + */ + public static Nonce random() { + return Sodium.randomBytes(length(), Nonce::new); + } + + /** + * Increment this nonce. + * + *

Note that this is not synchronized. If multiple threads are creating encrypted messages + * and incrementing this nonce, then external synchronization is required to ensure no two + * encrypt operations use the same nonce. + * + * @return A new {@link Nonce}. + */ + public Nonce increment() { + return Sodium.dupAndIncrement(value.pointer(), value.length(), Nonce::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Nonce)) { + return false; + } + Nonce other = (Nonce) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this nonce + * + * @return The bytes of this nonce. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this nonce + * + * @return The bytes of this nonce. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + private Pointer ctx; + + private Box(PublicKey publicKey, SecretKey secretKey) { + if (secretKey.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + ctx = Sodium.malloc(Sodium.crypto_box_beforenmbytes()); + try { + int rc = + Sodium.crypto_box_beforenm(ctx, publicKey.value.pointer(), secretKey.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_box_beforenm: failed with result " + rc); + } + } catch (Throwable e) { + Sodium.sodium_free(ctx); + ctx = null; + throw e; + } + } + + /** + * Precompute the shared key for a given sender and receiver. + * + *

Note that the returned instance of {@link Box} should be closed using {@link #close()} (or + * try-with-resources) to ensure timely release of the shared key, which is held in native memory. + * + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @return A {@link Box} instance. + */ + public static Box forKeys(PublicKey receiver, SecretKey sender) { + return new Box(receiver, sender); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, PublicKey receiver, SecretKey sender, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), receiver, sender, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, PublicKey receiver, SecretKey sender, Nonce nonce) { + if (sender.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + byte[] cipherText = new byte[combinedCypherTextLength(message)]; + + int rc = + Sodium.crypto_box_easy( + cipherText, + message, + message.length, + nonce.value.pointer(), + receiver.value.pointer(), + sender.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_box_easy: failed with result " + rc); + } + + return cipherText; + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public Bytes encrypt(Bytes message, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public byte[] encrypt(byte[] message, Nonce nonce) { + assertOpen(); + + byte[] cipherText = new byte[combinedCypherTextLength(message)]; + + int rc = + Sodium.crypto_box_easy_afternm( + cipherText, message, message.length, nonce.value.pointer(), ctx); + if (rc != 0) { + throw new SodiumException("crypto_box_easy_afternm: failed with result " + rc); + } + + return cipherText; + } + + /** + * Encrypt a sealed message for a given key. + * + *

Sealed boxes are designed to anonymously send messages to a recipient given its public key. + * + *

Only the recipient can decrypt these messages, using its private key. While the recipient + * can verify the integrity of the message, it cannot verify the identity of the sender. + * + *

A message is encrypted using an ephemeral key pair, whose secret part is destroyed right + * after the encryption process. + * + *

Without knowing the secret key used for a given message, the sender cannot decrypt its own + * message later. And without additional data, a message cannot be correlated with the identity of + * its sender. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @return The encrypted data. + */ + public static Bytes encryptSealed(Bytes message, PublicKey receiver) { + return Bytes.wrap(encryptSealed(message.toArrayUnsafe(), receiver)); + } + + /** + * Encrypt a sealed message for a given key. + * + *

Sealed boxes are designed to anonymously send messages to a recipient given its public key. + * + *

Only the recipient can decrypt these messages, using its private key. While the recipient + * can verify the integrity of the message, it cannot verify the identity of the sender. + * + *

A message is encrypted using an ephemeral key pair, whose secret part is destroyed right + * after the encryption process. + * + *

Without knowing the secret key used for a given message, the sender cannot decrypt its own + * message later. And without additional data, a message cannot be correlated with the identity of + * its sender. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @return The encrypted data. + */ + public static byte[] encryptSealed(byte[] message, PublicKey receiver) { + long sealbytes = Sodium.crypto_box_sealbytes(); + if (sealbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_sealbytes: " + sealbytes + " is too large"); + } + byte[] cipherText = new byte[(int) sealbytes + message.length]; + + int rc = Sodium.crypto_box_seal(cipherText, message, message.length, receiver.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_box_seal: failed with result " + rc); + } + + return cipherText; + } + + private static int combinedCypherTextLength(byte[] message) { + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + return (int) macbytes + message.length; + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached( + Bytes message, PublicKey receiver, SecretKey sender, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), receiver, sender, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param receiver The public key of the receiver. + * @param sender The secret key of the sender. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached( + byte[] message, PublicKey receiver, SecretKey sender, Nonce nonce) { + if (sender.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + byte[] cipherText = new byte[message.length]; + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + byte[] mac = new byte[(int) macbytes]; + + int rc = + Sodium.crypto_box_detached( + cipherText, + mac, + message, + message.length, + nonce.value.pointer(), + receiver.value.pointer(), + sender.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_box_detached: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult(cipherText, mac); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(Bytes message, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), nonce); + } + + /** + * Encrypt a message, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public DetachedEncryptionResult encryptDetached(byte[] message, Nonce nonce) { + assertOpen(); + + byte[] cipherText = new byte[message.length]; + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + byte[] mac = new byte[(int) macbytes]; + + int rc = + Sodium.crypto_box_detached_afternm( + cipherText, mac, message, message.length, nonce.value.pointer(), ctx); + if (rc != 0) { + throw new SodiumException("crypto_box_detached_afternm: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult(cipherText, mac); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, PublicKey sender, SecretKey receiver, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), sender, receiver, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decrypt( + byte[] cipherText, PublicKey sender, SecretKey receiver, Nonce nonce) { + if (sender.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + byte[] clearText = new byte[clearTextLength(cipherText)]; + + int rc = + Sodium.crypto_box_open_easy( + clearText, + cipherText, + cipherText.length, + nonce.value.pointer(), + sender.value.pointer(), + receiver.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_open_easy: failed with result " + rc); + } + + return clearText; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public Bytes decrypt(Bytes cipherText, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + public byte[] decrypt(byte[] cipherText, Nonce nonce) { + assertOpen(); + + byte[] clearText = new byte[clearTextLength(cipherText)]; + + int rc = + Sodium.crypto_box_open_easy_afternm( + clearText, cipherText, cipherText.length, nonce.value.pointer(), ctx); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_open_easy_afternm: failed with result " + rc); + } + + return clearText; + } + + private static int clearTextLength(byte[] cipherText) { + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + if (macbytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + return cipherText.length - ((int) macbytes); + } + + /** + * Decrypt a sealed message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptSealed(Bytes cipherText, PublicKey sender, SecretKey receiver) { + byte[] bytes = decryptSealed(cipherText.toArrayUnsafe(), sender, receiver); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a sealed message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param receiver The public key of the receiver. + * @param receiver The secret key of the receiver. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptSealed(byte[] cipherText, PublicKey receiverPk, SecretKey receiver) { + if (receiver.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + long sealbytes = Sodium.crypto_box_sealbytes(); + if (sealbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_sealbytes: " + sealbytes + " is too large"); + } + if (sealbytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + byte[] clearText = new byte[cipherText.length - ((int) sealbytes)]; + + int rc = + Sodium.crypto_box_seal_open( + clearText, + cipherText, + cipherText.length, + receiverPk.value.pointer(), + receiver.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_seal_open: failed with result " + rc); + } + + return clearText; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptDetached( + Bytes cipherText, Bytes mac, PublicKey sender, SecretKey receiver, Nonce nonce) { + byte[] bytes = + decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), sender, receiver, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param sender The public key of the sender. + * @param receiver The secret key of the receiver. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptDetached( + byte[] cipherText, byte[] mac, PublicKey sender, SecretKey receiver, Nonce nonce) { + if (receiver.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + if (mac.length != macbytes) { + throw new IllegalArgumentException("mac must be " + macbytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = + Sodium.crypto_box_open_detached( + clearText, + cipherText, + mac, + cipherText.length, + nonce.value.pointer(), + sender.value.pointer(), + receiver.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_open_detached: failed with result " + rc); + } + + return clearText; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public Bytes decryptDetached(Bytes cipherText, Bytes mac, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public byte[] decryptDetached(byte[] cipherText, byte[] mac, Nonce nonce) { + long macbytes = Sodium.crypto_box_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_box_macbytes: " + macbytes + " is too large"); + } + if (mac.length != macbytes) { + throw new IllegalArgumentException("mac must be " + macbytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = + Sodium.crypto_box_open_detached_afternm( + clearText, cipherText, mac, cipherText.length, nonce.value.pointer(), ctx); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_box_open_detached_afternm: failed with result " + rc); + } + + return clearText; + } + + private void assertOpen() { + if (ctx == null) { + throw new IllegalStateException(getClass().getName() + ": already closed"); + } + } + + @Override + public void close() { + if (ctx != null) { + Sodium.sodium_free(ctx); + ctx = null; + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Concatenate.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Concatenate.java new file mode 100644 index 000000000..329293fea --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Concatenate.java @@ -0,0 +1,135 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import java.util.ArrayList; +import java.util.List; + +import jnr.ffi.Pointer; + +/** Concatenate elements allocated to Sodium memory. */ +public final class Concatenate { + + private final List values = new ArrayList<>(); + + /** + * Adds a hash to the elements to concatenate. + * + * @param hash a generic hash + * @return the Concatenate instance + */ + public Concatenate add(GenericHash.Hash hash) { + values.add(hash.value); + return this; + } + + /** + * Adds a hash to the elements to concatenate. + * + * @param hash a generic hash + * @return the Concatenate instance + */ + public Concatenate add(SHA256Hash.Hash hash) { + values.add(hash.value); + return this; + } + + /** + * Adds a HMAC key to the elements to concatenate. + * + * @param key a HMAC key + * @return the Concatenate instance + */ + public Concatenate add(HMACSHA512256.Key key) { + values.add(key.value); + return this; + } + + /** + * Adds a memory allocated value to the elements to concatenate. + * + * @param allocated a memory allocated value + * @return the Concatenate instance + */ + public Concatenate add(Allocated allocated) { + values.add(allocated); + return this; + } + + /** + * Adds a key to the elements to concatenate. + * + * @param key a Diffie-Helman key + * @return the Concatenate instance + */ + public Concatenate add(DiffieHelman.Secret key) { + values.add(key.value); + return this; + } + + /** + * Adds a public key to the elements to concatenate. + * + * @param key a public key + * @return the Concatenate instance + */ + public Concatenate add(Signature.PublicKey key) { + values.add(key.value); + return this; + } + + /** + * Adds a public key to the elements to concatenate. + * + * @param key a public key + * @return the Concatenate instance + */ + public Concatenate add(Box.PublicKey key) { + values.add(key.value); + return this; + } + + /** + * Adds a key to the elements to concatenate. + * + * @param key a secret key + * @return the Concatenate instance + */ + public Concatenate add(Box.SecretKey key) { + values.add(key.value); + return this; + } + + /** + * Adds a key to the elements to concatenate. + * + * @param key a secret key + * @return the Concatenate instance + */ + public Concatenate add(Signature.SecretKey key) { + values.add(key.value); + return this; + } + + /** + * Concatenates the values collected into a new safe memory allocation + * + * @return the result of the concatenation operation + */ + @SuppressWarnings("unchecked") + public Allocated concatenate() { + int concatenatedLength = values.stream().mapToInt(v -> v.length()).sum(); + Pointer ptr = Sodium.malloc(concatenatedLength); + try { + int index = 0; + for (Allocated value : values) { + ptr.transferFrom(index, value.pointer(), 0, value.length()); + index += value.length(); + } + return new Allocated(ptr, concatenatedLength); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw new RuntimeException(e); + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DefaultDetachedEncryptionResult.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DefaultDetachedEncryptionResult.java new file mode 100644 index 000000000..e3d1d4854 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DefaultDetachedEncryptionResult.java @@ -0,0 +1,36 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.v2.bytes.Bytes; + +final class DefaultDetachedEncryptionResult implements DetachedEncryptionResult { + + private final byte[] cipherText; + private final byte[] mac; + + DefaultDetachedEncryptionResult(byte[] cipherText, byte[] mac) { + this.cipherText = cipherText; + this.mac = mac; + } + + @Override + public Bytes cipherText() { + return Bytes.wrap(cipherText); + } + + @Override + public byte[] cipherTextArray() { + return cipherText; + } + + @Override + public Bytes mac() { + return Bytes.wrap(mac); + } + + @Override + public byte[] macArray() { + return mac; + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DetachedEncryptionResult.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DetachedEncryptionResult.java new file mode 100644 index 000000000..f261fbc6e --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DetachedEncryptionResult.java @@ -0,0 +1,37 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.v2.bytes.Bytes; + +/** The result from a detached encryption. */ +public interface DetachedEncryptionResult { + + /** + * Provides the cipher text + * + * @return The cipher text. + */ + Bytes cipherText(); + + /** + * Provides the cipher text + * + * @return The cipher text. + */ + byte[] cipherTextArray(); + + /** + * Provides the message authentication code + * + * @return The message authentication code. + */ + Bytes mac(); + + /** + * Provides the message authentication code + * + * @return The message authentication code. + */ + byte[] macArray(); +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelman.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelman.java new file mode 100644 index 000000000..78ac936ec --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelman.java @@ -0,0 +1,463 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Objects; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; + +/** + * Sodium provides an API to perform scalar multiplication of elliptic curve points. + * + *

This can be used as a building block to construct key exchange mechanisms, or more generally + * to compute a public key from a secret key. + * + *

On current libsodium versions, you generally want to use the crypto_kx API for key exchange + * instead. + * + * @see KeyExchange + */ +public final class DiffieHelman { + + /** A Diffie-Helman public key. */ + public static final class PublicKey { + final Allocated value; + + private PublicKey(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Creates a new public key based on a signature public key. + * + * @param publicKey the signature public key to copy + * @return A public key. + */ + public static PublicKey forBoxPublicKey(Box.PublicKey publicKey) { + return new PublicKey(Sodium.dup(publicKey.value.pointer(), length()), length()); + } + + /** + * Creates a new public key based on a signature public key. + * + * @param publicKey the signature public key to copy + * @return A public key. + */ + public static PublicKey forSignaturePublicKey(Signature.PublicKey publicKey) { + return forBoxPublicKey(Box.PublicKey.forSignaturePublicKey(publicKey)); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_box_publickeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_box_publickeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, PublicKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_scalarmult_bytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_scalarmult_bytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof PublicKey)) { + return false; + } + PublicKey other = (PublicKey) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A Diffie-Helman secret key. */ + public static final class SecretKey implements Destroyable { + final Allocated value; + + private SecretKey(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Creates a new secret key based on a box secret key. + * + * @param secretKey the box secret key to copy + * @return A secret key. + */ + public static SecretKey forBoxSecretKey(Box.SecretKey secretKey) { + return new SecretKey(Sodium.dup(secretKey.value.pointer(), length()), length()); + } + + /** + * Creates a new secret key based on a signature secret key. + * + * @param secretKey the signature secret key to copy + * @return A secret key. + */ + public static SecretKey forSignatureSecretKey(Signature.SecretKey secretKey) { + return forBoxSecretKey(Box.SecretKey.forSignatureSecretKey(secretKey)); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_scalarmult_scalarbytes()) { + throw new IllegalArgumentException( + "key must be " + + Sodium.crypto_scalarmult_scalarbytes() + + " bytes, got " + + bytes.length); + } + return Sodium.dup(bytes, SecretKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_scalarmult_scalarbytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_scalarmult_scalarbytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SecretKey)) { + return false; + } + SecretKey other = (SecretKey) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A Diffie-Helman key pair. */ + public static final class KeyPair { + + private final PublicKey publicKey; + private final SecretKey secretKey; + + /** + * Create a {@link KeyPair} from pair of keys. + * + * @param publicKey The bytes for the public key. + * @param secretKey The bytes for the secret key. + */ + public KeyPair(PublicKey publicKey, SecretKey secretKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + } + + /** + * Create a {@link KeyPair} from an array of secret key bytes. + * + * @param secretKey The secret key. + * @return A {@link KeyPair}. + */ + public static KeyPair forSecretKey(SecretKey secretKey) { + if (secretKey.isDestroyed()) { + throw new IllegalStateException("SecretKey has been destroyed"); + } + return Sodium.scalarMultBase( + secretKey.value.pointer(), + SecretKey.length(), + (ptr, len) -> { + int publicKeyLength = PublicKey.length(); + if (len != publicKeyLength) { + throw new IllegalStateException( + "Public key length " + + publicKeyLength + + " is not same as generated key length " + + len); + } + return new KeyPair(new PublicKey(ptr, publicKeyLength), secretKey); + }); + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key pair. + */ + public static KeyPair random() { + return Sodium.randomBytes( + SecretKey.length(), (ptr, len) -> forSecretKey(new SecretKey(ptr, len))); + } + + /** + * Provides the public key + * + * @return The public key of the key pair. + */ + public PublicKey publicKey() { + return publicKey; + } + + /** + * Provides the secret key + * + * @return The secret key of the key pair. + */ + public SecretKey secretKey() { + return secretKey; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof KeyPair)) { + return false; + } + KeyPair other = (KeyPair) obj; + return this.publicKey.equals(other.publicKey) && this.secretKey.equals(other.secretKey); + } + + @Override + public int hashCode() { + return Objects.hash(publicKey, secretKey); + } + } + + /** A Diffie-Helman shared secret. */ + public static final class Secret implements Destroyable { + final Allocated value; + + private Secret(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Compute a shared {@link Secret} from a secret key and a public key. + * + * @param secretKey the user's secret key + * @param publicKey another user's public key + * @return A shared {@link Secret}. + */ + public static Secret forKeys(SecretKey secretKey, PublicKey publicKey) { + if (secretKey.isDestroyed()) { + throw new IllegalStateException("SecretKey has been destroyed"); + } + return Sodium.scalarMult( + secretKey.value.pointer(), + secretKey.value.length(), + publicKey.value.pointer(), + publicKey.value.length(), + (ptr, len) -> { + int secretLength = Secret.length(); + if (len != secretLength) { + throw new IllegalStateException( + "Secret length " + secretLength + " is not same as generated key length " + len); + } + return new Secret(ptr, secretLength); + }); + } + + /** + * Create a {@link Secret} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static Secret fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Secret} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static Secret fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_scalarmult_bytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_scalarmult_bytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Secret::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_scalarmult_bytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_scalarmult_bytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Secret)) { + return false; + } + Secret other = (Secret) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/GenericHash.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/GenericHash.java new file mode 100644 index 000000000..8fdd86fd2 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/GenericHash.java @@ -0,0 +1,325 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Objects; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; + +/** + * Generic hashing utility (BLAKE2b). + * + * @see Generic hashing + */ +public final class GenericHash { + + /** Input of generic hash function. */ + public static final class Input implements Destroyable { + private final Allocated value; + + private Input(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Provides the length of the input + * + * @return the length of the input + */ + public int length() { + return value.length(); + } + + /** + * Create a {@link GenericHash.Input} from a pointer. + * + * @param allocated the allocated pointer + * @return An input. + */ + public static Input fromPointer(Allocated allocated) { + return new Input(Sodium.dup(allocated.pointer(), allocated.length()), allocated.length()); + } + + /** + * Create a {@link GenericHash.Input} from a hash. + * + * @param hash the hash + * @return An input. + */ + public static Input fromHash(Hash hash) { + return new Input(Sodium.dup(hash.value.pointer(), hash.value.length()), hash.value.length()); + } + + /** + * Create a {@link GenericHash.Input} from an array of bytes. + * + * @param bytes The bytes for the input. + * @return An input. + */ + public static Input fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link GenericHash.Input} from an array of bytes. + * + * @param bytes The bytes for the input. + * @return An input. + */ + public static Input fromBytes(byte[] bytes) { + return Sodium.dup(bytes, GenericHash.Input::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof GenericHash.Input)) { + return false; + } + Input other = (Input) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** Key of generic hash function. */ + public static final class Key implements Destroyable { + private final Allocated value; + + private Key(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Provides the length of the key + * + * @return the length of the key + */ + public int length() { + return value.length(); + } + + /** + * Create a {@link GenericHash.Key} from a pointer. + * + * @param allocated the allocated pointer + * @return A key. + */ + public static Key fromPointer(Allocated allocated) { + return new Key(Sodium.dup(allocated.pointer(), allocated.length()), allocated.length()); + } + + /** + * Create a {@link GenericHash.Key} from a hash. + * + * @param hash the hash + * @return A key. + */ + public static Key fromHash(Hash hash) { + return new Key(Sodium.dup(hash.value.pointer(), hash.value.length()), hash.value.length()); + } + + /** + * Create a {@link GenericHash.Key} from an array of bytes. + * + * @param bytes The bytes for the key. + * @return A key. + */ + public static Key fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link GenericHash.Key} from an array of bytes. + * + * @param bytes The bytes for the key. + * @return A key. + */ + public static Key fromBytes(byte[] bytes) { + return Sodium.dup(bytes, GenericHash.Key::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof GenericHash.Key)) { + return false; + } + Key other = (Key) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** Generic hash function output. */ + public static final class Hash implements Destroyable { + Allocated value; + + Hash(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof GenericHash.Hash)) { + return false; + } + GenericHash.Hash other = (GenericHash.Hash) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Obtain the bytes of this hash. + * + *

WARNING: This will cause the hash to be copied into heap memory. + * + * @return The bytes of this hash. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this hash. + * + *

WARNING: This will cause the hash to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this hash. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + + /** + * Provide the length of this hash. + * + * @return the length of this hash. + */ + public int length() { + return value.length(); + } + } + + /** + * Creates a generic hash of specified length of the input + * + * @param hashLength the length of the hash + * @param input the input of the hash function + * @return the hash of the input + */ + public static Hash hash(int hashLength, Input input) { + Pointer output = Sodium.malloc(hashLength); + Sodium.crypto_generichash(output, hashLength, input.value.pointer(), input.length(), null, 0); + return new Hash(output, hashLength); + } + + /** + * Creates a generic hash of specified length of the input + * + * @param hashLength the length of the hash + * @param input the input of the hash function + * @param key the key of the hash function + * @return the hash of the input + */ + public static Hash hash(int hashLength, Input input, Key key) { + Pointer output = Sodium.malloc(hashLength); + Sodium.crypto_generichash( + output, + hashLength, + input.value.pointer(), + input.length(), + key.value.pointer(), + key.length()); + return new Hash(output, hashLength); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256.java new file mode 100644 index 000000000..9d33a5ac2 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256.java @@ -0,0 +1,196 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; + +/** Message authentication code support for HMAC-SHA-256. */ +public final class HMACSHA256 { + + private HMACSHA256() {} + + /** A HMACSHA256 secret key. */ + public static final class Key implements Destroyable { + final Allocated value; + + Key(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static Key fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static Key fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_auth_hmacsha256_keybytes()) { + throw new IllegalArgumentException( + "key must be " + + Sodium.crypto_auth_hmacsha256_keybytes() + + " bytes, got " + + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Generate a random {@link Key}. + * + * @return A randomly generated secret key. + */ + public static Key random() { + return Sodium.randomBytes(length(), Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_auth_hmacsha256_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_auth_hmacsha256_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Key)) { + return false; + } + Key other = (Key) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** + * Authenticates a message using a secret into a HMAC-SHA-256 authenticator. + * + * @param message the message to authenticate + * @param key the secret key to use for authentication + * @return the authenticator of the message + */ + public static Bytes authenticate(Bytes message, Key key) { + return Bytes.wrap(authenticate(message.toArrayUnsafe(), key)); + } + + /** + * Authenticates a message using a secret into a HMAC-SHA-256 authenticator. + * + * @param message the message to authenticate + * @param key the secret key to use for authentication + * @return the authenticator of the message + */ + public static byte[] authenticate(byte[] message, Key key) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + ; + long authBytes = Sodium.crypto_auth_hmacsha256_bytes(); + if (authBytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_auth_hmacsha256_bytes: " + authBytes + " is too large"); + } + byte[] out = new byte[(int) authBytes]; + int rc = Sodium.crypto_auth_hmacsha256(out, message, message.length, key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_auth_hmacsha256: failed with result " + rc); + } + return out; + } + + /** + * Verifies the authenticator of a message matches according to a secret. + * + * @param authenticator the authenticator to verify + * @param in the message to match against the authenticator + * @param key the secret key to use for verification + * @return true if the authenticator verifies the message according to the secret, false otherwise + */ + public static boolean verify(Bytes authenticator, Bytes in, Key key) { + return verify(authenticator.toArrayUnsafe(), in.toArrayUnsafe(), key); + } + + /** + * Verifies the authenticator of a message matches according to a secret. + * + * @param authenticator the authenticator to verify + * @param in the message to match against the authenticator + * @param key the secret key to use for verification + * @return true if the authenticator verifies the message according to the secret, false otherwise + */ + public static boolean verify(byte[] authenticator, byte[] in, Key key) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + ; + if (authenticator.length != Sodium.crypto_auth_hmacsha256_bytes()) { + throw new IllegalArgumentException( + "Expected authenticator of " + + Sodium.crypto_auth_hmacsha256_bytes() + + " bytes, got " + + authenticator.length + + " instead"); + } + int rc = + Sodium.crypto_auth_hmacsha256_verify(authenticator, in, in.length, key.value.pointer()); + return rc == 0; + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512.java new file mode 100644 index 000000000..3c0e5304c --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512.java @@ -0,0 +1,194 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; + +/** Message authentication code support for HMAC-SHA-512. */ +public final class HMACSHA512 { + + private HMACSHA512() {} + + /** A HMACSHA512 secret key. */ + public static final class Key implements Destroyable { + final Allocated value; + + Key(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static Key fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static Key fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_auth_hmacsha512_keybytes()) { + throw new IllegalArgumentException( + "key must be " + + Sodium.crypto_auth_hmacsha512_keybytes() + + " bytes, got " + + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Generate a random {@link Key}. + * + * @return A randomly generated secret key. + */ + public static Key random() { + return Sodium.randomBytes(length(), Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_auth_hmacsha512_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_auth_hmacsha512_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Key)) { + return false; + } + Key other = (Key) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** + * Authenticates a message using a secret into a HMAC-SHA-512 authenticator. + * + * @param message the message to authenticate + * @param key the secret key to use for authentication + * @return the authenticator of the message + */ + public static Bytes authenticate(Bytes message, Key key) { + return Bytes.wrap(authenticate(message.toArrayUnsafe(), key)); + } + + /** + * Authenticates a message using a secret into a HMAC-SHA-512 authenticator. + * + * @param message the message to authenticate + * @param key the secret key to use for authentication + * @return the authenticator of the message + */ + public static byte[] authenticate(byte[] message, Key key) { + if (key.isDestroyed()) { + throw new IllegalStateException("SecretKey has been destroyed"); + } + long authBytes = Sodium.crypto_auth_hmacsha512_bytes(); + if (authBytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_auth_hmacsha512_bytes: " + authBytes + " is too large"); + } + byte[] out = new byte[(int) authBytes]; + int rc = Sodium.crypto_auth_hmacsha512(out, message, message.length, key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_auth_hmacsha512: failed with result " + rc); + } + return out; + } + + /** + * Verifies the authenticator of a message matches according to a secret. + * + * @param authenticator the authenticator to verify + * @param in the message to match against the authenticator + * @param key the secret key to use for verification + * @return true if the authenticator verifies the message according to the secret, false otherwise + */ + public static boolean verify(Bytes authenticator, Bytes in, Key key) { + return verify(authenticator.toArrayUnsafe(), in.toArrayUnsafe(), key); + } + + /** + * Verifies the authenticator of a message matches according to a secret. + * + * @param authenticator the authenticator to verify + * @param in the message to match against the authenticator + * @param key the secret key to use for verification + * @return true if the authenticator verifies the message according to the secret, false otherwise + */ + public static boolean verify(byte[] authenticator, byte[] in, Key key) { + if (key.isDestroyed()) { + throw new IllegalStateException("key has been destroyed"); + } + if (authenticator.length != Sodium.crypto_auth_hmacsha512_bytes()) { + throw new IllegalArgumentException( + "Expected authenticator of " + + Sodium.crypto_auth_hmacsha512_bytes() + + " bytes, got " + + authenticator.length + + " instead"); + } + int rc = + Sodium.crypto_auth_hmacsha512_verify(authenticator, in, in.length, key.value.pointer()); + return rc == 0; + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256.java new file mode 100644 index 000000000..497ca2a66 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256.java @@ -0,0 +1,195 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; + +/** + * Message authentication code support for HMAC-SHA-512-256. + * + *

HMAC-SHA-512-256 is implemented as HMAC-SHA-512 with the output truncated to 256 bits. This is + * slightly faster than HMAC-SHA-256. Note that this construction is not the same as + * HMAC-SHA-512/256, which is HMAC using the SHA-512/256 function. + */ +public final class HMACSHA512256 { + + private HMACSHA512256() {} + + /** A HMACSHA512256 secret key. */ + public static final class Key implements Destroyable { + final Allocated value; + + Key(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static Key fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static Key fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_auth_hmacsha512256_keybytes()) { + throw new IllegalArgumentException( + "key must be " + + Sodium.crypto_auth_hmacsha512256_keybytes() + + " bytes, got " + + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Generate a random {@link Key}. + * + * @return A randomly generated secret key. + */ + public static Key random() { + return Sodium.randomBytes(length(), Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_auth_hmacsha512256_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException( + "crypto_auth_hmacsha512256_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Key)) { + return false; + } + Key other = (Key) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** + * Authenticates a message using a secret into a HMAC-SHA-512-256 authenticator. + * + * @param message the message to authenticate + * @param key the secret key to use for authentication + * @return the authenticator of the message + */ + public static Bytes authenticate(Bytes message, Key key) { + return Bytes.wrap(authenticate(message.toArrayUnsafe(), key)); + } + + /** + * Authenticates a message using a secret into a HMAC-SHA-512-256 authenticator. + * + * @param message the message to authenticate + * @param key the secret key to use for authentication + * @return the authenticator of the message + */ + public static byte[] authenticate(byte[] message, Key key) { + long authBytes = Sodium.crypto_auth_hmacsha512256_bytes(); + if (authBytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_auth_hmacsha512256_bytes: " + authBytes + " is too large"); + } + byte[] out = new byte[(int) authBytes]; + int rc = Sodium.crypto_auth_hmacsha512256(out, message, message.length, key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_auth_hmacsha512256: failed with result " + rc); + } + return out; + } + + /** + * Verifies the authenticator of a message matches according to a secret. + * + * @param authenticator the authenticator to verify + * @param in the message to match against the authenticator + * @param key the secret key to use for verification + * @return true if the authenticator verifies the message according to the secret, false otherwise + */ + public static boolean verify(Bytes authenticator, Bytes in, Key key) { + return verify(authenticator.toArrayUnsafe(), in.toArrayUnsafe(), key); + } + + /** + * Verifies the authenticator of a message matches according to a secret. + * + * @param authenticator the authenticator to verify + * @param in the message to match against the authenticator + * @param key the secret key to use for verification + * @return true if the authenticator verifies the message according to the secret, false otherwise + */ + public static boolean verify(byte[] authenticator, byte[] in, Key key) { + if (authenticator.length != Sodium.crypto_auth_hmacsha512256_bytes()) { + throw new IllegalArgumentException( + "Expected authenticator of " + + Sodium.crypto_auth_hmacsha512256_bytes() + + " bytes, got " + + authenticator.length + + " instead"); + } + int rc = + Sodium.crypto_auth_hmacsha512256_verify(authenticator, in, in.length, key.value.pointer()); + return rc == 0; + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivation.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivation.java new file mode 100644 index 000000000..e93fafd97 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivation.java @@ -0,0 +1,305 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Arrays; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; + +/** + * Key derivation. + * + *

Multiple secret subkeys can be derived from a single master key. + * + *

Given the master key and a key identifier, a subkey can be deterministically computed. + * However, given a subkey, an attacker cannot compute the master key nor any other subkeys. + */ +public final class KeyDerivation { + + /** + * Check if Sodium and key derivation support is available. + * + *

Key derivation is supported in sodium native library version >= 10.0.12. + * + * @return {@code true} if Sodium and key derivation support is available. + */ + public static boolean isAvailable() { + try { + return Sodium.supportsVersion(Sodium.VERSION_10_0_12); + } catch (UnsatisfiedLinkError e) { + return false; + } + } + + private static void assertAvailable() { + if (!isAvailable()) { + throw new UnsupportedOperationException( + "Sodium key derivation is not available (requires sodium native library version >= 10.0.12)"); + } + } + + /** A KeyDerivation master key. */ + public static final class MasterKey implements Destroyable { + final Allocated value; + + private MasterKey(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link MasterKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static MasterKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link MasterKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + * @throws UnsupportedOperationException If key derivation support is not available. + */ + public static MasterKey fromBytes(byte[] bytes) { + assertAvailable(); + if (bytes.length != Sodium.crypto_kdf_keybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kdf_keybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, MasterKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + * @throws UnsupportedOperationException If key derivation support is not available. + */ + public static int length() { + assertAvailable(); + long keybytes = Sodium.crypto_kdf_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kdf_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + * @throws UnsupportedOperationException If key derivation support is not available. + */ + public static MasterKey random() { + assertAvailable(); + int length = length(); + Pointer ptr = Sodium.malloc(length); + try { + // When support for 10.0.11 is dropped, use this instead + // Sodium.crypto_kdf_keygen(ptr); + Sodium.randombytes_buf(ptr, length); + return new MasterKey(ptr, length); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + /** + * Derive a sub key. + * + * @param length The length of the sub key, which must be between {@link #minSubKeyLength()} and + * {@link #maxSubKeyLength()}. + * @param subkeyId The id for the sub key. + * @param context The context for the sub key, which must be of length {@link #contextLength()}. + * @return The derived sub key. + */ + public Bytes deriveKey(int length, long subkeyId, byte[] context) { + return Bytes.wrap(deriveKeyArray(length, subkeyId, context)); + } + + /** + * Derive a sub key. + * + * @param length The length of the sub key, which must be between {@link #minSubKeyLength()} and + * {@link #maxSubKeyLength()}. + * @param subkeyId The id for the sub key. + * @param context The context for the sub key, which must be of length {@link #contextLength()}. + * @return The derived sub key. + */ + public byte[] deriveKeyArray(int length, long subkeyId, byte[] context) { + if (value.isDestroyed()) { + throw new IllegalStateException("MasterKey has been destroyed"); + } + assertSubKeyLength(length); + assertContextLength(context); + + byte[] subKey = new byte[length]; + int rc = + Sodium.crypto_kdf_derive_from_key( + subKey, subKey.length, subkeyId, context, value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_kdf_derive_from_key: failed with result " + rc); + } + return subKey; + } + + /** + * Derive a sub key. + * + * @param length The length of the subkey. + * @param subkeyId The id for the subkey. + * @param context The context for the sub key, which must be of length ≤ {@link + * #contextLength()}. + * @return The derived sub key. + */ + public Bytes deriveKey(int length, long subkeyId, String context) { + return Bytes.wrap(deriveKeyArray(length, subkeyId, context)); + } + + /** + * Derive a sub key. + * + * @param length The length of the subkey. + * @param subkeyId The id for the subkey. + * @param context The context for the sub key, which must be of length ≤ {@link + * #contextLength()}. + * @return The derived sub key. + */ + public byte[] deriveKeyArray(int length, long subkeyId, String context) { + int contextLen = contextLength(); + byte[] contextBytes = context.getBytes(UTF_8); + if (context.length() > contextLen) { + throw new IllegalArgumentException( + "context must be " + contextLen + " bytes, got " + context.length()); + } + byte[] ctx; + if (contextBytes.length == contextLen) { + ctx = contextBytes; + } else { + ctx = Arrays.copyOf(contextBytes, contextLen); + } + + return deriveKeyArray(length, subkeyId, ctx); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof MasterKey)) { + return false; + } + MasterKey other = (MasterKey) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** + * Provides the required length for the context + * + * @return The required length for the context (8). + */ + public static int contextLength() { + long contextbytes = Sodium.crypto_kdf_contextbytes(); + if (contextbytes > Integer.MAX_VALUE) { + throw new IllegalArgumentException("crypto_kdf_bytes_min: " + contextbytes + " is too large"); + } + return (int) contextbytes; + } + + /** + * Provides the minimum length for a new sub key + * + * @return The minimum length for a new sub key (16). + */ + public static int minSubKeyLength() { + long length = Sodium.crypto_kdf_bytes_min(); + if (length > Integer.MAX_VALUE) { + throw new IllegalArgumentException("crypto_kdf_bytes_min: " + length + " is too large"); + } + return (int) length; + } + + /** + * Provides the maximum length for a new sub key + * + * @return The maximum length for a new sub key (64). + */ + public static int maxSubKeyLength() { + long length = Sodium.crypto_kdf_bytes_max(); + if (length > Integer.MAX_VALUE) { + throw new IllegalArgumentException("crypto_kdf_bytes_max: " + length + " is too large"); + } + return (int) length; + } + + private static void assertContextLength(byte[] context) { + long contextBytes = Sodium.crypto_kdf_contextbytes(); + if (context.length != contextBytes) { + throw new IllegalArgumentException( + "context must be " + contextBytes + " bytes, got " + context.length); + } + } + + private static void assertSubKeyLength(int length) { + long minLength = Sodium.crypto_kdf_bytes_min(); + long maxLength = Sodium.crypto_kdf_bytes_max(); + if (length < minLength || length > maxLength) { + throw new IllegalArgumentException( + "length is out of range [" + minLength + ", " + maxLength + "]"); + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyExchange.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyExchange.java new file mode 100644 index 000000000..7331f32e8 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyExchange.java @@ -0,0 +1,697 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Objects; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; +import org.jetbrains.annotations.Nullable; + +/** + * Key exchange. + * + *

Allows two parties to securely compute a set of shared keys using their peer's public key and + * their own secret key. + */ +public final class KeyExchange { + + /** A KeyExchange public key. */ + public static final class PublicKey { + final Allocated value; + + private PublicKey(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link PublicKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static PublicKey fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_kx_publickeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kx_publickeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, PublicKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_kx_publickeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_publickeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof PublicKey other)) { + return false; + } + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A KeyExchange secret key. */ + public static final class SecretKey implements Destroyable { + final Allocated value; + + private SecretKey(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link SecretKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_kx_secretkeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kx_secretkeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, SecretKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_kx_secretkeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_secretkeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SecretKey other)) { + return false; + } + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A KeyExchange key pair seed. */ + public static final class Seed { + final Allocated value; + + private Seed(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_kx_seedbytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kx_seedbytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Seed::new); + } + + /** + * Obtain the length of the seed in bytes (32). + * + * @return The length of the seed in bytes (32). + */ + public static int length() { + long seedbytes = Sodium.crypto_kx_seedbytes(); + if (seedbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_seedbytes: " + seedbytes + " is too large"); + } + return (int) seedbytes; + } + + /** + * Generate a new {@link Seed} using a random generator. + * + * @return A randomly generated seed. + */ + public static Seed random() { + return Sodium.randomBytes(length(), Seed::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Seed other)) { + return false; + } + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this seed. + * + * @return The bytes of this seed. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this seed. + * + * @return The bytes of this seed. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A KeyExchange key pair. */ + public static final class KeyPair { + + private final PublicKey publicKey; + private final SecretKey secretKey; + + /** + * Create a {@link KeyPair} from pair of keys. + * + * @param publicKey The bytes for the public key. + * @param secretKey The bytes for the secret key. + */ + public KeyPair(PublicKey publicKey, SecretKey secretKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + } + + /** + * Create a {@link KeyPair} from a secret key. + * + * @param secretKey The secret key. + * @return A {@link KeyPair}. + */ + public static KeyPair forSecretKey(SecretKey secretKey) { + if (secretKey.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + return Sodium.scalarMultBase( + secretKey.value.pointer(), + SecretKey.length(), + (ptr, len) -> { + int publicKeyLength = PublicKey.length(); + if (len != publicKeyLength) { + throw new IllegalStateException( + "Public key length " + + publicKeyLength + + " is not same as generated key length " + + len); + } + return new KeyPair(new PublicKey(ptr, publicKeyLength), secretKey); + }); + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key pair. + */ + public static KeyPair random() { + int publicKeyLength = PublicKey.length(); + Pointer publicKey = Sodium.malloc(publicKeyLength); + Pointer secretKey = null; + try { + int secretKeyLength = SecretKey.length(); + secretKey = Sodium.malloc(secretKeyLength); + int rc = Sodium.crypto_kx_keypair(publicKey, secretKey); + if (rc != 0) { + throw new SodiumException("crypto_kx_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey, publicKeyLength); + publicKey = null; + SecretKey sk = new SecretKey(secretKey, secretKeyLength); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * Generate a new key using a seed. + * + * @param seed A seed. + * @return The generated key pair. + */ + public static KeyPair fromSeed(Seed seed) { + int publicKeyLength = PublicKey.length(); + Pointer publicKey = Sodium.malloc(publicKeyLength); + Pointer secretKey = null; + try { + int secretKeyLength = SecretKey.length(); + secretKey = Sodium.malloc(secretKeyLength); + int rc = Sodium.crypto_kx_seed_keypair(publicKey, secretKey, seed.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_kx_seed_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey, publicKeyLength); + publicKey = null; + SecretKey sk = new SecretKey(secretKey, secretKeyLength); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * Provides the public key of the key pair. + * + * @return The public key of the key pair. + */ + public PublicKey publicKey() { + return publicKey; + } + + /** + * Provides the secret key of the key pair. + * + * @return The secret key of the key pair. + */ + public SecretKey secretKey() { + return secretKey; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof KeyPair other)) { + return false; + } + return this.publicKey.equals(other.publicKey) && this.secretKey.equals(other.secretKey); + } + + @Override + public int hashCode() { + return Objects.hash(publicKey, secretKey); + } + } + + /** A KeyExchange session key. */ + public static final class SessionKey implements Destroyable { + @Nullable private Pointer ptr; + private final int length; + + private SessionKey(Pointer ptr, int length) { + this.ptr = ptr; + this.length = length; + } + + @Override + public void destroy() { + if (ptr != null) { + Pointer p = ptr; + ptr = null; + Sodium.sodium_free(p); + } + } + + @Override + public boolean isDestroyed() { + return ptr == null; + } + + /** + * Create a {@link SessionKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static SessionKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link SessionKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static SessionKey fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_kx_sessionkeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_kx_sessionkeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, SessionKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_kx_sessionkeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_sessionkeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SessionKey other)) { + return false; + } + if (this.ptr == null) { + throw new IllegalStateException("SessionKey has been destroyed"); + } + return other.ptr != null && Sodium.sodium_memcmp(this.ptr, other.ptr, length) == 0; + } + + @Override + public int hashCode() { + if (this.ptr == null) { + throw new IllegalStateException("SessionKey has been destroyed"); + } + return Sodium.hashCode(ptr, length); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return Bytes.wrap(bytesArray()); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + if (this.ptr == null) { + throw new IllegalStateException("SessionKey has been destroyed"); + } + return Sodium.reify(ptr, length); + } + } + + /** A KeyExchange session key pair. */ + public static final class SessionKeyPair { + private final SessionKey rxKey; + private final SessionKey txKey; + + /** + * Create a {@link KeyPair} from pair of keys. + * + * @param rxKey The bytes for the secret key. + * @param txKey The bytes for the public key. + */ + public SessionKeyPair(SessionKey rxKey, SessionKey txKey) { + this.rxKey = rxKey; + this.txKey = txKey; + } + + /** + * Provides the session key that will be used to receive data. + * + * @return The session key that will be used to receive data. + */ + public SessionKey rx() { + return rxKey; + } + + /** + * Provides the session key that will be used to send data. + * + * @return The session key that will be used to send data. + */ + public SessionKey tx() { + return txKey; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SessionKeyPair other)) { + return false; + } + return this.rxKey.equals(other.rxKey) && this.txKey.equals(other.txKey); + } + + @Override + public int hashCode() { + return Objects.hash(rxKey, txKey); + } + } + + /** + * Computer a pair of session keys for use by a client. + * + * @param clientKeys The client key pair. + * @param serverKey The server public key. + * @return A pair of session keys. + */ + public static SessionKeyPair client(KeyPair clientKeys, PublicKey serverKey) { + if (clientKeys.secretKey.isDestroyed()) { + throw new IllegalStateException("SecretKey has been destroyed"); + } + long sessionkeybytes = Sodium.crypto_kx_sessionkeybytes(); + if (sessionkeybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_sessionkeybytes: " + sessionkeybytes + " is too large"); + } + Pointer rxPtr = null; + Pointer txPtr = null; + try { + rxPtr = Sodium.malloc(sessionkeybytes); + txPtr = Sodium.malloc(sessionkeybytes); + int rc = + Sodium.crypto_kx_client_session_keys( + rxPtr, + txPtr, + clientKeys.publicKey.value.pointer(), + clientKeys.secretKey.value.pointer(), + serverKey.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_kx_client_session_keys: failed with result " + rc); + } + SessionKey rxKey = new SessionKey(rxPtr, (int) sessionkeybytes); + rxPtr = null; + SessionKey txKey = new SessionKey(txPtr, (int) sessionkeybytes); + txPtr = null; + return new SessionKeyPair(rxKey, txKey); + } catch (Throwable e) { + if (rxPtr != null) { + Sodium.sodium_free(rxPtr); + } + if (txPtr != null) { + Sodium.sodium_free(txPtr); + } + throw e; + } + } + + /** + * Computer a pair of session keys for use by a client. + * + * @param serverKeys The server key pair. + * @param clientKey The client public key. + * @return A pair of session keys. + */ + public static SessionKeyPair server(KeyPair serverKeys, PublicKey clientKey) { + if (serverKeys.secretKey.isDestroyed()) { + throw new IllegalArgumentException("SecretKey has been destroyed"); + } + long sessionkeybytes = Sodium.crypto_kx_sessionkeybytes(); + if (sessionkeybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_kx_sessionkeybytes: " + sessionkeybytes + " is too large"); + } + Pointer rxPtr = null; + Pointer txPtr = null; + try { + rxPtr = Sodium.malloc(sessionkeybytes); + txPtr = Sodium.malloc(sessionkeybytes); + int rc = + Sodium.crypto_kx_server_session_keys( + rxPtr, + txPtr, + serverKeys.publicKey.value.pointer(), + serverKeys.secretKey.value.pointer(), + clientKey.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_kx_client_session_keys: failed with result " + rc); + } + SessionKey rxKey = new SessionKey(rxPtr, (int) sessionkeybytes); + rxPtr = null; + SessionKey txKey = new SessionKey(txPtr, (int) sessionkeybytes); + txPtr = null; + return new SessionKeyPair(rxKey, txKey); + } catch (Throwable e) { + if (rxPtr != null) { + Sodium.sodium_free(rxPtr); + } + if (txPtr != null) { + Sodium.sodium_free(txPtr); + } + throw e; + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/PasswordHash.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/PasswordHash.java new file mode 100644 index 000000000..81abf895f --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/PasswordHash.java @@ -0,0 +1,1061 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import jnr.ffi.Pointer; +import org.jetbrains.annotations.Nullable; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/password_hashing/the_argon2i_function.md + +/** + * The Argon2 memory-hard hashing function. + * + *

Argon2 summarizes the state of the art in the design of memory-hard functions. + * + *

It aims at the highest memory filling rate and effective use of multiple computing units, + * while still providing defense against tradeoff attacks. + * + *

It prevents ASICs from having a significant advantage over software implementations. + * + *

Guidelines for choosing the parameters

+ * + *

Start by determining how much memory the function can use. What will be the highest number of + * threads/processes evaluating the function simultaneously (ideally, no more than 1 per CPU core)? + * How much physical memory is guaranteed to be available? + * + *

Set memlimit to the amount of memory you want to reserve for password hashing. + * + *

Then, set opslimit to 3 and measure the time it takes to hash a password. + * + *

If this it is way too long for your application, reduce memlimit, but keep opslimit set to 3. + * + *

If the function is so fast that you can afford it to be more computationally intensive without + * any usability issues, increase opslimit. + * + *

For online use (e.g. login in on a website), a 1 second computation is likely to be the + * acceptable maximum. + * + *

For interactive use (e.g. a desktop application), a 5 second pause after having entered a + * password is acceptable if the password doesn't need to be entered more than once per session. + * + *

For non-interactive use and infrequent use (e.g. restoring an encrypted backup), an even + * slower computation can be an option. + * + *

This class depends upon the JNR-FFI library being available on the classpath, along with its + * dependencies. See https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle + * dependency 'com.github.jnr:jnr-ffi'. + */ +public final class PasswordHash { + + /** A PasswordHash salt. */ + public static final class Salt { + final Allocated value; + + private Salt(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link Salt} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Salt fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Salt} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Salt fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_pwhash_saltbytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_pwhash_saltbytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Salt::new); + } + + /** + * Obtain the length of the salt in bytes (32). + * + * @return The length of the salt in bytes (32). + */ + public static int length() { + long saltLength = Sodium.crypto_pwhash_saltbytes(); + if (saltLength > Integer.MAX_VALUE) { + throw new SodiumException("crypto_pwhash_saltbytes: " + saltLength + " is too large"); + } + return (int) saltLength; + } + + /** + * Generate a new salt using a random generator. + * + * @return A randomly generated salt. + */ + public static Salt random() { + return Sodium.randomBytes(length(), Salt::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Salt)) { + return false; + } + Salt other = (Salt) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this salt. + * + * @return The bytes of this salt. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this salt. + * + * @return The bytes of this salt. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A PasswordHash algorithm. */ + public static final class Algorithm { + + private static Algorithm ARGON2I13 = new Algorithm("argon2i13", 1, 3, true); + private static Algorithm ARGON2ID13 = + new Algorithm("argon2id13", 2, 1, Sodium.supportsVersion(Sodium.VERSION_10_0_13)); + + private final String name; + private final int id; + private final long minOps; + private final boolean supported; + + private Algorithm(String name, int id, long minOps, boolean supported) { + this.name = name; + this.id = id; + this.minOps = minOps; + this.supported = supported; + } + + /** + * Returns the currently recommended algorithm + * + * @return The currently recommended algorithm. + */ + public static Algorithm recommended() { + return ARGON2ID13.isSupported() ? ARGON2ID13 : ARGON2I13; + } + + /** + * Returns the version 1.3 of the Argon2i algorithm. + * + * @return Version 1.3 of the Argon2i algorithm. + */ + public static Algorithm argon2i13() { + return ARGON2I13; + } + + /** + * Returns the version 1.3 of the Argon2id algorithm. + * + * @return Version 1.3 of the Argon2id algorithm. + */ + public static Algorithm argon2id13() { + return ARGON2ID13; + } + + @Nullable + static Algorithm fromId(int id) { + if (ARGON2ID13.id == id) { + return ARGON2ID13; + } else if (ARGON2I13.id == id) { + return ARGON2I13; + } + return null; + } + + public String name() { + return name; + } + + int id() { + return id; + } + + public boolean isSupported() { + return supported; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Algorithm)) { + return false; + } + Algorithm other = (Algorithm) obj; + return this.id == other.id; + } + + @Override + public int hashCode() { + return Integer.hashCode(id); + } + + @Override + public String toString() { + return name; + } + } + + /** + * Compute a key from a password, using the currently recommended algorithm and limits on + * operations and memory that are suitable for most use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @return The derived key. + */ + public static Bytes hash(String password, int length, Salt salt) { + return Bytes.wrap(hash(password.getBytes(UTF_8), length, salt)); + } + + /** + * Compute a key from a password, using the currently recommended algorithm and limits on + * operations and memory that are suitable for most use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @return The derived key. + */ + public static Bytes hash(Bytes password, int length, Salt salt) { + return Bytes.wrap(hash(password.toArrayUnsafe(), length, salt)); + } + + /** + * Compute a key from a password, using the currently recommended algorithm and limits on + * operations and memory that are suitable for most use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @return The derived key. + */ + public static byte[] hash(byte[] password, int length, Salt salt) { + return hash( + password, length, salt, moderateOpsLimit(), moderateMemLimit(), Algorithm.recommended()); + } + + /** + * Compute a key from a password, using limits on operations and memory that are suitable for most + * use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static Bytes hash(String password, int length, Salt salt, Algorithm algorithm) { + return Bytes.wrap(hash(password.getBytes(UTF_8), length, salt, algorithm)); + } + + /** + * Compute a key from a password, using limits on operations and memory that are suitable for most + * use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static Bytes hash(Bytes password, int length, Salt salt, Algorithm algorithm) { + return Bytes.wrap(hash(password.toArrayUnsafe(), length, salt, algorithm)); + } + + /** + * Compute a key from a password, using limits on operations and memory that are suitable for most + * use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static byte[] hash(byte[] password, int length, Salt salt, Algorithm algorithm) { + return hash(password, length, salt, moderateOpsLimit(), moderateMemLimit(), algorithm); + } + + /** + * Compute a key from a password, using the currently recommended algorithm and limits on + * operations and memory that are suitable for interactive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @return The derived key. + */ + public static Bytes hashInteractive(String password, int length, Salt salt) { + return Bytes.wrap(hash(password.getBytes(UTF_8), length, salt, Algorithm.recommended())); + } + + /** + * Compute a key from a password, using the currently recommended algorithm and limits on + * operations and memory that are suitable for interactive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @return The derived key. + */ + public static Bytes hashInteractive(Bytes password, int length, Salt salt) { + return Bytes.wrap(hash(password.toArrayUnsafe(), length, salt, Algorithm.recommended())); + } + + /** + * Compute a key from a password, using the currently recommended algorithm and limits on + * operations and memory that are suitable for interactive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @return The derived key. + */ + public static byte[] hashInteractive(byte[] password, int length, Salt salt) { + return hash( + password, + length, + salt, + interactiveOpsLimit(), + interactiveMemLimit(), + Algorithm.recommended()); + } + + /** + * Compute a key from a password, using limits on operations and memory that are suitable for + * interactive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static Bytes hashInteractive(String password, int length, Salt salt, Algorithm algorithm) { + return Bytes.wrap(hash(password.getBytes(UTF_8), length, salt, algorithm)); + } + + /** + * Compute a key from a password, using limits on operations and memory that are suitable for + * interactive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static Bytes hashInteractive(Bytes password, int length, Salt salt, Algorithm algorithm) { + return Bytes.wrap(hash(password.toArrayUnsafe(), length, salt, algorithm)); + } + + /** + * Compute a key from a password, using limits on operations and memory that are suitable for + * interactive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static byte[] hashInteractive( + byte[] password, int length, Salt salt, Algorithm algorithm) { + return hash(password, length, salt, interactiveOpsLimit(), interactiveMemLimit(), algorithm); + } + + /** + * Compute a key from a password, using the currently recommended algorithm and limits on + * operations and memory that are suitable for sensitive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @return The derived key. + */ + public static Bytes hashSensitive(String password, int length, Salt salt) { + return Bytes.wrap(hash(password.getBytes(UTF_8), length, salt, Algorithm.recommended())); + } + + /** + * Compute a key from a password, using the currently recommended algorithm and limits on + * operations and memory that are suitable for sensitive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @return The derived key. + */ + public static Bytes hashSensitive(Bytes password, int length, Salt salt) { + return Bytes.wrap(hash(password.toArrayUnsafe(), length, salt, Algorithm.recommended())); + } + + /** + * Compute a key from a password, using the currently recommended algorithm and limits on + * operations and memory that are suitable for sensitive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @return The derived key. + */ + public static byte[] hashSensitive(byte[] password, int length, Salt salt) { + return hash( + password, length, salt, sensitiveOpsLimit(), sensitiveMemLimit(), Algorithm.recommended()); + } + + /** + * Compute a key from a password, using limits on operations and memory that are suitable for + * sensitive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static Bytes hashSensitive(String password, int length, Salt salt, Algorithm algorithm) { + return Bytes.wrap(hash(password.getBytes(UTF_8), length, salt, algorithm)); + } + + /** + * Compute a key from a password, using limits on operations and memory that are suitable for + * sensitive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static Bytes hashSensitive(Bytes password, int length, Salt salt, Algorithm algorithm) { + return Bytes.wrap(hash(password.toArrayUnsafe(), length, salt, algorithm)); + } + + /** + * Compute a key from a password, using limits on operations and memory that are suitable for + * sensitive use-cases. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static byte[] hashSensitive(byte[] password, int length, Salt salt, Algorithm algorithm) { + return hash(password, length, salt, sensitiveOpsLimit(), sensitiveMemLimit(), algorithm); + } + + /** + * Compute a key from a password. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to + * {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link + * #maxMemLimit()}. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static Bytes hash( + String password, int length, Salt salt, long opsLimit, long memLimit, Algorithm algorithm) { + return Bytes.wrap(hash(password.getBytes(UTF_8), length, salt, opsLimit, memLimit, algorithm)); + } + + /** + * Compute a key from a password. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to + * {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link + * #maxMemLimit()}. + * @param algorithm The algorithm to use. + * @return The derived key. + */ + public static Bytes hash( + Bytes password, int length, Salt salt, long opsLimit, long memLimit, Algorithm algorithm) { + return Bytes.wrap(hash(password.toArrayUnsafe(), length, salt, opsLimit, memLimit, algorithm)); + } + + /** + * Compute a key from a password. + * + * @param password The password to hash. + * @param length The key length to generate. + * @param salt A salt. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to + * {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link + * #maxMemLimit()}. + * @param algorithm The algorithm to use. + * @return The derived key. + * @throws IllegalArgumentException If the opsLimit is too low for the specified algorithm. + * @throws UnsupportedOperationException If the specified algorithm is not supported by the + * currently loaded sodium native library. + */ + public static byte[] hash( + byte[] password, int length, Salt salt, long opsLimit, long memLimit, Algorithm algorithm) { + assertHashLength(length); + assertOpsLimit(opsLimit); + assertMemLimit(memLimit); + if (opsLimit < algorithm.minOps) { + throw new IllegalArgumentException( + "opsLimit " + opsLimit + " too low for specified algorithm"); + } + if (!algorithm.isSupported()) { + throw new UnsupportedOperationException( + algorithm.name() + " is not supported by the currently loaded sodium native library"); + } + byte[] out = new byte[length]; + + int rc = + Sodium.crypto_pwhash( + out, + length, + password, + password.length, + salt.value.pointer(), + opsLimit, + memLimit, + algorithm.id); + if (rc != 0) { + throw new SodiumException("crypto_pwhash: failed with result " + rc); + } + return out; + } + + /** + * Returns the minimum hash length + * + * @return The minimum hash length (16). + */ + public static int minHashLength() { + // When support for 10.0.11 is dropped, remove this + if (!Sodium.supportsVersion(Sodium.VERSION_10_0_12)) { + return 16; + } + long len = Sodium.crypto_pwhash_bytes_min(); + if (len > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_pwhash_bytes_min: " + len + " is too large"); + } + return (int) len; + } + + /** + * Returns the maximum hash length + * + * @return The maximum hash length. + */ + public static int maxHashLength() { + // When support for 10.0.11 is dropped, remove this + if (!Sodium.supportsVersion(Sodium.VERSION_10_0_12)) { + return Integer.MAX_VALUE; + } + long len = Sodium.crypto_pwhash_bytes_max(); + if (len > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } + return (int) len; + } + + private static void assertHashLength(int length) { + // When support for 10.0.11 is dropped, remove this + if (!Sodium.supportsVersion(Sodium.VERSION_10_0_12)) { + if (length < 16) { + throw new IllegalArgumentException("length out of range"); + } + return; + } + if (length < Sodium.crypto_pwhash_bytes_min() || length > Sodium.crypto_pwhash_bytes_max()) { + throw new IllegalArgumentException("length out of range"); + } + } + + /** + * Compute a hash from a password, using limits on operations and memory that are suitable for + * most use-cases. + * + *

Equivalent to {@code hash(password, moderateOpsLimit(), moderateMemLimit())}. + * + * @param password The password to hash. + * @return The hash string. + */ + public static String hash(String password) { + return hash(password, moderateOpsLimit(), moderateMemLimit()); + } + + /** + * Compute a hash from a password, using limits on operations and memory that are suitable for + * interactive use-cases. + * + *

Equivalent to {@code hash(password, sensitiveOpsLimit(), sensitiveMemLimit())}. + * + * @param password The password to hash. + * @return The hash string. + */ + public static String hashInteractive(String password) { + return hash(password, interactiveOpsLimit(), interactiveMemLimit()); + } + + /** + * Compute a hash from a password, using limits on operations and memory that are suitable for + * sensitive use-cases. + * + *

Equivalent to {@code hash(password, sensitiveOpsLimit(), sensitiveMemLimit())}. + * + * @param password The password to hash. + * @return The hash string. + */ + public static String hashSensitive(String password) { + return hash(password, sensitiveOpsLimit(), sensitiveMemLimit()); + } + + /** + * Compute a hash from a password. + * + * @param password The password to hash. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to + * {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link + * #maxMemLimit()}. + * @return The hash string. + */ + public static String hash(String password, long opsLimit, long memLimit) { + assertOpsLimit(opsLimit); + assertMemLimit(memLimit); + + byte[] out = new byte[hashStringLength()]; + + byte[] pwBytes = password.getBytes(UTF_8); + int rc = Sodium.crypto_pwhash_str(out, pwBytes, pwBytes.length, opsLimit, memLimit); + if (rc != 0) { + throw new SodiumException("crypto_pwhash_str: failed with result " + rc); + } + + int i = 0; + while (i < out.length && out[i] != 0) { + ++i; + } + return new String(out, 0, i, UTF_8); + } + + /** + * Verify a password against a hash. + * + * @param hash The hash. + * @param password The password to verify. + * @return {@code true} if the password matches the hash. + */ + public static boolean verify(String hash, String password) { + byte[] hashBytes = hash.getBytes(UTF_8); + + int hashLength = hashStringLength(); + if (hashBytes.length >= hashLength) { + return false; + } + + Pointer str = Sodium.malloc(hashLength); + try { + str.put(0, hashBytes, 0, hashBytes.length); + str.putByte(hashBytes.length, (byte) 0); + + byte[] pwBytes = password.getBytes(UTF_8); + return Sodium.crypto_pwhash_str_verify(str, pwBytes, pwBytes.length) == 0; + } finally { + Sodium.sodium_free(str); + } + } + + private static void assertCheckRehashAvailable() { + if (!Sodium.supportsVersion(Sodium.VERSION_10_0_14)) { + throw new UnsupportedOperationException( + "Sodium re-hash checking is not available (requires sodium native library version >= 10.0.14)"); + } + } + + /** + * A hash verification result. + * + *

Note: methods returning this result are only supported when the sodium native library + * version >= 10.0.14 is available. + */ + public enum VerificationResult { + /** The hash verification failed. */ + FAILED, + /** The hash verification passed. */ + PASSED, + /** The hash verification passed, but the hash is out-of-date and should be regenerated. */ + NEEDS_REHASH; + + /** + * Returns true if the verification passed. + * + * @return {@code true} if the verification passed. + */ + public boolean passed() { + return this != FAILED; + } + + /** + * Returns true if the hash should be regenerated. + * + * @return {@code true} if the hash should be regenerated. + */ + public boolean needsRehash() { + return this == NEEDS_REHASH; + } + } + + /** + * Verify a password against a hash and check the hash is suitable for normal use-cases. + * + *

Equivalent to {@code verify(hash, password, moderateOpsLimit(), moderateMemLimit())}. + * + *

Note: only supported when the sodium native library version >= 10.0.14 is available. + * + * @param hash The hash. + * @param password The password to verify. + * @return The result of verification. + */ + public static VerificationResult checkHash(String hash, String password) { + return checkHash(hash, password, moderateOpsLimit(), moderateMemLimit()); + } + + /** + * Verify a password against a hash and check the hash is suitable for interactive use-cases. + * + *

Equivalent to {@code verify(hash, password, interactiveOpsLimit(), interactiveMemLimit())}. + * + *

Note: only supported when the sodium native library version >= 10.0.14 is available. + * + * @param hash The hash. + * @param password The password to verify. + * @return The result of verification. + */ + public static VerificationResult checkHashForInteractive(String hash, String password) { + return checkHash(hash, password, interactiveOpsLimit(), interactiveMemLimit()); + } + + /** + * Verify a password against a hash and check the hash is suitable for sensitive use-cases. + * + *

Equivalent to {@code verify(hash, password, sensitiveOpsLimit(), sensitiveMemLimit())}. + * + *

Note: only supported when the sodium native library version >= 10.0.14 is available. + * + * @param hash The hash. + * @param password The password to verify. + * @return The result of verification. + */ + public static VerificationResult checkHashForSensitive(String hash, String password) { + return checkHash(hash, password, sensitiveOpsLimit(), sensitiveMemLimit()); + } + + /** + * Verify a password against a hash. + * + *

Note: only supported when the sodium native library version >= 10.0.14 is available. + * + * @param hash The hash. + * @param password The password to verify. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to + * {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link + * #maxMemLimit()}. + * @return The result of verification. + */ + public static VerificationResult checkHash( + String hash, String password, long opsLimit, long memLimit) { + assertCheckRehashAvailable(); + assertOpsLimit(opsLimit); + assertMemLimit(memLimit); + + byte[] hashBytes = hash.getBytes(UTF_8); + + int hashLength = hashStringLength(); + if (hashBytes.length >= hashLength) { + return VerificationResult.FAILED; + } + + Pointer str = Sodium.malloc(hashLength); + try { + str.put(0, hashBytes, 0, hashBytes.length); + str.putByte(hashBytes.length, (byte) 0); + + byte[] pwBytes = password.getBytes(UTF_8); + if (Sodium.crypto_pwhash_str_verify(str, pwBytes, pwBytes.length) != 0) { + return VerificationResult.FAILED; + } + + int rc = Sodium.crypto_pwhash_str_needs_rehash(str, opsLimit, memLimit); + if (rc < 0) { + throw new SodiumException("crypto_pwhash_str_needs_rehash: failed with result " + rc); + } + return (rc == 0) ? VerificationResult.PASSED : VerificationResult.NEEDS_REHASH; + } finally { + Sodium.sodium_free(str); + } + } + + /** + * Check if a hash needs to be regenerated using limits on operations and memory that are suitable + * for most use-cases. + * + *

Equivalent to {@code needsRehash(hash, moderateOpsLimit(), moderateMemLimit())}. + * + *

Note: only supported when the sodium native library version >= 10.0.14 is available. + * + * @param hash The hash. + * @return {@code true} if the hash should be regenerated. + */ + public static boolean needsRehash(String hash) { + return needsRehash(hash, moderateOpsLimit(), moderateMemLimit()); + } + + /** + * Check if a hash needs to be regenerated using limits on operations and memory that are suitable + * for interactive use-cases. + * + *

Equivalent to {@code needsRehash(hash, interactiveOpsLimit(), interactiveMemLimit())}. + * + *

Note: only supported when the sodium native library version >= 10.0.14 is available. + * + * @param hash The hash. + * @return {@code true} if the hash should be regenerated. + */ + public static boolean needsRehashForInteractive(String hash) { + return needsRehash(hash, interactiveOpsLimit(), interactiveMemLimit()); + } + + /** + * Check if a hash needs to be regenerated using limits on operations and memory that are suitable + * for sensitive use-cases. + * + *

Equivalent to {@code needsRehash(hash, sensitiveOpsLimit(), sensitiveMemLimit())}. + * + *

Note: only supported when the sodium native library version >= 10.0.14 is available. + * + * @param hash The hash. + * @return {@code true} if the hash should be regenerated. + */ + public static boolean needsRehashForSensitive(String hash) { + return needsRehash(hash, sensitiveOpsLimit(), sensitiveMemLimit()); + } + + /** + * Check if a hash needs to be regenerated. + * + *

Check if a hash matches the parameters opslimit and memlimit, and the current default + * algorithm. + * + *

Note: only supported when the sodium native library version >= 10.0.14 is available. + * + * @param hash The hash. + * @param opsLimit The operations limit, which must be in the range {@link #minOpsLimit()} to + * {@link #maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link #minMemLimit()} to {@link + * #maxMemLimit()}. + * @return {@code true} if the hash should be regenerated. + */ + public static boolean needsRehash(String hash, long opsLimit, long memLimit) { + assertCheckRehashAvailable(); + assertOpsLimit(opsLimit); + assertMemLimit(memLimit); + + byte[] hashBytes = hash.getBytes(UTF_8); + + int hashLength = hashStringLength(); + if (hashBytes.length >= hashLength) { + throw new IllegalArgumentException("hash is too long"); + } + + Pointer str = Sodium.malloc(hashLength); + try { + str.put(0, hashBytes, 0, hashBytes.length); + str.putByte(hashBytes.length, (byte) 0); + + int rc = Sodium.crypto_pwhash_str_needs_rehash(str, opsLimit, memLimit); + if (rc < 0) { + throw new SodiumException("crypto_pwhash_str_needs_rehash: failed with result " + rc); + } + return (rc != 0); + } finally { + Sodium.sodium_free(str); + } + } + + private static int hashStringLength() { + long hashLength = Sodium.crypto_pwhash_strbytes(); + if (hashLength > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_pwhash_strbytes: " + hashLength + " is too large"); + } + return (int) hashLength; + } + + /** + * Returns the minimum operations limit + * + * @return The minimum operations limit (1). + */ + public static long minOpsLimit() { + // When support for 10.0.11 is dropped, remove this + if (!Sodium.supportsVersion(Sodium.VERSION_10_0_12)) { + return 3; + } + return Sodium.crypto_pwhash_opslimit_min(); + } + + /** + * Returns an operations limit for interactive use-cases + * + * @return An operations limit suitable for interactive use-cases (2). + */ + public static long interactiveOpsLimit() { + return Sodium.crypto_pwhash_opslimit_interactive(); + } + + /** + * Returns an operations limit for most use-cases + * + * @return An operations limit suitable for most use-cases (3). + */ + public static long moderateOpsLimit() { + return Sodium.crypto_pwhash_opslimit_moderate(); + } + + /** + * Returns an operations limit for sensitive use-cases (4). + * + * @return An operations limit for sensitive use-cases (4). + */ + public static long sensitiveOpsLimit() { + return Sodium.crypto_pwhash_opslimit_sensitive(); + } + + /** + * Returns the maximum operations limit. + * + * @return The maximum operations limit (4294967295). + */ + public static long maxOpsLimit() { + // When support for 10.0.11 is dropped, remove this + if (!Sodium.supportsVersion(Sodium.VERSION_10_0_12)) { + return 4294967295L; + } + return Sodium.crypto_pwhash_opslimit_max(); + } + + private static void assertOpsLimit(long opsLimit) { + if (opsLimit < minOpsLimit() || opsLimit > maxOpsLimit()) { + throw new IllegalArgumentException("opsLimit out of range"); + } + } + + /** + * Returns the minimum memory limit. + * + * @return The minimum memory limit (8192). + */ + public static long minMemLimit() { + // When support for 10.0.11 is dropped, remove this + if (!Sodium.supportsVersion(Sodium.VERSION_10_0_12)) { + return 8192; + } + return Sodium.crypto_pwhash_memlimit_min(); + } + + /** + * Returns a memory limit for interactive use-cases. + * + * @return A memory limit suitable for interactive use-cases (67108864). + */ + public static long interactiveMemLimit() { + return Sodium.crypto_pwhash_memlimit_interactive(); + } + + /** + * Returns a memory limit for most use-cases + * + * @return A memory limit suitable for most use-cases (268435456). + */ + public static long moderateMemLimit() { + return Sodium.crypto_pwhash_memlimit_moderate(); + } + + /** + * Returns a memory limit for sensitive use-cases + * + * @return A memory limit suitable for sensitive use-cases (1073741824). + */ + public static long sensitiveMemLimit() { + return Sodium.crypto_pwhash_memlimit_sensitive(); + } + + /** + * Returns the max memory limit. + * + * @return The maximum memory limit (4398046510080). + */ + public static long maxMemLimit() { + // When support for 10.0.11 is dropped, remove this + if (!Sodium.supportsVersion(Sodium.VERSION_10_0_12)) { + return 4398046510080L; + } + return Sodium.crypto_pwhash_memlimit_max(); + } + + private static void assertMemLimit(long memLimit) { + if (memLimit < minMemLimit() || memLimit > maxMemLimit()) { + throw new IllegalArgumentException("memLimit out of range"); + } + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA256Hash.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA256Hash.java new file mode 100644 index 000000000..5aaf3c648 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA256Hash.java @@ -0,0 +1,232 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Objects; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; + +/** + * SHA-256 hashing. + * + *

The SHA-256 and SHA-512 functions are provided for interoperability with other applications. + * If you are looking for a generic hash function and not specifically SHA-2, using + * crypto_generichash() (BLAKE2b) might be a better choice. + * + *

These functions are also not suitable for hashing passwords or deriving keys from passwords. + * Use one of the password hashing APIs instead. + * + *

These functions are not keyed and are thus deterministic. In addition, the untruncated + * versions are vulnerable to length extension attacks. + * + *

+ * + * @see SHA-2 + */ +public class SHA256Hash { + + /** Input of a SHA-256 hash function */ + public static final class Input implements Destroyable { + /** + * Create a hash input from a Diffie-Helman secret + * + * @param secret a Diffie-Helman secret + * @return a hash input + */ + public static SHA256Hash.Input fromSecret(DiffieHelman.Secret secret) { + return new SHA256Hash.Input( + Sodium.dup(secret.value.pointer(), DiffieHelman.Secret.length()), + DiffieHelman.Secret.length()); + } + + /** + * Create a {@link SHA256Hash.Input} from a pointer. + * + * @param allocated the allocated pointer + * @return An input. + */ + public static SHA256Hash.Input fromPointer(Allocated allocated) { + return new SHA256Hash.Input( + Sodium.dup(allocated.pointer(), allocated.length()), allocated.length()); + } + + /** + * Create a {@link SHA256Hash.Input} from a hash. + * + * @param hash the hash + * @return An input. + */ + public static SHA256Hash.Input fromHash(SHA256Hash.Hash hash) { + return new SHA256Hash.Input( + Sodium.dup(hash.value.pointer(), hash.value.length()), hash.value.length()); + } + + /** + * Create a {@link SHA256Hash.Input} from an array of bytes. + * + * @param bytes The bytes for the input. + * @return An input. + */ + public static SHA256Hash.Input fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link SHA256Hash.Input} from an array of bytes. + * + * @param bytes The bytes for the input. + * @return An input. + */ + public static SHA256Hash.Input fromBytes(byte[] bytes) { + return Sodium.dup(bytes, SHA256Hash.Input::new); + } + + private final Allocated value; + + private Input(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Provides the length of the input + * + * @return the length of the input + */ + public int length() { + return value.length(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SHA256Hash.Input)) { + return false; + } + SHA256Hash.Input other = (SHA256Hash.Input) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** SHA-256 hash output */ + public static final class Hash implements Destroyable { + Allocated value; + + Hash(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SHA256Hash.Hash)) { + return false; + } + SHA256Hash.Hash other = (SHA256Hash.Hash) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Obtain the bytes of this hash. + * + *

WARNING: This will cause the hash to be copied into heap memory. + * + * @return The bytes of this hash. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this hash. + * + *

WARNING: This will cause the hash to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this hash. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + + /** + * Obtain the length of the hash in bytes (32). + * + * @return The length of the hash in bytes (32). + */ + public static int length() { + long hashbytes = Sodium.crypto_hash_sha256_bytes(); + if (hashbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_hash_sha256_bytes: " + hashbytes + " is too large"); + } + return (int) hashbytes; + } + } + + /** + * Hashes input to a SHA-256 hash + * + * @param input the input of the hash function + * @return a SHA-256 hash of the input + */ + public static SHA256Hash.Hash hash(SHA256Hash.Input input) { + Pointer output = Sodium.malloc(SHA256Hash.Hash.length()); + Sodium.crypto_hash_sha256(output, input.value.pointer(), input.length()); + return new SHA256Hash.Hash(output, SHA256Hash.Hash.length()); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA512Hash.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA512Hash.java new file mode 100644 index 000000000..0aebc69f8 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA512Hash.java @@ -0,0 +1,232 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Objects; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; + +/** + * SHA-512 hashing. + * + *

The SHA-256 and SHA-512 functions are provided for interoperability with other applications. + * If you are looking for a generic hash function and not specifically SHA-2, using + * crypto_generichash() (BLAKE2b) might be a better choice. + * + *

These functions are also not suitable for hashing passwords or deriving keys from passwords. + * Use one of the password hashing APIs instead. + * + *

These functions are not keyed and are thus deterministic. In addition, the untruncated + * versions are vulnerable to length extension attacks. + * + *

+ * + * @see SHA-2 + */ +public class SHA512Hash { + + /** Input of a SHA-512 hash function */ + public static final class Input implements Destroyable { + /** + * Create a hash input from a Diffie-Helman secret + * + * @param secret a Diffie-Helman secret + * @return a hash input + */ + public static SHA512Hash.Input fromSecret(DiffieHelman.Secret secret) { + return new SHA512Hash.Input( + Sodium.dup(secret.value.pointer(), DiffieHelman.Secret.length()), + DiffieHelman.Secret.length()); + } + + /** + * Create a {@link SHA512Hash.Input} from a pointer. + * + * @param allocated the allocated pointer + * @return An input. + */ + public static SHA512Hash.Input fromPointer(Allocated allocated) { + return new SHA512Hash.Input( + Sodium.dup(allocated.pointer(), allocated.length()), allocated.length()); + } + + /** + * Create a {@link SHA512Hash.Input} from a hash. + * + * @param hash the hash + * @return An input. + */ + public static SHA512Hash.Input fromHash(SHA512Hash.Hash hash) { + return new SHA512Hash.Input( + Sodium.dup(hash.value.pointer(), hash.value.length()), hash.value.length()); + } + + /** + * Create a {@link SHA512Hash.Input} from an array of bytes. + * + * @param bytes The bytes for the input. + * @return An input. + */ + public static SHA512Hash.Input fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link SHA512Hash.Input} from an array of bytes. + * + * @param bytes The bytes for the input. + * @return An input. + */ + public static SHA512Hash.Input fromBytes(byte[] bytes) { + return Sodium.dup(bytes, SHA512Hash.Input::new); + } + + private final Allocated value; + + private Input(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Provides the length of the input + * + * @return the length of the input + */ + public int length() { + return value.length(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SHA512Hash.Input)) { + return false; + } + SHA512Hash.Input other = (SHA512Hash.Input) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** SHA-512 hash output */ + public static final class Hash implements Destroyable { + Allocated value; + + Hash(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SHA512Hash.Hash)) { + return false; + } + SHA512Hash.Hash other = (SHA512Hash.Hash) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Obtain the bytes of this hash. + * + *

WARNING: This will cause the hash to be copied into heap memory. + * + * @return The bytes of this hash. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this hash. + * + *

WARNING: This will cause the hash to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this hash. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + + /** + * Obtain the length of the hash in bytes (32). + * + * @return The length of the hash in bytes (32). + */ + public static int length() { + long hashbytes = Sodium.crypto_hash_sha512_bytes(); + if (hashbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_hash_sha512_bytes: " + hashbytes + " is too large"); + } + return (int) hashbytes; + } + } + + /** + * Hashes input to a SHA-512 hash + * + * @param input the input of the hash function + * @return a SHA-512 hash of the input + */ + public static SHA512Hash.Hash hash(SHA512Hash.Input input) { + Pointer output = Sodium.malloc(SHA512Hash.Hash.length()); + Sodium.crypto_hash_sha512(output, input.value.pointer(), input.length()); + return new SHA512Hash.Hash(output, SHA512Hash.Hash.length()); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretBox.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretBox.java new file mode 100644 index 000000000..decd5d3c5 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretBox.java @@ -0,0 +1,1841 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Arrays; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; +import org.jetbrains.annotations.Nullable; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/secret-key_cryptography/authenticated_encryption.md + +/** + * Secret-key authenticated encryption. + * + *

Encrypts a message with a key and a nonce to keep it confidential, and computes an + * authentication tag. The tag is used to make sure that the message hasn't been tampered with + * before decrypting it. + * + *

A single key is used both to encrypt/sign and verify/decrypt messages. For this reason, it is + * critical to keep the key confidential. + * + *

The nonce doesn't have to be confidential, but it should never ever be reused with the same + * key. The easiest way to generate a nonce is to use randombytes_buf(). + * + *

Messages encrypted are assumed to be independent. If multiple messages are sent using this API + * and random nonces, there will be no way to detect if a message has been received twice, or if + * messages have been reordered. + * + *

This class depends upon the JNR-FFI library being available on the classpath, along with its + * dependencies. See https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle + * dependency 'com.github.jnr:jnr-ffi'. + */ +public final class SecretBox { + private SecretBox() {} + + /** A SecretBox key. */ + public static final class Key implements Destroyable { + final Allocated value; + + private Key(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + private Key(Allocated value) { + this.value = value; + } + + public static Key fromHash(GenericHash.Hash hash) { + return new Key(hash.value); + } + + public static Key fromHash(SHA256Hash.Hash hash) { + return new Key(hash.value); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_secretbox_keybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_secretbox_keybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_secretbox_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_secretbox_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + */ + public static Key random() { + int length = length(); + Pointer ptr = Sodium.malloc(length); + try { + // When support for 10.0.11 is dropped, use this instead + // Sodium.crypto_secretbox_keygen(ptr); + Sodium.randombytes_buf(ptr, length); + return new Key(ptr, length); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Key)) { + return false; + } + Key other = (Key) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A SecretBox nonce. */ + public static final class Nonce implements Destroyable { + final Allocated value; + + private Nonce(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_secretbox_noncebytes()) { + throw new IllegalArgumentException( + "nonce must be " + + Sodium.crypto_secretbox_noncebytes() + + " bytes, got " + + bytes.length); + } + return Sodium.dup(bytes, Nonce::new); + } + + /** + * Obtain the length of the nonce in bytes (24). + * + * @return The length of the nonce in bytes (24). + */ + public static int length() { + long noncebytes = Sodium.crypto_secretbox_noncebytes(); + if (noncebytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_secretbox_noncebytes: " + noncebytes + " is too large"); + } + return (int) noncebytes; + } + + /** + * Create a zero {@link Nonce}. + * + * @return A zero nonce. + */ + public static Nonce zero() { + int length = length(); + Pointer ptr = Sodium.malloc(length); + try { + Sodium.sodium_memzero(ptr, length); + return new Nonce(ptr, length); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + @Override + public void destroy() { + this.value.destroy(); + } + + /** + * Generate a random {@link Nonce}. + * + * @return A randomly generated nonce. + */ + public static Nonce random() { + return Sodium.randomBytes(length(), Nonce::new); + } + + /** + * Increment this nonce. + * + *

Note that this is not synchronized. If multiple threads are creating encrypted messages + * and incrementing this nonce, then external synchronization is required to ensure no two + * encrypt operations use the same nonce. + * + * @return A new {@link Nonce}. + */ + public Nonce increment() { + return Sodium.dupAndIncrement(value.pointer(), length(), Nonce::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Nonce)) { + return false; + } + Nonce other = (Nonce) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this nonce. + * + * @return The bytes of this nonce. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this nonce. + * + * @return The bytes of this nonce. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** + * Encrypt a message with a key. + * + * @param message The message to encrypt. + * @param key The key to use for encryption. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message with a key. + * + * @param message The message to encrypt. + * @param key The key to use for encryption. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Allocated encrypt(Allocated message, Key key, Nonce nonce) { + int macbytes = macLength(); + Allocated cipherText = Allocated.allocate(macbytes + message.length()); + int rc = + Sodium.crypto_secretbox_easy( + cipherText.pointer(), + message.pointer(), + message.length(), + nonce.value.pointer(), + key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_secretbox_easy: failed with result " + rc); + } + + return cipherText; + } + + /** + * Encrypt a message with a key. + * + * @param message The message to encrypt. + * @param key The key to use for encryption. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, Key key, Nonce nonce) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + int macbytes = macLength(); + + byte[] cipherText = new byte[macbytes + message.length]; + int rc = + Sodium.crypto_secretbox_easy( + cipherText, message, message.length, nonce.value.pointer(), key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_secretbox_easy: failed with result " + rc); + } + + return cipherText; + } + + /** + * Encrypt a message with a key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to use for encryption. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(Bytes message, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message with a key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to use for encryption. + * @param nonce A unique nonce. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(byte[] message, Key key, Nonce nonce) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + int macbytes = macLength(); + + byte[] cipherText = new byte[message.length]; + byte[] mac = new byte[macbytes]; + int rc = + Sodium.crypto_secretbox_detached( + cipherText, mac, message, message.length, nonce.value.pointer(), key.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_secretbox_detached: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult(cipherText, mac); + } + + /** + * Decrypt a message using a key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Allocated decrypt(Allocated cipherText, Key key, Nonce nonce) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + int macLength = macLength(); + if (macLength > cipherText.length()) { + throw new IllegalArgumentException("cipherText is too short"); + } + + Allocated clearText = Allocated.allocate(cipherText.length() - macLength); + int rc = + Sodium.crypto_secretbox_open_easy( + clearText.pointer(), + cipherText.pointer(), + cipherText.length(), + nonce.value.pointer(), + key.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_secretbox_open_easy: failed with result " + rc); + } + return clearText; + } + + /** + * Decrypt a message using a key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, Key key, Nonce nonce) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + int macLength = macLength(); + if (macLength > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + + byte[] clearText = new byte[cipherText.length - macLength]; + int rc = + Sodium.crypto_secretbox_open_easy( + clearText, cipherText, cipherText.length, nonce.value.pointer(), key.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_secretbox_open_easy: failed with result " + rc); + } + return clearText; + } + + /** + * Decrypt a message using a key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, Key key, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptDetached(byte[] cipherText, byte[] mac, Key key, Nonce nonce) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + int macLength = macLength(); + if (macLength != mac.length) { + throw new IllegalArgumentException("mac must be " + macLength + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = + Sodium.crypto_secretbox_open_detached( + clearText, + cipherText, + mac, + cipherText.length, + nonce.value.pointer(), + key.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_secretbox_open_detached: failed with result " + rc); + } + return clearText; + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for most + * use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, String password) { + return encrypt( + message, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for most + * use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, String password) { + return encrypt( + message, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for most use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, String password, PasswordHash.Algorithm algorithm) { + return encrypt( + message, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for most use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, String password, PasswordHash.Algorithm algorithm) { + return encrypt( + message, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for + * interactive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data. + */ + public static Bytes encryptInteractive(Bytes message, String password) { + return encrypt( + message, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for + * interactive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data. + */ + public static byte[] encryptInteractive(byte[] message, String password) { + return encrypt( + message, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for interactive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data. + */ + public static Bytes encryptInteractive( + Bytes message, String password, PasswordHash.Algorithm algorithm) { + return encrypt( + message, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for interactive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data. + */ + public static byte[] encryptInteractive( + byte[] message, String password, PasswordHash.Algorithm algorithm) { + return encrypt( + message, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for + * sensitive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data. + */ + public static Bytes encryptSensitive(Bytes message, String password) { + return encrypt( + message, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for + * sensitive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data. + */ + public static byte[] encryptSensitive(byte[] message, String password) { + return encrypt( + message, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for sensitive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data. + */ + public static Bytes encryptSensitive( + Bytes message, String password, PasswordHash.Algorithm algorithm) { + return encrypt( + message, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for sensitive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data. + */ + public static byte[] encryptSensitive( + byte[] message, String password, PasswordHash.Algorithm algorithm) { + return encrypt( + message, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation. + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param opsLimit The operations limit, which must be in the range {@link + * PasswordHash#minOpsLimit()} to {@link PasswordHash#maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link PasswordHash#minMemLimit()} + * to {@link PasswordHash#maxMemLimit()}. + * @param algorithm The algorithm to use. + * @return The encrypted data. + */ + public static Bytes encrypt( + Bytes message, + String password, + long opsLimit, + long memLimit, + PasswordHash.Algorithm algorithm) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), password, opsLimit, memLimit, algorithm)); + } + + /** + * Encrypt a message with a password, using {@link PasswordHash} for the key generation. + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param opsLimit The operations limit, which must be in the range {@link + * PasswordHash#minOpsLimit()} to {@link PasswordHash#maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link PasswordHash#minMemLimit()} + * to {@link PasswordHash#maxMemLimit()}. + * @param algorithm The algorithm to use. + * @return The encrypted data. + * @throws UnsupportedOperationException If the specified algorithm is not supported by the + * currently loaded sodium native library. + */ + public static byte[] encrypt( + byte[] message, + String password, + long opsLimit, + long memLimit, + PasswordHash.Algorithm algorithm) { + requireNonNull(message); + requireNonNull(password); + if (!algorithm.isSupported()) { + throw new UnsupportedOperationException( + algorithm.name() + " is not supported by the currently loaded sodium native library"); + } + + int macLength = macLength(); + + byte[] cipherText = new byte[macLength + message.length]; + Nonce nonce = Nonce.random(); + Key key = deriveKeyFromPassword(password, nonce, opsLimit, memLimit, algorithm); + assert !key.isDestroyed(); + + int rc; + try { + rc = + Sodium.crypto_secretbox_easy( + cipherText, message, message.length, nonce.value.pointer(), key.value.pointer()); + } finally { + key.destroy(); + } + if (rc != 0) { + throw new SodiumException("crypto_secretbox_easy: failed with result " + rc); + } + return prependNonce(nonce, cipherText); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with the currently recommended algorithm and + * limits on operations and memory that are suitable for most use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(Bytes message, String password) { + return encryptDetached( + message, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with the currently recommended algorithm and + * limits on operations and memory that are suitable for most use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached(byte[] message, String password) { + return encryptDetached( + message, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with limits on operations and memory that are + * suitable for most use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached( + Bytes message, String password, PasswordHash.Algorithm algorithm) { + return encryptDetached( + message, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with limits on operations and memory that are + * suitable for most use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached( + byte[] message, String password, PasswordHash.Algorithm algorithm) { + return encryptDetached( + message, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with the currently recommended algorithm and + * limits on operations and memory that are suitable for interactive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptInteractiveDetached( + Bytes message, String password) { + return encryptDetached( + message, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with the currently recommended algorithm and + * limits on operations and memory that are suitable for interactive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptInteractiveDetached( + byte[] message, String password) { + return encryptDetached( + message, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with limits on operations and memory that are + * suitable for interactive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptInteractiveDetached( + Bytes message, String password, PasswordHash.Algorithm algorithm) { + return encryptDetached( + message, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with limits on operations and memory that are + * suitable for interactive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptInteractiveDetached( + byte[] message, String password, PasswordHash.Algorithm algorithm) { + return encryptDetached( + message, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with the currently recommended algorithm and + * limits on operations and memory that are suitable for sensitive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptSensitiveDetached(Bytes message, String password) { + return encryptDetached( + message, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with the currently recommended algorithm and + * limits on operations and memory that are suitable for sensitive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptSensitiveDetached(byte[] message, String password) { + return encryptDetached( + message, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with limits on operations and memory that are + * suitable for sensitive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptSensitiveDetached( + Bytes message, String password, PasswordHash.Algorithm algorithm) { + return encryptDetached( + message, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation (with limits on operations and memory that are + * suitable for sensitive use-cases). + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param algorithm The algorithm to use. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptSensitiveDetached( + byte[] message, String password, PasswordHash.Algorithm algorithm) { + return encryptDetached( + message, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + algorithm); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param opsLimit The operations limit, which must be in the range {@link + * PasswordHash#minOpsLimit()} to {@link PasswordHash#maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link PasswordHash#minMemLimit()} + * to {@link PasswordHash#maxMemLimit()}. + * @param algorithm The algorithm to use. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached( + Bytes message, + String password, + long opsLimit, + long memLimit, + PasswordHash.Algorithm algorithm) { + return encryptDetached(message.toArrayUnsafe(), password, opsLimit, memLimit, algorithm); + } + + /** + * Encrypt a message with a password, generating a detached message authentication code, using + * {@link PasswordHash} for the key generation. + * + * @param message The message to encrypt. + * @param password The password to use for encryption. + * @param opsLimit The operations limit, which must be in the range {@link + * PasswordHash#minOpsLimit()} to {@link PasswordHash#maxOpsLimit()}. + * @param memLimit The memory limit, which must be in the range {@link PasswordHash#minMemLimit()} + * to {@link PasswordHash#maxMemLimit()}. + * @param algorithm The algorithm to use. + * @return The encrypted data and message authentication code. + */ + public static DetachedEncryptionResult encryptDetached( + byte[] message, + String password, + long opsLimit, + long memLimit, + PasswordHash.Algorithm algorithm) { + requireNonNull(message); + requireNonNull(password); + if (!algorithm.isSupported()) { + throw new UnsupportedOperationException( + algorithm.name() + " is not supported by the currently loaded sodium native library"); + } + int macLength = macLength(); + + byte[] cipherText = new byte[message.length]; + byte[] mac = new byte[macLength]; + Nonce nonce = Nonce.random(); + Key key = deriveKeyFromPassword(password, nonce, opsLimit, memLimit, algorithm); + assert !key.isDestroyed(); + + int rc; + try { + rc = + Sodium.crypto_secretbox_detached( + cipherText, mac, message, message.length, nonce.value.pointer(), key.value.pointer()); + } finally { + key.destroy(); + } + if (rc != 0) { + throw new SodiumException("crypto_secretbox_detached: failed with result " + rc); + } + return new DefaultDetachedEncryptionResult(cipherText, prependNonce(nonce, mac)); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for most + * use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, String password) { + return decrypt( + cipherText, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for most + * use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, String password) { + return decrypt( + cipherText, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for most use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, String password, PasswordHash.Algorithm algorithm) { + return decrypt( + cipherText, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for most use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decrypt( + byte[] cipherText, String password, PasswordHash.Algorithm algorithm) { + return decrypt( + cipherText, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for + * interactive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptInteractive(Bytes cipherText, String password) { + return decrypt( + cipherText, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for + * interactive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptInteractive(byte[] cipherText, String password) { + return decrypt( + cipherText, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for interactive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptInteractive( + Bytes cipherText, String password, PasswordHash.Algorithm algorithm) { + return decrypt( + cipherText, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for interactive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptInteractive( + byte[] cipherText, String password, PasswordHash.Algorithm algorithm) { + return decrypt( + cipherText, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for + * sensitive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptSensitive(Bytes cipherText, String password) { + return decrypt( + cipherText, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with the + * currently recommended algorithm and limits on operations and memory that are suitable for + * sensitive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptSensitive(byte[] cipherText, String password) { + return decrypt( + cipherText, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for sensitive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptSensitive( + Bytes cipherText, String password, PasswordHash.Algorithm algorithm) { + return decrypt( + cipherText, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation (with + * limits on operations and memory that are suitable for sensitive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptSensitive( + byte[] cipherText, String password, PasswordHash.Algorithm algorithm) { + return decrypt( + cipherText, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation. + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @param opsLimit The opsLimit that was used for encryption. + * @param memLimit The memLimit that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decrypt( + Bytes cipherText, + String password, + long opsLimit, + long memLimit, + PasswordHash.Algorithm algorithm) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), password, opsLimit, memLimit, algorithm); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a password, using {@link PasswordHash} for the key generation. + * + * @param cipherText The cipher text to decrypt. + * @param password The password that was used for encryption. + * @param opsLimit The opsLimit that was used for encryption. + * @param memLimit The memLimit that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + * @throws UnsupportedOperationException If the specified algorithm is not supported by the + * currently loaded sodium native library. + */ + @Nullable + public static byte[] decrypt( + byte[] cipherText, + String password, + long opsLimit, + long memLimit, + PasswordHash.Algorithm algorithm) { + requireNonNull(cipherText); + requireNonNull(password); + if (!algorithm.isSupported()) { + throw new UnsupportedOperationException( + algorithm.name() + " is not supported by the currently loaded sodium native library"); + } + + int noncebytes = Nonce.length(); + int macLength = macLength(); + if ((noncebytes + macLength) > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + + byte[] clearText = new byte[cipherText.length - noncebytes - macLength]; + Nonce nonce = Nonce.fromBytes(Arrays.copyOf(cipherText, noncebytes)); + Key key = deriveKeyFromPassword(password, nonce, opsLimit, memLimit, algorithm); + assert !key.isDestroyed(); + + int rc; + try { + rc = + Sodium.crypto_secretbox_open_easy( + clearText, + Arrays.copyOfRange(cipherText, noncebytes, cipherText.length), + cipherText.length - noncebytes, + nonce.value.pointer(), + key.value.pointer()); + } finally { + key.destroy(); + } + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_secretbox_open_easy: failed with result " + rc); + } + return clearText; + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with the currently recommended algorithm and limits on + * operations and memory that are suitable for most use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, String password) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with the currently recommended algorithm and limits on + * operations and memory that are suitable for most use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptDetached(byte[] cipherText, byte[] mac, String password) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with limits on operations and memory that are suitable + * for most use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptDetached( + Bytes cipherText, Bytes mac, String password, PasswordHash.Algorithm algorithm) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with limits on operations and memory that are suitable + * for most use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptDetached( + byte[] cipherText, byte[] mac, String password, PasswordHash.Algorithm algorithm) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.moderateOpsLimit(), + PasswordHash.moderateMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with the currently recommended algorithm and limits on + * operations and memory that are suitable for interactive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptInteractiveDetached(Bytes cipherText, Bytes mac, String password) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with the currently recommended algorithm and limits on + * operations and memory that are suitable for interactive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptInteractiveDetached(byte[] cipherText, byte[] mac, String password) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with limits on operations and memory that are suitable + * for interactive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptInteractiveDetached( + Bytes cipherText, Bytes mac, String password, PasswordHash.Algorithm algorithm) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with limits on operations and memory that are suitable + * for interactive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptInteractiveDetached( + byte[] cipherText, byte[] mac, String password, PasswordHash.Algorithm algorithm) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with the currently recommended algorithm and limits on + * operations and memory that are suitable for sensitive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptSensitiveDetached(Bytes cipherText, Bytes mac, String password) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with the currently recommended algorithm and limits on + * operations and memory that are suitable for sensitive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptSensitiveDetached(byte[] cipherText, byte[] mac, String password) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + PasswordHash.Algorithm.recommended()); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with limits on operations and memory that are suitable + * for sensitive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptSensitiveDetached( + Bytes cipherText, Bytes mac, String password, PasswordHash.Algorithm algorithm) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation (with limits on operations and memory that are suitable + * for sensitive use-cases). + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptSensitiveDetached( + byte[] cipherText, byte[] mac, String password, PasswordHash.Algorithm algorithm) { + return decryptDetached( + cipherText, + mac, + password, + PasswordHash.sensitiveOpsLimit(), + PasswordHash.sensitiveMemLimit(), + algorithm); + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @param opsLimit The opsLimit that was used for encryption. + * @param memLimit The memLimit that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptDetached( + Bytes cipherText, + Bytes mac, + String password, + long opsLimit, + long memLimit, + PasswordHash.Algorithm algorithm) { + byte[] bytes = + decryptDetached( + cipherText.toArrayUnsafe(), + mac.toArrayUnsafe(), + password, + opsLimit, + memLimit, + algorithm); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a password and a detached message authentication code, using {@link + * PasswordHash} for the key generation. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param password The password that was used for encryption. + * @param opsLimit The opsLimit that was used for encryption. + * @param memLimit The memLimit that was used for encryption. + * @param algorithm The algorithm that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptDetached( + byte[] cipherText, + byte[] mac, + String password, + long opsLimit, + long memLimit, + PasswordHash.Algorithm algorithm) { + requireNonNull(cipherText); + requireNonNull(mac); + requireNonNull(password); + if (!algorithm.isSupported()) { + throw new UnsupportedOperationException( + algorithm.name() + " is not supported by the currently loaded sodium native library"); + } + + int noncebytes = Nonce.length(); + int macLength = macLength(); + if ((noncebytes + macLength) != mac.length) { + throw new IllegalArgumentException( + "mac must be " + (noncebytes + macLength) + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + Nonce nonce = Nonce.fromBytes(Arrays.copyOf(mac, noncebytes)); + Key key = deriveKeyFromPassword(password, nonce, opsLimit, memLimit, algorithm); + assert !key.isDestroyed(); + + int rc; + try { + rc = + Sodium.crypto_secretbox_open_detached( + clearText, + cipherText, + Arrays.copyOfRange(mac, noncebytes, mac.length), + cipherText.length, + nonce.value.pointer(), + key.value.pointer()); + } finally { + key.destroy(); + } + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException("crypto_secretbox_open_detached: failed with result " + rc); + } + + return clearText; + } + + private static int macLength() { + long macbytes = Sodium.crypto_secretbox_macbytes(); + if (macbytes > Integer.MAX_VALUE) { + throw new IllegalStateException("crypto_secretbox_macbytes: " + macbytes + " is too large"); + } + return (int) macbytes; + } + + private static Key deriveKeyFromPassword( + String password, + Nonce nonce, + long opsLimit, + long memLimit, + PasswordHash.Algorithm algorithm) { + assert Nonce.length() >= PasswordHash.Salt.length() + : "SecretBox.Nonce has insufficient length for deriving a PasswordHash.Salt (" + + Nonce.length() + + " < " + + PasswordHash.Salt.length() + + ")"; + PasswordHash.Salt salt = + PasswordHash.Salt.fromBytes( + Arrays.copyOfRange(nonce.bytesArray(), 0, PasswordHash.Salt.length())); + byte[] passwordBytes = password.getBytes(UTF_8); + try { + byte[] keyBytes = + PasswordHash.hash(passwordBytes, Key.length(), salt, opsLimit, memLimit, algorithm); + try { + return Key.fromBytes(keyBytes); + } finally { + Arrays.fill(keyBytes, (byte) 0); + } + } finally { + Arrays.fill(passwordBytes, (byte) 0); + } + } + + private static byte[] prependNonce(Nonce nonce, byte[] bytes) { + int nonceLength = Nonce.length(); + byte[] data = new byte[nonceLength + bytes.length]; + nonce.value.pointer().get(0, data, 0, nonceLength); + System.arraycopy(bytes, 0, data, nonceLength, bytes.length); + return data; + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStream.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStream.java new file mode 100644 index 000000000..a46584f31 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStream.java @@ -0,0 +1,36 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.v2.bytes.Bytes; + +import javax.security.auth.Destroyable; + +/** Used to decrypt a sequence of messages, or a single message split into arbitrary chunks. */ +public interface SecretDecryptionStream extends Destroyable { + + /** + * Pull a message from this secret stream. + * + * @param cipherText The encrypted message. + * @return The clear text. + */ + default Bytes pull(Bytes cipherText) { + return Bytes.wrap(pull(cipherText.toArrayUnsafe())); + } + + /** + * Pull a message from this secret stream. + * + * @param cipherText The encrypted message. + * @return The clear text. + */ + byte[] pull(byte[] cipherText); + + /** + * Returns true if the stream is complete + * + * @return {@code true} if no more messages should be decrypted by this stream + */ + boolean isComplete(); +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretEncryptionStream.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretEncryptionStream.java new file mode 100644 index 000000000..80d86864a --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretEncryptionStream.java @@ -0,0 +1,87 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.v2.bytes.Bytes; + +import javax.security.auth.Destroyable; + +/** Used to encrypt a sequence of messages, or a single message split into arbitrary chunks. */ +public interface SecretEncryptionStream extends Destroyable { + + /** + * Returns the header for the stream + * + * @return The header for the stream. + */ + default Bytes header() { + return Bytes.wrap(headerArray()); + } + + /** + * Returns the header for the stream + * + * @return The header for the stream. + */ + byte[] headerArray(); + + /** + * Push a message to this secret stream. + * + * @param clearText The message to encrypt. + * @return The encrypted message. + */ + default Bytes push(Bytes clearText) { + return push(clearText, false); + } + + /** + * Push a message to this secret stream. + * + * @param clearText The message to encrypt. + * @return The encrypted message. + */ + default byte[] push(byte[] clearText) { + return push(clearText, false); + } + + /** + * Push the final message to this secret stream. + * + * @param clearText The message to encrypt. + * @return The encrypted message. + */ + default Bytes pushLast(Bytes clearText) { + return push(clearText, true); + } + + /** + * Push the final message to this secret stream. + * + * @param clearText The message to encrypt. + * @return The encrypted message. + */ + default byte[] pushLast(byte[] clearText) { + return push(clearText, true); + } + + /** + * Push a message to this secret stream. + * + * @param clearText The message to encrypt. + * @param isFinal {@code true} if this is the final message that will be sent on this stream. + * @return The encrypted message. + */ + default Bytes push(Bytes clearText, boolean isFinal) { + return Bytes.wrap(push(clearText.toArrayUnsafe(), isFinal)); + } + + /** + * Push a message to this secret stream. + * + * @param clearText The message to encrypt. + * @param isFinal {@code true} if this is the final message that will be sent on this stream. + * @return The encrypted message. + */ + byte[] push(byte[] clearText, boolean isFinal); +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Signature.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Signature.java new file mode 100644 index 000000000..2dde5f451 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Signature.java @@ -0,0 +1,683 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/public-key_cryptography/public-key_signatures.md + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Arrays; +import java.util.Objects; +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; +import jnr.ffi.byref.LongLongByReference; + +/** + * Public-key signatures. + * + *

In this system, a signer generates a key pair: + * + *

    + *
  • a secret key, that will be used to append a signature to any number of messages + *
  • a public key, that anybody can use to verify that the signature appended to a message was + * actually issued by the creator of the public key. + *
+ * + *

Verifiers need to already know and ultimately trust a public key before messages signed using + * it can be verified. + * + *

Warning: this is different from authenticated encryption. Appending a signature does not + * change the representation of the message itself. + * + *

This class depends upon the JNR-FFI library being available on the classpath, along with its + * dependencies. See https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle + * dependency 'com.github.jnr:jnr-ffi'. + */ +public final class Signature { + + /** A signing public key. */ + public static final class PublicKey implements Destroyable { + final Allocated value; + + private PublicKey(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link Signature.PublicKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static Signature.PublicKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Signature.PublicKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the public key. + * @return A public key. + */ + public static Signature.PublicKey fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_sign_publickeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_sign_publickeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, PublicKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_sign_publickeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_sign_publickeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Verifies the signature of a message. + * + * @param message the message itself + * @param signature the signature of the message + * @return true if the signature matches the message according to this public key + */ + public boolean verify(Bytes message, Bytes signature) { + return Signature.verifyDetached(message, signature, this); + } + + /** + * Verifies the signature of a message. + * + * @param message the message itself + * @param signature the signature of the message + * @return true if the signature matches the message according to this public key + */ + public boolean verify(Allocated message, Allocated signature) { + return Signature.verifyDetached(message, signature, this); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof PublicKey)) { + return false; + } + PublicKey other = (PublicKey) obj; + return Objects.equals(this.value, other.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this key. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + } + + /** A Signature secret key. */ + public static final class SecretKey implements Destroyable { + Allocated value; + + private SecretKey(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link Signature.SecretKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Signature.SecretKey} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the secret key. + * @return A secret key. + */ + public static SecretKey fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_sign_secretkeybytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_sign_secretkeybytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, SecretKey::new); + } + + public static SecretKey fromSeed(Seed seed) { + return Sodium.dup(seed.bytes().mutableCopy().toArray(), SecretKey::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + */ + public static int length() { + long keybytes = Sodium.crypto_sign_secretkeybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_sign_secretkeybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SecretKey)) { + return false; + } + + SecretKey other = (SecretKey) obj; + return other.value.equals(this.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A Signature key pair seed. */ + public static final class Seed { + private final Allocated value; + + private Seed(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Seed} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the seed. + * @return A seed. + */ + public static Seed fromBytes(byte[] bytes) { + if (bytes.length != Sodium.crypto_sign_seedbytes()) { + throw new IllegalArgumentException( + "key must be " + Sodium.crypto_sign_seedbytes() + " bytes, got " + bytes.length); + } + return Sodium.dup(bytes, Seed::new); + } + + /** + * Obtain the length of the seed in bytes (32). + * + * @return The length of the seed in bytes (32). + */ + public static int length() { + long seedbytes = Sodium.crypto_sign_seedbytes(); + if (seedbytes > Integer.MAX_VALUE) { + throw new SodiumException("crypto_sign_seedbytes: " + seedbytes + " is too large"); + } + return (int) seedbytes; + } + + /** + * Generate a new {@link Seed} using a random generator. + * + * @return A randomly generated seed. + */ + public static Seed random() { + return Sodium.randomBytes(length(), Seed::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Seed)) { + return false; + } + Seed other = (Seed) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + /** + * Provides the bytes of this seed. + * + * @return The bytes of this seed. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this seed. + * + * @return The bytes of this seed. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A Signature key pair. */ + public static final class KeyPair { + + private final PublicKey publicKey; + private final SecretKey secretKey; + + /** + * Create a {@link KeyPair} from pair of keys. + * + * @param publicKey The bytes for the public key. + * @param secretKey The bytes for the secret key. + */ + public KeyPair(PublicKey publicKey, SecretKey secretKey) { + this.publicKey = publicKey; + this.secretKey = secretKey; + } + + /** + * Create a {@link KeyPair} from an array of secret key bytes. + * + * @param secretKey The secret key. + * @return A {@link KeyPair}. + */ + public static KeyPair forSecretKey(SecretKey secretKey) { + if (secretKey.value.isDestroyed()) { + throw new IllegalStateException("SecretKey has been destroyed"); + } + int publicKeyLength = PublicKey.length(); + Pointer publicKey = Sodium.malloc(publicKeyLength); + try { + int rc = Sodium.crypto_sign_ed25519_sk_to_pk(publicKey, secretKey.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_sign_ed25519_sk_to_pk: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey, publicKeyLength); + publicKey = null; + return new KeyPair(pk, secretKey); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + throw e; + } + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key pair. + */ + public static KeyPair random() { + int publicKeyLength = PublicKey.length(); + Pointer publicKey = Sodium.malloc(publicKeyLength); + Pointer secretKey = null; + try { + int secretKeyLength = SecretKey.length(); + secretKey = Sodium.malloc(secretKeyLength); + int rc = Sodium.crypto_sign_keypair(publicKey, secretKey); + if (rc != 0) { + throw new SodiumException("crypto_sign_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey, publicKeyLength); + publicKey = null; + SecretKey sk = new SecretKey(secretKey, secretKeyLength); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * Generate a new key using a seed. + * + * @param seed A seed. + * @return The generated key pair. + */ + public static KeyPair fromSeed(Seed seed) { + int publicKeyLength = PublicKey.length(); + Pointer publicKey = Sodium.malloc(publicKeyLength); + Pointer secretKey = null; + try { + int secretKeyLength = SecretKey.length(); + secretKey = Sodium.malloc(secretKeyLength); + int rc = Sodium.crypto_sign_seed_keypair(publicKey, secretKey, seed.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_sign_seed_keypair: failed with result " + rc); + } + PublicKey pk = new PublicKey(publicKey, publicKeyLength); + publicKey = null; + SecretKey sk = new SecretKey(secretKey, secretKeyLength); + secretKey = null; + return new KeyPair(pk, sk); + } catch (Throwable e) { + if (publicKey != null) { + Sodium.sodium_free(publicKey); + } + if (secretKey != null) { + Sodium.sodium_free(secretKey); + } + throw e; + } + } + + /** + * Provides the public key. + * + * @return The public key of the key pair. + */ + public PublicKey publicKey() { + return publicKey; + } + + /** + * Provides the secret key. + * + * @return The secret key of the key pair. + */ + public SecretKey secretKey() { + return secretKey; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof KeyPair)) { + return false; + } + KeyPair other = (KeyPair) obj; + return this.publicKey.equals(other.publicKey) && this.secretKey.equals(other.secretKey); + } + + @Override + public int hashCode() { + return Objects.hash(publicKey, secretKey); + } + } + + private Signature() {} + + /** + * Signs a message for a given key. + * + * @param message The message to sign. + * @param secretKey The secret key to sign the message with. + * @return The signature of the message. + */ + public static Bytes signDetached(Bytes message, SecretKey secretKey) { + return Bytes.wrap(signDetached(message.toArrayUnsafe(), secretKey)); + } + + /** + * Signs a message for a given key. + * + * @param message The message to sign. + * @param secretKey The secret key to sign the message with. + * @return The signature of the message. + */ + public static Allocated signDetached(Allocated message, SecretKey secretKey) { + if (secretKey.value.isDestroyed()) { + throw new IllegalStateException("SecretKey has been destroyed"); + } + Allocated signature = Allocated.allocate(Sodium.crypto_sign_bytes()); + int rc = + Sodium.crypto_sign_detached( + signature.pointer(), + new LongLongByReference(Sodium.crypto_sign_bytes()), + message.pointer(), + (long) message.length(), + secretKey.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_sign_detached: failed with result " + rc); + } + + return signature; + } + + /** + * Signs a message for a given key. + * + * @param message The message to sign. + * @param secretKey The secret key to sign the message with. + * @return The signature of the message. + */ + public static byte[] signDetached(byte[] message, SecretKey secretKey) { + if (secretKey.value.isDestroyed()) { + throw new IllegalStateException("SecretKey has been destroyed"); + } + byte[] signature = new byte[(int) Sodium.crypto_sign_bytes()]; + int rc = + Sodium.crypto_sign_detached( + signature, null, message, message.length, secretKey.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_sign_detached: failed with result " + rc); + } + + return signature; + } + + /** + * Decrypt a message using a given key. + * + * @param message The cipher text to decrypt. + * @param signature The public key of the sender. + * @param publicKey The secret key of the receiver. + * @return whether the signature matches the message according to the public key. + */ + public static boolean verifyDetached(Bytes message, Bytes signature, PublicKey publicKey) { + return verifyDetached(message.toArrayUnsafe(), signature.toArrayUnsafe(), publicKey); + } + + /** + * Decrypt a message using a given key. + * + * @param message The cipher text to decrypt. + * @param signature The public key of the sender. + * @param publicKey The secret key of the receiver. + * @return whether the signature matches the message according to the public key. + */ + public static boolean verifyDetached( + Allocated message, Allocated signature, PublicKey publicKey) { + int rc = + Sodium.crypto_sign_verify_detached( + signature.pointer(), message.pointer(), message.length(), publicKey.value.pointer()); + if (rc == -1) { + return false; + } + if (rc != 0) { + throw new SodiumException("crypto_sign_verify_detached: failed with result " + rc); + } + + return true; + } + + /** + * Decrypt a message using a given key. + * + * @param message The cipher text to decrypt. + * @param signature The public key of the sender. + * @param publicKey The secret key of the receiver. + * @return whether the signature matches the message according to the public key. + */ + public static boolean verifyDetached(byte[] message, byte[] signature, PublicKey publicKey) { + int rc = + Sodium.crypto_sign_verify_detached( + signature, message, message.length, publicKey.value.pointer()); + if (rc == -1) { + return false; + } + if (rc != 0) { + throw new SodiumException("crypto_sign_verify_detached: failed with result " + rc); + } + + return true; + } + + /** + * Signs a message for a given key. + * + * @param message The message to sign. + * @param secretKey The secret key to sign the message with. + * @return The signature prepended to the message + */ + public static Bytes sign(Bytes message, SecretKey secretKey) { + return Bytes.wrap(sign(message.toArrayUnsafe(), secretKey)); + } + + /** + * Signs a message for a given key. + * + * @param message The message to sign. + * @param secretKey The secret key to sign the message with. + * @return The signature prepended to the message + */ + public static byte[] sign(byte[] message, SecretKey secretKey) { + if (secretKey.value.isDestroyed()) { + throw new IllegalStateException("SecretKey has been destroyed"); + } + byte[] signature = new byte[(int) Sodium.crypto_sign_bytes() + message.length]; + int rc = + Sodium.crypto_sign(signature, null, message, message.length, secretKey.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_sign: failed with result " + rc); + } + + return signature; + } + + /** + * Verifies the signature of the signed message using the public key and returns the message. + * + * @param signed signed message (signature + message) + * @param publicKey pk used to verify the signature + * @return the message + */ + public static Bytes verify(Bytes signed, PublicKey publicKey) { + return Bytes.wrap(verify(signed.toArrayUnsafe(), publicKey)); + } + + /** + * Verifies the signature of the signed message using the public key and returns the message. + * + * @param signed signed message (signature + message) + * @param publicKey pk used to verify the signature + * @return the message + */ + public static byte[] verify(byte[] signed, PublicKey publicKey) { + byte[] message = new byte[signed.length]; + LongLongByReference messageLongReference = new LongLongByReference(); + int rc = + Sodium.crypto_sign_open( + message, messageLongReference, signed, signed.length, publicKey.value.pointer()); + if (rc != 0) { + throw new SodiumException("crypto_sign_open: failed with result " + rc); + } + + return Arrays.copyOfRange(message, 0, messageLongReference.intValue()); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Sodium.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Sodium.java new file mode 100644 index 000000000..52eeb20f3 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Sodium.java @@ -0,0 +1,3194 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.crypto.sodium.LibSodium; +import org.apache.tuweni.crypto.sodium.SodiumException; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.BiFunction; + +import jnr.ffi.LibraryLoader; +import jnr.ffi.Platform; +import jnr.ffi.Pointer; +import jnr.ffi.byref.ByteByReference; +import jnr.ffi.byref.LongLongByReference; +import org.jetbrains.annotations.Nullable; + +/** + * Access to the sodium native library. + * + *

This class provides static methods for checking or loading the sodium native library. + */ +public final class Sodium { + private Sodium() {} + + static final SodiumVersion VERSION_10_0_11 = new SodiumVersion(9, 3, "10.0.11"); + static final SodiumVersion VERSION_10_0_12 = new SodiumVersion(9, 4, "10.0.12"); + static final SodiumVersion VERSION_10_0_13 = new SodiumVersion(9, 5, "10.0.13"); + static final SodiumVersion VERSION_10_0_14 = new SodiumVersion(9, 6, "10.0.14"); + static final SodiumVersion VERSION_10_0_15 = new SodiumVersion(10, 0, "10.0.15"); + static final SodiumVersion VERSION_10_0_16 = new SodiumVersion(10, 1, "10.0.16"); + static final SodiumVersion VERSION_10_0_17 = new SodiumVersion(10, 1, "10.0.17"); + static final SodiumVersion VERSION_10_0_18 = new SodiumVersion(10, 1, "10.0.18"); + + /** + * The minimum version of the sodium native library that this binding supports. + * + * @return The minimum version of the sodium native library that this binding supports. + */ + public static SodiumVersion minSupportedVersion() { + return VERSION_10_0_11; + } + + /** + * The version of the loaded sodium native library. + * + * @return The version of the loaded sodium library. + */ + public static SodiumVersion version() { + return version(libSodium()); + } + + private static SodiumVersion version(LibSodium lib) { + return new SodiumVersion( + lib.sodium_library_version_major(), + lib.sodium_library_version_minor(), + lib.sodium_version_string()); + } + + /** + * Check if the loaded sodium native library is the same or later than the specified version. + * + * @param requiredVersion The version to compare to. + * @return {@code true} if the loaded sodium native library is the same or a later version. + */ + public static boolean supportsVersion(SodiumVersion requiredVersion) { + return supportsVersion(requiredVersion, libSodium()); + } + + private static boolean supportsVersion(SodiumVersion requiredVersion, LibSodium lib) { + return version(lib).compareTo(requiredVersion) >= 0; + } + + private static final String LIBRARY_NAME; + + static { + try { + Class.forName("jnr.ffi.Platform"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException( + "JNR-FFI is not available on the classpath, see https://github.com/jnr/jnr-ffi"); + } + switch (Platform.getNativePlatform().getOS()) { + case WINDOWS: + LIBRARY_NAME = "libsodium"; + break; + default: + LIBRARY_NAME = "sodium"; + break; + } + } + + private static volatile LibSodium libSodium = null; + + /** + * Load and initialize the native libsodium shared library. + * + *

If this method returns successfully (without throwing a {@link LinkageError}), then all + * future calls to methods provided by this class will use the loaded library. + * + * @param path The path to the shared library. + * @throws LinkageError If the library cannot be found, dependent libraries are missing, or cannot + * be initialized. + */ + public static void loadLibrary(Path path) { + requireNonNull(path); + if (!Files.exists(path)) { + throw new IllegalArgumentException("Non-existent path"); + } + + Path dir = path.getParent(); + Path library = path.getFileName(); + + LibSodium lib = + LibraryLoader.create(LibSodium.class) + .search(dir.toFile().getAbsolutePath()) + .load(library.toString()); + initializeLibrary(lib); + + synchronized (Sodium.class) { + Sodium.libSodium = lib; + } + } + + /** + * Search for, then load and initialize the native libsodium shared library. + * + *

The library will be searched for in all the provided locations, using the library name + * {@code "sodium"}. If this method returns successfully (without throwing a {@link + * LinkageError}), then all future calls to methods provided by this class will use the loaded + * library. + * + * @param paths A set of directories to search for the library in. + * @throws LinkageError If the library cannot be found, dependent libraries are missing, or cannot + * be initialized. + */ + public static void searchLibrary(Path... paths) { + searchLibrary(LIBRARY_NAME, paths); + } + + /** + * Search for, then load and initialize the native libsodium shared library. + * + *

The library will be searched for in all the provided locations, using the provided library + * name. If this method returns successfully (without throwing a {@link LinkageError}), then all + * future calls to methods provided by this class will use the loaded library. + * + * @param libraryName The name of the library (e.g. {@code "sodium"}). + * @param paths A set of directories to search for the library in. + * @throws LinkageError If the library cannot be found, dependent libraries are missing, or cannot + * be initialized. + */ + public static void searchLibrary(String libraryName, Path... paths) { + LibraryLoader loader = LibraryLoader.create(LibSodium.class); + for (Path path : paths) { + loader = loader.search(path.toFile().getAbsolutePath()); + } + LibSodium lib = loader.load(libraryName); + initializeLibrary(lib); + + synchronized (Sodium.class) { + Sodium.libSodium = lib; + } + } + + private static LibSodium libSodium() { + if (libSodium == null) { + synchronized (Sodium.class) { + if (libSodium == null) { + LibSodium lib = + LibraryLoader.create(LibSodium.class) + .search("/usr/local/lib") + .search("/opt/local/lib") + .search("/usr/lib") + .search("/lib") + .load(LIBRARY_NAME); + libSodium = initializeLibrary(lib); + } + } + } + return libSodium; + } + + private static LibSodium initializeLibrary(LibSodium lib) { + if (!supportsVersion(minSupportedVersion(), lib)) { + throw new LinkageError( + String.format( + "Unsupported libsodium version %s (%s:%s)", + lib.sodium_version_string(), + lib.sodium_library_version_major(), + lib.sodium_library_version_minor())); + } + int result = lib.sodium_init(); + if (result == -1) { + throw new LinkageError("Failed to initialize libsodium: sodium_init returned " + result); + } + return lib; + } + + /** + * Check if the sodium library is available. + * + *

If the sodium library has not already been loaded, this will attempt to load and initialize + * it before returning. + * + * @return {@code true} if the library is loaded and available. + */ + public static boolean isAvailable() { + try { + libSodium(); + } catch (LinkageError e) { + return false; + } + return true; + } + + static Pointer malloc(long length) { + Pointer ptr = sodium_malloc(length); + if (ptr == null) { + throw new OutOfMemoryError("Sodium.sodium_malloc failed allocating " + length); + } + return ptr; + } + + static Pointer dup(Pointer src, int length) { + Pointer ptr = malloc(length); + try { + ptr.transferFrom(0, src, 0, length); + return ptr; + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static Pointer dupAndIncrement(Pointer src, int length) { + Pointer ptr = dup(src, length); + try { + sodium_increment(ptr, length); + return ptr; + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static T dupAndIncrement(Pointer src, int length, BiFunction ctr) { + Pointer ptr = Sodium.dupAndIncrement(src, length); + try { + return ctr.apply(ptr, length); + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static Pointer dup(byte[] bytes) { + Pointer ptr = malloc(bytes.length); + try { + ptr.put(0, bytes, 0, bytes.length); + return ptr; + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static T dup(byte[] bytes, BiFunction ctr) { + Pointer ptr = Sodium.dup(bytes); + try { + return ctr.apply(ptr, bytes.length); + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static byte[] reify(Pointer ptr, int length) { + byte[] bytes = new byte[length]; + ptr.get(0, bytes, 0, bytes.length); + return bytes; + } + + static Pointer randomBytes(int length) { + Pointer ptr = malloc(length); + try { + randombytes_buf(ptr, length); + return ptr; + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static T randomBytes(int length, BiFunction ctr) { + Pointer ptr = Sodium.randomBytes(length); + try { + return ctr.apply(ptr, length); + } catch (Throwable e) { + sodium_free(ptr); + throw e; + } + } + + static int hashCode(Pointer ptr, int length) { + int result = 1; + for (int i = 0; i < length; ++i) { + result = 31 * result + ((int) ptr.getByte(i)); + } + return result; + } + + static T scalarMultBase(Pointer n, long nlen, BiFunction ctr) { + if (nlen != Sodium.crypto_scalarmult_scalarbytes()) { + throw new IllegalArgumentException( + "secret key length is " + + nlen + + " but required " + + Sodium.crypto_scalarmult_scalarbytes()); + } + long qbytes = Sodium.crypto_scalarmult_bytes(); + Pointer dst = malloc(qbytes); + try { + int rc = Sodium.crypto_scalarmult_base(dst, n); + if (rc != 0) { + throw new SodiumException("crypto_scalarmult_base: failed with result " + rc); + } + return ctr.apply(dst, qbytes); + } catch (Throwable e) { + sodium_free(dst); + throw e; + } + } + + static T scalarMult( + Pointer n, long nlen, Pointer p, long plen, BiFunction ctr) { + if (nlen != Sodium.crypto_scalarmult_scalarbytes()) { + throw new IllegalArgumentException( + "secret key length is " + + nlen + + " but required " + + Sodium.crypto_scalarmult_scalarbytes()); + } + if (plen != Sodium.crypto_scalarmult_bytes()) { + throw new IllegalArgumentException( + "public key length is " + plen + " but required " + Sodium.crypto_scalarmult_bytes()); + } + long qbytes = Sodium.crypto_scalarmult_bytes(); + Pointer dst = malloc(qbytes); + try { + int rc = Sodium.crypto_scalarmult(dst, n, p); + if (rc != 0) { + throw new SodiumException("crypto_scalarmult_base: failed with result " + rc); + } + return ctr.apply(dst, qbytes); + } catch (Throwable e) { + sodium_free(dst); + throw e; + } + } + + ///////// + // Generated with https://gist.github.com/cleishm/39fbad03378f5e1ad82521ad821cd065, then modified + + static String sodium_version_string() { + return libSodium().sodium_version_string(); + } + + static int sodium_library_version_major() { + return libSodium().sodium_library_version_major(); + } + + static int sodium_library_version_minor() { + return libSodium().sodium_library_version_minor(); + } + + static int sodium_library_minimal() { + return libSodium().sodium_library_minimal(); + } + + static int sodium_set_misuse_handler(Pointer handler) { + return libSodium().sodium_set_misuse_handler(handler); + } + + static void sodium_misuse() { + libSodium().sodium_misuse(); + } + + static int crypto_aead_aes256gcm_is_available() { + return libSodium().crypto_aead_aes256gcm_is_available(); + } + + static long crypto_aead_aes256gcm_keybytes() { + return libSodium().crypto_aead_aes256gcm_keybytes(); + } + + static long crypto_aead_aes256gcm_nsecbytes() { + return libSodium().crypto_aead_aes256gcm_nsecbytes(); + } + + static long crypto_aead_aes256gcm_npubbytes() { + return libSodium().crypto_aead_aes256gcm_npubbytes(); + } + + static long crypto_aead_aes256gcm_abytes() { + return libSodium().crypto_aead_aes256gcm_abytes(); + } + + static long crypto_aead_aes256gcm_messagebytes_max() { + return libSodium().crypto_aead_aes256gcm_messagebytes_max(); + } + + static long crypto_aead_aes256gcm_statebytes() { + return libSodium().crypto_aead_aes256gcm_statebytes(); + } + + static int crypto_aead_aes256gcm_encrypt( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable Pointer nsec, + Pointer npub, + Pointer k) { + return libSodium().crypto_aead_aes256gcm_encrypt(c, clen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_aes256gcm_decrypt( + byte[] m, + LongLongByReference mlen_p, + @Nullable Pointer nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + Pointer npub, + Pointer k) { + return libSodium().crypto_aead_aes256gcm_decrypt(m, mlen_p, nsec, c, clen, ad, adlen, npub, k); + } + + static int crypto_aead_aes256gcm_encrypt_detached( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable Pointer nsec, + Pointer npub, + Pointer k) { + return libSodium() + .crypto_aead_aes256gcm_encrypt_detached( + c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_aes256gcm_decrypt_detached( + byte[] m, + @Nullable Pointer nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + Pointer npub, + Pointer k) { + return libSodium() + .crypto_aead_aes256gcm_decrypt_detached(m, nsec, c, clen, mac, ad, adlen, npub, k); + } + + static int crypto_aead_aes256gcm_beforenm(Pointer ctx_, Pointer k) { + return libSodium().crypto_aead_aes256gcm_beforenm(ctx_, k); + } + + static int crypto_aead_aes256gcm_encrypt_afternm( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable Pointer nsec, + Pointer npub, + Pointer ctx_) { + return libSodium() + .crypto_aead_aes256gcm_encrypt_afternm(c, clen_p, m, mlen, ad, adlen, nsec, npub, ctx_); + } + + static int crypto_aead_aes256gcm_decrypt_afternm( + byte[] m, + LongLongByReference mlen_p, + @Nullable Pointer nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + Pointer npub, + Pointer ctx_) { + return libSodium() + .crypto_aead_aes256gcm_decrypt_afternm(m, mlen_p, nsec, c, clen, ad, adlen, npub, ctx_); + } + + static int crypto_aead_aes256gcm_encrypt_detached_afternm( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable Pointer nsec, + Pointer npub, + Pointer ctx_) { + return libSodium() + .crypto_aead_aes256gcm_encrypt_detached_afternm( + c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, ctx_); + } + + static int crypto_aead_aes256gcm_decrypt_detached_afternm( + byte[] m, + @Nullable Pointer nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + Pointer npub, + Pointer ctx_) { + return libSodium() + .crypto_aead_aes256gcm_decrypt_detached_afternm( + m, nsec, c, clen, mac, ad, adlen, npub, ctx_); + } + + static void crypto_aead_aes256gcm_keygen(Pointer k) { + libSodium().crypto_aead_aes256gcm_keygen(k); + } + + static long crypto_aead_chacha20poly1305_ietf_keybytes() { + return libSodium().crypto_aead_chacha20poly1305_ietf_keybytes(); + } + + static long crypto_aead_chacha20poly1305_ietf_nsecbytes() { + return libSodium().crypto_aead_chacha20poly1305_ietf_nsecbytes(); + } + + static long crypto_aead_chacha20poly1305_ietf_npubbytes() { + return libSodium().crypto_aead_chacha20poly1305_ietf_npubbytes(); + } + + static long crypto_aead_chacha20poly1305_ietf_abytes() { + return libSodium().crypto_aead_chacha20poly1305_ietf_abytes(); + } + + static long crypto_aead_chacha20poly1305_ietf_messagebytes_max() { + return libSodium().crypto_aead_chacha20poly1305_ietf_messagebytes_max(); + } + + static int crypto_aead_chacha20poly1305_ietf_encrypt( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + byte[] nsec, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_ietf_encrypt(c, clen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_chacha20poly1305_ietf_decrypt( + byte[] m, + LongLongByReference mlen_p, + byte[] nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_ietf_decrypt(m, mlen_p, nsec, c, clen, ad, adlen, npub, k); + } + + static int crypto_aead_chacha20poly1305_ietf_encrypt_detached( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + byte[] nsec, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_ietf_encrypt_detached( + c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_chacha20poly1305_ietf_decrypt_detached( + byte[] m, + byte[] nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_ietf_decrypt_detached( + m, nsec, c, clen, mac, ad, adlen, npub, k); + } + + static void crypto_aead_chacha20poly1305_ietf_keygen(byte[] k) { + libSodium().crypto_aead_chacha20poly1305_ietf_keygen(k); + } + + static long crypto_aead_chacha20poly1305_keybytes() { + return libSodium().crypto_aead_chacha20poly1305_keybytes(); + } + + static long crypto_aead_chacha20poly1305_nsecbytes() { + return libSodium().crypto_aead_chacha20poly1305_nsecbytes(); + } + + static long crypto_aead_chacha20poly1305_npubbytes() { + return libSodium().crypto_aead_chacha20poly1305_npubbytes(); + } + + static long crypto_aead_chacha20poly1305_abytes() { + return libSodium().crypto_aead_chacha20poly1305_abytes(); + } + + static long crypto_aead_chacha20poly1305_messagebytes_max() { + return libSodium().crypto_aead_chacha20poly1305_messagebytes_max(); + } + + static int crypto_aead_chacha20poly1305_encrypt( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + byte[] nsec, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_encrypt(c, clen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_chacha20poly1305_decrypt( + byte[] m, + LongLongByReference mlen_p, + byte[] nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_decrypt(m, mlen_p, nsec, c, clen, ad, adlen, npub, k); + } + + static int crypto_aead_chacha20poly1305_encrypt_detached( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + byte[] nsec, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_encrypt_detached( + c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_chacha20poly1305_decrypt_detached( + byte[] m, + byte[] nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + byte[] npub, + byte[] k) { + return libSodium() + .crypto_aead_chacha20poly1305_decrypt_detached(m, nsec, c, clen, mac, ad, adlen, npub, k); + } + + static void crypto_aead_chacha20poly1305_keygen(byte[] k) { + libSodium().crypto_aead_chacha20poly1305_keygen(k); + } + + static long crypto_aead_xchacha20poly1305_ietf_keybytes() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_keybytes(); + } + + static long crypto_aead_xchacha20poly1305_ietf_nsecbytes() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_nsecbytes(); + } + + static long crypto_aead_xchacha20poly1305_ietf_npubbytes() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_npubbytes(); + } + + static long crypto_aead_xchacha20poly1305_ietf_abytes() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_abytes(); + } + + static long crypto_aead_xchacha20poly1305_ietf_messagebytes_max() { + return libSodium().crypto_aead_xchacha20poly1305_ietf_messagebytes_max(); + } + + static int crypto_aead_xchacha20poly1305_ietf_encrypt( + byte[] c, + LongLongByReference clen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable byte[] nsec, + Pointer npub, + Pointer k) { + return libSodium() + .crypto_aead_xchacha20poly1305_ietf_encrypt(c, clen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_xchacha20poly1305_ietf_decrypt( + byte[] m, + LongLongByReference mlen_p, + @Nullable byte[] nsec, + byte[] c, + long clen, + byte[] ad, + long adlen, + Pointer npub, + Pointer k) { + return libSodium() + .crypto_aead_xchacha20poly1305_ietf_decrypt(m, mlen_p, nsec, c, clen, ad, adlen, npub, k); + } + + static int crypto_aead_xchacha20poly1305_ietf_encrypt_detached( + byte[] c, + byte[] mac, + LongLongByReference maclen_p, + byte[] m, + long mlen, + byte[] ad, + long adlen, + @Nullable byte[] nsec, + Pointer npub, + Pointer k) { + return libSodium() + .crypto_aead_xchacha20poly1305_ietf_encrypt_detached( + c, mac, maclen_p, m, mlen, ad, adlen, nsec, npub, k); + } + + static int crypto_aead_xchacha20poly1305_ietf_decrypt_detached( + byte[] m, + @Nullable byte[] nsec, + byte[] c, + long clen, + byte[] mac, + byte[] ad, + long adlen, + Pointer npub, + Pointer k) { + return libSodium() + .crypto_aead_xchacha20poly1305_ietf_decrypt_detached( + m, nsec, c, clen, mac, ad, adlen, npub, k); + } + + static void crypto_aead_xchacha20poly1305_ietf_keygen(Pointer k) { + libSodium().crypto_aead_xchacha20poly1305_ietf_keygen(k); + } + + static long crypto_hash_sha512_statebytes() { + return libSodium().crypto_hash_sha512_statebytes(); + } + + static long crypto_hash_sha512_bytes() { + return libSodium().crypto_hash_sha512_bytes(); + } + + static int crypto_hash_sha512(Pointer out, Pointer in, long inlen) { + return libSodium().crypto_hash_sha512(out, in, inlen); + } + + static int crypto_hash_sha512_init(Pointer state) { + return libSodium().crypto_hash_sha512_init(state); + } + + static int crypto_hash_sha512_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_hash_sha512_update(state, in, inlen); + } + + static int crypto_hash_sha512_final(Pointer state, byte[] out) { + return libSodium().crypto_hash_sha512_final(state, out); + } + + static long crypto_auth_hmacsha512_bytes() { + return libSodium().crypto_auth_hmacsha512_bytes(); + } + + static long crypto_auth_hmacsha512_keybytes() { + return libSodium().crypto_auth_hmacsha512_keybytes(); + } + + static int crypto_auth_hmacsha512(byte[] out, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth_hmacsha512(out, in, inlen, k); + } + + static int crypto_auth_hmacsha512_verify(byte[] h, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth_hmacsha512_verify(h, in, inlen, k); + } + + static long crypto_auth_hmacsha512_statebytes() { + return libSodium().crypto_auth_hmacsha512_statebytes(); + } + + static int crypto_auth_hmacsha512_init(Pointer state, byte[] key, long keylen) { + return libSodium().crypto_auth_hmacsha512_init(state, key, keylen); + } + + static int crypto_auth_hmacsha512_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_auth_hmacsha512_update(state, in, inlen); + } + + static int crypto_auth_hmacsha512_final(Pointer state, byte[] out) { + return libSodium().crypto_auth_hmacsha512_final(state, out); + } + + static void crypto_auth_hmacsha512_keygen(byte[] k) { + libSodium().crypto_auth_hmacsha512_keygen(k); + } + + static long crypto_auth_hmacsha512256_bytes() { + return libSodium().crypto_auth_hmacsha512256_bytes(); + } + + static long crypto_auth_hmacsha512256_keybytes() { + return libSodium().crypto_auth_hmacsha512256_keybytes(); + } + + static int crypto_auth_hmacsha512256(byte[] out, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth_hmacsha512256(out, in, inlen, k); + } + + static int crypto_auth_hmacsha512256_verify(byte[] h, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth_hmacsha512256_verify(h, in, inlen, k); + } + + static long crypto_auth_hmacsha512256_statebytes() { + return libSodium().crypto_auth_hmacsha512256_statebytes(); + } + + static int crypto_auth_hmacsha512256_init(Pointer state, byte[] key, long keylen) { + return libSodium().crypto_auth_hmacsha512256_init(state, key, keylen); + } + + static int crypto_auth_hmacsha512256_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_auth_hmacsha512256_update(state, in, inlen); + } + + static int crypto_auth_hmacsha512256_final(Pointer state, byte[] out) { + return libSodium().crypto_auth_hmacsha512256_final(state, out); + } + + static void crypto_auth_hmacsha512256_keygen(byte[] k) { + libSodium().crypto_auth_hmacsha512256_keygen(k); + } + + static long crypto_auth_bytes() { + return libSodium().crypto_auth_bytes(); + } + + static long crypto_auth_keybytes() { + return libSodium().crypto_auth_keybytes(); + } + + static String crypto_auth_primitive() { + return libSodium().crypto_auth_primitive(); + } + + static int crypto_auth(byte[] out, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth(out, in, inlen, k); + } + + static int crypto_auth_verify(byte[] h, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth_verify(h, in, inlen, k); + } + + static void crypto_auth_keygen(Pointer k) { + libSodium().crypto_auth_keygen(k); + } + + static long crypto_hash_sha256_statebytes() { + return libSodium().crypto_hash_sha256_statebytes(); + } + + static long crypto_hash_sha256_bytes() { + return libSodium().crypto_hash_sha256_bytes(); + } + + static int crypto_hash_sha256(byte[] out, byte[] in, long inlen) { + return libSodium().crypto_hash_sha256(out, in, inlen); + } + + static int crypto_hash_sha256(Pointer out, Pointer in, long inlen) { + return libSodium().crypto_hash_sha256(out, in, inlen); + } + + static int crypto_hash_sha256_init(Pointer state) { + return libSodium().crypto_hash_sha256_init(state); + } + + static int crypto_hash_sha256_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_hash_sha256_update(state, in, inlen); + } + + static int crypto_hash_sha256_final(Pointer state, byte[] out) { + return libSodium().crypto_hash_sha256_final(state, out); + } + + static long crypto_auth_hmacsha256_bytes() { + return libSodium().crypto_auth_hmacsha256_bytes(); + } + + static long crypto_auth_hmacsha256_keybytes() { + return libSodium().crypto_auth_hmacsha256_keybytes(); + } + + static int crypto_auth_hmacsha256(byte[] out, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth_hmacsha256(out, in, inlen, k); + } + + static int crypto_auth_hmacsha256_verify(byte[] h, byte[] in, long inlen, Pointer k) { + return libSodium().crypto_auth_hmacsha256_verify(h, in, inlen, k); + } + + static long crypto_auth_hmacsha256_statebytes() { + return libSodium().crypto_auth_hmacsha256_statebytes(); + } + + static int crypto_auth_hmacsha256_init(Pointer state, byte[] key, long keylen) { + return libSodium().crypto_auth_hmacsha256_init(state, key, keylen); + } + + static int crypto_auth_hmacsha256_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_auth_hmacsha256_update(state, in, inlen); + } + + static int crypto_auth_hmacsha256_final(Pointer state, byte[] out) { + return libSodium().crypto_auth_hmacsha256_final(state, out); + } + + static void crypto_auth_hmacsha256_keygen(byte[] k) { + libSodium().crypto_auth_hmacsha256_keygen(k); + } + + static long crypto_stream_xsalsa20_keybytes() { + return libSodium().crypto_stream_xsalsa20_keybytes(); + } + + static long crypto_stream_xsalsa20_noncebytes() { + return libSodium().crypto_stream_xsalsa20_noncebytes(); + } + + static long crypto_stream_xsalsa20_messagebytes_max() { + return libSodium().crypto_stream_xsalsa20_messagebytes_max(); + } + + static int crypto_stream_xsalsa20(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xsalsa20(c, clen, n, k); + } + + static int crypto_stream_xsalsa20_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xsalsa20_xor(c, m, mlen, n, k); + } + + static int crypto_stream_xsalsa20_xor_ic( + byte[] c, byte[] m, long mlen, byte[] n, long ic, byte[] k) { + return libSodium().crypto_stream_xsalsa20_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_xsalsa20_keygen(byte[] k) { + libSodium().crypto_stream_xsalsa20_keygen(k); + } + + static long crypto_box_curve25519xsalsa20poly1305_seedbytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_seedbytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_publickeybytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_publickeybytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_secretkeybytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_secretkeybytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_beforenmbytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_beforenmbytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_noncebytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_noncebytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_macbytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_macbytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_messagebytes_max() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_messagebytes_max(); + } + + static int crypto_box_curve25519xsalsa20poly1305_seed_keypair(byte[] pk, byte[] sk, byte[] seed) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_seed_keypair(pk, sk, seed); + } + + static int crypto_box_curve25519xsalsa20poly1305_keypair(byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_keypair(pk, sk); + } + + static int crypto_box_curve25519xsalsa20poly1305_beforenm(Pointer k, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_beforenm(k, pk, sk); + } + + static long crypto_box_curve25519xsalsa20poly1305_boxzerobytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_boxzerobytes(); + } + + static long crypto_box_curve25519xsalsa20poly1305_zerobytes() { + return libSodium().crypto_box_curve25519xsalsa20poly1305_zerobytes(); + } + + static int crypto_box_curve25519xsalsa20poly1305( + byte[] c, byte[] m, long mlen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xsalsa20poly1305(c, m, mlen, n, pk, sk); + } + + static int crypto_box_curve25519xsalsa20poly1305_open( + byte[] m, byte[] c, long clen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_open(m, c, clen, n, pk, sk); + } + + static int crypto_box_curve25519xsalsa20poly1305_afternm( + byte[] c, byte[] m, long mlen, byte[] n, Pointer k) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_afternm(c, m, mlen, n, k); + } + + static int crypto_box_curve25519xsalsa20poly1305_open_afternm( + byte[] m, byte[] c, long clen, byte[] n, Pointer k) { + return libSodium().crypto_box_curve25519xsalsa20poly1305_open_afternm(m, c, clen, n, k); + } + + static long crypto_box_seedbytes() { + return libSodium().crypto_box_seedbytes(); + } + + static long crypto_box_publickeybytes() { + return libSodium().crypto_box_publickeybytes(); + } + + static long crypto_box_secretkeybytes() { + return libSodium().crypto_box_secretkeybytes(); + } + + static long crypto_box_noncebytes() { + return libSodium().crypto_box_noncebytes(); + } + + static long crypto_box_macbytes() { + return libSodium().crypto_box_macbytes(); + } + + static long crypto_box_messagebytes_max() { + return libSodium().crypto_box_messagebytes_max(); + } + + static String crypto_box_primitive() { + return libSodium().crypto_box_primitive(); + } + + static int crypto_box_seed_keypair(Pointer pk, Pointer sk, Pointer seed) { + return libSodium().crypto_box_seed_keypair(pk, sk, seed); + } + + static int crypto_box_keypair(Pointer pk, Pointer sk) { + return libSodium().crypto_box_keypair(pk, sk); + } + + static int crypto_box_easy(byte[] c, byte[] m, long mlen, Pointer n, Pointer pk, Pointer sk) { + return libSodium().crypto_box_easy(c, m, mlen, n, pk, sk); + } + + static int crypto_box_open_easy( + byte[] m, byte[] c, long clen, Pointer n, Pointer pk, Pointer sk) { + return libSodium().crypto_box_open_easy(m, c, clen, n, pk, sk); + } + + static int crypto_box_detached( + byte[] c, byte[] mac, byte[] m, long mlen, Pointer n, Pointer pk, Pointer sk) { + return libSodium().crypto_box_detached(c, mac, m, mlen, n, pk, sk); + } + + static int crypto_box_open_detached( + byte[] m, byte[] c, byte[] mac, long clen, Pointer n, Pointer pk, Pointer sk) { + return libSodium().crypto_box_open_detached(m, c, mac, clen, n, pk, sk); + } + + static long crypto_box_beforenmbytes() { + return libSodium().crypto_box_beforenmbytes(); + } + + static int crypto_box_beforenm(Pointer k, Pointer pk, Pointer sk) { + return libSodium().crypto_box_beforenm(k, pk, sk); + } + + static int crypto_box_easy_afternm(byte[] c, byte[] m, long mlen, Pointer n, Pointer k) { + return libSodium().crypto_box_easy_afternm(c, m, mlen, n, k); + } + + static int crypto_box_open_easy_afternm(byte[] m, byte[] c, long clen, Pointer n, Pointer k) { + return libSodium().crypto_box_open_easy_afternm(m, c, clen, n, k); + } + + static int crypto_box_detached_afternm( + byte[] c, byte[] mac, byte[] m, long mlen, Pointer n, Pointer k) { + return libSodium().crypto_box_detached_afternm(c, mac, m, mlen, n, k); + } + + static int crypto_box_open_detached_afternm( + byte[] m, byte[] c, byte[] mac, long clen, Pointer n, Pointer k) { + return libSodium().crypto_box_open_detached_afternm(m, c, mac, clen, n, k); + } + + static long crypto_box_sealbytes() { + return libSodium().crypto_box_sealbytes(); + } + + static int crypto_box_seal(byte[] c, byte[] m, long mlen, Pointer pk) { + return libSodium().crypto_box_seal(c, m, mlen, pk); + } + + static int crypto_box_seal_open(byte[] m, byte[] c, long clen, Pointer pk, Pointer sk) { + return libSodium().crypto_box_seal_open(m, c, clen, pk, sk); + } + + static long crypto_box_zerobytes() { + return libSodium().crypto_box_zerobytes(); + } + + static long crypto_box_boxzerobytes() { + return libSodium().crypto_box_boxzerobytes(); + } + + static int crypto_box(byte[] c, byte[] m, long mlen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box(c, m, mlen, n, pk, sk); + } + + static int crypto_box_open(byte[] m, byte[] c, long clen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box_open(m, c, clen, n, pk, sk); + } + + static int crypto_box_afternm(byte[] c, byte[] m, long mlen, byte[] n, Pointer k) { + return libSodium().crypto_box_afternm(c, m, mlen, n, k); + } + + static int crypto_box_open_afternm(byte[] m, byte[] c, long clen, byte[] n, Pointer k) { + return libSodium().crypto_box_open_afternm(m, c, clen, n, k); + } + + static long crypto_core_hsalsa20_outputbytes() { + return libSodium().crypto_core_hsalsa20_outputbytes(); + } + + static long crypto_core_hsalsa20_inputbytes() { + return libSodium().crypto_core_hsalsa20_inputbytes(); + } + + static long crypto_core_hsalsa20_keybytes() { + return libSodium().crypto_core_hsalsa20_keybytes(); + } + + static long crypto_core_hsalsa20_constbytes() { + return libSodium().crypto_core_hsalsa20_constbytes(); + } + + static int crypto_core_hsalsa20(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_hsalsa20(out, in, k, c); + } + + static long crypto_core_hchacha20_outputbytes() { + return libSodium().crypto_core_hchacha20_outputbytes(); + } + + static long crypto_core_hchacha20_inputbytes() { + return libSodium().crypto_core_hchacha20_inputbytes(); + } + + static long crypto_core_hchacha20_keybytes() { + return libSodium().crypto_core_hchacha20_keybytes(); + } + + static long crypto_core_hchacha20_constbytes() { + return libSodium().crypto_core_hchacha20_constbytes(); + } + + static int crypto_core_hchacha20(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_hchacha20(out, in, k, c); + } + + static long crypto_core_salsa20_outputbytes() { + return libSodium().crypto_core_salsa20_outputbytes(); + } + + static long crypto_core_salsa20_inputbytes() { + return libSodium().crypto_core_salsa20_inputbytes(); + } + + static long crypto_core_salsa20_keybytes() { + return libSodium().crypto_core_salsa20_keybytes(); + } + + static long crypto_core_salsa20_constbytes() { + return libSodium().crypto_core_salsa20_constbytes(); + } + + static int crypto_core_salsa20(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_salsa20(out, in, k, c); + } + + static long crypto_core_salsa2012_outputbytes() { + return libSodium().crypto_core_salsa2012_outputbytes(); + } + + static long crypto_core_salsa2012_inputbytes() { + return libSodium().crypto_core_salsa2012_inputbytes(); + } + + static long crypto_core_salsa2012_keybytes() { + return libSodium().crypto_core_salsa2012_keybytes(); + } + + static long crypto_core_salsa2012_constbytes() { + return libSodium().crypto_core_salsa2012_constbytes(); + } + + static int crypto_core_salsa2012(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_salsa2012(out, in, k, c); + } + + static long crypto_core_salsa208_outputbytes() { + return libSodium().crypto_core_salsa208_outputbytes(); + } + + static long crypto_core_salsa208_inputbytes() { + return libSodium().crypto_core_salsa208_inputbytes(); + } + + static long crypto_core_salsa208_keybytes() { + return libSodium().crypto_core_salsa208_keybytes(); + } + + static long crypto_core_salsa208_constbytes() { + return libSodium().crypto_core_salsa208_constbytes(); + } + + static int crypto_core_salsa208(byte[] out, byte[] in, byte[] k, byte[] c) { + return libSodium().crypto_core_salsa208(out, in, k, c); + } + + static long crypto_generichash_blake2b_bytes_min() { + return libSodium().crypto_generichash_blake2b_bytes_min(); + } + + static long crypto_generichash_blake2b_bytes_max() { + return libSodium().crypto_generichash_blake2b_bytes_max(); + } + + static long crypto_generichash_blake2b_bytes() { + return libSodium().crypto_generichash_blake2b_bytes(); + } + + static long crypto_generichash_blake2b_keybytes_min() { + return libSodium().crypto_generichash_blake2b_keybytes_min(); + } + + static long crypto_generichash_blake2b_keybytes_max() { + return libSodium().crypto_generichash_blake2b_keybytes_max(); + } + + static long crypto_generichash_blake2b_keybytes() { + return libSodium().crypto_generichash_blake2b_keybytes(); + } + + static long crypto_generichash_blake2b_saltbytes() { + return libSodium().crypto_generichash_blake2b_saltbytes(); + } + + static long crypto_generichash_blake2b_personalbytes() { + return libSodium().crypto_generichash_blake2b_personalbytes(); + } + + static long crypto_generichash_blake2b_statebytes() { + return libSodium().crypto_generichash_blake2b_statebytes(); + } + + static int crypto_generichash_blake2b( + byte[] out, long outlen, byte[] in, long inlen, byte[] key, long keylen) { + return libSodium().crypto_generichash_blake2b(out, outlen, in, inlen, key, keylen); + } + + static int crypto_generichash_blake2b_salt_personal( + byte[] out, + long outlen, + byte[] in, + long inlen, + byte[] key, + long keylen, + byte[] salt, + byte[] personal) { + return libSodium() + .crypto_generichash_blake2b_salt_personal( + out, outlen, in, inlen, key, keylen, salt, personal); + } + + static int crypto_generichash_blake2b_init(Pointer state, byte[] key, long keylen, long outlen) { + return libSodium().crypto_generichash_blake2b_init(state, key, keylen, outlen); + } + + static int crypto_generichash_blake2b_init_salt_personal( + Pointer state, byte[] key, long keylen, long outlen, byte[] salt, byte[] personal) { + return libSodium() + .crypto_generichash_blake2b_init_salt_personal(state, key, keylen, outlen, salt, personal); + } + + static int crypto_generichash_blake2b_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_generichash_blake2b_update(state, in, inlen); + } + + static int crypto_generichash_blake2b_final(Pointer state, byte[] out, long outlen) { + return libSodium().crypto_generichash_blake2b_final(state, out, outlen); + } + + static void crypto_generichash_blake2b_keygen(byte[] k) { + libSodium().crypto_generichash_blake2b_keygen(k); + } + + static long crypto_generichash_bytes_min() { + return libSodium().crypto_generichash_bytes_min(); + } + + static long crypto_generichash_bytes_max() { + return libSodium().crypto_generichash_bytes_max(); + } + + static long crypto_generichash_bytes() { + return libSodium().crypto_generichash_bytes(); + } + + static long crypto_generichash_keybytes_min() { + return libSodium().crypto_generichash_keybytes_min(); + } + + static long crypto_generichash_keybytes_max() { + return libSodium().crypto_generichash_keybytes_max(); + } + + static long crypto_generichash_keybytes() { + return libSodium().crypto_generichash_keybytes(); + } + + static String crypto_generichash_primitive() { + return libSodium().crypto_generichash_primitive(); + } + + static long crypto_generichash_statebytes() { + return libSodium().crypto_generichash_statebytes(); + } + + static int crypto_generichash( + Pointer out, long outlen, Pointer in, long inlen, Pointer key, long keylen) { + return libSodium().crypto_generichash(out, outlen, in, inlen, key, keylen); + } + + static int crypto_generichash_init(Pointer state, byte[] key, long keylen, long outlen) { + return libSodium().crypto_generichash_init(state, key, keylen, outlen); + } + + static int crypto_generichash_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_generichash_update(state, in, inlen); + } + + static int crypto_generichash_final(Pointer state, byte[] out, long outlen) { + return libSodium().crypto_generichash_final(state, out, outlen); + } + + static void crypto_generichash_keygen(byte[] k) { + libSodium().crypto_generichash_keygen(k); + } + + static long crypto_hash_bytes() { + return libSodium().crypto_hash_bytes(); + } + + static int crypto_hash(byte[] out, byte[] in, long inlen) { + return libSodium().crypto_hash(out, in, inlen); + } + + static String crypto_hash_primitive() { + return libSodium().crypto_hash_primitive(); + } + + static long crypto_kdf_blake2b_bytes_min() { + return libSodium().crypto_kdf_blake2b_bytes_min(); + } + + static long crypto_kdf_blake2b_bytes_max() { + return libSodium().crypto_kdf_blake2b_bytes_max(); + } + + static long crypto_kdf_blake2b_contextbytes() { + return libSodium().crypto_kdf_blake2b_contextbytes(); + } + + static long crypto_kdf_blake2b_keybytes() { + return libSodium().crypto_kdf_blake2b_keybytes(); + } + + static int crypto_kdf_blake2b_derive_from_key( + byte[] subkey, long subkey_len, long subkey_id, byte[] ctx, Pointer key) { + return libSodium().crypto_kdf_blake2b_derive_from_key(subkey, subkey_len, subkey_id, ctx, key); + } + + static long crypto_kdf_bytes_min() { + return libSodium().crypto_kdf_bytes_min(); + } + + static long crypto_kdf_bytes_max() { + return libSodium().crypto_kdf_bytes_max(); + } + + static long crypto_kdf_contextbytes() { + return libSodium().crypto_kdf_contextbytes(); + } + + static long crypto_kdf_keybytes() { + return libSodium().crypto_kdf_keybytes(); + } + + static String crypto_kdf_primitive() { + return libSodium().crypto_kdf_primitive(); + } + + static int crypto_kdf_derive_from_key( + byte[] subkey, long subkey_len, long subkey_id, byte[] ctx, Pointer key) { + return libSodium().crypto_kdf_derive_from_key(subkey, subkey_len, subkey_id, ctx, key); + } + + static void crypto_kdf_keygen(Pointer k) { + libSodium().crypto_kdf_keygen(k); + } + + static long crypto_kx_publickeybytes() { + return libSodium().crypto_kx_publickeybytes(); + } + + static long crypto_kx_secretkeybytes() { + return libSodium().crypto_kx_secretkeybytes(); + } + + static long crypto_kx_seedbytes() { + return libSodium().crypto_kx_seedbytes(); + } + + static long crypto_kx_sessionkeybytes() { + return libSodium().crypto_kx_sessionkeybytes(); + } + + static String crypto_kx_primitive() { + return libSodium().crypto_kx_primitive(); + } + + static int crypto_kx_seed_keypair(Pointer pk, Pointer sk, Pointer seed) { + return libSodium().crypto_kx_seed_keypair(pk, sk, seed); + } + + static int crypto_kx_keypair(Pointer pk, Pointer sk) { + return libSodium().crypto_kx_keypair(pk, sk); + } + + static int crypto_kx_client_session_keys( + Pointer rx, Pointer tx, Pointer client_pk, Pointer client_sk, Pointer server_pk) { + return libSodium().crypto_kx_client_session_keys(rx, tx, client_pk, client_sk, server_pk); + } + + static int crypto_kx_server_session_keys( + Pointer rx, Pointer tx, Pointer server_pk, Pointer server_sk, Pointer client_pk) { + return libSodium().crypto_kx_server_session_keys(rx, tx, server_pk, server_sk, client_pk); + } + + static long crypto_onetimeauth_poly1305_statebytes() { + return libSodium().crypto_onetimeauth_poly1305_statebytes(); + } + + static long crypto_onetimeauth_poly1305_bytes() { + return libSodium().crypto_onetimeauth_poly1305_bytes(); + } + + static long crypto_onetimeauth_poly1305_keybytes() { + return libSodium().crypto_onetimeauth_poly1305_keybytes(); + } + + static int crypto_onetimeauth_poly1305(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_onetimeauth_poly1305(out, in, inlen, k); + } + + static int crypto_onetimeauth_poly1305_verify(byte[] h, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_onetimeauth_poly1305_verify(h, in, inlen, k); + } + + static int crypto_onetimeauth_poly1305_init(Pointer state, byte[] key) { + return libSodium().crypto_onetimeauth_poly1305_init(state, key); + } + + static int crypto_onetimeauth_poly1305_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_onetimeauth_poly1305_update(state, in, inlen); + } + + static int crypto_onetimeauth_poly1305_final(Pointer state, byte[] out) { + return libSodium().crypto_onetimeauth_poly1305_final(state, out); + } + + static void crypto_onetimeauth_poly1305_keygen(byte[] k) { + libSodium().crypto_onetimeauth_poly1305_keygen(k); + } + + static long crypto_onetimeauth_statebytes() { + return libSodium().crypto_onetimeauth_statebytes(); + } + + static long crypto_onetimeauth_bytes() { + return libSodium().crypto_onetimeauth_bytes(); + } + + static long crypto_onetimeauth_keybytes() { + return libSodium().crypto_onetimeauth_keybytes(); + } + + static String crypto_onetimeauth_primitive() { + return libSodium().crypto_onetimeauth_primitive(); + } + + static int crypto_onetimeauth(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_onetimeauth(out, in, inlen, k); + } + + static int crypto_onetimeauth_verify(byte[] h, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_onetimeauth_verify(h, in, inlen, k); + } + + static int crypto_onetimeauth_init(Pointer state, byte[] key) { + return libSodium().crypto_onetimeauth_init(state, key); + } + + static int crypto_onetimeauth_update(Pointer state, byte[] in, long inlen) { + return libSodium().crypto_onetimeauth_update(state, in, inlen); + } + + static int crypto_onetimeauth_final(Pointer state, byte[] out) { + return libSodium().crypto_onetimeauth_final(state, out); + } + + static void crypto_onetimeauth_keygen(byte[] k) { + libSodium().crypto_onetimeauth_keygen(k); + } + + static int crypto_pwhash_argon2i_alg_argon2i13() { + return libSodium().crypto_pwhash_argon2i_alg_argon2i13(); + } + + static long crypto_pwhash_argon2i_bytes_min() { + return libSodium().crypto_pwhash_argon2i_bytes_min(); + } + + static long crypto_pwhash_argon2i_bytes_max() { + return libSodium().crypto_pwhash_argon2i_bytes_max(); + } + + static long crypto_pwhash_argon2i_passwd_min() { + return libSodium().crypto_pwhash_argon2i_passwd_min(); + } + + static long crypto_pwhash_argon2i_passwd_max() { + return libSodium().crypto_pwhash_argon2i_passwd_max(); + } + + static long crypto_pwhash_argon2i_saltbytes() { + return libSodium().crypto_pwhash_argon2i_saltbytes(); + } + + static long crypto_pwhash_argon2i_strbytes() { + return libSodium().crypto_pwhash_argon2i_strbytes(); + } + + static String crypto_pwhash_argon2i_strprefix() { + return libSodium().crypto_pwhash_argon2i_strprefix(); + } + + static long crypto_pwhash_argon2i_opslimit_min() { + return libSodium().crypto_pwhash_argon2i_opslimit_min(); + } + + static long crypto_pwhash_argon2i_opslimit_max() { + return libSodium().crypto_pwhash_argon2i_opslimit_max(); + } + + static long crypto_pwhash_argon2i_memlimit_min() { + return libSodium().crypto_pwhash_argon2i_memlimit_min(); + } + + static long crypto_pwhash_argon2i_memlimit_max() { + return libSodium().crypto_pwhash_argon2i_memlimit_max(); + } + + static long crypto_pwhash_argon2i_opslimit_interactive() { + return libSodium().crypto_pwhash_argon2i_opslimit_interactive(); + } + + static long crypto_pwhash_argon2i_memlimit_interactive() { + return libSodium().crypto_pwhash_argon2i_memlimit_interactive(); + } + + static long crypto_pwhash_argon2i_opslimit_moderate() { + return libSodium().crypto_pwhash_argon2i_opslimit_moderate(); + } + + static long crypto_pwhash_argon2i_memlimit_moderate() { + return libSodium().crypto_pwhash_argon2i_memlimit_moderate(); + } + + static long crypto_pwhash_argon2i_opslimit_sensitive() { + return libSodium().crypto_pwhash_argon2i_opslimit_sensitive(); + } + + static long crypto_pwhash_argon2i_memlimit_sensitive() { + return libSodium().crypto_pwhash_argon2i_memlimit_sensitive(); + } + + static int crypto_pwhash_argon2i( + byte[] out, + long outlen, + byte[] passwd, + long passwdlen, + byte[] salt, + long opslimit, + long memlimit, + int alg) { + return libSodium() + .crypto_pwhash_argon2i(out, outlen, passwd, passwdlen, salt, opslimit, memlimit, alg); + } + + static int crypto_pwhash_argon2i_str( + byte[] out, byte[] passwd, long passwdlen, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_argon2i_str(out, passwd, passwdlen, opslimit, memlimit); + } + + static int crypto_pwhash_argon2i_str_verify(byte[] str, byte[] passwd, long passwdlen) { + return libSodium().crypto_pwhash_argon2i_str_verify(str, passwd, passwdlen); + } + + static int crypto_pwhash_argon2i_str_needs_rehash(byte[] str, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_argon2i_str_needs_rehash(str, opslimit, memlimit); + } + + static int crypto_pwhash_argon2id_alg_argon2id13() { + return libSodium().crypto_pwhash_argon2id_alg_argon2id13(); + } + + static long crypto_pwhash_argon2id_bytes_min() { + return libSodium().crypto_pwhash_argon2id_bytes_min(); + } + + static long crypto_pwhash_argon2id_bytes_max() { + return libSodium().crypto_pwhash_argon2id_bytes_max(); + } + + static long crypto_pwhash_argon2id_passwd_min() { + return libSodium().crypto_pwhash_argon2id_passwd_min(); + } + + static long crypto_pwhash_argon2id_passwd_max() { + return libSodium().crypto_pwhash_argon2id_passwd_max(); + } + + static long crypto_pwhash_argon2id_saltbytes() { + return libSodium().crypto_pwhash_argon2id_saltbytes(); + } + + static long crypto_pwhash_argon2id_strbytes() { + return libSodium().crypto_pwhash_argon2id_strbytes(); + } + + static String crypto_pwhash_argon2id_strprefix() { + return libSodium().crypto_pwhash_argon2id_strprefix(); + } + + static long crypto_pwhash_argon2id_opslimit_min() { + return libSodium().crypto_pwhash_argon2id_opslimit_min(); + } + + static long crypto_pwhash_argon2id_opslimit_max() { + return libSodium().crypto_pwhash_argon2id_opslimit_max(); + } + + static long crypto_pwhash_argon2id_memlimit_min() { + return libSodium().crypto_pwhash_argon2id_memlimit_min(); + } + + static long crypto_pwhash_argon2id_memlimit_max() { + return libSodium().crypto_pwhash_argon2id_memlimit_max(); + } + + static long crypto_pwhash_argon2id_opslimit_interactive() { + return libSodium().crypto_pwhash_argon2id_opslimit_interactive(); + } + + static long crypto_pwhash_argon2id_memlimit_interactive() { + return libSodium().crypto_pwhash_argon2id_memlimit_interactive(); + } + + static long crypto_pwhash_argon2id_opslimit_moderate() { + return libSodium().crypto_pwhash_argon2id_opslimit_moderate(); + } + + static long crypto_pwhash_argon2id_memlimit_moderate() { + return libSodium().crypto_pwhash_argon2id_memlimit_moderate(); + } + + static long crypto_pwhash_argon2id_opslimit_sensitive() { + return libSodium().crypto_pwhash_argon2id_opslimit_sensitive(); + } + + static long crypto_pwhash_argon2id_memlimit_sensitive() { + return libSodium().crypto_pwhash_argon2id_memlimit_sensitive(); + } + + static int crypto_pwhash_argon2id( + byte[] out, + long outlen, + byte[] passwd, + long passwdlen, + byte[] salt, + long opslimit, + long memlimit, + int alg) { + return libSodium() + .crypto_pwhash_argon2id(out, outlen, passwd, passwdlen, salt, opslimit, memlimit, alg); + } + + static int crypto_pwhash_argon2id_str( + byte[] out, byte[] passwd, long passwdlen, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_argon2id_str(out, passwd, passwdlen, opslimit, memlimit); + } + + static int crypto_pwhash_argon2id_str_verify(byte[] str, byte[] passwd, long passwdlen) { + return libSodium().crypto_pwhash_argon2id_str_verify(str, passwd, passwdlen); + } + + static int crypto_pwhash_argon2id_str_needs_rehash(byte[] str, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_argon2id_str_needs_rehash(str, opslimit, memlimit); + } + + static int crypto_pwhash_alg_argon2i13() { + return libSodium().crypto_pwhash_alg_argon2i13(); + } + + static int crypto_pwhash_alg_argon2id13() { + return libSodium().crypto_pwhash_alg_argon2id13(); + } + + static int crypto_pwhash_alg_default() { + return libSodium().crypto_pwhash_alg_default(); + } + + static long crypto_pwhash_bytes_min() { + return libSodium().crypto_pwhash_bytes_min(); + } + + static long crypto_pwhash_bytes_max() { + return libSodium().crypto_pwhash_bytes_max(); + } + + static long crypto_pwhash_passwd_min() { + return libSodium().crypto_pwhash_passwd_min(); + } + + static long crypto_pwhash_passwd_max() { + return libSodium().crypto_pwhash_passwd_max(); + } + + static long crypto_pwhash_saltbytes() { + return libSodium().crypto_pwhash_saltbytes(); + } + + static long crypto_pwhash_strbytes() { + return libSodium().crypto_pwhash_strbytes(); + } + + static String crypto_pwhash_strprefix() { + return libSodium().crypto_pwhash_strprefix(); + } + + static long crypto_pwhash_opslimit_min() { + return libSodium().crypto_pwhash_opslimit_min(); + } + + static long crypto_pwhash_opslimit_max() { + return libSodium().crypto_pwhash_opslimit_max(); + } + + static long crypto_pwhash_memlimit_min() { + return libSodium().crypto_pwhash_memlimit_min(); + } + + static long crypto_pwhash_memlimit_max() { + return libSodium().crypto_pwhash_memlimit_max(); + } + + static long crypto_pwhash_opslimit_interactive() { + return libSodium().crypto_pwhash_opslimit_interactive(); + } + + static long crypto_pwhash_memlimit_interactive() { + return libSodium().crypto_pwhash_memlimit_interactive(); + } + + static long crypto_pwhash_opslimit_moderate() { + return libSodium().crypto_pwhash_opslimit_moderate(); + } + + static long crypto_pwhash_memlimit_moderate() { + return libSodium().crypto_pwhash_memlimit_moderate(); + } + + static long crypto_pwhash_opslimit_sensitive() { + return libSodium().crypto_pwhash_opslimit_sensitive(); + } + + static long crypto_pwhash_memlimit_sensitive() { + return libSodium().crypto_pwhash_memlimit_sensitive(); + } + + static int crypto_pwhash( + byte[] out, + long outlen, + byte[] passwd, + long passwdlen, + Pointer salt, + long opslimit, + long memlimit, + int alg) { + return libSodium().crypto_pwhash(out, outlen, passwd, passwdlen, salt, opslimit, memlimit, alg); + } + + static int crypto_pwhash_str( + byte[] out, byte[] passwd, long passwdlen, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_str(out, passwd, passwdlen, opslimit, memlimit); + } + + static int crypto_pwhash_str_alg( + byte[] out, byte[] passwd, long passwdlen, long opslimit, long memlimit, int alg) { + return libSodium().crypto_pwhash_str_alg(out, passwd, passwdlen, opslimit, memlimit, alg); + } + + static int crypto_pwhash_str_verify(Pointer str, byte[] passwd, long passwdlen) { + return libSodium().crypto_pwhash_str_verify(str, passwd, passwdlen); + } + + static int crypto_pwhash_str_needs_rehash(Pointer str, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_str_needs_rehash(str, opslimit, memlimit); + } + + static String crypto_pwhash_primitive() { + return libSodium().crypto_pwhash_primitive(); + } + + static long crypto_scalarmult_curve25519_bytes() { + return libSodium().crypto_scalarmult_curve25519_bytes(); + } + + static long crypto_scalarmult_curve25519_scalarbytes() { + return libSodium().crypto_scalarmult_curve25519_scalarbytes(); + } + + static int crypto_scalarmult_curve25519(byte[] q, byte[] n, byte[] p) { + return libSodium().crypto_scalarmult_curve25519(q, n, p); + } + + static int crypto_scalarmult_curve25519_base(byte[] q, byte[] n) { + return libSodium().crypto_scalarmult_curve25519_base(q, n); + } + + static long crypto_scalarmult_bytes() { + return libSodium().crypto_scalarmult_bytes(); + } + + static long crypto_scalarmult_scalarbytes() { + return libSodium().crypto_scalarmult_scalarbytes(); + } + + static String crypto_scalarmult_primitive() { + return libSodium().crypto_scalarmult_primitive(); + } + + static int crypto_scalarmult_base(Pointer q, Pointer n) { + return libSodium().crypto_scalarmult_base(q, n); + } + + static int crypto_scalarmult(Pointer q, Pointer n, Pointer p) { + return libSodium().crypto_scalarmult(q, n, p); + } + + static long crypto_secretbox_xsalsa20poly1305_keybytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_keybytes(); + } + + static long crypto_secretbox_xsalsa20poly1305_noncebytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_noncebytes(); + } + + static long crypto_secretbox_xsalsa20poly1305_macbytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_macbytes(); + } + + static long crypto_secretbox_xsalsa20poly1305_messagebytes_max() { + return libSodium().crypto_secretbox_xsalsa20poly1305_messagebytes_max(); + } + + static int crypto_secretbox_xsalsa20poly1305(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xsalsa20poly1305(c, m, mlen, n, k); + } + + static int crypto_secretbox_xsalsa20poly1305_open( + byte[] m, byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xsalsa20poly1305_open(m, c, clen, n, k); + } + + static void crypto_secretbox_xsalsa20poly1305_keygen(byte[] k) { + libSodium().crypto_secretbox_xsalsa20poly1305_keygen(k); + } + + static long crypto_secretbox_xsalsa20poly1305_boxzerobytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_boxzerobytes(); + } + + static long crypto_secretbox_xsalsa20poly1305_zerobytes() { + return libSodium().crypto_secretbox_xsalsa20poly1305_zerobytes(); + } + + static long crypto_secretbox_keybytes() { + return libSodium().crypto_secretbox_keybytes(); + } + + static long crypto_secretbox_noncebytes() { + return libSodium().crypto_secretbox_noncebytes(); + } + + static long crypto_secretbox_macbytes() { + return libSodium().crypto_secretbox_macbytes(); + } + + static String crypto_secretbox_primitive() { + return libSodium().crypto_secretbox_primitive(); + } + + static long crypto_secretbox_messagebytes_max() { + return libSodium().crypto_secretbox_messagebytes_max(); + } + + static int crypto_secretbox_easy(byte[] c, byte[] m, long mlen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_easy(c, m, mlen, n, k); + } + + static int crypto_secretbox_easy(Pointer c, Pointer m, long mlen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_easy(c, m, mlen, n, k); + } + + static int crypto_secretbox_open_easy(byte[] m, byte[] c, long clen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_open_easy(m, c, clen, n, k); + } + + static int crypto_secretbox_open_easy(Pointer m, Pointer c, long clen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_open_easy(m, c, clen, n, k); + } + + static int crypto_secretbox_detached( + byte[] c, byte[] mac, byte[] m, long mlen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_detached(c, mac, m, mlen, n, k); + } + + static int crypto_secretbox_open_detached( + byte[] m, byte[] c, byte[] mac, long clen, Pointer n, Pointer k) { + return libSodium().crypto_secretbox_open_detached(m, c, mac, clen, n, k); + } + + static void crypto_secretbox_keygen(Pointer k) { + libSodium().crypto_secretbox_keygen(k); + } + + static long crypto_secretbox_zerobytes() { + return libSodium().crypto_secretbox_zerobytes(); + } + + static long crypto_secretbox_boxzerobytes() { + return libSodium().crypto_secretbox_boxzerobytes(); + } + + static int crypto_secretbox(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox(c, m, mlen, n, k); + } + + static int crypto_secretbox_open(byte[] m, byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_open(m, c, clen, n, k); + } + + static long crypto_stream_chacha20_keybytes() { + return libSodium().crypto_stream_chacha20_keybytes(); + } + + static long crypto_stream_chacha20_noncebytes() { + return libSodium().crypto_stream_chacha20_noncebytes(); + } + + static long crypto_stream_chacha20_messagebytes_max() { + return libSodium().crypto_stream_chacha20_messagebytes_max(); + } + + static int crypto_stream_chacha20(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_chacha20(c, clen, n, k); + } + + static int crypto_stream_chacha20_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_chacha20_xor(c, m, mlen, n, k); + } + + static int crypto_stream_chacha20_xor_ic( + byte[] c, byte[] m, long mlen, byte[] n, long ic, byte[] k) { + return libSodium().crypto_stream_chacha20_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_chacha20_keygen(byte[] k) { + libSodium().crypto_stream_chacha20_keygen(k); + } + + static long crypto_stream_chacha20_ietf_keybytes() { + return libSodium().crypto_stream_chacha20_ietf_keybytes(); + } + + static long crypto_stream_chacha20_ietf_noncebytes() { + return libSodium().crypto_stream_chacha20_ietf_noncebytes(); + } + + static long crypto_stream_chacha20_ietf_messagebytes_max() { + return libSodium().crypto_stream_chacha20_ietf_messagebytes_max(); + } + + static int crypto_stream_chacha20_ietf(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_chacha20_ietf(c, clen, n, k); + } + + static int crypto_stream_chacha20_ietf_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_chacha20_ietf_xor(c, m, mlen, n, k); + } + + static int crypto_stream_chacha20_ietf_xor_ic( + byte[] c, byte[] m, long mlen, byte[] n, int ic, byte[] k) { + return libSodium().crypto_stream_chacha20_ietf_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_chacha20_ietf_keygen(byte[] k) { + libSodium().crypto_stream_chacha20_ietf_keygen(k); + } + + static long crypto_secretstream_xchacha20poly1305_abytes() { + return libSodium().crypto_secretstream_xchacha20poly1305_abytes(); + } + + static long crypto_secretstream_xchacha20poly1305_headerbytes() { + return libSodium().crypto_secretstream_xchacha20poly1305_headerbytes(); + } + + static long crypto_secretstream_xchacha20poly1305_keybytes() { + return libSodium().crypto_secretstream_xchacha20poly1305_keybytes(); + } + + static long crypto_secretstream_xchacha20poly1305_messagebytes_max() { + return libSodium().crypto_secretstream_xchacha20poly1305_messagebytes_max(); + } + + static char crypto_secretstream_xchacha20poly1305_tag_message() { + return libSodium().crypto_secretstream_xchacha20poly1305_tag_message(); + } + + static char crypto_secretstream_xchacha20poly1305_tag_push() { + return libSodium().crypto_secretstream_xchacha20poly1305_tag_push(); + } + + static char crypto_secretstream_xchacha20poly1305_tag_rekey() { + return libSodium().crypto_secretstream_xchacha20poly1305_tag_rekey(); + } + + static char crypto_secretstream_xchacha20poly1305_tag_final() { + return libSodium().crypto_secretstream_xchacha20poly1305_tag_final(); + } + + static long crypto_secretstream_xchacha20poly1305_statebytes() { + return libSodium().crypto_secretstream_xchacha20poly1305_statebytes(); + } + + static void crypto_secretstream_xchacha20poly1305_keygen(Pointer k) { + libSodium().crypto_secretstream_xchacha20poly1305_keygen(k); + } + + static int crypto_secretstream_xchacha20poly1305_init_push( + Pointer state, byte[] header, Pointer k) { + return libSodium().crypto_secretstream_xchacha20poly1305_init_push(state, header, k); + } + + static int crypto_secretstream_xchacha20poly1305_push( + Pointer state, + byte[] c, + @Nullable LongLongByReference clen_p, + byte[] m, + long mlen, + @Nullable byte[] ad, + long adlen, + byte tag) { + return libSodium() + .crypto_secretstream_xchacha20poly1305_push(state, c, clen_p, m, mlen, ad, adlen, tag); + } + + static int crypto_secretstream_xchacha20poly1305_init_pull( + Pointer state, byte[] header, Pointer k) { + return libSodium().crypto_secretstream_xchacha20poly1305_init_pull(state, header, k); + } + + static int crypto_secretstream_xchacha20poly1305_pull( + Pointer state, + byte[] m, + @Nullable LongLongByReference mlen_p, + ByteByReference tag_p, + byte[] c, + long clen, + @Nullable byte[] ad, + long adlen) { + return libSodium() + .crypto_secretstream_xchacha20poly1305_pull(state, m, mlen_p, tag_p, c, clen, ad, adlen); + } + + static void crypto_secretstream_xchacha20poly1305_rekey(Pointer state) { + libSodium().crypto_secretstream_xchacha20poly1305_rekey(state); + } + + static long crypto_shorthash_siphash24_bytes() { + return libSodium().crypto_shorthash_siphash24_bytes(); + } + + static long crypto_shorthash_siphash24_keybytes() { + return libSodium().crypto_shorthash_siphash24_keybytes(); + } + + static int crypto_shorthash_siphash24(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_shorthash_siphash24(out, in, inlen, k); + } + + static long crypto_shorthash_siphashx24_bytes() { + return libSodium().crypto_shorthash_siphashx24_bytes(); + } + + static long crypto_shorthash_siphashx24_keybytes() { + return libSodium().crypto_shorthash_siphashx24_keybytes(); + } + + static int crypto_shorthash_siphashx24(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_shorthash_siphashx24(out, in, inlen, k); + } + + static long crypto_shorthash_bytes() { + return libSodium().crypto_shorthash_bytes(); + } + + static long crypto_shorthash_keybytes() { + return libSodium().crypto_shorthash_keybytes(); + } + + static String crypto_shorthash_primitive() { + return libSodium().crypto_shorthash_primitive(); + } + + static int crypto_shorthash(byte[] out, byte[] in, long inlen, byte[] k) { + return libSodium().crypto_shorthash(out, in, inlen, k); + } + + static void crypto_shorthash_keygen(byte[] k) { + libSodium().crypto_shorthash_keygen(k); + } + + static long crypto_sign_ed25519ph_statebytes() { + return libSodium().crypto_sign_ed25519ph_statebytes(); + } + + static long crypto_sign_ed25519_bytes() { + return libSodium().crypto_sign_ed25519_bytes(); + } + + static long crypto_sign_ed25519_seedbytes() { + return libSodium().crypto_sign_ed25519_seedbytes(); + } + + static long crypto_sign_ed25519_publickeybytes() { + return libSodium().crypto_sign_ed25519_publickeybytes(); + } + + static long crypto_sign_ed25519_secretkeybytes() { + return libSodium().crypto_sign_ed25519_secretkeybytes(); + } + + static long crypto_sign_ed25519_messagebytes_max() { + return libSodium().crypto_sign_ed25519_messagebytes_max(); + } + + static int crypto_sign_ed25519( + byte[] sm, LongLongByReference smlen_p, byte[] m, long mlen, byte[] sk) { + return libSodium().crypto_sign_ed25519(sm, smlen_p, m, mlen, sk); + } + + static int crypto_sign_ed25519_open( + byte[] m, LongLongByReference mlen_p, byte[] sm, long smlen, byte[] pk) { + return libSodium().crypto_sign_ed25519_open(m, mlen_p, sm, smlen, pk); + } + + static int crypto_sign_ed25519_detached( + byte[] sig, LongLongByReference siglen_p, byte[] m, long mlen, byte[] sk) { + return libSodium().crypto_sign_ed25519_detached(sig, siglen_p, m, mlen, sk); + } + + static int crypto_sign_ed25519_verify_detached(byte[] sig, byte[] m, long mlen, byte[] pk) { + return libSodium().crypto_sign_ed25519_verify_detached(sig, m, mlen, pk); + } + + static int crypto_sign_ed25519_keypair(byte[] pk, byte[] sk) { + return libSodium().crypto_sign_ed25519_keypair(pk, sk); + } + + static int crypto_sign_ed25519_seed_keypair(byte[] pk, byte[] sk, byte[] seed) { + return libSodium().crypto_sign_ed25519_seed_keypair(pk, sk, seed); + } + + static int crypto_sign_ed25519_pk_to_curve25519(Pointer curve25519_pk, Pointer ed25519_pk) { + return libSodium().crypto_sign_ed25519_pk_to_curve25519(curve25519_pk, ed25519_pk); + } + + static int crypto_sign_ed25519_sk_to_curve25519(Pointer curve25519_sk, Pointer ed25519_sk) { + return libSodium().crypto_sign_ed25519_sk_to_curve25519(curve25519_sk, ed25519_sk); + } + + static int crypto_sign_ed25519_sk_to_seed(byte[] seed, byte[] sk) { + return libSodium().crypto_sign_ed25519_sk_to_seed(seed, sk); + } + + static int crypto_sign_ed25519_sk_to_pk(Pointer pk, Pointer sk) { + return libSodium().crypto_sign_ed25519_sk_to_pk(pk, sk); + } + + static int crypto_sign_ed25519ph_init(Pointer state) { + return libSodium().crypto_sign_ed25519ph_init(state); + } + + static int crypto_sign_ed25519ph_update(Pointer state, byte[] m, long mlen) { + return libSodium().crypto_sign_ed25519ph_update(state, m, mlen); + } + + static int crypto_sign_ed25519ph_final_create( + Pointer state, byte[] sig, LongLongByReference siglen_p, byte[] sk) { + return libSodium().crypto_sign_ed25519ph_final_create(state, sig, siglen_p, sk); + } + + static int crypto_sign_ed25519ph_final_verify(Pointer state, byte[] sig, byte[] pk) { + return libSodium().crypto_sign_ed25519ph_final_verify(state, sig, pk); + } + + static long crypto_sign_statebytes() { + return libSodium().crypto_sign_statebytes(); + } + + static long crypto_sign_bytes() { + return libSodium().crypto_sign_bytes(); + } + + static long crypto_sign_seedbytes() { + return libSodium().crypto_sign_seedbytes(); + } + + static long crypto_sign_publickeybytes() { + return libSodium().crypto_sign_publickeybytes(); + } + + static long crypto_sign_secretkeybytes() { + return libSodium().crypto_sign_secretkeybytes(); + } + + static long crypto_sign_messagebytes_max() { + return libSodium().crypto_sign_messagebytes_max(); + } + + static String crypto_sign_primitive() { + return libSodium().crypto_sign_primitive(); + } + + static int crypto_sign_seed_keypair(Pointer pk, Pointer sk, Pointer seed) { + return libSodium().crypto_sign_seed_keypair(pk, sk, seed); + } + + static int crypto_sign_keypair(Pointer pk, Pointer sk) { + return libSodium().crypto_sign_keypair(pk, sk); + } + + static int crypto_sign( + byte[] sm, @Nullable LongLongByReference smlen_p, byte[] m, long mlen, Pointer sk) { + return libSodium().crypto_sign(sm, smlen_p, m, mlen, sk); + } + + static int crypto_sign_open( + byte[] m, LongLongByReference mlen_p, byte[] sm, long smlen, Pointer pk) { + return libSodium().crypto_sign_open(m, mlen_p, sm, smlen, pk); + } + + static int crypto_sign_detached( + byte[] sig, @Nullable LongLongByReference siglen_p, byte[] m, long mlen, Pointer sk) { + return libSodium().crypto_sign_detached(sig, siglen_p, m, mlen, sk); + } + + static int crypto_sign_detached( + Pointer sig, @Nullable LongLongByReference siglen_p, Pointer m, long mlen, Pointer sk) { + return libSodium().crypto_sign_detached(sig, siglen_p, m, mlen, sk); + } + + static int crypto_sign_verify_detached(Pointer sig, Pointer m, long mlen, Pointer pk) { + return libSodium().crypto_sign_verify_detached(sig, m, mlen, pk); + } + + static int crypto_sign_verify_detached(byte[] sig, byte[] m, long mlen, Pointer pk) { + return libSodium().crypto_sign_verify_detached(sig, m, mlen, pk); + } + + static int crypto_sign_init(Pointer state) { + return libSodium().crypto_sign_init(state); + } + + static int crypto_sign_update(Pointer state, byte[] m, long mlen) { + return libSodium().crypto_sign_update(state, m, mlen); + } + + static int crypto_sign_final_create( + Pointer state, byte[] sig, LongLongByReference siglen_p, byte[] sk) { + return libSodium().crypto_sign_final_create(state, sig, siglen_p, sk); + } + + static int crypto_sign_final_verify(Pointer state, byte[] sig, byte[] pk) { + return libSodium().crypto_sign_final_verify(state, sig, pk); + } + + static long crypto_stream_keybytes() { + return libSodium().crypto_stream_keybytes(); + } + + static long crypto_stream_noncebytes() { + return libSodium().crypto_stream_noncebytes(); + } + + static long crypto_stream_messagebytes_max() { + return libSodium().crypto_stream_messagebytes_max(); + } + + static String crypto_stream_primitive() { + return libSodium().crypto_stream_primitive(); + } + + static int crypto_stream(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream(c, clen, n, k); + } + + static int crypto_stream_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xor(c, m, mlen, n, k); + } + + static void crypto_stream_keygen(byte[] k) { + libSodium().crypto_stream_keygen(k); + } + + static long crypto_stream_salsa20_keybytes() { + return libSodium().crypto_stream_salsa20_keybytes(); + } + + static long crypto_stream_salsa20_noncebytes() { + return libSodium().crypto_stream_salsa20_noncebytes(); + } + + static long crypto_stream_salsa20_messagebytes_max() { + return libSodium().crypto_stream_salsa20_messagebytes_max(); + } + + static int crypto_stream_salsa20(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa20(c, clen, n, k); + } + + static int crypto_stream_salsa20_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa20_xor(c, m, mlen, n, k); + } + + static int crypto_stream_salsa20_xor_ic( + byte[] c, byte[] m, long mlen, byte[] n, long ic, byte[] k) { + return libSodium().crypto_stream_salsa20_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_salsa20_keygen(byte[] k) { + libSodium().crypto_stream_salsa20_keygen(k); + } + + static long crypto_verify_16_bytes() { + return libSodium().crypto_verify_16_bytes(); + } + + static int crypto_verify_16(byte[] x, byte[] y) { + return libSodium().crypto_verify_16(x, y); + } + + static long crypto_verify_32_bytes() { + return libSodium().crypto_verify_32_bytes(); + } + + static int crypto_verify_32(byte[] x, byte[] y) { + return libSodium().crypto_verify_32(x, y); + } + + static long crypto_verify_64_bytes() { + return libSodium().crypto_verify_64_bytes(); + } + + static int crypto_verify_64(byte[] x, byte[] y) { + return libSodium().crypto_verify_64(x, y); + } + + static String implementation_name() { + return libSodium().implementation_name(); + } + + static int random() { + return libSodium().random(); + } + + static void stir() { + libSodium().stir(); + } + + static int uniform(int upper_bound) { + return libSodium().uniform(upper_bound); + } + + static void buf(byte[] buf, long size) { + libSodium().buf(buf, size); + } + + static int close() { + return libSodium().close(); + } + + static long randombytes_seedbytes() { + return libSodium().randombytes_seedbytes(); + } + + static void randombytes_buf(Pointer buf, long size) { + libSodium().randombytes_buf(buf, size); + } + + static void randombytes_buf_deterministic(byte[] buf, long size, byte[] seed) { + libSodium().randombytes_buf_deterministic(buf, size, seed); + } + + static int randombytes_random() { + return libSodium().randombytes_random(); + } + + static int randombytes_uniform(int upper_bound) { + return libSodium().randombytes_uniform(upper_bound); + } + + static void randombytes_stir() { + libSodium().randombytes_stir(); + } + + static int randombytes_close() { + return libSodium().randombytes_close(); + } + + static int randombytes_set_implementation(Pointer impl) { + return libSodium().randombytes_set_implementation(impl); + } + + static String randombytes_implementation_name() { + return libSodium().randombytes_implementation_name(); + } + + static void randombytes(byte[] buf, long buf_len) { + libSodium().randombytes(buf, buf_len); + } + + static int sodium_runtime_has_neon() { + return libSodium().sodium_runtime_has_neon(); + } + + static int sodium_runtime_has_sse2() { + return libSodium().sodium_runtime_has_sse2(); + } + + static int sodium_runtime_has_sse3() { + return libSodium().sodium_runtime_has_sse3(); + } + + static int sodium_runtime_has_ssse3() { + return libSodium().sodium_runtime_has_ssse3(); + } + + static int sodium_runtime_has_sse41() { + return libSodium().sodium_runtime_has_sse41(); + } + + static int sodium_runtime_has_avx() { + return libSodium().sodium_runtime_has_avx(); + } + + static int sodium_runtime_has_avx2() { + return libSodium().sodium_runtime_has_avx2(); + } + + static int sodium_runtime_has_avx512f() { + return libSodium().sodium_runtime_has_avx512f(); + } + + static int sodium_runtime_has_pclmul() { + return libSodium().sodium_runtime_has_pclmul(); + } + + static int sodium_runtime_has_aesni() { + return libSodium().sodium_runtime_has_aesni(); + } + + static int sodium_runtime_has_rdrand() { + return libSodium().sodium_runtime_has_rdrand(); + } + + static int _sodium_runtime_get_cpu_features() { + return libSodium()._sodium_runtime_get_cpu_features(); + } + + static void sodium_memzero(Pointer pnt, long len) { + libSodium().sodium_memzero(pnt, len); + } + + // static void sodium_stackzero(long len) { + // libSodium().sodium_stackzero(len); + // } + + static int sodium_memcmp(Pointer b1_, Pointer b2_, long len) { + return libSodium().sodium_memcmp(b1_, b2_, len); + } + + static int sodium_compare(Pointer b1_, Pointer b2_, long len) { + return libSodium().sodium_compare(b1_, b2_, len); + } + + static int sodium_is_zero(Pointer n, long nlen) { + return libSodium().sodium_is_zero(n, nlen); + } + + static void sodium_increment(Pointer n, long nlen) { + libSodium().sodium_increment(n, nlen); + } + + static void sodium_add(Pointer a, Pointer b, long len) { + libSodium().sodium_add(a, b, len); + } + + // FIXME: not available due to issue with LibSodium#sodium_bin2hex + // static byte[] sodium_bin2hex(byte[] hex, long hex_maxlen, byte[] bin, long bin_len) { + // return libSodium().sodium_bin2hex(hex, hex_maxlen, bin, bin_len); + // } + + static int sodium_hex2bin( + byte[] bin, + long bin_maxlen, + byte[] hex, + long hex_len, + byte[] ignore, + LongLongByReference bin_len, + Pointer hex_end) { + return libSodium().sodium_hex2bin(bin, bin_maxlen, hex, hex_len, ignore, bin_len, hex_end); + } + + static long sodium_base64_encoded_len(long bin_len, int variant) { + return libSodium().sodium_base64_encoded_len(bin_len, variant); + } + + // FIXME: not available due to issue with LibSodium#sodium_bin2base64 + // static byte[] sodium_bin2base64(byte[] b64, long b64_maxlen, byte[] bin, long bin_len, int + // variant) { + // return libSodium().sodium_bin2base64(b64, b64_maxlen, bin, bin_len, variant); + // } + + static int sodium_base642bin( + byte[] bin, + long bin_maxlen, + byte[] b64, + long b64_len, + byte[] ignore, + LongLongByReference bin_len, + Pointer b64_end, + int variant) { + return libSodium() + .sodium_base642bin(bin, bin_maxlen, b64, b64_len, ignore, bin_len, b64_end, variant); + } + + static int sodium_mlock(Pointer addr, long len) { + return libSodium().sodium_mlock(addr, len); + } + + static int sodium_munlock(Pointer addr, long len) { + return libSodium().sodium_munlock(addr, len); + } + + static Pointer sodium_malloc(long size) { + return libSodium().sodium_malloc(size); + } + + static Pointer sodium_allocarray(long count, long size) { + return libSodium().sodium_allocarray(count, size); + } + + static void sodium_free(Pointer ptr) { + libSodium().sodium_free(ptr); + } + + static int sodium_mprotect_noaccess(Pointer ptr) { + return libSodium().sodium_mprotect_noaccess(ptr); + } + + static int sodium_mprotect_readonly(Pointer ptr) { + return libSodium().sodium_mprotect_readonly(ptr); + } + + static int sodium_mprotect_readwrite(Pointer ptr) { + return libSodium().sodium_mprotect_readwrite(ptr); + } + + static int sodium_pad( + LongLongByReference padded_buflen_p, + byte[] buf, + long unpadded_buflen, + long blocksize, + long max_buflen) { + return libSodium().sodium_pad(padded_buflen_p, buf, unpadded_buflen, blocksize, max_buflen); + } + + static int sodium_unpad( + LongLongByReference unpadded_buflen_p, byte[] buf, long padded_buflen, long blocksize) { + return libSodium().sodium_unpad(unpadded_buflen_p, buf, padded_buflen, blocksize); + } + + static long crypto_stream_xchacha20_keybytes() { + return libSodium().crypto_stream_xchacha20_keybytes(); + } + + static long crypto_stream_xchacha20_noncebytes() { + return libSodium().crypto_stream_xchacha20_noncebytes(); + } + + static long crypto_stream_xchacha20_messagebytes_max() { + return libSodium().crypto_stream_xchacha20_messagebytes_max(); + } + + static int crypto_stream_xchacha20(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xchacha20(c, clen, n, k); + } + + static int crypto_stream_xchacha20_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_xchacha20_xor(c, m, mlen, n, k); + } + + static int crypto_stream_xchacha20_xor_ic( + byte[] c, byte[] m, long mlen, byte[] n, long ic, byte[] k) { + return libSodium().crypto_stream_xchacha20_xor_ic(c, m, mlen, n, ic, k); + } + + static void crypto_stream_xchacha20_keygen(byte[] k) { + libSodium().crypto_stream_xchacha20_keygen(k); + } + + static long crypto_box_curve25519xchacha20poly1305_seedbytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_seedbytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_publickeybytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_publickeybytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_secretkeybytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_secretkeybytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_beforenmbytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_beforenmbytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_noncebytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_noncebytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_macbytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_macbytes(); + } + + static long crypto_box_curve25519xchacha20poly1305_messagebytes_max() { + return libSodium().crypto_box_curve25519xchacha20poly1305_messagebytes_max(); + } + + static int crypto_box_curve25519xchacha20poly1305_seed_keypair( + byte[] pk, byte[] sk, byte[] seed) { + return libSodium().crypto_box_curve25519xchacha20poly1305_seed_keypair(pk, sk, seed); + } + + static int crypto_box_curve25519xchacha20poly1305_keypair(byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_keypair(pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_easy( + byte[] c, byte[] m, long mlen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_easy(c, m, mlen, n, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_open_easy( + byte[] m, byte[] c, long clen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_open_easy(m, c, clen, n, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_detached( + byte[] c, byte[] mac, byte[] m, long mlen, byte[] n, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_detached(c, mac, m, mlen, n, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_open_detached( + byte[] m, byte[] c, byte[] mac, long clen, byte[] n, byte[] pk, byte[] sk) { + return libSodium() + .crypto_box_curve25519xchacha20poly1305_open_detached(m, c, mac, clen, n, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_beforenm(Pointer k, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_beforenm(k, pk, sk); + } + + static int crypto_box_curve25519xchacha20poly1305_easy_afternm( + byte[] c, byte[] m, long mlen, byte[] n, Pointer k) { + return libSodium().crypto_box_curve25519xchacha20poly1305_easy_afternm(c, m, mlen, n, k); + } + + static int crypto_box_curve25519xchacha20poly1305_open_easy_afternm( + byte[] m, byte[] c, long clen, byte[] n, Pointer k) { + return libSodium().crypto_box_curve25519xchacha20poly1305_open_easy_afternm(m, c, clen, n, k); + } + + static int crypto_box_curve25519xchacha20poly1305_detached_afternm( + byte[] c, byte[] mac, byte[] m, long mlen, byte[] n, Pointer k) { + return libSodium() + .crypto_box_curve25519xchacha20poly1305_detached_afternm(c, mac, m, mlen, n, k); + } + + static int crypto_box_curve25519xchacha20poly1305_open_detached_afternm( + byte[] m, byte[] c, byte[] mac, long clen, byte[] n, Pointer k) { + return libSodium() + .crypto_box_curve25519xchacha20poly1305_open_detached_afternm(m, c, mac, clen, n, k); + } + + static long crypto_box_curve25519xchacha20poly1305_sealbytes() { + return libSodium().crypto_box_curve25519xchacha20poly1305_sealbytes(); + } + + static int crypto_box_curve25519xchacha20poly1305_seal(byte[] c, byte[] m, long mlen, byte[] pk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_seal(c, m, mlen, pk); + } + + static int crypto_box_curve25519xchacha20poly1305_seal_open( + byte[] m, byte[] c, long clen, byte[] pk, byte[] sk) { + return libSodium().crypto_box_curve25519xchacha20poly1305_seal_open(m, c, clen, pk, sk); + } + + static long crypto_core_ed25519_bytes() { + return libSodium().crypto_core_ed25519_bytes(); + } + + static long crypto_core_ed25519_uniformbytes() { + return libSodium().crypto_core_ed25519_uniformbytes(); + } + + static long crypto_core_ed25519_hashbytes() { + return libSodium().crypto_core_ed25519_hashbytes(); + } + + static long crypto_core_ed25519_scalarbytes() { + return libSodium().crypto_core_ed25519_scalarbytes(); + } + + static long crypto_core_ed25519_nonreducedscalarbytes() { + return libSodium().crypto_core_ed25519_nonreducedscalarbytes(); + } + + static int crypto_core_ed25519_is_valid_point(byte[] p) { + return libSodium().crypto_core_ed25519_is_valid_point(p); + } + + static int crypto_core_ed25519_add(byte[] r, byte[] p, byte[] q) { + return libSodium().crypto_core_ed25519_add(r, p, q); + } + + static int crypto_core_ed25519_sub(byte[] r, byte[] p, byte[] q) { + return libSodium().crypto_core_ed25519_sub(r, p, q); + } + + static int crypto_core_ed25519_from_uniform(byte[] p, byte[] r) { + return libSodium().crypto_core_ed25519_from_uniform(p, r); + } + + static int crypto_core_ed25519_from_hash(byte[] p, byte[] h) { + return libSodium().crypto_core_ed25519_from_hash(p, h); + } + + static void crypto_core_ed25519_random(byte[] p) { + libSodium().crypto_core_ed25519_random(p); + } + + static void crypto_core_ed25519_scalar_random(byte[] r) { + libSodium().crypto_core_ed25519_scalar_random(r); + } + + static int crypto_core_ed25519_scalar_invert(byte[] recip, byte[] s) { + return libSodium().crypto_core_ed25519_scalar_invert(recip, s); + } + + static void crypto_core_ed25519_scalar_negate(byte[] neg, byte[] s) { + libSodium().crypto_core_ed25519_scalar_negate(neg, s); + } + + static void crypto_core_ed25519_scalar_complement(byte[] comp, byte[] s) { + libSodium().crypto_core_ed25519_scalar_complement(comp, s); + } + + static void crypto_core_ed25519_scalar_add(byte[] z, byte[] x, byte[] y) { + libSodium().crypto_core_ed25519_scalar_add(z, x, y); + } + + static void crypto_core_ed25519_scalar_sub(byte[] z, byte[] x, byte[] y) { + libSodium().crypto_core_ed25519_scalar_sub(z, x, y); + } + + static void crypto_core_ed25519_scalar_mul(byte[] z, byte[] x, byte[] y) { + libSodium().crypto_core_ed25519_scalar_mul(z, x, y); + } + + static void crypto_core_ed25519_scalar_reduce(byte[] r, byte[] s) { + libSodium().crypto_core_ed25519_scalar_reduce(r, s); + } + + static long crypto_core_ristretto255_bytes() { + return libSodium().crypto_core_ristretto255_bytes(); + } + + static long crypto_core_ristretto255_hashbytes() { + return libSodium().crypto_core_ristretto255_hashbytes(); + } + + static long crypto_core_ristretto255_scalarbytes() { + return libSodium().crypto_core_ristretto255_scalarbytes(); + } + + static long crypto_core_ristretto255_nonreducedscalarbytes() { + return libSodium().crypto_core_ristretto255_nonreducedscalarbytes(); + } + + static int crypto_core_ristretto255_is_valid_point(byte[] p) { + return libSodium().crypto_core_ristretto255_is_valid_point(p); + } + + static int crypto_core_ristretto255_add(byte[] r, byte[] p, byte[] q) { + return libSodium().crypto_core_ristretto255_add(r, p, q); + } + + static int crypto_core_ristretto255_sub(byte[] r, byte[] p, byte[] q) { + return libSodium().crypto_core_ristretto255_sub(r, p, q); + } + + static int crypto_core_ristretto255_from_hash(byte[] p, byte[] r) { + return libSodium().crypto_core_ristretto255_from_hash(p, r); + } + + static void crypto_core_ristretto255_random(byte[] p) { + libSodium().crypto_core_ristretto255_random(p); + } + + static void crypto_core_ristretto255_scalar_random(byte[] r) { + libSodium().crypto_core_ristretto255_scalar_random(r); + } + + static int crypto_core_ristretto255_scalar_invert(byte[] recip, byte[] s) { + return libSodium().crypto_core_ristretto255_scalar_invert(recip, s); + } + + static void crypto_core_ristretto255_scalar_negate(byte[] neg, byte[] s) { + libSodium().crypto_core_ristretto255_scalar_negate(neg, s); + } + + static void crypto_core_ristretto255_scalar_complement(byte[] comp, byte[] s) { + libSodium().crypto_core_ristretto255_scalar_complement(comp, s); + } + + static void crypto_core_ristretto255_scalar_add(byte[] z, byte[] x, byte[] y) { + libSodium().crypto_core_ristretto255_scalar_add(z, x, y); + } + + static void crypto_core_ristretto255_scalar_sub(byte[] z, byte[] x, byte[] y) { + libSodium().crypto_core_ristretto255_scalar_sub(z, x, y); + } + + static void crypto_core_ristretto255_scalar_mul(byte[] z, byte[] x, byte[] y) { + libSodium().crypto_core_ristretto255_scalar_mul(z, x, y); + } + + static void crypto_core_ristretto255_scalar_reduce(byte[] r, byte[] s) { + libSodium().crypto_core_ristretto255_scalar_reduce(r, s); + } + + static long crypto_scalarmult_ed25519_bytes() { + return libSodium().crypto_scalarmult_ed25519_bytes(); + } + + static long crypto_scalarmult_ed25519_scalarbytes() { + return libSodium().crypto_scalarmult_ed25519_scalarbytes(); + } + + static int crypto_scalarmult_ed25519(byte[] q, byte[] n, byte[] p) { + return libSodium().crypto_scalarmult_ed25519(q, n, p); + } + + static int crypto_scalarmult_ed25519_noclamp(byte[] q, byte[] n, byte[] p) { + return libSodium().crypto_scalarmult_ed25519_noclamp(q, n, p); + } + + static int crypto_scalarmult_ed25519_base(byte[] q, byte[] n) { + return libSodium().crypto_scalarmult_ed25519_base(q, n); + } + + static int crypto_scalarmult_ed25519_base_noclamp(byte[] q, byte[] n) { + return libSodium().crypto_scalarmult_ed25519_base_noclamp(q, n); + } + + static long crypto_scalarmult_ristretto255_bytes() { + return libSodium().crypto_scalarmult_ristretto255_bytes(); + } + + static long crypto_scalarmult_ristretto255_scalarbytes() { + return libSodium().crypto_scalarmult_ristretto255_scalarbytes(); + } + + static int crypto_scalarmult_ristretto255(byte[] q, byte[] n, byte[] p) { + return libSodium().crypto_scalarmult_ristretto255(q, n, p); + } + + static int crypto_scalarmult_ristretto255_base(byte[] q, byte[] n) { + return libSodium().crypto_scalarmult_ristretto255_base(q, n); + } + + static long crypto_secretbox_xchacha20poly1305_keybytes() { + return libSodium().crypto_secretbox_xchacha20poly1305_keybytes(); + } + + static long crypto_secretbox_xchacha20poly1305_noncebytes() { + return libSodium().crypto_secretbox_xchacha20poly1305_noncebytes(); + } + + static long crypto_secretbox_xchacha20poly1305_macbytes() { + return libSodium().crypto_secretbox_xchacha20poly1305_macbytes(); + } + + static long crypto_secretbox_xchacha20poly1305_messagebytes_max() { + return libSodium().crypto_secretbox_xchacha20poly1305_messagebytes_max(); + } + + static int crypto_secretbox_xchacha20poly1305_easy( + byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xchacha20poly1305_easy(c, m, mlen, n, k); + } + + static int crypto_secretbox_xchacha20poly1305_open_easy( + byte[] m, byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xchacha20poly1305_open_easy(m, c, clen, n, k); + } + + static int crypto_secretbox_xchacha20poly1305_detached( + byte[] c, byte[] mac, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xchacha20poly1305_detached(c, mac, m, mlen, n, k); + } + + static int crypto_secretbox_xchacha20poly1305_open_detached( + byte[] m, byte[] c, byte[] mac, long clen, byte[] n, byte[] k) { + return libSodium().crypto_secretbox_xchacha20poly1305_open_detached(m, c, mac, clen, n, k); + } + + static long crypto_pwhash_scryptsalsa208sha256_bytes_min() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_bytes_min(); + } + + static long crypto_pwhash_scryptsalsa208sha256_bytes_max() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_bytes_max(); + } + + static long crypto_pwhash_scryptsalsa208sha256_passwd_min() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_passwd_min(); + } + + static long crypto_pwhash_scryptsalsa208sha256_passwd_max() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_passwd_max(); + } + + static long crypto_pwhash_scryptsalsa208sha256_saltbytes() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_saltbytes(); + } + + static long crypto_pwhash_scryptsalsa208sha256_strbytes() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_strbytes(); + } + + static String crypto_pwhash_scryptsalsa208sha256_strprefix() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_strprefix(); + } + + static long crypto_pwhash_scryptsalsa208sha256_opslimit_min() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_opslimit_min(); + } + + static long crypto_pwhash_scryptsalsa208sha256_opslimit_max() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_opslimit_max(); + } + + static long crypto_pwhash_scryptsalsa208sha256_memlimit_min() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_memlimit_min(); + } + + static long crypto_pwhash_scryptsalsa208sha256_memlimit_max() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_memlimit_max(); + } + + static long crypto_pwhash_scryptsalsa208sha256_opslimit_interactive() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_opslimit_interactive(); + } + + static long crypto_pwhash_scryptsalsa208sha256_memlimit_interactive() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_memlimit_interactive(); + } + + static long crypto_pwhash_scryptsalsa208sha256_opslimit_sensitive() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_opslimit_sensitive(); + } + + static long crypto_pwhash_scryptsalsa208sha256_memlimit_sensitive() { + return libSodium().crypto_pwhash_scryptsalsa208sha256_memlimit_sensitive(); + } + + static int crypto_pwhash_scryptsalsa208sha256( + byte[] out, + long outlen, + byte[] passwd, + long passwdlen, + byte[] salt, + long opslimit, + long memlimit) { + return libSodium() + .crypto_pwhash_scryptsalsa208sha256( + out, outlen, passwd, passwdlen, salt, opslimit, memlimit); + } + + static int crypto_pwhash_scryptsalsa208sha256_str( + byte[] out, byte[] passwd, long passwdlen, long opslimit, long memlimit) { + return libSodium() + .crypto_pwhash_scryptsalsa208sha256_str(out, passwd, passwdlen, opslimit, memlimit); + } + + static int crypto_pwhash_scryptsalsa208sha256_str_verify( + byte[] str, byte[] passwd, long passwdlen) { + return libSodium().crypto_pwhash_scryptsalsa208sha256_str_verify(str, passwd, passwdlen); + } + + static int crypto_pwhash_scryptsalsa208sha256_ll( + byte[] passwd, + long passwdlen, + byte[] salt, + long saltlen, + long N, + int r, + int p, + byte[] buf, + long buflen) { + return libSodium() + .crypto_pwhash_scryptsalsa208sha256_ll( + passwd, passwdlen, salt, saltlen, N, r, p, buf, buflen); + } + + static int crypto_pwhash_scryptsalsa208sha256_str_needs_rehash( + byte[] str, long opslimit, long memlimit) { + return libSodium().crypto_pwhash_scryptsalsa208sha256_str_needs_rehash(str, opslimit, memlimit); + } + + static long crypto_stream_salsa2012_keybytes() { + return libSodium().crypto_stream_salsa2012_keybytes(); + } + + static long crypto_stream_salsa2012_noncebytes() { + return libSodium().crypto_stream_salsa2012_noncebytes(); + } + + static long crypto_stream_salsa2012_messagebytes_max() { + return libSodium().crypto_stream_salsa2012_messagebytes_max(); + } + + static int crypto_stream_salsa2012(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa2012(c, clen, n, k); + } + + static int crypto_stream_salsa2012_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa2012_xor(c, m, mlen, n, k); + } + + static void crypto_stream_salsa2012_keygen(byte[] k) { + libSodium().crypto_stream_salsa2012_keygen(k); + } + + static long crypto_stream_salsa208_keybytes() { + return libSodium().crypto_stream_salsa208_keybytes(); + } + + static long crypto_stream_salsa208_noncebytes() { + return libSodium().crypto_stream_salsa208_noncebytes(); + } + + static long crypto_stream_salsa208_messagebytes_max() { + return libSodium().crypto_stream_salsa208_messagebytes_max(); + } + + static int crypto_stream_salsa208(byte[] c, long clen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa208(c, clen, n, k); + } + + static int crypto_stream_salsa208_xor(byte[] c, byte[] m, long mlen, byte[] n, byte[] k) { + return libSodium().crypto_stream_salsa208_xor(c, m, mlen, n, k); + } + + static void crypto_stream_salsa208_keygen(byte[] k) { + libSodium().crypto_stream_salsa208_keygen(k); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SodiumVersion.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SodiumVersion.java new file mode 100644 index 000000000..ff1a8cc84 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SodiumVersion.java @@ -0,0 +1,47 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +/** Details of a sodium native library version. */ +public final class SodiumVersion implements Comparable { + private final int major; + private final int minor; + private final String name; + + SodiumVersion(int major, int minor, String name) { + this.major = major; + this.minor = minor; + this.name = name; + } + + /** + * The major version number. + * + * @return The major version number. + */ + public int major() { + return major; + } + + /** + * The minor version number. + * + * @return The minor version number. + */ + public int minor() { + return minor; + } + + @Override + public String toString() { + return name; + } + + @Override + public int compareTo(SodiumVersion other) { + if (this.major == other.major) { + return Integer.compare(this.minor, other.minor); + } + return Integer.compare(this.major, other.major); + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/XChaCha20Poly1305.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/XChaCha20Poly1305.java new file mode 100644 index 000000000..60ca76f51 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/XChaCha20Poly1305.java @@ -0,0 +1,923 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import org.apache.tuweni.crypto.sodium.SodiumException; +import org.apache.tuweni.v2.bytes.Bytes; + +import javax.security.auth.Destroyable; + +import jnr.ffi.Pointer; +import jnr.ffi.byref.ByteByReference; +import jnr.ffi.byref.LongLongByReference; +import org.jetbrains.annotations.Nullable; + +// Documentation copied under the ISC License, from +// https://github.com/jedisct1/libsodium-doc/blob/424b7480562c2e063bc8c52c452ef891621c8480/secret-key_cryptography/xchacha20-poly1305_construction.md + +/** + * Authenticated Encryption with Additional Data using XChaCha20-Poly1305. + * + *

The XChaCha20-Poly1305 construction can safely encrypt a practically unlimited number of + * messages with the same key, without any practical limit to the size of a message (up to ~ 2^64 + * bytes). + * + *

As an alternative to counters, its large nonce size (192-bit) allows random nonces to be + * safely used. + * + *

For this reason, and if interoperability with other libraries is not a concern, this is the + * recommended AEAD construction. + * + *

This class depends upon the JNR-FFI library being available on the classpath, along with its + * dependencies. See https://github.com/jnr/jnr-ffi. JNR-FFI can be included using the gradle + * dependency 'com.github.jnr:jnr-ffi'. + */ +public final class XChaCha20Poly1305 { + private XChaCha20Poly1305() {} + + private static final byte[] EMPTY_BYTES = new byte[0]; + + /** + * Check if Sodium and the XChaCha20Poly1305 algorithm is available. + * + *

XChaCha20Poly1305 is supported in sodium native library version >= 10.0.12. + * + * @return {@code true} if Sodium and the XChaCha20Poly1305 algorithm is available. + */ + public static boolean isAvailable() { + try { + return Sodium.supportsVersion(Sodium.VERSION_10_0_12); + } catch (UnsatisfiedLinkError e) { + return false; + } + } + + private static void assertAvailable() { + if (!isAvailable()) { + throw new UnsupportedOperationException( + "Sodium XChaCha20Poly1305 is not available (requires sodium native library >= 10.0.12)"); + } + } + + /** + * Check if Sodium and the XChaCha20Poly1305 secret stream algorithm is available. + * + *

XChaCha20Poly1305 secret stream is supported in sodium native library version >= 10.0.14. + * + * @return {@code true} if Sodium and the XChaCha20Poly1305 secret stream algorithm is available. + */ + public static boolean isSecretStreamAvailable() { + try { + return Sodium.supportsVersion(Sodium.VERSION_10_0_14); + } catch (UnsatisfiedLinkError e) { + return false; + } + } + + private static void assertSecretStreamAvailable() { + if (!isSecretStreamAvailable()) { + throw new UnsupportedOperationException( + "Sodium XChaCha20Poly1305 secret stream is not available (requires sodium native library >= 10.0.14)"); + } + } + + /** A XChaCha20-Poly1305 key. */ + public static final class Key implements Destroyable { + final Allocated value; + + private Key(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + @Override + public void destroy() { + value.destroy(); + } + + @Override + public boolean isDestroyed() { + return value.isDestroyed(); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + */ + public static Key fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Key} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the key. + * @return A key, based on the supplied bytes. + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + public static Key fromBytes(byte[] bytes) { + assertAvailable(); + if (bytes.length != Sodium.crypto_aead_xchacha20poly1305_ietf_keybytes()) { + throw new IllegalArgumentException( + "key must be " + + Sodium.crypto_aead_xchacha20poly1305_ietf_keybytes() + + " bytes, got " + + bytes.length); + } + return Sodium.dup(bytes, Key::new); + } + + /** + * Obtain the length of the key in bytes (32). + * + * @return The length of the key in bytes (32). + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + public static int length() { + assertAvailable(); + long keybytes = Sodium.crypto_aead_xchacha20poly1305_ietf_keybytes(); + if (keybytes > Integer.MAX_VALUE) { + throw new SodiumException( + "crypto_aead_xchacha20poly1305_ietf_keybytes: " + keybytes + " is too large"); + } + return (int) keybytes; + } + + /** + * Generate a new key using a random generator. + * + * @return A randomly generated key. + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + public static Key random() { + assertAvailable(); + int length = length(); + Pointer ptr = Sodium.malloc(length); + try { + // When support for 10.0.11 is dropped, use this instead + // Sodium.crypto_aead_xchacha20poly1305_ietf_keygen(ptr); + Sodium.randombytes_buf(ptr, length); + return new Key(ptr, length); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Key)) { + return false; + } + Key other = (Key) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. + * + * @return The bytes of this key. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Obtain the bytes of this key. + * + *

WARNING: This will cause the key to be copied into heap memory. The returned array should + * be overwritten when no longer required. + * + * @return The bytes of this key. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** A XChaCha20-Poly1305 nonce. */ + public static final class Nonce { + final Allocated value; + + private Nonce(Pointer ptr, int length) { + this.value = new Allocated(ptr, length); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + */ + public static Nonce fromBytes(Bytes bytes) { + return fromBytes(bytes.toArrayUnsafe()); + } + + /** + * Create a {@link Nonce} from an array of bytes. + * + *

The byte array must be of length {@link #length()}. + * + * @param bytes The bytes for the nonce. + * @return A nonce, based on these bytes. + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + public static Nonce fromBytes(byte[] bytes) { + assertAvailable(); + if (bytes.length != Sodium.crypto_aead_xchacha20poly1305_ietf_npubbytes()) { + throw new IllegalArgumentException( + "nonce must be " + + Sodium.crypto_aead_xchacha20poly1305_ietf_npubbytes() + + " bytes, got " + + bytes.length); + } + return Sodium.dup(bytes, Nonce::new); + } + + /** + * Obtain the length of the nonce in bytes (24). + * + * @return The length of the nonce in bytes (24). + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + public static int length() { + assertAvailable(); + long npubbytes = Sodium.crypto_aead_xchacha20poly1305_ietf_npubbytes(); + if (npubbytes > Integer.MAX_VALUE) { + throw new SodiumException( + "crypto_aead_xchacha20poly1305_ietf_npubbytes: " + npubbytes + " is too large"); + } + return (int) npubbytes; + } + + /** + * Create a zero {@link Nonce}. + * + * @return A zero nonce. + */ + public static Nonce zero() { + int length = length(); + Pointer ptr = Sodium.malloc(length); + try { + Sodium.sodium_memzero(ptr, length); + return new Nonce(ptr, length); + } catch (Throwable e) { + Sodium.sodium_free(ptr); + throw e; + } + } + + /** + * Generate a random {@link Nonce}. + * + * @return A randomly generated nonce. + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + public static Nonce random() { + assertAvailable(); + return Sodium.randomBytes(length(), Nonce::new); + } + + /** + * Increment this nonce. + * + *

Note that this is not synchronized. If multiple threads are creating encrypted messages + * and incrementing this nonce, then external synchronization is required to ensure no two + * encrypt operations use the same nonce. + * + * @return A new {@link Nonce}. + */ + public Nonce increment() { + return Sodium.dupAndIncrement(value.pointer(), value.length(), Nonce::new); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Nonce)) { + return false; + } + Nonce other = (Nonce) obj; + return other.value.equals(value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Provides the bytes of this nonce + * + * @return The bytes of this nonce. + */ + public Bytes bytes() { + return value.bytes(); + } + + /** + * Provides the bytes of this nonce + * + * @return The bytes of this nonce. + */ + public byte[] bytesArray() { + return value.bytesArray(); + } + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static byte[] encrypt(byte[] message, Key key, Nonce nonce) { + return encrypt(message, EMPTY_BYTES, key, nonce); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static Bytes encrypt(Bytes message, Bytes data, Key key, Nonce nonce) { + return Bytes.wrap(encrypt(message.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce)); + } + + /** + * Encrypt a message for a given key. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + public static byte[] encrypt(byte[] message, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + byte[] cipherText = new byte[maxCypherTextLength(message)]; + + LongLongByReference cipherTextLen = new LongLongByReference(); + int rc = + Sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( + cipherText, + cipherTextLen, + message, + message.length, + data, + data.length, + null, + nonce.value.pointer(), + key.value.pointer()); + if (rc != 0) { + throw new SodiumException( + "crypto_aead_xchacha20poly1305_ietf_encrypt: failed with result " + rc); + } + + return maybeSliceResult( + cipherText, cipherTextLen, "crypto_aead_xchacha20poly1305_ietf_encrypt"); + } + + private static int maxCypherTextLength(byte[] message) { + long abytes = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException( + "crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + return (int) abytes + message.length; + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static DetachedEncryptionResult encryptDetached(Bytes message, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static DetachedEncryptionResult encryptDetached(byte[] message, Key key, Nonce nonce) { + return encryptDetached(message, EMPTY_BYTES, key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + */ + public static DetachedEncryptionResult encryptDetached( + Bytes message, Bytes data, Key key, Nonce nonce) { + return encryptDetached(message.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + } + + /** + * Encrypt a message for a given key, generating a detached message authentication code. + * + * @param message The message to encrypt. + * @param data Extra non-confidential data that will be included with the encrypted payload. + * @param key The key to encrypt for. + * @param nonce A unique nonce. + * @return The encrypted data. + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + public static DetachedEncryptionResult encryptDetached( + byte[] message, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + byte[] cipherText = new byte[message.length]; + long abytes = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException( + "crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + byte[] mac = new byte[(int) abytes]; + + LongLongByReference macLen = new LongLongByReference(); + int rc = + Sodium.crypto_aead_xchacha20poly1305_ietf_encrypt_detached( + cipherText, + mac, + macLen, + message, + message.length, + data, + data.length, + null, + nonce.value.pointer(), + key.value.pointer()); + if (rc != 0) { + throw new SodiumException( + "crypto_aead_xchacha20poly1305_ietf_encrypt_detached: failed with result " + rc); + } + + return new DefaultDetachedEncryptionResult( + cipherText, + maybeSliceResult(mac, macLen, "crypto_aead_xchacha20poly1305_ietf_encrypt_detached")); + } + + private static final byte TAG_FINAL = (0x01 | 0x02); + + private static final class SSEncrypt implements SecretEncryptionStream { + private final int abytes; + private final byte[] header; + @Nullable private Pointer state; + private boolean complete = false; + + private SSEncrypt(Key key) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + long abytes = Sodium.crypto_secretstream_xchacha20poly1305_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException( + "crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + this.abytes = (int) abytes; + + long headerbytes = Sodium.crypto_secretstream_xchacha20poly1305_headerbytes(); + if (headerbytes > Integer.MAX_VALUE) { + throw new IllegalStateException( + "crypto_secretstream_xchacha20poly1305_headerbytes: " + abytes + " is too large"); + } + this.header = new byte[(int) headerbytes]; + + Pointer state = Sodium.malloc(Sodium.crypto_secretstream_xchacha20poly1305_statebytes()); + try { + int rc = + Sodium.crypto_secretstream_xchacha20poly1305_init_push( + state, header, key.value.pointer()); + if (rc != 0) { + throw new SodiumException( + "crypto_secretstream_xchacha20poly1305_init_push: failed with result " + rc); + } + } catch (Throwable e) { + Sodium.sodium_free(state); + throw e; + } + this.state = state; + } + + @Override + public void destroy() { + if (state != null) { + Pointer p = state; + state = null; + Sodium.sodium_free(p); + } + } + + @Override + public boolean isDestroyed() { + return state == null; + } + + @Override + public byte[] headerArray() { + return header; + } + + @Override + public byte[] push(byte[] clearText, boolean isFinal) { + if (complete) { + throw new IllegalStateException("stream already completed"); + } + if (state == null) { + throw new IllegalStateException("stream has been destroyed"); + } + byte[] cipherText = new byte[abytes + clearText.length]; + byte tag = isFinal ? TAG_FINAL : 0; + int rc = + Sodium.crypto_secretstream_xchacha20poly1305_push( + state, cipherText, null, clearText, clearText.length, null, 0, tag); + if (rc != 0) { + throw new SodiumException( + "crypto_secretstream_xchacha20poly1305_push: failed with result " + rc); + } + if (isFinal) { + complete = true; + // destroy state before finalization, as it will not be re-used + destroy(); + } + return cipherText; + } + } + + /** + * Open an encryption stream. + * + * @param key The key to encrypt for. + * @return The input stream. + * @throws UnsupportedOperationException If XChaCha20Poly1305 secret stream support is not + * available. + */ + public static SecretEncryptionStream openEncryptionStream(Key key) { + assertSecretStreamAvailable(); + return new SSEncrypt(key); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, Key key, Nonce nonce) { + return decrypt(cipherText, EMPTY_BYTES, key, nonce); + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decrypt(Bytes cipherText, Bytes data, Key key, Nonce nonce) { + byte[] bytes = decrypt(cipherText.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key. + * + * @param cipherText The cipher text to decrypt. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + @Nullable + public static byte[] decrypt(byte[] cipherText, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + byte[] clearText = new byte[maxClearTextLength(cipherText)]; + + LongLongByReference clearTextLen = new LongLongByReference(); + int rc = + Sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( + clearText, + clearTextLen, + null, + cipherText, + cipherText.length, + data, + data.length, + nonce.value.pointer(), + key.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException( + "crypto_aead_xchacha20poly1305_ietf_decrypt: failed with result " + rc); + } + + return maybeSliceResult(clearText, clearTextLen, "crypto_aead_xchacha20poly1305_ietf_decrypt"); + } + + private static int maxClearTextLength(byte[] cipherText) { + long abytes = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException( + "crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + if (abytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + return cipherText.length - ((int) abytes); + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + public static Bytes decryptDetached(Bytes cipherText, Bytes mac, Key key, Nonce nonce) { + byte[] bytes = decryptDetached(cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static byte[] decryptDetached(byte[] cipherText, byte[] mac, Key key, Nonce nonce) { + return decryptDetached(cipherText, mac, EMPTY_BYTES, key, nonce); + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + */ + @Nullable + public static Bytes decryptDetached( + Bytes cipherText, Bytes mac, Bytes data, Key key, Nonce nonce) { + byte[] bytes = + decryptDetached( + cipherText.toArrayUnsafe(), mac.toArrayUnsafe(), data.toArrayUnsafe(), key, nonce); + return (bytes != null) ? Bytes.wrap(bytes) : null; + } + + /** + * Decrypt a message using a given key and a detached message authentication code. + * + * @param cipherText The cipher text to decrypt. + * @param mac The message authentication code. + * @param data Extra non-confidential data that is included within the encrypted payload. + * @param key The key to use for decryption. + * @param nonce The nonce that was used for encryption. + * @return The decrypted data, or {@code null} if verification failed. + * @throws UnsupportedOperationException If XChaCha20Poly1305 support is not available. + */ + @Nullable + public static byte[] decryptDetached( + byte[] cipherText, byte[] mac, byte[] data, Key key, Nonce nonce) { + assertAvailable(); + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + long abytes = Sodium.crypto_aead_xchacha20poly1305_ietf_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException( + "crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + if (mac.length != abytes) { + throw new IllegalArgumentException("mac must be " + abytes + " bytes, got " + mac.length); + } + + byte[] clearText = new byte[cipherText.length]; + int rc = + Sodium.crypto_aead_xchacha20poly1305_ietf_decrypt_detached( + clearText, + null, + cipherText, + cipherText.length, + mac, + data, + data.length, + nonce.value.pointer(), + key.value.pointer()); + if (rc == -1) { + return null; + } + if (rc != 0) { + throw new SodiumException( + "crypto_aead_xchacha20poly1305_ietf_decrypt_detached: failed with result " + rc); + } + + return clearText; + } + + private static final class SSDecrypt implements SecretDecryptionStream { + private final int abytes; + @Nullable private Pointer state; + private boolean complete = false; + + private SSDecrypt(Key key, byte[] header) { + if (key.isDestroyed()) { + throw new IllegalArgumentException("Key has been destroyed"); + } + if (header.length != Sodium.crypto_secretstream_xchacha20poly1305_headerbytes()) { + throw new IllegalArgumentException( + "header must be " + + Sodium.crypto_secretstream_xchacha20poly1305_headerbytes() + + " bytes, got " + + header.length); + } + + long abytes = Sodium.crypto_secretstream_xchacha20poly1305_abytes(); + if (abytes > Integer.MAX_VALUE) { + throw new IllegalStateException( + "crypto_aead_xchacha20poly1305_ietf_abytes: " + abytes + " is too large"); + } + this.abytes = (int) abytes; + + Pointer state = Sodium.malloc(Sodium.crypto_secretstream_xchacha20poly1305_statebytes()); + try { + int rc = + Sodium.crypto_secretstream_xchacha20poly1305_init_pull( + state, header, key.value.pointer()); + if (rc != 0) { + throw new SodiumException( + "crypto_secretstream_xchacha20poly1305_init_push: failed with result " + rc); + } + } catch (Throwable e) { + Sodium.sodium_free(state); + throw e; + } + this.state = state; + } + + @Override + public void destroy() { + if (state != null) { + Pointer p = state; + state = null; + Sodium.sodium_free(p); + } + } + + @Override + public boolean isDestroyed() { + return state == null; + } + + @Override + public byte[] pull(byte[] cipherText) { + if (complete) { + throw new IllegalStateException("stream already completed"); + } + if (state == null) { + throw new IllegalStateException("stream has been destroyed"); + } + if (abytes > cipherText.length) { + throw new IllegalArgumentException("cipherText is too short"); + } + byte[] clearText = new byte[cipherText.length - abytes]; + ByteByReference tag = new ByteByReference(); + int rc = + Sodium.crypto_secretstream_xchacha20poly1305_pull( + state, clearText, null, tag, cipherText, cipherText.length, null, 0); + if (rc != 0) { + throw new SodiumException( + "crypto_secretstream_xchacha20poly1305_push: failed with result " + rc); + } + if (tag.byteValue() == TAG_FINAL) { + complete = true; + // destroy state before finalization, as it will not be re-used + destroy(); + } + return clearText; + } + + @Override + public boolean isComplete() { + return complete; + } + } + + /** + * Open an decryption stream. + * + * @param key The key to use for decryption. + * @param header The header for the stream. + * @return The input stream. + * @throws UnsupportedOperationException If XChaCha20Poly1305 secret stream support is not + * available. + */ + public static SecretDecryptionStream openDecryptionStream(Key key, byte[] header) { + assertSecretStreamAvailable(); + return new SSDecrypt(key, header); + } + + private static byte[] maybeSliceResult( + byte[] bytes, LongLongByReference actualLength, String methodName) { + if (actualLength.longValue() == bytes.length) { + return bytes; + } + if (actualLength.longValue() > Integer.MAX_VALUE) { + throw new SodiumException( + methodName + ": result of length " + actualLength.longValue() + " is too large"); + } + byte[] result = new byte[actualLength.intValue()]; + System.arraycopy(bytes, 0, result, 0, result.length); + return result; + } +} diff --git a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/package-info.java b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/package-info.java new file mode 100644 index 000000000..871886603 --- /dev/null +++ b/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/package-info.java @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You 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. + * + *

Classes and utilities for working with the sodium native library. + * + *

Classes and utilities in this package provide an interface to the native Sodium crypto library + * (https://www.libsodium.org/), which must be installed on the same system as the JVM. It will be + * searched for in common library locations, or it can be loaded explicitly using {@link + * org.apache.tuweni.crypto.sodium.Sodium#searchLibrary(java.nio.file.Path...)} or {@link + * org.apache.tuweni.crypto.sodium.Sodium#loadLibrary(java.nio.file.Path)}. + * + *

Classes in this package also depend upon the JNR-FFI library being available on the classpath, + * along with its dependencies. See https://github.com/jnr/jnr-ffi. JNR-FFI can be included using + * the gradle dependency 'com.github.jnr:jnr-ffi'. + */ +@ParametersAreNonnullByDefault +package org.apache.tuweni.v2.crypto.sodium; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/HashTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/HashTest.java new file mode 100644 index 000000000..76f5fd73f --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/HashTest.java @@ -0,0 +1,166 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.*; + +import org.apache.tuweni.junit.BouncyCastleExtension; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.security.Provider; +import java.security.Security; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class HashTest { + + @Test + void sha2_256() { + String horseSha2 = "fd62862b6dc213bee77c2badd6311528253c6cb3107e03c16051aa15584eca1c"; + String cowSha2 = "beb134754910a4b4790c69ab17d3975221f4c534b70c8d6e82b30c165e8c0c09"; + + Bytes resultHorse = Hash.sha2_256(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseSha2), resultHorse); + + byte[] resultHorse2 = Hash.sha2_256("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseSha2).toArrayUnsafe(), resultHorse2); + + Bytes resultCow = Hash.sha2_256(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowSha2), resultCow); + + byte[] resultCow2 = Hash.sha2_256("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowSha2).toArrayUnsafe(), resultCow2); + } + + @Test + void sha2_256_withoutSodium() { + Hash.USE_SODIUM = false; + try { + String horseSha2 = "fd62862b6dc213bee77c2badd6311528253c6cb3107e03c16051aa15584eca1c"; + String cowSha2 = "beb134754910a4b4790c69ab17d3975221f4c534b70c8d6e82b30c165e8c0c09"; + + Bytes resultHorse = Hash.sha2_256(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseSha2), resultHorse); + + byte[] resultHorse2 = Hash.sha2_256("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseSha2).toArrayUnsafe(), resultHorse2); + + Bytes resultCow = Hash.sha2_256(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowSha2), resultCow); + + byte[] resultCow2 = Hash.sha2_256("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowSha2).toArrayUnsafe(), resultCow2); + } finally { + Hash.USE_SODIUM = true; + } + } + + @Test + void sha2_512_256() { + String horseSha2 = "6d64886cd066b81cf2dcf16ae70e97017d35f2f4ab73c5c5810aaa9ab573dab3"; + String cowSha2 = "7d26bad15e2f266cb4cbe9b1913978cb8a8bd08d92ee157b6be87c92dfce2d3e"; + + Bytes resultHorse = Hash.sha2_512_256(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseSha2), resultHorse); + + byte[] resultHorse2 = Hash.sha2_512_256("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseSha2).toArrayUnsafe(), resultHorse2); + + Bytes resultCow = Hash.sha2_512_256(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowSha2), resultCow); + + byte[] resultCow2 = Hash.sha2_512_256("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowSha2).toArrayUnsafe(), resultCow2); + } + + @Test + void keccak256() { + String horseKeccak256 = "c87f65ff3f271bf5dc8643484f66b200109caffe4bf98c4cb393dc35740b28c0"; + String cowKeccak256 = "c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4"; + + Bytes resultHorse = Hash.keccak256(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseKeccak256), resultHorse); + + byte[] resultHorse2 = Hash.keccak256("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseKeccak256).toArrayUnsafe(), resultHorse2); + + Bytes resultCow = Hash.keccak256(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowKeccak256), resultCow); + + byte[] resultCow2 = Hash.keccak256("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowKeccak256).toArrayUnsafe(), resultCow2); + } + + @Test + void sha3_256() { + String horseSha3 = "d8137088d21c7c0d69107cd51d1c32440a57aa5c59f73ed7310522ea491000ac"; + String cowSha3 = "fba26f1556b8c7b473d01e3eae218318f752e808407794fc0b6490988a33a82d"; + + Bytes resultHorse = Hash.sha3_256(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseSha3), resultHorse); + + byte[] resultHorse2 = Hash.sha3_256("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseSha3).toArrayUnsafe(), resultHorse2); + + Bytes resultCow = Hash.sha3_256(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowSha3), resultCow); + + byte[] resultCow2 = Hash.sha3_256("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowSha3).toArrayUnsafe(), resultCow2); + } + + @Test + void sha3_512() { + String horseSha3 = + "d78700def5dd85a9f5a1f8cce8614889e696d4dc82b17189e4974acc050659b49494f03cd0bfbb13a32132b4b4af5e16efd8b0643a5453c87e8e6dfb086b3568"; + String cowSha3 = + "14accdcf3380cd31674aa5edcd2a53f1b1dad3922eb335e89399321e17a8be5ea315b5346a4c45f6a2595b8e2e24bb345daeb97c7ddd2e970b9e53c9ae439f23"; + + Bytes resultHorse = Hash.sha3_512(Bytes.wrap("horse".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(horseSha3), resultHorse); + + byte[] resultHorse2 = Hash.sha3_512("horse".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(horseSha3).toArrayUnsafe(), resultHorse2); + + Bytes resultCow = Hash.sha3_512(Bytes.wrap("cow".getBytes(UTF_8))); + assertEquals(Bytes.fromHexString(cowSha3), resultCow); + + byte[] resultCow2 = Hash.sha3_512("cow".getBytes(UTF_8)); + assertArrayEquals(Bytes.fromHexString(cowSha3).toArrayUnsafe(), resultCow2); + } + + @Test + void testWithoutProviders() { + Provider[] providers = Security.getProviders(); + Stream.of(Security.getProviders()).map(Provider::getName).forEach(Security::removeProvider); + Hash.USE_SODIUM = false; + Hash.cachedDigests.get().clear(); + try { + assertThrows(IllegalStateException.class, () -> Hash.sha2_256("horse".getBytes(UTF_8))); + assertThrows( + IllegalStateException.class, () -> Hash.sha2_256(Bytes.wrap("horse".getBytes(UTF_8)))); + assertThrows(IllegalStateException.class, () -> Hash.sha3_256("horse".getBytes(UTF_8))); + assertThrows( + IllegalStateException.class, () -> Hash.sha3_256(Bytes.wrap("horse".getBytes(UTF_8)))); + assertThrows(IllegalStateException.class, () -> Hash.sha3_512("horse".getBytes(UTF_8))); + assertThrows( + IllegalStateException.class, () -> Hash.sha3_512(Bytes.wrap("horse".getBytes(UTF_8)))); + assertThrows(IllegalStateException.class, () -> Hash.keccak256("horse".getBytes(UTF_8))); + assertThrows( + IllegalStateException.class, () -> Hash.keccak256(Bytes.wrap("horse".getBytes(UTF_8)))); + assertThrows(IllegalStateException.class, () -> Hash.sha2_512_256("horse".getBytes(UTF_8))); + assertThrows( + IllegalStateException.class, + () -> Hash.sha2_512_256(Bytes.wrap("horse".getBytes(UTF_8)))); + } finally { + for (Provider p : providers) { + Security.addProvider(p); + } + Hash.USE_SODIUM = true; + } + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/SECP256K1Test.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/SECP256K1Test.java new file mode 100644 index 000000000..63cf879ba --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/SECP256K1Test.java @@ -0,0 +1,388 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; +import static org.junit.jupiter.api.Assertions.*; + +import org.apache.tuweni.crypto.InvalidSEC256K1SecretKeyStoreException; +import org.apache.tuweni.junit.BouncyCastleExtension; +import org.apache.tuweni.junit.TempDirectory; +import org.apache.tuweni.junit.TempDirectoryExtension; +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.MutableBytes; +import org.apache.tuweni.v2.crypto.SECP256K1.KeyPair; +import org.apache.tuweni.v2.crypto.SECP256K1.PublicKey; +import org.apache.tuweni.v2.crypto.SECP256K1.SecretKey; +import org.apache.tuweni.v2.crypto.SECP256K1.Signature; + +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +@ExtendWith(BouncyCastleExtension.class) +class SECP256K1Test { + + @Test + void testCreatePrivateKey_NullEncoding() { + assertThrows(NullPointerException.class, () -> SecretKey.fromBytes(null)); + } + + @Test + void testPrivateKeyEquals() { + SecretKey secretKey1 = SecretKey.fromInteger(BigInteger.TEN); + SecretKey secretKey2 = SecretKey.fromInteger(BigInteger.TEN); + assertEquals(secretKey1, secretKey2); + } + + @Test + void testPrivateHashCode() { + SecretKey secretKey = SecretKey.fromInteger(BigInteger.TEN); + assertNotEquals(0, secretKey.hashCode()); + } + + @Test + void testCreatePublicKey_NullEncoding() { + assertThrows(NullPointerException.class, () -> SECP256K1.PublicKey.fromBytes(null)); + } + + @Test + void testCreatePublicKey_EncodingTooShort() { + assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.PublicKey.fromBytes(Bytes.wrap(new byte[63]))); + } + + @Test + void testCreatePublicKey_EncodingTooLong() { + assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.PublicKey.fromBytes(Bytes.wrap(new byte[65]))); + } + + @Test + void testPublicKeyEquals() { + SECP256K1.PublicKey publicKey1 = + SECP256K1.PublicKey.fromBytes( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + SECP256K1.PublicKey publicKey2 = + SECP256K1.PublicKey.fromBytes( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + assertEquals(publicKey1, publicKey2); + } + + @Test + void testPublicHashCode() { + SECP256K1.PublicKey publicKey = + SECP256K1.PublicKey.fromBytes( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + assertNotEquals(0, publicKey.hashCode()); + } + + @Test + void testCreateKeyPair_PublicKeyNull() { + assertThrows( + NullPointerException.class, + () -> + SECP256K1.KeyPair.create(null, SECP256K1.PublicKey.fromBytes(MutableBytes.create(64)))); + } + + @Test + void testCreateKeyPair_PrivateKeyNull() { + assertThrows( + NullPointerException.class, + () -> SECP256K1.KeyPair.create(SecretKey.fromBytes(MutableBytes.create(32)), null)); + } + + @Test + void testKeyPairGeneration() { + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.random(); + assertNotNull(keyPair); + assertNotNull(keyPair.secretKey()); + assertNotNull(keyPair.publicKey()); + } + + @Test + void testKeyPairEquals() { + SecretKey secretKey1 = SecretKey.fromInteger(BigInteger.TEN); + SecretKey secretKey2 = SecretKey.fromInteger(BigInteger.TEN); + SECP256K1.PublicKey publicKey1 = + SECP256K1.PublicKey.fromBytes( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + SECP256K1.PublicKey publicKey2 = + SECP256K1.PublicKey.fromBytes( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + SECP256K1.KeyPair keyPair1 = SECP256K1.KeyPair.create(secretKey1, publicKey1); + SECP256K1.KeyPair keyPair2 = SECP256K1.KeyPair.create(secretKey2, publicKey2); + + assertEquals(keyPair1, keyPair2); + } + + @Test + void testKeyPairHashCode() { + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.random(); + assertNotEquals(0, keyPair.hashCode()); + } + + @Test + void testKeyPairGeneration_PublicKeyRecovery() { + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.random(); + assertEquals(keyPair.publicKey(), SECP256K1.PublicKey.fromSecretKey(keyPair.secretKey())); + } + + @Test + void testPublicKeyRecovery() { + SecretKey secretKey = SecretKey.fromInteger(BigInteger.TEN); + SECP256K1.PublicKey expectedPublicKey = + SECP256K1.PublicKey.fromBytes( + fromHexString( + "a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7893aba425419bc27a3b6c7e693a24c696f794c2ed877a1593cbee53b037368d7")); + + SECP256K1.PublicKey publicKey = SECP256K1.PublicKey.fromSecretKey(secretKey); + assertEquals(expectedPublicKey, publicKey); + } + + @Test + void testCreateSignature() { + SECP256K1.Signature signature = + new SECP256K1.Signature((byte) 0, BigInteger.ONE, BigInteger.TEN); + assertEquals(BigInteger.ONE, signature.r()); + assertEquals(BigInteger.TEN, signature.s()); + assertEquals((byte) 0, signature.v()); + } + + @Test + void testEncodeSignature() { + SECP256K1.Signature signature = + new SECP256K1.Signature((byte) 0, BigInteger.ONE, BigInteger.TEN); + assertEquals( + "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a00", + signature.bytes().toString()); + } + + @Test + void testCreateSignatureFromEncoding() { + SECP256K1.Signature signature = + SECP256K1.Signature.fromBytes( + fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000A01")); + assertEquals(BigInteger.ONE, signature.r()); + assertEquals(BigInteger.TEN, signature.s()); + assertEquals((byte) 1, signature.v()); + } + + @Test + void testCreateSignatureWithNullR() { + assertThrows( + NullPointerException.class, + () -> SECP256K1.Signature.create((byte) 1, null, BigInteger.ONE)); + } + + @Test + void testCreateSignatureWithNullS() { + assertThrows( + NullPointerException.class, + () -> SECP256K1.Signature.create((byte) 1, BigInteger.ONE, null)); + } + + @Test + void testCreateSignatureWithZeroR() { + Exception throwable = + assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.Signature.create((byte) 1, BigInteger.ZERO, BigInteger.ONE)); + assertEquals( + "Invalid r-value, should be >= 1 and < " + SECP256K1.Parameters.CURVE.getN() + ", got 0", + throwable.getMessage()); + } + + @Test + void testCreateSignatureWithZeroS() { + Exception throwable = + assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.Signature.create((byte) 1, BigInteger.ONE, BigInteger.ZERO)); + assertEquals( + "Invalid s-value, should be >= 1 and < " + SECP256K1.Parameters.CURVE.getN() + ", got 0", + throwable.getMessage()); + } + + @Test + void testCreateSignatureWithRHigherThanCurve() { + BigInteger curveN = SECP256K1.Parameters.CURVE.getN(); + Exception throwable = + assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.Signature.create((byte) 1, curveN.add(BigInteger.ONE), BigInteger.ONE)); + assertEquals( + "Invalid r-value, should be >= 1 and < " + curveN + ", got " + curveN.add(BigInteger.ONE), + throwable.getMessage()); + } + + @Test + void testCreateSignatureWithSHigherThanCurve() { + BigInteger curveN = SECP256K1.Parameters.CURVE.getN(); + Exception throwable = + assertThrows( + IllegalArgumentException.class, + () -> SECP256K1.Signature.create((byte) 1, BigInteger.ONE, curveN.add(BigInteger.ONE))); + assertEquals( + "Invalid s-value, should be >= 1 and < " + curveN + ", got " + curveN.add(BigInteger.ONE), + throwable.getMessage()); + } + + @Test + void testRecoverPublicKeyFromSignature() { + SecretKey secretKey = + SecretKey.fromInteger( + new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.fromSecretKey(secretKey); + + long seed = new Random().nextLong(); + Random random = new Random(seed); + for (int i = 0; i < 100; ++i) { + try { + byte[] data = new byte[20]; + random.nextBytes(data); + SECP256K1.Signature signature = SECP256K1.sign(data, keyPair); + + PublicKey recoveredPublicKey = SECP256K1.PublicKey.recoverFromSignature(data, signature); + assertNotNull(recoveredPublicKey); + assertEquals(keyPair.publicKey().toString(), recoveredPublicKey.toString()); + assertTrue(SECP256K1.verify(data, signature, recoveredPublicKey)); + } catch (AssertionError e) { + System.err.println("Random seed: " + seed); + throw e; + } + } + + Bytes hash = + Bytes.fromHexString("ACB1C19AC0832320815B5E886C6B73AD7D6177853D44B026F2A7A9E11BB899FC"); + SECP256K1.Signature signature = + SECP256K1.Signature.create( + (byte) 1, + new BigInteger( + "62380806879052346173879701944100777919767605075819957043497305774369260714318"), + new BigInteger( + "38020116821208196490118623452490256423459205241616519723877133146103446128360")); + assertNull(SECP256K1.PublicKey.recoverFromHashAndSignature(hash, signature)); + } + + @Test + void testCannotRecoverPublicKeyFromSignature() { + SECP256K1.Signature signature = + new Signature( + (byte) 0, + SECP256K1.Parameters.CURVE_ORDER.subtract(BigInteger.ONE), + BigInteger.valueOf(10)); + assertNull( + SECP256K1.PublicKey.recoverFromSignature( + Bytes.of("Random data".getBytes(UTF_8)), signature)); + } + + @Test + void testSignatureGeneration() { + SecretKey secretKey = + SecretKey.fromInteger( + new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.fromSecretKey(secretKey); + + Bytes data = Bytes.wrap("This is an example of a signed message.".getBytes(UTF_8)); + SECP256K1.Signature expectedSignature = + new SECP256K1.Signature( + (byte) 1, + new BigInteger("d2ce488f4da29e68f22cb05cac1b19b75df170a12b4ad1bdd4531b8e9115c6fb", 16), + new BigInteger("75c1fe50a95e8ccffcbb5482a1e42fbbdd6324131dfe75c3b3b7f9a7c721eccb", 16)); + + SECP256K1.Signature actualSignature = SECP256K1.sign(data, keyPair); + assertEquals(expectedSignature, actualSignature); + } + + @Test + void testSignatureVerification() { + SecretKey secretKey = + SecretKey.fromInteger( + new BigInteger("c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4", 16)); + SECP256K1.KeyPair keyPair = SECP256K1.KeyPair.fromSecretKey(secretKey); + + Bytes data = Bytes.wrap("This is an example of a signed message.".getBytes(UTF_8)); + + SECP256K1.Signature signature = SECP256K1.sign(data, keyPair); + assertTrue(SECP256K1.verify(data, signature, keyPair.publicKey())); + } + + @Test + void testFileContainsValidPrivateKey(@TempDirectory Path tempDir) throws Exception { + Path tempFile = tempDir.resolve("tempId"); + Files.write( + tempFile, + "000000000000000000000000000000000000000000000000000000000000000A".getBytes(UTF_8)); + SecretKey secretKey = SecretKey.load(tempFile); + assertEquals( + fromHexString("000000000000000000000000000000000000000000000000000000000000000A"), + secretKey.bytes()); + } + + @Test + void testReadWritePrivateKeyString(@TempDirectory Path tempDir) throws Exception { + SecretKey secretKey = SecretKey.fromInteger(BigInteger.TEN); + SECP256K1.KeyPair keyPair1 = SECP256K1.KeyPair.fromSecretKey(secretKey); + Path tempFile = tempDir.resolve("tempId"); + keyPair1.store(tempFile); + SECP256K1.KeyPair keyPair2 = SECP256K1.KeyPair.load(tempFile); + assertEquals(keyPair1, keyPair2); + } + + @Test + void testInvalidFileThrowsInvalidKeyPairException(@TempDirectory Path tempDir) throws Exception { + Path tempFile = tempDir.resolve("tempId"); + Files.write(tempFile, "not valid".getBytes(UTF_8)); + assertThrows(InvalidSEC256K1SecretKeyStoreException.class, () -> SecretKey.load(tempFile)); + } + + @Test + void testInvalidMultiLineFileThrowsInvalidIdException(@TempDirectory Path tempDir) + throws Exception { + Path tempFile = tempDir.resolve("tempId"); + Files.write(tempFile, "not\n\nvalid".getBytes(UTF_8)); + assertThrows(InvalidSEC256K1SecretKeyStoreException.class, () -> SecretKey.load(tempFile)); + } + + @Test + void testEncodedBytes() { + KeyPair kp = SECP256K1.KeyPair.random(); + Signature sig = SECP256K1.sign(Bytes.of(1, 2, 3), kp); + assertEquals(65, sig.bytes().size()); + assertTrue(sig.bytes().get(64) <= 3 && sig.bytes().get(64) >= 0); + } + + @Test + void testSharedSecretBytes() { + KeyPair kp = SECP256K1.KeyPair.random(); + KeyPair otherKP = SECP256K1.KeyPair.random(); + Bytes sharedSecret = SECP256K1.calculateKeyAgreement(kp.secretKey(), otherKP.publicKey()); + Bytes otherSharedSecret = SECP256K1.calculateKeyAgreement(otherKP.secretKey(), kp.publicKey()); + assertEquals(sharedSecret, otherSharedSecret); + } + + @Test + void encryptDecrypt() { + KeyPair kp = SECP256K1.KeyPair.random(); + Bytes encrypted = SECP256K1.encrypt(kp.publicKey(), Bytes.fromHexString("0xdeadbeef")); + Bytes decrypted = SECP256K1.decrypt(kp.secretKey(), encrypted); + assertEquals(Bytes.fromHexString("0xdeadbeef"), decrypted); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigestTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigestTest.java new file mode 100644 index 000000000..f746b3da7 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigestTest.java @@ -0,0 +1,124 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.blake2bf; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.bouncycastle.util.Pack; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test vectors adapted from + * https://github.com/keep-network/blake2b/blob/master/compression/f_test.go + */ +public class Blake2bfMessageDigestTest { + + private Blake2bfMessageDigest messageDigest; + + // output when input is all 0 + private static final Bytes BLAKE2F_ALL_ZERO = + Bytes.wrap( + new byte[] { + 8, -55, -68, -13, 103, -26, 9, 106, 59, -89, -54, -124, -123, -82, 103, -69, 43, -8, + -108, -2, 114, -13, 110, 60, -15, 54, 29, 95, 58, -11, 79, -91, -47, -126, -26, -83, + 127, 82, 14, 81, 31, 108, 62, 43, -116, 104, 5, -101, 107, -67, 65, -5, -85, -39, -125, + 31, 121, 33, 126, 19, 25, -51, -32, 91 + }); + + // output when input is all 0 for 4294967295 rounds + private static final Bytes BLAKE2F_ALL_ZERO_NEGATIVE_ROUNDS = + Bytes.wrap( + new byte[] { + -111, -99, -124, 115, 29, 109, 127, 118, 18, 21, 75, -89, 60, 35, 112, 81, 110, 78, -8, + 40, -102, 19, -73, -97, 57, 69, 69, -89, 83, 66, 124, -43, -92, 78, 115, 115, 117, 123, + -105, -25, 25, -74, -1, -94, -127, 14, 87, 123, -26, 84, -75, -82, -78, 54, 48, -125, + 38, -58, 7, -61, 120, -93, -42, -38 + }); + + @BeforeEach + public void setUp() { + messageDigest = new Blake2bfMessageDigest(); + } + + @Test + public void digestIfUpdatedCorrectlyWithBytes() { + for (int i = 0; i < 213; i++) { + messageDigest.update((byte) 0); + } + assertEquals(BLAKE2F_ALL_ZERO, Bytes.wrap(messageDigest.digest())); + } + + @Test + public void digestIfUpdatedCorrectlyWithByteArray() { + final byte[] update = new byte[213]; + messageDigest.update(update, 0, 213); + assertEquals(BLAKE2F_ALL_ZERO, Bytes.wrap(messageDigest.digest())); + } + + @Test + public void digestIfUpdatedCorrectlyMixed() { + final byte[] update = new byte[213]; + messageDigest.update((byte) 0); + messageDigest.update(update, 2, 211); + messageDigest.update((byte) 0); + assertEquals(BLAKE2F_ALL_ZERO, Bytes.wrap(messageDigest.digest())); + } + + @Test + public void digestWithMaxRounds() { + // equal to unsigned int max value (4294967295, or signed -1) + final byte[] rounds = Pack.intToBigEndian(Integer.MIN_VALUE); + messageDigest.update(rounds, 0, 4); + messageDigest.update(new byte[213], 0, 209); + assertEquals(BLAKE2F_ALL_ZERO_NEGATIVE_ROUNDS, Bytes.wrap(messageDigest.digest())); + } + + @Test + public void throwsIfBufferUpdatedWithLessThat213Bytes() { + for (int i = 0; i < 212; i++) { + messageDigest.update((byte) 0); + } + assertThrows( + IllegalStateException.class, + () -> { + messageDigest.digest(); + }); + } + + @Test + public void throwsIfBufferUpdatedWithMoreThat213Bytes() { + for (int i = 0; i < 213; i++) { + messageDigest.update((byte) 0); + } + assertThrows( + IllegalArgumentException.class, + () -> { + messageDigest.update((byte) 0); + }); + } + + @Test + public void throwsIfBufferUpdatedLargeByteArray() { + final byte[] update = new byte[213]; + messageDigest.update((byte) 0); + assertThrows( + IllegalArgumentException.class, + () -> { + messageDigest.update(update, 0, 213); + }); + } + + @Test + public void throwsIfEmptyBufferUpdatedLargeByteArray() { + final byte[] update = new byte[214]; + assertThrows( + IllegalArgumentException.class, + () -> { + messageDigest.update(update, 0, 214); + }); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/GTPointTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/GTPointTest.java new file mode 100644 index 000000000..61689fc73 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/GTPointTest.java @@ -0,0 +1,23 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.milagro.amcl.BLS381.FP12; +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.Test; + +class GTPointTest { + + @Test + void equalsAndHashcode() { + FP12 fp12 = FP12.fromBytes(Bytes.random(576).toArrayUnsafe()); + GTPoint point = new GTPoint(fp12); + assertEquals(point, point); + assertEquals(point.hashCode(), point.hashCode()); + assertEquals(new GTPoint(fp12), point); + assertEquals(new GTPoint(fp12).hashCode(), point.hashCode()); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/SignatureTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/SignatureTest.java new file mode 100644 index 000000000..67cd2ee1c --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/SignatureTest.java @@ -0,0 +1,164 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.mikuli; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class SignatureTest { + + @Test + void testNoSigs() { + assertThrows( + IllegalArgumentException.class, + () -> { + Signature.aggregate(Collections.emptyList()); + }); + } + + @Test + void testNoSigsAndPubKeys() { + assertThrows( + IllegalArgumentException.class, + () -> { + SignatureAndPublicKey.aggregate(Collections.emptyList()); + }); + } + + @Test + void testSimpleSignature() { + KeyPair keyPair = KeyPair.random(); + byte[] message = "Hello".getBytes(UTF_8); + SignatureAndPublicKey sigAndPubKey = BLS12381.sign(keyPair, message, 48); + + Boolean isValid = + BLS12381.verify(sigAndPubKey.publicKey(), sigAndPubKey.signature(), message, 48); + assertTrue(isValid); + } + + @Test + void testAggregatedSignature() { + byte[] message = "Hello".getBytes(UTF_8); + List sigs = getSignaturesAndPublicKeys(message); + SignatureAndPublicKey sigAndPubKey = SignatureAndPublicKey.aggregate(sigs); + + Boolean isValid = BLS12381.verify(sigAndPubKey, message, 48); + assertTrue(isValid); + } + + @Test + void testCorruptedMessage() { + byte[] message = "Hello".getBytes(UTF_8); + List sigs = getSignaturesAndPublicKeys(message); + SignatureAndPublicKey sigAndPubKey = SignatureAndPublicKey.aggregate(sigs); + byte[] corruptedMessage = "Not Hello".getBytes(UTF_8); + + Boolean isValid = BLS12381.verify(sigAndPubKey, corruptedMessage, 48); + assertFalse(isValid); + } + + @Test + void testCorruptedSignature() { + byte[] message = "Hello".getBytes(UTF_8); + List sigs = getSignaturesAndPublicKeys(message); + KeyPair keyPair = KeyPair.random(); + byte[] notHello = "Not Hello".getBytes(UTF_8); + + SignatureAndPublicKey additionalSignature = BLS12381.sign(keyPair, notHello, 48); + sigs.add(additionalSignature); + + SignatureAndPublicKey sigAndPubKey = SignatureAndPublicKey.aggregate(sigs); + + Boolean isValid = BLS12381.verify(sigAndPubKey, message, 48); + assertFalse(isValid); + } + + @Test + void testCorruptedSignatureWithoutPubKeys() { + byte[] message = "Hello".getBytes(UTF_8); + List sigs = getSignatures(message); + KeyPair keyPair = KeyPair.random(); + byte[] notHello = "Not Hello".getBytes(UTF_8); + + SignatureAndPublicKey additionalSignature = BLS12381.sign(keyPair, notHello, 48); + sigs.add(additionalSignature.signature()); + + Signature sig = Signature.aggregate(sigs); + + Boolean isValid = BLS12381.verify(additionalSignature.publicKey(), sig, message, 48); + assertFalse(isValid); + } + + @Test + void testSerialization() { + KeyPair keyPair = KeyPair.random(); + byte[] message = "Hello".getBytes(UTF_8); + Signature signature = BLS12381.sign(keyPair, message, 48).signature(); + + Bytes sigTobytes = signature.encode(); + Signature sigFromBytes = Signature.decode(sigTobytes); + + assertEquals(signature, sigFromBytes); + assertEquals(signature.hashCode(), sigFromBytes.hashCode()); + + PublicKey pubKey = keyPair.publicKey(); + byte[] pubKeyTobytes = pubKey.toByteArray(); + PublicKey pubKeyFromBytes = PublicKey.fromBytes(pubKeyTobytes); + + assertEquals(pubKey, pubKeyFromBytes); + assertEquals(pubKey.hashCode(), pubKeyFromBytes.hashCode()); + } + + List getSignatures(byte[] message) { + KeyPair keyPair1 = KeyPair.random(); + KeyPair keyPair2 = KeyPair.random(); + KeyPair keyPair3 = KeyPair.random(); + + Signature sigAndPubKey1 = BLS12381.sign(keyPair1, message, 48).signature(); + Signature sigAndPubKey2 = BLS12381.sign(keyPair2, message, 48).signature(); + Signature sigAndPubKey3 = BLS12381.sign(keyPair3, message, 48).signature(); + + List sigs = new ArrayList<>(); + sigs.add(sigAndPubKey1); + sigs.add(sigAndPubKey2); + sigs.add(sigAndPubKey3); + + return sigs; + } + + List getSignaturesAndPublicKeys(byte[] message) { + KeyPair keyPair1 = KeyPair.random(); + KeyPair keyPair2 = KeyPair.random(); + KeyPair keyPair3 = KeyPair.random(); + + SignatureAndPublicKey sigAndPubKey1 = BLS12381.sign(keyPair1, message, 48); + SignatureAndPublicKey sigAndPubKey2 = BLS12381.sign(keyPair2, message, 48); + SignatureAndPublicKey sigAndPubKey3 = BLS12381.sign(keyPair3, message, 48); + + List sigs = new ArrayList(); + sigs.add(sigAndPubKey1); + sigs.add(sigAndPubKey2); + sigs.add(sigAndPubKey3); + + return sigs; + } + + @Test + void secretKeyRoundtrip() { + KeyPair kp = KeyPair.random(); + SecretKey key = kp.secretKey(); + Bytes bytes = key.toBytes(); + assertEquals(key, SecretKey.fromBytes(bytes)); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/AllocatedTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/AllocatedTest.java new file mode 100644 index 000000000..8ca740765 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/AllocatedTest.java @@ -0,0 +1,37 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class AllocatedTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void destroyedValue() { + Allocated allocated = Allocated.allocate(32); + assertEquals(32, allocated.length()); + allocated.destroy(); + assertTrue(allocated.isDestroyed()); + assertThrows(IllegalStateException.class, () -> allocated.equals(Allocated.allocate(3))); + assertThrows(IllegalStateException.class, () -> allocated.hashCode()); + } + + @Test + void allocateBytes() { + Allocated value = Allocated.fromBytes(Bytes.fromHexString("deadbeef")); + assertEquals(Bytes.fromHexString("deadbeef"), value.bytes()); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/BoxTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/BoxTest.java new file mode 100644 index 000000000..fdd478318 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/BoxTest.java @@ -0,0 +1,272 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes32; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BoxTest { + + private static Box.Seed seed; + private static Box.Nonce nonce; + + @BeforeAll + static void setup() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + nonce = Box.Nonce.random(); + // @formatter:off + seed = + Box.Seed.fromBytes( + new byte[] { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f + }); + // @formatter:on + } + + @BeforeEach + void incrementNonce() { + nonce = nonce.increment(); + } + + @Test + void badBytes() { + assertThrows(IllegalArgumentException.class, () -> Box.PublicKey.fromBytes(Bytes.random(20))); + } + + @Test + void testObjectEquality() { + Box.PublicKey pk = Box.PublicKey.fromBytes(Bytes32.fromRandom()); + assertEquals(pk, pk); + Box.PublicKey pk2 = Box.PublicKey.fromBytes(Bytes32.fromRandom()); + assertNotEquals(pk, pk2); + assertEquals(pk.hashCode(), pk.hashCode()); + assertNotEquals(pk.hashCode(), pk2.hashCode()); + } + + @Test + void testObjectEqualityNonce() { + Box.Nonce pk = Box.Nonce.fromBytes(Bytes.random(24)); + assertEquals(pk, pk); + Box.Nonce pk2 = Box.Nonce.fromBytes(Bytes.random(24)); + assertNotEquals(pk, pk2); + assertEquals(pk.hashCode(), pk.hashCode()); + assertNotEquals(pk.hashCode(), pk2.hashCode()); + } + + @Test + void toBytes() { + Bytes value = Bytes32.fromRandom(); + Box.PublicKey pk = Box.PublicKey.fromBytes(value); + assertEquals(value, pk.bytes()); + assertArrayEquals(value.toArrayUnsafe(), pk.bytesArray()); + } + + @Test + void encryptDecryptSealed() { + Box.KeyPair receiver = Box.KeyPair.random(); + Bytes encrypted = Box.encryptSealed(Bytes.fromHexString("deadbeef"), receiver.publicKey()); + Bytes decrypted = Box.decryptSealed(encrypted, receiver.publicKey(), receiver.secretKey()); + assertEquals(Bytes.fromHexString("deadbeef"), decrypted); + } + + @Test + void encryptDecryptDetached() { + Box.KeyPair sender = Box.KeyPair.random(); + Box.KeyPair receiver = Box.KeyPair.random(); + Box.Nonce nonce = Box.Nonce.zero(); + DetachedEncryptionResult encrypted = + Box.encryptDetached( + Bytes.fromHexString("deadbeef"), receiver.publicKey(), sender.secretKey(), nonce); + Bytes decrypted = + Box.decryptDetached( + encrypted.cipherText(), + encrypted.mac(), + sender.publicKey(), + receiver.secretKey(), + nonce); + assertEquals(Bytes.fromHexString("deadbeef"), decrypted); + } + + @Test + void checkCombinedEncryptDecrypt() { + Box.KeyPair aliceKeyPair = Box.KeyPair.random(); + Box.KeyPair bobKeyPair = Box.KeyPair.fromSeed(seed); + + byte[] message = "This is a test message".getBytes(UTF_8); + + byte[] cipherText = + Box.encrypt(message, aliceKeyPair.publicKey(), bobKeyPair.secretKey(), nonce); + byte[] clearText = + Box.decrypt(cipherText, bobKeyPair.publicKey(), aliceKeyPair.secretKey(), nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + clearText = + Box.decrypt( + cipherText, bobKeyPair.publicKey(), aliceKeyPair.secretKey(), nonce.increment()); + assertNull(clearText); + + Box.KeyPair otherKeyPair = Box.KeyPair.random(); + clearText = Box.decrypt(cipherText, otherKeyPair.publicKey(), bobKeyPair.secretKey(), nonce); + assertNull(clearText); + } + + @Test + void checkCombinedPrecomputedEncryptDecrypt() { + Box.KeyPair aliceKeyPair = Box.KeyPair.random(); + Box.KeyPair bobKeyPair = Box.KeyPair.random(); + + byte[] message = "This is a test message".getBytes(UTF_8); + byte[] cipherText; + + try (Box precomputed = Box.forKeys(aliceKeyPair.publicKey(), bobKeyPair.secretKey())) { + cipherText = precomputed.encrypt(message, nonce); + } + + byte[] clearText = + Box.decrypt(cipherText, bobKeyPair.publicKey(), aliceKeyPair.secretKey(), nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + try (Box precomputed = Box.forKeys(bobKeyPair.publicKey(), aliceKeyPair.secretKey())) { + clearText = precomputed.decrypt(cipherText, nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + assertNull(precomputed.decrypt(cipherText, nonce.increment())); + } + + Box.KeyPair otherKeyPair = Box.KeyPair.random(); + try (Box precomputed = Box.forKeys(otherKeyPair.publicKey(), bobKeyPair.secretKey())) { + assertNull(precomputed.decrypt(cipherText, nonce)); + } + } + + @Test + void checkDetachedEncryptDecrypt() { + Box.KeyPair aliceKeyPair = Box.KeyPair.random(); + Box.KeyPair bobKeyPair = Box.KeyPair.random(); + + byte[] message = "This is a test message".getBytes(UTF_8); + + DetachedEncryptionResult result = + Box.encryptDetached(message, aliceKeyPair.publicKey(), bobKeyPair.secretKey(), nonce); + byte[] clearText = + Box.decryptDetached( + result.cipherTextArray(), + result.macArray(), + bobKeyPair.publicKey(), + aliceKeyPair.secretKey(), + nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + clearText = + Box.decryptDetached( + result.cipherTextArray(), + result.macArray(), + bobKeyPair.publicKey(), + aliceKeyPair.secretKey(), + nonce.increment()); + assertNull(clearText); + + Box.KeyPair otherKeyPair = Box.KeyPair.random(); + clearText = + Box.decryptDetached( + result.cipherTextArray(), + result.macArray(), + otherKeyPair.publicKey(), + bobKeyPair.secretKey(), + nonce); + assertNull(clearText); + } + + @Test + void checkDetachedPrecomputedEncryptDecrypt() { + Box.KeyPair aliceKeyPair = Box.KeyPair.random(); + Box.KeyPair bobKeyPair = Box.KeyPair.random(); + + byte[] message = "This is a test message".getBytes(UTF_8); + DetachedEncryptionResult result; + + try (Box precomputed = Box.forKeys(aliceKeyPair.publicKey(), bobKeyPair.secretKey())) { + result = precomputed.encryptDetached(message, nonce); + } + + byte[] clearText = + Box.decryptDetached( + result.cipherTextArray(), + result.macArray(), + bobKeyPair.publicKey(), + aliceKeyPair.secretKey(), + nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + try (Box precomputed = Box.forKeys(bobKeyPair.publicKey(), aliceKeyPair.secretKey())) { + clearText = precomputed.decryptDetached(result.cipherTextArray(), result.macArray(), nonce); + + assertNotNull(clearText); + assertArrayEquals(message, clearText); + + assertNull( + precomputed.decryptDetached( + result.cipherTextArray(), result.macArray(), nonce.increment())); + } + + Box.KeyPair otherKeyPair = Box.KeyPair.random(); + try (Box precomputed = Box.forKeys(otherKeyPair.publicKey(), bobKeyPair.secretKey())) { + assertNull(precomputed.decryptDetached(result.cipherTextArray(), result.macArray(), nonce)); + } + } + + @Test + void checkBoxKeyPairForSignatureKeyPair() { + Signature.KeyPair signKeyPair = Signature.KeyPair.random(); + Box.KeyPair boxKeyPair = Box.KeyPair.forSignatureKeyPair(signKeyPair); + assertNotNull(boxKeyPair); + } + + @Test + void checkBoxKeysForSignatureKeys() { + Signature.KeyPair keyPair = Signature.KeyPair.random(); + Box.PublicKey boxPubKey = Box.PublicKey.forSignaturePublicKey(keyPair.publicKey()); + Box.SecretKey boxSecretKey = Box.SecretKey.forSignatureSecretKey(keyPair.secretKey()); + assertEquals(boxPubKey, Box.KeyPair.forSecretKey(boxSecretKey).publicKey()); + + Box.KeyPair boxKeyPair = Box.KeyPair.forSignatureKeyPair(keyPair); + assertEquals(boxKeyPair, Box.KeyPair.forSecretKey(boxSecretKey)); + } + + @Test + void testDestroyPublicKey() { + Box.KeyPair keyPair = Box.KeyPair.random(); + Box.PublicKey boxPubKey = Box.PublicKey.fromBytes(keyPair.publicKey().bytes()); + boxPubKey.destroy(); + assertTrue(boxPubKey.isDestroyed()); + assertFalse(keyPair.publicKey().isDestroyed()); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/ConcatenateTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/ConcatenateTest.java new file mode 100644 index 000000000..b31817eb5 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/ConcatenateTest.java @@ -0,0 +1,33 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes32; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class ConcatenateTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void testConcatenateTwoValues() { + Concatenate concatenate = new Concatenate(); + Bytes random = Bytes32.fromRandom(); + + concatenate.add(Signature.PublicKey.fromBytes(random)); + concatenate.add(Signature.PublicKey.fromBytes(random)); + + Allocated result = concatenate.concatenate(); + + assertEquals(Bytes.wrap(random, random), result.bytes()); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelmanTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelmanTest.java new file mode 100644 index 000000000..c7adea397 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelmanTest.java @@ -0,0 +1,97 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes32; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class DiffieHelmanTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void testScalarMultiplication() { + DiffieHelman.KeyPair keyPair = DiffieHelman.KeyPair.random(); + DiffieHelman.KeyPair secondKeyPair = DiffieHelman.KeyPair.random(); + + DiffieHelman.Secret scalar1 = + DiffieHelman.Secret.forKeys(keyPair.secretKey(), secondKeyPair.publicKey()); + DiffieHelman.Secret scalar2 = + DiffieHelman.Secret.forKeys(secondKeyPair.secretKey(), keyPair.publicKey()); + + assertEquals(scalar1, scalar2); + } + + @Test + void testEquals() { + DiffieHelman.KeyPair keyPair = DiffieHelman.KeyPair.random(); + DiffieHelman.KeyPair keyPair2 = DiffieHelman.KeyPair.forSecretKey(keyPair.secretKey()); + assertEquals(keyPair, keyPair2); + assertEquals(keyPair.hashCode(), keyPair2.hashCode()); + } + + @Test + void testEqualsSecretKey() { + DiffieHelman.KeyPair keyPair = DiffieHelman.KeyPair.random(); + DiffieHelman.KeyPair keyPair2 = DiffieHelman.KeyPair.forSecretKey(keyPair.secretKey()); + assertEquals(keyPair.secretKey(), keyPair2.secretKey()); + assertEquals(keyPair.hashCode(), keyPair2.hashCode()); + } + + @Test + void testEqualsPublicKey() { + DiffieHelman.KeyPair keyPair = DiffieHelman.KeyPair.random(); + DiffieHelman.KeyPair keyPair2 = DiffieHelman.KeyPair.forSecretKey(keyPair.secretKey()); + assertEquals(keyPair.publicKey(), keyPair2.publicKey()); + assertEquals(keyPair.hashCode(), keyPair2.hashCode()); + } + + @Test + void testDestroy() { + DiffieHelman.KeyPair keyPair = DiffieHelman.KeyPair.random(); + keyPair.secretKey().destroy(); + assertTrue(keyPair.secretKey().isDestroyed()); + } + + @Test + void testFromBoxPubKey() { + Bytes bytes = Bytes32.fromRandom(); + Box.PublicKey pkey = Box.PublicKey.fromBytes(bytes); + DiffieHelman.PublicKey dpk = DiffieHelman.PublicKey.forBoxPublicKey(pkey); + assertEquals(bytes, dpk.bytes()); + assertArrayEquals(bytes.toArrayUnsafe(), dpk.bytesArray()); + } + + @Test + void testEqualsPublicKeyFromBytes() { + Bytes bytes = Bytes32.fromRandom(); + DiffieHelman.PublicKey pkey = DiffieHelman.PublicKey.fromBytes(bytes); + DiffieHelman.PublicKey pkey2 = DiffieHelman.PublicKey.fromBytes(bytes); + assertEquals(pkey, pkey2); + assertEquals(pkey.hashCode(), pkey2.hashCode()); + } + + @Test + void testInvalidBytes() { + Bytes bytes = Bytes.random(20); + assertThrows(IllegalArgumentException.class, () -> DiffieHelman.PublicKey.fromBytes(bytes)); + } + + @Test + void testInvalidBytesSecretKey() { + Bytes bytes = Bytes.random(20); + assertThrows(IllegalArgumentException.class, () -> DiffieHelman.SecretKey.fromBytes(bytes)); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/GenericHashTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/GenericHashTest.java new file mode 100644 index 000000000..15ec4d3f9 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/GenericHashTest.java @@ -0,0 +1,39 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes32; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class GenericHashTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void hashValue() { + GenericHash.Hash output = GenericHash.hash(64, GenericHash.Input.fromBytes(Bytes.random(384))); + assertNotNull(output); + assertEquals(64, output.bytes().size()); + } + + @Test + void hashWithKeyValue() { + GenericHash.Hash output = + GenericHash.hash( + 64, + GenericHash.Input.fromBytes(Bytes.random(384)), + GenericHash.Key.fromBytes(Bytes32.fromRandom())); + assertNotNull(output); + assertEquals(64, output.bytes().size()); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256Test.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256Test.java new file mode 100644 index 000000000..0c4d5ae87 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256Test.java @@ -0,0 +1,49 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class HMACSHA256Test { + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void testHmacsha256() { + HMACSHA256.Key key = HMACSHA256.Key.random(); + Bytes authenticator = HMACSHA256.authenticate(Bytes.fromHexString("deadbeef"), key); + assertTrue(HMACSHA256.verify(authenticator, Bytes.fromHexString("deadbeef"), key)); + } + + @Test + void testHmacsha256InvalidAuthenticator() { + HMACSHA256.Key key = HMACSHA256.Key.random(); + Bytes authenticator = HMACSHA256.authenticate(Bytes.fromHexString("deadbeef"), key); + assertThrows( + IllegalArgumentException.class, + () -> + HMACSHA256.verify( + Bytes.wrap(authenticator, Bytes.of(1, 2, 3)), + Bytes.fromHexString("deadbeef"), + key)); + } + + @Test + void testHmacsha512NoMatch() { + HMACSHA256.Key key = HMACSHA256.Key.random(); + Bytes authenticator = HMACSHA256.authenticate(Bytes.fromHexString("deadbeef"), key); + assertFalse( + HMACSHA256.verify( + authenticator.mutableCopy().reverse(), Bytes.fromHexString("deadbeef"), key)); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256Test.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256Test.java new file mode 100644 index 000000000..275ee5cb8 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256Test.java @@ -0,0 +1,49 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class HMACSHA512256Test { + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void testHmacsha512256() { + HMACSHA512256.Key key = HMACSHA512256.Key.random(); + Bytes authenticator = HMACSHA512256.authenticate(Bytes.fromHexString("deadbeef"), key); + assertTrue(HMACSHA512256.verify(authenticator, Bytes.fromHexString("deadbeef"), key)); + } + + @Test + void testHmacsha512256InvalidAuthenticator() { + HMACSHA512256.Key key = HMACSHA512256.Key.random(); + Bytes authenticator = HMACSHA512256.authenticate(Bytes.fromHexString("deadbeef"), key); + assertThrows( + IllegalArgumentException.class, + () -> + HMACSHA512256.verify( + Bytes.wrap(authenticator, Bytes.of(1, 2, 3)), + Bytes.fromHexString("deadbeef"), + key)); + } + + @Test + void testHmacsha512256NoMatch() { + HMACSHA512256.Key key = HMACSHA512256.Key.random(); + Bytes authenticator = HMACSHA512256.authenticate(Bytes.fromHexString("deadbeef"), key); + assertFalse( + HMACSHA512256.verify( + authenticator.mutableCopy().reverse(), Bytes.fromHexString("deadbeef"), key)); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512Test.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512Test.java new file mode 100644 index 000000000..272a64600 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512Test.java @@ -0,0 +1,49 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class HMACSHA512Test { + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void testHmacsha512() { + HMACSHA512.Key key = HMACSHA512.Key.random(); + Bytes authenticator = HMACSHA512.authenticate(Bytes.fromHexString("deadbeef"), key); + assertTrue(HMACSHA512.verify(authenticator, Bytes.fromHexString("deadbeef"), key)); + } + + @Test + void testHmacsha512InvalidAuthenticator() { + HMACSHA512.Key key = HMACSHA512.Key.random(); + Bytes authenticator = HMACSHA512.authenticate(Bytes.fromHexString("deadbeef"), key); + assertThrows( + IllegalArgumentException.class, + () -> + HMACSHA512.verify( + Bytes.wrap(authenticator, Bytes.of(1, 2, 3)), + Bytes.fromHexString("deadbeef"), + key)); + } + + @Test + void testHmacsha512NoMatch() { + HMACSHA512.Key key = HMACSHA512.Key.random(); + Bytes authenticator = HMACSHA512.authenticate(Bytes.fromHexString("deadbeef"), key); + assertFalse( + HMACSHA512.verify( + authenticator.mutableCopy().reverse(), Bytes.fromHexString("deadbeef"), key)); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivationTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivationTest.java new file mode 100644 index 000000000..840b3ffc2 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivationTest.java @@ -0,0 +1,34 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.crypto.sodium.KeyDerivation.MasterKey; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class KeyDerivationTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + assumeTrue( + KeyDerivation.isAvailable(), "KeyDerivation support is not available (requires >= 10.0.12"); + } + + @Test + void differentIdsShouldGenerateDifferentKeys() { + MasterKey masterKey = MasterKey.random(); + + Bytes subKey1 = masterKey.deriveKey(40, 1, "abcdefg"); + assertEquals(subKey1, masterKey.deriveKey(40, 1, "abcdefg")); + + assertNotEquals(subKey1, masterKey.deriveKey(40, 2, "abcdefg")); + assertNotEquals(subKey1, masterKey.deriveKey(40, 1, new byte[KeyDerivation.contextLength()])); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/PasswordHashTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/PasswordHashTest.java new file mode 100644 index 000000000..a944a09ee --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/PasswordHashTest.java @@ -0,0 +1,119 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.crypto.sodium.PasswordHash.Algorithm; +import org.apache.tuweni.v2.crypto.sodium.PasswordHash.Salt; +import org.apache.tuweni.v2.crypto.sodium.PasswordHash.VerificationResult; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class PasswordHashTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void shouldGenerateSameKeyForSameParameters() { + String password = "A very insecure password"; + Salt salt = Salt.random(); + + Bytes hash = + PasswordHash.hash( + password, + 20, + salt, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + Algorithm.recommended()); + assertEquals(20, hash.size()); + + Bytes generated = + PasswordHash.hash( + password, + 20, + salt, + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + Algorithm.recommended()); + assertEquals(hash, generated); + + generated = + PasswordHash.hash( + password, + 20, + Salt.random(), + PasswordHash.interactiveOpsLimit(), + PasswordHash.interactiveMemLimit(), + Algorithm.recommended()); + assertNotEquals(hash, generated); + + generated = + PasswordHash.hash( + password, + 20, + salt, + PasswordHash.moderateOpsLimit(), + PasswordHash.interactiveMemLimit(), + Algorithm.recommended()); + assertNotEquals(hash, generated); + } + + @Test + void shouldThrowForLowOpsLimitWithArgon2i() { + assertThrows( + IllegalArgumentException.class, + () -> { + PasswordHash.hash( + "A very insecure password", + 20, + Salt.random(), + 1, + PasswordHash.moderateMemLimit(), + Algorithm.argon2i13()); + }); + } + + @Test + void checkHashAndVerify() { + assumeTrue( + Sodium.supportsVersion(Sodium.VERSION_10_0_14), + "Requires sodium native library >= 10.0.14"); + String password = "A very insecure password"; + + String hash = PasswordHash.hashInteractive(password); + assertTrue(PasswordHash.verify(hash, password)); + VerificationResult result = PasswordHash.checkHashForInteractive(hash, password); + assertEquals(VerificationResult.PASSED, result); + assertTrue(result.passed()); + + assertFalse(PasswordHash.verify(hash, "Bad password")); + result = PasswordHash.checkHashForInteractive(hash, "Bad password"); + assertEquals(VerificationResult.FAILED, result); + assertFalse(result.passed()); + } + + @Test + void checkHashAndVerifyNeedingRehash() { + assumeTrue( + Sodium.supportsVersion(Sodium.VERSION_10_0_14), + "Requires sodium native library >= 10.0.14"); + String password = "A very insecure password"; + String hash = PasswordHash.hashInteractive(password); + assertTrue(PasswordHash.needsRehash(hash)); + VerificationResult result = PasswordHash.checkHash(hash, password); + assertEquals(VerificationResult.NEEDS_REHASH, result); + assertTrue(result.passed()); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SHA256HashTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SHA256HashTest.java new file mode 100644 index 000000000..02a5efa83 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SHA256HashTest.java @@ -0,0 +1,66 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.junit.BouncyCastleExtension; +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.crypto.Hash; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class SHA256HashTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void hashValue() { + SHA256Hash.Hash output = SHA256Hash.hash(SHA256Hash.Input.fromBytes(Bytes.random(384))); + assertNotNull(output); + assertEquals(32, output.bytes().size()); + assertFalse(output.isDestroyed()); + output.destroy(); + assertTrue(output.isDestroyed()); + } + + @Test + void inputValueEquals() { + SHA256Hash.Input input = SHA256Hash.Input.fromBytes(Bytes.random(384)); + assertEquals(input, input); + assertEquals(input.hashCode(), input.hashCode()); + assertEquals(input, SHA256Hash.Input.fromBytes(input.bytes())); + assertEquals(input.hashCode(), SHA256Hash.Input.fromBytes(input.bytes()).hashCode()); + assertFalse(input.isDestroyed()); + input.destroy(); + assertTrue(input.isDestroyed()); + } + + @Test + void outputEquals() { + SHA256Hash.Input input = SHA256Hash.Input.fromBytes(Bytes.random(384)); + SHA256Hash.Hash output = SHA256Hash.hash(input); + assertEquals(output, output); + assertEquals(output.hashCode(), output.hashCode()); + assertEquals(output, SHA256Hash.hash(input)); + assertEquals(output.hashCode(), SHA256Hash.hash(input).hashCode()); + } + + @Test + void testCompat() { + Bytes toHash = Bytes.random(384); + SHA256Hash.Input input = SHA256Hash.Input.fromBytes(toHash); + SHA256Hash.Hash output = SHA256Hash.hash(input); + assertEquals(Hash.sha2_256(toHash), output.bytes()); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStreamTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStreamTest.java new file mode 100644 index 000000000..128f3e7fa --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStreamTest.java @@ -0,0 +1,29 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.Test; + +class SecretDecryptionStreamTest { + + @Test + void testBytesPull() { + SecretDecryptionStream stream = + new SecretDecryptionStream() { + @Override + public byte[] pull(byte[] cipherText) { + return Bytes.fromHexString("deadbeef").toArrayUnsafe(); + } + + @Override + public boolean isComplete() { + return false; + } + }; + assertEquals(Bytes.fromHexString("deadbeef"), stream.pull(Bytes.EMPTY)); + } +} diff --git a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SignatureTest.java b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SignatureTest.java new file mode 100644 index 000000000..ca4febd89 --- /dev/null +++ b/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SignatureTest.java @@ -0,0 +1,54 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.crypto.sodium; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class SignatureTest { + + @BeforeAll + static void checkAvailable() { + assumeTrue(Sodium.isAvailable(), "Sodium native library is not available"); + } + + @Test + void testEqualityAndRecovery() { + Signature.KeyPair kp = Signature.KeyPair.random(); + Signature.KeyPair otherKp = Signature.KeyPair.forSecretKey(kp.secretKey()); + assertEquals(kp, otherKp); + } + + @Test + void checkDetachedSignVerify() { + Signature.KeyPair kp = Signature.KeyPair.random(); + Bytes signature = Signature.signDetached(Bytes.fromHexString("deadbeef"), kp.secretKey()); + boolean result = + Signature.verifyDetached(Bytes.fromHexString("deadbeef"), signature, kp.publicKey()); + assertTrue(result); + } + + @Test + void checkSignAndVerify() { + Signature.KeyPair keyPair = Signature.KeyPair.random(); + Bytes signed = Signature.sign(Bytes.fromHexString("deadbeef"), keyPair.secretKey()); + Bytes messageBytes = Signature.verify(signed, keyPair.publicKey()); + assertEquals(Bytes.fromHexString("deadbeef"), messageBytes); + } + + @Test + void testDestroyPublicKey() { + Signature.KeyPair keyPair = Signature.KeyPair.random(); + Signature.PublicKey sigPubKey = Signature.PublicKey.fromBytes(keyPair.publicKey().bytes()); + sigPubKey.destroy(); + assertTrue(sigPubKey.isDestroyed()); + assertFalse(keyPair.publicKey().isDestroyed()); + } +} diff --git a/io/src/main/java/org/apache/tuweni/v2/io/Base32.java b/io/src/main/java/org/apache/tuweni/v2/io/Base32.java new file mode 100644 index 000000000..783195c16 --- /dev/null +++ b/io/src/main/java/org/apache/tuweni/v2/io/Base32.java @@ -0,0 +1,59 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.io; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.v2.bytes.Bytes; + +/** Utility methods for encoding and decoding base32 strings. */ +public final class Base32 { + private static final org.apache.commons.codec.binary.Base32 codec = + new org.apache.commons.codec.binary.Base32(); + + private Base32() {} + + /** + * Encode a byte array to a base32 encoded string. + * + * @param bytes The bytes to encode. + * @return A base32 encoded string. + */ + public static String encodeBytes(byte[] bytes) { + requireNonNull(bytes); + return new String(codec.encode(bytes), UTF_8); + } + + /** + * Encode bytes to a base32 encoded string. + * + * @param bytes The bytes to encode. + * @return A base32 encoded string. + */ + public static String encode(Bytes bytes) { + requireNonNull(bytes); + return encodeBytes(bytes.toArrayUnsafe()); + } + + /** + * Decode a base32 encoded string to a byte array. + * + * @param b32 The base32 encoded string. + * @return A byte array. + */ + public static byte[] decodeBytes(String b32) { + requireNonNull(b32); + return codec.decode(b32.getBytes(UTF_8)); + } + + /** + * Decode a base32 encoded string to bytes. + * + * @param b32 The base32 encoded string. + * @return The decoded bytes. + */ + public static Bytes decode(String b32) { + return Bytes.wrap(decodeBytes(b32)); + } +} diff --git a/io/src/main/java/org/apache/tuweni/v2/io/Base58.java b/io/src/main/java/org/apache/tuweni/v2/io/Base58.java new file mode 100644 index 000000000..c317f0ae4 --- /dev/null +++ b/io/src/main/java/org/apache/tuweni/v2/io/Base58.java @@ -0,0 +1,56 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.io; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.v2.bytes.Bytes; + +/** Utility methods for encoding and decoding base58 strings. */ +public final class Base58 { + private Base58() {} + + /** + * Encode a byte array to a base58 encoded string. + * + * @param bytes The bytes to encode. + * @return A base58 encoded string. + */ + public static String encodeBytes(byte[] bytes) { + requireNonNull(bytes); + return new String(Base58Codec.encode(bytes), UTF_8); + } + + /** + * Encode bytes to a base58 encoded string. + * + * @param bytes The bytes to encode. + * @return A base58 encoded string. + */ + public static String encode(Bytes bytes) { + requireNonNull(bytes); + return encodeBytes(bytes.toArrayUnsafe()); + } + + /** + * Decode a base58 encoded string to a byte array. + * + * @param b58 The base58 encoded string. + * @return A byte array. + */ + public static byte[] decodeBytes(String b58) { + requireNonNull(b58); + return Base58Codec.decode(b58); + } + + /** + * Decode a base58 encoded string to bytes. + * + * @param b58 The base58 encoded string. + * @return The decoded bytes. + */ + public static Bytes decode(String b58) { + return Bytes.wrap(decodeBytes(b58)); + } +} diff --git a/io/src/main/java/org/apache/tuweni/v2/io/Base58Codec.java b/io/src/main/java/org/apache/tuweni/v2/io/Base58Codec.java new file mode 100644 index 000000000..d1af7ab89 --- /dev/null +++ b/io/src/main/java/org/apache/tuweni/v2/io/Base58Codec.java @@ -0,0 +1,95 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.io; + +import java.util.Arrays; + +class Base58Codec { + + // @formatter:off + private static final byte[] ENCODE_TABLE = { + '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', + 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z' + }; + + private static final byte[] DECODE_TABLE = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20-2f + - / + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, // 30-3f 0-9 + -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1, // 40-4f A-O + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1, // 50-5f P-Z _ + -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, // 60-6f a-o + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57 // 70-7a p-z + }; + + // @formatter:on + + static byte[] encode(byte[] decoded) { + byte[] input = Arrays.copyOf(decoded, decoded.length); + byte[] encoded = new byte[input.length * 2]; + int inputStart = 0; + int outputStart = encoded.length; + int zeros = 0; + + while (inputStart < input.length) { + if (input[inputStart] == 0 && outputStart == encoded.length) { + zeros++; + inputStart++; + continue; + } + int remainder = 0; + for (int i = 0; i < input.length; i++) { + int digit = (int) input[i] & 0xFF; + int temp = remainder * 256 + digit; + input[i] = (byte) (temp / 58); + remainder = temp % 58; + } + encoded[--outputStart] = ENCODE_TABLE[remainder]; + if (input[inputStart] == 0) { + inputStart++; + } + } + Arrays.fill(encoded, outputStart - zeros, outputStart, ENCODE_TABLE[0]); + return Arrays.copyOfRange(encoded, outputStart - zeros, encoded.length); + } + + static byte[] decode(String encoded) { + byte[] input = new byte[encoded.length()]; + byte[] decoded = new byte[input.length]; + for (int i = 0; i < input.length; i++) { + input[i] = DECODE_TABLE[encoded.charAt(i)]; + if (input[i] == -1) { + throw new IllegalArgumentException("Invalid character " + encoded.charAt(i)); + } + } + int inputStart = 0; + int outputStart = input.length; + int zeros = 0; + + while (inputStart < input.length) { + if (input[inputStart] == 0 && outputStart == input.length) { + zeros++; + inputStart++; + continue; + } + int remainder = 0; + for (int i = 0; i < input.length; i++) { + int digit = (int) input[i] & 0xFF; + int temp = remainder * 58 + digit; + input[i] = (byte) (temp / 256); + remainder = temp % 256; + } + decoded[--outputStart] = (byte) remainder; + if (input[inputStart] == 0) { + inputStart++; + } + } + Arrays.fill(decoded, outputStart - zeros, outputStart, (byte) 0); + return Arrays.copyOfRange(decoded, outputStart - zeros, decoded.length); + } +} diff --git a/io/src/main/java/org/apache/tuweni/v2/io/Base64.java b/io/src/main/java/org/apache/tuweni/v2/io/Base64.java new file mode 100644 index 000000000..32c4e2945 --- /dev/null +++ b/io/src/main/java/org/apache/tuweni/v2/io/Base64.java @@ -0,0 +1,56 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.io; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.v2.bytes.Bytes; + +/** Utility methods for encoding and decoding base64 strings. */ +public final class Base64 { + private Base64() {} + + /** + * Encode a byte array to a base64 encoded string. + * + * @param bytes The bytes to encode. + * @return A base64 encoded string. + */ + public static String encodeBytes(byte[] bytes) { + requireNonNull(bytes); + return new String(java.util.Base64.getEncoder().encode(bytes), UTF_8); + } + + /** + * Encode bytes to a base64 encoded string. + * + * @param bytes The bytes to encode. + * @return A base64 encoded string. + */ + public static String encode(Bytes bytes) { + requireNonNull(bytes); + return encodeBytes(bytes.toArrayUnsafe()); + } + + /** + * Decode a base64 encoded string to a byte array. + * + * @param b64 The base64 encoded string. + * @return A byte array. + */ + public static byte[] decodeBytes(String b64) { + requireNonNull(b64); + return java.util.Base64.getDecoder().decode(b64.getBytes(UTF_8)); + } + + /** + * Decode a base64 encoded string to bytes. + * + * @param b64 The base64 encoded string. + * @return The decoded bytes. + */ + public static Bytes decode(String b64) { + return Bytes.wrap(decodeBytes(b64)); + } +} diff --git a/io/src/main/java/org/apache/tuweni/v2/io/Base64URLSafe.java b/io/src/main/java/org/apache/tuweni/v2/io/Base64URLSafe.java new file mode 100644 index 000000000..db22b2244 --- /dev/null +++ b/io/src/main/java/org/apache/tuweni/v2/io/Base64URLSafe.java @@ -0,0 +1,56 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.io; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.v2.bytes.Bytes; + +/** Utility methods for encoding and decoding base64 URL safe strings. */ +public final class Base64URLSafe { + private Base64URLSafe() {} + + /** + * Encode a byte array to a base64 encoded string. + * + * @param bytes The bytes to encode. + * @return A base64 encoded string. + */ + public static String encodeBytes(byte[] bytes) { + requireNonNull(bytes); + return new String(java.util.Base64.getUrlEncoder().encode(bytes), UTF_8); + } + + /** + * Encode bytes to a base64 encoded string. + * + * @param bytes The bytes to encode. + * @return A base64 encoded string. + */ + public static String encode(Bytes bytes) { + requireNonNull(bytes); + return encodeBytes(bytes.toArrayUnsafe()); + } + + /** + * Decode a base64 encoded string to a byte array. + * + * @param b64 The base64 encoded string. + * @return A byte array. + */ + public static byte[] decodeBytes(String b64) { + requireNonNull(b64); + return java.util.Base64.getUrlDecoder().decode(b64.getBytes(UTF_8)); + } + + /** + * Decode a base64 encoded string to bytes. + * + * @param b64 The base64 encoded string. + * @return The decoded bytes. + */ + public static Bytes decode(String b64) { + return Bytes.wrap(decodeBytes(b64)); + } +} diff --git a/io/src/main/java/org/apache/tuweni/v2/io/package-info.java b/io/src/main/java/org/apache/tuweni/v2/io/package-info.java new file mode 100644 index 000000000..08aa51e7b --- /dev/null +++ b/io/src/main/java/org/apache/tuweni/v2/io/package-info.java @@ -0,0 +1,23 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You 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. + * + *

Classes and utilities for handling file and network IO. + * + *

These classes are included in the standard Tuweni distribution, or separately when using the + * gradle dependency 'org.apache.tuweni:tuweni-io' (tuweni-io.jar). + */ +@ParametersAreNonnullByDefault +package org.apache.tuweni.v2.io; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/io/src/test/java/org/apache/tuweni/v2/io/Base32Test.java b/io/src/test/java/org/apache/tuweni/v2/io/Base32Test.java new file mode 100644 index 000000000..90c6b10e5 --- /dev/null +++ b/io/src/test/java/org/apache/tuweni/v2/io/Base32Test.java @@ -0,0 +1,37 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.io; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.Test; + +class Base32Test { + + @Test + void shouldEncodeByteArray() { + String s = Base32.encodeBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}); + assertEquals("AEBAGBAFAYDQQ===", s); + } + + @Test + void shouldEncodeBytesValue() { + String s = Base32.encode(Bytes.of(1, 2, 3, 4, 5, 6, 7, 8)); + assertEquals("AEBAGBAFAYDQQ===", s); + } + + @Test + void shouldDecodeToByteArray() { + byte[] bytes = Base32.decodeBytes("AEBAGBAFAYDQQ==="); + assertArrayEquals(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}, bytes); + } + + @Test + void shouldDecodeToBytesValue() { + Bytes bytes = Base32.decode("AEBAGBAFAYDQQ==="); + assertEquals(Bytes.of(1, 2, 3, 4, 5, 6, 7, 8), bytes); + } +} diff --git a/io/src/test/java/org/apache/tuweni/v2/io/Base58Test.java b/io/src/test/java/org/apache/tuweni/v2/io/Base58Test.java new file mode 100644 index 000000000..78b7f2448 --- /dev/null +++ b/io/src/test/java/org/apache/tuweni/v2/io/Base58Test.java @@ -0,0 +1,77 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.io; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +class Base58Test { + + @Test + void testHelloWorld() { + String result = Base58.encode(Bytes.wrap("Hello World!".getBytes(StandardCharsets.US_ASCII))); + assertEquals("2NEpo7TZRRrLZSi2U", result); + } + + @Test + void testQuickBrownFox() { + String result = + Base58.encode( + Bytes.wrap( + "The quick brown fox jumps over the lazy dog." + .getBytes(StandardCharsets.US_ASCII))); + assertEquals("USm3fpXnKG5EUBx2ndxBDMPVciP5hGey2Jh4NDv6gmeo1LkMeiKrLJUUBk6Z", result); + } + + @Test + void testHex() { + Bytes value = + Bytes.fromHexString("1220BA8632EF1A07986B171B3C8FAF0F79B3EE01B6C30BBE15A13261AD6CB0D02E3A"); + assertEquals("QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", Base58.encode(value)); + } + + @Test + void testHexDecode() { + Bytes value = Bytes.fromHexString("00000000"); + assertEquals(value, Bytes.wrap(Base58.decode(Base58.encode(value)))); + } + + @Test + void testHexDecodeOne() { + Bytes value = Bytes.fromHexString("01"); + assertEquals(value, Bytes.wrap(Base58.decode(Base58.encode(value)))); + } + + @Test + void testHexDecode256() { + Bytes value = Bytes.fromHexString("0100"); + assertEquals(value, Bytes.wrap(Base58.decode(Base58.encode(value)))); + } + + @Test + void testZeros() { + Bytes value = Bytes.fromHexString("000000"); + assertEquals("111", Base58.encode(value)); + } + + @Test + void testZerosThenOne() { + Bytes value = Bytes.fromHexString("00000001"); + assertEquals("1112", Base58.encode(value)); + } + + @Test + void testBadCharacter() { + assertThrows( + IllegalArgumentException.class, + () -> { + Base58.decode("%^"); + }); + } +} diff --git a/io/src/test/java/org/apache/tuweni/v2/io/Base64Test.java b/io/src/test/java/org/apache/tuweni/v2/io/Base64Test.java new file mode 100644 index 000000000..a485397e7 --- /dev/null +++ b/io/src/test/java/org/apache/tuweni/v2/io/Base64Test.java @@ -0,0 +1,43 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.io; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.Test; + +class Base64Test { + + @Test + void shouldEncodeByteArray() { + String s = Base64.encodeBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}); + assertEquals("AQIDBAUGBwg=", s); + } + + @Test + void shouldEncodeBytesValue() { + String s = Base64.encode(Bytes.of(1, 2, 3, 4, 5, 6, 7, 8)); + assertEquals("AQIDBAUGBwg=", s); + } + + @Test + void shouldDecodeToByteArray() { + byte[] bytes = Base64.decodeBytes("AQIDBAUGBwg="); + assertArrayEquals(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}, bytes); + } + + @Test + void shouldDecodeToBytesValue() { + Bytes bytes = Base64.decode("AQIDBAUGBwg="); + assertEquals(Bytes.of(1, 2, 3, 4, 5, 6, 7, 8), bytes); + } + + @Test + void shouldEncodeValueWithSlashes() { + String value = Base64.encode(Bytes.fromHexString("deadbeefffffff")); + assertEquals("3q2+7////w==", value); + } +} diff --git a/io/src/test/java/org/apache/tuweni/v2/io/Base64URLSafeTest.java b/io/src/test/java/org/apache/tuweni/v2/io/Base64URLSafeTest.java new file mode 100644 index 000000000..0d8ff0814 --- /dev/null +++ b/io/src/test/java/org/apache/tuweni/v2/io/Base64URLSafeTest.java @@ -0,0 +1,43 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.io; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.tuweni.v2.bytes.Bytes; + +import org.junit.jupiter.api.Test; + +class Base64URLSafeTest { + + @Test + void shouldEncodeByteArray() { + String s = Base64URLSafe.encodeBytes(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}); + assertEquals("AQIDBAUGBwg=", s); + } + + @Test + void shouldEncodeBytesValue() { + String s = Base64URLSafe.encode(Bytes.of(1, 2, 3, 4, 5, 6, 7, 8)); + assertEquals("AQIDBAUGBwg=", s); + } + + @Test + void shouldDecodeToByteArray() { + byte[] bytes = Base64URLSafe.decodeBytes("AQIDBAUGBwg"); + assertArrayEquals(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}, bytes); + } + + @Test + void shouldDecodeToBytesValue() { + Bytes bytes = Base64URLSafe.decode("AQIDBAUGBwg"); + assertEquals(Bytes.of(1, 2, 3, 4, 5, 6, 7, 8), bytes); + } + + @Test + void shouldEncodeValueWithDashes() { + String value = Base64URLSafe.encode(Bytes.fromHexString("deadbeefffffff")); + assertEquals("3q2-7____w==", value); + } +} diff --git a/net/src/main/java/org/apache/tuweni/v2/net/package-info.java b/net/src/main/java/org/apache/tuweni/v2/net/package-info.java new file mode 100644 index 000000000..e7c321139 --- /dev/null +++ b/net/src/main/java/org/apache/tuweni/v2/net/package-info.java @@ -0,0 +1,20 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You 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. + * + *

Classes and utilities for working with networking. + * + *

These classes are included in the standard Tuweni distribution, or separately when using the + * gradle dependency 'org.apache.tuweni:tuweni-net' (tuweni-net.jar). + */ +package org.apache.tuweni.v2.net; diff --git a/net/src/main/java/org/apache/tuweni/v2/net/tls/ClientFingerprintTrustManager.java b/net/src/main/java/org/apache/tuweni/v2/net/tls/ClientFingerprintTrustManager.java new file mode 100644 index 000000000..2df434e64 --- /dev/null +++ b/net/src/main/java/org/apache/tuweni/v2/net/tls/ClientFingerprintTrustManager.java @@ -0,0 +1,123 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.net.tls; + +import static java.lang.String.format; +import static org.apache.tuweni.v2.net.tls.TLS.certificateFingerprint; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Locale; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x500.style.IETFUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; + +final class ClientFingerprintTrustManager extends X509ExtendedTrustManager { + + private static final X509Certificate[] EMPTY_X509_CERTIFICATES = new X509Certificate[0]; + + static ClientFingerprintTrustManager record(FingerprintRepository repository) { + return new ClientFingerprintTrustManager(repository, true, true); + } + + static ClientFingerprintTrustManager tofa(FingerprintRepository repository) { + return new ClientFingerprintTrustManager(repository, true, false); + } + + static ClientFingerprintTrustManager allowlist(FingerprintRepository repository) { + return new ClientFingerprintTrustManager(repository, false, false); + } + + private final FingerprintRepository repository; + private final boolean acceptNewFingerprints; + private final boolean updateFingerprints; + + private ClientFingerprintTrustManager( + FingerprintRepository repository, boolean acceptNewFingerprints, boolean updateFingerprints) { + this.repository = repository; + this.acceptNewFingerprints = acceptNewFingerprints; + this.updateFingerprints = updateFingerprints; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + X509Certificate cert = chain[0]; + X500Name x500name = new JcaX509CertificateHolder(cert).getSubject(); + RDN cn = x500name.getRDNs(BCStyle.CN)[0]; + String hostname = IETFUtils.valueToString(cn.getFirst().getValue()); + checkTrusted(chain, hostname); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) { + throw new UnsupportedOperationException(); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + X509Certificate cert = chain[0]; + X500Name x500name = new JcaX509CertificateHolder(cert).getSubject(); + RDN cn = x500name.getRDNs(BCStyle.CN)[0]; + String hostname = IETFUtils.valueToString(cn.getFirst().getValue()); + checkTrusted(chain, hostname); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) { + throw new UnsupportedOperationException(); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + throw new UnsupportedOperationException(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + throw new UnsupportedOperationException(); + } + + private void checkTrusted(X509Certificate[] chain, String host) throws CertificateException { + X509Certificate cert = chain[0]; + Bytes fingerprint = Bytes.wrap(certificateFingerprint(cert)); + if (repository.contains(host, fingerprint)) { + return; + } + + if (repository.contains(host)) { + if (!updateFingerprints) { + throw new CertificateException( + format( + "Client identification has changed!!" + + " Certificate for %s (%s) has fingerprint %s", + host, + cert.getSubjectDN(), + fingerprint.toHexString().substring(2).toLowerCase(Locale.ENGLISH))); + } + } else if (!acceptNewFingerprints) { + throw new CertificateException( + format( + "Certificate for %s (%s) has unknown fingerprint %s", + host, + cert.getSubjectDN(), + fingerprint.toHexString().substring(2).toLowerCase(Locale.ENGLISH))); + } + + repository.addFingerprint(host, fingerprint); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EMPTY_X509_CERTIFICATES; + } +} diff --git a/net/src/main/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepository.java b/net/src/main/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepository.java new file mode 100644 index 000000000..eec5ab75e --- /dev/null +++ b/net/src/main/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepository.java @@ -0,0 +1,167 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.net.tls; + +import static java.nio.file.Files.createDirectories; +import static org.apache.tuweni.io.file.Files.atomicReplace; +import static org.apache.tuweni.io.file.Files.createFileIfMissing; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +final class FileBackedFingerprintRepository implements FingerprintRepository { + + private final Path fingerprintFile; + private volatile Map fingerprints; + + FileBackedFingerprintRepository(Path fingerprintFile) { + try { + createDirectories(fingerprintFile.toAbsolutePath().getParent()); + createFileIfMissing(fingerprintFile); + } catch (IOException e) { + throw new TLSEnvironmentException("Cannot create fingerprint file " + fingerprintFile, e); + } + this.fingerprintFile = fingerprintFile; + this.fingerprints = parseFingerprintFile(fingerprintFile); + } + + @Override + public boolean contains(String identifier) { + return fingerprints.containsKey(identifier); + } + + @Override + public boolean contains(String identifier, Bytes fingerprint) { + return fingerprint.equals(fingerprints.get(identifier)); + } + + @Override + public void addFingerprint(String identifier, Bytes fingerprint) { + if (!contains(identifier, fingerprint)) { + synchronized (this) { + if (!contains(identifier, fingerprint)) { + // put into a copy first, then atomically replace + HashMap fingerprintsCopy = new HashMap<>(fingerprints); + fingerprintsCopy.put(identifier, fingerprint); + fingerprints = writeFingerprintFile(fingerprintFile, fingerprintsCopy); + } + } + } + } + + private static Map parseFingerprintFile(Path fingerprintFile) { + List lines; + try { + lines = Files.readAllLines(fingerprintFile); + } catch (IOException e) { + throw new TLSEnvironmentException("Cannot read fingerprint file " + fingerprintFile, e); + } + + Map fingerprints = new HashMap<>(); + + for (int i = 0; i < lines.size(); ++i) { + String line = lines.get(i).trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + Map.Entry entry; + try { + entry = parseLine(line); + } catch (IOException e) { + throw new TLSEnvironmentException( + e.getMessage() + " in " + fingerprintFile + " (line " + (i + 1) + ")"); + } + fingerprints.put(entry.getKey(), entry.getValue()); + } + + return Collections.unmodifiableMap(fingerprints); + } + + private static Map writeFingerprintFile( + Path fingerprintFile, Map updatedFingerprints) { + List lines; + try { + lines = Files.readAllLines(fingerprintFile); + } catch (IOException e) { + throw new TLSEnvironmentException("Cannot read fingerprint file " + fingerprintFile, e); + } + + Map fingerprints = new HashMap<>(); + HashSet updatedIdentifiers = new HashSet<>(updatedFingerprints.keySet()); + + try { + atomicReplace( + fingerprintFile, + writer -> { + // copy lines, replacing any updated fingerprints + for (int i = 0; i < lines.size(); ++i) { + String line = lines.get(i).trim(); + if (line.isEmpty() || line.startsWith("#")) { + writer.write(lines.get(i)); + writer.write(System.lineSeparator()); + continue; + } + + Map.Entry entry; + try { + entry = parseLine(line); + } catch (IOException e) { + throw new TLSEnvironmentException( + e.getMessage() + " in " + fingerprintFile + " (line " + (i + 1) + ")"); + } + + String identifier = entry.getKey(); + Bytes fingerprint = updatedFingerprints.getOrDefault(identifier, entry.getValue()); + fingerprints.put(identifier, fingerprint); + updatedIdentifiers.remove(identifier); + + writer.write(identifier); + writer.write(' '); + writer.write(fingerprint.toHexString().substring(2).toLowerCase(Locale.ENGLISH)); + writer.write(System.lineSeparator()); + } + + // write any new fingerprints at the end + for (String identifier : updatedIdentifiers) { + Bytes fingerprint = updatedFingerprints.get(identifier); + fingerprints.put(identifier, fingerprint); + writer.write(identifier); + writer.write(' '); + writer.write(fingerprint.toHexString().substring(2).toLowerCase(Locale.ENGLISH)); + writer.write(System.lineSeparator()); + } + }); + + return Collections.unmodifiableMap(fingerprints); + } catch (IOException e) { + throw new TLSEnvironmentException("Cannot write fingerprint file " + fingerprintFile, e); + } + } + + private static Map.Entry parseLine(String line) throws IOException { + String[] segments = line.split("\\s+", 2); + if (segments.length != 2) { + throw new IOException("Invalid line"); + } + String identifier = segments[0]; + String fingerprintString = segments[1].trim().replace(":", ""); + Bytes fingerprint; + try { + fingerprint = Bytes.fromHexString(fingerprintString); + } catch (IllegalArgumentException e) { + throw new IOException("Invalid fingerprint", e); + } + return new SimpleImmutableEntry<>(identifier, fingerprint); + } +} diff --git a/net/src/main/java/org/apache/tuweni/v2/net/tls/FingerprintRepository.java b/net/src/main/java/org/apache/tuweni/v2/net/tls/FingerprintRepository.java new file mode 100644 index 000000000..7931ade26 --- /dev/null +++ b/net/src/main/java/org/apache/tuweni/v2/net/tls/FingerprintRepository.java @@ -0,0 +1,35 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.net.tls; + +import org.apache.tuweni.v2.bytes.Bytes; + +/** Repository of remote peer fingerprints. */ +public interface FingerprintRepository { + + /** + * Checks whether the identifier of the remote peer is present in the repository. + * + * @param identifier the identifier of a remote peer + * @return true if the remote peer identifier is present in the repository + */ + boolean contains(String identifier); + + /** + * Checks whether the identifier of the remote peer is present in the repository, and its + * fingerprint matches the fingerprint present. + * + * @param identifier the identifier of a remote peer + * @param fingerprint the fingerprint of a remote peer + * @return true if there is a peer in the repository associated with that fingerprint + */ + boolean contains(String identifier, Bytes fingerprint); + + /** + * Adds the fingerprint of a remote peer to the repository. + * + * @param identifier the identifier of a remote peer + * @param fingerprint the fingerprint of a remote peer + */ + void addFingerprint(String identifier, Bytes fingerprint); +} diff --git a/net/src/main/java/org/apache/tuweni/v2/net/tls/ServerFingerprintTrustManager.java b/net/src/main/java/org/apache/tuweni/v2/net/tls/ServerFingerprintTrustManager.java new file mode 100644 index 000000000..cf580363b --- /dev/null +++ b/net/src/main/java/org/apache/tuweni/v2/net/tls/ServerFingerprintTrustManager.java @@ -0,0 +1,121 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.net.tls; + +import static java.lang.String.format; +import static org.apache.tuweni.v2.net.tls.TLS.certificateFingerprint; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Locale; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +final class ServerFingerprintTrustManager extends X509ExtendedTrustManager { + + private static final X509Certificate[] EMPTY_X509_CERTIFICATES = new X509Certificate[0]; + + static ServerFingerprintTrustManager record(FingerprintRepository repository) { + return new ServerFingerprintTrustManager(repository, true, true); + } + + static ServerFingerprintTrustManager tofu(FingerprintRepository repository) { + return new ServerFingerprintTrustManager(repository, true, false); + } + + static ServerFingerprintTrustManager allowlist(FingerprintRepository repository) { + return new ServerFingerprintTrustManager(repository, false, false); + } + + private final FingerprintRepository repository; + private final boolean acceptNewFingerprints; + private final boolean updateFingerprints; + + private ServerFingerprintTrustManager( + FingerprintRepository repository, boolean acceptNewFingerprints, boolean updateFingerprints) { + this.repository = repository; + this.acceptNewFingerprints = acceptNewFingerprints; + this.updateFingerprints = updateFingerprints; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + InetSocketAddress socketAddress = (InetSocketAddress) socket.getRemoteSocketAddress(); + checkTrusted(chain, socketAddress.getHostName(), socketAddress.getPort()); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + checkTrusted(chain, engine.getPeerHost(), engine.getPeerPort()); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + throw new UnsupportedOperationException(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + throw new UnsupportedOperationException(); + } + + private void checkTrusted(X509Certificate[] chain, String host, int port) + throws CertificateException { + X509Certificate cert = chain[0]; + String identifier = hostIdentifier(host, port); + Bytes fingerprint = Bytes.wrap(certificateFingerprint(cert)); + if (repository.contains(identifier, fingerprint)) { + return; + } + + if (repository.contains(identifier)) { + if (!updateFingerprints) { + throw new CertificateException( + format( + "Remote host identification has changed!!" + + " Certificate for %s (%s) has fingerprint %s", + identifier, + cert.getSubjectDN(), + fingerprint.toHexString().substring(2).toLowerCase(Locale.ENGLISH))); + } + } else if (!acceptNewFingerprints) { + throw new CertificateException( + format( + "Certificate for %s (%s) has unknown fingerprint %s", + identifier, + cert.getSubjectDN(), + fingerprint.toHexString().substring(2).toLowerCase(Locale.ENGLISH))); + } + + repository.addFingerprint(identifier, fingerprint); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EMPTY_X509_CERTIFICATES; + } + + private String hostIdentifier(String host, int port) { + return host.trim().toLowerCase(Locale.ENGLISH) + ":" + port; + } +} diff --git a/net/src/main/java/org/apache/tuweni/v2/net/tls/TLS.java b/net/src/main/java/org/apache/tuweni/v2/net/tls/TLS.java new file mode 100644 index 000000000..16b0934e2 --- /dev/null +++ b/net/src/main/java/org/apache/tuweni/v2/net/tls/TLS.java @@ -0,0 +1,219 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.net.tls; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.Files.createDirectories; +import static org.apache.tuweni.v2.crypto.Hash.sha2_256; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.UUID; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemReader; +import org.bouncycastle.util.io.pem.PemWriter; + +/** + * Common utilities for TLS. + * + *

This class depends upon the BouncyCastle library being available and added as a {@link + * java.security.Provider}. See https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation. + * + *

BouncyCastle can be included using the gradle dependencies + * org.bouncycastle:bcprov-jdk15on and org.bouncycastle:bcpkix-jdk15on. + */ +public final class TLS { + private TLS() {} + + /** + * Create a self-signed certificate, if it is not already present. + * + *

If both the key or the certificate file are missing, they will be re-created as a + * self-signed certificate. + * + * @param key The key path. + * @param certificate The certificate path. + * @return {@code true} if a self-signed certificate was created. + * @throws IOException If an IO error occurs creating the certificate. + */ + public static boolean createSelfSignedCertificateIfMissing(Path key, Path certificate) + throws IOException { + return createSelfSignedCertificateIfMissing(key, certificate, null); + } + + /** + * Create a self-signed certificate, if it is not already present. + * + *

If both the key or the certificate file are missing, they will be re-created as a + * self-signed certificate. + * + * @param key The key path. + * @param certificate The certificate path. + * @param commonName the name to use for the CN attribute of the certificate. If null or empty, a + * random value is used. + * @return {@code true} if a self-signed certificate was created. + * @throws IOException If an IO error occurs creating the certificate. + */ + @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) + public static boolean createSelfSignedCertificateIfMissing( + Path key, Path certificate, String commonName) throws IOException { + if (Files.exists(certificate) || Files.exists(key)) { + return false; + } + + createDirectories(certificate.getParent()); + createDirectories(key.getParent()); + + Path keyFile = Files.createTempFile(key.getParent(), "client-key", ".tmp"); + Path certFile = Files.createTempFile(certificate.getParent(), "client-cert", ".tmp"); + + try { + createSelfSignedCertificate(new Date(), keyFile, certFile, commonName); + } catch (CertificateException | NoSuchAlgorithmException | OperatorCreationException e) { + throw new TLSEnvironmentException("Could not generate certificate: " + e.getMessage(), e); + } + + Files.move(keyFile, key, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + Files.move( + certFile, certificate, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + return true; + } + + @SuppressWarnings("JavaUtilDate") + private static void createSelfSignedCertificate( + Date now, Path key, Path certificate, String commonName) + throws NoSuchAlgorithmException, + IOException, + OperatorCreationException, + CertificateException { + KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA"); + rsa.initialize(2048, new SecureRandom()); + + KeyPair keyPair = rsa.generateKeyPair(); + + Calendar cal = Calendar.getInstance(); + cal.setTime(now); + cal.add(Calendar.YEAR, 1); + Date yearFromNow = cal.getTime(); + + if (commonName == null || commonName.isEmpty()) { + commonName = UUID.randomUUID().toString() + ".com"; + } + + X500Name dn = new X500Name("CN=" + commonName); + + X509v3CertificateBuilder builder = + new JcaX509v3CertificateBuilder( + dn, new BigInteger(64, new SecureRandom()), now, yearFromNow, dn, keyPair.getPublic()); + + ContentSigner signer = + new JcaContentSignerBuilder("SHA256WithRSAEncryption") + .setProvider("BC") + .build(keyPair.getPrivate()); + X509Certificate x509Certificate = + new JcaX509CertificateConverter().setProvider("BC").getCertificate(builder.build(signer)); + + try (BufferedWriter writer = Files.newBufferedWriter(key, UTF_8); + PemWriter pemWriter = new PemWriter(writer)) { + pemWriter.writeObject(new PemObject("PRIVATE KEY", keyPair.getPrivate().getEncoded())); + } + + try (BufferedWriter writer = Files.newBufferedWriter(certificate, UTF_8); + PemWriter pemWriter = new PemWriter(writer)) { + pemWriter.writeObject(new PemObject("CERTIFICATE", x509Certificate.getEncoded())); + } + } + + /** + * Read a PEM-encoded file. + * + * @param certificate The path to a PEM-encoded file. + * @return The bytes for the PEM content. + * @throws IOException If an IO error occurs. + */ + public static byte[] readPemFile(Path certificate) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(certificate, UTF_8); + PemReader pemReader = new PemReader(reader)) { + PemObject pemObject = pemReader.readPemObject(); + return pemObject.getContent(); + } + } + + /** + * Calculate the fingerprint for a PEM-encoded certificate. + * + * @param certificate The path to a PEM-encoded certificate. + * @return The fingerprint bytes for the certificate. + * @throws IOException If an IO error occurs. + */ + public static byte[] certificateFingerprint(Path certificate) throws IOException { + return sha2_256(readPemFile(certificate)); + } + + /** + * Calculate the fingerprint for a PEM-encoded certificate. + * + * @param certificate The path to a PEM-encoded certificate. + * @return The fingerprint hex-string for the certificate. + * @throws IOException If an IO error occurs. + */ + public static String certificateHexFingerprint(Path certificate) throws IOException { + return Bytes.wrap(certificateFingerprint(certificate)) + .toHexString() + .substring(2) + .toLowerCase(Locale.ENGLISH); + } + + /** + * Calculate the fingerprint for certificate. + * + * @param certificate The certificate. + * @return The fingerprint bytes for the certificate. + * @throws CertificateEncodingException If the certificate cannot be encoded. + */ + public static byte[] certificateFingerprint(Certificate certificate) + throws CertificateEncodingException { + return sha2_256(certificate.getEncoded()); + } + + /** + * Calculate the fingerprint for certificate. + * + * @param certificate The certificate. + * @return The fingerprint hex-string for the certificate. + * @throws CertificateEncodingException If the certificate cannot be encoded. + */ + public static String certificateHexFingerprint(Certificate certificate) + throws CertificateEncodingException { + return Bytes.wrap(certificateFingerprint(certificate)) + .toHexString() + .substring(2) + .toLowerCase(Locale.ENGLISH); + } +} diff --git a/net/src/main/java/org/apache/tuweni/v2/net/tls/TLSEnvironmentException.java b/net/src/main/java/org/apache/tuweni/v2/net/tls/TLSEnvironmentException.java new file mode 100644 index 000000000..c1b982308 --- /dev/null +++ b/net/src/main/java/org/apache/tuweni/v2/net/tls/TLSEnvironmentException.java @@ -0,0 +1,14 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.net.tls; + +final class TLSEnvironmentException extends RuntimeException { + + TLSEnvironmentException(String message) { + super(message); + } + + TLSEnvironmentException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/net/src/main/java/org/apache/tuweni/v2/net/tls/package-info.java b/net/src/main/java/org/apache/tuweni/v2/net/tls/package-info.java new file mode 100644 index 000000000..7c3a08257 --- /dev/null +++ b/net/src/main/java/org/apache/tuweni/v2/net/tls/package-info.java @@ -0,0 +1,17 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You 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. + * + *

Utilities for doing fingerprint based TLS certificate checking. + */ +package org.apache.tuweni.v2.net.tls; diff --git a/net/src/test/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepositoryTest.java b/net/src/test/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepositoryTest.java new file mode 100644 index 000000000..b907d8f3e --- /dev/null +++ b/net/src/test/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepositoryTest.java @@ -0,0 +1,98 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.net.tls; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.tuweni.io.file.Files.deleteRecursively; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.tuweni.junit.TempDirectory; +import org.apache.tuweni.junit.TempDirectoryExtension; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.SecureRandom; +import java.util.Locale; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TempDirectoryExtension.class) +class FileBackedFingerprintRepositoryTest { + + private SecureRandom secureRandom = new SecureRandom(); + + private Bytes generateFingerprint() { + byte[] bytes = new byte[32]; + secureRandom.nextBytes(bytes); + return Bytes.wrap(bytes); + } + + @Test + void testRelativePath() throws IOException { + try { + new FileBackedFingerprintRepository(Paths.get("tmp", "foo")); + } finally { + deleteRecursively(Paths.get("tmp")); + } + } + + @Test + void testCaseSensitiveIdentifier(@TempDirectory Path tempFolder) throws IOException { + Path repoFile = tempFolder.resolve("repo"); + String identifier1 = "foo"; + String identifier2 = "Foo"; + + Bytes fingerprint1 = generateFingerprint(); + Bytes fingerprint2 = generateFingerprint(); + + String content = + String.format("%s %s%n%s %s", identifier1, fingerprint1, identifier2, fingerprint2); + Files.writeString(repoFile, content); + + FileBackedFingerprintRepository repo = new FileBackedFingerprintRepository(repoFile); + assertTrue(repo.contains(identifier1, fingerprint1)); + assertTrue(repo.contains(identifier2, fingerprint2)); + } + + @Test + FileBackedFingerprintRepository testAddingNewFingerprint(@TempDirectory Path tempFolder) + throws IOException { + FileBackedFingerprintRepository repo = + new FileBackedFingerprintRepository(tempFolder.resolve("repo")); + Bytes fingerprint = generateFingerprint(); + repo.addFingerprint("foo", fingerprint); + assertTrue(repo.contains("foo", fingerprint)); + assertEquals( + "foo " + fingerprint.toHexString().substring(2).toLowerCase(Locale.ENGLISH), + Files.readAllLines(tempFolder.resolve("repo")).get(0)); + return repo; + } + + @Test + void testUpdateFingerprint(@TempDirectory Path tempFolder) throws IOException { + FileBackedFingerprintRepository repo = testAddingNewFingerprint(tempFolder); + Bytes fingerprint = generateFingerprint(); + repo.addFingerprint("foo", fingerprint); + assertTrue(repo.contains("foo", fingerprint)); + assertEquals( + "foo " + fingerprint.toHexString().substring(2).toLowerCase(Locale.ENGLISH), + Files.readAllLines(tempFolder.resolve("repo")).get(0)); + } + + @Test + void testInvalidFingerprintAddedToFile(@TempDirectory Path tempFolder) throws IOException { + FileBackedFingerprintRepository repo = + new FileBackedFingerprintRepository(tempFolder.resolve("repo-bad2")); + Bytes fingerprint = generateFingerprint(); + Files.write( + tempFolder.resolve("repo-bad2"), + ("bar " + fingerprint.slice(8).toHexString().substring(2) + "GGG").getBytes(UTF_8)); + assertThrows(TLSEnvironmentException.class, () -> repo.addFingerprint("foo", fingerprint)); + } +} diff --git a/units/src/main/java/org/apache/tuweni/units/bigints/Utils.java b/units/src/main/java/org/apache/tuweni/units/bigints/Utils.java new file mode 100644 index 000000000..8cf1fd97f --- /dev/null +++ b/units/src/main/java/org/apache/tuweni/units/bigints/Utils.java @@ -0,0 +1,47 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.units.bigints; + +public class Utils { + static void and( + int[] sourceBytesArray, int sourceOffset, byte[] destBytesArray, int destOffset, int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + destBytesArray[destOffset + i] = + (byte) (unpackByte(sourceBytesArray, sourceOffset + i) & destBytesArray[destOffset + i]); + } + } + + static void or( + int[] sourceBytesArray, int sourceOffset, byte[] destBytesArray, int destOffset, int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + destBytesArray[destOffset + i] = + (byte) (unpackByte(sourceBytesArray, sourceOffset + i) | destBytesArray[destOffset + i]); + } + } + + static void xor( + int[] sourceBytesArray, int sourceOffset, byte[] destBytesArray, int destOffset, int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + destBytesArray[destOffset + i] = + (byte) (unpackByte(sourceBytesArray, sourceOffset + i) ^ destBytesArray[destOffset + i]); + } + } + + static byte unpackByte(int[] ints, int index) { + int whichInt = index / 4; + return unpackByte(ints[whichInt], index); + } + + static byte unpackByte(int integer, int index) { + int whichIndex = 3 - index % 4; + return (byte) ((integer >> (8 * whichIndex)) & 0xFF); + } + + static byte unpackByte(long value, int index) { + int whichIndex = 7 - index; + return (byte) ((value >> (8 * whichIndex)) & 0xFF); + } +} diff --git a/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt256.java b/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt256.java new file mode 100644 index 000000000..64ead5e58 --- /dev/null +++ b/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt256.java @@ -0,0 +1,1051 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.units.bigints; + +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes32; +import org.apache.tuweni.v2.bytes.MutableBytes; + +import java.math.BigInteger; +import java.util.Arrays; + +import org.jetbrains.annotations.Nullable; + +/** + * An unsigned 256-bit precision number. + * + *

This is a raw 256-bit precision unsigned number of no particular unit. + */ +public final class UInt256 extends Bytes { + private static final int MAX_CONSTANT = 64; + private static final BigInteger BI_MAX_CONSTANT = BigInteger.valueOf(MAX_CONSTANT); + private static UInt256[] CONSTANTS = new UInt256[MAX_CONSTANT + 1]; + + /** The maximum value of a UInt256 */ + public static final UInt256 MAX_VALUE; + + static { + CONSTANTS[0] = new UInt256(Bytes32.ZERO); + for (int i = 1; i <= MAX_CONSTANT; ++i) { + CONSTANTS[i] = new UInt256(i); + } + MAX_VALUE = new UInt256(Bytes32.ZERO.mutableCopy().not()); + } + + /** The minimum value of a UInt256 */ + public static final UInt256 MIN_VALUE = valueOf(0); + + /** The value 0 */ + public static final UInt256 ZERO = valueOf(0); + + /** The value 1 */ + public static final UInt256 ONE = valueOf(1); + + private static final int INTS_SIZE = 32 / 4; + // The mask is used to obtain the value of an int as if it were unsigned. + private static final long LONG_MASK = 0xFFFFFFFFL; + private static final BigInteger P_2_256 = BigInteger.valueOf(2).pow(256); + + // The unsigned int components of the value + private final int[] ints; + + /** + * Return a {@code UInt256} containing the specified value. + * + * @param value The value to create a {@code UInt256} for. + * @return A {@code UInt256} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static UInt256 valueOf(long value) { + if (value < 0) { + throw new IllegalArgumentException("Argument must be positive"); + } + if (value <= MAX_CONSTANT) { + return CONSTANTS[(int) value]; + } + return new UInt256(value); + } + + /** + * Return a {@link UInt256} containing the specified value. + * + * @param value the value to create a {@link UInt256} for + * @return a {@link UInt256} containing the specified value + * @throws IllegalArgumentException if the value is negative or too large to be represented as a + * UInt256 + */ + public static UInt256 valueOf(BigInteger value) { + if (value.signum() < 0) { + throw new IllegalArgumentException("Argument must be positive"); + } + if (value.bitLength() > 256) { + throw new IllegalArgumentException("Argument is too large to represent a UInt256"); + } + if (value.compareTo(BI_MAX_CONSTANT) <= 0) { + return CONSTANTS[value.intValue()]; + } + int[] ints = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + ints[i] = value.intValue(); + value = value.shiftRight(32); + } + return new UInt256(ints); + } + + /** + * Return a {@link UInt256} containing the value described by the specified bytes. + * + * @param bytes The bytes containing a {@link UInt256}. + * @return A {@link UInt256} containing the specified value. + * @throws IllegalArgumentException if {@code bytes.size() > 32}. + */ + public static UInt256 fromBytes(final Bytes bytes) { + // TODO: add a fast path for if Bytes.getImpl returns a UInt256 type + if (bytes instanceof UInt256) { + return (UInt256) bytes; + } + if (bytes instanceof Bytes32) { + final byte[] array = bytes.toArrayUnsafe(); + return new UInt256( + new int[] { + (Byte.toUnsignedInt(array[0])) << 24 + | (Byte.toUnsignedInt(array[1]) << 16) + | (Byte.toUnsignedInt(array[2]) << 8) + | (Byte.toUnsignedInt(array[3])), + (Byte.toUnsignedInt(array[4]) << 24) + | (Byte.toUnsignedInt(array[5]) << 16) + | (Byte.toUnsignedInt(array[6]) << 8) + | (Byte.toUnsignedInt(array[7])), + (Byte.toUnsignedInt(array[8]) << 24) + | (Byte.toUnsignedInt(array[9]) << 16) + | (Byte.toUnsignedInt(array[10]) << 8) + | (Byte.toUnsignedInt(array[11])), + (Byte.toUnsignedInt(array[12]) << 24) + | (Byte.toUnsignedInt(array[13]) << 16) + | (Byte.toUnsignedInt(array[14]) << 8) + | (Byte.toUnsignedInt(array[15])), + (Byte.toUnsignedInt(array[16]) << 24) + | (Byte.toUnsignedInt(array[17]) << 16) + | (Byte.toUnsignedInt(array[18]) << 8) + | (Byte.toUnsignedInt(array[19])), + (Byte.toUnsignedInt(array[20]) << 24) + | (Byte.toUnsignedInt(array[21]) << 16) + | (Byte.toUnsignedInt(array[22]) << 8) + | (Byte.toUnsignedInt(array[23])), + (Byte.toUnsignedInt(array[24]) << 24) + | (Byte.toUnsignedInt(array[25]) << 16) + | (Byte.toUnsignedInt(array[26]) << 8) + | (Byte.toUnsignedInt(array[27])), + (Byte.toUnsignedInt(array[28]) << 24) + | (Byte.toUnsignedInt(array[29]) << 16) + | (Byte.toUnsignedInt(array[30]) << 8) + | (Byte.toUnsignedInt(array[31])) + }); + } else { + return new UInt256(bytes.mutableCopy().leftPad(32)); + } + } + + /** + * Parse a hexadecimal string into a {@link UInt256}. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". That + * representation may contain less than 32 bytes, in which case the result is left padded with + * zeros. + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation or contains more than 32 bytes. + */ + public static UInt256 fromHexString(String str) { + return new UInt256(Bytes32.fromHexStringLenient(str)); + } + + private UInt256(Bytes bytes) { + super(32); + this.ints = new int[INTS_SIZE]; + for (int i = 0, j = 0; i < INTS_SIZE; ++i, j += 4) { + ints[i] = bytes.getInt(j); + } + } + + private UInt256(long value) { + super(32); + this.ints = new int[INTS_SIZE]; + this.ints[INTS_SIZE - 2] = (int) ((value >>> 32) & LONG_MASK); + this.ints[INTS_SIZE - 1] = (int) (value & LONG_MASK); + } + + private UInt256(int[] ints) { + super(32); + this.ints = ints; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isZero() { + if (this == ZERO) { + return true; + } + for (int i = INTS_SIZE - 1; i >= 0; --i) { + if (this.ints[i] != 0) { + return false; + } + } + return true; + } + + public boolean greaterOrEqualThan(UInt256 other) { + return compareTo(other) >= 0; + } + + public boolean greaterThan(UInt256 other) { + return compareTo(other) > 0; + } + + public boolean lessThan(UInt256 other) { + return compareTo(other) < 0; + } + + public boolean lessOrEqualThan(UInt256 other) { + return compareTo(other) <= 0; + } + + public UInt256 add(UInt256 value) { + if (value.isZero()) { + return this; + } + if (isZero()) { + return value; + } + int[] result = new int[INTS_SIZE]; + boolean constant = true; + long sum = (this.ints[INTS_SIZE - 1] & LONG_MASK) + (value.ints[INTS_SIZE - 1] & LONG_MASK); + result[INTS_SIZE - 1] = (int) (sum & LONG_MASK); + if (result[INTS_SIZE - 1] < 0 || result[INTS_SIZE - 1] > MAX_CONSTANT) { + constant = false; + } + for (int i = INTS_SIZE - 2; i >= 0; --i) { + sum = (this.ints[i] & LONG_MASK) + (value.ints[i] & LONG_MASK) + (sum >>> 32); + result[i] = (int) (sum & LONG_MASK); + constant &= result[i] == 0; + } + if (constant) { + return CONSTANTS[result[INTS_SIZE - 1]]; + } + return new UInt256(result); + } + + public UInt256 add(long value) { + if (value == 0) { + return this; + } + if (value > 0 && isZero()) { + return UInt256.valueOf(value); + } + int[] result = new int[INTS_SIZE]; + boolean constant = true; + long sum = (this.ints[INTS_SIZE - 1] & LONG_MASK) + (value & LONG_MASK); + result[INTS_SIZE - 1] = (int) (sum & LONG_MASK); + if (result[INTS_SIZE - 1] < 0 || result[INTS_SIZE - 1] > MAX_CONSTANT) { + constant = false; + } + sum = (this.ints[INTS_SIZE - 2] & LONG_MASK) + (value >>> 32) + (sum >>> 32); + result[INTS_SIZE - 2] = (int) (sum & LONG_MASK); + constant &= result[INTS_SIZE - 2] == 0; + long signExtent = (value >> 63) & LONG_MASK; + for (int i = INTS_SIZE - 3; i >= 0; --i) { + sum = (this.ints[i] & LONG_MASK) + signExtent + (sum >>> 32); + result[i] = (int) (sum & LONG_MASK); + constant &= result[i] == 0; + } + if (constant) { + return CONSTANTS[result[INTS_SIZE - 1]]; + } + return new UInt256(result); + } + + public UInt256 addMod(UInt256 value, UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return UInt256.valueOf( + toUnsignedBigInteger() + .add(value.toUnsignedBigInteger()) + .mod(modulus.toUnsignedBigInteger())); + } + + public UInt256 addMod(long value, UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return UInt256.valueOf( + toUnsignedBigInteger().add(BigInteger.valueOf(value)).mod(modulus.toUnsignedBigInteger())); + } + + public UInt256 addMod(long value, long modulus) { + if (modulus == 0) { + throw new ArithmeticException("addMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("addMod unsigned with negative modulus"); + } + return UInt256.valueOf( + toUnsignedBigInteger().add(BigInteger.valueOf(value)).mod(BigInteger.valueOf(modulus))); + } + + public UInt256 subtract(UInt256 value) { + if (value.isZero()) { + return this; + } + + int[] result = new int[INTS_SIZE]; + boolean constant = true; + long sum = + (this.ints[INTS_SIZE - 1] & LONG_MASK) + ((~value.ints[INTS_SIZE - 1]) & LONG_MASK) + 1; + result[INTS_SIZE - 1] = (int) (sum & LONG_MASK); + if (result[INTS_SIZE - 1] < 0 || result[INTS_SIZE - 1] > MAX_CONSTANT) { + constant = false; + } + for (int i = INTS_SIZE - 2; i >= 0; --i) { + sum = (this.ints[i] & LONG_MASK) + ((~value.ints[i]) & LONG_MASK) + (sum >>> 32); + result[i] = (int) (sum & LONG_MASK); + constant &= result[i] == 0; + } + if (constant) { + return CONSTANTS[result[INTS_SIZE - 1]]; + } + return new UInt256(result); + } + + public UInt256 subtract(long value) { + return add(-value); + } + + public UInt256 multiply(UInt256 value) { + if (isZero() || value.isZero()) { + return ZERO; + } + if (value.equals(UInt256.ONE)) { + return this; + } + if (this.equals(UInt256.ONE)) { + return value; + } + return multiply(this.ints, value.ints); + } + + private static UInt256 multiply(int[] x, int[] y) { + int[] result = new int[INTS_SIZE + INTS_SIZE]; + + long carry = 0; + for (int j = INTS_SIZE - 1, k = INTS_SIZE + INTS_SIZE - 1; j >= 0; j--, k--) { + long product = (y[j] & LONG_MASK) * (x[INTS_SIZE - 1] & LONG_MASK) + carry; + result[k] = (int) product; + carry = product >>> 32; + } + result[INTS_SIZE - 1] = (int) carry; + + for (int i = INTS_SIZE - 2; i >= 0; i--) { + carry = 0; + for (int j = INTS_SIZE - 1, k = INTS_SIZE + i; j >= 0; j--, k--) { + long product = (y[j] & LONG_MASK) * (x[i] & LONG_MASK) + (result[k] & LONG_MASK) + carry; + + result[k] = (int) product; + carry = product >>> 32; + } + result[i] = (int) carry; + } + + boolean constant = true; + for (int i = INTS_SIZE; i < (INTS_SIZE + INTS_SIZE) - 1; ++i) { + constant &= (result[i] == 0); + } + if (constant + && result[INTS_SIZE + INTS_SIZE - 1] >= 0 + && result[INTS_SIZE + INTS_SIZE - 1] <= MAX_CONSTANT) { + return CONSTANTS[result[INTS_SIZE + INTS_SIZE - 1]]; + } + return new UInt256(Arrays.copyOfRange(result, INTS_SIZE, INTS_SIZE + INTS_SIZE)); + } + + public UInt256 multiply(long value) { + if (value == 0 || isZero()) { + return ZERO; + } + if (value == 1) { + return this; + } + if (value < 0) { + throw new ArithmeticException("multiply unsigned by negative"); + } + UInt256 other = new UInt256(value); + if (this.equals(UInt256.ONE)) { + return other; + } + return multiply(this.ints, other.ints); + } + + public UInt256 multiplyMod(UInt256 value, UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (isZero() || value.isZero()) { + return ZERO; + } + if (value.equals(UInt256.ONE)) { + return mod(modulus); + } + return UInt256.valueOf( + toUnsignedBigInteger() + .multiply(value.toUnsignedBigInteger()) + .mod(modulus.toUnsignedBigInteger())); + } + + public UInt256 multiplyMod(long value, UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (value == 0 || isZero()) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return UInt256.valueOf( + toUnsignedBigInteger() + .multiply(BigInteger.valueOf(value)) + .mod(modulus.toUnsignedBigInteger())); + } + + public UInt256 multiplyMod(long value, long modulus) { + if (modulus == 0) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("multiplyMod unsigned with negative modulus"); + } + if (value == 0 || isZero()) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return UInt256.valueOf( + toUnsignedBigInteger() + .multiply(BigInteger.valueOf(value)) + .mod(BigInteger.valueOf(modulus))); + } + + public UInt256 divide(UInt256 value) { + if (value.isZero()) { + throw new ArithmeticException("divide by zero"); + } + if (value.equals(UInt256.ONE)) { + return this; + } + return UInt256.valueOf(toUnsignedBigInteger().divide(value.toUnsignedBigInteger())); + } + + public UInt256 divide(long value) { + if (value == 0) { + throw new ArithmeticException("divide by zero"); + } + if (value < 0) { + throw new ArithmeticException("divide unsigned by negative"); + } + if (value == 1) { + return this; + } + if (isPowerOf2(value)) { + return shiftRight(log2(value)); + } + return UInt256.valueOf(toUnsignedBigInteger().divide(BigInteger.valueOf(value))); + } + + public UInt256 sdiv0(UInt256 divisor) { + if (divisor.isZero()) { + return UInt256.ZERO; + } else { + BigInteger result = this.toSignedBigInteger().divide(divisor.toSignedBigInteger()); + Bytes resultBytes = Bytes.wrap(result.toByteArray()); + if (resultBytes.size() > 32) { + resultBytes = resultBytes.slice(resultBytes.size() - 32, 32); + } + MutableBytes mutableBytes = resultBytes.mutableCopy().leftPad(32); + if (result.signum() < 0) { + mutableBytes.or(MAX_VALUE.shiftLeft(resultBytes.size() * 8)); + } + return UInt256.fromBytes(mutableBytes); + } + } + + public UInt256 divideCeil(UInt256 value) { + return this.divide(value).add(this.mod(value).isZero() ? 0 : 1); + } + + public UInt256 divideCeil(long value) { + return this.divide(value).add(this.mod(value).isZero() ? 0 : 1); + } + + public UInt256 pow(UInt256 exponent) { + return UInt256.valueOf(toUnsignedBigInteger().modPow(exponent.toUnsignedBigInteger(), P_2_256)); + } + + public UInt256 pow(long exponent) { + return UInt256.valueOf(toUnsignedBigInteger().modPow(BigInteger.valueOf(exponent), P_2_256)); + } + + public UInt256 mod(UInt256 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("mod by zero"); + } + return UInt256.valueOf(toUnsignedBigInteger().mod(modulus.toUnsignedBigInteger())); + } + + public UInt256 mod(long modulus) { + if (modulus == 0) { + throw new ArithmeticException("mod by zero"); + } + if (modulus < 0) { + throw new ArithmeticException("mod by negative"); + } + if (isPowerOf2(modulus)) { + int log2 = log2(modulus); + int d = log2 / 32; + int s = log2 % 32; + assert (d == 0 || d == 1); + + int[] result = new int[INTS_SIZE]; + // Mask the byte at d to only include the s right-most bits + result[INTS_SIZE - 1 - d] = this.ints[INTS_SIZE - 1 - d] & ~(0xFFFFFFFF << s); + if (d != 0) { + result[INTS_SIZE - 1] = this.ints[INTS_SIZE - 1]; + } + return new UInt256(result); + } + return UInt256.valueOf(toUnsignedBigInteger().mod(BigInteger.valueOf(modulus))); + } + + public UInt256 mod0(UInt256 modulus) { + if (modulus.equals(UInt256.ZERO)) { + return UInt256.ZERO; + } + return mod(modulus); + } + + /** + * Returns a value that is the {@code (this signed mod modulus)}, or 0 if modulus is 0. + * + * @param modulus The modulus. + * @return {@code this signed mod modulus}. + */ + public UInt256 smod0(UInt256 modulus) { + if (modulus.equals(UInt256.ZERO)) { + return UInt256.ZERO; + } + + BigInteger bi = this.toSignedBigInteger(); + BigInteger result = bi.abs().mod(modulus.toSignedBigInteger().abs()); + + if (bi.signum() < 0) { + result = result.negate(); + } + + Bytes resultBytes = Bytes.wrap(result.toByteArray()); + if (resultBytes.size() > 32) { + resultBytes = resultBytes.slice(resultBytes.size() - 32, 32); + } + + MutableBytes mutableBytes = resultBytes.mutableCopy().leftPad(32); + if (result.signum() < 0) { + mutableBytes.or(MAX_VALUE.shiftLeft(resultBytes.size() * 8)); + } + return UInt256.fromBytes(mutableBytes); + } + + public UInt256 mod0(long modulus) { + if (modulus == 0) { + return UInt256.ZERO; + } + if (modulus < 0) { + throw new ArithmeticException("mod by negative"); + } + return mod(modulus); + } + + /** + * Return a bit-wise AND of this value and the supplied value. + * + * @param value the value to perform the operation with + * @return the result of a bit-wise AND + */ + public UInt256 and(UInt256 value) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = this.ints[i] & value.ints[i]; + } + return new UInt256(result); + } + + /** + * Return a bit-wise AND of this value and the supplied bytes. + * + * @param bytes the bytes to perform the operation with + * @return the result of a bit-wise AND + */ + public UInt256 and(Bytes32 bytes) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1, j = 28; i >= 0; --i, j -= 4) { + int other = ((int) bytes.get(j) & 0xFF) << 24; + other |= ((int) bytes.get(j + 1) & 0xFF) << 16; + other |= ((int) bytes.get(j + 2) & 0xFF) << 8; + other |= ((int) bytes.get(j + 3) & 0xFF); + result[i] = this.ints[i] & other; + } + return new UInt256(result); + } + + /** + * Return a bit-wise OR of this value and the supplied value. + * + * @param value the value to perform the operation with + * @return the result of a bit-wise OR + */ + public UInt256 or(UInt256 value) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = this.ints[i] | value.ints[i]; + } + return new UInt256(result); + } + + /** + * Return a bit-wise OR of this value and the supplied bytes. + * + * @param bytes the bytes to perform the operation with + * @return the result of a bit-wise OR + */ + public UInt256 or(Bytes32 bytes) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1, j = 28; i >= 0; --i, j -= 4) { + int other = ((int) bytes.get(j) & 0xFF) << 24; + other |= ((int) bytes.get(j + 1) & 0xFF) << 16; + other |= ((int) bytes.get(j + 2) & 0xFF) << 8; + other |= ((int) bytes.get(j + 3) & 0xFF); + result[i] = this.ints[i] | other; + } + return new UInt256(result); + } + + /** + * Return a bit-wise XOR of this value and the supplied value. + * + * @param value the value to perform the operation with + * @return the result of a bit-wise XOR + */ + public UInt256 xor(UInt256 value) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = this.ints[i] ^ value.ints[i]; + } + return new UInt256(result); + } + + /** + * Return a bit-wise XOR of this value and the supplied bytes. + * + * @param bytes the bytes to perform the operation with + * @return the result of a bit-wise XOR + */ + public UInt256 xor(Bytes32 bytes) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1, j = 28; i >= 0; --i, j -= 4) { + int other = ((int) bytes.get(j) & 0xFF) << 24; + other |= ((int) bytes.get(j + 1) & 0xFF) << 16; + other |= ((int) bytes.get(j + 2) & 0xFF) << 8; + other |= ((int) bytes.get(j + 3) & 0xFF); + result[i] = this.ints[i] ^ other; + } + return new UInt256(result); + } + + /** + * Return a bit-wise NOT of this value. + * + * @return the result of a bit-wise NOT + */ + public UInt256 not() { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = ~(this.ints[i]); + } + return new UInt256(result); + } + + /** + * Shift all bits in this value to the right. + * + * @param distance The number of bits to shift by. + * @return A value containing the shifted bits. + */ + public UInt256 shiftRight(int distance) { + if (distance == 0) { + return this; + } + if (distance >= 256) { + return ZERO; + } + int[] result = new int[INTS_SIZE]; + int d = distance / 32; + int s = distance % 32; + + int resIdx = INTS_SIZE; + if (s == 0) { + for (int i = INTS_SIZE - d; i > 0; ) { + result[--resIdx] = this.ints[--i]; + } + } else { + for (int i = INTS_SIZE - 1 - d; i >= 0; i--) { + int leftSide = this.ints[i] >>> s; + int rightSide = (i == 0) ? 0 : this.ints[i - 1] << (32 - s); + result[--resIdx] = (leftSide | rightSide); + } + } + return new UInt256(result); + } + + /** + * Shift all bits in this value to the left. + * + * @param distance The number of bits to shift by. + * @return A value containing the shifted bits. + */ + public UInt256 shiftLeft(int distance) { + if (distance == 0) { + return this; + } + if (distance >= 256) { + return ZERO; + } + int[] result = new int[INTS_SIZE]; + int d = distance / 32; + int s = distance % 32; + + int resIdx = 0; + if (s == 0) { + for (int i = d; i < INTS_SIZE; ) { + result[resIdx++] = this.ints[i++]; + } + } else { + for (int i = d; i < INTS_SIZE; ++i) { + int leftSide = this.ints[i] << s; + int rightSide = (i == INTS_SIZE - 1) ? 0 : (this.ints[i + 1] >>> (32 - s)); + result[resIdx++] = (leftSide | rightSide); + } + } + return new UInt256(result); + } + + @Override + public boolean equals(@Nullable Object object) { + if (object == this) { + return true; + } + if (object instanceof UInt256) { + UInt256 other = (UInt256) object; + for (int i = 0; i < INTS_SIZE; ++i) { + if (this.ints[i] != other.ints[i]) { + return false; + } + } + return true; + } + if (object instanceof Bytes) { + Bytes other = (Bytes) object; + if (this.size() != other.size()) { + return false; + } + + for (int i = 0; i < size(); i++) { + if (this.get(i) != other.get(i)) { + return false; + } + } + + return true; + } + return false; + } + + @Override + protected int computeHashcode() { + int result = 1; + for (int i = 0; i < size(); i++) { + result = 31 * result + Utils.unpackByte(ints, i); + } + return result; + } + + public boolean fitsInt() { + for (int i = 0; i < INTS_SIZE - 1; i++) { + if (this.ints[i] != 0) { + return false; + } + } + // Lastly, the left-most byte of the int must not start with a 1. + return this.ints[INTS_SIZE - 1] >= 0; + } + + public int intValue() { + if (!fitsInt()) { + throw new ArithmeticException("Value does not fit a 4 byte int"); + } + return this.ints[INTS_SIZE - 1]; + } + + public boolean fitsLong() { + for (int i = 0; i < INTS_SIZE - 2; i++) { + if (this.ints[i] != 0) { + return false; + } + } + // Lastly, the left-most byte of the int must not start with a 1. + return this.ints[INTS_SIZE - 2] >= 0; + } + + @Override + public byte get(int i) { + checkElementIndex(i, size()); + return Utils.unpackByte(ints, i); + } + + @Override + public long toLong() { + if (!fitsLong()) { + throw new ArithmeticException("Value does not fit a 8 byte long"); + } + return (((long) this.ints[INTS_SIZE - 2]) << 32) + | (((long) (this.ints[INTS_SIZE - 1])) & LONG_MASK); + } + + @Override + public String toString() { + return toHexString(); + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + Utils.and(ints, 0, bytesArray, offset, length); + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + Utils.or(ints, 0, bytesArray, offset, length); + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + Utils.xor(ints, 0, bytesArray, offset, length); + } + + public UInt256 toUInt256() { + return this; + } + + @Override + public Bytes slice(int i, int length) { + return mutableCopy().slice(i, length); + } + + @Override + public MutableBytes mutableCopy() { + return MutableBytes.fromArray(toArrayUnsafe()); + } + + @Override + public byte[] toArrayUnsafe() { + return new byte[] { + (byte) (ints[0] >> 24), + (byte) (ints[0] >> 16), + (byte) (ints[0] >> 8), + (byte) (ints[0]), + (byte) (ints[1] >> 24), + (byte) (ints[1] >> 16), + (byte) (ints[1] >> 8), + (byte) (ints[1]), + (byte) (ints[2] >> 24), + (byte) (ints[2] >> 16), + (byte) (ints[2] >> 8), + (byte) (ints[2]), + (byte) (ints[3] >> 24), + (byte) (ints[3] >> 16), + (byte) (ints[3] >> 8), + (byte) (ints[3]), + (byte) (ints[4] >> 24), + (byte) (ints[4] >> 16), + (byte) (ints[4] >> 8), + (byte) (ints[4]), + (byte) (ints[5] >> 24), + (byte) (ints[5] >> 16), + (byte) (ints[5] >> 8), + (byte) (ints[5]), + (byte) (ints[6] >> 24), + (byte) (ints[6] >> 16), + (byte) (ints[6] >> 8), + (byte) (ints[6]), + (byte) (ints[7] >> 24), + (byte) (ints[7] >> 16), + (byte) (ints[7] >> 8), + (byte) (ints[7]) + }; + } + + public Bytes toBytes() { + return Bytes.wrap(toArrayUnsafe()); + } + + public Bytes toMinimalBytes() { + int i = 0; + while (i < INTS_SIZE && this.ints[i] == 0) { + ++i; + } + if (i == INTS_SIZE) { + return Bytes.EMPTY; + } + int firstIntBytes = 4 - (Integer.numberOfLeadingZeros(this.ints[i]) / 8); + int totalBytes = firstIntBytes + ((INTS_SIZE - (i + 1)) * 4); + MutableBytes bytes = MutableBytes.create(totalBytes); + int j = 0; + switch (firstIntBytes) { + case 4: + bytes.set(j++, (byte) (this.ints[i] >>> 24)); + // fall through + case 3: + bytes.set(j++, (byte) ((this.ints[i] >>> 16) & 0xFF)); + // fall through + case 2: + bytes.set(j++, (byte) ((this.ints[i] >>> 8) & 0xFF)); + // fall through + case 1: + bytes.set(j++, (byte) (this.ints[i] & 0xFF)); + } + ++i; + for (; i < INTS_SIZE; ++i, j += 4) { + bytes.setInt(j, this.ints[i]); + } + return bytes; + } + + @Override + public int numberOfLeadingZeros() { + for (int i = 0; i < INTS_SIZE; i++) { + if (this.ints[i] == 0) { + continue; + } + return (i * 32) + Integer.numberOfLeadingZeros(this.ints[i]); + } + return 256; + } + + @Override + public int bitLength() { + for (int i = 0; i < INTS_SIZE; i++) { + if (this.ints[i] == 0) { + continue; + } + return (INTS_SIZE * 32) - (i * 32) - Integer.numberOfLeadingZeros(this.ints[i]); + } + return 0; + } + + public UInt256 max() { + return UInt256.MAX_VALUE; + } + + private static boolean isPowerOf2(long n) { + assert n > 0; + return (n & (n - 1)) == 0; + } + + private static int log2(long v) { + assert v > 0; + return 63 - Long.numberOfLeadingZeros(v); + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value the amount to be added to this value + * @return {@code this + value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt256 addExact(UInt256 value) { + UInt256 result = add(value); + if (compareTo(result) > 0) { + throw new ArithmeticException("UInt256 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value the amount to be added to this value + * @return {@code this + value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt256 addExact(long value) { + UInt256 result = add(value); + if ((value > 0 && compareTo(result) > 0) || (value < 0 && compareTo(result) < 0)) { + throw new ArithmeticException("UInt256 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value the amount to be subtracted to this value + * @return {@code this - value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt256 subtractExact(UInt256 value) { + UInt256 result = subtract(value); + if (compareTo(result) < 0) { + throw new ArithmeticException("UInt256 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value the amount to be subtracted to this value + * @return {@code this - value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt256 subtractExact(long value) { + UInt256 result = subtract(value); + if ((value > 0 && compareTo(result) < 0) || (value < 0 && compareTo(result) > 0)) { + throw new ArithmeticException("UInt256 overflow"); + } + return result; + } + + /** + * Returns the decimal representation of this value as a String. + * + * @return the decimal representation of this value as a String. + */ + public String toDecimalString() { + return toBigInteger().toString(10); + } +} diff --git a/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt32.java b/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt32.java new file mode 100644 index 000000000..0f94334e5 --- /dev/null +++ b/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt32.java @@ -0,0 +1,483 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.units.bigints; + +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.MutableBytes; + +import java.math.BigInteger; +import java.nio.ByteOrder; + +/** + * An unsigned 32-bit precision number. + * + *

This is a raw 32-bit precision unsigned number of no particular unit. + */ +public final class UInt32 extends Bytes { + private static final int MAX_CONSTANT = 0xff; + private static UInt32[] CONSTANTS = new UInt32[MAX_CONSTANT + 1]; + + static { + CONSTANTS[0] = new UInt32(0); + for (int i = 1; i <= MAX_CONSTANT; ++i) { + CONSTANTS[i] = new UInt32(i); + } + } + + /** The minimum value of a UInt32 */ + public static final UInt32 MIN_VALUE = valueOf(0); + + /** The maximum value of a UInt32 */ + public static final UInt32 MAX_VALUE = create(~0); + + /** The value 0 */ + public static final UInt32 ZERO = valueOf(0); + + /** The value 1 */ + public static final UInt32 ONE = valueOf(1); + + private static final BigInteger P_2_32 = BigInteger.valueOf(2).pow(32); + + private final int value; + + /** + * Return a {@code UInt32} containing the specified value. + * + * @param value The value to create a {@code UInt32} for. + * @return A {@code UInt32} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static UInt32 valueOf(int value) { + if (value < 0) { + throw new IllegalArgumentException("Argument must be positive"); + } + return create(value); + } + + /** + * Return a {@link UInt32} containing the specified value. + * + * @param value the value to create a {@link UInt32} for + * @return a {@link UInt32} containing the specified value + * @throws IllegalArgumentException if the value is negative or too large to be represented as a + * UInt32 + */ + public static UInt32 valueOf(BigInteger value) { + if (value.bitLength() > 32) { + throw new IllegalArgumentException("Argument is too large to represent a UInt32"); + } + if (value.signum() < 0) { + throw new IllegalArgumentException("Argument must be positive"); + } + return create(value.intValue()); + } + + /** + * Return a {@link UInt32} containing the value described by the specified bytes. + * + * @param bytes The bytes containing a {@link UInt32}. \ * @return A {@link UInt32} containing the + * specified value. + * @throws IllegalArgumentException if {@code bytes.size() > 4}. + */ + public static UInt32 fromBytes(Bytes bytes) { + return fromBytes(bytes, ByteOrder.BIG_ENDIAN); + } + + /** + * Return a {@link UInt32} containing the value described by the specified bytes. + * + * @param bytes The bytes containing a {@link UInt32}. + * @param byteOrder the byte order of the value + * @return A {@link UInt32} containing the specified value. + * @throws IllegalArgumentException if {@code bytes.size() > 4}. + */ + public static UInt32 fromBytes(Bytes bytes, ByteOrder byteOrder) { + if (bytes.size() > 4) { + throw new IllegalArgumentException("Argument is greater than 4 bytes"); + } + return create(byteOrder == ByteOrder.LITTLE_ENDIAN ? bytes.mutableCopy().reverse() : bytes); + } + + /** + * Parse a hexadecimal string into a {@link UInt32}. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". That + * representation may contain less than 8 bytes, in which case the result is left padded with + * zeros. + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation or contains more than 8 bytes. + */ + public static UInt32 fromHexString(String str) { + return fromBytes(Bytes.fromHexStringLenient(str)); + } + + private static UInt32 create(Bytes value) { + return create(value.toInt()); + } + + private static UInt32 create(int value) { + if (value >= 0 && value <= MAX_CONSTANT) { + return CONSTANTS[value]; + } + return new UInt32(value); + } + + private UInt32(int value) { + super(4); + this.value = value; + } + + @Override + public boolean isZero() { + return ZERO.equals(this); + } + + public UInt32 add(UInt32 value) { + if (value.isZero()) { + return this; + } + return create(this.value + value.value); + } + + public UInt32 add(int value) { + if (value == 0) { + return this; + } + return create(this.value + value); + } + + public UInt32 addMod(UInt32 value, UInt32 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return create(toBigInteger().add(value.toBigInteger()).mod(modulus.toBigInteger()).intValue()); + } + + public UInt32 addMod(long value, UInt32 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return create( + toBigInteger().add(BigInteger.valueOf(value)).mod(modulus.toBigInteger()).intValue()); + } + + public UInt32 addMod(long value, long modulus) { + if (modulus == 0) { + throw new ArithmeticException("addMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("addMod unsigned with negative modulus"); + } + return create( + toBigInteger().add(BigInteger.valueOf(value)).mod(BigInteger.valueOf(modulus)).intValue()); + } + + public UInt32 subtract(UInt32 value) { + if (value.isZero()) { + return this; + } + + return create(this.value - value.value); + } + + public UInt32 subtract(int value) { + if (value == 0) { + return this; + } + return create(this.value - value); + } + + public UInt32 multiply(UInt32 value) { + return create(this.value * value.value); + } + + public UInt32 multiply(int value) { + if (value < 0) { + throw new ArithmeticException("multiply unsigned by negative"); + } + if (value == 0 || isZero()) { + return ZERO; + } + return multiply(UInt32.valueOf(value)); + } + + public UInt32 multiplyMod(UInt32 value, UInt32 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (isZero() || value.isZero()) { + return ZERO; + } + if (ONE.equals(value)) { + return mod(modulus); + } + return create( + toBigInteger().multiply(value.toBigInteger()).mod(modulus.toBigInteger()).intValue()); + } + + public UInt32 multiplyMod(int value, UInt32 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (value == 0 || this.isZero()) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return create( + toBigInteger().multiply(BigInteger.valueOf(value)).mod(modulus.toBigInteger()).intValue()); + } + + public UInt32 multiplyMod(int value, int modulus) { + if (modulus == 0) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("multiplyMod unsigned with negative modulus"); + } + if (value == 0 || this.isZero()) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return create( + toBigInteger() + .multiply(BigInteger.valueOf(value)) + .mod(BigInteger.valueOf(modulus)) + .intValue()); + } + + public UInt32 divide(UInt32 value) { + if (value.isZero()) { + throw new ArithmeticException("divide by zero"); + } + + if (value.equals(ONE)) { + return this; + } + return create(toBigInteger().divide(value.toBigInteger()).intValue()); + } + + public UInt32 divide(int value) { + if (value == 0) { + throw new ArithmeticException("divide by zero"); + } + if (value < 0) { + throw new ArithmeticException("divide unsigned by negative"); + } + if (value == 1) { + return this; + } + if (isPowerOf2(value)) { + return fromBytes(mutableCopy().shiftRight(log2(value))); + } + return create(toBigInteger().divide(BigInteger.valueOf(value)).intValue()); + } + + public UInt32 pow(UInt32 exponent) { + return create(toBigInteger().modPow(exponent.toBigInteger(), P_2_32).intValue()); + } + + public UInt32 pow(long exponent) { + return create(toBigInteger().modPow(BigInteger.valueOf(exponent), P_2_32).intValue()); + } + + public UInt32 mod(UInt32 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("mod by zero"); + } + return create(Integer.remainderUnsigned(this.value, modulus.value)); + } + + public UInt32 mod(int modulus) { + if (modulus == 0) { + throw new ArithmeticException("mod by zero"); + } + if (modulus < 0) { + throw new ArithmeticException("mod by negative"); + } + return create(Integer.remainderUnsigned(this.value, modulus)); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (!(object instanceof UInt32 other)) { + return false; + } + return this.value == other.value; + } + + @Override + public int computeHashcode() { + return Integer.hashCode(this.value); + } + + public int compareTo(UInt32 other) { + return Integer.compareUnsigned(this.value, other.value); + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (get(i) & bytesArray[offset + i]); + } + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (get(i) | bytesArray[offset + i]); + } + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (get(i) ^ bytesArray[offset + i]); + } + } + + @Override + public byte get(int i) { + checkElementIndex(i, size()); + return Utils.unpackByte(value, i); + } + + @Override + public BigInteger toBigInteger() { + byte[] mag = new byte[4]; + mag[0] = (byte) (this.value >>> 24); + mag[1] = (byte) (this.value >>> 16); + mag[2] = (byte) (this.value >>> 8); + mag[3] = (byte) this.value; + return new BigInteger(1, mag); + } + + public UInt32 toUInt32() { + return this; + } + + public Bytes toBytes() { + return Bytes.wrap(toArrayUnsafe()); + } + + public Bytes toMinimalBytes() { + int numberOfLeadingZeroBytes = Integer.numberOfLeadingZeros(this.value) / 8; + return slice(numberOfLeadingZeroBytes); + } + + @Override + public int bitLength() { + return 32 - numberOfLeadingZeros(); + } + + @Override + public Bytes slice(int i, int length) { + return toBytes().slice(i, length); + } + + @Override + public MutableBytes mutableCopy() { + return MutableBytes.fromArray(toArrayUnsafe()); + } + + @Override + public byte[] toArrayUnsafe() { + return new byte[] { + (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value + }; + } + + private static boolean isPowerOf2(long n) { + assert n > 0; + return (n & (n - 1)) == 0; + } + + private static int log2(int v) { + assert v > 0; + return 31 - Integer.numberOfLeadingZeros(v); + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value the amount to be added to this value + * @return {@code this + value} + * @throws ArithmeticException if the result of the addition overflows + */ + UInt32 addExact(UInt32 value) { + UInt32 result = add(value); + if (compareTo(result) > 0) { + throw new ArithmeticException("UInt32 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value the amount to be added to this value + * @return {@code this + value} + * @throws ArithmeticException if the result of the addition overflows + */ + UInt32 addExact(int value) { + UInt32 result = add(value); + if ((value > 0 && compareTo(result) > 0) || (value < 0 && compareTo(result) < 0)) { + throw new ArithmeticException("UInt32 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value the amount to be subtracted to this value + * @return {@code this - value} + * @throws ArithmeticException if the result of the subtraction overflows + */ + public UInt32 subtractExact(UInt32 value) { + UInt32 result = subtract(value); + if (compareTo(result) < 0) { + throw new ArithmeticException("UInt32 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value the amount to be subtracted to this value + * @return {@code this - value} + * @throws ArithmeticException if the result of the subtraction overflows + */ + public UInt32 subtractExact(int value) { + UInt32 result = subtract(value); + if ((value > 0 && compareTo(result) < 0) || (value < 0 && compareTo(result) > 0)) { + throw new ArithmeticException("UInt32 overflow"); + } + return result; + } + + /** + * Returns the decimal representation of this value as a String. + * + * @return the decimal representation of this value as a String. + */ + public String toDecimalString() { + return toBigInteger().toString(10); + } +} diff --git a/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt384.java b/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt384.java new file mode 100644 index 000000000..3b02eedd8 --- /dev/null +++ b/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt384.java @@ -0,0 +1,877 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.units.bigints; + +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes48; +import org.apache.tuweni.v2.bytes.MutableBytes; + +import java.math.BigInteger; +import java.util.Arrays; + +/** + * An unsigned 384-bit precision number. + * + *

This is a 384-bit precision unsigned number of no particular unit. + */ +public final class UInt384 extends Bytes { + private static final int MAX_CONSTANT = 64; + private static final BigInteger BI_MAX_CONSTANT = BigInteger.valueOf(MAX_CONSTANT); + private static UInt384[] CONSTANTS = new UInt384[MAX_CONSTANT + 1]; + + static { + CONSTANTS[0] = new UInt384(Bytes48.ZERO); + for (int i = 1; i <= MAX_CONSTANT; ++i) { + CONSTANTS[i] = new UInt384(i); + } + } + + /** The minimum value of a UInt384 */ + public static final UInt384 MIN_VALUE = valueOf(0); + + /** The maximum value of a UInt384 */ + public static final UInt384 MAX_VALUE = new UInt384(Bytes48.ZERO.mutableCopy().not()); + + /** The value 0 */ + public static final UInt384 ZERO = valueOf(0); + + /** The value 1 */ + public static final UInt384 ONE = valueOf(1); + + private static final int INTS_SIZE = 48 / 4; + // The mask is used to obtain the value of an int as if it were unsigned. + private static final long LONG_MASK = 0xFFFFFFFFL; + private static final BigInteger P_2_384 = BigInteger.valueOf(2).pow(384); + + // The unsigned int components of the value + private final int[] ints; + + /** + * Return a {@code UInt384} containing the specified value. + * + * @param value The value to create a {@code UInt384} for. + * @return A {@code UInt384} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static UInt384 valueOf(long value) { + if (value < 0) { + throw new IllegalArgumentException("Argument must be positive"); + } + if (value <= MAX_CONSTANT) { + return CONSTANTS[(int) value]; + } + return new UInt384(value); + } + + /** + * Return a {@link UInt384} containing the specified value. + * + * @param value the value to create a {@link UInt384} for + * @return a {@link UInt384} containing the specified value + * @throws IllegalArgumentException if the value is negative or too large to be represented as a + * UInt384 + */ + public static UInt384 valueOf(BigInteger value) { + if (value.signum() < 0) { + throw new IllegalArgumentException("Argument must be positive"); + } + if (value.bitLength() > 384) { + throw new IllegalArgumentException("Argument is too large to represent a UInt384"); + } + if (value.compareTo(BI_MAX_CONSTANT) <= 0) { + return CONSTANTS[value.intValue()]; + } + int[] ints = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + ints[i] = value.intValue(); + value = value.shiftRight(32); + } + return new UInt384(ints); + } + + /** + * Return a {@link UInt384} containing the value described by the specified bytes. + * + * @param bytes The bytes containing a {@link UInt384}. + * @return A {@link UInt384} containing the specified value. + * @throws IllegalArgumentException if {@code bytes.size() > 48}. + */ + public static UInt384 fromBytes(Bytes bytes) { + return new UInt384(bytes.mutableCopy().leftPad(48)); + } + + /** + * Parse a hexadecimal string into a {@link UInt384}. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". That + * representation may contain less than 48 bytes, in which case the result is left padded with + * zeros. + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation or contains more than 48 bytes. + */ + public static UInt384 fromHexString(String str) { + return new UInt384(Bytes48.fromHexStringLenient(str)); + } + + private UInt384(Bytes bytes) { + super(48); + this.ints = new int[INTS_SIZE]; + for (int i = 0, j = 0; i < INTS_SIZE; ++i, j += 4) { + ints[i] = bytes.getInt(j); + } + } + + private UInt384(long value) { + super(48); + this.ints = new int[INTS_SIZE]; + this.ints[INTS_SIZE - 2] = (int) ((value >>> 32) & LONG_MASK); + this.ints[INTS_SIZE - 1] = (int) (value & LONG_MASK); + } + + private UInt384(int[] ints) { + super(48); + this.ints = ints; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isZero() { + if (this == ZERO) { + return true; + } + for (int i = INTS_SIZE - 1; i >= 0; --i) { + if (this.ints[i] != 0) { + return false; + } + } + return true; + } + + public UInt384 add(UInt384 value) { + if (value.isZero()) { + return this; + } + if (isZero()) { + return value; + } + int[] result = new int[INTS_SIZE]; + boolean constant = true; + long sum = (this.ints[INTS_SIZE - 1] & LONG_MASK) + (value.ints[INTS_SIZE - 1] & LONG_MASK); + result[INTS_SIZE - 1] = (int) (sum & LONG_MASK); + if (result[INTS_SIZE - 1] < 0 || result[INTS_SIZE - 1] > MAX_CONSTANT) { + constant = false; + } + for (int i = INTS_SIZE - 2; i >= 0; --i) { + sum = (this.ints[i] & LONG_MASK) + (value.ints[i] & LONG_MASK) + (sum >>> 32); + result[i] = (int) (sum & LONG_MASK); + constant &= result[i] == 0; + } + if (constant) { + return CONSTANTS[result[INTS_SIZE - 1]]; + } + return new UInt384(result); + } + + public UInt384 add(long value) { + if (value == 0) { + return this; + } + if (value > 0 && isZero()) { + return UInt384.valueOf(value); + } + int[] result = new int[INTS_SIZE]; + boolean constant = true; + long sum = (this.ints[INTS_SIZE - 1] & LONG_MASK) + (value & LONG_MASK); + result[INTS_SIZE - 1] = (int) (sum & LONG_MASK); + if (result[INTS_SIZE - 1] < 0 || result[INTS_SIZE - 1] > MAX_CONSTANT) { + constant = false; + } + sum = (this.ints[INTS_SIZE - 2] & LONG_MASK) + (value >>> 32) + (sum >>> 32); + result[INTS_SIZE - 2] = (int) (sum & LONG_MASK); + constant &= result[INTS_SIZE - 2] == 0; + long signExtent = (value >> 63) & LONG_MASK; + for (int i = INTS_SIZE - 3; i >= 0; --i) { + sum = (this.ints[i] & LONG_MASK) + signExtent + (sum >>> 32); + result[i] = (int) (sum & LONG_MASK); + constant &= result[i] == 0; + } + if (constant) { + return CONSTANTS[result[INTS_SIZE - 1]]; + } + return new UInt384(result); + } + + public UInt384 addMod(UInt384 value, UInt384 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return UInt384.valueOf(toBigInteger().add(value.toBigInteger()).mod(modulus.toBigInteger())); + } + + public UInt384 addMod(long value, UInt384 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return UInt384.valueOf( + toBigInteger().add(BigInteger.valueOf(value)).mod(modulus.toBigInteger())); + } + + public UInt384 addMod(long value, long modulus) { + if (modulus == 0) { + throw new ArithmeticException("addMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("addMod unsigned with negative modulus"); + } + return UInt384.valueOf( + toBigInteger().add(BigInteger.valueOf(value)).mod(BigInteger.valueOf(modulus))); + } + + public UInt384 subtract(UInt384 value) { + if (value.isZero()) { + return this; + } + + int[] result = new int[INTS_SIZE]; + boolean constant = true; + long sum = + (this.ints[INTS_SIZE - 1] & LONG_MASK) + ((~value.ints[INTS_SIZE - 1]) & LONG_MASK) + 1; + result[INTS_SIZE - 1] = (int) (sum & LONG_MASK); + if (result[INTS_SIZE - 1] < 0 || result[INTS_SIZE - 1] > MAX_CONSTANT) { + constant = false; + } + for (int i = INTS_SIZE - 2; i >= 0; --i) { + sum = (this.ints[i] & LONG_MASK) + ((~value.ints[i]) & LONG_MASK) + (sum >>> 32); + result[i] = (int) (sum & LONG_MASK); + constant &= result[i] == 0; + } + if (constant) { + return CONSTANTS[result[INTS_SIZE - 1]]; + } + return new UInt384(result); + } + + public UInt384 subtract(long value) { + return add(-value); + } + + public UInt384 multiply(UInt384 value) { + if (isZero() || value.isZero()) { + return ZERO; + } + if (value.equals(UInt384.ONE)) { + return this; + } + return multiply(this.ints, value.ints); + } + + private static UInt384 multiply(int[] x, int[] y) { + int[] result = new int[INTS_SIZE + INTS_SIZE]; + + long carry = 0; + for (int j = INTS_SIZE - 1, k = INTS_SIZE + INTS_SIZE - 1; j >= 0; j--, k--) { + long product = (y[j] & LONG_MASK) * (x[INTS_SIZE - 1] & LONG_MASK) + carry; + result[k] = (int) product; + carry = product >>> 32; + } + result[INTS_SIZE - 1] = (int) carry; + + for (int i = INTS_SIZE - 2; i >= 0; i--) { + carry = 0; + for (int j = INTS_SIZE - 1, k = INTS_SIZE + i; j >= 0; j--, k--) { + long product = (y[j] & LONG_MASK) * (x[i] & LONG_MASK) + (result[k] & LONG_MASK) + carry; + + result[k] = (int) product; + carry = product >>> 32; + } + result[i] = (int) carry; + } + + boolean constant = true; + for (int i = INTS_SIZE; i < (INTS_SIZE + INTS_SIZE) - 2; ++i) { + constant &= (result[i] == 0); + } + if (constant + && result[INTS_SIZE + INTS_SIZE - 1] >= 0 + && result[INTS_SIZE + INTS_SIZE - 1] <= MAX_CONSTANT) { + return CONSTANTS[result[INTS_SIZE + INTS_SIZE - 1]]; + } + return new UInt384(Arrays.copyOfRange(result, INTS_SIZE, INTS_SIZE + INTS_SIZE)); + } + + public UInt384 multiply(long value) { + if (value == 0 || isZero()) { + return ZERO; + } + if (value == 1) { + return this; + } + if (value < 0) { + throw new ArithmeticException("multiply unsigned by negative"); + } + UInt384 other = new UInt384(value); + return multiply(this.ints, other.ints); + } + + public UInt384 multiplyMod(UInt384 value, UInt384 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (isZero() || value.isZero()) { + return ZERO; + } + if (value.equals(UInt384.ONE)) { + return mod(modulus); + } + return UInt384.valueOf( + toBigInteger().multiply(value.toBigInteger()).mod(modulus.toBigInteger())); + } + + public UInt384 multiplyMod(long value, UInt384 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (value == 0 || isZero()) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return UInt384.valueOf( + toBigInteger().multiply(BigInteger.valueOf(value)).mod(modulus.toBigInteger())); + } + + public UInt384 multiplyMod(long value, long modulus) { + if (modulus == 0) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("multiplyMod unsigned with negative modulus"); + } + if (value == 0 || isZero()) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return UInt384.valueOf( + toBigInteger().multiply(BigInteger.valueOf(value)).mod(BigInteger.valueOf(modulus))); + } + + public UInt384 divide(UInt384 value) { + if (value.isZero()) { + throw new ArithmeticException("divide by zero"); + } + if (value.equals(UInt384.ONE)) { + return this; + } + return UInt384.valueOf(toBigInteger().divide(value.toBigInteger())); + } + + public UInt384 divide(long value) { + if (value == 0) { + throw new ArithmeticException("divide by zero"); + } + if (value < 0) { + throw new ArithmeticException("divide unsigned by negative"); + } + if (value == 1) { + return this; + } + if (isPowerOf2(value)) { + return shiftRight(log2(value)); + } + return UInt384.valueOf(toBigInteger().divide(BigInteger.valueOf(value))); + } + + public UInt384 pow(UInt384 exponent) { + return UInt384.valueOf(toBigInteger().modPow(exponent.toBigInteger(), P_2_384)); + } + + public UInt384 pow(long exponent) { + return UInt384.valueOf(toBigInteger().modPow(BigInteger.valueOf(exponent), P_2_384)); + } + + public UInt384 mod(UInt384 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("mod by zero"); + } + return UInt384.valueOf(toBigInteger().mod(modulus.toBigInteger())); + } + + public UInt384 mod(long modulus) { + if (modulus == 0) { + throw new ArithmeticException("mod by zero"); + } + if (modulus < 0) { + throw new ArithmeticException("mod by negative"); + } + if (isPowerOf2(modulus)) { + int log2 = log2(modulus); + int d = log2 / 32; + int s = log2 % 32; + assert (d == 0 || d == 1); + + int[] result = new int[INTS_SIZE]; + // Mask the byte at d to only include the s right-most bits + result[INTS_SIZE - 1 - d] = this.ints[INTS_SIZE - 1 - d] & ~(0xFFFFFFFF << s); + if (d != 0) { + result[INTS_SIZE - 1] = this.ints[INTS_SIZE - 1]; + } + return new UInt384(result); + } + return UInt384.valueOf(toBigInteger().mod(BigInteger.valueOf(modulus))); + } + + /** + * Return a bit-wise AND of this value and the supplied value. + * + * @param value the value to perform the operation with + * @return the result of a bit-wise AND + */ + public UInt384 and(UInt384 value) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = this.ints[i] & value.ints[i]; + } + return new UInt384(result); + } + + /** + * Return a bit-wise AND of this value and the supplied bytes. + * + * @param bytes the bytes to perform the operation with + * @return the result of a bit-wise AND + */ + public UInt384 and(Bytes48 bytes) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1, j = 44; i >= 0; --i, j -= 4) { + int other = ((int) bytes.get(j) & 0xFF) << 24; + other |= ((int) bytes.get(j + 1) & 0xFF) << 16; + other |= ((int) bytes.get(j + 2) & 0xFF) << 8; + other |= ((int) bytes.get(j + 3) & 0xFF); + result[i] = this.ints[i] & other; + } + return new UInt384(result); + } + + /** + * Return a bit-wise OR of this value and the supplied value. + * + * @param value the value to perform the operation with + * @return the result of a bit-wise OR + */ + public UInt384 or(UInt384 value) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = this.ints[i] | value.ints[i]; + } + return new UInt384(result); + } + + /** + * Return a bit-wise OR of this value and the supplied bytes. + * + * @param bytes the bytes to perform the operation with + * @return the result of a bit-wise OR + */ + public UInt384 or(Bytes48 bytes) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1, j = 44; i >= 0; --i, j -= 4) { + int other = ((int) bytes.get(j) & 0xFF) << 24; + other |= ((int) bytes.get(j + 1) & 0xFF) << 16; + other |= ((int) bytes.get(j + 2) & 0xFF) << 8; + other |= ((int) bytes.get(j + 3) & 0xFF); + result[i] = this.ints[i] | other; + } + return new UInt384(result); + } + + /** + * Return a bit-wise XOR of this value and the supplied value. + * + * @param value the value to perform the operation with + * @return the result of a bit-wise XOR + */ + public UInt384 xor(UInt384 value) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = this.ints[i] ^ value.ints[i]; + } + return new UInt384(result); + } + + /** + * Return a bit-wise XOR of this value and the supplied bytes. + * + * @param bytes the bytes to perform the operation with + * @return the result of a bit-wise XOR + */ + public UInt384 xor(Bytes48 bytes) { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1, j = 44; i >= 0; --i, j -= 4) { + int other = ((int) bytes.get(j) & 0xFF) << 24; + other |= ((int) bytes.get(j + 1) & 0xFF) << 16; + other |= ((int) bytes.get(j + 2) & 0xFF) << 8; + other |= ((int) bytes.get(j + 3) & 0xFF); + result[i] = this.ints[i] ^ other; + } + return new UInt384(result); + } + + /** + * Return a bit-wise NOT of this value. + * + * @return the result of a bit-wise NOT + */ + public UInt384 not() { + int[] result = new int[INTS_SIZE]; + for (int i = INTS_SIZE - 1; i >= 0; --i) { + result[i] = ~(this.ints[i]); + } + return new UInt384(result); + } + + /** + * Shift all bits in this value to the right. + * + * @param distance The number of bits to shift by. + * @return A value containing the shifted bits. + */ + public UInt384 shiftRight(int distance) { + if (distance == 0) { + return this; + } + if (distance >= 384) { + return ZERO; + } + int[] result = new int[INTS_SIZE]; + int d = distance / 32; + int s = distance % 32; + + int resIdx = INTS_SIZE; + if (s == 0) { + for (int i = INTS_SIZE - d; i > 0; ) { + result[--resIdx] = this.ints[--i]; + } + } else { + for (int i = INTS_SIZE - 1 - d; i >= 0; i--) { + int leftSide = this.ints[i] >>> s; + int rightSide = (i == 0) ? 0 : this.ints[i - 1] << (32 - s); + result[--resIdx] = (leftSide | rightSide); + } + } + return new UInt384(result); + } + + /** + * Shift all bits in this value to the left. + * + * @param distance The number of bits to shift by. + * @return A value containing the shifted bits. + */ + public UInt384 shiftLeft(int distance) { + if (distance == 0) { + return this; + } + if (distance >= 384) { + return ZERO; + } + int[] result = new int[INTS_SIZE]; + int d = distance / 32; + int s = distance % 32; + + int resIdx = 0; + if (s == 0) { + for (int i = d; i < INTS_SIZE; ) { + result[resIdx++] = this.ints[i++]; + } + } else { + for (int i = d; i < INTS_SIZE; ++i) { + int leftSide = this.ints[i] << s; + int rightSide = (i == INTS_SIZE - 1) ? 0 : (this.ints[i + 1] >>> (32 - s)); + result[resIdx++] = (leftSide | rightSide); + } + } + return new UInt384(result); + } + + @Override + public Bytes slice(int i, int length) { + return mutableCopy().slice(i, length); + } + + @Override + public MutableBytes mutableCopy() { + return MutableBytes.fromArray(toArrayUnsafe()); + } + + @Override + public byte[] toArrayUnsafe() { + byte[] byteArray = new byte[size()]; + int j = 0; + for (int i = 0; i < INTS_SIZE; i++) { + byteArray[j] = (byte) (ints[i] >> 24); + byteArray[j + 1] = (byte) (ints[i] >> 16); + byteArray[j + 2] = (byte) (ints[i] >> 8); + byteArray[j + 3] = (byte) ints[i]; + j += 4; + } + return byteArray; + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (!(object instanceof UInt384 other)) { + return false; + } + for (int i = 0; i < INTS_SIZE; ++i) { + if (this.ints[i] != other.ints[i]) { + return false; + } + } + return true; + } + + @Override + public int computeHashcode() { + int result = 1; + for (int i = 0; i < INTS_SIZE; ++i) { + result = 31 * result + this.ints[i]; + } + return result; + } + + public int compareTo(UInt384 other) { + for (int i = 0; i < INTS_SIZE; ++i) { + int cmp = Long.compare(((long) this.ints[i]) & LONG_MASK, ((long) other.ints[i]) & LONG_MASK); + if (cmp != 0) { + return cmp; + } + } + return 0; + } + + public boolean fitsInt() { + for (int i = 0; i < INTS_SIZE - 1; i++) { + if (this.ints[i] != 0) { + return false; + } + } + // Lastly, the left-most byte of the int must not start with a 1. + return this.ints[INTS_SIZE - 1] >= 0; + } + + public int intValue() { + if (!fitsInt()) { + throw new ArithmeticException("Value does not fit a 4 byte int"); + } + return this.ints[INTS_SIZE - 1]; + } + + public boolean fitsLong() { + for (int i = 0; i < INTS_SIZE - 2; i++) { + if (this.ints[i] != 0) { + return false; + } + } + // Lastly, the left-most byte of the int must not start with a 1. + return this.ints[INTS_SIZE - 2] >= 0; + } + + @Override + public byte get(int i) { + checkElementIndex(i, size()); + return Utils.unpackByte(ints, i); + } + + @Override + public long toLong() { + if (!fitsLong()) { + throw new ArithmeticException("Value does not fit a 8 byte long"); + } + return (((long) this.ints[INTS_SIZE - 2]) << 32) + | (((long) (this.ints[INTS_SIZE - 1])) & LONG_MASK); + } + + @Override + public String toString() { + return toBigInteger().toString(); + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + Utils.and(ints, 0, bytesArray, offset, length); + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + Utils.or(ints, 0, bytesArray, offset, length); + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + Utils.xor(ints, 0, bytesArray, offset, length); + } + + @Override + public BigInteger toBigInteger() { + byte[] mag = new byte[48]; + for (int i = 0, j = 0; i < INTS_SIZE; ++i) { + mag[j++] = (byte) (this.ints[i] >>> 24); + mag[j++] = (byte) ((this.ints[i] >>> 16) & 0xFF); + mag[j++] = (byte) ((this.ints[i] >>> 8) & 0xFF); + mag[j++] = (byte) (this.ints[i] & 0xFF); + } + return new BigInteger(1, mag); + } + + public UInt384 toUInt384() { + return this; + } + + public Bytes toMinimalBytes() { + int i = 0; + while (i < INTS_SIZE && this.ints[i] == 0) { + ++i; + } + if (i == INTS_SIZE) { + return Bytes.EMPTY; + } + int firstIntBytes = 4 - (Integer.numberOfLeadingZeros(this.ints[i]) / 8); + int totalBytes = firstIntBytes + ((INTS_SIZE - (i + 1)) * 4); + MutableBytes bytes = MutableBytes.create(totalBytes); + int j = 0; + switch (firstIntBytes) { + case 4: + bytes.set(j++, (byte) (this.ints[i] >>> 24)); + // fall through + case 3: + bytes.set(j++, (byte) ((this.ints[i] >>> 16) & 0xFF)); + // fall through + case 2: + bytes.set(j++, (byte) ((this.ints[i] >>> 8) & 0xFF)); + // fall through + case 1: + bytes.set(j++, (byte) (this.ints[i] & 0xFF)); + } + ++i; + for (; i < INTS_SIZE; ++i, j += 4) { + bytes.setInt(j, this.ints[i]); + } + return bytes; + } + + @Override + public int numberOfLeadingZeros() { + for (int i = 0; i < INTS_SIZE; i++) { + if (this.ints[i] == 0) { + continue; + } + return (i * 32) + Integer.numberOfLeadingZeros(this.ints[i]); + } + return 384; + } + + @Override + public int bitLength() { + for (int i = 0; i < INTS_SIZE; i++) { + if (this.ints[i] == 0) { + continue; + } + return (INTS_SIZE * 32) - (i * 32) - Integer.numberOfLeadingZeros(this.ints[i]); + } + return 0; + } + + private static boolean isPowerOf2(long n) { + assert n > 0; + return (n & (n - 1)) == 0; + } + + private static int log2(long v) { + assert v > 0; + return 63 - Long.numberOfLeadingZeros(v); + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value the amount to be added to this value + * @return {@code this + value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt384 addExact(UInt384 value) { + UInt384 result = add(value); + if (compareTo(result) > 0) { + throw new ArithmeticException("UInt384 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value the amount to be added to this value + * @return {@code this + value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt384 addExact(long value) { + UInt384 result = add(value); + if ((value > 0 && compareTo(result) > 0) || (value < 0 && compareTo(result) < 0)) { + throw new ArithmeticException("UInt384 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value the amount to be subtracted to this value + * @return {@code this - value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt384 subtractExact(UInt384 value) { + UInt384 result = subtract(value); + if (compareTo(result) < 0) { + throw new ArithmeticException("UInt384 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value the amount to be subtracted to this value + * @return {@code this - value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt384 subtractExact(long value) { + UInt384 result = subtract(value); + if ((value > 0 && compareTo(result) < 0) || (value < 0 && compareTo(result) > 0)) { + throw new ArithmeticException("UInt384 overflow"); + } + return result; + } + + /** + * Returns the decimal representation of this value as a String. + * + * @return the decimal representation of this value as a String. + */ + public String toDecimalString() { + return toBigInteger().toString(10); + } +} diff --git a/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt64.java b/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt64.java new file mode 100644 index 000000000..1a581ad3e --- /dev/null +++ b/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt64.java @@ -0,0 +1,541 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.units.bigints; + +import static org.apache.tuweni.v2.bytes.Utils.checkElementIndex; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.MutableBytes; + +import java.math.BigInteger; + +/** + * An unsigned 64-bit precision number. + * + *

This is a raw 64-bit precision unsigned number of no particular unit. + */ +public final class UInt64 extends Bytes { + private static final int MAX_CONSTANT = 64; + private static UInt64[] CONSTANTS = new UInt64[MAX_CONSTANT + 1]; + + static { + CONSTANTS[0] = new UInt64(0); + for (int i = 1; i <= MAX_CONSTANT; ++i) { + CONSTANTS[i] = new UInt64(i); + } + } + + /** The minimum value of a UInt64 */ + public static final UInt64 MIN_VALUE = valueOf(0); + + /** The maximum value of a UInt64 */ + public static final UInt64 MAX_VALUE = new UInt64(~0L); + + /** The value 0 */ + public static final UInt64 ZERO = valueOf(0); + + /** The value 1 */ + public static final UInt64 ONE = valueOf(1); + + private static final BigInteger P_2_64 = BigInteger.valueOf(2).pow(64); + + private final long value; + + /** + * Return a {@code UInt64} containing the specified value. + * + * @param value The value to create a {@code UInt64} for. + * @return A {@code UInt64} containing the specified value. + * @throws IllegalArgumentException If the value is negative. + */ + public static UInt64 valueOf(long value) { + if (value < 0) { + throw new IllegalArgumentException("Argument must be positive"); + } + return create(value); + } + + /** + * Return a {@link UInt64} containing a random value. + * + * @return a {@link UInt64} containing a random value + */ + public static UInt64 random() { + return UInt64.fromBytes(Bytes.random(8)); + } + + /** + * Return a {@link UInt64} containing the specified value. + * + * @param value the value to create a {@link UInt64} for + * @return a {@link UInt64} containing the specified value + * @throws IllegalArgumentException if the value is negative or too large to be represented as a + * UInt64 + */ + public static UInt64 valueOf(BigInteger value) { + if (value.signum() < 0) { + throw new IllegalArgumentException("Argument must be positive"); + } + if (value.bitLength() > 64) { + throw new IllegalArgumentException("Argument is too large to represent a UInt64"); + } + return create(value.longValue()); + } + + /** + * Return a {@link UInt64} containing the value described by the specified bytes. + * + * @param bytes The bytes containing a {@link UInt64}. + * @return A {@link UInt64} containing the specified value. + * @throws IllegalArgumentException if {@code bytes.size() > 8}. + */ + public static UInt64 fromBytes(Bytes bytes) { + if (bytes.size() > 8) { + throw new IllegalArgumentException("Argument is greater than 8 bytes"); + } + return create(bytes.toLong()); + } + + /** + * Parse a hexadecimal string into a {@link UInt64}. + * + * @param str The hexadecimal string to parse, which may or may not start with "0x". That + * representation may contain less than 8 bytes, in which case the result is left padded with + * zeros. + * @return The value corresponding to {@code str}. + * @throws IllegalArgumentException if {@code str} does not correspond to a valid hexadecimal + * representation or contains more than 8 bytes. + */ + public static UInt64 fromHexString(String str) { + return fromBytes(Bytes.fromHexStringLenient(str)); + } + + private static UInt64 create(long value) { + if (value >= 0 && value <= MAX_CONSTANT) { + return CONSTANTS[(int) value]; + } + return new UInt64(value); + } + + private UInt64(long value) { + super(8); + this.value = value; + } + + @Override + public boolean isZero() { + return this.value == 0; + } + + public UInt64 add(UInt64 value) { + if (value.value == 0) { + return this; + } + if (this.value == 0) { + return value; + } + return create(this.value + value.value); + } + + public UInt64 add(long value) { + if (value == 0) { + return this; + } + return create(this.value + value); + } + + public UInt64 addMod(UInt64 value, UInt64 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return create(toBigInteger().add(value.toBigInteger()).mod(modulus.toBigInteger()).longValue()); + } + + public UInt64 addMod(long value, UInt64 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("addMod with zero modulus"); + } + return create( + toBigInteger().add(BigInteger.valueOf(value)).mod(modulus.toBigInteger()).longValue()); + } + + public UInt64 addMod(long value, long modulus) { + if (modulus == 0) { + throw new ArithmeticException("addMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("addMod unsigned with negative modulus"); + } + return create( + toBigInteger().add(BigInteger.valueOf(value)).mod(BigInteger.valueOf(modulus)).longValue()); + } + + public UInt64 subtract(UInt64 value) { + if (value.isZero()) { + return this; + } + return create(this.value - value.value); + } + + public UInt64 subtract(long value) { + return add(-value); + } + + public UInt64 multiply(UInt64 value) { + if (this.value == 0 || value.value == 0) { + return ZERO; + } + if (value.value == 1) { + return this; + } + return create(this.value * value.value); + } + + public UInt64 multiply(long value) { + if (value < 0) { + throw new ArithmeticException("multiply unsigned by negative"); + } + if (value == 0 || this.value == 0) { + return ZERO; + } + if (value == 1) { + return this; + } + return create(this.value * value); + } + + public UInt64 multiplyMod(UInt64 value, UInt64 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (this.value == 0 || value.value == 0) { + return ZERO; + } + if (value.value == 1) { + return mod(modulus); + } + return create( + toBigInteger().multiply(value.toBigInteger()).mod(modulus.toBigInteger()).longValue()); + } + + public UInt64 multiplyMod(long value, UInt64 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (value == 0 || this.value == 0) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return create( + toBigInteger().multiply(BigInteger.valueOf(value)).mod(modulus.toBigInteger()).longValue()); + } + + public UInt64 multiplyMod(long value, long modulus) { + if (modulus == 0) { + throw new ArithmeticException("multiplyMod with zero modulus"); + } + if (modulus < 0) { + throw new ArithmeticException("multiplyMod unsigned with negative modulus"); + } + if (value == 0 || this.value == 0) { + return ZERO; + } + if (value == 1) { + return mod(modulus); + } + if (value < 0) { + throw new ArithmeticException("multiplyMod unsigned by negative"); + } + return create( + toBigInteger() + .multiply(BigInteger.valueOf(value)) + .mod(BigInteger.valueOf(modulus)) + .longValue()); + } + + public UInt64 divide(UInt64 value) { + if (value.value == 0) { + throw new ArithmeticException("divide by zero"); + } + if (value.value == 1) { + return this; + } + return create(toBigInteger().divide(value.toBigInteger()).longValue()); + } + + public UInt64 divide(long value) { + if (value == 0) { + throw new ArithmeticException("divide by zero"); + } + if (value < 0) { + throw new ArithmeticException("divide unsigned by negative"); + } + if (value == 1) { + return this; + } + if (isPowerOf2(value)) { + return fromBytes(mutableCopy().shiftRight(log2(value))); + } + return create(toBigInteger().divide(BigInteger.valueOf(value)).longValue()); + } + + public UInt64 pow(UInt64 exponent) { + return create(toBigInteger().modPow(exponent.toBigInteger(), P_2_64).longValue()); + } + + public UInt64 pow(long exponent) { + return create(toBigInteger().modPow(BigInteger.valueOf(exponent), P_2_64).longValue()); + } + + public UInt64 mod(UInt64 modulus) { + if (modulus.isZero()) { + throw new ArithmeticException("mod by zero"); + } + return create(toBigInteger().mod(modulus.toBigInteger()).longValue()); + } + + public UInt64 mod(long modulus) { + if (modulus == 0) { + throw new ArithmeticException("mod by zero"); + } + if (modulus < 0) { + throw new ArithmeticException("mod by negative"); + } + return create(this.value % modulus); + } + + @Override + public boolean equals(Object object) { + if (object == this) { + return true; + } + if (!(object instanceof UInt64 other)) { + return false; + } + return this.value == other.value; + } + + @Override + public int computeHashcode() { + return Long.hashCode(this.value); + } + + public int compareTo(UInt64 other) { + return Long.compareUnsigned(this.value, other.value); + } + + public boolean fitsInt() { + return this.value >= 0 && this.value <= Integer.MAX_VALUE; + } + + public int intValue() { + if (!fitsInt()) { + throw new ArithmeticException("Value does not fit a 4 byte int"); + } + return (int) this.value; + } + + public boolean fitsLong() { + return this.value >= 0; + } + + @Override + public byte get(int i) { + checkElementIndex(i, size()); + return Utils.unpackByte(value, i); + } + + @Override + public long toLong() { + if (!fitsLong()) { + throw new ArithmeticException("Value does not fit a 8 byte long"); + } + return this.value; + } + + @Override + public BigInteger toBigInteger() { + return new BigInteger(1, toArrayUnsafe()); + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (get(i) & bytesArray[offset + i]); + } + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (get(i) | bytesArray[offset + i]); + } + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + for (int i = 0; i < length; i++) { + bytesArray[offset + i] = (byte) (get(i) ^ bytesArray[offset + i]); + } + } + + public UInt64 toUInt64() { + return this; + } + + public Bytes toBytes() { + MutableBytes bytes = MutableBytes.create(8); + bytes.setLong(0, this.value); + return bytes; + } + + public Bytes toMinimalBytes() { + int requiredBytes = 8 - (Long.numberOfLeadingZeros(this.value) / 8); + MutableBytes bytes = MutableBytes.create(requiredBytes); + int j = 0; + switch (requiredBytes) { + case 8: + bytes.set(j++, (byte) (this.value >>> 56)); + // fall through + case 7: + bytes.set(j++, (byte) ((this.value >>> 48) & 0xFF)); + // fall through + case 6: + bytes.set(j++, (byte) ((this.value >>> 40) & 0xFF)); + // fall through + case 5: + bytes.set(j++, (byte) ((this.value >>> 32) & 0xFF)); + // fall through + case 4: + bytes.set(j++, (byte) ((this.value >>> 24) & 0xFF)); + // fall through + case 3: + bytes.set(j++, (byte) ((this.value >>> 16) & 0xFF)); + // fall through + case 2: + bytes.set(j++, (byte) ((this.value >>> 8) & 0xFF)); + // fall through + case 1: + bytes.set(j, (byte) (this.value & 0xFF)); + } + return bytes; + } + + @Override + public int numberOfLeadingZeros() { + return Long.numberOfLeadingZeros(this.value); + } + + @Override + public int bitLength() { + return 64 - Long.numberOfLeadingZeros(this.value); + } + + @Override + public Bytes slice(int i, int length) { + return toBytes().slice(i, length); + } + + @Override + public MutableBytes mutableCopy() { + return MutableBytes.fromArray(toArrayUnsafe()); + } + + @Override + public byte[] toArrayUnsafe() { + byte[] bytesArray = new byte[8]; + bytesArray[0] = (byte) ((this.value >>> 56) & 0xFF); + bytesArray[1] = (byte) ((this.value >>> 48) & 0xFF); + bytesArray[2] = (byte) ((this.value >>> 40) & 0xFF); + bytesArray[3] = (byte) ((this.value >>> 32) & 0xFF); + bytesArray[4] = (byte) ((this.value >>> 24) & 0xFF); + bytesArray[5] = (byte) ((this.value >>> 16) & 0xFF); + bytesArray[6] = (byte) ((this.value >>> 8) & 0xFF); + bytesArray[7] = (byte) (this.value & 0xFF); + return bytesArray; + } + + private static boolean isPowerOf2(long n) { + assert n > 0; + return (n & (n - 1)) == 0; + } + + private static int log2(long v) { + assert v > 0; + return 63 - Long.numberOfLeadingZeros(v); + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value the amount to be added to this value + * @return {@code this + value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt64 addExact(UInt64 value) { + UInt64 result = add(value); + if (compareTo(result) > 0) { + throw new ArithmeticException("UInt64 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this + value)}. + * + * @param value the amount to be added to this value + * @return {@code this + value} + * @throws ArithmeticException if the result of the addition overflows + */ + public UInt64 addExact(long value) { + UInt64 result = add(value); + if ((value > 0 && compareTo(result) > 0) || (value < 0 && compareTo(result) < 0)) { + throw new ArithmeticException("UInt64 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value the amount to be subtracted to this value + * @return {@code this - value} + * @throws ArithmeticException if the result of the subtraction overflows + */ + public UInt64 subtractExact(UInt64 value) { + UInt64 result = subtract(value); + if (compareTo(result) < 0) { + throw new ArithmeticException("UInt64 overflow"); + } + return result; + } + + /** + * Returns a value that is {@code (this - value)}. + * + * @param value the amount to be subtracted to this value + * @return {@code this - value} + * @throws ArithmeticException if the result of the subtraction overflows + */ + public UInt64 subtractExact(long value) { + UInt64 result = subtract(value); + if ((value > 0 && compareTo(result) < 0) || (value < 0 && compareTo(result) > 0)) { + throw new ArithmeticException("UInt64 overflow"); + } + return result; + } + + /** + * Returns the decimal representation of this value as a String. + * + * @return the decimal representation of this value as a String. + */ + public String toDecimalString() { + return toBigInteger().toString(10); + } +} diff --git a/units/src/main/java/org/apache/tuweni/v2/units/bigints/Utils.java b/units/src/main/java/org/apache/tuweni/v2/units/bigints/Utils.java new file mode 100644 index 000000000..cbd062374 --- /dev/null +++ b/units/src/main/java/org/apache/tuweni/v2/units/bigints/Utils.java @@ -0,0 +1,47 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.units.bigints; + +public class Utils { + static void and( + int[] sourceBytesArray, int sourceOffset, byte[] destBytesArray, int destOffset, int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + destBytesArray[destOffset + i] = + (byte) (unpackByte(sourceBytesArray, sourceOffset + i) & destBytesArray[destOffset + i]); + } + } + + static void or( + int[] sourceBytesArray, int sourceOffset, byte[] destBytesArray, int destOffset, int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + destBytesArray[destOffset + i] = + (byte) (unpackByte(sourceBytesArray, sourceOffset + i) | destBytesArray[destOffset + i]); + } + } + + static void xor( + int[] sourceBytesArray, int sourceOffset, byte[] destBytesArray, int destOffset, int length) { + for (int i = 0; i < length; i++) { + // TODO: Speed this up with SIMD + destBytesArray[destOffset + i] = + (byte) (unpackByte(sourceBytesArray, sourceOffset + i) ^ destBytesArray[destOffset + i]); + } + } + + static byte unpackByte(int[] ints, int index) { + int whichInt = index / 4; + return unpackByte(ints[whichInt], index); + } + + static byte unpackByte(int integer, int index) { + int whichIndex = 3 - index % 4; + return (byte) ((integer >> (8 * whichIndex)) & 0xFF); + } + + static byte unpackByte(long value, int index) { + int whichIndex = 7 - index; + return (byte) ((value >> (8 * whichIndex)) & 0xFF); + } +} diff --git a/units/src/main/java/org/apache/tuweni/v2/units/bigints/package-info.java b/units/src/main/java/org/apache/tuweni/v2/units/bigints/package-info.java new file mode 100644 index 000000000..261b20a94 --- /dev/null +++ b/units/src/main/java/org/apache/tuweni/v2/units/bigints/package-info.java @@ -0,0 +1,20 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You 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. + * + *

Classes and utilities for working with 256 bit integers. + */ +@ParametersAreNonnullByDefault +package org.apache.tuweni.v2.units.bigints; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/units/src/main/java/org/apache/tuweni/v2/units/package-info.java b/units/src/main/java/org/apache/tuweni/v2/units/package-info.java new file mode 100644 index 000000000..61c9fa3b8 --- /dev/null +++ b/units/src/main/java/org/apache/tuweni/v2/units/package-info.java @@ -0,0 +1,20 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You 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. + * + *

Classes and utilities for working with 256 bit integers and Ethereum units. + * + *

These classes are included in the standard Tuweni distribution, or separately when using the + * gradle dependency 'org.apache.tuweni:tuweni-units' (tuweni-units.jar). + */ +package org.apache.tuweni.v2.units; diff --git a/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt256Test.java b/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt256Test.java new file mode 100644 index 000000000..af8a526df --- /dev/null +++ b/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt256Test.java @@ -0,0 +1,1245 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.units.bigints; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes32; +import org.apache.tuweni.v2.bytes.MutableBytes; + +import java.math.BigInteger; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class UInt256Test { + + private static UInt256 v(long v) { + return UInt256.valueOf(v); + } + + private static UInt256 biv(String s) { + return UInt256.valueOf(new BigInteger(s)); + } + + private static UInt256 hv(String s) { + return UInt256.fromHexString(s); + } + + @Test + void valueOfLong() { + assertThrows(IllegalArgumentException.class, () -> UInt256.valueOf(-1)); + assertThrows(IllegalArgumentException.class, () -> UInt256.valueOf(Long.MIN_VALUE)); + assertThrows(IllegalArgumentException.class, () -> UInt256.valueOf(~0L)); + } + + @Test + void valueOfBigInteger() { + assertThrows(IllegalArgumentException.class, () -> UInt256.valueOf(BigInteger.valueOf(-1))); + assertThrows( + IllegalArgumentException.class, () -> UInt256.valueOf(BigInteger.valueOf(2).pow(256))); + } + + @ParameterizedTest + @MethodSource("addProvider") + void add(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(0), v(1), v(1)), + Arguments.of(v(0), v(100), v(100)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(100), v(90), v(190)), + Arguments.of(biv("9223372036854775807"), v(1), biv("9223372036854775808")), + Arguments.of( + biv("13492324908428420834234908342"), v(10), biv("13492324908428420834234908352")), + Arguments.of( + biv("13492324908428420834234908342"), + v(23422141424214L), + biv("13492324908428444256376332556")), + Arguments.of(UInt256.MAX_VALUE, v(1), v(0)), + Arguments.of(UInt256.MAX_VALUE, v(2), v(1)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"), + v(1), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + v(1), + UInt256.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addLongProvider") + void addLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(0), 1L, v(1)), + Arguments.of(v(0), 100L, v(100)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(100), 90L, v(190)), + Arguments.of( + biv("13492324908428420834234908342"), 10L, biv("13492324908428420834234908352")), + Arguments.of( + biv("13492324908428420834234908342"), + 23422141424214L, + biv("13492324908428444256376332556")), + Arguments.of(UInt256.MAX_VALUE, 1L, v(0)), + Arguments.of(UInt256.MAX_VALUE, 2L, v(1)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"), + 1L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 1L, + UInt256.MAX_VALUE), + Arguments.of(v(10), -5L, v(5)), + Arguments.of(v(0), -1L, UInt256.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addModProvider") + void addMod(UInt256 v1, UInt256 v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModProvider() { + return Stream.of( + Arguments.of(v(0), v(1), UInt256.valueOf(2), v(1)), + Arguments.of(v(1), v(1), UInt256.valueOf(2), v(0)), + Arguments.of( + UInt256.MAX_VALUE.subtract(2), v(1), UInt256.MAX_VALUE, UInt256.MAX_VALUE.subtract(1)), + Arguments.of(UInt256.MAX_VALUE.subtract(1), v(1), UInt256.MAX_VALUE, v(0)), + Arguments.of(v(2), v(1), UInt256.valueOf(2), v(1)), + Arguments.of(v(3), v(2), UInt256.valueOf(6), v(5)), + Arguments.of(v(3), v(4), UInt256.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModOfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(v(1), UInt256.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModUInt256UInt256Provider") + void addModUInt256UInt256(UInt256 v1, UInt256 v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModUInt256UInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.ONE, UInt256.valueOf(2), v(1)), + Arguments.of(v(1), UInt256.ONE, UInt256.valueOf(2), v(0)), + Arguments.of( + UInt256.MAX_VALUE.subtract(2), + UInt256.ONE, + UInt256.MAX_VALUE, + UInt256.MAX_VALUE.subtract(1)), + Arguments.of(UInt256.MAX_VALUE.subtract(1), UInt256.ONE, UInt256.MAX_VALUE, v(0)), + Arguments.of(v(2), UInt256.ONE, UInt256.valueOf(2), v(1)), + Arguments.of(v(3), UInt256.valueOf(2), UInt256.valueOf(6), v(5)), + Arguments.of(v(3), UInt256.valueOf(4), UInt256.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModLongUInt256OfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(1, UInt256.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongUInt256Provider") + void addModLongUInt256(UInt256 v1, long v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModLongUInt256Provider() { + return Stream.of( + Arguments.of(v(0), 1L, UInt256.valueOf(2), v(1)), + Arguments.of(v(1), 1L, UInt256.valueOf(2), v(0)), + Arguments.of( + UInt256.MAX_VALUE.subtract(2), 1L, UInt256.MAX_VALUE, UInt256.MAX_VALUE.subtract(1)), + Arguments.of(UInt256.MAX_VALUE.subtract(1), 1L, UInt256.MAX_VALUE, v(0)), + Arguments.of(v(2), 1L, UInt256.valueOf(2), v(1)), + Arguments.of(v(2), -1L, UInt256.valueOf(2), v(1)), + Arguments.of(v(1), -7L, UInt256.valueOf(5), v(4))); + } + + @Test + void shouldThrowForAddModUInt256UInt256OfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(UInt256.ONE, UInt256.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongLongProvider") + void addModLongLong(UInt256 v1, long v2, long m, UInt256 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 1L, 2L, v(1)), + Arguments.of(v(1), 1L, 2L, v(0)), + Arguments.of(v(2), 1L, 2L, v(1))); + } + + @Test + void shouldThrowForAddModLongLongOfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, 0)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForAddModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, -5)); + assertEquals("addMod unsigned with negative modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("subtractProvider") + void subtract(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(2), v(1), v(1)), + Arguments.of(v(100), v(100), v(0)), + Arguments.of( + biv("13492324908428420834234908342"), v(10), biv("13492324908428420834234908332")), + Arguments.of( + biv("13492324908428420834234908342"), + v(23422141424214L), + biv("13492324908428397412093484128")), + Arguments.of(v(0), v(1), UInt256.MAX_VALUE), + Arguments.of(v(1), v(2), UInt256.MAX_VALUE), + Arguments.of( + UInt256.MAX_VALUE, + v(1), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @ParameterizedTest + @MethodSource("subtractLongProvider") + void subtractLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(2), 1L, v(1)), + Arguments.of(v(100), 100L, v(0)), + Arguments.of( + biv("13492324908428420834234908342"), 10L, biv("13492324908428420834234908332")), + Arguments.of( + biv("13492324908428420834234908342"), + 23422141424214L, + biv("13492324908428397412093484128")), + Arguments.of(v(0), 1L, UInt256.MAX_VALUE), + Arguments.of(v(1), 2L, UInt256.MAX_VALUE), + Arguments.of( + UInt256.MAX_VALUE, + 1L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE")), + Arguments.of(v(0), -1L, v(1)), + Arguments.of(v(0), -100L, v(100)), + Arguments.of(v(2), -2L, v(4))); + } + + @ParameterizedTest + @MethodSource("multiplyProvider") + void multiply(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyProvider() { + return Stream.of( + Arguments.of(v(0), v(1), v(0)), + Arguments.of(v(1), v(0), v(0)), + Arguments.of(v(1), v(20), v(20)), + Arguments.of(hv("0x0a0000000000"), v(1), hv("0x0a0000000000")), + Arguments.of(v(1), hv("0x0a0000000000"), hv("0x0a0000000000")), + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(2)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(3), v(2), v(6)), + Arguments.of(v(4), v(2), v(8)), + Arguments.of(v(10), v(18), v(180)), + Arguments.of( + biv("13492324908428420834234908341"), v(2), biv("26984649816856841668469816682")), + Arguments.of( + biv("13492324908428420834234908342"), v(2), biv("26984649816856841668469816684")), + Arguments.of(v(2), v(8), v(16)), + Arguments.of(v(7), v(8), v(56)), + Arguments.of(v(8), v(8), v(64)), + Arguments.of(v(17), v(8), v(136)), + Arguments.of( + biv("13492324908428420834234908342"), v(8), biv("107938599267427366673879266736")), + Arguments.of( + biv("13492324908428420834234908342"), v(2048), biv("27632281412461405868513092284416")), + Arguments.of( + biv("13492324908428420834234908342"), + v(131072), + biv("1768466010397529975584837906202624")), + Arguments.of(v(22), v(0), v(0))); + } + + @ParameterizedTest + @MethodSource("multiplyLongProvider") + void multiplyLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyLongProvider() { + return Stream.of( + Arguments.of(v(0), 1L, v(0)), + Arguments.of(v(1), 0L, v(0)), + Arguments.of(v(1), 20L, v(20)), + Arguments.of(v(20), 1L, v(20)), + Arguments.of(hv("0x0a0000000000"), 1L, hv("0x0a0000000000")), + Arguments.of(v(1), 10995116277760L, hv("0x0a0000000000")), + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(2)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(3), 2L, v(6)), + Arguments.of(v(4), 2L, v(8)), + Arguments.of(v(10), 18L, v(180)), + Arguments.of( + biv("13492324908428420834234908341"), 2L, biv("26984649816856841668469816682")), + Arguments.of( + biv("13492324908428420834234908342"), 2L, biv("26984649816856841668469816684")), + Arguments.of(v(2), 8L, v(16)), + Arguments.of(v(7), 8L, v(56)), + Arguments.of(v(8), 8L, v(64)), + Arguments.of(v(17), 8L, v(136)), + Arguments.of( + biv("13492324908428420834234908342"), 8L, biv("107938599267427366673879266736")), + Arguments.of( + biv("13492324908428420834234908342"), 2048L, biv("27632281412461405868513092284416")), + Arguments.of( + biv("13492324908428420834234908342"), + 131072L, + biv("1768466010397529975584837906202624")), + Arguments.of(v(22), 0L, v(0)), + Arguments.of( + hv("0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 2L, + hv("0x1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 2L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @Test + void shouldThrowForMultiplyLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiply(-5)); + assertEquals("multiply unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModProvider") + void multiplyMod(UInt256 v1, UInt256 v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModProvider() { + return Stream.of( + Arguments.of(v(0), v(5), UInt256.valueOf(2), v(0)), + Arguments.of(v(2), v(3), UInt256.valueOf(7), v(6)), + Arguments.of(v(2), v(3), UInt256.valueOf(6), v(0)), + Arguments.of(v(2), v(0), UInt256.valueOf(6), v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + v(2), + UInt256.MAX_VALUE, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD"))); + } + + @Test + void shouldThrowForMultiplyModOfModZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(v(1), UInt256.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongUInt256Provider") + void multiplyModLongUInt256(UInt256 v1, long v2, UInt256 m, UInt256 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModLongUInt256Provider() { + return Stream.of( + Arguments.of(v(0), 5L, UInt256.valueOf(2), v(0)), + Arguments.of(v(2), 3L, UInt256.valueOf(7), v(6)), + Arguments.of(v(2), 3L, UInt256.valueOf(6), v(0)), + Arguments.of(v(2), 0L, UInt256.valueOf(6), v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 2L, + UInt256.MAX_VALUE, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD"))); + } + + @Test + void shouldThrowForMultiplyModLongUInt256OfModZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1L, UInt256.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongUInt256OfNegative() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, UInt256.valueOf(2))); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongLongProvider") + void multiplyModLongLong(UInt256 v1, long v2, long m, UInt256 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 5L, 2L, v(0)), + Arguments.of(v(2), 3L, 7L, v(6)), + Arguments.of(v(2), 3L, 6L, v(0)), + Arguments.of(v(2), 0L, 6L, v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 2L, + Long.MAX_VALUE, + hv("0x000000000000000000000000000000000000000000000000000000000000001C"))); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, 0)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiplyMod(5, -7)); + assertEquals("multiplyMod unsigned with negative modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, 2)); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideProvider") + void divide(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream divideProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(0)), + Arguments.of(v(2), v(2), v(1)), + Arguments.of(v(3), v(2), v(1)), + Arguments.of(v(4), v(2), v(2)), + Arguments.of( + biv("13492324908428420834234908341"), v(2), biv("6746162454214210417117454170")), + Arguments.of( + biv("13492324908428420834234908342"), v(2), biv("6746162454214210417117454171")), + Arguments.of( + biv("13492324908428420834234908343"), v(2), biv("6746162454214210417117454171")), + Arguments.of(v(2), v(8), v(0)), + Arguments.of(v(7), v(8), v(0)), + Arguments.of(v(8), v(8), v(1)), + Arguments.of(v(9), v(8), v(1)), + Arguments.of(v(17), v(8), v(2)), + Arguments.of(v(1024), v(8), v(128)), + Arguments.of(v(1026), v(8), v(128)), + Arguments.of( + biv("13492324908428420834234908342"), v(8), biv("1686540613553552604279363542")), + Arguments.of( + biv("13492324908428420834234908342"), v(2048), biv("6588049271693564860466263")), + Arguments.of( + biv("13492324908428420834234908342"), v(131072), biv("102938269870211950944785"))); + } + + @Test + void shouldThrowForDivideByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(v(0))); + assertEquals("divide by zero", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideCeilProvider") + void divideCeil(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.divideCeil(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream divideCeilProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(1)), + Arguments.of(v(2), v(2), v(1)), + Arguments.of(v(3), v(2), v(2)), + Arguments.of(v(4), v(2), v(2)), + Arguments.of( + biv("13492324908428420834234908341"), v(2), biv("6746162454214210417117454171")), + Arguments.of( + biv("13492324908428420834234908342"), v(2), biv("6746162454214210417117454171")), + Arguments.of( + biv("13492324908428420834234908343"), v(2), biv("6746162454214210417117454172")), + Arguments.of(v(2), v(8), v(1)), + Arguments.of(v(7), v(8), v(1)), + Arguments.of(v(8), v(8), v(1)), + Arguments.of(v(9), v(8), v(2)), + Arguments.of(v(17), v(8), v(3)), + Arguments.of(v(1024), v(8), v(128)), + Arguments.of(v(1026), v(8), v(129)), + Arguments.of( + biv("13492324908428420834234908342"), v(8), biv("1686540613553552604279363543")), + Arguments.of( + biv("13492324908428420834234908342"), v(2048), biv("6588049271693564860466264")), + Arguments.of( + biv("13492324908428420834234908342"), v(131072), biv("102938269870211950944786"))); + } + + @Test + void shouldThrowForDivideCeilByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divideCeil(v(0))); + assertEquals("divide by zero", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideLongProvider") + void divideLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream divideLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(0)), + Arguments.of(v(2), 2L, v(1)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(v(4), 2L, v(2)), + Arguments.of(biv("13492324908428420834234908341"), 2L, biv("6746162454214210417117454170")), + Arguments.of(biv("13492324908428420834234908342"), 2L, biv("6746162454214210417117454171")), + Arguments.of(biv("13492324908428420834234908343"), 2L, biv("6746162454214210417117454171")), + Arguments.of(v(2), 8L, v(0)), + Arguments.of(v(7), 8L, v(0)), + Arguments.of(v(8), 8L, v(1)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(17), 8L, v(2)), + Arguments.of(v(1024), 8L, v(128)), + Arguments.of(v(1026), 8L, v(128)), + Arguments.of(biv("13492324908428420834234908342"), 8L, biv("1686540613553552604279363542")), + Arguments.of(biv("13492324908428420834234908342"), 2048L, biv("6588049271693564860466263")), + Arguments.of( + biv("13492324908428420834234908342"), 131072L, biv("102938269870211950944785"))); + } + + @Test + void shouldThrowForDivideLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(0)); + assertEquals("divide by zero", exception.getMessage()); + } + + @Test + void shouldThrowForDivideLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(-5)); + assertEquals("divide unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("powUInt256Provider") + void powUInt256(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream powUInt256Provider() { + return Stream.of( + Arguments.of(v(0), UInt256.valueOf(2), v(0)), + Arguments.of(v(2), UInt256.valueOf(2), v(4)), + Arguments.of(v(2), UInt256.valueOf(8), v(256)), + Arguments.of(v(3), UInt256.valueOf(3), v(27)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F0F0"), + UInt256.valueOf(3), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A920E119A2F000"))); + } + + @ParameterizedTest + @MethodSource("powLongProvider") + void powLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream powLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(2), 8L, v(256)), + Arguments.of(v(3), 3L, v(27)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F0F0"), + 3L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A920E119A2F000")), + Arguments.of( + v(3), -3L, hv("0x2F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA13"))); + } + + @ParameterizedTest + @MethodSource("modLongProvider") + void modLong(UInt256 v1, long v2, UInt256 expected) { + assertValueEquals(expected, v1.mod(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream modLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(1)), + Arguments.of(v(2), 2L, v(0)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(biv("13492324908428420834234908342"), 2L, v(0)), + Arguments.of(biv("13492324908428420834234908343"), 2L, v(1)), + Arguments.of(v(0), 8L, v(0)), + Arguments.of(v(1), 8L, v(1)), + Arguments.of(v(2), 8L, v(2)), + Arguments.of(v(3), 8L, v(3)), + Arguments.of(v(7), 8L, v(7)), + Arguments.of(v(8), 8L, v(0)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(1024), 8L, v(0)), + Arguments.of(v(1026), 8L, v(2)), + Arguments.of(biv("13492324908428420834234908342"), 8L, v(6)), + Arguments.of(biv("13492324908428420834234908343"), 8L, v(7)), + Arguments.of(biv("13492324908428420834234908344"), 8L, v(0))); + } + + @Test + void shouldThrowForModLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(0)); + assertEquals("mod by zero", exception.getMessage()); + } + + @Test + void shouldThrowForModLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(-5)); + assertEquals("mod by negative", exception.getMessage()); + } + + @Test + void shouldReturnZeroForMod0LongByZero() { + assertEquals(UInt256.ZERO, v(5).mod0(0)); + } + + @Test + void shouldThrowForMod0LongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod0(-5)); + assertEquals("mod by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("andProvider") + void and(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.and(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream andProvider() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x000000000000000000000000000000FF00000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("andProviderBytes32") + void andBytes32(UInt256 v1, Bytes32 v2, UInt256 expected) { + assertValueEquals(expected, v1.and(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream andProviderBytes32() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes32.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes32.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x000000000000000000000000000000FF00000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("orProvider") + void or(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.or(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream orProvider() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x0000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("orProviderBytes32") + void orBytes32(UInt256 v1, Bytes32 v2, UInt256 expected) { + assertValueEquals(expected, v1.or(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream orProviderBytes32() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes32.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes32.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x0000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes32.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("xorProvider") + void xor(UInt256 v1, UInt256 v2, UInt256 expected) { + assertValueEquals(expected, v1.xor(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream xorProvider() { + return Stream.of( + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("xorProviderBytes32") + void xorBytes32(UInt256 v1, Bytes32 v2, UInt256 expected) { + assertValueEquals(expected, v1.xor(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream xorProviderBytes32() { + return Stream.of( + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes32.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes32.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes32.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("notProvider") + void not(UInt256 value, UInt256 expected) { + assertValueEquals(expected, value.not()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream notProvider() { + return Stream.of( + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("shiftLeftProvider") + void shiftLeft(UInt256 value, int distance, UInt256 expected) { + assertValueEquals(expected, value.shiftLeft(distance)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream shiftLeftProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 1, hv("0x02")), + Arguments.of(hv("0x01"), 2, hv("0x04")), + Arguments.of(hv("0x01"), 8, hv("0x0100")), + Arguments.of(hv("0x01"), 9, hv("0x0200")), + Arguments.of(hv("0x01"), 16, hv("0x10000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x0FF000")), + Arguments.of(hv("0x00FF00"), 8, hv("0xFF0000")), + Arguments.of(hv("0x00FF00"), 1, hv("0x01FE00")), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000001"), + 16, + hv("0x0000000000000000000000000000000000000000000000000000000000010000")), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000001"), + 15, + hv("0x0000000000000000000000000000000000000000000000000000000000008000")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 55, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 202, + hv("0xFFFFFFFFFFFFFC00000000000000000000000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("shiftRightProvider") + void shiftRight(UInt256 value, int distance, UInt256 expected) { + assertValueEquals(expected, value.shiftRight(distance)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream shiftRightProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 1, hv("0x00")), + Arguments.of(hv("0x10"), 1, hv("0x08")), + Arguments.of(hv("0x10"), 2, hv("0x04")), + Arguments.of(hv("0x10"), 8, hv("0x00")), + Arguments.of(hv("0x1000"), 4, hv("0x0100")), + Arguments.of(hv("0x1000"), 5, hv("0x0080")), + Arguments.of(hv("0x1000"), 8, hv("0x0010")), + Arguments.of(hv("0x1000"), 9, hv("0x0008")), + Arguments.of(hv("0x1000"), 16, hv("0x0000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x000FF0")), + Arguments.of(hv("0x00FF00"), 8, hv("0x0000FF")), + Arguments.of(hv("0x00FF00"), 1, hv("0x007F80")), + Arguments.of( + hv("0x1000000000000000000000000000000000000000000000000000000000000000"), + 16, + hv("0x0000100000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x1000000000000000000000000000000000000000000000000000000000000000"), + 15, + hv("0x0000200000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 55, + hv("0x00000000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000"), + 202, + hv("0x000000000000000000000000000000000000000000000000003FFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("intValueProvider") + void intValue(UInt256 value, int expected) { + assertEquals(expected, value.intValue()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream intValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0), + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x00000000"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x0001"), 1), + Arguments.of(hv("0x000001"), 1), + Arguments.of(hv("0x00000001"), 1), + Arguments.of(hv("0x0100"), 256), + Arguments.of(hv("0x000100"), 256), + Arguments.of(hv("0x00000100"), 256)); + } + + @Test + void shouldThrowForIntValueOfOversizeValue() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> hv("0x0100000000").intValue()); + assertEquals("Value does not fit a 4 byte int", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("longValueProvider") + void longValue(UInt256 value, long expected) { + assertEquals(expected, value.toLong()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream longValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0L), + Arguments.of(hv("0x00"), 0L), + Arguments.of(hv("0x00000000"), 0L), + Arguments.of(hv("0x01"), 1L), + Arguments.of(hv("0x0001"), 1L), + Arguments.of(hv("0x000001"), 1L), + Arguments.of(hv("0x00000001"), 1L), + Arguments.of(hv("0x0000000001"), 1L), + Arguments.of(hv("0x000000000001"), 1L), + Arguments.of(hv("0x0100"), 256L), + Arguments.of(hv("0x000100"), 256L), + Arguments.of(hv("0x00000100"), 256L), + Arguments.of(hv("0x00000100"), 256L), + Arguments.of(hv("0x000000000100"), 256L), + Arguments.of(hv("0x00000000000100"), 256L), + Arguments.of(hv("0x0000000000000100"), 256L), + Arguments.of(hv("0xFFFFFFFF"), (1L << 32) - 1)); + } + + @Test + void shouldThrowForLongValueOfOversizeValue() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> hv("0x010000000000000000").toLong()); + assertEquals("Value does not fit a 8 byte long", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("compareToProvider") + void compareTo(UInt256 v1, UInt256 v2, int expected) { + assertEquals(expected, v1.compareTo(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream compareToProvider() { + return Stream.of( + Arguments.of(v(5), v(5), 0), + Arguments.of(v(5), v(3), 1), + Arguments.of(v(5), v(6), -1), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + 0), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + 1), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + -1), + Arguments.of( + hv("0x000000000000000000000000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 1), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + -1)); + } + + @ParameterizedTest + @MethodSource("toBytesProvider") + void toBytesTest(UInt256 value, Bytes expected) { + assertEquals(expected, value); + } + + @SuppressWarnings("UnusedMethod") + private static Stream toBytesProvider() { + return Stream.of( + Arguments.of( + hv("0x00"), + Bytes.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x01000000"), + Bytes.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000001000000")), + Arguments.of( + hv("0x0100000000"), + Bytes.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000100000000")), + Arguments.of( + hv("0xf100000000ab"), + Bytes.fromHexString( + "0x0000000000000000000000000000000000000000000000000000f100000000ab")), + Arguments.of( + hv("0x0400000000000000000000000000000000000000000000000000f100000000ab"), + Bytes.fromHexString( + "0x0400000000000000000000000000000000000000000000000000f100000000ab"))); + } + + @ParameterizedTest + @MethodSource("fromBytesProvider") + void fromBytesTest(Bytes value, UInt256 expected, boolean isBytes32) { + assertEquals(expected, UInt256.fromBytes(value)); + assertEquals(isBytes32, value instanceof Bytes32); + } + + @SuppressWarnings("UnusedMethod") + private static Stream fromBytesProvider() { + String onesString = "11111111111111111111111111111111"; + String twosString = "22222222222222222222222222222222"; + String eString = "e000000000e000000000e000000000e0"; + Bytes onesBytes = Bytes.fromHexString(onesString); + Bytes twosBytes = Bytes.fromHexString(twosString); + Bytes eBytes = Bytes.fromHexString(eString); + Bytes onetwoBytes = Bytes.fromHexString(onesString + twosString); + Bytes oneeBytes = Bytes.fromHexString(onesString + eString); + return Stream.of( + // Mutable Bytes + Arguments.of(Bytes.wrap(onesBytes), hv(onesString), false), + Arguments.of(Bytes.wrap(eBytes), hv(eString), false), + Arguments.of(Bytes.wrap(onesBytes, twosBytes), hv(onesString + twosString), false), + Arguments.of(Bytes.wrap(onesBytes, eBytes), hv(onesString + eString), false), + // Array Wrapping Bytes + Arguments.of(Bytes.fromHexString(onesString), hv(onesString), false), + Arguments.of(Bytes.fromHexString(eString), hv(eString), false), + Arguments.of( + Bytes.fromHexString(onesString + twosString), hv(onesString + twosString), false), + Arguments.of(Bytes.fromHexString(onesString + eString), hv(onesString + eString), false), + // Delegating Bytes32 + Arguments.of(Bytes32.wrap(onetwoBytes), hv(onesString + twosString), true), + Arguments.of(Bytes32.wrap(oneeBytes), hv(onesString + eString), true)); + } + + @ParameterizedTest + @MethodSource("toMinimalBytesProvider") + void toMinimalBytesTest(UInt256 value, Bytes expected) { + assertEquals(expected, value.toMinimalBytes()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream toMinimalBytesProvider() { + return Stream.of( + Arguments.of(hv("0x00"), Bytes.EMPTY), + Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")), + Arguments.of(hv("0x0100000000"), Bytes.fromHexString("0x0100000000")), + Arguments.of(hv("0xf100000000ab"), Bytes.fromHexString("0xf100000000ab")), + Arguments.of( + hv("0x0400000000000000000000000000000000000000000000000000f100000000ab"), + Bytes.fromHexString( + "0x0400000000000000000000000000000000000000000000000000f100000000ab"))); + } + + @ParameterizedTest + @MethodSource("numberOfLeadingZerosProvider") + void numberOfLeadingZeros(UInt256 value, int expected) { + assertEquals(expected, value.numberOfLeadingZeros()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream numberOfLeadingZerosProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 256), + Arguments.of(hv("0x01"), 255), + Arguments.of(hv("0x02"), 254), + Arguments.of(hv("0x03"), 254), + Arguments.of(hv("0x0F"), 252), + Arguments.of(hv("0x8F"), 248), + Arguments.of(hv("0x100000000"), 223)); + } + + @ParameterizedTest + @MethodSource("bitLengthProvider") + void bitLength(UInt256 value, int expected) { + assertEquals(expected, value.bitLength()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream bitLengthProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x02"), 2), + Arguments.of(hv("0x03"), 2), + Arguments.of(hv("0x0F"), 4), + Arguments.of(hv("0x8F"), 8), + Arguments.of(hv("0x100000000"), 33)); + } + + @ParameterizedTest + @MethodSource("addExactProvider") + void addExact(UInt256 value, UInt256 operand) { + assertThrows(ArithmeticException.class, () -> value.addExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addExactProvider() { + return Stream.of( + Arguments.of(UInt256.MAX_VALUE, v(1)), Arguments.of(UInt256.MAX_VALUE, UInt256.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addExactLongProvider") + void addExactLong(UInt256 value, long operand) { + assertThrows(ArithmeticException.class, () -> value.addExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addExactLongProvider() { + return Stream.of( + Arguments.of(UInt256.MAX_VALUE, 3), + Arguments.of(UInt256.MAX_VALUE, Long.MAX_VALUE), + Arguments.of(v(0), -1)); + } + + @ParameterizedTest + @MethodSource("subtractExactProvider") + void subtractExact(UInt256 value, UInt256 operand) { + assertThrows(ArithmeticException.class, () -> value.subtractExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractExactProvider() { + return Stream.of(Arguments.of(v(0), v(1)), Arguments.of(v(0), UInt256.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("subtractExactLongProvider") + void subtractExactLong(UInt256 value, long operand) { + assertThrows(ArithmeticException.class, () -> value.subtractExact(operand)); + } + + @Test + void testGet() { + UInt256 value = UInt256.ONE; + assertEquals(1, value.get(31)); + UInt256 value5 = UInt256.valueOf(5); + assertEquals(5, value5.get(31)); + UInt256 value255 = UInt256.valueOf(255); + assertEquals((byte) 0xff, value255.get(31)); + UInt256 value256 = UInt256.valueOf(256); + assertEquals(1, value256.get(30)); + + for (int i = 0; i < 32; i++) { + assertEquals(0, UInt256.ZERO.get(i)); + } + } + + @Test + void testHashcode() { + UInt256 value = UInt256.ZERO; + assertEquals(2111290369, value.hashCode()); + UInt256 valueOne = UInt256.ONE; + assertEquals(2111290370, valueOne.hashCode()); + } + + @Test + void testOverflowSubtraction() { + UInt256 value = + UInt256.fromHexString("0x8000000000000000000000000000000000000000000000000000000000000000"); + UInt256 result = UInt256.ZERO.subtract(value); + assertEquals(value, result); + } + + @Test + void testEquals() { + UInt256 value = UInt256.ZERO; + assertEquals(MutableBytes.create(32), value); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractExactLongProvider() { + return Stream.of( + Arguments.of(v(0), 1), + Arguments.of(v(0), Long.MAX_VALUE), + Arguments.of(UInt256.MAX_VALUE, -1)); + } + + private void assertValueEquals(UInt256 expected, UInt256 actual) { + String msg = + String.format("Expected %s but got %s", expected.toHexString(), actual.toHexString()); + assertEquals(expected, actual, msg); + } + + @Test + void testToDecimalString() { + assertEquals("3456", UInt256.valueOf(3456).toDecimalString()); + } +} diff --git a/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt32Test.java b/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt32Test.java new file mode 100644 index 000000000..7b5af69d9 --- /dev/null +++ b/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt32Test.java @@ -0,0 +1,832 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.units.bigints; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.math.BigInteger; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class UInt32Test { + + private static UInt32 v(int v) { + return UInt32.valueOf(v); + } + + private static UInt32 hv(String s) { + return UInt32.fromHexString(s); + } + + private static Bytes b(String s) { + return Bytes.fromHexString(s); + } + + @Test + void valueOfInt() { + assertThrows(IllegalArgumentException.class, () -> UInt32.valueOf(-1)); + assertThrows(IllegalArgumentException.class, () -> UInt32.valueOf(Integer.MIN_VALUE)); + assertThrows(IllegalArgumentException.class, () -> UInt32.valueOf(~0)); + } + + @Test + void valueOfBigInteger() { + assertThrows(IllegalArgumentException.class, () -> UInt32.valueOf(BigInteger.valueOf(-1))); + assertThrows( + IllegalArgumentException.class, () -> UInt32.valueOf(BigInteger.valueOf(2).pow(32))); + } + + @ParameterizedTest + @MethodSource("addProvider") + void add(UInt32 v1, UInt32 v2, UInt32 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(0), v(1), v(1)), + Arguments.of(v(0), v(100), v(100)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(100), v(90), v(190)), + Arguments.of(UInt32.MAX_VALUE, v(1), v(0)), + Arguments.of(UInt32.MAX_VALUE, v(2), v(1)), + Arguments.of(hv("0xFFFFFFF0"), v(1), hv("0xFFFFFFF1")), + Arguments.of(hv("0xFFFFFFFE"), v(1), UInt32.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addLongProvider") + void addLong(UInt32 v1, int v2, UInt32 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addLongProvider() { + return Stream.of( + Arguments.of(v(1), 0, v(1)), + Arguments.of(v(5), 0, v(5)), + Arguments.of(v(0), 1, v(1)), + Arguments.of(v(0), 100, v(100)), + Arguments.of(v(2), 2, v(4)), + Arguments.of(v(100), 90, v(190)), + Arguments.of(UInt32.MAX_VALUE, 1, v(0)), + Arguments.of(UInt32.MAX_VALUE, 2, v(1)), + Arguments.of(hv("0xFFFFFFF0"), 1, hv("0xFFFFFFF1")), + Arguments.of(hv("0xFFFFFFFE"), 1, UInt32.MAX_VALUE), + Arguments.of(v(10), -5, v(5)), + Arguments.of(v(0), -1, UInt32.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addModProvider") + void addMod(UInt32 v1, UInt32 v2, UInt32 m, UInt32 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModProvider() { + return Stream.of( + Arguments.of(v(0), v(1), UInt32.valueOf(2), v(1)), + Arguments.of(v(1), v(1), UInt32.valueOf(2), v(0)), + Arguments.of( + UInt32.MAX_VALUE.subtract(2), v(1), UInt32.MAX_VALUE, UInt32.MAX_VALUE.subtract(1)), + Arguments.of(UInt32.MAX_VALUE.subtract(1), v(1), UInt32.MAX_VALUE, v(0)), + Arguments.of(v(2), v(1), UInt32.valueOf(2), v(1)), + Arguments.of(v(3), v(2), UInt32.valueOf(6), v(5)), + Arguments.of(v(3), v(4), UInt32.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModOfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(v(1), UInt32.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModUInt32UInt32Provider") + void addModUInt32UInt32(UInt32 v1, UInt32 v2, UInt32 m, UInt32 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModUInt32UInt32Provider() { + return Stream.of( + Arguments.of(v(0), UInt32.ONE, UInt32.valueOf(2), v(1)), + Arguments.of(v(1), UInt32.ONE, UInt32.valueOf(2), v(0)), + Arguments.of( + UInt32.MAX_VALUE.subtract(2), + UInt32.ONE, + UInt32.MAX_VALUE, + UInt32.MAX_VALUE.subtract(1)), + Arguments.of(UInt32.MAX_VALUE.subtract(1), UInt32.ONE, UInt32.MAX_VALUE, v(0)), + Arguments.of(v(2), UInt32.ONE, UInt32.valueOf(2), v(1)), + Arguments.of(v(3), UInt32.valueOf(2), UInt32.valueOf(6), v(5)), + Arguments.of(v(3), UInt32.valueOf(4), UInt32.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModLongUInt32OfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(1, UInt32.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongUInt32Provider") + void addModLongUInt32(UInt32 v1, long v2, UInt32 m, UInt32 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModLongUInt32Provider() { + return Stream.of( + Arguments.of(v(0), 1, UInt32.valueOf(2), v(1)), + Arguments.of(v(1), 1, UInt32.valueOf(2), v(0)), + Arguments.of( + UInt32.MAX_VALUE.subtract(2), 1, UInt32.MAX_VALUE, UInt32.MAX_VALUE.subtract(1)), + Arguments.of(UInt32.MAX_VALUE.subtract(1), 1, UInt32.MAX_VALUE, v(0)), + Arguments.of(v(2), 1, UInt32.valueOf(2), v(1)), + Arguments.of(v(2), -1, UInt32.valueOf(2), v(1)), + Arguments.of(v(1), -7, UInt32.valueOf(5), v(4))); + } + + @Test + void shouldThrowForAddModUInt32UInt32OfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(UInt32.ONE, UInt32.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongLongProvider") + void addModLongLong(UInt32 v1, long v2, long m, UInt32 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 1, 2, v(1)), + Arguments.of(v(1), 1, 2, v(0)), + Arguments.of(v(2), 1, 2, v(1))); + } + + @Test + void shouldThrowForAddModLongLongOfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, 0)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForAddModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, -5)); + assertEquals("addMod unsigned with negative modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("subtractProvider") + void subtract(UInt32 v1, UInt32 v2, UInt32 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(2), v(1), v(1)), + Arguments.of(v(100), v(100), v(0)), + Arguments.of(v(0), v(1), UInt32.MAX_VALUE), + Arguments.of(v(1), v(2), UInt32.MAX_VALUE), + Arguments.of(UInt32.MAX_VALUE, v(1), hv("0xFFFFFFFE"))); + } + + @ParameterizedTest + @MethodSource("subtractLongProvider") + void subtractLong(UInt32 v1, int v2, UInt32 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractLongProvider() { + return Stream.of( + Arguments.of(v(1), 0, v(1)), + Arguments.of(v(5), 0, v(5)), + Arguments.of(v(2), 1, v(1)), + Arguments.of(v(100), 100, v(0)), + Arguments.of(v(0), 1, UInt32.MAX_VALUE), + Arguments.of(v(1), 2, UInt32.MAX_VALUE), + Arguments.of(UInt32.MAX_VALUE, 1, hv("0xFFFFFFFE")), + Arguments.of(v(0), -1, v(1)), + Arguments.of(v(0), -100, v(100)), + Arguments.of(v(2), -2, v(4))); + } + + @ParameterizedTest + @MethodSource("multiplyProvider") + void multiply(UInt32 v1, UInt32 v2, UInt32 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyProvider() { + return Stream.of( + Arguments.of(v(1), v(1), v(1)), + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(2)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(3), v(2), v(6)), + Arguments.of(v(4), v(2), v(8)), + Arguments.of(v(10), v(18), v(180)), + Arguments.of(v(2), v(8), v(16)), + Arguments.of(v(7), v(8), v(56)), + Arguments.of(v(8), v(8), v(64)), + Arguments.of(v(17), v(8), v(136)), + Arguments.of(v(22), v(0), v(0))); + } + + @ParameterizedTest + @MethodSource("multiplyLongProvider") + void multiplyLong(UInt32 v1, int v2, UInt32 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyLongProvider() { + return Stream.of( + Arguments.of(v(1), 1, v(1)), + Arguments.of(v(0), 2, v(0)), + Arguments.of(v(1), 2, v(2)), + Arguments.of(v(2), 2, v(4)), + Arguments.of(v(3), 2, v(6)), + Arguments.of(v(4), 2, v(8)), + Arguments.of(v(10), 18, v(180)), + Arguments.of(v(2), 8, v(16)), + Arguments.of(v(7), 8, v(56)), + Arguments.of(v(8), 8, v(64)), + Arguments.of(v(17), 8, v(136)), + Arguments.of(v(22), 0, v(0)), + Arguments.of(hv("0x0FFFFFFF"), 2, hv("0x1FFFFFFE")), + Arguments.of(hv("0xFFFFFFFF"), 2, hv("0xFFFFFFFE"))); + } + + @Test + void shouldThrowForMultiplyLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiply(-5)); + assertEquals("multiply unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModProvider") + void multiplyMod(UInt32 v1, UInt32 v2, UInt32 m, UInt32 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModProvider() { + return Stream.of( + Arguments.of(v(0), v(5), UInt32.valueOf(2), v(0)), + Arguments.of(v(2), v(3), UInt32.valueOf(7), v(6)), + Arguments.of(v(2), v(3), UInt32.valueOf(6), v(0)), + Arguments.of(v(2), v(0), UInt32.valueOf(6), v(0)), + Arguments.of(hv("0x0FFFFFFE"), v(2), UInt32.MAX_VALUE, hv("0x1FFFFFFC"))); + } + + @Test + void shouldThrowForMultiplyModOfModZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(v(1), UInt32.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongUInt32Provider") + void multiplyModLongUInt32(UInt32 v1, int v2, UInt32 m, UInt32 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModLongUInt32Provider() { + return Stream.of( + Arguments.of(v(0), 5, UInt32.valueOf(2), v(0)), + Arguments.of(v(2), 3, UInt32.valueOf(7), v(6)), + Arguments.of(v(2), 3, UInt32.valueOf(6), v(0)), + Arguments.of(v(2), 0, UInt32.valueOf(6), v(0)), + Arguments.of(hv("0x0FFFFFFE"), 2, UInt32.MAX_VALUE, hv("0x1FFFFFFC"))); + } + + @Test + void shouldThrowForMultiplyModLongUInt32OfModZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, UInt32.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongUInt32OfNegative() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, UInt32.valueOf(2))); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongLongProvider") + void multiplyModLongLong(UInt32 v1, int v2, int m, UInt32 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 5, 2, v(0)), + Arguments.of(v(2), 3, 7, v(6)), + Arguments.of(v(2), 3, 6, v(0)), + Arguments.of(v(2), 0, 6, v(0)), + Arguments.of(hv("0x0FFFFFFE"), 2, Integer.MAX_VALUE, hv("0x1FFFFFFC"))); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, 0)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiplyMod(5, -7)); + assertEquals("multiplyMod unsigned with negative modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, 2)); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideProvider") + void divide(UInt32 v1, UInt32 v2, UInt32 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream divideProvider() { + return Stream.of( + Arguments.of(v(1), v(1), v(1)), + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(0)), + Arguments.of(v(2), v(2), v(1)), + Arguments.of(v(3), v(2), v(1)), + Arguments.of(v(4), v(2), v(2)), + Arguments.of(v(2), v(8), v(0)), + Arguments.of(v(7), v(8), v(0)), + Arguments.of(v(8), v(8), v(1)), + Arguments.of(v(9), v(8), v(1)), + Arguments.of(v(17), v(8), v(2)), + Arguments.of(v(1024), v(8), v(128)), + Arguments.of(v(1026), v(8), v(128))); + } + + @Test + void shouldThrowForDivideByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(v(0))); + assertEquals("divide by zero", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideLongProvider") + void divideLong(UInt32 v1, int v2, UInt32 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream divideLongProvider() { + return Stream.of( + Arguments.of(v(1), 1, v(1)), + Arguments.of(v(0), 2, v(0)), + Arguments.of(v(1), 2, v(0)), + Arguments.of(v(2), 2, v(1)), + Arguments.of(v(3), 2, v(1)), + Arguments.of(v(4), 2, v(2)), + Arguments.of(v(2), 8, v(0)), + Arguments.of(v(7), 8, v(0)), + Arguments.of(v(8), 8, v(1)), + Arguments.of(v(9), 8, v(1)), + Arguments.of(v(17), 8, v(2)), + Arguments.of(v(1024), 8, v(128)), + Arguments.of(v(1026), 8, v(128))); + } + + @Test + void shouldThrowForDivideLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(0)); + assertEquals("divide by zero", exception.getMessage()); + } + + @Test + void shouldThrowForDivideLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(-5)); + assertEquals("divide unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("powUInt32Provider") + void powUInt32(UInt32 v1, UInt32 v2, UInt32 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream powUInt32Provider() { + return Stream.of( + Arguments.of(v(0), UInt32.valueOf(2), v(0)), + Arguments.of(v(2), UInt32.valueOf(2), v(4)), + Arguments.of(v(2), UInt32.valueOf(8), v(256)), + Arguments.of(v(3), UInt32.valueOf(3), v(27)), + Arguments.of(hv("0xFFF0F0F0"), UInt32.valueOf(3), hv("0x19A2F000"))); + } + + @ParameterizedTest + @MethodSource("powLongProvider") + void powLong(UInt32 v1, long v2, UInt32 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream powLongProvider() { + return Stream.of( + Arguments.of(v(0), 2, v(0)), + Arguments.of(v(2), 2, v(4)), + Arguments.of(v(2), 8, v(256)), + Arguments.of(v(3), 3, v(27)), + Arguments.of(hv("0xFFF0F0F0"), 3, hv("0x19A2F000"))); + } + + @ParameterizedTest + @MethodSource("modLongProvider") + void modLong(UInt32 v1, int v2, UInt32 expected) { + assertValueEquals(expected, v1.mod(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream modLongProvider() { + return Stream.of( + Arguments.of(v(0), 2, v(0)), + Arguments.of(v(1), 2, v(1)), + Arguments.of(v(2), 2, v(0)), + Arguments.of(v(3), 2, v(1)), + Arguments.of(v(0), 8, v(0)), + Arguments.of(v(1), 8, v(1)), + Arguments.of(v(2), 8, v(2)), + Arguments.of(v(3), 8, v(3)), + Arguments.of(v(7), 8, v(7)), + Arguments.of(v(8), 8, v(0)), + Arguments.of(v(9), 8, v(1)), + Arguments.of(v(1024), 8, v(0)), + Arguments.of(v(1026), 8, v(2))); + } + + @Test + void shouldThrowForModLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(0)); + assertEquals("mod by zero", exception.getMessage()); + } + + @Test + void shouldThrowForModLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(-5)); + assertEquals("mod by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("andProvider") + void and(UInt32 v1, Bytes v2, UInt32 expected) { + assertValueEquals(expected, UInt32.fromBytes(v1.mutableCopy().and(v2))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream andProvider() { + return Stream.of( + Arguments.of(hv("0x0000FFFF"), b("0xFFFF0000"), hv("0x00000000")), + Arguments.of(hv("0x0000FFFF"), b("0xFFFFFF00"), hv("0x0000FF00")), + Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000"), hv("0x00000000")), + Arguments.of(hv("0x0000FFFF"), hv("0xFFFFFF00"), hv("0x0000FF00"))); + } + + @ParameterizedTest + @MethodSource("orProvider") + void or(UInt32 v1, Bytes v2, UInt32 expected) { + assertValueEquals(expected, UInt32.fromBytes(v1.mutableCopy().or(v2))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream orProvider() { + return Stream.of( + Arguments.of(hv("0x0000FFFF"), b("0xFFFF0000"), hv("0xFFFFFFFF")), + Arguments.of(hv("0x0000FFFF"), b("0xFFFF0000"), hv("0xFFFFFFFF")), + Arguments.of(hv("0x000000FF"), b("0xFFFF0000"), hv("0xFFFF00FF")), + Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000"), hv("0xFFFFFFFF")), + Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000"), hv("0xFFFFFFFF")), + Arguments.of(hv("0x000000FF"), hv("0xFFFF0000"), hv("0xFFFF00FF"))); + } + + @ParameterizedTest + @MethodSource("xorProvider") + void xor(UInt32 v1, Bytes v2, UInt32 expected) { + assertValueEquals(expected, UInt32.fromBytes(v1.mutableCopy().xor(v2))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream xorProvider() { + return Stream.of( + Arguments.of(hv("0xFFFFFFFF"), b("0xFFFFFFFF"), hv("0x00000000")), + Arguments.of(hv("0x0000FFFF"), b("0xFFFF0000"), hv("0xFFFFFFFF")), + Arguments.of(hv("0x0000FFFF"), b("0xFFFFFF00"), hv("0xFFFF00FF")), + Arguments.of(hv("0xFFFFFFFF"), hv("0xFFFFFFFF"), hv("0x00000000")), + Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000"), hv("0xFFFFFFFF")), + Arguments.of(hv("0x0000FFFF"), hv("0xFFFFFF00"), hv("0xFFFF00FF"))); + } + + @ParameterizedTest + @MethodSource("notProvider") + void not(UInt32 value, UInt32 expected) { + assertValueEquals(expected, UInt32.fromBytes(value.mutableCopy().not())); + } + + @SuppressWarnings("UnusedMethod") + private static Stream notProvider() { + return Stream.of( + Arguments.of(hv("0xFFFFFFFF"), hv("0x00000000")), + Arguments.of(hv("0x00000000"), hv("0xFFFFFFFF")), + Arguments.of(hv("0x0000FFFF"), hv("0xFFFF0000"))); + } + + @ParameterizedTest + @MethodSource("shiftLeftProvider") + void shiftLeft(UInt32 value, int distance, UInt32 expected) { + assertValueEquals(expected, UInt32.fromBytes(value.mutableCopy().shiftLeft(distance))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream shiftLeftProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 0, hv("0x01")), + Arguments.of(hv("0x01"), 1, hv("0x02")), + Arguments.of(hv("0x01"), 2, hv("0x04")), + Arguments.of(hv("0x01"), 8, hv("0x0100")), + Arguments.of(hv("0x01"), 9, hv("0x0200")), + Arguments.of(hv("0x01"), 16, hv("0x10000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x0FF000")), + Arguments.of(hv("0x00FF00"), 8, hv("0xFF0000")), + Arguments.of(hv("0x00FF00"), 1, hv("0x01FE00")), + Arguments.of(hv("0x00000001"), 16, hv("0x00010000")), + Arguments.of(hv("0x00000001"), 15, hv("0x00008000")), + Arguments.of(hv("0xFFFFFFFF"), 23, hv("0xFF800000")), + Arguments.of(hv("0x0000FFFF"), 18, hv("0xFFFC0000"))); + } + + @ParameterizedTest + @MethodSource("shiftRightProvider") + void shiftRight(UInt32 value, int distance, UInt32 expected) { + assertValueEquals(expected, UInt32.fromBytes(value.mutableCopy().shiftRight(distance))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream shiftRightProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 0, hv("0x01")), + Arguments.of(hv("0x01"), 1, hv("0x00")), + Arguments.of(hv("0x10"), 1, hv("0x08")), + Arguments.of(hv("0x10"), 2, hv("0x04")), + Arguments.of(hv("0x10"), 8, hv("0x00")), + Arguments.of(hv("0x1000"), 4, hv("0x0100")), + Arguments.of(hv("0x1000"), 5, hv("0x0080")), + Arguments.of(hv("0x1000"), 8, hv("0x0010")), + Arguments.of(hv("0x1000"), 9, hv("0x0008")), + Arguments.of(hv("0x1000"), 16, hv("0x0000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x000FF0")), + Arguments.of(hv("0x00FF00"), 8, hv("0x0000FF")), + Arguments.of(hv("0x00FF00"), 1, hv("0x007F80")), + Arguments.of(hv("0x100000"), 16, hv("0x000010")), + Arguments.of(hv("0x100000"), 15, hv("0x000020")), + Arguments.of(hv("0xFFFFFFFF"), 23, hv("0x000001FF")), + Arguments.of(hv("0xFFFFFFFF"), 202, hv("0x00000000"))); + } + + @ParameterizedTest + @MethodSource("intValueProvider") + void intValue(UInt32 value, int expected) { + assertEquals(expected, value.toInt()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream intValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0), + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x00000000"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x0001"), 1), + Arguments.of(hv("0x000001"), 1), + Arguments.of(hv("0x00000001"), 1), + Arguments.of(hv("0x0100"), 256), + Arguments.of(hv("0x000100"), 256), + Arguments.of(hv("0x00000100"), 256)); + } + + @ParameterizedTest + @MethodSource("longValueProvider") + void longValue(UInt32 value, long expected) { + assertEquals(expected, value.toLong()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream longValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0L), + Arguments.of(hv("0x00"), 0L), + Arguments.of(hv("0x00000000"), 0L), + Arguments.of(hv("0x01"), 1L), + Arguments.of(hv("0x0001"), 1L), + Arguments.of(hv("0x000001"), 1L), + Arguments.of(hv("0x0100"), 256L), + Arguments.of(hv("0x000100"), 256L), + Arguments.of(hv("0x00000100"), 256L), + Arguments.of(hv("0xFFFFFFFF"), (1L << 32) - 1)); + } + + @ParameterizedTest + @MethodSource("compareToProvider") + void compareTo(UInt32 v1, UInt32 v2, int expected) { + assertEquals(expected, v1.compareTo(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream compareToProvider() { + return Stream.of( + Arguments.of(v(5), v(5), 0), + Arguments.of(v(5), v(3), 1), + Arguments.of(v(5), v(6), -1), + Arguments.of(hv("0x00000000"), hv("0x00000000"), 0), + Arguments.of(hv("0xFFFFFFFF"), hv("0xFFFFFFFF"), 0), + Arguments.of(hv("0x0000FFFF"), hv("0x0000FFFF"), 0), + Arguments.of(hv("0xFFFFFFFF"), hv("0x00000000"), 1), + Arguments.of(hv("0x00000000"), hv("0xFFFFFFFF"), -1), + Arguments.of(hv("0x0001FFFF"), hv("0x0000FFFF"), 1), + Arguments.of(hv("0x0000FFFE"), hv("0x0000FFFF"), -1)); + } + + @ParameterizedTest + @MethodSource("toBytesProvider") + void toBytesTest(UInt32 value, Bytes expected) { + assertEquals(expected, value.toBytes()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream toBytesProvider() { + return Stream.of( + Arguments.of(hv("0x00"), Bytes.fromHexString("0x00000000")), + Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")), + Arguments.of(hv("0xf10000ab"), Bytes.fromHexString("0xF10000AB"))); + } + + @ParameterizedTest + @MethodSource("toMinimalBytesProvider") + void toMinimalBytesTest(UInt32 value, Bytes expected) { + assertEquals(expected, value.toMinimalBytes()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream toMinimalBytesProvider() { + return Stream.of( + Arguments.of(hv("0x00"), Bytes.EMPTY), + Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")), + Arguments.of(hv("0xf10000ab"), Bytes.fromHexString("0xf10000ab")), + Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000"))); + } + + @ParameterizedTest + @MethodSource("numberOfLeadingZerosProvider") + void numberOfLeadingZeros(UInt32 value, int expected) { + assertEquals(expected, value.numberOfLeadingZeros()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream numberOfLeadingZerosProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 32), + Arguments.of(hv("0x01"), 31), + Arguments.of(hv("0x02"), 30), + Arguments.of(hv("0x03"), 30), + Arguments.of(hv("0x0F"), 28), + Arguments.of(hv("0x8F"), 24), + Arguments.of(hv("0x1000000"), 7)); + } + + @ParameterizedTest + @MethodSource("bitLengthProvider") + void bitLength(UInt32 value, int expected) { + assertEquals(expected, value.bitLength()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream bitLengthProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x02"), 2), + Arguments.of(hv("0x03"), 2), + Arguments.of(hv("0x0F"), 4), + Arguments.of(hv("0x8F"), 8), + Arguments.of(hv("0x10000000"), 29)); + } + + @ParameterizedTest + @MethodSource("addExactProvider") + void addExact(UInt32 value, UInt32 operand) { + assertThrows(ArithmeticException.class, () -> value.addExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addExactProvider() { + return Stream.of( + Arguments.of(UInt32.MAX_VALUE, v(1)), Arguments.of(UInt32.MAX_VALUE, UInt32.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addExactLongProvider") + void addExactLong(UInt32 value, int operand) { + assertThrows(ArithmeticException.class, () -> value.addExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addExactLongProvider() { + return Stream.of( + Arguments.of(UInt32.MAX_VALUE, 3), + Arguments.of(UInt32.MAX_VALUE, Integer.MAX_VALUE), + Arguments.of(v(0), -1)); + } + + @ParameterizedTest + @MethodSource("subtractExactProvider") + void subtractExact(UInt32 value, UInt32 operand) { + assertThrows(ArithmeticException.class, () -> value.subtractExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractExactProvider() { + return Stream.of(Arguments.of(v(0), v(1)), Arguments.of(v(0), UInt32.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("subtractExactLongProvider") + void subtractExactLong(UInt32 value, int operand) { + assertThrows(ArithmeticException.class, () -> value.subtractExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractExactLongProvider() { + return Stream.of( + Arguments.of(v(0), 1), + Arguments.of(v(0), Integer.MAX_VALUE), + Arguments.of(UInt32.MAX_VALUE, -1)); + } + + private void assertValueEquals(UInt32 expected, UInt32 actual) { + String msg = + String.format("Expected %s but got %s", expected.toHexString(), actual.toHexString()); + assertEquals(expected, actual, msg); + } + + @Test + void testToUInt32() { + UInt32 value = UInt32.valueOf(42); + assertSame(value, value.toUInt32()); + } + + @Test + void toIntTooLarge() { + assertThrows(ArithmeticException.class, () -> UInt32.MAX_VALUE.toBigInteger().intValueExact()); + } + + @Test + void toLongTooLarge() { + assertEquals(4294967295L, UInt32.MAX_VALUE.toLong()); + } + + @Test + void testToDecimalString() { + assertEquals("3456", UInt32.valueOf(3456).toDecimalString()); + } +} diff --git a/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt384Test.java b/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt384Test.java new file mode 100644 index 000000000..918c20360 --- /dev/null +++ b/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt384Test.java @@ -0,0 +1,1144 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.units.bigints; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes48; + +import java.math.BigInteger; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class UInt384Test { + + private static UInt384 v(long v) { + return UInt384.valueOf(v); + } + + private static UInt384 biv(String s) { + return UInt384.valueOf(new BigInteger(s)); + } + + private static UInt384 hv(String s) { + return UInt384.fromHexString(s); + } + + @Test + void valueOfLong() { + assertThrows(IllegalArgumentException.class, () -> UInt384.valueOf(-1)); + assertThrows(IllegalArgumentException.class, () -> UInt384.valueOf(Long.MIN_VALUE)); + assertThrows(IllegalArgumentException.class, () -> UInt384.valueOf(~0L)); + } + + @Test + void valueOfBigInteger() { + assertThrows(IllegalArgumentException.class, () -> UInt384.valueOf(BigInteger.valueOf(-1))); + assertThrows( + IllegalArgumentException.class, () -> UInt384.valueOf(BigInteger.valueOf(2).pow(384))); + } + + @ParameterizedTest + @MethodSource("addProvider") + void add(UInt384 v1, UInt384 v2, UInt384 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(0), v(1), v(1)), + Arguments.of(v(0), v(100), v(100)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(100), v(90), v(190)), + Arguments.of(biv("9223372036854775807"), v(1), biv("9223372036854775808")), + Arguments.of( + biv("13492324908428420834234908342"), v(10), biv("13492324908428420834234908352")), + Arguments.of( + biv("13492324908428420834234908342"), + v(23422141424214L), + biv("13492324908428444256376332556")), + Arguments.of(UInt384.MAX_VALUE, v(1), v(0)), + Arguments.of(UInt384.MAX_VALUE, v(2), v(1)), + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"), + v(1), + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1")), + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + v(1), + UInt384.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addLongProvider") + void addLong(UInt384 v1, long v2, UInt384 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(0), 1L, v(1)), + Arguments.of(v(0), 100L, v(100)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(100), 90L, v(190)), + Arguments.of( + biv("13492324908428420834234908342"), 10L, biv("13492324908428420834234908352")), + Arguments.of( + biv("13492324908428420834234908342"), + 23422141424214L, + biv("13492324908428444256376332556")), + Arguments.of(UInt384.MAX_VALUE, 1L, v(0)), + Arguments.of(UInt384.MAX_VALUE, 2L, v(1)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0"), + 1L, + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1")), + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 1L, + UInt384.MAX_VALUE), + Arguments.of(v(10), -5L, v(5)), + Arguments.of(v(0), -1L, UInt384.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addModProvider") + void addMod(UInt384 v1, UInt384 v2, UInt384 m, UInt384 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModProvider() { + return Stream.of( + Arguments.of(v(0), v(1), UInt384.valueOf(2), v(1)), + Arguments.of(v(1), v(1), UInt384.valueOf(2), v(0)), + Arguments.of( + UInt384.MAX_VALUE.subtract(2), v(1), UInt384.MAX_VALUE, UInt384.MAX_VALUE.subtract(1)), + Arguments.of(UInt384.MAX_VALUE.subtract(1), v(1), UInt384.MAX_VALUE, v(0)), + Arguments.of(v(2), v(1), UInt384.valueOf(2), v(1)), + Arguments.of(v(3), v(2), UInt384.valueOf(6), v(5)), + Arguments.of(v(3), v(4), UInt384.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModOfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(v(1), UInt384.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModUInt384UInt384Provider") + void addModUInt384UInt384(UInt384 v1, UInt384 v2, UInt384 m, UInt384 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModUInt384UInt384Provider() { + return Stream.of( + Arguments.of(v(0), UInt384.ONE, UInt384.valueOf(2), v(1)), + Arguments.of(v(1), UInt384.ONE, UInt384.valueOf(2), v(0)), + Arguments.of( + UInt384.MAX_VALUE.subtract(2), + UInt384.ONE, + UInt384.MAX_VALUE, + UInt384.MAX_VALUE.subtract(1)), + Arguments.of(UInt384.MAX_VALUE.subtract(1), UInt384.ONE, UInt384.MAX_VALUE, v(0)), + Arguments.of(v(2), UInt384.ONE, UInt384.valueOf(2), v(1)), + Arguments.of(v(3), UInt384.valueOf(2), UInt384.valueOf(6), v(5)), + Arguments.of(v(3), UInt384.valueOf(4), UInt384.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModLongUInt384OfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(1, UInt384.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongUInt384Provider") + void addModLongUInt384(UInt384 v1, long v2, UInt384 m, UInt384 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModLongUInt384Provider() { + return Stream.of( + Arguments.of(v(0), 1L, UInt384.valueOf(2), v(1)), + Arguments.of(v(1), 1L, UInt384.valueOf(2), v(0)), + Arguments.of( + UInt384.MAX_VALUE.subtract(2), 1L, UInt384.MAX_VALUE, UInt384.MAX_VALUE.subtract(1)), + Arguments.of(UInt384.MAX_VALUE.subtract(1), 1L, UInt384.MAX_VALUE, v(0)), + Arguments.of(v(2), 1L, UInt384.valueOf(2), v(1)), + Arguments.of(v(2), -1L, UInt384.valueOf(2), v(1)), + Arguments.of(v(1), -7L, UInt384.valueOf(5), v(4))); + } + + @Test + void shouldThrowForAddModUInt384UInt384OfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(UInt384.ONE, UInt384.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongLongProvider") + void addModLongLong(UInt384 v1, long v2, long m, UInt384 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 1L, 2L, v(1)), + Arguments.of(v(1), 1L, 2L, v(0)), + Arguments.of(v(2), 1L, 2L, v(1))); + } + + @Test + void shouldThrowForAddModLongLongOfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, 0)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForAddModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, -5)); + assertEquals("addMod unsigned with negative modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("subtractProvider") + void subtract(UInt384 v1, UInt384 v2, UInt384 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(2), v(1), v(1)), + Arguments.of(v(100), v(100), v(0)), + Arguments.of( + biv("13492324908428420834234908342"), v(10), biv("13492324908428420834234908332")), + Arguments.of( + biv("13492324908428420834234908342"), + v(23422141424214L), + biv("13492324908428397412093484128")), + Arguments.of(v(0), v(1), UInt384.MAX_VALUE), + Arguments.of(v(1), v(2), UInt384.MAX_VALUE), + Arguments.of( + UInt384.MAX_VALUE, + v(1), + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @ParameterizedTest + @MethodSource("subtractLongProvider") + void subtractLong(UInt384 v1, long v2, UInt384 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(2), 1L, v(1)), + Arguments.of(v(100), 100L, v(0)), + Arguments.of( + biv("13492324908428420834234908342"), 10L, biv("13492324908428420834234908332")), + Arguments.of( + biv("13492324908428420834234908342"), + 23422141424214L, + biv("13492324908428397412093484128")), + Arguments.of(v(0), 1L, UInt384.MAX_VALUE), + Arguments.of(v(1), 2L, UInt384.MAX_VALUE), + Arguments.of( + UInt384.MAX_VALUE, + 1L, + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE")), + Arguments.of(v(0), -1L, v(1)), + Arguments.of(v(0), -100L, v(100)), + Arguments.of(v(2), -2L, v(4))); + } + + @ParameterizedTest + @MethodSource("multiplyProvider") + void multiply(UInt384 v1, UInt384 v2, UInt384 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(2)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(3), v(2), v(6)), + Arguments.of(v(4), v(2), v(8)), + Arguments.of(v(10), v(18), v(180)), + Arguments.of( + biv("13492324908428420834234908341"), v(2), biv("26984649816856841668469816682")), + Arguments.of( + biv("13492324908428420834234908342"), v(2), biv("26984649816856841668469816684")), + Arguments.of(v(2), v(8), v(16)), + Arguments.of(v(7), v(8), v(56)), + Arguments.of(v(8), v(8), v(64)), + Arguments.of(v(17), v(8), v(136)), + Arguments.of( + biv("13492324908428420834234908342"), v(8), biv("107938599267427366673879266736")), + Arguments.of( + biv("13492324908428420834234908342"), v(2048), biv("27632281412461405868513092284416")), + Arguments.of( + biv("13492324908428420834234908342"), + v(131072), + biv("1768466010397529975584837906202624")), + Arguments.of(v(22), v(0), v(0))); + } + + @ParameterizedTest + @MethodSource("multiplyLongProvider") + void multiplyLong(UInt384 v1, long v2, UInt384 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(2)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(3), 2L, v(6)), + Arguments.of(v(4), 2L, v(8)), + Arguments.of(v(10), 18L, v(180)), + Arguments.of( + biv("13492324908428420834234908341"), 2L, biv("26984649816856841668469816682")), + Arguments.of( + biv("13492324908428420834234908342"), 2L, biv("26984649816856841668469816684")), + Arguments.of(v(2), 8L, v(16)), + Arguments.of(v(7), 8L, v(56)), + Arguments.of(v(8), 8L, v(64)), + Arguments.of(v(17), 8L, v(136)), + Arguments.of( + biv("13492324908428420834234908342"), 8L, biv("107938599267427366673879266736")), + Arguments.of( + biv("13492324908428420834234908342"), 2048L, biv("27632281412461405868513092284416")), + Arguments.of( + biv("13492324908428420834234908342"), + 131072L, + biv("1768466010397529975584837906202624")), + Arguments.of(v(22), 0L, v(0)), + Arguments.of( + hv( + "0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 2L, + hv( + "0x1FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE")), + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 2L, + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"))); + } + + @Test + void shouldThrowForMultiplyLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiply(-5)); + assertEquals("multiply unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModProvider") + void multiplyMod(UInt384 v1, UInt384 v2, UInt384 m, UInt384 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModProvider() { + return Stream.of( + Arguments.of(v(0), v(5), UInt384.valueOf(2), v(0)), + Arguments.of(v(2), v(3), UInt384.valueOf(7), v(6)), + Arguments.of(v(2), v(3), UInt384.valueOf(6), v(0)), + Arguments.of(v(2), v(0), UInt384.valueOf(6), v(0)), + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + v(2), + UInt384.MAX_VALUE, + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD"))); + } + + @Test + void shouldThrowForMultiplyModOfModZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(v(1), UInt384.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongUInt384Provider") + void multiplyModLongUInt384(UInt384 v1, long v2, UInt384 m, UInt384 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModLongUInt384Provider() { + return Stream.of( + Arguments.of(v(0), 5L, UInt384.valueOf(2), v(0)), + Arguments.of(v(2), 3L, UInt384.valueOf(7), v(6)), + Arguments.of(v(2), 3L, UInt384.valueOf(6), v(0)), + Arguments.of(v(2), 0L, UInt384.valueOf(6), v(0)), + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 2L, + UInt384.MAX_VALUE, + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD"))); + } + + @Test + void shouldThrowForMultiplyModLongUInt384OfModZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1L, UInt384.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongUInt384OfNegative() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, UInt384.valueOf(2))); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongLongProvider") + void multiplyModLongLong(UInt384 v1, long v2, long m, UInt384 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 5L, 2L, v(0)), + Arguments.of(v(2), 3L, 7L, v(6)), + Arguments.of(v(2), 3L, 6L, v(0)), + Arguments.of(v(2), 0L, 6L, v(0)), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + 2L, + Long.MAX_VALUE, + hv("0x000000000000000000000000000000000000000000000000000000000000001C"))); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, 0)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiplyMod(5, -7)); + assertEquals("multiplyMod unsigned with negative modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, 2)); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideProvider") + void divide(UInt384 v1, UInt384 v2, UInt384 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream divideProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(0)), + Arguments.of(v(2), v(2), v(1)), + Arguments.of(v(3), v(2), v(1)), + Arguments.of(v(4), v(2), v(2)), + Arguments.of( + biv("13492324908428420834234908341"), v(2), biv("6746162454214210417117454170")), + Arguments.of( + biv("13492324908428420834234908342"), v(2), biv("6746162454214210417117454171")), + Arguments.of( + biv("13492324908428420834234908343"), v(2), biv("6746162454214210417117454171")), + Arguments.of(v(2), v(8), v(0)), + Arguments.of(v(7), v(8), v(0)), + Arguments.of(v(8), v(8), v(1)), + Arguments.of(v(9), v(8), v(1)), + Arguments.of(v(17), v(8), v(2)), + Arguments.of(v(1024), v(8), v(128)), + Arguments.of(v(1026), v(8), v(128)), + Arguments.of( + biv("13492324908428420834234908342"), v(8), biv("1686540613553552604279363542")), + Arguments.of( + biv("13492324908428420834234908342"), v(2048), biv("6588049271693564860466263")), + Arguments.of( + biv("13492324908428420834234908342"), v(131072), biv("102938269870211950944785"))); + } + + @Test + void shouldThrowForDivideByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(v(0))); + assertEquals("divide by zero", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideLongProvider") + void divideLong(UInt384 v1, long v2, UInt384 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream divideLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(0)), + Arguments.of(v(2), 2L, v(1)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(v(4), 2L, v(2)), + Arguments.of(biv("13492324908428420834234908341"), 2L, biv("6746162454214210417117454170")), + Arguments.of(biv("13492324908428420834234908342"), 2L, biv("6746162454214210417117454171")), + Arguments.of(biv("13492324908428420834234908343"), 2L, biv("6746162454214210417117454171")), + Arguments.of(v(2), 8L, v(0)), + Arguments.of(v(7), 8L, v(0)), + Arguments.of(v(8), 8L, v(1)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(17), 8L, v(2)), + Arguments.of(v(1024), 8L, v(128)), + Arguments.of(v(1026), 8L, v(128)), + Arguments.of(biv("13492324908428420834234908342"), 8L, biv("1686540613553552604279363542")), + Arguments.of(biv("13492324908428420834234908342"), 2048L, biv("6588049271693564860466263")), + Arguments.of( + biv("13492324908428420834234908342"), 131072L, biv("102938269870211950944785"))); + } + + @Test + void shouldThrowForDivideLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(0)); + assertEquals("divide by zero", exception.getMessage()); + } + + @Test + void shouldThrowForDivideLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(-5)); + assertEquals("divide unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("powUInt384Provider") + void powUInt384(UInt384 v1, UInt384 v2, UInt384 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream powUInt384Provider() { + return Stream.of( + Arguments.of(v(0), UInt384.valueOf(2), v(0)), + Arguments.of(v(2), UInt384.valueOf(2), v(4)), + Arguments.of(v(2), UInt384.valueOf(8), v(256)), + Arguments.of(v(3), UInt384.valueOf(3), v(27)), + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F0F0"), + UInt384.valueOf(3), + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A920E119A2F000"))); + } + + @ParameterizedTest + @MethodSource("powLongProvider") + void powLong(UInt384 v1, long v2, UInt384 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream powLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(2), 8L, v(256)), + Arguments.of(v(3), 3L, v(27)), + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F0F0"), + 3L, + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A920E119A2F000")), + Arguments.of( + v(3), + -3L, + hv( + "0x4BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA12F684BDA13"))); + } + + @ParameterizedTest + @MethodSource("modLongProvider") + void modLong(UInt384 v1, long v2, UInt384 expected) { + assertValueEquals(expected, v1.mod(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream modLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(1)), + Arguments.of(v(2), 2L, v(0)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(biv("13492324908428420834234908342"), 2L, v(0)), + Arguments.of(biv("13492324908428420834234908343"), 2L, v(1)), + Arguments.of(v(0), 8L, v(0)), + Arguments.of(v(1), 8L, v(1)), + Arguments.of(v(2), 8L, v(2)), + Arguments.of(v(3), 8L, v(3)), + Arguments.of(v(7), 8L, v(7)), + Arguments.of(v(8), 8L, v(0)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(1024), 8L, v(0)), + Arguments.of(v(1026), 8L, v(2)), + Arguments.of(biv("13492324908428420834234908342"), 8L, v(6)), + Arguments.of(biv("13492324908428420834234908343"), 8L, v(7)), + Arguments.of(biv("13492324908428420834234908344"), 8L, v(0))); + } + + @Test + void shouldThrowForModLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(0)); + assertEquals("mod by zero", exception.getMessage()); + } + + @Test + void shouldThrowForModLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(-5)); + assertEquals("mod by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("andProvider") + void and(UInt384 v1, UInt384 v2, UInt384 expected) { + assertValueEquals(expected, v1.and(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream andProvider() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x000000000000000000000000000000FF00000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("andProviderBytes48") + void andBytes48(UInt384 v1, Bytes48 v2, UInt384 expected) { + assertValueEquals(expected, v1.and(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream andProviderBytes48() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes48.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes48.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0x000000000000000000000000000000FF00000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("orProvider") + void or(UInt384 v1, UInt384 v2, UInt384 expected) { + assertValueEquals(expected, v1.or(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream orProvider() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x0000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("orProviderBytes48") + void orBytes48(UInt384 v1, Bytes48 v2, UInt384 expected) { + assertValueEquals(expected, v1.or(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream orProviderBytes48() { + return Stream.of( + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes48.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes48.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x0000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes48.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("xorProvider") + void xor(UInt384 v1, UInt384 v2, UInt384 expected) { + assertValueEquals(expected, v1.xor(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream xorProvider() { + return Stream.of( + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("xorProviderBytes48") + void xorBytes48(UInt384 v1, Bytes48 v2, UInt384 expected) { + assertValueEquals(expected, v1.xor(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream xorProviderBytes48() { + return Stream.of( + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes48.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes48.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + Bytes48.wrapHexString( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("notProvider") + void not(UInt384 value, UInt384 expected) { + assertValueEquals(expected, value.not()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream notProvider() { + return Stream.of( + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv( + "0x00000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("shiftLeftProvider") + void shiftLeft(UInt384 value, int distance, UInt384 expected) { + assertValueEquals(expected, value.shiftLeft(distance)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream shiftLeftProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 1, hv("0x02")), + Arguments.of(hv("0x01"), 2, hv("0x04")), + Arguments.of(hv("0x01"), 8, hv("0x0100")), + Arguments.of(hv("0x01"), 9, hv("0x0200")), + Arguments.of(hv("0x01"), 16, hv("0x10000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x0FF000")), + Arguments.of(hv("0x00FF00"), 8, hv("0xFF0000")), + Arguments.of(hv("0x00FF00"), 1, hv("0x01FE00")), + Arguments.of( + hv( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), + 16, + hv( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000")), + Arguments.of( + hv( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001"), + 15, + hv( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000")), + Arguments.of( + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 55, + hv( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80000000000000")), + Arguments.of( + hv( + "0x00000000000000000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 330, + hv( + "0xFFFFFFFFFFFFFC0000000000000000000000000000000000000000000000000000000000000000000000000000000000"))); + } + + @ParameterizedTest + @MethodSource("shiftRightProvider") + void shiftRight(UInt384 value, int distance, UInt384 expected) { + assertValueEquals(expected, value.shiftRight(distance)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream shiftRightProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 1, hv("0x00")), + Arguments.of(hv("0x10"), 1, hv("0x08")), + Arguments.of(hv("0x10"), 2, hv("0x04")), + Arguments.of(hv("0x10"), 8, hv("0x00")), + Arguments.of(hv("0x1000"), 4, hv("0x0100")), + Arguments.of(hv("0x1000"), 5, hv("0x0080")), + Arguments.of(hv("0x1000"), 8, hv("0x0010")), + Arguments.of(hv("0x1000"), 9, hv("0x0008")), + Arguments.of(hv("0x1000"), 16, hv("0x0000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x000FF0")), + Arguments.of(hv("0x00FF00"), 8, hv("0x0000FF")), + Arguments.of(hv("0x00FF00"), 1, hv("0x007F80")), + Arguments.of( + hv("0x1000000000000000000000000000000000000000000000000000000000000000"), + 16, + hv("0x0000100000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x1000000000000000000000000000000000000000000000000000000000000000"), + 15, + hv("0x0000200000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 55, + hv("0x00000000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000"), + 202, + hv("0x000000000000000000000000000000000000000000000000003FFFFFFFFFFFFF"))); + } + + @ParameterizedTest + @MethodSource("intValueProvider") + void intValue(UInt384 value, int expected) { + assertEquals(expected, value.intValue()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream intValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0), + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x00000000"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x0001"), 1), + Arguments.of(hv("0x000001"), 1), + Arguments.of(hv("0x00000001"), 1), + Arguments.of(hv("0x0100"), 256), + Arguments.of(hv("0x000100"), 256), + Arguments.of(hv("0x00000100"), 256)); + } + + @Test + void shouldThrowForIntValueOfOversizeValue() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> hv("0x0100000000").intValue()); + assertEquals("Value does not fit a 4 byte int", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("longValueProvider") + void longValue(UInt384 value, long expected) { + assertEquals(expected, value.toLong()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream longValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0L), + Arguments.of(hv("0x00"), 0L), + Arguments.of(hv("0x00000000"), 0L), + Arguments.of(hv("0x01"), 1L), + Arguments.of(hv("0x0001"), 1L), + Arguments.of(hv("0x000001"), 1L), + Arguments.of(hv("0x00000001"), 1L), + Arguments.of(hv("0x0000000001"), 1L), + Arguments.of(hv("0x000000000001"), 1L), + Arguments.of(hv("0x0100"), 256L), + Arguments.of(hv("0x000100"), 256L), + Arguments.of(hv("0x00000100"), 256L), + Arguments.of(hv("0x00000100"), 256L), + Arguments.of(hv("0x000000000100"), 256L), + Arguments.of(hv("0x00000000000100"), 256L), + Arguments.of(hv("0x0000000000000100"), 256L), + Arguments.of(hv("0xFFFFFFFF"), (1L << 32) - 1)); + } + + @Test + void shouldThrowForLongValueOfOversizeValue() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> hv("0x010000000000000000").toLong()); + assertEquals("Value does not fit a 8 byte long", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("compareToProvider") + void compareTo(UInt384 v1, UInt384 v2, int expected) { + assertEquals(expected, v1.compareTo(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream compareToProvider() { + return Stream.of( + Arguments.of(v(5), v(5), 0), + Arguments.of(v(5), v(3), 1), + Arguments.of(v(5), v(6), -1), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + 0), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 0), + Arguments.of( + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + 1), + Arguments.of( + hv("0x0000000000000000000000000000000000000000000000000000000000000000"), + hv("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + -1), + Arguments.of( + hv("0x000000000000000000000000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + 1), + Arguments.of( + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE"), + hv("0x000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + -1)); + } + + @ParameterizedTest + @MethodSource("toBytesProvider") + void toBytesTest(UInt384 value, Bytes expected) { + assertEquals(expected, value); + } + + @SuppressWarnings("UnusedMethod") + private static Stream toBytesProvider() { + return Stream.of( + Arguments.of( + hv("0x00"), + Bytes.fromHexString( + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")), + Arguments.of( + hv("0x01000000"), + Bytes.fromHexString( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000")), + Arguments.of( + hv("0x0100000000"), + Bytes.fromHexString( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000")), + Arguments.of( + hv("0xf100000000ab"), + Bytes.fromHexString( + "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000f100000000ab")), + Arguments.of( + hv("0x0400000000000000000000000000000000000000000000000000f100000000ab"), + Bytes.fromHexString( + "0x000000000000000000000000000000000400000000000000000000000000000000000000000000000000f100000000ab"))); + } + + @ParameterizedTest + @MethodSource("toMinimalBytesProvider") + void toMinimalBytesTest(UInt384 value, Bytes expected) { + assertEquals(expected, value.toMinimalBytes()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream toMinimalBytesProvider() { + return Stream.of( + Arguments.of(hv("0x00"), Bytes.EMPTY), + Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")), + Arguments.of(hv("0x0100000000"), Bytes.fromHexString("0x0100000000")), + Arguments.of(hv("0xf100000000ab"), Bytes.fromHexString("0xf100000000ab")), + Arguments.of( + hv("0x0400000000000000000000000000000000000000000000000000f100000000ab"), + Bytes.fromHexString( + "0x0400000000000000000000000000000000000000000000000000f100000000ab"))); + } + + @ParameterizedTest + @MethodSource("numberOfLeadingZerosProvider") + void numberOfLeadingZeros(UInt384 value, int expected) { + assertEquals(expected, value.numberOfLeadingZeros()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream numberOfLeadingZerosProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 384), + Arguments.of(hv("0x01"), 383), + Arguments.of(hv("0x02"), 382), + Arguments.of(hv("0x03"), 382), + Arguments.of(hv("0x0F"), 380), + Arguments.of(hv("0x8F"), 376), + Arguments.of(hv("0x100000000"), 351)); + } + + @ParameterizedTest + @MethodSource("bitLengthProvider") + void bitLength(UInt384 value, int expected) { + assertEquals(expected, value.bitLength()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream bitLengthProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x02"), 2), + Arguments.of(hv("0x03"), 2), + Arguments.of(hv("0x0F"), 4), + Arguments.of(hv("0x8F"), 8), + Arguments.of(hv("0x100000000"), 33)); + } + + @ParameterizedTest + @MethodSource("addExactProvider") + void addExact(UInt384 value, UInt384 operand) { + assertThrows(ArithmeticException.class, () -> value.addExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addExactProvider() { + return Stream.of( + Arguments.of(UInt384.MAX_VALUE, v(1)), Arguments.of(UInt384.MAX_VALUE, UInt384.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addExactLongProvider") + void addExactLong(UInt384 value, long operand) { + assertThrows(ArithmeticException.class, () -> value.addExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addExactLongProvider() { + return Stream.of( + Arguments.of(UInt384.MAX_VALUE, 3), + Arguments.of(UInt384.MAX_VALUE, Long.MAX_VALUE), + Arguments.of(v(0), -1)); + } + + @ParameterizedTest + @MethodSource("subtractExactProvider") + void subtractExact(UInt384 value, UInt384 operand) { + assertThrows(ArithmeticException.class, () -> value.subtractExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractExactProvider() { + return Stream.of(Arguments.of(v(0), v(1)), Arguments.of(v(0), UInt384.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("subtractExactLongProvider") + void subtractExactLong(UInt384 value, long operand) { + assertThrows(ArithmeticException.class, () -> value.subtractExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractExactLongProvider() { + return Stream.of( + Arguments.of(v(0), 1), + Arguments.of(v(0), Long.MAX_VALUE), + Arguments.of(UInt384.MAX_VALUE, -1)); + } + + private void assertValueEquals(UInt384 expected, UInt384 actual) { + String msg = + String.format("Expected %s but got %s", expected.toHexString(), actual.toHexString()); + assertEquals(expected, actual, msg); + } + + @Test + void testToDecimalString() { + assertEquals("3456", UInt384.valueOf(3456).toDecimalString()); + } +} diff --git a/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt64Test.java b/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt64Test.java new file mode 100644 index 000000000..798659a46 --- /dev/null +++ b/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt64Test.java @@ -0,0 +1,820 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.units.bigints; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.math.BigInteger; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class UInt64Test { + + private static UInt64 v(long v) { + return UInt64.valueOf(v); + } + + private static UInt64 hv(String s) { + return UInt64.fromHexString(s); + } + + private static Bytes b(String s) { + return Bytes.fromHexString(s); + } + + @Test + void valueOfLong() { + assertThrows(IllegalArgumentException.class, () -> UInt64.valueOf(-1)); + assertThrows(IllegalArgumentException.class, () -> UInt64.valueOf(Long.MIN_VALUE)); + assertThrows(IllegalArgumentException.class, () -> UInt64.valueOf(~0L)); + } + + @Test + void valueOfBigInteger() { + assertThrows(IllegalArgumentException.class, () -> UInt64.valueOf(BigInteger.valueOf(-1))); + assertThrows( + IllegalArgumentException.class, () -> UInt64.valueOf(BigInteger.valueOf(2).pow(64))); + } + + @ParameterizedTest + @MethodSource("addProvider") + void add(UInt64 v1, UInt64 v2, UInt64 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(0), v(1), v(1)), + Arguments.of(v(0), v(100), v(100)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(100), v(90), v(190)), + Arguments.of(UInt64.MAX_VALUE, v(1), v(0)), + Arguments.of(UInt64.MAX_VALUE, v(2), v(1)), + Arguments.of(hv("0xFFFFFFFFFFFFFFF0"), v(1), hv("0xFFFFFFFFFFFFFFF1")), + Arguments.of(hv("0xFFFFFFFFFFFFFFFE"), v(1), UInt64.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addLongProvider") + void addLong(UInt64 v1, long v2, UInt64 expected) { + assertValueEquals(expected, v1.add(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(0), 1L, v(1)), + Arguments.of(v(0), 100L, v(100)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(100), 90L, v(190)), + Arguments.of(UInt64.MAX_VALUE, 1L, v(0)), + Arguments.of(UInt64.MAX_VALUE, 2L, v(1)), + Arguments.of(hv("0xFFFFFFFFFFFFFFF0"), 1L, hv("0xFFFFFFFFFFFFFFF1")), + Arguments.of(hv("0xFFFFFFFFFFFFFFFE"), 1L, UInt64.MAX_VALUE), + Arguments.of(v(10), -5L, v(5)), + Arguments.of(v(0), -1L, UInt64.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addModProvider") + void addMod(UInt64 v1, UInt64 v2, UInt64 m, UInt64 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModProvider() { + return Stream.of( + Arguments.of(v(0), v(1), UInt64.valueOf(2), v(1)), + Arguments.of(v(1), v(1), UInt64.valueOf(2), v(0)), + Arguments.of( + UInt64.MAX_VALUE.subtract(2), v(1), UInt64.MAX_VALUE, UInt64.MAX_VALUE.subtract(1)), + Arguments.of(UInt64.MAX_VALUE.subtract(1), v(1), UInt64.MAX_VALUE, v(0)), + Arguments.of(v(2), v(1), UInt64.valueOf(2), v(1)), + Arguments.of(v(3), v(2), UInt64.valueOf(6), v(5)), + Arguments.of(v(3), v(4), UInt64.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModOfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(v(1), UInt64.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModUInt64UInt64Provider") + void addModUInt64UInt64(UInt64 v1, UInt64 v2, UInt64 m, UInt64 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModUInt64UInt64Provider() { + return Stream.of( + Arguments.of(v(0), UInt64.ONE, UInt64.valueOf(2), v(1)), + Arguments.of(v(1), UInt64.ONE, UInt64.valueOf(2), v(0)), + Arguments.of( + UInt64.MAX_VALUE.subtract(2), + UInt64.ONE, + UInt64.MAX_VALUE, + UInt64.MAX_VALUE.subtract(1)), + Arguments.of(UInt64.MAX_VALUE.subtract(1), UInt64.ONE, UInt64.MAX_VALUE, v(0)), + Arguments.of(v(2), UInt64.ONE, UInt64.valueOf(2), v(1)), + Arguments.of(v(3), UInt64.valueOf(2), UInt64.valueOf(6), v(5)), + Arguments.of(v(3), UInt64.valueOf(4), UInt64.valueOf(2), v(1))); + } + + @Test + void shouldThrowForAddModLongUInt64OfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(1, UInt64.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongUInt64Provider") + void addModLongUInt64(UInt64 v1, long v2, UInt64 m, UInt64 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModLongUInt64Provider() { + return Stream.of( + Arguments.of(v(0), 1L, UInt64.valueOf(2), v(1)), + Arguments.of(v(1), 1L, UInt64.valueOf(2), v(0)), + Arguments.of( + UInt64.MAX_VALUE.subtract(2), 1L, UInt64.MAX_VALUE, UInt64.MAX_VALUE.subtract(1)), + Arguments.of(UInt64.MAX_VALUE.subtract(1), 1L, UInt64.MAX_VALUE, v(0)), + Arguments.of(v(2), 1L, UInt64.valueOf(2), v(1)), + Arguments.of(v(2), -1L, UInt64.valueOf(2), v(1)), + Arguments.of(v(1), -7L, UInt64.valueOf(5), v(4))); + } + + @Test + void shouldThrowForAddModUInt64UInt64OfZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).addMod(UInt64.ONE, UInt64.ZERO)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("addModLongLongProvider") + void addModLongLong(UInt64 v1, long v2, long m, UInt64 expected) { + assertValueEquals(expected, v1.addMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 1L, 2L, v(1)), + Arguments.of(v(1), 1L, 2L, v(0)), + Arguments.of(v(2), 1L, 2L, v(1))); + } + + @Test + void shouldThrowForAddModLongLongOfZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, 0)); + assertEquals("addMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForAddModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(0).addMod(1, -5)); + assertEquals("addMod unsigned with negative modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("subtractProvider") + void subtract(UInt64 v1, UInt64 v2, UInt64 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractProvider() { + return Stream.of( + Arguments.of(v(1), v(0), v(1)), + Arguments.of(v(5), v(0), v(5)), + Arguments.of(v(2), v(1), v(1)), + Arguments.of(v(100), v(100), v(0)), + Arguments.of(v(0), v(1), UInt64.MAX_VALUE), + Arguments.of(v(1), v(2), UInt64.MAX_VALUE), + Arguments.of(UInt64.MAX_VALUE, v(1), hv("0xFFFFFFFFFFFFFFFE"))); + } + + @ParameterizedTest + @MethodSource("subtractLongProvider") + void subtractLong(UInt64 v1, long v2, UInt64 expected) { + assertValueEquals(expected, v1.subtract(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractLongProvider() { + return Stream.of( + Arguments.of(v(1), 0L, v(1)), + Arguments.of(v(5), 0L, v(5)), + Arguments.of(v(2), 1L, v(1)), + Arguments.of(v(100), 100L, v(0)), + Arguments.of(v(0), 1L, UInt64.MAX_VALUE), + Arguments.of(v(1), 2L, UInt64.MAX_VALUE), + Arguments.of(UInt64.MAX_VALUE, 1L, hv("0xFFFFFFFFFFFFFFFE")), + Arguments.of(v(0), -1L, v(1)), + Arguments.of(v(0), -100L, v(100)), + Arguments.of(v(2), -2L, v(4))); + } + + @ParameterizedTest + @MethodSource("multiplyProvider") + void multiply(UInt64 v1, UInt64 v2, UInt64 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(2)), + Arguments.of(v(2), v(2), v(4)), + Arguments.of(v(3), v(2), v(6)), + Arguments.of(v(4), v(2), v(8)), + Arguments.of(v(10), v(18), v(180)), + Arguments.of(v(2), v(8), v(16)), + Arguments.of(v(7), v(8), v(56)), + Arguments.of(v(8), v(8), v(64)), + Arguments.of(v(17), v(8), v(136)), + Arguments.of(v(22), v(0), v(0))); + } + + @ParameterizedTest + @MethodSource("multiplyLongProvider") + void multiplyLong(UInt64 v1, long v2, UInt64 expected) { + assertValueEquals(expected, v1.multiply(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(2)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(3), 2L, v(6)), + Arguments.of(v(4), 2L, v(8)), + Arguments.of(v(10), 18L, v(180)), + Arguments.of(v(2), 8L, v(16)), + Arguments.of(v(7), 8L, v(56)), + Arguments.of(v(8), 8L, v(64)), + Arguments.of(v(17), 8L, v(136)), + Arguments.of(v(22), 0L, v(0)), + Arguments.of(hv("0x0FFFFFFFFFFFFFFF"), 2L, hv("0x1FFFFFFFFFFFFFFE")), + Arguments.of(hv("0xFFFFFFFFFFFFFFFF"), 2L, hv("0xFFFFFFFFFFFFFFFE"))); + } + + @Test + void shouldThrowForMultiplyLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiply(-5)); + assertEquals("multiply unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModProvider") + void multiplyMod(UInt64 v1, UInt64 v2, UInt64 m, UInt64 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModProvider() { + return Stream.of( + Arguments.of(v(0), v(5), UInt64.valueOf(2), v(0)), + Arguments.of(v(2), v(3), UInt64.valueOf(7), v(6)), + Arguments.of(v(2), v(3), UInt64.valueOf(6), v(0)), + Arguments.of(v(2), v(0), UInt64.valueOf(6), v(0)), + Arguments.of(hv("0x0FFFFFFFFFFFFFFE"), v(2), UInt64.MAX_VALUE, hv("0x1FFFFFFFFFFFFFFC"))); + } + + @Test + void shouldThrowForMultiplyModOfModZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(0).multiplyMod(v(1), UInt64.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongUInt64Provider") + void multiplyModLongUInt64(UInt64 v1, long v2, UInt64 m, UInt64 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModLongUInt64Provider() { + return Stream.of( + Arguments.of(v(0), 5L, UInt64.valueOf(2), v(0)), + Arguments.of(v(2), 3L, UInt64.valueOf(7), v(6)), + Arguments.of(v(2), 3L, UInt64.valueOf(6), v(0)), + Arguments.of(v(2), 0L, UInt64.valueOf(6), v(0)), + Arguments.of(hv("0x0FFFFFFFFFFFFFFE"), 2L, UInt64.MAX_VALUE, hv("0x1FFFFFFFFFFFFFFC"))); + } + + @Test + void shouldThrowForMultiplyModLongUInt64OfModZero() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1L, UInt64.ZERO)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongUInt64OfNegative() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, UInt64.valueOf(2))); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("multiplyModLongLongProvider") + void multiplyModLongLong(UInt64 v1, long v2, long m, UInt64 expected) { + assertValueEquals(expected, v1.multiplyMod(v2, m)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream multiplyModLongLongProvider() { + return Stream.of( + Arguments.of(v(0), 5L, 2L, v(0)), + Arguments.of(v(2), 3L, 7L, v(6)), + Arguments.of(v(2), 3L, 6L, v(0)), + Arguments.of(v(2), 0L, 6L, v(0)), + Arguments.of(hv("0x0FFFFFFFFFFFFFFE"), 2L, Long.MAX_VALUE, hv("0x1FFFFFFFFFFFFFFC"))); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).multiplyMod(1, 0)); + assertEquals("multiplyMod with zero modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfModNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(2).multiplyMod(5, -7)); + assertEquals("multiplyMod unsigned with negative modulus", exception.getMessage()); + } + + @Test + void shouldThrowForMultiplyModLongLongOfNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(3).multiplyMod(-1, 2)); + assertEquals("multiplyMod unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideProvider") + void divide(UInt64 v1, UInt64 v2, UInt64 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream divideProvider() { + return Stream.of( + Arguments.of(v(0), v(2), v(0)), + Arguments.of(v(1), v(2), v(0)), + Arguments.of(v(2), v(2), v(1)), + Arguments.of(v(3), v(2), v(1)), + Arguments.of(v(4), v(2), v(2)), + Arguments.of(v(2), v(8), v(0)), + Arguments.of(v(7), v(8), v(0)), + Arguments.of(v(8), v(8), v(1)), + Arguments.of(v(9), v(8), v(1)), + Arguments.of(v(17), v(8), v(2)), + Arguments.of(v(1024), v(8), v(128)), + Arguments.of(v(1026), v(8), v(128))); + } + + @Test + void shouldThrowForDivideByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(v(0))); + assertEquals("divide by zero", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("divideLongProvider") + void divideLong(UInt64 v1, long v2, UInt64 expected) { + assertValueEquals(expected, v1.divide(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream divideLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(0)), + Arguments.of(v(2), 2L, v(1)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(v(4), 2L, v(2)), + Arguments.of(v(2), 8L, v(0)), + Arguments.of(v(7), 8L, v(0)), + Arguments.of(v(8), 8L, v(1)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(17), 8L, v(2)), + Arguments.of(v(1024), 8L, v(128)), + Arguments.of(v(1026), 8L, v(128))); + } + + @Test + void shouldThrowForDivideLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(0)); + assertEquals("divide by zero", exception.getMessage()); + } + + @Test + void shouldThrowForDivideLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).divide(-5)); + assertEquals("divide unsigned by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("powUInt64Provider") + void powUInt64(UInt64 v1, UInt64 v2, UInt64 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream powUInt64Provider() { + return Stream.of( + Arguments.of(v(0), UInt64.valueOf(2), v(0)), + Arguments.of(v(2), UInt64.valueOf(2), v(4)), + Arguments.of(v(2), UInt64.valueOf(8), v(256)), + Arguments.of(v(3), UInt64.valueOf(3), v(27)), + Arguments.of(hv("0xFFFFFFFFFFF0F0F0"), UInt64.valueOf(3), hv("0xF2A920E119A2F000"))); + } + + @ParameterizedTest + @MethodSource("powLongProvider") + void powLong(UInt64 v1, long v2, UInt64 expected) { + assertValueEquals(expected, v1.pow(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream powLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(2), 2L, v(4)), + Arguments.of(v(2), 8L, v(256)), + Arguments.of(v(3), 3L, v(27)), + Arguments.of(hv("0xFFFFFFFFFFF0F0F0"), 3L, hv("0xF2A920E119A2F000"))); + } + + @ParameterizedTest + @MethodSource("modLongProvider") + void modLong(UInt64 v1, long v2, UInt64 expected) { + assertValueEquals(expected, v1.mod(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream modLongProvider() { + return Stream.of( + Arguments.of(v(0), 2L, v(0)), + Arguments.of(v(1), 2L, v(1)), + Arguments.of(v(2), 2L, v(0)), + Arguments.of(v(3), 2L, v(1)), + Arguments.of(v(0), 8L, v(0)), + Arguments.of(v(1), 8L, v(1)), + Arguments.of(v(2), 8L, v(2)), + Arguments.of(v(3), 8L, v(3)), + Arguments.of(v(7), 8L, v(7)), + Arguments.of(v(8), 8L, v(0)), + Arguments.of(v(9), 8L, v(1)), + Arguments.of(v(1024), 8L, v(0)), + Arguments.of(v(1026), 8L, v(2))); + } + + @Test + void shouldThrowForModLongByZero() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(0)); + assertEquals("mod by zero", exception.getMessage()); + } + + @Test + void shouldThrowForModLongByNegative() { + Throwable exception = assertThrows(ArithmeticException.class, () -> v(5).mod(-5)); + assertEquals("mod by negative", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("andProvider") + void and(UInt64 v1, Bytes v2, UInt64 expected) { + assertValueEquals(expected, UInt64.fromBytes(v1.mutableCopy().and(v2))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream andProvider() { + return Stream.of( + Arguments.of(hv("0x00000000FFFFFFFF"), hv("0xFFFFFFFF00000000"), hv("0x0000000000000000")), + Arguments.of(hv("0x00000000FFFFFFFF"), hv("0xFFFFFFFFFF000000"), hv("0x00000000FF000000")), + Arguments.of(hv("0x00000000FFFFFFFF"), b("0xFFFFFFFFFF000000"), hv("0x00000000FF000000"))); + } + + @ParameterizedTest + @MethodSource("orProvider") + void or(UInt64 v1, Bytes v2, UInt64 expected) { + assertValueEquals(expected, UInt64.fromBytes(v1.mutableCopy().or(v2))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream orProvider() { + return Stream.of( + Arguments.of(hv("0x00000000FFFFFFFF"), hv("0xFFFFFFFF00000000"), hv("0xFFFFFFFFFFFFFFFF")), + Arguments.of(hv("0x00000000FFFFFFFF"), hv("0xFFFFFFFF00000000"), hv("0xFFFFFFFFFFFFFFFF")), + Arguments.of(hv("0x00000000000000FF"), hv("0xFFFFFFFF00000000"), hv("0xFFFFFFFF000000FF")), + Arguments.of(hv("0x00000000000000FF"), b("0xFFFFFFFF00000000"), hv("0xFFFFFFFF000000FF"))); + } + + @ParameterizedTest + @MethodSource("xorProvider") + void xor(UInt64 v1, Bytes v2, UInt64 expected) { + assertValueEquals(expected, UInt64.fromBytes(v1.mutableCopy().xor(v2))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream xorProvider() { + return Stream.of( + Arguments.of(hv("0xFFFFFFFFFFFFFFFF"), hv("0xFFFFFFFFFFFFFFFF"), hv("0x0000000000000000")), + Arguments.of(hv("0x00000000FFFFFFFF"), hv("0xFFFFFFFF00000000"), hv("0xFFFFFFFFFFFFFFFF")), + Arguments.of(hv("0x00000000FFFFFFFF"), hv("0xFFFFFFFFFF000000"), hv("0xFFFFFFFF00FFFFFF")), + Arguments.of(hv("0x00000000FFFFFFFF"), b("0xFFFFFFFFFF000000"), hv("0xFFFFFFFF00FFFFFF"))); + } + + @ParameterizedTest + @MethodSource("notProvider") + void not(UInt64 value, UInt64 expected) { + assertValueEquals(expected, UInt64.fromBytes(value.mutableCopy().not())); + } + + @SuppressWarnings("UnusedMethod") + private static Stream notProvider() { + return Stream.of( + Arguments.of(hv("0xFFFFFFFFFFFFFFFF"), hv("0x0000000000000000")), + Arguments.of(hv("0x0000000000000000"), hv("0xFFFFFFFFFFFFFFFF")), + Arguments.of(hv("0x00000000FFFFFFFF"), hv("0xFFFFFFFF00000000"))); + } + + @ParameterizedTest + @MethodSource("shiftLeftProvider") + void shiftLeft(UInt64 value, int distance, UInt64 expected) { + assertValueEquals(expected, UInt64.fromBytes(value.mutableCopy().shiftLeft(distance))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream shiftLeftProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 1, hv("0x02")), + Arguments.of(hv("0x01"), 2, hv("0x04")), + Arguments.of(hv("0x01"), 8, hv("0x0100")), + Arguments.of(hv("0x01"), 9, hv("0x0200")), + Arguments.of(hv("0x01"), 16, hv("0x10000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x0FF000")), + Arguments.of(hv("0x00FF00"), 8, hv("0xFF0000")), + Arguments.of(hv("0x00FF00"), 1, hv("0x01FE00")), + Arguments.of(hv("0x0000000000000001"), 16, hv("0x0000000000010000")), + Arguments.of(hv("0x0000000000000001"), 15, hv("0x0000000000008000")), + Arguments.of(hv("0xFFFFFFFFFFFFFFFF"), 55, hv("0xFF80000000000000")), + Arguments.of(hv("0x00000000FFFFFFFF"), 50, hv("0xFFFC000000000000"))); + } + + @ParameterizedTest + @MethodSource("shiftRightProvider") + void shiftRight(UInt64 value, int distance, UInt64 expected) { + assertValueEquals(expected, UInt64.fromBytes(value.mutableCopy().shiftRight(distance))); + } + + @SuppressWarnings("UnusedMethod") + private static Stream shiftRightProvider() { + return Stream.of( + Arguments.of(hv("0x01"), 1, hv("0x00")), + Arguments.of(hv("0x10"), 1, hv("0x08")), + Arguments.of(hv("0x10"), 2, hv("0x04")), + Arguments.of(hv("0x10"), 8, hv("0x00")), + Arguments.of(hv("0x1000"), 4, hv("0x0100")), + Arguments.of(hv("0x1000"), 5, hv("0x0080")), + Arguments.of(hv("0x1000"), 8, hv("0x0010")), + Arguments.of(hv("0x1000"), 9, hv("0x0008")), + Arguments.of(hv("0x1000"), 16, hv("0x0000")), + Arguments.of(hv("0x00FF00"), 4, hv("0x000FF0")), + Arguments.of(hv("0x00FF00"), 8, hv("0x0000FF")), + Arguments.of(hv("0x00FF00"), 1, hv("0x007F80")), + Arguments.of(hv("0x1000000000000000"), 16, hv("0x0000100000000000")), + Arguments.of(hv("0x1000000000000000"), 15, hv("0x0000200000000000")), + Arguments.of(hv("0xFFFFFFFFFFFFFFFF"), 55, hv("0x00000000000001FF")), + Arguments.of(hv("0xFFFFFFFFFFFFFFFF"), 202, hv("0x0000000000000000"))); + } + + @ParameterizedTest + @MethodSource("intValueProvider") + void intValue(UInt64 value, int expected) { + assertEquals(expected, value.intValue()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream intValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0), + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x00000000"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x0001"), 1), + Arguments.of(hv("0x000001"), 1), + Arguments.of(hv("0x00000001"), 1), + Arguments.of(hv("0x0100"), 256), + Arguments.of(hv("0x000100"), 256), + Arguments.of(hv("0x00000100"), 256)); + } + + @Test + void shouldThrowForIntValueOfOversizeValue() { + Throwable exception = + assertThrows(ArithmeticException.class, () -> hv("0x0100000000").intValue()); + assertEquals("Value does not fit a 4 byte int", exception.getMessage()); + } + + @ParameterizedTest + @MethodSource("longValueProvider") + void longValue(UInt64 value, long expected) { + assertEquals(expected, value.toLong()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream longValueProvider() { + return Stream.of( + Arguments.of(hv("0x"), 0L), + Arguments.of(hv("0x00"), 0L), + Arguments.of(hv("0x00000000"), 0L), + Arguments.of(hv("0x01"), 1L), + Arguments.of(hv("0x0001"), 1L), + Arguments.of(hv("0x000001"), 1L), + Arguments.of(hv("0x00000001"), 1L), + Arguments.of(hv("0x0000000001"), 1L), + Arguments.of(hv("0x000000000001"), 1L), + Arguments.of(hv("0x0100"), 256L), + Arguments.of(hv("0x000100"), 256L), + Arguments.of(hv("0x00000100"), 256L), + Arguments.of(hv("0x00000100"), 256L), + Arguments.of(hv("0x000000000100"), 256L), + Arguments.of(hv("0x00000000000100"), 256L), + Arguments.of(hv("0x00000000000100"), 256L), + Arguments.of(hv("0xFFFFFFFF"), (1L << 32) - 1)); + } + + @ParameterizedTest + @MethodSource("compareToProvider") + void compareTo(UInt64 v1, UInt64 v2, int expected) { + assertEquals(expected, v1.compareTo(v2)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream compareToProvider() { + return Stream.of( + Arguments.of(v(5), v(5), 0), + Arguments.of(v(5), v(3), 1), + Arguments.of(v(5), v(6), -1), + Arguments.of(hv("0x0000000000000000"), hv("0x0000000000000000"), 0), + Arguments.of(hv("0xFFFFFFFFFFFFFFFF"), hv("0xFFFFFFFFFFFFFFFF"), 0), + Arguments.of(hv("0x00000000FFFFFFFF"), hv("0x00000000FFFFFFFF"), 0), + Arguments.of(hv("0xFFFFFFFFFFFFFFFF"), hv("0x0000000000000000"), 1), + Arguments.of(hv("0x0000000000000000"), hv("0xFFFFFFFFFFFFFFFF"), -1), + Arguments.of(hv("0x00000001FFFFFFFF"), hv("0x00000000FFFFFFFF"), 1), + Arguments.of(hv("0x00000000FFFFFFFE"), hv("0x00000000FFFFFFFF"), -1)); + } + + @ParameterizedTest + @MethodSource("toBytesProvider") + void toBytesTest(UInt64 value, Bytes expected) { + assertEquals(expected, value.toBytes()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream toBytesProvider() { + return Stream.of( + Arguments.of(hv("0x00"), Bytes.fromHexString("0x0000000000000000")), + Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x0000000001000000")), + Arguments.of(hv("0x0100000000"), Bytes.fromHexString("0x0000000100000000")), + Arguments.of(hv("0xf100000000ab"), Bytes.fromHexString("0x0000F100000000AB"))); + } + + @ParameterizedTest + @MethodSource("toMinimalBytesProvider") + void toMinimalBytesTest(UInt64 value, Bytes expected) { + assertEquals(expected, value.toMinimalBytes()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream toMinimalBytesProvider() { + return Stream.of( + Arguments.of(hv("0x00"), Bytes.EMPTY), + Arguments.of(hv("0x01000000"), Bytes.fromHexString("0x01000000")), + Arguments.of(hv("0x0100000000"), Bytes.fromHexString("0x0100000000")), + Arguments.of(hv("0xf100000000ab"), Bytes.fromHexString("0xf100000000ab")), + Arguments.of(hv("0100000000000000"), Bytes.fromHexString("0x0100000000000000"))); + } + + @ParameterizedTest + @MethodSource("numberOfLeadingZerosProvider") + void numberOfLeadingZeros(UInt64 value, int expected) { + assertEquals(expected, value.numberOfLeadingZeros()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream numberOfLeadingZerosProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 64), + Arguments.of(hv("0x01"), 63), + Arguments.of(hv("0x02"), 62), + Arguments.of(hv("0x03"), 62), + Arguments.of(hv("0x0F"), 60), + Arguments.of(hv("0x8F"), 56), + Arguments.of(hv("0x100000000"), 31)); + } + + @ParameterizedTest + @MethodSource("bitLengthProvider") + void bitLength(UInt64 value, int expected) { + assertEquals(expected, value.bitLength()); + } + + @SuppressWarnings("UnusedMethod") + private static Stream bitLengthProvider() { + return Stream.of( + Arguments.of(hv("0x00"), 0), + Arguments.of(hv("0x01"), 1), + Arguments.of(hv("0x02"), 2), + Arguments.of(hv("0x03"), 2), + Arguments.of(hv("0x0F"), 4), + Arguments.of(hv("0x8F"), 8), + Arguments.of(hv("0x100000000"), 33)); + } + + @ParameterizedTest + @MethodSource("addExactProvider") + void addExact(UInt64 value, UInt64 operand) { + assertThrows(ArithmeticException.class, () -> value.addExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addExactProvider() { + return Stream.of( + Arguments.of(UInt64.MAX_VALUE, v(1)), Arguments.of(UInt64.MAX_VALUE, UInt64.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("addExactLongProvider") + void addExactLong(UInt64 value, long operand) { + assertThrows(ArithmeticException.class, () -> value.addExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream addExactLongProvider() { + return Stream.of( + Arguments.of(UInt64.MAX_VALUE, 3), + Arguments.of(UInt64.MAX_VALUE, Long.MAX_VALUE), + Arguments.of(v(0), -1)); + } + + @ParameterizedTest + @MethodSource("subtractExactProvider") + void subtractExact(UInt64 value, UInt64 operand) { + assertThrows(ArithmeticException.class, () -> value.subtractExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractExactProvider() { + return Stream.of(Arguments.of(v(0), v(1)), Arguments.of(v(0), UInt64.MAX_VALUE)); + } + + @ParameterizedTest + @MethodSource("subtractExactLongProvider") + void subtractExactLong(UInt64 value, long operand) { + assertThrows(ArithmeticException.class, () -> value.subtractExact(operand)); + } + + @SuppressWarnings("UnusedMethod") + private static Stream subtractExactLongProvider() { + return Stream.of( + Arguments.of(v(0), 1), + Arguments.of(v(0), Long.MAX_VALUE), + Arguments.of(UInt64.MAX_VALUE, -1)); + } + + private void assertValueEquals(UInt64 expected, UInt64 actual) { + String msg = + String.format("Expected %s but got %s", expected.toHexString(), actual.toHexString()); + assertEquals(expected, actual, msg); + } + + @Test + void testToDecimalString() { + assertEquals("3456", UInt64.valueOf(3456).toDecimalString()); + } +}