From 50f89251e078e3f9470db5a9dfde7a7bb076301b Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Wed, 29 Jan 2025 17:16:08 +0000 Subject: [PATCH 1/7] Implement Tuweni v2 This is a collapsed view of the previous old commits: * 0929d61ae Merge remote-tracking branch 'upstream/main' into flatten-Bytes-class-hierarchy * | 6d794916e Miscelaneous fixes * | a4def2a34 Fix memleak in Hash and bug in devp2p Packet creation * | 10e6e81a7 fix bug in and/or/xor operations with UInt256/UInt384 * | 578e6bb6f Move bound checks for Bytes implementation out of constructors * | b9c6b0538 Merge branch 'main' into flatten-Bytes-class-hierarchy * 4100d8b35 review feedback * afba2b9dc change megamorphic benchmarks to slice and optimize size() * b2066fc27 JMH integration fixes * e40d796aa remove InterfaceCall and VirtualCall benchmarks * dff599ab1 fix ConnectTwoServersTest - missed package changes and port bindings * 5aba56216 add missing package-info.java files * 1c641ff80 fix failure in DefaultDiscoveryV5ServiceTest about reusing ports * 279acd6cd bring in old tuweni v1 after move * ace791060 move all other touched files to org.apache.tuweni.v2 package * 65f217b92 mv org.apache.tuweni.bytes.v2 to org.apache.tuweni.v2.bytes * ebaa8bdc1 Implement Tuweni v2 Signed-off-by: Luis Pinto --- build.gradle | 49 + bytes/build.gradle | 1 + .../BytesMegamorphicBenchmarkV1.java | 62 + .../BytesMegamorphicBenchmarkV2.java | 62 + .../tuweni/v2/bytes/ArrayWrappingBytes.java | 139 + .../tuweni/v2/bytes/BufferWrappingBytes.java | 124 + .../tuweni/v2/bytes/ByteBufWrappingBytes.java | 128 + .../v2/bytes/ByteBufferWrappingBytes.java | 144 + .../org/apache/tuweni/v2/bytes/Bytes.java | 1349 +++++++ .../org/apache/tuweni/v2/bytes/Bytes32.java | 235 ++ .../org/apache/tuweni/v2/bytes/Bytes48.java | 236 ++ .../apache/tuweni/v2/bytes/BytesValues.java | 74 + .../tuweni/v2/bytes/ConcatenatedBytes.java | 273 ++ .../tuweni/v2/bytes/ConstantBytesValue.java | 110 + .../tuweni/v2/bytes/DelegatingBytes.java | 260 ++ .../apache/tuweni/v2/bytes/MutableBytes.java | 737 ++++ .../org/apache/tuweni/v2/bytes/Utils.java | 113 + .../apache/tuweni/v2/bytes/package-info.java | 23 + .../tuweni/bytes/ConcatenatedBytesTest.java | 1 + .../tuweni/v2/bytes/BufferBytesTest.java | 28 + .../tuweni/v2/bytes/ByteBufBytesTest.java | 28 + .../tuweni/v2/bytes/ByteBufferBytesTest.java | 28 + .../apache/tuweni/v2/bytes/Bytes32Test.java | 160 + .../apache/tuweni/v2/bytes/Bytes48Test.java | 100 + .../org/apache/tuweni/v2/bytes/BytesTest.java | 569 +++ .../tuweni/v2/bytes/CommonBytesTest.java | 624 ++++ .../v2/bytes/ConcatenatedBytesTest.java | 181 + .../tuweni/v2/bytes/DelegateBytesTest.java | 30 + .../tuweni/v2/bytes/MutableBytesTest.java | 421 +++ .../org/apache/tuweni/v2/crypto/Hash.java | 296 ++ .../apache/tuweni/v2/crypto/SECP256K1.java | 1045 ++++++ .../blake2bf/Blake2bfMessageDigest.java | 233 ++ .../tuweni/v2/crypto/mikuli/AtePairing.java | 22 + .../tuweni/v2/crypto/mikuli/BLS12381.java | 122 + .../tuweni/v2/crypto/mikuli/G1Point.java | 88 + .../tuweni/v2/crypto/mikuli/G2Point.java | 85 + .../tuweni/v2/crypto/mikuli/GTPoint.java | 33 + .../apache/tuweni/v2/crypto/mikuli/Group.java | 11 + .../tuweni/v2/crypto/mikuli/KeyPair.java | 46 + .../tuweni/v2/crypto/mikuli/PublicKey.java | 101 + .../tuweni/v2/crypto/mikuli/Scalar.java | 34 + .../tuweni/v2/crypto/mikuli/SecretKey.java | 63 + .../tuweni/v2/crypto/mikuli/Signature.java | 94 + .../crypto/mikuli/SignatureAndPublicKey.java | 62 + .../tuweni/v2/crypto/mikuli/package-info.java | 23 + .../apache/tuweni/v2/crypto/package-info.java | 20 + .../tuweni/v2/crypto/sodium/AES256GCM.java | 1046 ++++++ .../tuweni/v2/crypto/sodium/Allocated.java | 127 + .../apache/tuweni/v2/crypto/sodium/Auth.java | 228 ++ .../apache/tuweni/v2/crypto/sodium/Box.java | 1225 +++++++ .../tuweni/v2/crypto/sodium/Concatenate.java | 135 + .../DefaultDetachedEncryptionResult.java | 36 + .../sodium/DetachedEncryptionResult.java | 37 + .../tuweni/v2/crypto/sodium/DiffieHelman.java | 463 +++ .../tuweni/v2/crypto/sodium/GenericHash.java | 325 ++ .../tuweni/v2/crypto/sodium/HMACSHA256.java | 196 + .../tuweni/v2/crypto/sodium/HMACSHA512.java | 194 + .../v2/crypto/sodium/HMACSHA512256.java | 195 + .../v2/crypto/sodium/KeyDerivation.java | 305 ++ .../tuweni/v2/crypto/sodium/KeyExchange.java | 697 ++++ .../tuweni/v2/crypto/sodium/PasswordHash.java | 1061 ++++++ .../tuweni/v2/crypto/sodium/SHA256Hash.java | 232 ++ .../tuweni/v2/crypto/sodium/SHA512Hash.java | 232 ++ .../tuweni/v2/crypto/sodium/SecretBox.java | 1841 ++++++++++ .../crypto/sodium/SecretDecryptionStream.java | 36 + .../crypto/sodium/SecretEncryptionStream.java | 87 + .../tuweni/v2/crypto/sodium/Signature.java | 683 ++++ .../tuweni/v2/crypto/sodium/Sodium.java | 3194 +++++++++++++++++ .../v2/crypto/sodium/SodiumVersion.java | 47 + .../v2/crypto/sodium/XChaCha20Poly1305.java | 923 +++++ .../tuweni/v2/crypto/sodium/package-info.java | 30 + .../org/apache/tuweni/v2/crypto/HashTest.java | 166 + .../tuweni/v2/crypto/SECP256K1Test.java | 388 ++ .../blake2bf/Blake2bfMessageDigestTest.java | 124 + .../tuweni/v2/crypto/mikuli/GTPointTest.java | 23 + .../v2/crypto/mikuli/SignatureTest.java | 164 + .../v2/crypto/sodium/AllocatedTest.java | 37 + .../tuweni/v2/crypto/sodium/BoxTest.java | 272 ++ .../v2/crypto/sodium/ConcatenateTest.java | 33 + .../v2/crypto/sodium/DiffieHelmanTest.java | 97 + .../v2/crypto/sodium/GenericHashTest.java | 39 + .../v2/crypto/sodium/HMACSHA256Test.java | 49 + .../v2/crypto/sodium/HMACSHA512256Test.java | 49 + .../v2/crypto/sodium/HMACSHA512Test.java | 49 + .../v2/crypto/sodium/KeyDerivationTest.java | 34 + .../v2/crypto/sodium/PasswordHashTest.java | 119 + .../v2/crypto/sodium/SHA256HashTest.java | 66 + .../sodium/SecretDecryptionStreamTest.java | 29 + .../v2/crypto/sodium/SignatureTest.java | 54 + .../v2/devp2p/v5/ConnectTwoServersTest.kt | 67 + .../kotlin/org/apache/tuweni/devp2p/Packet.kt | 2 +- .../tuweni/v2/devp2p/DiscoveryService.kt | 922 +++++ .../org/apache/tuweni/v2/devp2p/Endpoint.kt | 99 + .../tuweni/v2/devp2p/EnodeUriComponents.kt | 55 + .../tuweni/v2/devp2p/EthereumNodeRecord.kt | 329 ++ .../org/apache/tuweni/v2/devp2p/Node.kt | 31 + .../org/apache/tuweni/v2/devp2p/Packet.kt | 477 +++ .../org/apache/tuweni/v2/devp2p/PacketType.kt | 115 + .../org/apache/tuweni/v2/devp2p/Peer.kt | 98 + .../apache/tuweni/v2/devp2p/PeerRepository.kt | 210 ++ .../tuweni/v2/devp2p/PeerRoutingTable.kt | 96 + .../tuweni/v2/devp2p/v5/DiscoveryV5Service.kt | 316 ++ .../apache/tuweni/v2/devp2p/v5/ENRStorage.kt | 50 + .../tuweni/v2/devp2p/v5/HandshakeSession.kt | 201 ++ .../org/apache/tuweni/v2/devp2p/v5/Message.kt | 75 + .../apache/tuweni/v2/devp2p/v5/Messages.kt | 305 ++ .../tuweni/v2/devp2p/v5/RoutingTable.kt | 62 + .../org/apache/tuweni/v2/devp2p/v5/Session.kt | 333 ++ .../tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt | 58 + .../tuweni/v2/devp2p/v5/encrypt/SessionKey.kt | 11 + .../devp2p/v5/encrypt/SessionKeyGenerator.kt | 40 + .../tuweni/v2/devp2p/v5/topic/Ticket.kt | 70 + .../apache/tuweni/v2/devp2p/v5/topic/Topic.kt | 12 + .../tuweni/v2/devp2p/v5/topic/TopicTable.kt | 95 + .../tuweni/devp2p/v5/HandshakeSessionTest.kt | 11 +- .../tuweni/v2/devp2p/DiscoveryServiceTest.kt | 403 +++ .../tuweni/v2/devp2p/ENRResponsePacketTest.kt | 44 + .../v2/devp2p/EthereumNodeRecordTest.kt | 62 + .../tuweni/v2/devp2p/FindNodePacketTest.kt | 48 + .../tuweni/v2/devp2p/NeighborsPacketTest.kt | 66 + .../apache/tuweni/v2/devp2p/PingPacketTest.kt | 107 + .../apache/tuweni/v2/devp2p/PongPacketTest.kt | 73 + .../v5/DefaultDiscoveryV5ServiceTest.kt | 67 + .../v2/devp2p/v5/HandshakeSessionTest.kt | 88 + .../v2/devp2p/v5/PeerRoutingTableTest.kt | 23 + .../v2/devp2p/v5/encrypt/AES128GCMTest.kt | 42 + .../v5/encrypt/SessionKeyGeneratorTest.kt | 28 + .../devp2p/v5/packet/FindNodeMessageTest.kt | 27 + .../tuweni/v2/devp2p/v5/packet/MessageTest.kt | 66 + .../v2/devp2p/v5/packet/NodesMessageTest.kt | 39 + .../v2/devp2p/v5/packet/PingMessageTest.kt | 24 + .../v2/devp2p/v5/packet/PongMessageTest.kt | 26 + .../v2/devp2p/v5/packet/RandomMessageTest.kt | 40 + .../v5/packet/RegConfirmationMessageTest.kt | 25 + .../devp2p/v5/packet/RegTopicMessageTest.kt | 35 + .../v2/devp2p/v5/packet/TicketMessageTest.kt | 26 + .../devp2p/v5/packet/TopicQueryMessageTest.kt | 25 + .../devp2p/v5/packet/WhoAreYouMessageTest.kt | 23 + .../tuweni/v2/devp2p/v5/topic/TicketTest.kt | 20 + .../java/org/apache/tuweni/v2/io/Base32.java | 59 + .../java/org/apache/tuweni/v2/io/Base58.java | 56 + .../org/apache/tuweni/v2/io/Base58Codec.java | 95 + .../java/org/apache/tuweni/v2/io/Base64.java | 56 + .../apache/tuweni/v2/io/Base64URLSafe.java | 56 + .../org/apache/tuweni/v2/io/package-info.java | 23 + .../org/apache/tuweni/v2/io/Base32Test.java | 37 + .../org/apache/tuweni/v2/io/Base58Test.java | 77 + .../org/apache/tuweni/v2/io/Base64Test.java | 43 + .../tuweni/v2/io/Base64URLSafeTest.java | 43 + .../apache/tuweni/v2/net/package-info.java | 20 + .../tls/ClientFingerprintTrustManager.java | 123 + .../tls/FileBackedFingerprintRepository.java | 167 + .../v2/net/tls/FingerprintRepository.java | 35 + .../tls/ServerFingerprintTrustManager.java | 121 + .../org/apache/tuweni/v2/net/tls/TLS.java | 219 ++ .../v2/net/tls/TLSEnvironmentException.java | 14 + .../tuweni/v2/net/tls/package-info.java | 17 + .../FileBackedFingerprintRepositoryTest.java | 98 + .../tuweni/v2/rlp/AccumulatingRLPWriter.java | 87 + .../tuweni/v2/rlp/ByteBufferRLPWriter.java | 70 + .../apache/tuweni/v2/rlp/BytesRLPReader.java | 313 ++ .../apache/tuweni/v2/rlp/BytesRLPWriter.java | 22 + .../tuweni/v2/rlp/DelegatingRLPWriter.java | 68 + .../java/org/apache/tuweni/v2/rlp/RLP.java | 528 +++ .../org/apache/tuweni/v2/rlp/RLPReader.java | 424 +++ .../org/apache/tuweni/v2/rlp/RLPWriter.java | 128 + .../apache/tuweni/v2/rlp/package-info.java | 23 + .../tuweni/v2/rlp/ByteBufferWriterTest.java | 163 + .../tuweni/v2/rlp/BytesRLPReaderTest.java | 275 ++ .../tuweni/v2/rlp/BytesRLPWriterTest.java | 187 + .../tuweni/v2/ssz/ByteBufferSSZWriter.java | 26 + .../apache/tuweni/v2/ssz/BytesSSZReader.java | 479 +++ .../apache/tuweni/v2/ssz/BytesSSZWriter.java | 25 + .../java/org/apache/tuweni/v2/ssz/SSZ.java | 2037 +++++++++++ .../tuweni/v2/ssz/SSZFixedSizeTypeList.java | 41 + .../tuweni/v2/ssz/SSZFixedSizeVector.java | 42 + .../org/apache/tuweni/v2/ssz/SSZReadable.java | 9 + .../org/apache/tuweni/v2/ssz/SSZReader.java | 799 +++++ .../v2/ssz/SSZVariableSizeTypeList.java | 37 + .../org/apache/tuweni/v2/ssz/SSZWritable.java | 10 + .../org/apache/tuweni/v2/ssz/SSZWriter.java | 739 ++++ .../apache/tuweni/v2/ssz/package-info.java | 23 + .../tuweni/v2/ssz/ByteBufferWriterTest.java | 121 + .../tuweni/v2/ssz/BytesSSZReaderTest.java | 252 ++ .../tuweni/v2/ssz/BytesSSZWriterTest.java | 539 +++ .../tuweni/v2/ssz/HashTreeRootTest.java | 105 + .../v2/ssz/TransactionNetworkPayload.java | 302 ++ units/.factorypath | 25 + .../apache/tuweni/units/bigints/Utils.java | 47 + .../tuweni/v2/units/bigints/UInt256.java | 1051 ++++++ .../tuweni/v2/units/bigints/UInt32.java | 483 +++ .../tuweni/v2/units/bigints/UInt384.java | 877 +++++ .../tuweni/v2/units/bigints/UInt64.java | 541 +++ .../apache/tuweni/v2/units/bigints/Utils.java | 47 + .../tuweni/v2/units/bigints/package-info.java | 20 + .../apache/tuweni/v2/units/package-info.java | 20 + .../tuweni/v2/units/bigints/UInt256Test.java | 1245 +++++++ .../tuweni/v2/units/bigints/UInt32Test.java | 832 +++++ .../tuweni/v2/units/bigints/UInt384Test.java | 1144 ++++++ .../tuweni/v2/units/bigints/UInt64Test.java | 820 +++++ 200 files changed, 45983 insertions(+), 3 deletions(-) create mode 100644 bytes/src/jmh/java/org/benchmark/BytesMegamorphicBenchmarkV1.java create mode 100644 bytes/src/jmh/java/org/benchmark/BytesMegamorphicBenchmarkV2.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/ArrayWrappingBytes.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/BufferWrappingBytes.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/ByteBufWrappingBytes.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/ByteBufferWrappingBytes.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes32.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/Bytes48.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/BytesValues.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/ConcatenatedBytes.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/ConstantBytesValue.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/DelegatingBytes.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/MutableBytes.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/Utils.java create mode 100644 bytes/src/main/java/org/apache/tuweni/v2/bytes/package-info.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/BufferBytesTest.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/ByteBufBytesTest.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/ByteBufferBytesTest.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/Bytes32Test.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/Bytes48Test.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/BytesTest.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/CommonBytesTest.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/ConcatenatedBytesTest.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/DelegateBytesTest.java create mode 100644 bytes/src/test/java/org/apache/tuweni/v2/bytes/MutableBytesTest.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/Hash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/SECP256K1.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigest.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/AtePairing.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/BLS12381.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G1Point.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G2Point.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/GTPoint.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Group.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/KeyPair.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/PublicKey.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Scalar.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SecretKey.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Signature.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SignatureAndPublicKey.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/package-info.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/package-info.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/AES256GCM.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Allocated.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Auth.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Box.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Concatenate.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DefaultDetachedEncryptionResult.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DetachedEncryptionResult.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelman.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/GenericHash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivation.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyExchange.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/PasswordHash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA256Hash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA512Hash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretBox.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStream.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretEncryptionStream.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Signature.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Sodium.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SodiumVersion.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/XChaCha20Poly1305.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/package-info.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/HashTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/SECP256K1Test.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigestTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/GTPointTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/SignatureTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/AllocatedTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/BoxTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/ConcatenateTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelmanTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/GenericHashTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256Test.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256Test.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512Test.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivationTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/PasswordHashTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SHA256HashTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStreamTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SignatureTest.java create mode 100644 devp2p/src/integrationTest/kotlin/org/apache/tuweni/v2/devp2p/v5/ConnectTwoServersTest.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryService.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Endpoint.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EnodeUriComponents.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecord.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Node.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Packet.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PacketType.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Peer.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRepository.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRoutingTable.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/DiscoveryV5Service.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/ENRStorage.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSession.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Message.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Messages.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/RoutingTable.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Session.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKey.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGenerator.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Ticket.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Topic.kt create mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TopicTable.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryServiceTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/ENRResponsePacketTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecordTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/FindNodePacketTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/NeighborsPacketTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PingPacketTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PongPacketTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/DefaultDiscoveryV5ServiceTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSessionTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/PeerRoutingTableTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCMTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/FindNodeMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/MessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/NodesMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PingMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PongMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RandomMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegConfirmationMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegTopicMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TicketMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TopicQueryMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/WhoAreYouMessageTest.kt create mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TicketTest.kt create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base32.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base58.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base58Codec.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base64.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base64URLSafe.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/package-info.java create mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base32Test.java create mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base58Test.java create mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base64Test.java create mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base64URLSafeTest.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/package-info.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/ClientFingerprintTrustManager.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepository.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/FingerprintRepository.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/ServerFingerprintTrustManager.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/TLS.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/TLSEnvironmentException.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/package-info.java create mode 100644 net/src/test/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepositoryTest.java create mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/AccumulatingRLPWriter.java create mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/ByteBufferRLPWriter.java create mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPReader.java create mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPWriter.java create mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/DelegatingRLPWriter.java create mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/RLP.java create mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPReader.java create mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPWriter.java create mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/package-info.java create mode 100644 rlp/src/test/java/org/apache/tuweni/v2/rlp/ByteBufferWriterTest.java create mode 100644 rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPReaderTest.java create mode 100644 rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPWriterTest.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/ByteBufferSSZWriter.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZReader.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZWriter.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZ.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeTypeList.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeVector.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReadable.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReader.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZVariableSizeTypeList.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWritable.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWriter.java create mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/package-info.java create mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/ByteBufferWriterTest.java create mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZReaderTest.java create mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZWriterTest.java create mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/HashTreeRootTest.java create mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/TransactionNetworkPayload.java create mode 100644 units/.factorypath create mode 100644 units/src/main/java/org/apache/tuweni/units/bigints/Utils.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt256.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt32.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt384.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt64.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/Utils.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/package-info.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/package-info.java create mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt256Test.java create mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt32Test.java create mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt384Test.java create mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt64Test.java 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..2b7d7e317 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ArrayWrappingBytes.java @@ -0,0 +1,139 @@ +// 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 final byte[] bytes; + protected final int offset; + + 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..a000d7448 --- /dev/null +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/MutableBytes.java @@ -0,0 +1,737 @@ +// 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 Bytes { + private byte[] bytesArray; + + MutableBytes(int size) { + super(size); + this.bytesArray = new byte[size]; + } + + MutableBytes(byte[] bytesArray) { + super(bytesArray.length); + this.bytesArray = new byte[size]; + System.arraycopy(bytesArray, 0, this.bytesArray, 0, size); + } + + MutableBytes(byte[] bytesArray, int offset, int length) { + super(length); + this.bytesArray = new byte[length]; + System.arraycopy(bytesArray, offset, this.bytesArray, 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.bytesArray); + 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.bytesArray); + 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.bytesArray); + 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.bytesArray); + 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(bytesArray.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(bytesArray.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); + bytesArray[index] = b; + } + + /** + * 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 (bytesArray[i] == (byte) 0xFF) { + bytesArray[i] = (byte) 0x00; + } else { + byte currentValue = bytesArray[i]; + bytesArray[i] = ++currentValue; + break; + } + } + 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 (bytesArray[i] == (byte) 0x00) { + bytesArray[i] = (byte) 0xFF; + } else { + byte currentValue = bytesArray[i]; + bytesArray[i] = --currentValue; + break; + } + } + 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++) { + bytesArray[i] = b; + } + 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] = bytesArray[i]; + } + bytesArray = reverse; + 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(bytesArray, 0, size); + return this; + } + + int otherOffset = 0; + if (size < otherSize) { + byte[] newBytesArray = new byte[otherSize]; + System.arraycopy(bytesArray, 0, newBytesArray, otherSize - size, size); + bytesArray = newBytesArray; + size = otherSize; + } else { + Arrays.fill(bytesArray, 0, size - otherSize, (byte) 0); + otherOffset = size - otherSize; + } + other.and(bytesArray, otherOffset, otherSize); + return this; + } + + @Override + protected void and(byte[] bytesArray, int offset, int length) { + Utils.and(this.bytesArray, 0, bytesArray, offset, length); + } + + /** + * 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(bytesArray, 0, size); + return this; + } + + int otherOffset = 0; + if (size < otherSize) { + byte[] newBytesArray = new byte[otherSize]; + System.arraycopy(bytesArray, 0, newBytesArray, otherSize - size, size); + bytesArray = newBytesArray; + size = otherSize; + } else { + otherOffset = size - otherSize; + } + other.or(bytesArray, otherOffset, otherSize); + return this; + } + + @Override + protected void or(byte[] bytesArray, int offset, int length) { + Utils.or(this.bytesArray, 0, bytesArray, offset, length); + } + + /** + * 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(bytesArray, 0, size); + return this; + } + + int otherOffset = 0; + if (size < otherSize) { + byte[] newBytesArray = new byte[otherSize]; + System.arraycopy(bytesArray, 0, newBytesArray, otherSize - size, size); + bytesArray = newBytesArray; + size = otherSize; + } else { + otherOffset = size - otherSize; + } + other.xor(bytesArray, otherOffset, otherSize); + return this; + } + + @Override + protected void xor(byte[] bytesArray, int offset, int length) { + Utils.xor(this.bytesArray, 0, bytesArray, offset, length); + } + + /** + * Calculate a bit-wise NOT of these bytes. + * + * @return This mutable bytes instance. + */ + public MutableBytes not() { + for (int i = 0; i < size; i++) { + bytesArray[i] = (byte) ~bytesArray[i]; + } + 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) { + 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 : bytesArray[i - byteShift]; + bytesArray[i] = previousByte; + } + } + + if (bitShift > 0) { + for (int i = size - 1; i >= 0; i--) { + byte currentByte = bytesArray[i]; + byte previousByte = (i == 0) ? 0 : bytesArray[i - 1]; + int rightSide = (currentByte & 0XFF) >>> bitShift; + int leftSide = previousByte << (8 - bitShift); + bytesArray[i] = (byte) (leftSide | rightSide); + } + } + 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) { + 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) ? bytesArray[i + byteShift] : 0; + bytesArray[i] = nextByte; + } + } + + if (bitShift > 0) { + for (int i = 0; i < size; i++) { + byte currentByte = bytesArray[i]; + byte nextByte = (i == size - 1) ? 0 : bytesArray[i + 1]; + int leftSide = currentByte << bitShift; + int rightSide = (nextByte & 0XFF) >>> (8 - bitShift); + bytesArray[i] = (byte) (leftSide | rightSide); + } + } + 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(bytesArray, 0, newBytesArray, length - size, size); + bytesArray = newBytesArray; + size = length; + 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(bytesArray, 0, newBytesArray, 0, size); + bytesArray = newBytesArray; + size = length; + return this; + } + + @Override + public Bytes slice(int i, int length) { + checkArgument(length >= 0, "Invalid negative length"); + if (bytesArray.length > 0) { + checkElementIndex(i, bytesArray.length); + } + checkLength(bytesArray.length, i, length); + if (length == size) { + return this; + } + return new ArrayWrappingBytes(this.bytesArray, i, length); + } + + @Override + public MutableBytes mutableCopy() { + return new MutableBytes(bytesArray); + } + + @Override + public byte[] toArrayUnsafe() { + return bytesArray; + } + + public byte[] toArray() { + return toArrayUnsafe(); + } + + @Override + public byte get(int i) { + return bytesArray[i]; + } + + @Override + public int hashCode() { + int result = 1; + for (int i = 0; i < size; i++) { + result = 31 * result + bytesArray[i]; + } + return result; + } + + @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 (bytesArray[i] != other.get(i)) { + return false; + } + } + + return true; + } + + /** + * 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/devp2p/src/integrationTest/kotlin/org/apache/tuweni/v2/devp2p/v5/ConnectTwoServersTest.kt b/devp2p/src/integrationTest/kotlin/org/apache/tuweni/v2/devp2p/v5/ConnectTwoServersTest.kt new file mode 100644 index 000000000..10e8340b6 --- /dev/null +++ b/devp2p/src/integrationTest/kotlin/org/apache/tuweni/v2/devp2p/v5/ConnectTwoServersTest.kt @@ -0,0 +1,67 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import io.vertx.core.Vertx +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.apache.tuweni.concurrent.coroutines.await +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.junit.VertxExtension +import org.apache.tuweni.junit.VertxInstance +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.Hash +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.util.concurrent.ConcurrentHashMap + +internal class SimpleTestENRStorage : ENRStorage { + + val storage: MutableMap = ConcurrentHashMap() + + override fun find(nodeId: Bytes): EthereumNodeRecord? = storage[nodeId] + + override fun put(nodeId: Bytes, enr: EthereumNodeRecord) { + storage[nodeId] = enr + } +} + +@ExtendWith(BouncyCastleExtension::class, VertxExtension::class) +class ConnectTwoServersTest { + + @Test + fun testConnectTwoServers(@VertxInstance vertx: Vertx) = runBlocking { + val storage = SimpleTestENRStorage() + val service = DiscoveryService.open( + vertx, + SECP256K1.KeyPair.random(), + localPort = 40002, + bootstrapENRList = emptyList(), + enrStorage = storage, + ) + service.start().await() + + val otherStorage = SimpleTestENRStorage() + val otherService = DiscoveryService.open( + vertx, + SECP256K1.KeyPair.random(), + localPort = 40003, + bootstrapENRList = emptyList(), + enrStorage = otherStorage, + ) + otherService.start().await() + otherService.addPeer(service.enr()).await() + delay(500) + assertEquals(1, storage.storage.size) + assertEquals(1, otherStorage.storage.size) + assertNotNull(otherStorage.find(Hash.sha2_256(service.enr().toRLP()))) + assertNotNull(storage.find(Hash.sha2_256(otherService.enr().toRLP()))) + + service.terminate() + otherService.terminate() + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/Packet.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/Packet.kt index 40266dec5..8bd1fd130 100644 --- a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/Packet.kt +++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/Packet.kt @@ -398,7 +398,7 @@ internal class ENRRequestPacket private constructor( fun create(keyPair: SECP256K1.KeyPair, now: Long): Packet { val expiration = expirationFor(now) val sigHash = createSignature( - PacketType.ENRRESPONSE, + PacketType.ENRREQUEST, keyPair, ) { writer -> encodeTo(writer, expiration) diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryService.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryService.kt new file mode 100644 index 000000000..bdf1960f5 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryService.kt @@ -0,0 +1,922 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import com.google.common.cache.Cache +import com.google.common.cache.CacheBuilder +import io.vertx.core.Vertx +import io.vertx.core.buffer.Buffer +import io.vertx.core.datagram.DatagramPacket +import io.vertx.core.net.SocketAddress +import io.vertx.kotlin.coroutines.coAwait +import io.vertx.kotlin.coroutines.dispatcher +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull +import org.apache.tuweni.concurrent.AsyncCompletion +import org.apache.tuweni.concurrent.AsyncResult +import org.apache.tuweni.concurrent.coroutines.asyncCompletion +import org.apache.tuweni.concurrent.coroutines.asyncResult +import org.apache.tuweni.concurrent.coroutines.await +import org.apache.tuweni.devp2p.getValue +import org.apache.tuweni.devp2p.setValue +import org.apache.tuweni.kademlia.orderedInsert +import org.apache.tuweni.kademlia.xorDistCmp +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.slf4j.LoggerFactory +import java.net.InetAddress +import java.net.URI +import java.nio.ByteBuffer +import java.time.Instant +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import kotlin.coroutines.CoroutineContext + +internal const val PACKET_EXPIRATION_PERIOD_MS = (20 * 1000).toLong() // 20 seconds +internal const val PACKET_EXPIRATION_CHECK_GRACE_MS = (5 * 1000).toLong() // 5 seconds +internal const val PEER_VERIFICATION_TIMEOUT_MS = (22 * 1000).toLong() // 22 seconds (packet expiration + a little) +internal const val ENR_REQUEST_TIMEOUT_MS = (22 * 1000).toLong() // 22 seconds (packet expiration + a little) +internal const val PEER_VERIFICATION_RETRY_DELAY_MS = (5 * 60 * 1000).toLong() // 5 minutes +internal const val ENR_REQUEST_RETRY_DELAY_MS = (5 * 60 * 1000).toLong() // 5 minutes +internal const val BOOTSTRAP_PEER_VERIFICATION_TIMEOUT_MS = (2 * 60 * 1000).toLong() // 2 minutes +internal const val REFRESH_INTERVAL_MS = (60 * 1000).toLong() // 1 minute +internal const val PING_RETRIES: Int = 20 +internal const val RESEND_DELAY_MS = 1000.toLong() // 1 second +internal const val RESEND_DELAY_INCREASE_MS = 500.toLong() // 500 milliseconds +internal const val RESEND_MAX_DELAY_MS = (30 * 1000).toLong() // 30 seconds +internal const val ENDPOINT_PROOF_LONGEVITY_MS = (12 * 60 * 60 * 1000).toLong() // 12 hours +internal const val FIND_NODES_CACHE_EXPIRY = (3 * 60 * 1000).toLong() // 3 minutes +internal const val FIND_NODES_QUERY_GAP_MS = (30 * 1000).toLong() // 30 seconds +internal const val LOOKUP_RESPONSE_TIMEOUT_MS = 500.toLong() // 500 milliseconds + +/** + * An Ethereum ÐΞVp2p discovery service. + * + * This service supports devp2p discovery v4, alongside support for EIP-868. + * http://eips.ethereum.org/EIPS/eip-868 + * + */ +interface DiscoveryService { + + companion object { + internal val CURRENT_TIME_SUPPLIER: () -> Long = { System.currentTimeMillis() } + + /** + * Start the discovery service. + * + * @param vertx Vert.x instance + * @param keyPair the local node's keypair + * @param port the port to listen on (defaults to `0`, which will cause a random free port to be chosen) + * @param host the host name or IP address of the interface to bind to (defaults to `null`, which will cause the + * service to listen on all interfaces + * @param seq the sequence number of the Ethereum Node Record + * @param enrData the additional key/value pair entries to broadcast as an Ethereum Node Record (ENR). + * @param bootstrapURIs the URIs for bootstrap nodes + * @param peerRepository a [PeerRepository] for obtaining [Peer] instances + * @param advertiseAddress the IP address to advertise to peers, or `null` if the address of the first bound + * interface should be used. + * @param advertiseUdpPort the UDP port to advertise to peers, or `null` if the bound port should to be used. + * @param advertiseTcpPort the TCP port to advertise to peers, or `null` if it should be the same as the UDP port. + * @param routingTable a [PeerRoutingTable] which handles the ÐΞVp2p routing table + * @param packetFilter a filter for incoming packets + \ * @param bufferAllocator a [ByteBuffer] allocator, which must return buffers of size 1280 bytes or larger + * @param timeSupplier a function supplying the current time, in milliseconds since the epoch + */ + @JvmOverloads + fun open( + vertx: Vertx, + keyPair: SECP256K1.KeyPair, + port: Int = 0, + host: String? = null, + seq: Long = Instant.now().toEpochMilli(), + enrData: Map = emptyMap(), + bootstrapURIs: List = emptyList(), + peerRepository: PeerRepository = EphemeralPeerRepository(), + advertiseAddress: String? = null, + advertiseUdpPort: Int? = null, + advertiseTcpPort: Int? = null, + routingTable: PeerRoutingTable = DevP2PPeerRoutingTable(keyPair.publicKey()), + packetFilter: ((SECP256K1.PublicKey, SocketAddress) -> Boolean)? = null, + timeSupplier: () -> Long = CURRENT_TIME_SUPPLIER, + ): DiscoveryService { + val bindAddress = + if (host == null) { + SocketAddress.inetSocketAddress(port, "127.0.0.1") + } else { + SocketAddress.inetSocketAddress( + port, + host, + ) + } + return open( + vertx, + keyPair, + bindAddress, + seq, + enrData, + bootstrapURIs, + peerRepository, + advertiseAddress, + advertiseUdpPort, + advertiseTcpPort, + routingTable, + packetFilter, + timeSupplier, + ) + } + + /** + * Start the discovery service. + * + * @param vertx Vert.x instance + * @param keyPair the local node's keypair + * @param bindAddress the address to listen on + * @param seq the sequence number of the Ethereum Node Record + * @param enrData the additional key/value pair entries to broadcast as an Ethereum Node Record (ENR). + * @param bootstrapURIs the URIs for bootstrap nodes + * @param peerRepository a [PeerRepository] for obtaining [Peer] instances + * @param advertiseAddress the IP address to advertise for incoming packets + * @param advertiseUdpPort the UDP port to advertise to peers, or `null` if the bound port should to be used. + * @param advertiseTcpPort the TCP port to advertise to peers, or `null` if it should be the same as the UDP port. + * @param routingTable a [PeerRoutingTable] which handles the ÐΞVp2p routing table + * @param packetFilter a filter for incoming packets + * @param timeSupplier a function supplying the current time, in milliseconds since the epoch + */ + @JvmOverloads + fun open( + vertx: Vertx, + keyPair: SECP256K1.KeyPair, + bindAddress: SocketAddress, + seq: Long = Instant.now().toEpochMilli(), + enrData: Map = emptyMap(), + bootstrapURIs: List = emptyList(), + peerRepository: PeerRepository = EphemeralPeerRepository(), + advertiseAddress: String? = null, + advertiseUdpPort: Int? = null, + advertiseTcpPort: Int? = null, + routingTable: PeerRoutingTable = DevP2PPeerRoutingTable(keyPair.publicKey()), + packetFilter: ((SECP256K1.PublicKey, SocketAddress) -> Boolean)? = null, + timeSupplier: () -> Long = CURRENT_TIME_SUPPLIER, + ): DiscoveryService { + return CoroutineDiscoveryService( + vertx, + keyPair, seq, enrData, bindAddress, bootstrapURIs, advertiseAddress, advertiseUdpPort, advertiseTcpPort, + peerRepository, routingTable, packetFilter, timeSupplier, + ) + } + } + + /** + * `true` if the service has been shutdown + */ + val isShutdown: Boolean + + /** + * the UDP port that the service is listening on + */ + val localPort: Int + + /** + * the node id for this node (i.e. it's public key) + */ + val nodeId: SECP256K1.PublicKey + + /** + * Suspend until the bootstrap peers have been reached, or failed. + * + */ + suspend fun awaitBootstrap() + + /** + * Suspend until the bootstrap peers have been reached, or failed. + * + */ + fun awaitBootstrapAsync(): AsyncCompletion + + /** + * Attempt to find a specific peer, or peers close to it. + * + * @param target the node-id to search for + * @return a list of 16 peers, ordered by their distance to the target node-id. + */ + suspend fun lookup(target: SECP256K1.PublicKey): List + + /** + * Attempt to find a specific peer, or peers close to it asynchronously. + * + * @param target the node-id to search for + * @return a future of a list of 16 peers, ordered by their distance to the target node-id. + */ + fun lookupAsync(target: SECP256K1.PublicKey): AsyncResult> + + /** + * Shuts down this service. + */ + suspend fun shutdown() + + /** + * Suspend until this service has terminated. + */ + fun shutdownAsync(): AsyncCompletion + + /** + * Counter of invalid packets + */ + val invalidPackets: Long + + /** + * Counter of packets sent to self + */ + val selfPackets: Long + + /** + * Counter of expired packets + */ + val expiredPackets: Long + + /** + * Counter of filtered packets + */ + val filteredPackets: Long + + /** + * Counter of unvalidated peer packets + */ + val unvalidatedPeerPackets: Long + + /** + * Counter of unexpected pongs + */ + val unexpectedPongs: Long + + /** + * Counter of unexpected NEIGHBORS messages + */ + val unexpectedNeighbors: Long + + /** + * Counter of unexpected ENRResponse messages + */ + val unexpectedENRResponses: Long +} + +internal class CoroutineDiscoveryService( + vertx: Vertx, + private val keyPair: SECP256K1.KeyPair, + private val seq: Long = Instant.now().toEpochMilli(), + private val enrData: Map, + private val bindAddress: SocketAddress, + private val bootstrapURIs: List = emptyList(), + private val advertiseAddress: String? = null, + private val advertiseUdpPort: Int? = null, + private val advertiseTcpPort: Int? = null, + private val peerRepository: PeerRepository = EphemeralPeerRepository(), + private val routingTable: PeerRoutingTable = DevP2PPeerRoutingTable(keyPair.publicKey()), + private val packetFilter: ((SECP256K1.PublicKey, SocketAddress) -> Boolean)? = null, + private val timeSupplier: () -> Long = DiscoveryService.CURRENT_TIME_SUPPLIER, + override val coroutineContext: CoroutineContext = vertx.dispatcher() + CoroutineExceptionHandler { _, _ -> }, +) : DiscoveryService, CoroutineScope { + + companion object { + internal val logger = LoggerFactory.getLogger(DiscoveryService::class.java) + } + + private val serviceDescriptor = "ÐΞVp2p discovery " + System.identityHashCode(this) + private var selfEndpoint: Endpoint? = null + private var enr: Bytes? = null + + private val shutdown = AtomicBoolean(false) + private val bootstrapped = AsyncCompletion.incomplete() + private var refreshLoop: Job? = null + private val server = vertx.createDatagramSocket() + + override val isShutdown: Boolean + get() = shutdown.get() + + override val localPort: Int + get() = server.localAddress().port() + + override val nodeId: SECP256K1.PublicKey + get() = keyPair.publicKey() + + private val verifyingEndpoints: Cache = + CacheBuilder.newBuilder().expireAfterAccess(PEER_VERIFICATION_RETRY_DELAY_MS, TimeUnit.MILLISECONDS).build() + private val requestingENRs: Cache = + CacheBuilder.newBuilder().expireAfterAccess(ENR_REQUEST_RETRY_DELAY_MS, TimeUnit.MILLISECONDS).build() + private val awaitingPongs = ConcurrentHashMap() + private val awaitingENRs = ConcurrentHashMap() + private val findNodeStates: Cache = + CacheBuilder.newBuilder().expireAfterAccess(FIND_NODES_CACHE_EXPIRY, TimeUnit.MILLISECONDS) + .removalListener { it.value?.close() } + .build() + + override var invalidPackets: Long by AtomicLong(0) + override var selfPackets: Long by AtomicLong(0) + override var expiredPackets: Long by AtomicLong(0) + override var filteredPackets: Long by AtomicLong(0) + override var unvalidatedPeerPackets: Long by AtomicLong(0) + override var unexpectedPongs: Long by AtomicLong(0) + override var unexpectedENRResponses: Long by AtomicLong(0) + override var unexpectedNeighbors: Long by AtomicLong(0) + + init { + start() + } + + private fun start() = launch { + server.handler { receiveDatagram(it) }.listen(bindAddress.port(), bindAddress.host()).coAwait() + val endpoint = Endpoint( + advertiseAddress ?: (server.localAddress()).host(), + advertiseUdpPort ?: server.localAddress().port(), + advertiseTcpPort, + ) + enr = EthereumNodeRecord.toRLP( + keyPair, + seq, + enrData, + null, + InetAddress.getByName(endpoint.address), + endpoint.tcpPort, + endpoint.udpPort, + ) + selfEndpoint = endpoint + refreshLoop = launch { + while (true) { + delay(REFRESH_INTERVAL_MS) + refresh() + } + } + launch { + AsyncCompletion.allOf( + bootstrapURIs.map { uri -> + asyncCompletion { bootstrapFrom(uri) } + }, + ).thenRun { + bootstrapped.complete() + }.await() + } + + logger.info("{}: started, listening on {}", serviceDescriptor, server.localAddress()) + } + + private suspend fun bootstrapFrom(uri: URI): Boolean { + logger.info("Starting verification of bootnode $uri") + val peer = peerRepository.get(uri) + logger.info("Starting verification of bootnode $uri") + try { + val result = withTimeout(BOOTSTRAP_PEER_VERIFICATION_TIMEOUT_MS) { + endpointVerification(peer.endpoint, peer).verifyWithRetries() + } ?: return false + if (result.peer != peer) { + logger.warn( + "{}: ignoring bootstrap peer {} - responding node used a different node-id", + serviceDescriptor, + uri, + ) + return false + } + logger.info("{}: verified bootstrap peer {}", serviceDescriptor, uri) + addToRoutingTable(peer) + findNodes(peer, nodeId).thenRun { + logger.info("{}: completed bootstrapping from {}", serviceDescriptor, uri) + } + return true + } catch (_: TimeoutCancellationException) { + logger.warn("{}: timeout verifying bootstrap node {}", serviceDescriptor, uri) + return false + } + } + + private fun receiveDatagram(packet: DatagramPacket) { + // do quick sanity checks and discard bad packets before launching a co-routine + if (packet.data().length() < Packet.MIN_SIZE) { + logger.debug("{}: ignoring under-sized packet with source {}", serviceDescriptor, packet.sender()) + ++invalidPackets + return + } + if (packet.data().length() > Packet.MAX_SIZE) { + logger.debug("{}: ignoring over-sized packet with source {}", serviceDescriptor, packet.sender()) + ++invalidPackets + return + } + + val arrivalTime = timeSupplier() + launch { + try { + receivePacket(packet.data(), packet.sender(), arrivalTime) + } catch (e: Throwable) { + logger.error("$serviceDescriptor: unexpected error during packet handling", e) + } + } + } + + override suspend fun awaitBootstrap() = bootstrapped.await() + + override fun awaitBootstrapAsync(): AsyncCompletion = bootstrapped + + override suspend fun shutdown() { + if (shutdown.compareAndSet(false, true)) { + logger.info("{}: shutdown", serviceDescriptor) + server.close().coAwait() + for (pending in awaitingPongs.values) { + pending.complete(null) + } + awaitingPongs.clear() + verifyingEndpoints.invalidateAll() + verifyingEndpoints.cleanUp() + findNodeStates.invalidateAll() + findNodeStates.cleanUp() + refreshLoop?.cancel() + } + } + + override fun shutdownAsync(): AsyncCompletion = asyncCompletion { shutdown() } + + override suspend fun lookup(target: SECP256K1.PublicKey): List { + val targetId = target.bytesArray() + val results = neighbors(target).toMutableList() + logger.debug("Initial neighbors query {}", results) + + // maybe add ourselves to the set + val selfPeer = peerRepository.get(selfEndpoint!!.address, selfEndpoint!!.udpPort, nodeId) + results.orderedInsert(selfPeer) { a, _ -> targetId.xorDistCmp(a.nodeId.bytesArray(), nodeId.bytesArray()) } + results.removeAt(results.lastIndex) + + withTimeout(LOOKUP_RESPONSE_TIMEOUT_MS) { + results.map { nodeToQuery -> + async { + val nodes = findNodes(nodeToQuery, target).await() + for (node in ArrayList(nodes)) { + val peer = peerRepository.get(selfEndpoint!!.address, selfEndpoint!!.udpPort, node.nodeId) + if (!results.contains(peer)) { + results.orderedInsert(peer) { a, _ -> + targetId.xorDistCmp( + a.nodeId.bytesArray(), + node.nodeId.bytesArray(), + ) + } + results.removeAt(results.lastIndex) + } + } + } + } + }.awaitAll() + return results + } + + override fun lookupAsync(target: SECP256K1.PublicKey) = asyncResult { lookup(target) } + + private suspend fun refresh() { + logger.debug("{}: table refresh triggered", serviceDescriptor) + // TODO: instead of a random target, choose a target to optimally fill the peer table + lookup(SECP256K1.KeyPair.random().publicKey()) + } + + private suspend fun receivePacket(datagram: Buffer, address: SocketAddress, arrivalTime: Long) { + val packet: Packet + try { + packet = Packet.decodeFrom(Bytes.wrap(datagram.bytes)) + } catch (e: DecodingException) { + logger.debug("{}: ignoring invalid packet from {}", serviceDescriptor, address) + ++invalidPackets + return + } + + if (packet.nodeId == nodeId) { + logger.debug("{}: ignoring packet from self", serviceDescriptor) + ++selfPackets + return + } + + if (packet.isExpired(arrivalTime - PACKET_EXPIRATION_CHECK_GRACE_MS)) { + logger.debug("{}: ignoring expired packet", serviceDescriptor) + ++expiredPackets + return + } + + if (packetFilter?.invoke(packet.nodeId, address) == false) { + logger.debug("{}: packet rejected by filter", serviceDescriptor) + ++filteredPackets + return + } + + when (packet) { + is PingPacket -> handlePing(packet, address, arrivalTime) + is PongPacket -> handlePong(packet, address, arrivalTime) + is FindNodePacket -> handleFindNode(packet, address, arrivalTime) + is NeighborsPacket -> handleNeighbors(packet, address) + is ENRRequestPacket -> handleENRRequest(packet, address, arrivalTime) + is ENRResponsePacket -> handleENRResponse(packet, address, arrivalTime) + }.let {} // guarantees "when" matching is exhaustive + } + + private suspend fun handlePing(packet: PingPacket, from: SocketAddress, arrivalTime: Long) { + // COMPATIBILITY: The ping packet should contain the canonical endpoint for the peer, yet it is often observed to + // be incorrect (using private-subnet addresses, wildcard addresses, etc). So instead, respond to the source + // address of the packet itself. + val fromEndpoint = Endpoint(from, packet.from.tcpPort) + val peer = peerRepository.get(from.host(), from.port(), packet.nodeId) + // update the endpoint if the peer does not have one that's been proven + var currentEndpoint = peer.updateEndpoint(fromEndpoint, arrivalTime, arrivalTime - ENDPOINT_PROOF_LONGEVITY_MS) + if (currentEndpoint.tcpPort != packet.from.tcpPort) { + currentEndpoint = peer.updateEndpoint( + Endpoint(currentEndpoint.address, currentEndpoint.udpPort, packet.from.tcpPort), + arrivalTime, + ) + } + + val pong = PongPacket.create(keyPair, timeSupplier(), currentEndpoint, packet.hash, seq) + sendPacket(from, pong) + // https://github.com/ethereum/devp2p/blob/master/discv4.md#ping-packet-0x01 also suggests sending a ping + // packet if the peer is unknown, however sending two packets in response to a single incoming would allow a + // traffic amplification attack + } + + private suspend fun handlePong(packet: PongPacket, from: SocketAddress, arrivalTime: Long) { + val pending = awaitingPongs.remove(packet.pingHash) ?: run { + logger.debug("{}: received unexpected or late pong from {}", serviceDescriptor, from) + ++unexpectedPongs + return + } + + val sender = pending.peer + // COMPATIBILITY: If the node-id's don't match, the pong should probably be rejected. However, if a different + // peer is listening at the same address, it will respond to the ping with its node-id. Instead of rejecting, + // accept the pong and update the new peer record with the proven endpoint, preferring to keep its current + // tcpPort and otherwise keeping the tcpPort of the original peer. + val peer = + if (sender.nodeId == packet.nodeId) sender else peerRepository.get(from.host(), from.port(), packet.nodeId) + val endpoint = if (peer.verifyEndpoint(pending.endpoint, arrivalTime)) { + pending.endpoint + } else { + val endpoint = peer.endpoint.tcpPort?.let { port -> + Endpoint(pending.endpoint.address, pending.endpoint.udpPort, port) + } ?: pending.endpoint + peer.updateEndpoint(endpoint, arrivalTime) + peer.verifyEndpoint(endpoint, arrivalTime) + endpoint + } + + if (sender.nodeId == packet.nodeId) { + logger.debug("{}: verified peer endpoint {} (node-id: {})", serviceDescriptor, endpoint.address, peer.nodeId) + } else { + logger.debug( + "{}: verified peer endpoint {} (node-id: {} - changed from {})", + serviceDescriptor, + endpoint.address, + peer.nodeId, + sender.nodeId, + ) + } + + pending.complete(VerificationResult(peer, endpoint)) + + if (packet.enrSeq != null) { + if (peer.enr == null || peer.enr!!.seq() < packet.enrSeq) { + val now = timeSupplier() + withTimeoutOrNull(ENR_REQUEST_TIMEOUT_MS) { enrRequest(endpoint, peer).verify(now) } + } + } + } + + private suspend fun handleFindNode(packet: FindNodePacket, from: SocketAddress, arrivalTime: Long) { + // if the peer has not been validated, delay sending neighbors until it is + val peer = peerRepository.get(from.host(), from.port(), packet.nodeId) + val (_, endpoint) = ensurePeerIsValid(peer, from, arrivalTime) ?: run { + logger.debug("{}: received findNode from {} which cannot be validated", serviceDescriptor, from) + ++unvalidatedPeerPackets + return + } + + logger.debug("{}: received findNode from {} for target-id {}", serviceDescriptor, from, packet.target) + val nodes = neighbors(packet.target).map { p -> p.toNode() } + + val address = endpoint.udpSocketAddress + NeighborsPacket.createRequired(keyPair, timeSupplier(), nodes).forEach { p -> + logger.debug("{}: sending {} neighbors to {}", serviceDescriptor, p.nodes.size, address) + sendPacket(address, p) + } + } + + private fun handleNeighbors(packet: NeighborsPacket, from: SocketAddress) { + findNodeStates.getIfPresent(packet.nodeId)?.let { state -> + for (node in packet.nodes) { + launch { + logger.debug("{}: received neighbour {} from {}", serviceDescriptor, node.endpoint.address, from) + val neighbor = peerRepository.get(from.host(), from.port(), node.nodeId) + val now = timeSupplier() + neighbor.updateEndpoint(node.endpoint, now, now - ENDPOINT_PROOF_LONGEVITY_MS) + + withTimeoutOrNull(PEER_VERIFICATION_TIMEOUT_MS) { + endpointVerification(node.endpoint, neighbor).verify(now) + }?.let { result -> + logger.debug( + "{}: adding {} to the routing table (node-id: {})", + serviceDescriptor, + result.endpoint.address, + result.peer.nodeId, + ) + addToRoutingTable(result.peer) + } + } + } + state.receive(packet.nodes) + } ?: run { + logger.debug("{}: received unexpected or late neighbors packet from {}", serviceDescriptor, from) + ++unexpectedNeighbors + } + } + + private suspend fun handleENRRequest(packet: ENRRequestPacket, from: SocketAddress, arrivalTime: Long) { + val peer = peerRepository.get(from.host(), from.port(), packet.nodeId) + val (_, endpoint) = ensurePeerIsValid(peer, from, arrivalTime) ?: run { + logger.debug("{}: received enrRequest from {} which cannot be validated", serviceDescriptor, from) + ++unvalidatedPeerPackets + return + } + + logger.debug("{}: received enrRequest from {}", serviceDescriptor, from) + + val address = endpoint.udpSocketAddress + sendPacket(address, ENRResponsePacket.create(keyPair, timeSupplier(), packet.hash, enr!!)) + } + + private suspend fun handleENRResponse(packet: ENRResponsePacket, from: SocketAddress, arrivalTime: Long) { + val pending = awaitingENRs.remove(packet.requestHash) ?: run { + logger.debug("{}: received unexpected or late enr response from {}", serviceDescriptor, from) + ++unexpectedENRResponses + return + } + packet.requestHash + val sender = pending.peer + // COMPATIBILITY: If the node-id's don't match, the pong should probably be rejected. However, if a different + // peer is listening at the same address, it will respond to the ping with its node-id. Instead of rejecting, + // accept the pong and update the new peer record with the proven endpoint, preferring to keep its current + // tcpPort and otherwise keeping the tcpPort of the original peer. + val peer = + if (sender.nodeId == packet.nodeId) sender else peerRepository.get(from.host(), from.port(), packet.nodeId) + + val enr = EthereumNodeRecord.fromRLP(packet.enr) + try { + enr.validate() + } catch (e: InvalidNodeRecordException) { + logger.debug("Invalid ENR", e) + return + } + + peer.updateENR(enr, arrivalTime) + + pending.complete(ENRResult(peer, enr)) + } + + private fun addToRoutingTable(peer: Peer) { + routingTable.add(peer)?.let { contested -> + launch { + contested.endpoint.let { endpoint -> + withTimeoutOrNull(PEER_VERIFICATION_TIMEOUT_MS) { endpointVerification(endpoint, contested).verify() } + } ?: routingTable.evict(contested) + } + } + } + + private suspend fun ensurePeerIsValid( + peer: Peer, + address: SocketAddress, + arrivalTime: Long, + ): VerificationResult? { + val now = timeSupplier() + peer.getEndpoint(now - ENDPOINT_PROOF_LONGEVITY_MS)?.let { endpoint -> + // already valid + return VerificationResult(peer, endpoint) + } + + val endpoint = peer.updateEndpoint( + Endpoint(address, peer.endpoint.tcpPort), + arrivalTime, + now - ENDPOINT_PROOF_LONGEVITY_MS, + ) + return withTimeoutOrNull(PEER_VERIFICATION_TIMEOUT_MS) { endpointVerification(endpoint, peer).verify(now) } + } + + private fun endpointVerification(endpoint: Endpoint, peer: Peer) = + verifyingEndpoints.get(endpoint.udpSocketAddress) { EndpointVerification(endpoint, peer) } + + // a representation of the state and current action for verifying an endpoint, + // to avoid concurrent attempts to verify the same endpoint + private inner class EndpointVerification(val endpoint: Endpoint, val peer: Peer) { + private val deferred = CompletableDeferred() + + @Volatile + private var active: Job? = null + private var nextPingMs: Long = 0 + private var retryDelay: Long = 0 + + suspend fun verify(now: Long = timeSupplier()): VerificationResult? { + if (!deferred.isCompleted) { + // if not already actively pinging and enough time has passed since the last ping, send a single ping + synchronized(this) { + if (active?.isCompleted != false && now >= nextPingMs) { + nextPingMs = now + RESEND_DELAY_MS + launch { sendPing(now) } + } + } + } + return deferred.await() + } + + suspend fun verifyWithRetries(): VerificationResult? { + if (!deferred.isCompleted) { + // if not already actively pinging, start pinging with retries + synchronized(this) { + if (active?.isCompleted != false) { + active = launch { + repeat(PING_RETRIES) { + delay(nextPingMs - timeSupplier()) + nextPingMs = timeSupplier() + RESEND_DELAY_MS + retryDelay += RESEND_DELAY_INCREASE_MS + if (retryDelay > RESEND_MAX_DELAY_MS) { + retryDelay = RESEND_MAX_DELAY_MS + } + sendPing() + } + } + } + } + } + return deferred.await() + } + + private suspend fun sendPing(now: Long = timeSupplier()) { + val pingPacket = PingPacket.create(keyPair, now, selfEndpoint!!, endpoint, seq) + + // create local references to be captured in the closure, rather than the whole packet instance + val hash = pingPacket.hash + val timeout = pingPacket.expiration - now + + // very unlikely that there is another ping packet created with the same hash yet a different EndpointVerification + // instance, but if there is then the first will be waiting on a deferred that never completes and will + // eventually time out + if (awaitingPongs.put(hash, this) != this) { + launch { + delay(timeout) + awaitingPongs.remove(hash) + } + sendPacket(endpoint.udpSocketAddress, pingPacket) + } + } + + fun complete(result: VerificationResult?): Boolean { + active?.cancel() + return deferred.complete(result) + } + } + + private data class VerificationResult( + /** The peer that responded to the verification request. */ + val peer: Peer, + /** + * The endpoint that was verified. + * + * This will typically be the same as peer.endpoint, but may not be due to concurrent updates. + */ + val endpoint: Endpoint, + ) + + private fun enrRequest(endpoint: Endpoint, peer: Peer) = + requestingENRs.get(endpoint.udpSocketAddress) { ENRRequest(endpoint, peer) } + + // a representation of the state and current action for querying an ENR from a peer, + // to avoid concurrent attempts to request the same information. + private inner class ENRRequest(val endpoint: Endpoint, val peer: Peer) { + private val deferred = CompletableDeferred() + + @Volatile + private var active: Job? = null + private var nextENRRequest: Long = 0 + + suspend fun verify(now: Long = timeSupplier()): ENRResult? { + if (!deferred.isCompleted) { + // if not already actively requesting and enough time has passed since the last request, send a single request + synchronized(this) { + if (active?.isCompleted != false && now >= nextENRRequest) { + nextENRRequest = now + RESEND_DELAY_MS + launch { sendENRRequest(now) } + } + } + } + return deferred.await() + } + + private suspend fun sendENRRequest(now: Long = timeSupplier()) { + val enrRequestPacket = ENRRequestPacket.create(keyPair, now) + + // create local references to be captured in the closure, rather than the whole packet instance + val hash = enrRequestPacket.hash + val timeout = enrRequestPacket.expiration - now + + // very unlikely that there is another ping packet created with the same hash yet a different ENRRequest + // instance, but if there is then the first will be waiting on a deferred that never completes and will + // eventually time out + if (awaitingENRs.put(hash, this) != this) { + launch { + delay(timeout) + awaitingENRs.remove(hash) + } + sendPacket(endpoint.udpSocketAddress, enrRequestPacket) + } + } + + fun complete(result: ENRResult?): Boolean { + active?.cancel() + return deferred.complete(result) + } + } + + private data class ENRResult( + val peer: Peer, + val enr: EthereumNodeRecord, + ) + + private suspend fun findNodes(peer: Peer, target: SECP256K1.PublicKey): AsyncResult> { + if (peer.nodeId == nodeId) { + // for queries to self, respond directly + return AsyncResult.completed(neighbors(target).map { p -> p.toNode() }) + } + return findNodeStates.get(peer.nodeId) { FindNodeState(peer) }.findNodes(target) + } + + private fun neighbors(target: SECP256K1.PublicKey) = routingTable.nearest(target, DEVP2P_BUCKET_SIZE) + + private data class FindNodeRequest(val target: SECP256K1.PublicKey) + + private inner class FindNodeState(val peer: Peer) { + + private val nodesCollected = Collections.synchronizedList(ArrayList()) + private val result = AsyncResult.incomplete>() + private val start = timeSupplier() + + @Volatile + private var lastReceive: Long = 0 + + private suspend fun send(request: FindNodeRequest) { + try { + val endpoint = peer.endpoint + val now = timeSupplier() + lastReceive = now + val findNodePacket = FindNodePacket.create(keyPair, now, request.target) + sendPacket(endpoint.udpSocketAddress, findNodePacket) + logger.debug("{}: sent findNode to {} for {}", serviceDescriptor, endpoint.udpSocketAddress, request.target) + + // issue a "get" on the state cache, to indicate that this state is still in use + val state = findNodeStates.getIfPresent(peer.nodeId) + if (state != this) { + logger.warn("{}: findNode state for {} has been replaced", serviceDescriptor, state) + close() + } + } catch (e: TimeoutCancellationException) { + logger.debug( + "$serviceDescriptor: Timeout while sending FindNode requests for peer ${peer.nodeId}", + e, + ) + } catch (e: Exception) { + logger.error( + "$serviceDescriptor: Error while sending FindNode requests for peer ${peer.nodeId}", + e, + ) + } + } + + suspend fun findNodes(target: SECP256K1.PublicKey): AsyncResult> { + send(FindNodeRequest(target)) + return result + } + + fun receive(nodes: List) { + lastReceive = timeSupplier() + nodesCollected.addAll(nodes) + if (lastReceive - start > FIND_NODES_QUERY_GAP_MS) { + close() + } + } + + fun close() { + result.complete(nodesCollected) + } + } + + private suspend fun sendPacket(address: SocketAddress, packet: Packet) { + server.send(Buffer.buffer(packet.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Endpoint.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Endpoint.kt new file mode 100644 index 000000000..9f646edf6 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Endpoint.kt @@ -0,0 +1,99 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import io.vertx.core.net.SocketAddress +import org.apache.tuweni.rlp.RLPException +import org.apache.tuweni.v2.rlp.RLPReader +import org.apache.tuweni.v2.rlp.RLPWriter +import java.net.InetAddress +import java.net.UnknownHostException + +/** + * An Ethereum node endpoint. + * + * @constructor Create a new endpoint. + * @param address the InetAddress + * @param udpPort the UDP port for the endpoint + * @param tcpPort the TCP port for the endpoint or `null` if no TCP port is known + * @throws IllegalArgumentException if either port is out of range + */ +data class Endpoint( + val address: String, + val udpPort: Int = DEFAULT_PORT, + val tcpPort: Int? = null, +) { + + /** + * Create a new endpoint. + * + * @param address a SocketAddress, containing the IP address the UDP port + */ + constructor(address: SocketAddress, tcpPort: Int? = null) : this(address.host(), address.port(), tcpPort) + + companion object { + + /** + * The default port used by Ethereum DevP2P. + */ + const val DEFAULT_PORT = 30303 + + /** + * Create an Endpoint by reading fields from the RLP input stream. + * + * If the fields are wrapped into an RLP list, use `reader.readList` to unwrap before calling this method. + * + * @param reader the RLP input stream from which to read + * @return the decoded endpoint + * @throws RLPException if the RLP source does not decode to a valid endpoint + */ + fun readFrom(reader: RLPReader): Endpoint { + val addr: InetAddress + try { + addr = InetAddress.getByAddress(reader.readValue().toArrayUnsafe()) + } catch (e: UnknownHostException) { + throw RLPException(e) + } + + var udpPort = reader.readInt() + if (udpPort == 0) { // this is an invalid port number we see in the wild. Use DEFAULT_PORT instead. + udpPort = DEFAULT_PORT + } + // Some implementations seem to send packets that either do not have the TCP port field, or to have an + // RLP NULL value for it. + var tcpPort: Int? = null + if (!reader.isComplete) { + tcpPort = reader.readInt() + if (tcpPort == 0) { + tcpPort = null + } + } + + return Endpoint(addr.hostAddress, udpPort, tcpPort) + } + } + + init { + require(udpPort in 1..65535) { "udpPort should be between 1 and 65535, got $udpPort" } + require(tcpPort == null || tcpPort in 1..65535) { "tcpPort should be between 1 and 65535, got $tcpPort" } + } + + /** + * UDP socket address of the endpoint + */ + val udpSocketAddress: SocketAddress = SocketAddress.inetSocketAddress(udpPort, address) + + /** + * Write this endpoint to an RLP output. + * + * @param writer the RLP writer + */ + internal fun writeTo(writer: RLPWriter) { + writer.writeByteArray(InetAddress.getByName(address).address) + writer.writeInt(udpPort) + writer.writeInt(tcpPort ?: 0) + } + + // rough over-estimate, assuming maximum size encoding for the port numbers + internal fun rlpSize(): Int = 1 + InetAddress.getByName(address).address.size + 2 * (1 + 2) +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EnodeUriComponents.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EnodeUriComponents.kt new file mode 100644 index 000000000..9e7bcb409 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EnodeUriComponents.kt @@ -0,0 +1,55 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import java.net.URI +import java.util.regex.Pattern + +private val DISCPORT_QUERY_STRING_REGEX = Pattern.compile(".*discport=([^&]+).*") + +/** + * The components of an enode URI. + * @param nodeId the public key of the node + * @param endpoint the ndoe endpoint + */ +data class EnodeUriComponents(val nodeId: SECP256K1.PublicKey, val endpoint: Endpoint) + +/** + * Parse an enode URI. + * + * @param uri the URI to parse + * @return the node id and the endpoint + * @throws IllegalArgumentException if the uri is not a valid enode URI + */ +fun parseEnodeUri(uri: URI): EnodeUriComponents { + require("enode" == uri.scheme) { "URI must be an enode:// uri" } + require(uri.userInfo != null) { "URI must have a node id" } + val nodeId = SECP256K1.PublicKey.fromBytes(Bytes.fromHexString(uri.userInfo)) + + var tcpPort = Endpoint.DEFAULT_PORT + if (uri.port >= 0) { + tcpPort = uri.port + } + + // If TCP and UDP ports differ, expect a query param 'discport' with the UDP port. + // See https://github.com/ethereum/wiki/wiki/enode-url-format + var udpPort = tcpPort + val query = uri.query + if (query != null) { + val matcher = DISCPORT_QUERY_STRING_REGEX.matcher(query) + if (matcher.matches()) { + try { + udpPort = Integer.parseInt(matcher.group(1)) + } catch (e: NumberFormatException) { + throw IllegalArgumentException("Invalid discport query parameter") + } + } + } + + return EnodeUriComponents( + nodeId, + Endpoint(uri.host, udpPort, tcpPort), + ) +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecord.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecord.kt new file mode 100644 index 000000000..4c0bf2604 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecord.kt @@ -0,0 +1,329 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.bytes.MutableBytes +import org.apache.tuweni.v2.crypto.Hash +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.rlp.RLP +import org.apache.tuweni.v2.rlp.RLPReader +import org.apache.tuweni.v2.rlp.RLPWriter +import org.apache.tuweni.v2.units.bigints.UInt256 +import java.lang.IllegalArgumentException +import java.lang.RuntimeException +import java.net.InetAddress +import java.time.Instant + +/** + * Ethereum Node Record (ENR) as described in [EIP-778](https://eips.ethereum.org/EIPS/eip-778). + * + * @param signature the record signature + * @param seq the sequence of the record, its revision number + * @param data the arbitrary data of the record + * @param listData the arbitrary data of the record as list + */ +class EthereumNodeRecord( + val signature: Bytes, + val seq: Long, + val data: Map, + val listData: Map> = emptyMap(), + val rlp: Bytes, +) { + + companion object { + + /** + * Derives the public key of an ethereum node record into a unique 32 bytes hash. + * @param publicKey the public key to hash + * @return the hash of the public key + * @throws IllegalArgumentException if the public key is not valid. + */ + fun nodeId(publicKey: SECP256K1.PublicKey): Bytes { + val pt = publicKey.asEcPoint() + val xPart = UInt256.valueOf(pt.xCoord.toBigInteger()) + val yPart = UInt256.valueOf(pt.yCoord.toBigInteger()) + return Hash.keccak256(Bytes.wrap(xPart, yPart)) + } + + /** + * Creates an ENR from its serialized form as a RLP list + * @param rlp the serialized form of the ENR + * @return the ENR + * @throws IllegalArgumentException if the rlp bytes length is longer than 300 bytes + */ + @JvmStatic + fun fromRLP(rlp: Bytes): EthereumNodeRecord { + if (rlp.size() > 300) { + throw IllegalArgumentException("Record too long") + } + return RLP.decodeList(rlp) { fromRLP(it, rlp) } + } + + /** + * Creates an ENR from its serialized form as a RLP list + * @param reader the RLP reader + * @return the ENR + * @throws IllegalArgumentException if the rlp bytes length is longer than 300 bytes + */ + @JvmStatic + fun fromRLP(reader: RLPReader): EthereumNodeRecord { + val tempRecord = fromRLP(reader, Bytes.EMPTY) + val encoded = RLP.encodeList { + it.writeValue(tempRecord.signature) + encode(data = tempRecord.data, seq = tempRecord.seq, writer = it) + } + + return fromRLP(encoded) + } + + /** + * Creates an ENR from its serialized form as a RLP list + * @param reader the RLP reader + * @return the ENR + * @throws IllegalArgumentException if the rlp bytes length is longer than 300 bytes + */ + @JvmStatic + fun fromRLP(reader: RLPReader, rlp: Bytes): EthereumNodeRecord { + val sig = reader.readValue() + + val seq = reader.readLong() + + val data = mutableMapOf() + val listData = mutableMapOf>() + while (!reader.isComplete) { + val key = reader.readString() + if (reader.nextIsList()) { + listData[key] = reader.readListContents { listreader -> + if (listreader.nextIsList()) { + // TODO complex structures not supported + listreader.skipNext() + null + } else { + listreader.readValue() + } + }.filterNotNull() + } else { + val value = reader.readValue() + data[key] = value + } + } + + return EthereumNodeRecord(sig, seq, data, listData, rlp) + } + + fun encode( + signatureKeyPair: SECP256K1.KeyPair? = null, + seq: Long = Instant.now().toEpochMilli(), + ip: InetAddress? = null, + tcp: Int? = null, + udp: Int? = null, + data: Map? = null, + listData: Map>? = null, + writer: RLPWriter, + ) { + writer.writeLong(seq) + val mutableData = data?.toMutableMap() ?: mutableMapOf() + mutableData["id"] = Bytes.wrap("v4".toByteArray()) + signatureKeyPair?.let { + mutableData["secp256k1"] = Bytes.wrap(it.publicKey().asEcPoint().getEncoded(true)) + } + ip?.let { + mutableData["ip"] = Bytes.wrap(it.address) + } + tcp?.let { + mutableData["tcp"] = Bytes.ofUnsignedShort(it) + } + udp?.let { + mutableData["udp"] = Bytes.ofUnsignedShort(it) + } + val keys = mutableListOf() + keys.addAll(mutableData.keys) + listData?.let { keys.addAll(it.keys) } + keys.sorted().forEach { key -> + mutableData[key]?.let { value -> + writer.writeString(key) + writer.writeValue(value) + } + listData?.get(key)?.let { value -> + writer.writeString(key) + writer.writeList(value) { writer, v -> writer.writeValue(v) } + } + } + } + + /** + * Creates the serialized form of a ENR + * @param signatureKeyPair the key pair to use to sign the ENR + * @param seq the sequence number for the ENR. It should be higher than the previous time the ENR was generated. It defaults to the current time since epoch in milliseconds. + * @param data the key pairs to encode in the ENR + * @param listData the key pairs of list values to encode in the ENR + * @param ip the IP address of the host + * @param tcp an optional parameter to a TCP port used for the wire protocol + * @param udp an optional parameter to a UDP port used for discovery + * @return the ENR + */ + @JvmOverloads + @JvmStatic + fun create( + signatureKeyPair: SECP256K1.KeyPair, + seq: Long = Instant.now().toEpochMilli(), + data: Map? = null, + listData: Map>? = null, + ip: InetAddress, + tcp: Int? = null, + udp: Int? = null, + ): EthereumNodeRecord { + return fromRLP(toRLP(signatureKeyPair, seq, data, listData, ip, tcp, udp)) + } + + /** + * Creates the serialized form of a ENR + * @param signatureKeyPair the key pair to use to sign the ENR + * @param seq the sequence number for the ENR. It should be higher than the previous time the ENR was generated. It defaults to the current time since epoch in milliseconds. + * @param data the key pairs to encode in the ENR + * @param listData the key pairs of list values to encode in the ENR + * @param ip the IP address of the host + * @param tcp an optional parameter to a TCP port used for the wire protocol + * @param udp an optional parameter to a UDP port used for discovery + * @return the serialized form of the ENR as a RLP-encoded list + */ + @JvmOverloads + @JvmStatic + fun toRLP( + signatureKeyPair: SECP256K1.KeyPair, + seq: Long = Instant.now().toEpochMilli(), + data: Map? = null, + listData: Map>? = null, + ip: InetAddress, + tcp: Int? = null, + udp: Int? = null, + ): Bytes { + val encoded = RLP.encodeList { writer -> + encode(signatureKeyPair, seq, ip, tcp, udp, data, listData, writer) + } + val signature = SECP256K1.sign(encoded, signatureKeyPair) + val sigBytes = MutableBytes.create(64) + sigBytes.set(0, UInt256.valueOf(signature.r())) + sigBytes.set(32, UInt256.valueOf(signature.s())) + + val completeEncoding = RLP.encodeList { writer -> + writer.writeValue(sigBytes) + encode(signatureKeyPair, seq, ip, tcp, udp, data, listData, writer) + } + return completeEncoding + } + } + + /** + * Validates an ENR to check that it conforms to a valid ENR scheme. + * + * Only the v4 scheme is supported at this time. + */ + fun validate() { + if (Bytes.wrap("v4".toByteArray()) != data["id"]) { + throw InvalidNodeRecordException("id attribute is not set to v4") + } + + val encoded = RLP.encodeList { + encode(data = data, seq = seq, writer = it) + } + + val sig = SECP256K1.Signature.create( + 1, + signature.slice(0, 32).toUnsignedBigInteger(), + signature.slice(32).toUnsignedBigInteger(), + ) + + val pubKey = publicKey() + val recovered = SECP256K1.PublicKey.recoverFromSignature(encoded, sig) + + if (pubKey != recovered) { + val sig0 = SECP256K1.Signature.create( + 0, + signature.slice(0, 32).toUnsignedBigInteger(), + signature.slice(32).toUnsignedBigInteger(), + ) + val recovered0 = SECP256K1.PublicKey.recoverFromSignature(encoded, sig0) + if (pubKey != recovered0) { + throw InvalidNodeRecordException("Public key does not match signature") + } + } + } + + /** + * The ENR public key entry + * @return the ENR public key + */ + fun publicKey(): SECP256K1.PublicKey { + return SECP256K1.PublicKey.fromBytes(publicKeyBytes()) + } + + /** + * The ENR public key entry bytes + * @return the ENR public key bytes + */ + private fun publicKeyBytes(): Bytes { + val keyBytes = data["secp256k1"] ?: throw InvalidNodeRecordException("Missing secp256k1 entry") + val ecPoint = SECP256K1.Parameters.CURVE.curve.decodePoint(keyBytes.toArrayUnsafe()) + return Bytes.wrap(ecPoint.getEncoded(false)).slice(1) + } + + /** + * Derives the public key of an ethereum node record into a unique 32 bytes hash. + * @return the hash of the public key + */ + fun nodeId() = nodeId(publicKey()) + + /** + * The ip associated with the ENR + * @return The IP adress of the ENR + */ + fun ip(): InetAddress { + return data["ip"]?.let { InetAddress.getByAddress(it.toArrayUnsafe()) } ?: InetAddress.getLoopbackAddress() + } + + /** + * The TCP port of the ENR + * @return the TCP port associated with this ENR + */ + fun tcp(): Int? { + return data["tcp"]?.toInt() + } + + /** + * The UDP port of the ENR + * @return the UDP port associated with this ENR + */ + fun udp(): Int? { + return data["udp"]?.toInt() ?: tcp() + } + + fun seq(): Long { + return seq + } + + /** + * @return the ENR as a URI + */ + override fun toString(): String { + return "enr:${ip()}:${tcp()}?udp=${udp()}" + } + + fun toRLP(): Bytes = rlp + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EthereumNodeRecord + + return rlp == other.rlp + } + + override fun hashCode(): Int { + return rlp.hashCode() + } +} + +internal class InvalidNodeRecordException(message: String?) : RuntimeException(message) diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Node.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Node.kt new file mode 100644 index 000000000..6d4d0000f --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Node.kt @@ -0,0 +1,31 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.rlp.RLPReader +import org.apache.tuweni.v2.rlp.RLPWriter + +internal data class Node( + val endpoint: Endpoint, + val nodeId: SECP256K1.PublicKey, +) { + + companion object { + fun readFrom(reader: RLPReader): Node { + val endpoint = Endpoint.readFrom(reader) + val nodeId = SECP256K1.PublicKey.fromBytes(reader.readValue()) + return Node(endpoint, nodeId) + } + } + + internal fun writeTo(writer: RLPWriter) { + endpoint.writeTo(writer) + writer.writeValue(nodeId.bytes()) + } + + internal fun rlpSize(): Int = 1 + endpoint.rlpSize() + 3 + 64 +} + +internal fun Peer.toNode(): Node = + Node(endpoint, nodeId) diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Packet.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Packet.kt new file mode 100644 index 000000000..91de3ed45 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Packet.kt @@ -0,0 +1,477 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.rlp.RLPException +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.Hash +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.rlp.RLP +import org.apache.tuweni.v2.rlp.RLPWriter +import java.nio.ByteBuffer + +internal class DecodingException(message: String, cause: Throwable? = null) : Exception(message, cause) + +internal data class SigHash(val signature: SECP256K1.Signature, val hash: Bytes) + +private fun msecToSec(time: Long) = (time + 999) / 1000 +private fun secToMsec(time: Long) = time * 1000 + +internal sealed class Packet( + val nodeId: SECP256K1.PublicKey, + private val signature: SECP256K1.Signature, + val hash: Bytes, + val expiration: Long, +) { + + companion object { + const val MIN_SIZE = 104 + const val MAX_SIZE = 1280 + + private const val HASH_INDEX = 0 + private const val SIGNATURE_INDEX = 32 + private const val PACKET_TYPE_INDEX = 97 + private const val PACKET_DATA_INDEX = 98 + + fun decodeFrom(datagram: ByteBuffer) = + decodeFrom(Bytes.wrapByteBuffer(datagram)) + + fun decodeFrom(datagram: Bytes): Packet { + val typeByte = datagram.get(PACKET_TYPE_INDEX) + val packetType = PacketType.forType(typeByte) ?: throw DecodingException("Unrecognized packet type: $typeByte") + + val signature = SECP256K1.Signature.fromBytes( + datagram.slice(SIGNATURE_INDEX, PACKET_TYPE_INDEX - SIGNATURE_INDEX), + ) + + val publicKey = SECP256K1.PublicKey.recoverFromSignature( + datagram.slice(PACKET_TYPE_INDEX, datagram.size() - PACKET_TYPE_INDEX), + signature, + ) ?: throw DecodingException("Invalid packet signature") + + val hash = Bytes.wrap( + datagram.slice( + HASH_INDEX, + SIGNATURE_INDEX, + ), + ) + if (Hash.keccak256(datagram.slice(SIGNATURE_INDEX)) != hash) { + throw DecodingException("Invalid packet hash") + } + + return packetType.decode(datagram.slice(PACKET_DATA_INDEX), hash, publicKey, signature) + } + + @JvmStatic + protected fun expirationFor(now: Long) = now + PACKET_EXPIRATION_PERIOD_MS + + @JvmStatic + protected fun createSignature( + packetType: PacketType, + keyPair: SECP256K1.KeyPair, + encoder: (RLPWriter) -> Unit, + ): SigHash { + val typeByte = Bytes.of(packetType.typeId) + val dataBytes = RLP.encodeList { writer -> encoder(writer) } + val payloadBytes = Bytes.wrap(typeByte, dataBytes) + val signature = SECP256K1.sign(payloadBytes, keyPair) + val hash = Hash.keccak256(Bytes.wrap(signature.bytes(), payloadBytes)) + return SigHash(signature, hash) + } + } + + fun isExpired(now: Long): Boolean = expiration <= now + + abstract fun encode(): Bytes + + protected fun encodeTo(packetType: PacketType, contentWriter: (RLPWriter) -> Unit): Bytes { + return Bytes.wrap(hash, signature.bytes(), Bytes.of(packetType.typeId), RLP.encodeList(contentWriter)) + } +} + +internal class PingPacket private constructor( + nodeId: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + hash: Bytes, + val from: Endpoint, + val to: Endpoint, + expiration: Long, + val enrSeq: Long?, +) : Packet(nodeId, signature, hash, expiration) { + + companion object { + private const val VERSION = 4 + + fun create(keyPair: SECP256K1.KeyPair, now: Long, from: Endpoint, to: Endpoint, seq: Long?): PingPacket { + val expiration = expirationFor(now) + val sigHash = createSignature( + PacketType.PING, + keyPair, + ) { writer -> + encodeTo(writer, from, to, expiration, seq) + } + return PingPacket( + keyPair.publicKey(), + sigHash.signature, + sigHash.hash, + from, + to, + expiration, + seq, + ) + } + + fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ): PingPacket { + try { + return RLP.decodeList(payload) { reader -> + val version = reader.readInt() + val from = reader.readList { r -> Endpoint.readFrom(r) } + val to = reader.readList { r -> Endpoint.readFrom(r) } + val expiration = reader.readLong() // seconds + val seq: Long? = if (!reader.isComplete) { + reader.readLong() + } else { + null + } + + if (version < VERSION) { + throw DecodingException("Unexpected version $VERSION in ping") + } + PingPacket(publicKey, signature, hash, from, to, secToMsec(expiration), seq) + } + } catch (e: RLPException) { + throw DecodingException("Invalid ping packet", e) + } + } + + private fun encodeTo(writer: RLPWriter, from: Endpoint, to: Endpoint, expiration: Long, seq: Long?) { + writer.writeInt(VERSION) + writer.writeList { w -> from.writeTo(w) } + writer.writeList { w -> to.writeTo(w) } + writer.writeLong(msecToSec(expiration)) // write in seconds + seq?.let { + writer.writeLong(it) + } + } + } + + override fun encode() = encodeTo(PacketType.PING) { writer -> + encodeTo(writer, from, to, expiration, enrSeq) + } +} + +internal class PongPacket private constructor( + nodeId: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + hash: Bytes, + val to: Endpoint, + val pingHash: Bytes, + expiration: Long, + val enrSeq: Long?, +) : Packet(nodeId, signature, hash, expiration) { + + companion object { + fun create(keyPair: SECP256K1.KeyPair, now: Long, to: Endpoint, pingHash: Bytes, enrSeq: Long?): PongPacket { + val expiration = expirationFor(now) + val sigHash = createSignature( + PacketType.PONG, + keyPair, + ) { writer -> + encodeTo(writer, to, pingHash, expiration, enrSeq) + } + return PongPacket( + keyPair.publicKey(), + sigHash.signature, + sigHash.hash, + to, + pingHash, + expiration, + enrSeq, + ) + } + + fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ): PongPacket { + try { + return RLP.decodeList(payload) { reader -> + val to = reader.readList { r -> Endpoint.readFrom(r) } + val pingHash = Bytes.wrap(reader.readValue()) + val expiration = reader.readLong() // seconds + val seq: Long? = if (!reader.isComplete) { + reader.readLong() + } else { + null + } + PongPacket(publicKey, signature, hash, to, pingHash, secToMsec(expiration), seq) + } + } catch (e: RLPException) { + throw DecodingException("Invalid pong packet", e) + } + } + + private fun encodeTo(writer: RLPWriter, to: Endpoint, pingHash: Bytes, expiration: Long, enrSeq: Long?) { + writer.writeList { w -> to.writeTo(w) } + writer.writeValue(pingHash) + writer.writeLong(msecToSec(expiration)) + enrSeq?.let { writer.writeLong(it) } + } + } + + override fun encode() = encodeTo(PacketType.PONG) { writer -> + encodeTo(writer, to, pingHash, expiration, enrSeq) + } +} + +internal class FindNodePacket private constructor( + nodeId: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + hash: Bytes, + val target: SECP256K1.PublicKey, + expiration: Long, +) : Packet(nodeId, signature, hash, expiration) { + + companion object { + fun create(keyPair: SECP256K1.KeyPair, now: Long, target: SECP256K1.PublicKey): FindNodePacket { + val expiration = expirationFor(now) + val sigHash = createSignature( + PacketType.FIND_NODE, + keyPair, + ) { writer -> + encodeTo(writer, target, expiration) + } + return FindNodePacket( + keyPair.publicKey(), + sigHash.signature, + sigHash.hash, + target, + expiration, + ) + } + + fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ): FindNodePacket { + try { + return RLP.decodeList(payload) { reader -> + val target = SECP256K1.PublicKey.fromBytes(reader.readValue()) + val expiration = reader.readLong() + FindNodePacket(publicKey, signature, hash, target, secToMsec(expiration)) + } + } catch (e: RLPException) { + throw DecodingException("Invalid find nodes packet", e) + } + } + + private fun encodeTo(writer: RLPWriter, target: SECP256K1.PublicKey, expiration: Long) { + writer.writeValue(target.bytes()) + writer.writeLong(msecToSec(expiration)) + } + } + + override fun encode() = encodeTo(PacketType.FIND_NODE) { writer -> + encodeTo(writer, target, expiration) + } +} + +internal class NeighborsPacket private constructor( + nodeId: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + hash: Bytes, + val nodes: List, + expiration: Long, +) : Packet(nodeId, signature, hash, expiration) { + + companion object { + // an over-estimate of the minimum size, based on a full 64-bit expiration time and a full list length prefix + internal const val RLP_MIN_SIZE = 109 + + fun create(keyPair: SECP256K1.KeyPair, now: Long, nodes: List): NeighborsPacket { + val expiration = expirationFor(now) + val sigHash = createSignature( + PacketType.NEIGHBORS, + keyPair, + ) { writer -> + encodeTo(writer, nodes, expiration) + } + return NeighborsPacket( + keyPair.publicKey(), + sigHash.signature, + sigHash.hash, + nodes, + expiration, + ) + } + + fun createRequired(keyPair: SECP256K1.KeyPair, now: Long, nodes: List): List { + val result = mutableListOf() + var nodeSubset = mutableListOf() + var size = RLP_MIN_SIZE + for (node in nodes) { + val nodeSize = node.rlpSize() + size += nodeSize + if (size > MAX_SIZE) { + result.add(create(keyPair, now, nodeSubset)) + nodeSubset = mutableListOf() + size = RLP_MIN_SIZE + nodeSize + } + nodeSubset.add(node) + } + result.add(create(keyPair, now, nodeSubset)) + return result + } + + fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ): NeighborsPacket { + try { + return RLP.decodeList(payload) { reader -> + val nodes = mutableListOf() + reader.readList { r -> + while (!r.isComplete) { + val node = r.readList { nr -> Node.readFrom(nr) } + nodes.add(node) + } + } + val expiration = reader.readLong() + NeighborsPacket(publicKey, signature, hash, nodes, secToMsec(expiration)) + } + } catch (e: RLPException) { + throw DecodingException("Invalid nodes packet", e) + } + } + + private fun encodeTo(writer: RLPWriter, nodes: List, expiration: Long) { + writer.writeList { w -> nodes.forEach { node -> w.writeList { nw -> node.writeTo(nw) } } } + writer.writeLong(msecToSec(expiration)) + } + } + + override fun encode() = encodeTo(PacketType.NEIGHBORS) { writer -> + encodeTo(writer, nodes, expiration) + } +} + +internal class ENRRequestPacket private constructor( + nodeId: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + hash: Bytes, + expiration: Long, +) : Packet(nodeId, signature, hash, expiration) { + + companion object { + fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ): ENRRequestPacket { + try { + return RLP.decodeList(payload) { reader -> + val expiration = reader.readLong() + ENRRequestPacket(publicKey, signature, hash, secToMsec(expiration)) + } + } catch (e: RLPException) { + throw DecodingException("Invalid enr request packet", e) + } + } + + private fun encodeTo(writer: RLPWriter, expiration: Long) { + writer.writeLong(msecToSec(expiration)) + } + + fun create(keyPair: SECP256K1.KeyPair, now: Long): Packet { + val expiration = expirationFor(now) + val sigHash = createSignature( + PacketType.ENRREQUEST, + keyPair, + ) { writer -> + encodeTo(writer, expiration) + } + return ENRRequestPacket( + keyPair.publicKey(), + sigHash.signature, + sigHash.hash, + expiration, + ) + } + } + + override fun encode() = encodeTo(PacketType.ENRREQUEST) { writer -> + encodeTo(writer, expiration) + } +} + +internal class ENRResponsePacket private constructor( + nodeId: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + hash: Bytes, + expiration: Long, + val requestHash: Bytes, + val enr: Bytes, +) : Packet(nodeId, signature, hash, expiration) { + + companion object { + + fun create(keyPair: SECP256K1.KeyPair, now: Long, requestHash: Bytes, enr: Bytes): ENRResponsePacket { + val expiration = expirationFor(now) + val sigHash = createSignature( + PacketType.ENRRESPONSE, + keyPair, + ) { writer -> + encodeTo(writer, requestHash, enr, expiration) + } + return ENRResponsePacket( + keyPair.publicKey(), + sigHash.signature, + sigHash.hash, + expiration, + requestHash, + enr, + ) + } + + fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ): ENRResponsePacket { + try { + return RLP.decodeList(payload) { reader -> + // request-hash, ENR + val requestHash = reader.readValue() + val enr = reader.readValue() + val expiration = reader.readLong() + ENRResponsePacket(publicKey, signature, hash, secToMsec(expiration), requestHash, enr) + } + } catch (e: RLPException) { + throw DecodingException("Invalid enr response packet", e) + } + } + + private fun encodeTo(writer: RLPWriter, requestHash: Bytes, enr: Bytes, expiration: Long) { + writer.writeValue(requestHash) + writer.writeValue(enr) + writer.writeLong(msecToSec(expiration)) + } + } + + override fun encode() = encodeTo(PacketType.ENRRESPONSE) { writer -> + encodeTo(writer, requestHash, enr, expiration) + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PacketType.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PacketType.kt new file mode 100644 index 000000000..576fa423e --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PacketType.kt @@ -0,0 +1,115 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 + +/** + * DevP2P discovery packet types + * @param typeId the byte representing the type + */ +internal enum class PacketType( + val typeId: Byte, +) { + /** + * Ping packet + */ + PING(0x01) { + override fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ) = PingPacket.decode(payload, hash, publicKey, signature) as Packet + }, + + /** + * Pong packet as response to pings + */ + PONG(0x02) { + override fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ) = PongPacket.decode(payload, hash, publicKey, signature) as Packet + }, + + /** + * FindNode packet + */ + FIND_NODE(0x03) { + override fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ) = FindNodePacket.decode(payload, hash, publicKey, signature) as Packet + }, + + /** + * Neighbors packet response + */ + NEIGHBORS(0x04) { + override fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ) = NeighborsPacket.decode(payload, hash, publicKey, signature) as Packet + }, + + /** + * ENR request packet + */ + ENRREQUEST(0x05) { + override fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ) = ENRRequestPacket.decode(payload, hash, publicKey, signature) as Packet + }, + + /** + * ENR response packet + */ + ENRRESPONSE(0x06) { + override fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ) = ENRResponsePacket.decode(payload, hash, publicKey, signature) as Packet + }, ; + + companion object { + private const val MAX_VALUE: Byte = 0x7f + private val INDEX = arrayOfNulls(MAX_VALUE.toInt()) + + init { + // populate an array by packet type id for index-based lookup in `forType(Byte)` + entries.forEach { type -> INDEX[type.typeId.toInt()] = type } + } + + fun forType(typeId: Byte): PacketType? { + return INDEX[typeId.toInt()] + } + } + + init { + checkTypeId() + } + + private fun checkTypeId() { + require(typeId <= PacketType.MAX_VALUE) { "Packet typeId must be in range [0x00, 0x80]" } + } + + abstract fun decode( + payload: Bytes, + hash: Bytes, + publicKey: SECP256K1.PublicKey, + signature: SECP256K1.Signature, + ): Packet +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Peer.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Peer.kt new file mode 100644 index 000000000..cf673fbae --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Peer.kt @@ -0,0 +1,98 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.v2.crypto.SECP256K1 + +/** + * An Ethereum P2P network peer. + */ +interface Peer { + + /** + * The nodeId for this peer. + */ + val nodeId: SECP256K1.PublicKey + + /** + * The endpoint for communicating with this peer, if known. + */ + val endpoint: Endpoint + + /** + * The Ethereum Node Record associated with the peer, if known. + */ + val enr: EthereumNodeRecord? + + /** + * The last time the current endpoint of this peer was verified, in milliseconds since the epoch. + * + * Endpoint is verified by a ping/pong cycle: https://github.com/ethereum/devp2p/blob/master/discv4.md#endpoint-proof + */ + val lastVerified: Long? + + /** + * The time this peer was last seen at its current endpoint, in milliseconds since the epoch. + */ + val lastSeen: Long? + + /** + * Get the endpoint for this peer, if it has been verified on or after a specified time. + * + * @param ifVerifiedOnOrAfter the earliest time, in milliseconds since the epoch, when the + * endpoint of this peer must have been verified + * @return the endpoint of this peer, if it has been verified on or after the specified time + */ + fun getEndpoint(ifVerifiedOnOrAfter: Long): Endpoint? + + /** + * Update the peer with a new endpoint. + * + * If successful, the [endpoint] property will be set, the [lastSeen] timestamp will be updated. If the IP address + * or UDP port of the endpoint was changed, then the [lastVerified] timestamp will be cleared. + * + * @param endpoint the endpoint for the peer + * @param time the time this endpoint information was determined, in milliseconds since the epoch + * @param ifVerifiedBefore the latest allowable time, in milliseconds since the epoch, when this peer was last + * verified at its current endpoint. If this peers endpoint was verified after this time, the endpoint + * will not be updated. If `null`, then no check will be made and the endpoint will always be updated. + * @return the resulting endpoint of the peer + */ + fun updateEndpoint(endpoint: Endpoint, time: Long, ifVerifiedBefore: Long? = null): Endpoint + + /** + * Set the [lastVerified] and [lastSeen] time to the provided time, if the endpoint matches. + * + * Will only update [lastVerified] and/or [lastSeen] if the new time is more recent than their current values. + * + * @param endpoint the endpoint that was verified, which must match this peer's endpoint + * @param time the time when the endpoint was verified, in milliseconds since the epoch + * @return `true` if the endpoint matched the endpoint of this peer + */ + fun verifyEndpoint(endpoint: Endpoint, time: Long): Boolean + + /** + * Set the [lastSeen] time to the current time. + * + * Will only update [lastSeen] if the new time is more recent than their current values. + * + * @param time the time when the endpoint was verified, in milliseconds since the epoch + * @throws IllegalStateException if there is no endpoint for this peer + */ + fun seenAt(time: Long) + + /** + * Update the peer's ENR. + * + * Will only update if the `seq` is larger than the one associated with the peer. + * + * @param record the ENR record associated with the peer + * @param time the time this endpoint information was determined, in milliseconds since the epoch + */ + fun updateENR(record: EthereumNodeRecord, time: Long) + + fun uri(): String { + return "enode://${nodeId.toHexString()}@${endpoint.address}:${endpoint.tcpPort + ?: 30303}?discPort=${endpoint.udpPort}" + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRepository.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRepository.kt new file mode 100644 index 000000000..a263b6835 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRepository.kt @@ -0,0 +1,210 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.apache.tuweni.concurrent.AsyncResult +import org.apache.tuweni.concurrent.coroutines.asyncResult +import org.apache.tuweni.v2.crypto.SECP256K1 +import java.net.URI +import java.util.concurrent.ConcurrentHashMap +import kotlin.coroutines.CoroutineContext + +/** + * A repository of peers in an Ethereum network. + * + * Conceptually, this repository stores information about all peers in an Ethereum network, hence the + * retrieval methods always return a valid [Peer]. However, the [Peer] objects are only generated on demand and + * may be purged from underlying storage if they can be recreated easily. + */ +interface PeerRepository { + + /** + * Adds a listener to the repository, which will consume peer entries whenever they are added to the repository. + */ + fun addListener(listener: (Peer) -> Unit) + + /** + * Get a Peer based on a URI components. + * + * The returned peer will use the endpoint from the URI, unless the peer is already active, in + * which case its endpoint will be unchanged. + * + * @param host the peer host + * @param port the peer port + * @param nodeId the public key associated with the peer + */ + suspend fun get(host: String, port: Int, nodeId: SECP256K1.PublicKey): Peer + + /** + * Get a Peer based on a URI. + * + * The returned peer will use the endpoint from the URI, unless the peer is already active, in + * which case its endpoint will be unchanged. + * + * @param uri the enode URI + * @return the peer + * @throws IllegalArgumentException if the URI is not a valid enode URI + */ + suspend fun get(uri: URI): Peer + + /** + * Get a Peer based on a URI. + * + * The returned peer will use the endpoint from the URI, unless the peer is already active, in + * which case its endpoint will be unchanged. + * + * @param uri the enode URI + * @return the peer + * @throws IllegalArgumentException if the URI is not a valid enode URI + */ + fun getAsync(uri: URI): AsyncResult + + /** + * Get a Peer based on a URI string. + * + * The returned peer will use the endpoint from the URI, unless the peer is already active, in + * which case its endpoint will be unchanged. + * + * @param uri the enode URI + * @return the peer + * @throws IllegalArgumentException if the URI is not a valid enode URI + */ + suspend fun get(uri: String) = get(URI.create(uri)) + + /** + * Get a Peer based on a URI string. + * + * The returned peer will use the endpoint from the URI, unless the peer is already active, in + * which case its endpoint will be unchanged. + * + * @param uri the enode URI + * @return the peer + * @throws IllegalArgumentException if the URI is not a valid enode URI + */ + fun getAsync(uri: String): AsyncResult +} + +/** + * An in-memory peer repository. + * + * Note: as the storage is in-memory, no retrieval methods in this implementation will suspend. + */ +class EphemeralPeerRepository( + private val peers: MutableMap = ConcurrentHashMap(), + override val coroutineContext: CoroutineContext = Dispatchers.Default, +) : + PeerRepository, CoroutineScope { + + private val listeners = mutableListOf<(Peer) -> Unit>() + + override fun addListener(listener: (Peer) -> Unit) { + listeners.add(listener) + } + + /** + * Get a peer from node ID and endpoint information + * @param nodeId the peer public key + * @param endpoint the peer endpoint + * @return the peer + */ + fun get(nodeId: SECP256K1.PublicKey, endpoint: Endpoint) = + peers.compute(nodeId) { _, peer -> + if (peer == null) { + val newPeer = EphemeralPeer(nodeId, endpoint) + listeners.let { + for (listener in listeners) { + listener(newPeer) + } + } + newPeer + } else { + peer + } + } as Peer + + override suspend fun get(host: String, port: Int, nodeId: SECP256K1.PublicKey): Peer { + return get(nodeId, Endpoint(host, port)) + } + + override suspend fun get(uri: URI): Peer { + val (nodeId, endpoint) = parseEnodeUri(uri) + return get(nodeId, endpoint) + } + + override fun getAsync(uri: URI): AsyncResult = asyncResult { get(uri) } + + override fun getAsync(uri: String): AsyncResult = asyncResult { get(uri) } + + private inner class EphemeralPeer( + override val nodeId: SECP256K1.PublicKey, + knownEndpoint: Endpoint, + ) : Peer { + @Volatile + override var endpoint: Endpoint = knownEndpoint + + override var enr: EthereumNodeRecord? = null + + @Synchronized + override fun getEndpoint(ifVerifiedOnOrAfter: Long): Endpoint? { + if ((lastVerified ?: 0) >= ifVerifiedOnOrAfter) { + return this.endpoint + } + return null + } + + @Volatile + override var lastVerified: Long? = null + + @Volatile + override var lastSeen: Long? = null + + @Synchronized + override fun updateEndpoint(endpoint: Endpoint, time: Long, ifVerifiedBefore: Long?): Endpoint { + val currentEndpoint = this.endpoint + if (currentEndpoint == endpoint) { + this.seenAt(time) + return currentEndpoint + } + + if (ifVerifiedBefore == null || (lastVerified ?: 0) < ifVerifiedBefore) { + if (currentEndpoint.address != endpoint.address || currentEndpoint.udpPort != endpoint.udpPort) { + lastVerified = null + } + this.endpoint = endpoint + this.seenAt(time) + return endpoint + } + + return currentEndpoint + } + + @Synchronized + override fun verifyEndpoint(endpoint: Endpoint, time: Long): Boolean { + if (endpoint != this.endpoint) { + return false + } + seenAt(time) + if ((lastVerified ?: 0) < time) { + lastVerified = time + } + return true + } + + @Synchronized + override fun seenAt(time: Long) { + if ((lastSeen ?: 0) < time) { + lastSeen = time + } + } + + @Synchronized + override fun updateENR(record: EthereumNodeRecord, time: Long) { + if (enr == null || enr!!.seq() < record.seq()) { + enr = record + updateEndpoint(Endpoint(record.ip().hostAddress, record.udp()!!, record.tcp()), time) + } + } + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRoutingTable.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRoutingTable.kt new file mode 100644 index 000000000..2095dcb89 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRoutingTable.kt @@ -0,0 +1,96 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import com.google.common.cache.Cache +import com.google.common.cache.CacheBuilder +import com.google.common.util.concurrent.UncheckedExecutionException +import org.apache.tuweni.kademlia.KademliaRoutingTable +import org.apache.tuweni.v2.crypto.SECP256K1 +import java.lang.IllegalArgumentException + +/** + * A routing table for ÐΞVp2p peers. + */ +interface PeerRoutingTable : Set { + + /** + * Return the nearest nodes to a target id, in order from closest to furthest. + * + * The sort order is the log distance from the target to the node-id's of the Peers. + * + * @param targetId the id to find nodes nearest to + * @param limit the maximum number of nodes to return + * @return a list of nodes from the routing table + */ + fun nearest(targetId: SECP256K1.PublicKey, limit: Int): List + + /** + * Add a node to the table. + * + * @param node the node to add + * @return `null` if the node was successfully added to the table (or already in the table). Otherwise, a node + * will be returned that is a suitable candidate for eviction, and the provided node will be stored in + * the replacements list. + */ + fun add(node: Peer): Peer? + + /** + * Remove a node from the table, potentially adding an alternative from the replacement cache. + * + * @param node the node to evict + * @param `true` if the node was removed + */ + fun evict(node: Peer): Boolean +} + +internal const val DEVP2P_BUCKET_SIZE = 16 + +/** + * A Peer routing table for the Ethereum ÐΞVp2p network. + * + * This is an implementation of a [KademliaRoutingTable] using keccak256 hashed node ids and a k-bucket size of 6. + * + * @constructor Create a new ÐΞVp2p routing table. + * @param selfId the ID of the local node + */ +internal class DevP2PPeerRoutingTable(selfId: SECP256K1.PublicKey) : PeerRoutingTable { + + private val idHashCache: Cache = + CacheBuilder.newBuilder().maximumSize((1L + 256) * 16).weakKeys().build() + + private val table = KademliaRoutingTable( + selfId = hashForId(selfId)!!, + k = DEVP2P_BUCKET_SIZE, + nodeId = { p -> hashForId(p.nodeId)!! }, + ) + + override val size: Int + get() = table.size + + override fun contains(element: Peer): Boolean = table.contains(element) + + override fun containsAll(elements: Collection): Boolean = table.containsAll(elements) + + override fun isEmpty(): Boolean = table.isEmpty() + + override fun iterator(): Iterator = table.iterator() + + override fun nearest(targetId: SECP256K1.PublicKey, limit: Int): List = + hashForId(targetId)?.let { table.nearest(it, limit) } ?: listOf() + + override fun add(node: Peer): Peer? = table.add(node) + + override fun evict(node: Peer): Boolean = table.evict(node) + + private fun hashForId(id: SECP256K1.PublicKey): ByteArray? { + try { + return idHashCache.get(id) { EthereumNodeRecord.nodeId(id).toArrayUnsafe() } + } catch (e: UncheckedExecutionException) { + if (e.cause is IllegalArgumentException) { + return null + } + throw e + } + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/DiscoveryV5Service.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/DiscoveryV5Service.kt new file mode 100644 index 000000000..376162c61 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/DiscoveryV5Service.kt @@ -0,0 +1,316 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import io.vertx.core.Vertx +import io.vertx.core.buffer.Buffer +import io.vertx.core.datagram.DatagramPacket +import io.vertx.core.net.SocketAddress +import io.vertx.kotlin.coroutines.coAwait +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.launch +import org.apache.tuweni.concurrent.AsyncCompletion +import org.apache.tuweni.concurrent.AsyncResult +import org.apache.tuweni.concurrent.ExpiringMap +import org.apache.tuweni.concurrent.coroutines.asyncCompletion +import org.apache.tuweni.concurrent.coroutines.asyncResult +import org.apache.tuweni.concurrent.coroutines.await +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.Hash +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import org.apache.tuweni.v2.devp2p.Packet +import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKey +import org.apache.tuweni.v2.devp2p.v5.topic.TopicTable +import org.apache.tuweni.v2.io.Base64URLSafe +import org.slf4j.LoggerFactory +import java.net.InetAddress +import java.net.InetSocketAddress +import java.nio.ByteBuffer +import java.time.Instant +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.coroutines.CoroutineContext + +/** + * A creator of discovery service objects. + */ +object DiscoveryService { + + /** + * Creates a new discovery service, generating the node ENR and configuring the UDP connector. + * @param vertx Vert.x instance + * @param keyPair the key pair identifying the node running the service. + * @param bindAddress the address to bind the node to. + * @param enrSeq the sequence of the ENR of the node + * @param bootstrapENRList the list of other nodes to connect to on bootstrap. + * @param enrStorage the permanent storage of ENRs. Defaults to an in-memory store. + * @param coroutineContext the coroutine context associated with the store. + */ + @JvmStatic + @JvmOverloads + fun open( + vertx: Vertx, + keyPair: SECP256K1.KeyPair, + localPort: Int, + bindAddress: InetSocketAddress = InetSocketAddress(InetAddress.getLoopbackAddress(), localPort), + enrSeq: Long = Instant.now().toEpochMilli(), + bootstrapENRList: List = emptyList(), + enrStorage: ENRStorage = DefaultENRStorage(), + coroutineContext: CoroutineContext = Dispatchers.Default, + ): DiscoveryV5Service { + val selfENR = EthereumNodeRecord.create( + keyPair, + enrSeq, + emptyMap(), + emptyMap(), + bindAddress.address, + bindAddress.port, // TODO allow override + bindAddress.port, + ) + // val connector = UdpConnector(bindAddress, keyPair, selfENR, enrStorage) + return DefaultDiscoveryV5Service( + vertx, + bindAddress, + bootstrapENRList, + enrStorage, + keyPair, + selfENR, + coroutineContext = coroutineContext, + ) + } +} + +/** + * Service executes network discovery, according to discv5 specification + * (https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) + */ +interface DiscoveryV5Service : CoroutineScope { + + /** + * Starts the node discovery service. + */ + suspend fun start(): AsyncCompletion + + /** + * Stops the node discovery service. + */ + suspend fun terminate() + + /** + * Starts the discovery service, providing a handle to the completion of the start operation. + */ + fun startAsync() = asyncCompletion { start() } + + /** + * Stops the node discovery service, providing a handle to the completion of the shutdown operation. + */ + fun terminateAsync() = asyncCompletion { terminate() } + + /** + * Provides the ENR identifying the service. + */ + fun enr(): EthereumNodeRecord + + /** + * Adds a peer to the routing table. + * + * @param rlpENR the RLP representation of the peer ENR. + */ + suspend fun addPeer(rlpENR: Bytes): AsyncCompletion { + val enr: EthereumNodeRecord = EthereumNodeRecord.fromRLP(rlpENR) + return addPeer(enr) + } + + /** + * Adds a peer to the routing table. + * + * @param enr the peer Ethereum Node Record + * @param address optionally, the UDP address to call for this peer. + */ + suspend fun addPeer( + enr: EthereumNodeRecord, + address: SocketAddress = SocketAddress.inetSocketAddress(enr.udp()!!, enr.ip().hostAddress), + ): AsyncCompletion + + /** + * Requests nodes from all connected peers. + * + * @param distance the distance between the node and the peer. Helps pick a Kademlia bucket. + * @param maxSecondsToWait number of seconds to wait for a response. + */ + suspend fun requestNodes( + distance: Int = 1, + maxSecondsToWait: Long = 10, + ): AsyncResult>> +} + +internal class DefaultDiscoveryV5Service( + vertx: Vertx, + private val bindAddress: InetSocketAddress, + private val bootstrapENRList: List, + private val enrStorage: ENRStorage, + private val keyPair: SECP256K1.KeyPair, + private val selfEnr: EthereumNodeRecord, + private val routingTable: RoutingTable = RoutingTable(selfEnr), + private val topicTable: TopicTable = TopicTable(), + override val coroutineContext: CoroutineContext = Dispatchers.Default, +) : DiscoveryV5Service { + + companion object { + private val logger = LoggerFactory.getLogger(DefaultDiscoveryV5Service::class.java) + } + + private val server = vertx.createDatagramSocket() + private val handshakes = ExpiringMap() + private val sessions = ConcurrentHashMap() + private val started = AtomicBoolean(false) + private val nodeId = EthereumNodeRecord.nodeId(keyPair.publicKey()) + private val whoAreYouHeader = Hash.sha2_256(Bytes.wrap(nodeId, Bytes.wrap("WHOAREYOU".toByteArray()))) + + private lateinit var receiveJob: Job + + override suspend fun start(): AsyncCompletion { + server.handler(this::receiveDatagram).listen(bindAddress.port, bindAddress.hostString).coAwait() + return bootstrap() + } + + override suspend fun terminate() { + if (started.compareAndSet(true, false)) { + receiveJob.cancel() + server.close().coAwait() + } + } + + override fun enr(): EthereumNodeRecord = selfEnr + + override suspend fun addPeer(enr: EthereumNodeRecord, address: SocketAddress): AsyncCompletion { + val session = sessions[address] + if (session == null) { + logger.trace("Creating new session for peer {}", enr) + val handshakeSession = handshakes.computeIfAbsent(address) { addr -> createHandshake(addr, enr.publicKey(), enr) } + return asyncCompletion { + logger.trace("Handshake connection start {}", enr) + handshakeSession.connect().await() + logger.trace("Handshake connection done {}", enr) + } + } else { + logger.trace("Session found for peer {}", enr) + return AsyncCompletion.completed() + } + } + + private fun send(addr: SocketAddress, message: Bytes) { + launch { + server.send(Buffer.buffer(message.toArrayUnsafe()), addr.port(), addr.host()).coAwait() + } + } + + private suspend fun bootstrap(): AsyncCompletion = AsyncCompletion.allOf( + bootstrapENRList.map { + logger.trace("Connecting to bootstrap peer {}", it) + var encodedEnr = it + if (it.startsWith("enr:")) { + encodedEnr = it.substringAfter("enr:") + } + val rlpENR = Base64URLSafe.decode(encodedEnr) + addPeer(rlpENR) + }, + ) + + private fun receiveDatagram(packet: DatagramPacket) { + var session = sessions[packet.sender()] + val size = Packet.MAX_SIZE.coerceAtMost(packet.data().length()) + val buffer = ByteBuffer.allocate(size) + buffer.put(packet.data().bytes) + buffer.flip() + val message = Bytes.wrapByteBuffer(buffer) + if (message.slice(0, 32) == whoAreYouHeader && session != null) { + sessions.remove(packet.sender()) + session = null + } + if (session == null) { + val handshakeSession = + handshakes.computeIfAbsent(packet.sender()) { createHandshake(it) } + launch { + handshakeSession.processMessage(message) + } + } else { + launch { + session.processMessage(message) + } + } + } + + private fun createHandshake( + address: SocketAddress, + publicKey: SECP256K1.PublicKey? = null, + receivedEnr: EthereumNodeRecord? = null, + ): HandshakeSession { + logger.trace("Creating new handshake with {}", address) + val newSession = HandshakeSession(keyPair, address, publicKey, this::send, this::enr, coroutineContext) + newSession.awaitConnection().thenAccept { + val peerEnr = receivedEnr ?: newSession.receivedEnr!! + logger.trace("Handshake connection done {}", peerEnr) + val session = createSession(newSession, address, it, peerEnr) + newSession.requestId?.let { requestId -> + session.activeFindNodes[requestId] = AsyncResult.incomplete() + } + }.exceptionally { logger.error("Error during connection", it) } + return newSession + } + + private fun createSession( + newSession: HandshakeSession, + address: SocketAddress, + sessionKey: SessionKey, + receivedEnr: EthereumNodeRecord, + ): Session { + val session = Session( + receivedEnr, + keyPair, + newSession.nodeId, + newSession.tag(), + sessionKey, + address, + this::send, + this::enr, + routingTable, + topicTable, + { missedPings -> + missedPings > 5 + }, + coroutineContext, + ) + logger.trace("Adding ENR discovered by connecting to peer") + enrStorage.set(receivedEnr) + sessions[address] = session + return session + } + + override suspend fun requestNodes( + distance: Int, + maxSecondsToWait: Long, + ): AsyncResult>> = + asyncResult { + val results = ConcurrentHashMap>() + logger.debug("Requesting from ${sessions.size} sessions with distance $distance") + sessions.values.map { session -> + async { + try { + val oneResult = session.sendFindNodes(distance).get(maxSecondsToWait, TimeUnit.SECONDS) + logger.debug("Received ${oneResult!!.size} results") + results.put(session.enr, oneResult) + } catch (e: Exception) { + logger.debug("Timeout waiting for nodes") + } + } + }.awaitAll() + results + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/ENRStorage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/ENRStorage.kt new file mode 100644 index 000000000..c5be90dc9 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/ENRStorage.kt @@ -0,0 +1,50 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.Hash +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import java.util.concurrent.ConcurrentHashMap + +/** + * Storage of node records + */ +interface ENRStorage { + + /** + * Add an ENR record to the store + * + * @param enr node record + */ + fun set(enr: EthereumNodeRecord) { + val nodeId = Hash.sha2_256(enr.toRLP()) + put(nodeId, enr) + } + + /** + * Store an ENR record associated with a nodeId in the store. + */ + fun put(nodeId: Bytes, enr: EthereumNodeRecord) + + /** + * Find a stored node record + * + * @param nodeId node identifier + * + * @return node record, if present. + */ + fun find(nodeId: Bytes): EthereumNodeRecord? +} + +/** + * Default storage for Ethereum Node Records, backed by an in-memory hash map. + */ +internal class DefaultENRStorage : ENRStorage { + + private val storage: MutableMap = ConcurrentHashMap() + + override fun find(nodeId: Bytes): EthereumNodeRecord? = storage[nodeId] + + override fun put(nodeId: Bytes, enr: EthereumNodeRecord) { storage.put(nodeId, enr) } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSession.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSession.kt new file mode 100644 index 000000000..b12e601d3 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSession.kt @@ -0,0 +1,201 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import io.vertx.core.net.SocketAddress +import kotlinx.coroutines.CoroutineScope +import org.apache.tuweni.concurrent.AsyncResult +import org.apache.tuweni.concurrent.CompletableAsyncResult +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.Hash +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import org.apache.tuweni.v2.devp2p.v5.encrypt.AES128GCM +import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKey +import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKeyGenerator +import org.apache.tuweni.v2.rlp.RLP +import org.apache.tuweni.v2.rlp.RLPReader +import org.apache.tuweni.v2.units.bigints.UInt256 +import org.slf4j.LoggerFactory +import kotlin.coroutines.CoroutineContext + +private val DISCOVERY_ID_NONCE: Bytes = Bytes.wrap("discovery-id-nonce".toByteArray()) + +internal class HandshakeSession( + private val keyPair: SECP256K1.KeyPair, + private val address: SocketAddress, + private var publicKey: SECP256K1.PublicKey? = null, + private val sendFn: (SocketAddress, Bytes) -> Unit, + private val enr: () -> EthereumNodeRecord, + override val coroutineContext: CoroutineContext, +) : CoroutineScope { + + var requestId: Bytes? = null + private val connected: CompletableAsyncResult = AsyncResult.incomplete() + var receivedEnr: EthereumNodeRecord? = null + val nodeId = EthereumNodeRecord.nodeId(keyPair.publicKey()) + private val whoAreYouHeader = Hash.sha2_256(Bytes.wrap(nodeId, Bytes.wrap("WHOAREYOU".toByteArray()))) + + private val tokens = ArrayList() + + companion object { + private val logger = LoggerFactory.getLogger(HandshakeSession::class.java) + } + + suspend fun connect(): AsyncResult { + val message = RandomMessage() + tokens.add(message.authTag) + val tag = tag() + val rlpAuthTag = RLP.encodeValue(message.authTag) + val content = Bytes.wrap(tag, rlpAuthTag, message.toRLP()) + logger.trace("Sending random packet {} {}", address, content) + sendFn(address, content) + return connected + } + + suspend fun processMessage(messageBytes: Bytes) { + if (messageBytes.size() > Message.MAX_UDP_MESSAGE_SIZE) { + logger.trace("Message too long, dropping from {}", address) + return + } + if (messageBytes.size() < 32) { + logger.trace("Message too short, dropping from {}", address) + } + + logger.trace("Received message from {}", address) + val tag = messageBytes.slice(0, 32) + val content = messageBytes.slice(32) + // it's either a WHOAREYOU or a RANDOM message. + if (whoAreYouHeader == tag) { + logger.trace("Identified a WHOAREYOU message") + val message = WhoAreYouMessage.create(tag, content) + if (!this.tokens.contains(message.token)) { + // We were not expecting this WHOAREYOU. + logger.trace("Unexpected WHOAREYOU packet {}", message.token) + return + } + // Use the WHOAREYOU info to send handshake. + // Generate ephemeral key pair + val ephemeralKeyPair = SECP256K1.KeyPair.random() + val ephemeralKey = ephemeralKeyPair.secretKey() + + val destNodeId = EthereumNodeRecord.nodeId(publicKey!!) + val secret = SECP256K1.deriveECDHKeyAgreement(ephemeralKey.bytes(), publicKey!!.bytes()) + + // Derive keys + val newSession = SessionKeyGenerator.generate(nodeId, destNodeId, secret, message.idNonce) + val signValue = Bytes.wrap(DISCOVERY_ID_NONCE, message.idNonce, ephemeralKeyPair.publicKey().bytes()) + val signature = SECP256K1.signHashed(Hash.sha2_256(signValue), keyPair) + val plain = RLP.encodeList { writer -> + writer.writeInt(5) + writer.writeValue( + Bytes.wrap( + UInt256.valueOf(signature.r()), + UInt256.valueOf(signature.s()), + ), + ) + writer.writeRLP(enr().toRLP()) + } + val zeroNonce = Bytes.wrap(ByteArray(12)) + val authResponse = AES128GCM.encrypt(newSession.authRespKey, zeroNonce, plain, Bytes.EMPTY) + val authTag = Message.authTag() + val newTag = tag() + val findNode = FindNodeMessage() + requestId = findNode.requestId + val encryptedMessage = AES128GCM.encrypt( + newSession.initiatorKey, + authTag, + Bytes.wrap(Bytes.of(MessageType.FINDNODE.byte()), findNode.toRLP()), + newTag, + ) + val response = Bytes.wrap( + newTag, + RLP.encodeList { + it.writeValue(authTag) + it.writeValue(message.idNonce) + it.writeValue(Bytes.wrap("gcm".toByteArray())) + it.writeValue(ephemeralKeyPair.publicKey().bytes()) + it.writeValue(authResponse) + }, + encryptedMessage, + ) + logger.trace("Sending handshake FindNode {}", response) + connected.complete(newSession) + sendFn(address, response) + } else { + // connection initiated by the peer. + // try to see if this a message with a header we can read: + val hasHeader = RLP.decode(content, RLPReader::nextIsList) + if (hasHeader) { + logger.trace("Identified a valid message") + RLP.decodeList(content) { + it.skipNext() + val idNonce = it.readValue() + it.skipNext() + val ephemeralPublicKey = SECP256K1.PublicKey.fromBytes(it.readValue()) + val authResponse = it.readValue() + + val secret = SECP256K1.deriveECDHKeyAgreement(keyPair.secretKey().bytes(), ephemeralPublicKey.bytes()) + val senderNodeId = Message.getSourceFromTag(tag, nodeId) + val sessionKey = SessionKeyGenerator.generate(senderNodeId, nodeId, secret, idNonce) + val decryptedAuthResponse = + Bytes.wrap(AES128GCM.decrypt(sessionKey.authRespKey, Bytes.wrap(ByteArray(12)), authResponse, Bytes.EMPTY)) + RLP.decodeList(decryptedAuthResponse) { reader -> + reader.skipNext() + val signatureBytes = reader.readValue() + val enr = reader.readList { enrReader -> EthereumNodeRecord.fromRLP(enrReader) } + receivedEnr = enr + publicKey = enr.publicKey() + val signatureVerified = verifySignature(signatureBytes, idNonce, ephemeralPublicKey, enr.publicKey()) + if (!signatureVerified) { + throw IllegalArgumentException("Signature is not verified") + } + logger.trace("Finalized handshake") + connected.complete(sessionKey) + } + } + } else { + logger.trace("Identified a RANDOM message") + val token = RLP.decodeValue(content) + val peerNodeId = Message.getSourceFromTag(tag, nodeId) + logger.trace("Found peerNodeId $peerNodeId") + // Build a WHOAREYOU message with the tag of the random message. + val whoAreYouTag = Hash.sha2_256(Bytes.wrap(peerNodeId, Bytes.wrap("WHOAREYOU".toByteArray()))) + val response = WhoAreYouMessage(whoAreYouTag, token, Message.idNonce(), enr().seq()) + this.tokens.add(token) + logger.trace("Sending WHOAREYOU to {}", address) + sendFn(address, response.toRLP()) + } + } + } + + private fun verifySignature( + signatureBytes: Bytes, + idNonce: Bytes, + ephemeralPublicKey: SECP256K1.PublicKey, + publicKey: SECP256K1.PublicKey, + ): Boolean { + val signature = SECP256K1.Signature.create( + 1, + signatureBytes.slice(0, 32).toUnsignedBigInteger(), + signatureBytes.slice(32).toUnsignedBigInteger(), + ) + + val signValue = Bytes.wrap(DISCOVERY_ID_NONCE, idNonce, ephemeralPublicKey.bytes()) + val hashedSignValue = Hash.sha2_256(signValue) + if (!SECP256K1.verifyHashed(hashedSignValue, signature, publicKey)) { + val signature0 = SECP256K1.Signature.create( + 0, + signatureBytes.slice(0, 32).toUnsignedBigInteger(), + signatureBytes.slice(32).toUnsignedBigInteger(), + ) + return SECP256K1.verifyHashed(hashedSignValue, signature0, publicKey) + } else { + return true + } + } + + fun awaitConnection(): AsyncResult = connected + + fun tag() = Message.tag(nodeId, EthereumNodeRecord.nodeId(publicKey!!)) +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Message.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Message.kt new file mode 100644 index 000000000..0aa35916e --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Message.kt @@ -0,0 +1,75 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.Hash + +/** + * Discovery message sent over UDP. + */ +internal interface Message { + + companion object { + + const val MAX_UDP_MESSAGE_SIZE = 1280 + const val TAG_LENGTH: Int = 32 + const val AUTH_TAG_LENGTH: Int = 12 + const val RANDOM_DATA_LENGTH: Int = 44 + const val ID_NONCE_LENGTH: Int = 32 + const val REQUEST_ID_LENGTH: Int = 8 + + private val WHO_ARE_YOU: Bytes = Bytes.wrap("WHOAREYOU".toByteArray()) + + fun magic(dest: Bytes): Bytes { + return Hash.sha2_256(Bytes.wrap(dest, WHO_ARE_YOU)) + } + + fun tag(src: Bytes, dest: Bytes): Bytes { + val encodedDestKey = Hash.sha2_256(dest) + return encodedDestKey.mutableCopy().xor(src) + } + + fun getSourceFromTag(tag: Bytes, dest: Bytes): Bytes { + val encodedDestKey = Hash.sha2_256(dest) + return encodedDestKey.mutableCopy().xor(tag) + } + + fun requestId(): Bytes = Bytes.random(REQUEST_ID_LENGTH) + + fun authTag(): Bytes = Bytes.random(AUTH_TAG_LENGTH) + + fun idNonce(): Bytes = Bytes.random(ID_NONCE_LENGTH) + } + + fun toRLP(): Bytes + + fun type(): MessageType +} + +internal enum class MessageType(val code: Int) { + RANDOM(0), + WHOAREYOU(0), + FINDNODE(3), + NODES(4), + PING(1), + PONG(2), + REGTOPIC(5), + REGCONFIRM(7), + TICKET(6), + TOPICQUERY(8), + ; + + fun byte(): Byte = code.toByte() + + companion object { + fun valueOf(code: Int): MessageType { + for (messageType in MessageType.values()) { + if (messageType.code == code) { + return messageType + } + } + throw IllegalArgumentException("No known message with code $code") + } + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Messages.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Messages.kt new file mode 100644 index 000000000..bef28ebbc --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Messages.kt @@ -0,0 +1,305 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import org.apache.tuweni.v2.rlp.RLP +import java.net.InetAddress + +internal class FindNodeMessage( + val requestId: Bytes = Message.requestId(), + val distance: Int = 0, +) : Message { + + private val encodedMessageType: Bytes = Bytes.fromHexString("0x03") + + override fun toRLP(): Bytes { + return RLP.encodeList { writer -> + writer.writeValue(requestId) + writer.writeInt(distance) + } + } + + override fun type(): MessageType = MessageType.FINDNODE + + companion object { + fun create(content: Bytes): FindNodeMessage { + return RLP.decodeList(content) { reader -> + val requestId = reader.readValue() + val distance = reader.readInt() + return@decodeList FindNodeMessage(requestId, distance) + } + } + } +} + +internal class NodesMessage( + val requestId: Bytes = Message.requestId(), + val total: Int, + val nodeRecords: List, +) : Message { + + private val encodedMessageType: Bytes = Bytes.fromHexString("0x04") + + override fun type(): MessageType = MessageType.NODES + + override fun toRLP(): Bytes { + return RLP.encodeList { writer -> + writer.writeValue(requestId) + writer.writeInt(total) + writer.writeList(nodeRecords) { listWriter, it -> + listWriter.writeRLP(it.toRLP()) + } + } + } + + companion object { + fun create(content: Bytes): NodesMessage { + return RLP.decodeList(content) { reader -> + val requestId = reader.readValue() + val total = reader.readInt() + val nodeRecords = reader.readListContents { listReader -> + listReader.readList { enrReader -> + EthereumNodeRecord.fromRLP(enrReader) + } + } + return@decodeList NodesMessage(requestId, total, nodeRecords) + } + } + } +} + +internal class PingMessage( + val requestId: Bytes = Message.requestId(), + val enrSeq: Long = 0, +) : Message { + + private val encodedMessageType: Bytes = Bytes.fromHexString("0x01") + + override fun type(): MessageType = MessageType.PING + + override fun toRLP(): Bytes { + return RLP.encodeList { reader -> + reader.writeValue(requestId) + reader.writeLong(enrSeq) + } + } + + companion object { + fun create(content: Bytes): PingMessage { + return RLP.decodeList(content) { reader -> + val requestId = reader.readValue() + val enrSeq = reader.readLong() + return@decodeList PingMessage(requestId, enrSeq) + } + } + } +} + +internal class RandomMessage( + val authTag: Bytes = Message.authTag(), + val data: Bytes = randomData(), +) : Message { + + companion object { + fun randomData(): Bytes = Bytes.random(Message.RANDOM_DATA_LENGTH) + + fun create(authTag: Bytes, content: Bytes = randomData()): RandomMessage { + return RandomMessage(authTag, content) + } + } + + override fun type(): MessageType = MessageType.RANDOM + + override fun toRLP(): Bytes { + return data + } +} + +internal class TicketMessage( + val requestId: Bytes = Message.requestId(), + val ticket: Bytes, + val waitTime: Long, +) : Message { + + override fun type(): MessageType = MessageType.TICKET + + override fun toRLP(): Bytes { + return RLP.encodeList { writer -> + writer.writeValue(requestId) + writer.writeValue(ticket) + writer.writeLong(waitTime) + } + } + + companion object { + fun create(content: Bytes): TicketMessage { + return RLP.decodeList(content) { reader -> + val requestId = reader.readValue() + val ticket = reader.readValue() + val waitTime = reader.readLong() + return@decodeList TicketMessage(requestId, ticket, waitTime) + } + } + } +} + +internal class WhoAreYouMessage( + val magic: Bytes, + val token: Bytes, + val idNonce: Bytes, + val enrSeq: Long = 0, +) : Message { + + companion object { + fun create(magic: Bytes, content: Bytes): WhoAreYouMessage { + return RLP.decodeList(content) { r -> + val token = r.readValue() + val idNonce = r.readValue() + val enrSeq = r.readValue() + WhoAreYouMessage(magic = magic, token = token, idNonce = idNonce, enrSeq = enrSeq.toLong()) + } + } + } + + override fun type(): MessageType = MessageType.WHOAREYOU + + override fun toRLP(): Bytes { + return Bytes.wrap( + magic, + RLP.encodeList { w -> + w.writeValue(token) + w.writeValue(idNonce) + w.writeLong(enrSeq) + }, + ) + } +} + +internal class TopicQueryMessage( + val requestId: Bytes = Message.requestId(), + val topic: Bytes, +) : Message { + + private val encodedMessageType: Bytes = Bytes.fromHexString("0x08") + + override fun type(): MessageType = MessageType.TOPICQUERY + + override fun toRLP(): Bytes { + return RLP.encodeList { writer -> + writer.writeValue(requestId) + writer.writeValue(topic) + } + } + + companion object { + fun create(content: Bytes): TopicQueryMessage { + return RLP.decodeList(content) { reader -> + val requestId = reader.readValue() + val topic = reader.readValue() + return@decodeList TopicQueryMessage(requestId, topic) + } + } + } +} + +/** + * Message to register a topic. + */ +internal class RegTopicMessage( + val requestId: Bytes = Message.requestId(), + val nodeRecord: EthereumNodeRecord, + val topic: Bytes, + val ticket: Bytes, +) : Message { + + private val encodedMessageType: Bytes = Bytes.fromHexString("0x05") + + override fun type(): MessageType = MessageType.REGTOPIC + + override fun toRLP(): Bytes { + return RLP.encodeList { writer -> + writer.writeValue(requestId) + writer.writeRLP(nodeRecord.toRLP()) + writer.writeValue(topic) + writer.writeValue(ticket) + } + } + + companion object { + fun create(content: Bytes): RegTopicMessage { + return RLP.decodeList(content) { reader -> + val requestId = reader.readValue() + val nodeRecord = reader.readList { enrReader -> + EthereumNodeRecord.fromRLP(enrReader) + } + val topic = reader.readValue() + val ticket = reader.readValue() + return@decodeList RegTopicMessage(requestId, nodeRecord, topic, ticket) + } + } + } +} + +internal class PongMessage( + val requestId: Bytes = Message.requestId(), + val enrSeq: Long = 0, + val recipientIp: String, + val recipientPort: Int, +) : Message { + + private val encodedMessageType: Bytes = Bytes.fromHexString("0x02") + + override fun type(): MessageType = MessageType.PONG + + override fun toRLP(): Bytes { + return RLP.encodeList { writer -> + writer.writeValue(requestId) + writer.writeLong(enrSeq) + + val bytesIp = Bytes.wrap(InetAddress.getByName(recipientIp).address) + writer.writeValue(bytesIp) + writer.writeInt(recipientPort) + } + } + + companion object { + fun create(content: Bytes): PongMessage { + return RLP.decodeList(content) { reader -> + val requestId = reader.readValue() + val enrSeq = reader.readLong() + val address = InetAddress.getByAddress(reader.readValue().toArrayUnsafe()) + val recipientPort = reader.readInt() + return@decodeList PongMessage(requestId, enrSeq, address.hostAddress, recipientPort) + } + } + } +} + +internal class RegConfirmationMessage( + val requestId: Bytes = Message.requestId(), + val topic: Bytes, +) : Message { + + private val encodedMessageType: Bytes = Bytes.fromHexString("0x07") + + override fun type(): MessageType = MessageType.REGCONFIRM + + override fun toRLP(): Bytes { + return RLP.encodeList { writer -> + writer.writeValue(requestId) + writer.writeValue(topic) + } + } + + companion object { + fun create(content: Bytes): RegConfirmationMessage { + return RLP.decodeList(content) { reader -> + val requestId = reader.readValue() + val topic = reader.readValue() + return@decodeList RegConfirmationMessage(requestId, topic) + } + } + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/RoutingTable.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/RoutingTable.kt new file mode 100644 index 000000000..5dbc252da --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/RoutingTable.kt @@ -0,0 +1,62 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import com.google.common.math.IntMath +import org.apache.tuweni.kademlia.KademliaRoutingTable +import org.apache.tuweni.kademlia.xorDist +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import java.math.RoundingMode + +internal class RoutingTable( + private val selfEnr: EthereumNodeRecord, +) { + + private val selfNodeId = EthereumNodeRecord.nodeId(selfEnr.publicKey()).toArrayUnsafe() + private val nodeIdCalculation: (Bytes) -> ByteArray = { enr -> key(enr) } + + private val table = KademliaRoutingTable( + selfId = selfNodeId, + k = BUCKET_SIZE, + nodeId = nodeIdCalculation, + distanceToSelf = { + val xorResult = key(it) xorDist selfNodeId + if (xorResult == 0) 0 else IntMath.log2(xorResult, RoundingMode.FLOOR) + }, + ) + + val size: Int + get() = table.size + + fun getSelfEnr(): EthereumNodeRecord = selfEnr + + fun add(enr: EthereumNodeRecord) { + add(enr.toRLP()) + } + + fun add(enr: Bytes) { + if (enr != selfEnr.toRLP()) { + table.add(enr) + } + } + + fun distanceToSelf(targetId: Bytes): Int = table.logDistToSelf(targetId) + + fun evict(enr: Bytes): Boolean = table.evict(enr) + + fun random(): Bytes = table.getRandom() + + fun isEmpty(): Boolean = table.isEmpty() + + fun nodesOfDistance(distance: Int): List = + table.peersOfDistance(distance).map { EthereumNodeRecord.fromRLP(it) } + + fun clear() = table.clear() + + private fun key(enr: Bytes): ByteArray = EthereumNodeRecord.fromRLP(enr).nodeId().toArrayUnsafe() + + companion object { + private const val BUCKET_SIZE: Int = 16 + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Session.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Session.kt new file mode 100644 index 000000000..338ab7308 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Session.kt @@ -0,0 +1,333 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import io.vertx.core.net.SocketAddress +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.apache.tuweni.concurrent.AsyncCompletion +import org.apache.tuweni.concurrent.AsyncResult +import org.apache.tuweni.concurrent.CompletableAsyncCompletion +import org.apache.tuweni.concurrent.CompletableAsyncResult +import org.apache.tuweni.concurrent.ExpiringMap +import org.apache.tuweni.rlp.InvalidRLPTypeException +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.devp2p.DiscoveryService +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import org.apache.tuweni.v2.devp2p.v5.encrypt.AES128GCM +import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKey +import org.apache.tuweni.v2.devp2p.v5.topic.Ticket +import org.apache.tuweni.v2.devp2p.v5.topic.Topic +import org.apache.tuweni.v2.devp2p.v5.topic.TopicTable +import org.apache.tuweni.v2.rlp.RLP +import org.apache.tuweni.v2.rlp.RLPReader +import org.slf4j.LoggerFactory +import java.lang.UnsupportedOperationException +import kotlin.coroutines.CoroutineContext + +private const val MAX_NODES_IN_RESPONSE: Int = 4 +private const val WHO_ARE_YOU_MESSAGE_LENGTH = 48 +private const val SEND_REGTOPIC_DELAY_MS = 15 * 60 * 1000L // 15 min + +/** + * Tracks a session with another peer. + */ +internal class Session( + val enr: EthereumNodeRecord, + private val keyPair: SECP256K1.KeyPair, + private val nodeId: Bytes, + private val tag: Bytes, + private val sessionKey: SessionKey, + private val address: SocketAddress, + private val sendFn: (SocketAddress, Bytes) -> Unit, + private val ourENR: () -> EthereumNodeRecord, + private val routingTable: RoutingTable, + private val topicTable: TopicTable, + private val failedPingsListener: (missedPings: Int) -> Boolean, + override val coroutineContext: CoroutineContext, +) : CoroutineScope { + + companion object { + private val logger = LoggerFactory.getLogger(Session::class.java) + + const val PING_REFRESH = 10000L + } + + val activeFindNodes = HashMap>>() + private var activePing: CompletableAsyncCompletion? = null + private val chunkedNodeResults = ExpiringMap>() + private var missedPings = 0 + private val ticketHolder = HashMap() + private var peerSeq: Long = -1 + + private fun launchPing() { + launch { + delay(PING_REFRESH) + sendPing() + } + } + + private suspend fun sendPing(): AsyncCompletion { + activePing?.let { + if (!it.isDone) { + it.cancel() + } + } + val newPing = AsyncCompletion.incomplete() + newPing.exceptionally { + missedPings++ + if (!failedPingsListener(missedPings)) { + launchPing() + } + } + newPing.thenRun { + this.missedPings = 0 + launchPing() + } + activePing = newPing + send(PingMessage()) + return newPing + } + + suspend fun sendFindNodes(distance: Int): AsyncResult> { + val message = FindNodeMessage(distance = distance) + val result: CompletableAsyncResult> = AsyncResult.incomplete() + activeFindNodes[message.requestId] = result + send(message) + return result + } + + suspend fun processMessage(messageBytes: Bytes) { + if (messageBytes.size() > Message.MAX_UDP_MESSAGE_SIZE) { + logger.trace("Message too long, dropping from {}", address) + return + } + logger.trace("Received message from {}", address) + + var message: Message + try { + message = decode(messageBytes) + } catch (e: InvalidRLPTypeException) { + logger.trace("Bad message content, dropping from {}: {}", address, messageBytes) + return + } + logger.trace("Received message of type {}", message.type()) + when (message.type()) { + MessageType.FINDNODE -> handleFindNode(message as FindNodeMessage) + MessageType.NODES -> handleNodes(message as NodesMessage) + MessageType.PING -> handlePing(message as PingMessage) + MessageType.PONG -> handlePong(message as PongMessage) + MessageType.REGTOPIC -> handleRegTopic( + message as RegTopicMessage, + ) + MessageType.REGCONFIRM -> handleRegConfirmation( + message as RegConfirmationMessage, + ) + MessageType.TICKET -> handleTicket(message as TicketMessage) + MessageType.TOPICQUERY -> handleTopicQuery(message as TopicQueryMessage) + MessageType.RANDOM -> throw UnsupportedOperationException("random") + MessageType.WHOAREYOU -> throw UnsupportedOperationException("whoareyou") + } + } + + private suspend fun handleTopicQuery(message: TopicQueryMessage) { + val nodes = topicTable.getNodes(Topic(message.topic.toHexString())) + + for (chunk in nodes.chunked(MAX_NODES_IN_RESPONSE)) { + val response = NodesMessage(message.requestId, nodes.size, chunk) + send(response) + } + } + + private suspend fun handlePong( + message: PongMessage, + ) { + if (activePing?.isDone == true) { + logger.trace("Received pong when no ping was active") + return + } + if (peerSeq != message.enrSeq) { + val request = FindNodeMessage(message.requestId) + send(request) + } + activePing?.complete() + } + + private suspend fun handlePing( + message: PingMessage, + ) { + activePing = AsyncCompletion.incomplete() + val response = + PongMessage(message.requestId, ourENR().seq(), address.host(), address.port()) + send(response) + } + + private val now: () -> Long = DiscoveryService.CURRENT_TIME_SUPPLIER + + private suspend fun handleNodes(message: NodesMessage) { + if (activeFindNodes[message.requestId] == null) { + logger.trace("Received NODES message but no matching FINDNODES present. Dropping") + return + } + val enrs = message.nodeRecords + val records = chunkedNodeResults.computeIfAbsent(message.requestId) { mutableListOf() } + records.addAll(enrs) + logger.debug("Received ${enrs.size} for ${records.size}/${message.total}") + + // there seems to be a bug where no nodes are sent yet the total is above 0. + if ((records.size == 0 && message.total != 0) || records.size >= message.total) { + activeFindNodes[message.requestId]?.let { + it.complete(chunkedNodeResults[message.requestId]) + chunkedNodeResults.remove(message.requestId) + activeFindNodes.remove(message.requestId) + } + } + } + + private suspend fun handleTicket(message: TicketMessage) { + ticketHolder.put(message.requestId, message.ticket) + + if (message.waitTime != 0L) { + val ticket = Ticket.decrypt(message.ticket, sessionKey.initiatorKey) + delayRegTopic(message.requestId, ticket.topic, message.waitTime) + } + } + + private suspend fun handleRegTopic( + message: RegTopicMessage, + ) { + val topic = Topic(message.topic.toHexString()) + + val existingTicket = if (!message.ticket.isEmpty) { + val ticket = Ticket.decrypt(message.ticket, sessionKey.initiatorKey) + ticket.validate(nodeId, address.host(), now(), message.topic) + ticket + } else { + null + } + + // Create new ticket + val waitTime = topicTable.put(topic, message.nodeRecord) + val cumTime = (existingTicket?.cumTime ?: waitTime) + waitTime + val ticket = Ticket(message.topic, nodeId, address.host(), now(), waitTime, cumTime) + val encryptedTicket = ticket.encrypt(sessionKey.initiatorKey) + + // Send ticket + val response = TicketMessage(message.requestId, encryptedTicket, waitTime) + sendFn(address, response.toRLP()) + + // Send confirmation if topic was placed + if (waitTime == 0L) { + val confirmation = RegConfirmationMessage(message.requestId, message.topic) + send(confirmation) + } + } + + private suspend fun handleRegConfirmation(message: RegConfirmationMessage) { + ticketHolder.remove(message.requestId) + registerTopic(message.topic, true) + } + + private suspend fun send(message: Message) { + logger.trace("Sending an encrypted message of type {}", message.type()) + val messagePlain = Bytes.wrap(Bytes.of(message.type().byte()), message.toRLP()) + val authTag = Message.authTag() + val encryptionResult = AES128GCM.encrypt(sessionKey.initiatorKey, authTag, messagePlain, tag) + sendFn(address, Bytes.wrap(tag, RLP.encodeValue(authTag), encryptionResult)) + } + + private suspend fun handleFindNode(message: FindNodeMessage) { + if (0 == message.distance) { + val response = NodesMessage(message.requestId, 1, listOf(ourENR())) + send(response) + return + } + + val nodes = routingTable.nodesOfDistance(message.distance) + + for (chunk in nodes.chunked(MAX_NODES_IN_RESPONSE)) { + val response = NodesMessage(message.requestId, nodes.size, chunk) + send(response) + } + } + + fun decode(message: Bytes): Message { + val tag = message.slice(0, Message.TAG_LENGTH) + val contentWithHeader = message.slice(Message.TAG_LENGTH) + val decodedMessage = RLP.decode(contentWithHeader) { reader -> read(tag, contentWithHeader, reader) } + return decodedMessage + } + + internal fun read(tag: Bytes, contentWithHeader: Bytes, reader: RLPReader): Message { + if (reader.nextIsList()) { + // this looks like a WHOAREYOU. + } + val authTag = reader.readValue() + + val encryptedContent = contentWithHeader.slice(reader.position()) + val decryptionKey = sessionKey.recipientKey + val decryptedContent = AES128GCM.decrypt(decryptionKey, authTag, encryptedContent, tag) + val type = decryptedContent.slice(0, 1) + val message = decryptedContent.slice(1) + + // Retrieve result + val messageType = MessageType.valueOf(type.toInt()) + return when (messageType) { + MessageType.PING -> PingMessage.create(message) + MessageType.PONG -> PongMessage.create(message) + MessageType.FINDNODE -> FindNodeMessage.create(message) + MessageType.NODES -> NodesMessage.create(message) + MessageType.REGTOPIC -> RegTopicMessage.create(message) + MessageType.TICKET -> TicketMessage.create(message) + MessageType.REGCONFIRM -> RegConfirmationMessage.create(message) + MessageType.TOPICQUERY -> TopicQueryMessage.create(message) + else -> throw IllegalArgumentException("Unsupported message type $messageType") + } + } + + suspend fun delayRegTopic(requestId: Bytes, topic: Bytes, waitTime: Long) { + delay(waitTime) + + val ticket = ticketHolder.get(requestId) + ticket?.let { + sendRegTopic(topic, ticket, requestId) + } + } + + suspend fun registerTopic(topic: Bytes, withDelay: Boolean = false) { + if (withDelay) { + delay(SEND_REGTOPIC_DELAY_MS) + } + + sendRegTopic(topic, Bytes.EMPTY) + } + + private suspend fun sendRegTopic( + topic: Bytes, + ticket: Bytes, + requestId: Bytes = Message.requestId(), + ) { + TODO("" + topic + ticket + requestId) + } + +// private suspend fun sendRegTopic( +// topic: Bytes, +// ticket: Bytes, +// requestId: Bytes = Message.requestId() +// ) { +// val nodeEnr = enr().toRLP() +// //val message = RegTopicMessage(requestId, nodeEnr, topic, ticket) +// +// val distance = 1 +// val receivers = routingTable.nodesOfDistance(distance) +// receivers.forEach { rlp -> +// val receiver = EthereumNodeRecord.fromRLP(rlp) +// val address = InetSocketAddress(receiver.ip(), receiver.udp()) +// val nodeId = Hash.sha2_256(rlp) +// TODO("" +address + nodeId) +// //send(address, message, nodeId) +// } +// } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt new file mode 100644 index 000000000..149a5951e --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt @@ -0,0 +1,58 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.encrypt + +import org.apache.tuweni.v2.bytes.Bytes +import javax.crypto.Cipher +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec + +/** + * Util dedicated for AES-GCM encoding with key size equal 16 bytes + */ +object AES128GCM { + + private const val ALGO_NAME: String = "AES" + private const val CIPHER_NAME: String = "AES/GCM/NoPadding" + private const val KEY_SIZE: Int = 128 + + /** + * AES128GCM encryption function + * + * @param key 16-byte encryption key + * @param nonce initialization vector + * @param message content for encryption + * @param data encryption metadata + */ + fun encrypt(privateKey: Bytes, nonce: Bytes, message: Bytes, additionalAuthenticatedData: Bytes): Bytes { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init( + Cipher.ENCRYPT_MODE, + SecretKeySpec(privateKey.toArrayUnsafe(), "AES"), + GCMParameterSpec(128, nonce.toArrayUnsafe()), + ) + cipher.updateAAD(additionalAuthenticatedData.toArrayUnsafe()) + val result = Bytes.wrap(cipher.doFinal(message.toArrayUnsafe())) + return result + } + + /** + * AES128GCM decryption function + * + * @param privateKey the key to use for decryption + * @param nonce the nonce of the encrypted data + * @param encoded the encrypted content + * @param additionalAuthenticatedData the AAD that should be decrypted alongside + * @return the decrypted data + */ + fun decrypt(privateKey: Bytes, nonce: Bytes, encoded: Bytes, additionalAuthenticatedData: Bytes): Bytes { + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init( + Cipher.DECRYPT_MODE, + SecretKeySpec(privateKey.toArrayUnsafe(), "AES"), + GCMParameterSpec(128, nonce.toArrayUnsafe()), + ) + cipher.updateAAD(additionalAuthenticatedData.toArrayUnsafe()) + return Bytes.wrap(cipher.doFinal(encoded.toArrayUnsafe())) + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKey.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKey.kt new file mode 100644 index 000000000..57c5b86c8 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKey.kt @@ -0,0 +1,11 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.encrypt + +import org.apache.tuweni.v2.bytes.Bytes + +internal data class SessionKey( + val initiatorKey: Bytes, + val recipientKey: Bytes, + val authRespKey: Bytes, +) diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGenerator.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGenerator.kt new file mode 100644 index 000000000..96f84406c --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGenerator.kt @@ -0,0 +1,40 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.encrypt + +import org.apache.tuweni.v2.bytes.Bytes +import org.bouncycastle.crypto.digests.SHA256Digest +import org.bouncycastle.crypto.generators.HKDFBytesGenerator +import org.bouncycastle.crypto.params.HKDFParameters + +/** + * Generates session keys on handshake, using HKDF key derivation function + */ +internal object SessionKeyGenerator { + + private const val DERIVED_KEY_SIZE: Int = 16 + private val INFO_PREFIX = Bytes.wrap("discovery v5 key agreement".toByteArray()) + + /** + * Executes session keys generation + * + * @param srcNodeId sender node identifier + * @param destNodeId receiver node identifier + * @param secret the input keying material or seed + * @param idNonce nonce used as salt + */ + fun generate(srcNodeId: Bytes, destNodeId: Bytes, secret: Bytes, idNonce: Bytes): SessionKey { + val info = Bytes.wrap(INFO_PREFIX, srcNodeId, destNodeId) + + val hkdf = HKDFBytesGenerator(SHA256Digest()) + val params = HKDFParameters(secret.toArrayUnsafe(), idNonce.toArrayUnsafe(), info.toArrayUnsafe()) + hkdf.init(params) + val output = Bytes.wrap(ByteArray(DERIVED_KEY_SIZE * 3)) + hkdf.generateBytes(output.toArrayUnsafe(), 0, output.size()) + return SessionKey( + output.slice(0, DERIVED_KEY_SIZE), + output.slice(DERIVED_KEY_SIZE, DERIVED_KEY_SIZE), + output.slice(DERIVED_KEY_SIZE * 2), + ) + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Ticket.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Ticket.kt new file mode 100644 index 000000000..bfea72b53 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Ticket.kt @@ -0,0 +1,70 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.topic + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.devp2p.v5.encrypt.AES128GCM +import org.apache.tuweni.v2.rlp.RLP +import java.net.InetAddress + +internal data class Ticket( + val topic: Bytes, + val srcNodeId: Bytes, + val srcIp: String, + val requestTime: Long, + val waitTime: Long, + val cumTime: Long, +) { + + companion object { + private const val ZERO_NONCE_SIZE: Int = 12 + internal const val TIME_WINDOW_MS: Int = 10000 // 10 seconds + internal const val TICKET_INVALID_MSG = "Ticket is invalid" + + fun create(content: Bytes): Ticket { + return RLP.decodeList(content) { reader -> + val topic = reader.readValue() + val srcNodeId = reader.readValue() + val srcIp = InetAddress.getByAddress(reader.readValue().toArrayUnsafe()) + val requestTime = reader.readLong() + val waitTime = reader.readLong() + val cumTime = reader.readLong() + return@decodeList Ticket(topic, srcNodeId, srcIp.hostAddress, requestTime, waitTime, cumTime) + } + } + + fun decrypt(encrypted: Bytes, key: Bytes): Ticket { + val decrypted = AES128GCM.decrypt(key, Bytes.wrap(ByteArray(ZERO_NONCE_SIZE)), encrypted, Bytes.EMPTY) + return create(decrypted) + } + } + + fun encode(): Bytes { + return RLP.encodeList { writer -> + writer.writeValue(topic) + writer.writeValue(srcNodeId) + writer.writeValue(Bytes.wrap(InetAddress.getByName(srcIp).address)) + writer.writeLong(requestTime) + writer.writeLong(waitTime) + writer.writeLong(cumTime) + } + } + + fun encrypt(key: Bytes): Bytes { + val ticketBytes = encode() + return AES128GCM.encrypt(key, Bytes.wrap(ByteArray(ZERO_NONCE_SIZE)), ticketBytes, Bytes.EMPTY) + } + + fun validate( + srcNodeId: Bytes, + srcIp: String, + now: Long, + topic: Bytes, + ) { + require(this.srcNodeId == srcNodeId) { TICKET_INVALID_MSG } + require(this.srcIp == srcIp) { TICKET_INVALID_MSG } + require(this.topic == topic) { TICKET_INVALID_MSG } + val windowStart = this.requestTime + this.waitTime + require(now >= windowStart && now <= windowStart + TIME_WINDOW_MS) { TICKET_INVALID_MSG } + } +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Topic.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Topic.kt new file mode 100644 index 000000000..1d584fff5 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Topic.kt @@ -0,0 +1,12 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.topic + +import org.apache.tuweni.v2.bytes.Bytes + +internal data class Topic( + val content: String, +) { + + fun toBytes(): Bytes = Bytes.fromHexString(content) +} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TopicTable.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TopicTable.kt new file mode 100644 index 000000000..ab7cb5369 --- /dev/null +++ b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TopicTable.kt @@ -0,0 +1,95 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.topic + +import com.google.common.cache.Cache +import com.google.common.cache.CacheBuilder +import org.apache.tuweni.v2.devp2p.DiscoveryService +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import java.util.concurrent.TimeUnit + +internal class TopicTable( + private val tableCapacity: Int = MAX_TABLE_CAPACITY, + private val queueCapacity: Int = MAX_ENTRIES_PER_TOPIC, +) { + + private val timeSupplier: () -> Long = DiscoveryService.CURRENT_TIME_SUPPLIER + private val table: HashMap> = HashMap(tableCapacity) + + init { + require(tableCapacity > 0) { "Table capacity value must be positive" } + require(queueCapacity > 0) { "Queue capacity value must be positive" } + } + + fun getNodes(topic: Topic): List { + val values = table[topic] + return values?.let { values.asMap().values.map { it.enr } } ?: emptyList() + } + + /** + * Puts a topic in a table + * + * @return wait time for registrant node (0 is topic was putted immediately, -1 in case of error) + */ + @Synchronized + fun put(topic: Topic, enr: EthereumNodeRecord): Long { + gcTable() + + val topicQueue = table[topic] + val nodeId = enr.nodeId().toHexString() + + if (null != topicQueue) { + if (topicQueue.size() < queueCapacity) { + topicQueue.put(nodeId, TargetAd(timeSupplier(), enr)) + return 0 // put immediately + } else { + // Queue if full (wait time = target-ad-lifetime - oldest ad lifetime in queue) + return TARGET_AD_LIFETIME_MS - (timeSupplier() - topicQueue.oldest().regTime) + } + } + + if (table.size < tableCapacity) { + table[topic] = createNewQueue().apply { put(nodeId, TargetAd(timeSupplier(), enr)) } + return 0 // put immediately + } else { + // table is full (wait time = target-ad-lifetime - oldest in table of youngest in queue) + val oldestInTable = table.entries.map { it.value.youngest().regTime }.minOrNull() ?: -1 + return TARGET_AD_LIFETIME_MS - (timeSupplier() - oldestInTable) + } + } + + fun contains(topic: Topic): Boolean = table.containsKey(topic) + + fun isEmpty(): Boolean = table.isEmpty() + + fun clear() = table.clear() + + private fun createNewQueue(): Cache { + return CacheBuilder.newBuilder() + .expireAfterWrite(TARGET_AD_LIFETIME_MS, TimeUnit.MILLISECONDS) + .initialCapacity(queueCapacity) + .build() + } + + private fun gcTable() { + table.entries.removeIf { it.value.size() == 0L } + } + + private fun Cache.oldest(): TargetAd { + return asMap().values.minByOrNull { it.regTime } ?: throw IllegalArgumentException(QUEUE_EMPTY_MSG) + } + + private fun Cache.youngest(): TargetAd { + return asMap().values.maxByOrNull { it.regTime } ?: throw IllegalArgumentException(QUEUE_EMPTY_MSG) + } + + companion object { + internal const val MAX_ENTRIES_PER_TOPIC: Int = 100 + private const val MAX_TABLE_CAPACITY: Int = 500 + private const val TARGET_AD_LIFETIME_MS = (15 * 60 * 1000).toLong() // 15 min + + private const val QUEUE_EMPTY_MSG = "Queue is empty." + } +} + +internal class TargetAd(val regTime: Long, val enr: EthereumNodeRecord) diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeSessionTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeSessionTest.kt index 3a7d15380..db9302098 100644 --- a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeSessionTest.kt +++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeSessionTest.kt @@ -6,8 +6,11 @@ import io.vertx.core.net.SocketAddress import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import org.apache.tuweni.bytes.Bytes +import org.apache.tuweni.bytes.Bytes32 import org.apache.tuweni.concurrent.coroutines.await import org.apache.tuweni.crypto.SECP256K1 +import org.apache.tuweni.crypto.SECP256K1.PublicKey +import org.apache.tuweni.crypto.SECP256K1.SecretKey import org.apache.tuweni.devp2p.EthereumNodeRecord import org.apache.tuweni.devp2p.v5.encrypt.AES128GCM import org.apache.tuweni.devp2p.v5.encrypt.SessionKeyGenerator @@ -23,8 +26,12 @@ class HandshakeSessionTest { @Test fun testConnectTwoClients() = runBlocking { - val keyPair = SECP256K1.KeyPair.random() - val peerKeyPair = SECP256K1.KeyPair.random() + val secretKey = SecretKey.fromBytes(Bytes32.fromHexString("0x01")) + val publicKey = PublicKey.fromSecretKey(secretKey) + val keyPair = SECP256K1.KeyPair.create(secretKey, publicKey) + val peerSecretKey = SecretKey.fromBytes(Bytes32.fromHexString("0x02")) + val peerPublicKey = PublicKey.fromSecretKey(peerSecretKey) + val peerKeyPair = SECP256K1.KeyPair.create(peerSecretKey, peerPublicKey) val address = SocketAddress.inetSocketAddress(1234, "localhost") val peerAddress = SocketAddress.inetSocketAddress(1235, "localhost") val enr = EthereumNodeRecord.create(keyPair, ip = InetAddress.getLoopbackAddress(), udp = 1234) diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryServiceTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryServiceTest.kt new file mode 100644 index 000000000..a8c97065b --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryServiceTest.kt @@ -0,0 +1,403 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import io.vertx.core.Vertx +import io.vertx.core.buffer.Buffer +import io.vertx.core.net.SocketAddress +import io.vertx.kotlin.coroutines.coAwait +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.apache.tuweni.concurrent.AsyncResult +import org.apache.tuweni.concurrent.CompletableAsyncResult +import org.apache.tuweni.concurrent.coroutines.await +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.junit.VertxExtension +import org.apache.tuweni.junit.VertxInstance +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.junit.jupiter.api.extension.ExtendWith +import java.net.URI +import java.nio.ByteBuffer +import java.util.concurrent.atomic.AtomicReference + +@Timeout(10) +@ExtendWith(BouncyCastleExtension::class, VertxExtension::class) +internal class DiscoveryServiceTest { + + @Test + fun shouldStartAndShutdownService(@VertxInstance vertx: Vertx) = runBlocking { + val discoveryService = DiscoveryService.open( + vertx, + host = "127.0.0.1", + keyPair = SECP256K1.KeyPair.random(), + ) + discoveryService.awaitBootstrap() + assertFalse(discoveryService.isShutdown) + discoveryService.shutdown() + assertTrue(discoveryService.isShutdown) + } + + @Test + fun shouldRespondToPingAndRecordEndpoint(@VertxInstance vertx: Vertx): Unit = runBlocking { + val peerRepository = EphemeralPeerRepository() + val discoveryService = DiscoveryService.open( + vertx, + host = "127.0.0.1", + keyPair = SECP256K1.KeyPair.random(), + peerRepository = peerRepository, + ) + discoveryService.awaitBootstrap() + val address = SocketAddress.inetSocketAddress(discoveryService.localPort, "127.0.0.1") + val clientKeyPair = SECP256K1.KeyPair.random() + val reference = AsyncResult.incomplete() + val client = vertx.createDatagramSocket().handler { res -> + reference.complete(res.data()) + }.listen(0, "localhost").coAwait() + val clientEndpoint = Endpoint("192.168.1.1", 5678, 7654) + val ping = PingPacket.create( + clientKeyPair, + System.currentTimeMillis(), + clientEndpoint, + Endpoint(address), + null, + ) + client.send(Buffer.buffer(ping.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() + val datagram = reference.await() + val buffer = ByteBuffer.allocate(datagram.length()) + buffer.put(datagram.bytes) + val pong = Packet.decodeFrom(buffer) as PongPacket + assertEquals(discoveryService.nodeId, pong.nodeId) + assertEquals(ping.hash, pong.pingHash) + + val peer = peerRepository.get(URI("enode://" + clientKeyPair.publicKey().toHexString() + "@127.0.0.1:5678")) + assertNotNull(peer.endpoint) + assertEquals(clientEndpoint.tcpPort, peer.endpoint.tcpPort) + discoveryService.shutdown() + client.close() + } + + @Test + fun shouldPingBootstrapNodeAndValidate(@VertxInstance vertx: Vertx): Unit = runBlocking { + val bootstrapKeyPair = SECP256K1.KeyPair.random() + val reference = AtomicReference>() + reference.set(AsyncResult.incomplete()) + val bootstrapClient = vertx.createDatagramSocket().handler { res -> + reference.get().complete(res.data()) + }.listen(0, "127.0.0.1").coAwait() + + val serviceKeyPair = SECP256K1.KeyPair.random() + val peerRepository = EphemeralPeerRepository() + val routingTable = DevP2PPeerRoutingTable(serviceKeyPair.publicKey()) + val discoveryService = DiscoveryService.open( + vertx, + host = "127.0.0.1", + keyPair = serviceKeyPair, + bootstrapURIs = listOf( + URI( + "enode://" + bootstrapKeyPair.publicKey().toHexString() + "@127.0.0.1:" + bootstrapClient.localAddress() + .port(), + ), + ), + peerRepository = peerRepository, + routingTable = routingTable, + ) + + val datagram = reference.get().await() + val buffer = ByteBuffer.allocate(datagram.length()) + buffer.put(datagram.bytes) + val ping = Packet.decodeFrom(buffer) as PingPacket + assertEquals(discoveryService.nodeId, ping.nodeId) + assertEquals( + ping.to, + Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), + ) + assertEquals(discoveryService.localPort, ping.from.udpPort) + assertNull(ping.from.tcpPort) + + val pong = PongPacket.create( + bootstrapKeyPair, + System.currentTimeMillis(), + ping.from, + ping.hash, + null, + ) + reference.set(AsyncResult.incomplete()) + val address = SocketAddress.inetSocketAddress(discoveryService.localPort, "127.0.0.1") + bootstrapClient.send(Buffer.buffer(pong.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() + + val findNodesDatagram = reference.get().await() + + val findNodes = Packet.decodeFrom(Bytes.wrap(findNodesDatagram.bytes)) as FindNodePacket + assertEquals(discoveryService.nodeId, findNodes.nodeId) + assertEquals(discoveryService.nodeId, findNodes.target) + + val bootstrapPeer = + peerRepository.get( + URI( + "enode://" + bootstrapKeyPair.publicKey().toHexString() + + "@127.0.0.1:" + bootstrapClient.localAddress().port(), + ), + ) + assertNotNull(bootstrapPeer.lastVerified) + assertNotNull(bootstrapPeer.endpoint) + assertEquals(bootstrapClient.localAddress().port(), bootstrapPeer.endpoint.tcpPort) + + assertTrue(routingTable.contains(bootstrapPeer)) + + discoveryService.shutdown() + bootstrapClient.close() + } + + @Test + fun shouldIgnoreBootstrapNodeRespondingWithDifferentNodeId(@VertxInstance vertx: Vertx): Unit = runBlocking { + println("foo") + val bootstrapKeyPair = SECP256K1.KeyPair.random() + val reference = AsyncResult.incomplete() + val bootstrapClient = vertx.createDatagramSocket().handler { res -> + reference.complete(res.data()) + }.listen(0, "localhost").coAwait() + + val serviceKeyPair = SECP256K1.KeyPair.random() + val peerRepository = EphemeralPeerRepository() + val routingTable = DevP2PPeerRoutingTable(serviceKeyPair.publicKey()) + val discoveryService = DiscoveryService.open( + vertx, + host = "127.0.0.1", + keyPair = serviceKeyPair, + bootstrapURIs = listOf( + URI( + "enode://" + bootstrapKeyPair.publicKey().toHexString() + "@127.0.0.1:" + bootstrapClient.localAddress() + .port(), + ), + ), + peerRepository = peerRepository, + routingTable = routingTable, + ) + val datagram = reference.await() + val buffer = ByteBuffer.allocate(datagram.length()) + buffer.put(datagram.bytes) + val ping = Packet.decodeFrom(buffer) as PingPacket + assertEquals(discoveryService.nodeId, ping.nodeId) + assertEquals( + ping.to, + Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), + ) + assertEquals(discoveryService.localPort, ping.from.udpPort) + assertNull(ping.from.tcpPort) + + val pong = PongPacket.create( + SECP256K1.KeyPair.random(), + System.currentTimeMillis(), + ping.from, + ping.hash, + null, + ) + val address = SocketAddress.inetSocketAddress(discoveryService.localPort, "127.0.0.1") + bootstrapClient.send(Buffer.buffer(pong.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() + + delay(1000) + val bootstrapPeer = + peerRepository.get( + URI( + "enode://" + bootstrapKeyPair.publicKey().toHexString() + + "@127.0.0.1:" + bootstrapClient.localAddress().port(), + ), + ) + assertNull(bootstrapPeer.lastVerified) + assertFalse(routingTable.contains(bootstrapPeer)) + + discoveryService.shutdown() + bootstrapClient.close() + } + + @Test + fun shouldPingBootstrapNodeWithAdvertisedAddress(@VertxInstance vertx: Vertx): Unit = runBlocking { + val bootstrapKeyPair = SECP256K1.KeyPair.random() + val reference = AsyncResult.incomplete() + val bootstrapClient = vertx.createDatagramSocket().handler { res -> + reference.complete(res.data()) + }.listen(0, "localhost").coAwait() + + val discoveryService = DiscoveryService.open( + vertx, + host = "127.0.0.1", + keyPair = SECP256K1.KeyPair.random(), + bootstrapURIs = listOf( + URI( + "enode://" + bootstrapKeyPair.publicKey().bytes() + .toHexString() + "@127.0.0.1:" + bootstrapClient.localAddress().port(), + ), + ), + advertiseAddress = "192.168.66.55", + advertiseUdpPort = 3836, + advertiseTcpPort = 8765, + ) + + val datagram = reference.await() + val buffer = ByteBuffer.allocate(datagram.length()) + buffer.put(datagram.bytes) + val ping = Packet.decodeFrom(buffer) as PingPacket + assertEquals(discoveryService.nodeId, ping.nodeId) + assertEquals( + Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), + ping.to, + ) + assertEquals(Endpoint("192.168.66.55", 3836, 8765), ping.from) + + discoveryService.shutdown() + bootstrapClient.close() + } + + @Test + fun shouldRetryPingsToBootstrapNodes(@VertxInstance vertx: Vertx): Unit = runBlocking { + val bootstrapKeyPair = SECP256K1.KeyPair.random() + val reference = AtomicReference>() + reference.set(AsyncResult.incomplete()) + val bootstrapClient = vertx.createDatagramSocket().handler { res -> + reference.get().complete(res.data()) + }.listen(0, "localhost").coAwait() + + val discoveryService = DiscoveryService.open( + vertx, + host = "127.0.0.1", + keyPair = SECP256K1.KeyPair.random(), + bootstrapURIs = listOf( + URI( + "enode://" + bootstrapKeyPair.publicKey().bytes() + .toHexString() + "@127.0.0.1:" + bootstrapClient.localAddress().port(), + ), + ), + ) + val datagram1 = reference.get().await() + reference.set(AsyncResult.incomplete()) + val buffer1 = ByteBuffer.allocate(datagram1.length()) + buffer1.put(datagram1.bytes) + val ping1 = Packet.decodeFrom(buffer1) as PingPacket + assertEquals(discoveryService.nodeId, ping1.nodeId) + assertEquals( + Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), + ping1.to, + ) + val datagram2 = reference.get().await() + reference.set(AsyncResult.incomplete()) + val buffer2 = ByteBuffer.allocate(datagram2.length()) + buffer2.put(datagram2.bytes) + val ping2 = Packet.decodeFrom(buffer2) as PingPacket + assertEquals(discoveryService.nodeId, ping2.nodeId) + assertEquals( + Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), + ping2.to, + ) + val datagram3 = reference.get().await() + reference.set(AsyncResult.incomplete()) + val buffer3 = ByteBuffer.allocate(datagram3.length()) + buffer3.put(datagram3.bytes) + val ping3 = Packet.decodeFrom(buffer3) as PingPacket + assertEquals(discoveryService.nodeId, ping3.nodeId) + assertEquals( + Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), + ping3.to, + ) + discoveryService.shutdown() + bootstrapClient.close() + } + + @Test + fun shouldRequirePingPongBeforeRespondingToFindNodesUnverifiedPeer(@VertxInstance vertx: Vertx): Unit = runBlocking { + val peerRepository = EphemeralPeerRepository() + val discoveryService = DiscoveryService.open( + vertx, + host = "127.0.0.1", + keyPair = SECP256K1.KeyPair.random(), + peerRepository = peerRepository, + ) + discoveryService.awaitBootstrap() + val address = SocketAddress.inetSocketAddress(discoveryService.localPort, "127.0.0.1") + + val clientKeyPair = SECP256K1.KeyPair.random() + val reference = AtomicReference>() + reference.set(AsyncResult.incomplete()) + val client = vertx.createDatagramSocket().handler { res -> + reference.get().complete(res.data()) + }.listen(0, "localhost").coAwait() + val findNodes = + FindNodePacket.create( + clientKeyPair, + System.currentTimeMillis(), + SECP256K1.KeyPair.random().publicKey(), + ) + client.send(Buffer.buffer(findNodes.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() + + val datagram = reference.get().await() + val buffer = ByteBuffer.allocate(datagram.length()) + buffer.put(datagram.bytes) + val ping = Packet.decodeFrom(buffer) as PingPacket + assertEquals(discoveryService.nodeId, ping.nodeId) + + // check it didn't immediately send neighbors + delay(500) + + val pong = PongPacket.create( + clientKeyPair, + System.currentTimeMillis(), + ping.from, + ping.hash, + null, + ) + + reference.set(AsyncResult.incomplete()) + client.send(Buffer.buffer(pong.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() + + val datagram2 = reference.get().await() + val buffer2 = ByteBuffer.allocate(datagram2.length()) + buffer2.put(datagram2.bytes) + val neighbors = Packet.decodeFrom(buffer2) as NeighborsPacket + assertEquals(discoveryService.nodeId, neighbors.nodeId) + + val peer = + peerRepository.get( + URI( + "enode://" + clientKeyPair.publicKey().toHexString() + + "@127.0.0.1:" + discoveryService.localPort, + ), + ) + assertNotNull(peer.lastVerified) + assertNotNull(peer.endpoint) + + discoveryService.shutdown() + client.close() + } + + @Disabled + @Test + fun shouldConnectToNetworkAndDoALookup(@VertxInstance vertx: Vertx) { + /* ktlint-disable */ + val boostrapNodes = listOf( + "enode://6332792c4a00e3e4ee0926ed89e0d27ef985424d97b6a45bf0f23e51f0dcb5e66b875777506458aea7af6f9e4ffb69f43f3778ee73c81ed9d34c51c4b16b0b0f@52.232.243.152:30303" +// "enode://94c15d1b9e2fe7ce56e458b9a3b672ef11894ddedd0c6f247e0f1d3487f52b66208fb4aeb8179fce6e3a749ea93ed147c37976d67af557508d199d9594c35f09@192.81.208.223:30303" + ).map { s -> URI.create(s) } + /* ktlint-enable */ + val discoveryService = DiscoveryService.open( + vertx, + host = "127.0.0.1", + keyPair = SECP256K1.KeyPair.random(), + bootstrapURIs = boostrapNodes, + ) + + runBlocking { + discoveryService.awaitBootstrap() + val result = discoveryService.lookup(SECP256K1.KeyPair.random().publicKey()) + assertTrue(result.isNotEmpty()) + discoveryService.shutdown() + } + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/ENRResponsePacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/ENRResponsePacketTest.kt new file mode 100644 index 000000000..38c5d574c --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/ENRResponsePacketTest.kt @@ -0,0 +1,44 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes32 +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.net.InetAddress + +@ExtendWith(BouncyCastleExtension::class) +internal class ENRResponsePacketTest { + + @Test + fun shouldEncodeThenDecodePacket() { + val keyPair = SECP256K1.KeyPair.random() + + val requestHash = Bytes32.fromRandom() + val enr = EthereumNodeRecord.toRLP( + SECP256K1.KeyPair.random(), + 2, + emptyMap(), + emptyMap(), + InetAddress.getByName("localhost"), + 3000, + 12000, + ) + val now = System.currentTimeMillis() + val pong = ENRResponsePacket.create(keyPair, now, requestHash, enr) + + val datagram = pong.encode() + val packet = Packet.decodeFrom(datagram) + assertTrue(packet is ENRResponsePacket) + + val responsePacket = packet as ENRResponsePacket + assertEquals(keyPair.publicKey(), responsePacket.nodeId) + assertEquals(enr, responsePacket.enr) + assertEquals(requestHash, responsePacket.requestHash) + assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, responsePacket.expiration) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecordTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecordTest.kt new file mode 100644 index 000000000..67ae15b3b --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecordTest.kt @@ -0,0 +1,62 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.extension.ExtendWith +import java.net.InetAddress + +@ExtendWith(BouncyCastleExtension::class) +class EthereumNodeRecordTest { + + @Test + fun tooLong() { + val tooLong = Bytes.random(312) + val exception: IllegalArgumentException = assertThrows { + EthereumNodeRecord.fromRLP(tooLong) + } + assertEquals("Record too long", exception.message) + } + + @Test + fun readFromRLP() { + val keyPair = SECP256K1.KeyPair.random() + val enr = EthereumNodeRecord.create( + keyPair, + 1, + emptyMap(), + emptyMap(), + InetAddress.getLoopbackAddress(), + null, + 10000, + ) + enr.validate() + assertEquals(1L, enr.seq()) + assertEquals(Bytes.wrap("v4".toByteArray()), enr.data["id"]) + assertEquals(Bytes.fromHexString("7f000001"), enr.data["ip"]) + } + + @Test + fun toRLP() { + val keypair = SECP256K1.KeyPair.random() + val rlp = EthereumNodeRecord.toRLP( + keypair, + seq = 1L, + data = mutableMapOf(Pair("key", Bytes.fromHexString("deadbeef"))), + listData = mutableMapOf(Pair("foo", listOf(Bytes.fromHexString("deadbeef")))), + ip = InetAddress.getByName("127.0.0.1"), + ) + val record = EthereumNodeRecord.fromRLP(rlp) + assertEquals(1L, record.seq()) + assertEquals(Bytes.wrap("v4".toByteArray()), record.data["id"]) + assertEquals(Bytes.fromHexString("7f000001"), record.data["ip"]) + assertEquals(keypair.publicKey(), record.publicKey()) + assertEquals(Bytes.fromHexString("deadbeef"), record.data["key"]) + assertEquals(Bytes.fromHexString("deadbeef"), (record.listData["foo"] ?: error("None"))[0]) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/FindNodePacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/FindNodePacketTest.kt new file mode 100644 index 000000000..a3e16aa27 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/FindNodePacketTest.kt @@ -0,0 +1,48 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(BouncyCastleExtension::class) +internal class FindNodePacketTest { + + @Test + fun shouldEncodeThenDecodePacket() { + val keyPair = SECP256K1.KeyPair.random() + val target = SECP256K1.KeyPair.random().publicKey() + val now = System.currentTimeMillis() + val pong = FindNodePacket.create(keyPair, now, target) + + val datagram = pong.encode() + val packet = Packet.decodeFrom(datagram) + assertTrue(packet is FindNodePacket) + + val findNodePacket = packet as FindNodePacket + assertEquals(keyPair.publicKey(), findNodePacket.nodeId) + assertEquals(target, findNodePacket.target) + assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, findNodePacket.expiration) + } + + @Test + fun decodeReferencePacket1() { + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md + val datagram = Bytes.fromHexString( + "c7c44041b9f7c7e41934417ebac9a8e1a4c6298f74553f2fcfdcae6ed6fe53163eb3d2b52e39fe91" + + "831b8a927bf4fc222c3902202027e5e9eb812195f95d20061ef5cd31d502e47ecb61183f74a504fe" + + "04c51e73df81f25c4d506b26db4517490103f84eb840ca634cae0d49acb401d8a4c6b6fe8c55b70d" + + "115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be0081290476" + + "7bf5ccd1fc7f8443b9a35582999983999999280dc62cc8255c73471e0a61da0c89acdc0e035e260a" + + "dd7fc0c04ad9ebf3919644c91cb247affc82b69bd2ca235c71eab8e49737c937a2c396", + ) + val packet = Packet.decodeFrom(datagram) + + assertTrue(packet is FindNodePacket) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/NeighborsPacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/NeighborsPacketTest.kt new file mode 100644 index 000000000..a31d62607 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/NeighborsPacketTest.kt @@ -0,0 +1,66 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(BouncyCastleExtension::class) +internal class NeighborsPacketTest { + + @Test + fun shouldHaveExpectedMinimumSize() { + val packet = + NeighborsPacket.create(SECP256K1.KeyPair.random(), System.currentTimeMillis(), emptyList()) + val buffer = packet.encode() + // the minimum also includes a list length prefix of 4 bytes + assertEquals(NeighborsPacket.RLP_MIN_SIZE, buffer.size() + 4) + } + + @Test + fun shouldEncodeThenDecodePacket() { + val keyPair = SECP256K1.KeyPair.random() + val neighbors = listOf( + Node(Endpoint("10.0.0.54", 6543, 6543), SECP256K1.KeyPair.random().publicKey()), + Node(Endpoint("192.168.34.65", 9832, 1453), SECP256K1.KeyPair.random().publicKey()), + ) + val now = System.currentTimeMillis() + val pong = NeighborsPacket.create(keyPair, now, neighbors) + + val datagram = pong.encode() + val packet = Packet.decodeFrom(datagram) + assertTrue(packet is NeighborsPacket) + + val neighborsPacket = packet as NeighborsPacket + assertEquals(keyPair.publicKey(), neighborsPacket.nodeId) + assertEquals(neighbors, neighborsPacket.nodes) + assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, neighborsPacket.expiration) + } + + @Test + fun decodeReferencePacket1() { + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md + val datagram = Bytes.fromHexString( + "c679fc8fe0b8b12f06577f2e802d34f6fa257e6137a995f6f4cbfc9ee50ed3710faf6e66f932c4c8" + + "d81d64343f429651328758b47d3dbc02c4042f0fff6946a50f4a49037a72bb550f3a7872363a83e1" + + "b9ee6469856c24eb4ef80b7535bcf99c0004f9015bf90150f84d846321163782115c82115db84031" + + "55e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa8291" + + "15d224c523596b401065a97f74010610fce76382c0bf32f84984010203040101b840312c55512422" + + "cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e82" + + "9f04c2d314fc2d4e255e0d3bc08792b069dbf8599020010db83c4d001500000000abcdef12820d05" + + "820d05b84038643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2" + + "d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aacf8599020010db885a308d3" + + "13198a2e037073488203e78203e8b8408dcab8618c3253b558d459da53bd8fa68935a719aff8b811" + + "197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df73" + + "8443b9a355010203b525a138aa34383fec3d2719a0", + ) + val packet = Packet.decodeFrom(datagram) + + assertTrue(packet is NeighborsPacket) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PingPacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PingPacketTest.kt new file mode 100644 index 000000000..26eb5d01b --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PingPacketTest.kt @@ -0,0 +1,107 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(BouncyCastleExtension::class) +internal class PingPacketTest { + + @Test + fun shouldEncodeThenDecodePacket() { + val keyPair = SECP256K1.KeyPair.random() + val from = Endpoint("10.0.0.54", 6543, 6543) + val to = Endpoint("192.168.34.65", 9832, 1453) + val now = System.currentTimeMillis() + val ping = PingPacket.create(keyPair, now, from, to, null) + + val datagram = ping.encode() + val packet = Packet.decodeFrom(datagram) + assertTrue(packet is PingPacket) + + val pingPacket = packet as PingPacket + assertEquals(keyPair.publicKey(), pingPacket.nodeId) + assertEquals(Endpoint("10.0.0.54", 6543, 6543), pingPacket.from) + assertEquals(Endpoint("192.168.34.65", 9832, 1453), pingPacket.to) + assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pingPacket.expiration) + } + + @Test + fun shouldEncodeThenDecodePacketWithEnrSeq() { + val keyPair = SECP256K1.KeyPair.random() + val from = Endpoint("10.0.0.54", 6543, 6543) + val to = Endpoint("192.168.34.65", 9832, 1453) + val now = System.currentTimeMillis() + val ping = PingPacket.create(keyPair, now, from, to, 64) + + val datagram = ping.encode() + val packet = Packet.decodeFrom(datagram) + assertTrue(packet is PingPacket) + + val pingPacket = packet as PingPacket + assertEquals(keyPair.publicKey(), pingPacket.nodeId) + assertEquals(Endpoint("10.0.0.54", 6543, 6543), pingPacket.from) + assertEquals(Endpoint("192.168.34.65", 9832, 1453), pingPacket.to) + assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pingPacket.expiration) + assertEquals(64L, pingPacket.enrSeq) + } + + @Test + fun shouldDecodePingPacketWithMissingEndpoint() { + val keyPair = SECP256K1.KeyPair.random() + val from = Endpoint("10.0.0.54", 6543, 6543) + val to = Endpoint("192.168.34.65", 9832, 1453) + val now = System.currentTimeMillis() + val ping = PingPacket.create(keyPair, now, from, to, null) + + val datagram = ping.encode() + val packet = Packet.decodeFrom(datagram) + assertTrue(packet is PingPacket) + + val pingPacket = packet as PingPacket + assertEquals(keyPair.publicKey(), pingPacket.nodeId) + assertEquals(Endpoint("10.0.0.54", 6543, 6543), pingPacket.from) + assertEquals(Endpoint("192.168.34.65", 9832, 1453), pingPacket.to) + assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pingPacket.expiration) + } + + @Test + fun decodeReferencePacket1() { + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md + val datagram = Bytes.fromHexString( + "e9614ccfd9fc3e74360018522d30e1419a143407ffcce748de3e22116b7e8dc92ff74788c0b6663a" + + "aa3d67d641936511c8f8d6ad8698b820a7cf9e1be7155e9a241f556658c55428ec0563514365799a" + + "4be2be5a685a80971ddcfa80cb422cdd0101ec04cb847f000001820cfa8215a8d790000000000000" + + "000000000000000000018208ae820d058443b9a3550102", + ) + val packet = Packet.decodeFrom(datagram) + + assertTrue(packet is PingPacket) + } + + @Disabled("EIP-868 supercedes EIP-8 behavior") + @Test + fun decodeReferencePacket2() { + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md + val datagram = Bytes.fromHexString( + "577be4349c4dd26768081f58de4c6f375a7a22f3f7adda654d1428637412c3d7fe917cadc56d4e5e" + + "7ffae1dbe3efffb9849feb71b262de37977e7c7a44e677295680e9e38ab26bee2fcbae207fba3ff3" + + "d74069a50b902a82c9903ed37cc993c50001f83e82022bd79020010db83c4d001500000000abcdef" + + "12820cfa8215a8d79020010db885a308d313198a2e037073488208ae82823a8443b9a355c5010203" + + "040531b9019afde696e582a78fa8d95ea13ce3297d4afb8ba6433e4154caa5ac6431af1b80ba7602" + + "3fa4090c408f6b4bc3701562c031041d4702971d102c9ab7fa5eed4cd6bab8f7af956f7d565ee191" + + "7084a95398b6a21eac920fe3dd1345ec0a7ef39367ee69ddf092cbfe5b93e5e568ebc491983c09c7" + + "6d922dc3", + ) + val packet = Packet.decodeFrom(datagram) + + assertTrue(packet is PingPacket) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PongPacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PongPacketTest.kt new file mode 100644 index 000000000..2c8daead8 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PongPacketTest.kt @@ -0,0 +1,73 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p + +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.bytes.Bytes32 +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(BouncyCastleExtension::class) +internal class PongPacketTest { + + @Test + fun shouldEncodeThenDecodePacket() { + val keyPair = SECP256K1.KeyPair.random() + val to = Endpoint("10.0.0.54", 6543, 6543) + val pingHash = Bytes32.fromRandom() + val now = System.currentTimeMillis() + val pong = PongPacket.create(keyPair, now, to, pingHash, null) + + val datagram = pong.encode() + val packet = Packet.decodeFrom(datagram) + assertTrue(packet is PongPacket) + + val pongPacket = packet as PongPacket + assertEquals(keyPair.publicKey(), pongPacket.nodeId) + assertEquals(Endpoint("10.0.0.54", 6543, 6543), pongPacket.to) + assertEquals(pingHash, pongPacket.pingHash) + assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pongPacket.expiration) + } + + @Test + fun shouldEncodeThenDecodePacketWithSeq() { + val keyPair = SECP256K1.KeyPair.random() + val to = Endpoint("10.0.0.54", 6543, 6543) + val pingHash = Bytes32.fromRandom() + val now = System.currentTimeMillis() + val pong = PongPacket.create(keyPair, now, to, pingHash, 32) + + val datagram = pong.encode() + val packet = Packet.decodeFrom(datagram) + assertTrue(packet is PongPacket) + + val pongPacket = packet as PongPacket + assertEquals(keyPair.publicKey(), pongPacket.nodeId) + assertEquals(Endpoint("10.0.0.54", 6543, 6543), pongPacket.to) + assertEquals(pingHash, pongPacket.pingHash) + assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pongPacket.expiration) + assertEquals(32L, pongPacket.enrSeq) + } + + @Disabled("EIP-868 supercedes EIP-8 behavior") + @Test + fun decodeReferencePacket1() { + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md + val datagram = Bytes.fromHexString( + "09b2428d83348d27cdf7064ad9024f526cebc19e4958f0fdad87c15eb598dd61d08423e0bf66b206" + + "9869e1724125f820d851c136684082774f870e614d95a2855d000f05d1648b2d5945470bc187c2d2" + + "216fbe870f43ed0909009882e176a46b0102f846d79020010db885a308d313198a2e037073488208" + + "ae82823aa0fbc914b16819237dcd8801d7e53f69e9719adecb3cc0e790c57e91ca4461c9548443b9" + + "a355c6010203c2040506a0c969a58f6f9095004c0177a6b47f451530cab38966a25cca5cb58f0555" + + "42124e", + ) + val packet = Packet.decodeFrom(datagram) + + assertTrue(packet is PongPacket) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/DefaultDiscoveryV5ServiceTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/DefaultDiscoveryV5ServiceTest.kt new file mode 100644 index 000000000..4cd4f30b2 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/DefaultDiscoveryV5ServiceTest.kt @@ -0,0 +1,67 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import io.vertx.core.Vertx +import io.vertx.core.buffer.Buffer +import io.vertx.kotlin.coroutines.coAwait +import kotlinx.coroutines.runBlocking +import org.apache.tuweni.concurrent.AsyncResult +import org.apache.tuweni.concurrent.coroutines.await +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.junit.VertxExtension +import org.apache.tuweni.junit.VertxInstance +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import org.apache.tuweni.v2.io.Base64URLSafe +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Timeout +import org.junit.jupiter.api.extension.ExtendWith +import java.net.InetAddress +import java.nio.ByteBuffer + +@Timeout(10) +@ExtendWith(BouncyCastleExtension::class, VertxExtension::class) +class DefaultDiscoveryV5ServiceTest { + + private val recipientKeyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random() + private val recipientEnr: Bytes = + EthereumNodeRecord.toRLP(recipientKeyPair, ip = InetAddress.getLoopbackAddress(), udp = 19003) + private val encodedEnr: String = "enr:${Base64URLSafe.encode(recipientEnr)}" + private val keyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random() + private val localPort: Int = 19002 + private val bootstrapENRList: List = listOf(encodedEnr) + + @Test + fun startInitializesConnectorAndBootstraps(@VertxInstance vertx: Vertx): Unit = runBlocking { + val reference = AsyncResult.incomplete() + val client = vertx.createDatagramSocket().handler { res -> + reference.complete(res.data()) + }.listen(19003, "localhost").coAwait() + val discoveryV5Service: DiscoveryV5Service = + DiscoveryService.open( + vertx, + keyPair, + localPort, + bootstrapENRList = bootstrapENRList, + ) + discoveryV5Service.start() + + val datagram = reference.await() + val buffer = ByteBuffer.allocate(datagram.length()) + buffer.put(datagram.bytes) + buffer.flip() + val receivedBytes = Bytes.wrapByteBuffer(buffer) + val content = receivedBytes.slice(45) + + val message = RandomMessage.create( + Message.authTag(), + content, + ) + assertEquals(message.data.size(), Message.RANDOM_DATA_LENGTH) + discoveryV5Service.terminate() + client.close() + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSessionTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSessionTest.kt new file mode 100644 index 000000000..9ee702a7f --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSessionTest.kt @@ -0,0 +1,88 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import io.vertx.core.net.SocketAddress +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.apache.tuweni.concurrent.coroutines.await +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.bytes.MutableBytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.crypto.SECP256K1.PublicKey +import org.apache.tuweni.v2.crypto.SECP256K1.SecretKey +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import org.apache.tuweni.v2.devp2p.v5.encrypt.AES128GCM +import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKeyGenerator +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.net.InetAddress + +@ExtendWith(BouncyCastleExtension::class) +class HandshakeSessionTest { + + @Test + fun testConnectTwoClients() = + runBlocking { + val secretKey = SecretKey.fromBytes(MutableBytes.fromHexString("0x01").leftPad(32)) + val publicKey = PublicKey.fromSecretKey(secretKey) + val keyPair = SECP256K1.KeyPair.create(secretKey, publicKey) + val peerSecretKey = SecretKey.fromBytes(MutableBytes.fromHexString("0x02").leftPad(32)) + val peerPublicKey = PublicKey.fromSecretKey(peerSecretKey) + val peerKeyPair = SECP256K1.KeyPair.create(peerSecretKey, peerPublicKey) + + val address = SocketAddress.inetSocketAddress(1234, "localhost") + val peerAddress = SocketAddress.inetSocketAddress(1235, "localhost") + val enr = EthereumNodeRecord.create(keyPair, ip = InetAddress.getLoopbackAddress(), udp = 1234) + val peerEnr = EthereumNodeRecord.create(peerKeyPair, ip = InetAddress.getLoopbackAddress(), udp = 1235) + var peerSession: HandshakeSession? = null + + val session = + HandshakeSession( + keyPair, + peerAddress, + peerKeyPair.publicKey(), + { _, message -> runBlocking { peerSession!!.processMessage(message) } }, + { enr }, + Dispatchers.Default, + ) + peerSession = + HandshakeSession( + peerKeyPair, + address, + keyPair.publicKey(), + { _, message -> runBlocking { session.processMessage(message) } }, + { peerEnr }, + Dispatchers.Default, + ) + + val key = session.connect().await() + val peerKey = peerSession.awaitConnection().await() + assertEquals(key, peerKey) + } + + @Test + fun testInitiatorAndRecipientKey() { + val keyPair = SECP256K1.KeyPair.random() + val peerKeyPair = SECP256K1.KeyPair.random() + val ephemeralKeyPair = SECP256K1.KeyPair.random() + val enr = EthereumNodeRecord.create(keyPair, ip = InetAddress.getLoopbackAddress(), udp = 1234) + val peerEnr = EthereumNodeRecord.create(peerKeyPair, ip = InetAddress.getLoopbackAddress(), udp = 1235) + val secret = SECP256K1.deriveECDHKeyAgreement(ephemeralKeyPair.secretKey().bytes(), keyPair.publicKey().bytes()) + val nonce = Bytes.random(12) + val session = SessionKeyGenerator.generate(enr.nodeId(), peerEnr.nodeId(), secret, nonce) + val peerSession = SessionKeyGenerator.generate(enr.nodeId(), peerEnr.nodeId(), secret, nonce) + val authTag = Message.authTag() + val token = Message.authTag() + val encryptedMessage = AES128GCM.encrypt( + session.initiatorKey, + authTag, + Bytes.wrap("hello world".toByteArray()), + token, + ) + val decryptedMessage = AES128GCM.decrypt(peerSession.initiatorKey, authTag, encryptedMessage, token) + assertEquals(Bytes.wrap("hello world".toByteArray()), decryptedMessage) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/PeerRoutingTableTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/PeerRoutingTableTest.kt new file mode 100644 index 000000000..16071c3bd --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/PeerRoutingTableTest.kt @@ -0,0 +1,23 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5 + +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.devp2p.DevP2PPeerRoutingTable +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(BouncyCastleExtension::class) +class PeerRoutingTableTest { + + @Test + fun invalidPublicKey() { + val routingTable = DevP2PPeerRoutingTable(SECP256K1.KeyPair.random().publicKey()) + val invalidPublicKey = SECP256K1.PublicKey.fromBytes(Bytes.repeat(0, 64)) + val peers = routingTable.nearest(invalidPublicKey, 3) + assertEquals(0, peers.size) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCMTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCMTest.kt new file mode 100644 index 000000000..c6da864b8 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCMTest.kt @@ -0,0 +1,42 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.encrypt + +import org.apache.tuweni.v2.bytes.Bytes +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class AES128GCMTest { + + @Test + fun encryptPerformsAES128GCMEncryption() { + val expectedResult = Bytes.fromHexString( + "0x943dab6b1f5a0b13e83c41964f818ab8a51d6d30550bae8b33a952aa1b68" + + "18ab88b66dbd60f5e016fa546808d983b70d", + ) + + val key = Bytes.fromHexString("0xA924872EAE2DA2C0057ED6DEBD8CAAB8") + val nonce = Bytes.fromHexString("0x7FC4FDB0E50ACBDA9CD993CFD3A3752104935B91F61B2AF2602C2DC4EFD97AFB") + val data = Bytes.fromHexString("0x19F23925525AF4C2697C1BED166EEB37B5381C10E508A27BCAA02CE661E62A2B") + + val result = AES128GCM.encrypt(key, nonce, data, Bytes.EMPTY) + + assertEquals(expectedResult, result) + } + + @Test + fun decryptPerformsAES128GCMDecryption() { + val expectedResult = Bytes.fromHexString("0x19F23925525AF4C2697C1BED166EEB37B5381C10E508A27BCAA02CE661E62A2B") + val nonce = Bytes.fromHexString("0x7FC4FDB0E50ACBDA9CD993CFD3A3752104935B91F61B2AF2602C2DC4EFD97AFB") + + val encryptedData = Bytes.fromHexString( + "0x943dab6b1f5a0b13e83c41964f818ab8a51d6d30550bae8b33a952aa1b6818a" + + "b88b66dbd60f5e016fa546808d983b70d", + ) + val key = Bytes.fromHexString("0xA924872EAE2DA2C0057ED6DEBD8CAAB8") + + val result = AES128GCM.decrypt(key, nonce, encryptedData, Bytes.EMPTY) + + assertEquals(expectedResult, result) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt new file mode 100644 index 000000000..f97fc967e --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt @@ -0,0 +1,28 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.encrypt + +import org.apache.tuweni.v2.bytes.Bytes +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class SessionKeyGeneratorTest { + + @Test + fun generateCreatesSessionKey() { + val expectedAuthRespKey = Bytes.fromHexString("0xDC999F3F7EF11907F6762497476117C9") + val expectedInitiatorKey = Bytes.fromHexString("0xBBBE757DCE9687BBE5E90CBF9C776163") + val expectedRecipientKey = Bytes.fromHexString("0xE83FC3ED3B32DEE7D81D706FECA6174F") + + val srcNodeId = Bytes.fromHexString("0x9CE70B8F317791EB4E775FF9314B9B7B2CD01D90FF5D0E1979B2EBEB92DCB48D") + val destNodeId = Bytes.fromHexString("0x0B1E82724DB4D17089EF64A441A2C367683EAC448E6AB7F6F8B3094D2B1B2229") + val secret = Bytes.fromHexString("0xAB285AD41C712A917DAC83DE8AAD963285067ED84BAC37052A32BB74DCC75AA5") + val idNonce = Bytes.fromHexString("0x630222D6CD1253BF40CB800F230759F117EC1890CD76792135BBC4D7AAD0B4C1") + + val result = SessionKeyGenerator.generate(srcNodeId, destNodeId, secret, idNonce) + + assertEquals(result.authRespKey, expectedAuthRespKey) + assertEquals(result.initiatorKey, expectedInitiatorKey) + assertEquals(result.recipientKey, expectedRecipientKey) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/FindNodeMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/FindNodeMessageTest.kt new file mode 100644 index 000000000..0b5dc42ce --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/FindNodeMessageTest.kt @@ -0,0 +1,27 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.devp2p.v5.FindNodeMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class FindNodeMessageTest { + + @Test + fun encodeCreatesValidBytesSequence() { + val expectedEncodingResult = "0xca88c6e32c5e89caa75480" + + val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") + val message = FindNodeMessage(requestId) + + val encodingResult = message.toRLP() + assertEquals(encodingResult.toHexString(), expectedEncodingResult) + + val decodingResult = FindNodeMessage.create(encodingResult) + + assertEquals(decodingResult.requestId, requestId) + assertEquals(decodingResult.distance, 0) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/MessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/MessageTest.kt new file mode 100644 index 000000000..9d7d1c037 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/MessageTest.kt @@ -0,0 +1,66 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.devp2p.v5.Message +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test + +class MessageTest { + + @Test + fun magicCreatesSha256OfDestNodeIdAndConstantString() { + val destId = Bytes.fromHexString("0xA5CFE10E0EFC543CBE023560B2900E2243D798FAFD0EA46267DDD20D283CE13C") + val expected = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640") + + val result = Message.magic(destId) + + assertEquals(expected, result) + } + + @Test + fun tagHashesSourceAndDestNodeIdCorrectly() { + val srcId = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640") + val destId = Bytes.fromHexString("0xA5CFE10E0EFC543CBE023560B2900E2243D798FAFD0EA46267DDD20D283CE13C") + val expected = Bytes.fromHexString("0xB7A0D7CA8BD37611315DA0882FF479DE14B442FD30AE0EFBE6FC6344D55DC632") + + val result = Message.tag(srcId, destId) + + assertEquals(expected, result) + } + + @Test + fun getSourceFromTagFetchesSrcNodeId() { + val srcId = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640") + val destId = Bytes.fromHexString("0xA5CFE10E0EFC543CBE023560B2900E2243D798FAFD0EA46267DDD20D283CE13C") + val tag = Message.tag(srcId, destId) + + val result = Message.getSourceFromTag(tag, destId) + + assertEquals(srcId, result) + } + + @Test + fun authTagGivesRandom12Bytes() { + val firstResult = Message.authTag() + + assertEquals(Message.AUTH_TAG_LENGTH, firstResult.size()) + + val secondResult = Message.authTag() + + assertNotEquals(secondResult, firstResult) + } + + @Test + fun idNonceGivesRandom32Bytes() { + val firstResult = Message.idNonce() + + assertEquals(Message.ID_NONCE_LENGTH, firstResult.size()) + + val secondResult = Message.idNonce() + + assertNotEquals(secondResult, firstResult) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/NodesMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/NodesMessageTest.kt new file mode 100644 index 000000000..c326fe002 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/NodesMessageTest.kt @@ -0,0 +1,39 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import org.apache.tuweni.v2.devp2p.v5.NodesMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import java.net.InetAddress + +@ExtendWith(BouncyCastleExtension::class) +class NodesMessageTest { + + @Test + fun encodeCreatesValidBytesSequence() { + val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") + val total = 10 + val nodeRecords = listOf( + EthereumNodeRecord.create(SECP256K1.KeyPair.random(), ip = InetAddress.getLoopbackAddress(), udp = 9090), + EthereumNodeRecord.create(SECP256K1.KeyPair.random(), ip = InetAddress.getLoopbackAddress(), udp = 9091), + EthereumNodeRecord.create(SECP256K1.KeyPair.random(), ip = InetAddress.getLoopbackAddress(), udp = 9092), + ) + val message = NodesMessage(requestId, total, nodeRecords) + + val encodingResult = message.toRLP() + + val decodingResult = NodesMessage.create(encodingResult) + + assertEquals(decodingResult.requestId, requestId) + assertEquals(decodingResult.total, 10) + assertEquals(decodingResult.nodeRecords[0].udp(), 9090) + assertEquals(decodingResult.nodeRecords[1].udp(), 9091) + assertEquals(decodingResult.nodeRecords[2].udp(), 9092) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PingMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PingMessageTest.kt new file mode 100644 index 000000000..79b17afbc --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PingMessageTest.kt @@ -0,0 +1,24 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.devp2p.v5.PingMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class PingMessageTest { + + @Test + fun encodeCreatesValidBytesSequence() { + val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") + val message = PingMessage(requestId) + + val encodingResult = message.toRLP() + + val decodingResult = PingMessage.create(encodingResult) + + assertEquals(decodingResult.requestId, requestId) + assertEquals(decodingResult.enrSeq, message.enrSeq) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PongMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PongMessageTest.kt new file mode 100644 index 000000000..77b779905 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PongMessageTest.kt @@ -0,0 +1,26 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.devp2p.v5.PongMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class PongMessageTest { + + @Test + fun encodeCreatesValidBytesSequence() { + val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") + val message = PongMessage(requestId, 0, "127.0.0.1", 9090) + + val encodingResult = message.toRLP() + + val decodingResult = PongMessage.create(encodingResult) + + assertEquals(decodingResult.requestId, requestId) + assertEquals(decodingResult.enrSeq, message.enrSeq) + assertEquals(decodingResult.recipientIp, message.recipientIp) + assertEquals(decodingResult.recipientPort, message.recipientPort) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RandomMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RandomMessageTest.kt new file mode 100644 index 000000000..123e4dab3 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RandomMessageTest.kt @@ -0,0 +1,40 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.devp2p.v5.Message +import org.apache.tuweni.v2.devp2p.v5.RandomMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Test + +class RandomMessageTest { + + @Test + fun encodeCreatesValidBytesSequence() { + val expectedEncodingResult = + "0xb53ccf732982b8e950836d1e02898c8b38cfdbfdf86bc65c8826506b454e14618ea73612a0f5582c130ff666" + + val data = Bytes.fromHexString(expectedEncodingResult) + val message = RandomMessage(Message.authTag(), data) + + val encodingResult = message.toRLP() + assertEquals(encodingResult.toHexString(), expectedEncodingResult) + + val decodingResult = RandomMessage.create(Message.authTag(), encodingResult) + + assertEquals(decodingResult.data, data) + } + + @Test + fun randomDataGivesRandom44Bytes() { + val firstResult = RandomMessage.randomData() + + assertEquals(Message.RANDOM_DATA_LENGTH, firstResult.size()) + + val secondResult = RandomMessage.randomData() + + assertNotEquals(secondResult, firstResult) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegConfirmationMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegConfirmationMessageTest.kt new file mode 100644 index 000000000..835cde810 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegConfirmationMessageTest.kt @@ -0,0 +1,25 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.bytes.Bytes32 +import org.apache.tuweni.v2.devp2p.v5.RegConfirmationMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class RegConfirmationMessageTest { + + @Test + fun encodeCreatesValidBytesSequence() { + val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") + val message = RegConfirmationMessage(requestId, Bytes32.fromRandom()) + + val encodingResult = message.toRLP() + + val decodingResult = RegConfirmationMessage.create(encodingResult) + + assertEquals(decodingResult.requestId, requestId) + assertEquals(decodingResult.topic, message.topic) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegTopicMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegTopicMessageTest.kt new file mode 100644 index 000000000..0f4794180 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegTopicMessageTest.kt @@ -0,0 +1,35 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.bytes.Bytes32 +import org.apache.tuweni.v2.crypto.SECP256K1 +import org.apache.tuweni.v2.devp2p.EthereumNodeRecord +import org.apache.tuweni.v2.devp2p.v5.RegTopicMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.net.InetAddress + +class RegTopicMessageTest { + + @Test + fun encodeCreatesValidBytesSequence() { + val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") + val message = + RegTopicMessage( + requestId, + EthereumNodeRecord.create(SECP256K1.KeyPair.random(), ip = InetAddress.getLoopbackAddress()), + Bytes32.fromRandom(), + Bytes.random(16), + ) + + val encodingResult = message.toRLP() + + val decodingResult = RegTopicMessage.create(encodingResult) + + assertEquals(decodingResult.requestId, requestId) + assertEquals(decodingResult.ticket, message.ticket) + assertEquals(decodingResult.nodeRecord, message.nodeRecord) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TicketMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TicketMessageTest.kt new file mode 100644 index 000000000..9929c8e5b --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TicketMessageTest.kt @@ -0,0 +1,26 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.bytes.Bytes32 +import org.apache.tuweni.v2.devp2p.v5.TicketMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TicketMessageTest { + + @Test + fun encodeCreatesValidBytesSequence() { + val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") + val message = TicketMessage(requestId, Bytes32.fromRandom(), 1000) + + val encodingResult = message.toRLP() + + val decodingResult = TicketMessage.create(encodingResult) + + assertEquals(decodingResult.requestId, requestId) + assertEquals(decodingResult.ticket, message.ticket) + assertEquals(decodingResult.waitTime, message.waitTime) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TopicQueryMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TopicQueryMessageTest.kt new file mode 100644 index 000000000..facc61285 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TopicQueryMessageTest.kt @@ -0,0 +1,25 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.bytes.Bytes32 +import org.apache.tuweni.v2.devp2p.v5.TopicQueryMessage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TopicQueryMessageTest { + + @Test + fun encodeCreatesValidBytesSequence() { + val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") + val message = TopicQueryMessage(requestId, Bytes32.fromRandom()) + + val encodingResult = message.toRLP() + + val decodingResult = TopicQueryMessage.create(encodingResult) + + assertEquals(decodingResult.requestId, requestId) + assertEquals(decodingResult.topic, message.topic) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/WhoAreYouMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/WhoAreYouMessageTest.kt new file mode 100644 index 000000000..592be8493 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/WhoAreYouMessageTest.kt @@ -0,0 +1,23 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.packet + +import org.apache.tuweni.junit.BouncyCastleExtension +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.devp2p.v5.WhoAreYouMessage +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(BouncyCastleExtension::class) +class WhoAreYouMessageTest { + + @Test + fun decodeSelf() { + val bytes = + Bytes.fromHexString( + "0x282E641D415A892C05FD03F0AE716BDD92D1569116FDC7C7D3DB39AC5F79B0F7EF8C" + + "E56EDC7BB967899B4C48EEA6A0E838C9091B71DADB98C59508306275AE37A1916EF2517E77CFE09FA006909FE880", + ) + WhoAreYouMessage.create(magic = bytes.slice(0, 32), content = bytes.slice(32)) + } +} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TicketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TicketTest.kt new file mode 100644 index 000000000..53f88e365 --- /dev/null +++ b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TicketTest.kt @@ -0,0 +1,20 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.devp2p.v5.topic + +import org.apache.tuweni.v2.bytes.Bytes +import org.apache.tuweni.v2.bytes.Bytes32 +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TicketTest { + + @Test + fun roundtrip() { + val ticket = + Ticket(Bytes.wrap("hello world".toByteArray()), Bytes32.fromRandom(), "127.0.0.1", 0L, 0L, 0L) + val key = Bytes.random(16) + val encrypted = ticket.encrypt(key) + assertEquals(Ticket.decrypt(encrypted, key), ticket) + } +} 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/rlp/src/main/java/org/apache/tuweni/v2/rlp/AccumulatingRLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/AccumulatingRLPWriter.java new file mode 100644 index 000000000..6d336d2ff --- /dev/null +++ b/rlp/src/main/java/org/apache/tuweni/v2/rlp/AccumulatingRLPWriter.java @@ -0,0 +1,87 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import static java.util.Objects.requireNonNull; +import static org.apache.tuweni.v2.rlp.RLP.encodeByteArray; +import static org.apache.tuweni.v2.rlp.RLP.encodeLength; +import static org.apache.tuweni.v2.rlp.RLP.encodeNumber; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.function.Consumer; + +final class AccumulatingRLPWriter implements RLPWriter { + + private static final int COMBINE_THRESHOLD = 32; + + private ArrayDeque values = new ArrayDeque<>(); + + Deque values() { + return values; + } + + @Override + public void writeRLP(Bytes value) { + requireNonNull(value); + appendBytes(value.toArrayUnsafe()); + } + + @Override + public void writeValue(Bytes value) { + requireNonNull(value); + writeByteArray(value.toArrayUnsafe()); + } + + @Override + public void writeByteArray(byte[] value) { + encodeByteArray(value, this::appendBytes); + } + + @Override + public void writeByte(byte value) { + encodeByteArray(new byte[] {value}, this::appendBytes); + } + + @Override + public void writeLong(long value) { + appendBytes(encodeNumber(value)); + } + + @Override + public void writeList(Consumer fn) { + requireNonNull(fn); + AccumulatingRLPWriter listWriter = new AccumulatingRLPWriter(); + fn.accept(listWriter); + int totalSize = 0; + for (byte[] value : listWriter.values) { + try { + totalSize = Math.addExact(totalSize, value.length); + } catch (ArithmeticException e) { + throw new IllegalArgumentException( + "Combined length of values is too long (> Integer.MAX_VALUE)"); + } + } + appendBytes(encodeLength(totalSize, 0xc0)); + this.values.addAll(listWriter.values); + } + + private void appendBytes(byte[] bytes) { + if (bytes.length < COMBINE_THRESHOLD) { + if (!values.isEmpty()) { + byte[] last = values.getLast(); + if (last.length <= (COMBINE_THRESHOLD - bytes.length)) { + byte[] combined = new byte[last.length + bytes.length]; + System.arraycopy(last, 0, combined, 0, last.length); + System.arraycopy(bytes, 0, combined, last.length, bytes.length); + values.pollLast(); + values.add(combined); + return; + } + } + } + values.add(bytes); + } +} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/ByteBufferRLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/ByteBufferRLPWriter.java new file mode 100644 index 000000000..4eca78f5a --- /dev/null +++ b/rlp/src/main/java/org/apache/tuweni/v2/rlp/ByteBufferRLPWriter.java @@ -0,0 +1,70 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import static java.util.Objects.requireNonNull; +import static org.apache.tuweni.v2.rlp.RLP.encodeByteArray; +import static org.apache.tuweni.v2.rlp.RLP.encodeLength; +import static org.apache.tuweni.v2.rlp.RLP.encodeNumber; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Deque; +import java.util.function.Consumer; + +final class ByteBufferRLPWriter implements RLPWriter { + + private ByteBuffer buffer; + + ByteBufferRLPWriter(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public void writeRLP(Bytes value) { + buffer.put(value.toArrayUnsafe()); + } + + @Override + public void writeValue(Bytes value) { + encodeByteArray(value.toArrayUnsafe(), buffer::put); + } + + @Override + public void writeByteArray(byte[] value) { + encodeByteArray(value, buffer::put); + } + + @Override + public void writeByte(byte value) { + encodeByteArray(new byte[] {value}, buffer::put); + } + + @Override + public void writeLong(long value) { + buffer.put(encodeNumber(value)); + } + + @Override + public void writeList(Consumer fn) { + requireNonNull(fn); + AccumulatingRLPWriter listWriter = new AccumulatingRLPWriter(); + fn.accept(listWriter); + writeEncodedValuesAsList(listWriter.values()); + } + + private void writeEncodedValuesAsList(Deque values) { + int totalSize = 0; + for (byte[] value : values) { + try { + totalSize = Math.addExact(totalSize, value.length); + } catch (ArithmeticException e) { + throw new BufferOverflowException(); + } + } + buffer.put(encodeLength(totalSize, 0xc0)); + values.forEach(bytes -> buffer.put(bytes)); + } +} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPReader.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPReader.java new file mode 100644 index 000000000..b54f77707 --- /dev/null +++ b/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPReader.java @@ -0,0 +1,313 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import org.apache.tuweni.rlp.EndOfRLPException; +import org.apache.tuweni.rlp.InvalidRLPEncodingException; +import org.apache.tuweni.rlp.InvalidRLPTypeException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.function.Function; + +final class BytesRLPReader implements RLPReader { + + private final Bytes content; + private boolean lenient; + private int index = 0; + + BytesRLPReader(Bytes content, boolean lenient) { + this.content = content; + this.lenient = lenient; + } + + @Override + public boolean isLenient() { + return lenient; + } + + @Override + public Bytes readRemaining() { + int remaining = content.size() - index; + if (remaining == 0) { + return Bytes.EMPTY; + } + return content.slice(index++, remaining); + } + + @Override + public Bytes readValue(boolean lenient) { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + if (prefix <= 0x7f) { + return content.slice(index++, 1); + } + remaining--; + + if (prefix <= 0xb7) { + int length = prefix - 0x80; + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + length + + " but have only " + + remaining); + } + Bytes bytes = content.slice(index + 1, length); + if (!lenient && length == 1 && (bytes.get(0) & 0xFF) <= 0x7f) { + throw new InvalidRLPEncodingException( + "Value should have been encoded as a single byte " + bytes.toHexString()); + } + index += 1 + length; + return bytes; + } + if (prefix <= 0xbf) { + int lengthOfLength = prefix - 0xb7; + if (remaining < lengthOfLength) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + lengthOfLength + + " but have only " + + remaining); + } + + remaining -= lengthOfLength; + int length = getLength(lengthOfLength, lenient, "value"); + + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + length + + " but have only " + + remaining); + } + + index += 1 + lengthOfLength; + Bytes bytes = content.slice(index, length); + index += length; + return bytes; + } + throw new InvalidRLPTypeException("Attempted to read a value but next item is a list"); + } + + @Override + public boolean nextIsList() { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + return prefix > 0xbf; + } + + @Override + public boolean nextIsEmpty() { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + return prefix == 0x80; + } + + @Override + public T readList(boolean lenient, Function fn) { + return fn.apply(new BytesRLPReader(readList(lenient), lenient)); + } + + @Override + public void skipNext(boolean lenient) { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + if (prefix <= 0x7f) { + index++; + return; + } + remaining--; + + if (prefix <= 0xb7) { + int length = prefix - 0x80; + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + length + + " but have only " + + remaining); + } + if (!lenient && length == 1 && (content.get(index + 1) & 0xFF) <= 0x7f) { + throw new InvalidRLPEncodingException( + "Value should have been encoded as a single byte " + + content.slice(index + 1, 1).toHexString()); + } + index += 1 + length; + return; + } + if (prefix <= 0xbf) { + int lengthOfLength = prefix - 0xb7; + if (remaining < lengthOfLength) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + lengthOfLength + + " but have only " + + remaining); + } + + remaining -= lengthOfLength; + int length = getLength(lengthOfLength, lenient, "value"); + + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + length + + " but have only " + + remaining); + } + + index += 1 + lengthOfLength + length; + return; + } + if (prefix <= 0xf7) { + int length = prefix - 0xc0; + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + length + + " but have only " + + remaining); + } + index += 1 + length; + return; + } + + int lengthOfLength = prefix - 0xf7; + if (remaining < lengthOfLength) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + lengthOfLength + + " but have only " + + remaining); + } + + remaining -= lengthOfLength; + int length = getLength(lengthOfLength, lenient, "list"); + + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + lengthOfLength + + " but have only " + + remaining); + } + + index += 1 + lengthOfLength + length; + } + + @Override + public int remaining() { + int oldIndex = index; + try { + int count = 0; + while (!isComplete()) { + count++; + skipNext(); + } + return count; + } finally { + index = oldIndex; + } + } + + @Override + public boolean isComplete() { + return (content.size() - index) == 0; + } + + @Override + public int position() { + return index; + } + + private Bytes readList(boolean lenient) { + int remaining = content.size() - index; + if (remaining == 0) { + throw new EndOfRLPException(); + } + int prefix = (((int) content.get(index)) & 0xFF); + if (prefix <= 0xbf) { + throw new InvalidRLPTypeException("Attempted to read a list but next item is a value"); + } + remaining--; + + if (prefix <= 0xf7) { + int length = prefix - 0xc0; + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + length + + " but have only " + + remaining); + } + index++; + Bytes bytes = content.slice(index, length); + index += length; + return bytes; + } + + int lengthOfLength = prefix - 0xf7; + if (remaining < lengthOfLength) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + + lengthOfLength + + " but have only " + + remaining); + } + + remaining -= lengthOfLength; + int length = getLength(lengthOfLength, lenient, "list"); + + if (remaining < length) { + throw new InvalidRLPEncodingException( + "Insufficient bytes in RLP encoding: expected " + length + " but have only " + remaining); + } + + index += 1 + lengthOfLength; + Bytes bytes = content.slice(index, length); + index += length; + return bytes; + } + + private int getLength(int lengthOfLength, boolean lenient, String type) { + Bytes lengthBytes = content.slice(index + 1, lengthOfLength); + if (!lenient) { + if (lengthBytes.hasLeadingZeroByte()) { + throw new InvalidRLPEncodingException( + "RLP " + type + " length contains leading zero bytes"); + } + } else { + lengthBytes = lengthBytes.trimLeadingZeros(); + } + if (lengthBytes.size() == 0) { + throw new InvalidRLPEncodingException("RLP " + type + " length is zero"); + } + // Check if the length is greater than a 4 byte integer + if (lengthBytes.size() > 4) { + throw new InvalidRLPEncodingException("RLP " + type + " length is oversized"); + } + int length = lengthBytes.toInt(); + if (length < 0) { + // Java ints are two's compliment, so this was oversized + throw new InvalidRLPEncodingException("RLP " + type + " length is oversized"); + } + assert length > 0; + if (!lenient && length <= 55) { + throw new InvalidRLPEncodingException( + "RLP " + type + " length of " + length + " was not minimally encoded"); + } + return length; + } +} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPWriter.java new file mode 100644 index 000000000..edbe5eeb9 --- /dev/null +++ b/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPWriter.java @@ -0,0 +1,22 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.Deque; + +final class BytesRLPWriter extends DelegatingRLPWriter { + + BytesRLPWriter() { + super(new AccumulatingRLPWriter()); + } + + Bytes toBytes() { + Deque values = delegate.values(); + if (values.isEmpty()) { + return Bytes.EMPTY; + } + return Bytes.wrap(values.stream().map(Bytes::wrap).toArray(Bytes[]::new)); + } +} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/DelegatingRLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/DelegatingRLPWriter.java new file mode 100644 index 000000000..f0c9ba3b0 --- /dev/null +++ b/rlp/src/main/java/org/apache/tuweni/v2/rlp/DelegatingRLPWriter.java @@ -0,0 +1,68 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; + +import java.math.BigInteger; +import java.util.function.Consumer; + +class DelegatingRLPWriter implements RLPWriter { + + T delegate; + + DelegatingRLPWriter(T delegate) { + this.delegate = delegate; + } + + @Override + public void writeRLP(Bytes value) { + delegate.writeRLP(value); + } + + @Override + public void writeValue(Bytes value) { + delegate.writeValue(value); + } + + @Override + public void writeByteArray(byte[] value) { + delegate.writeByteArray(value); + } + + @Override + public void writeByte(byte value) { + delegate.writeByte(value); + } + + @Override + public void writeInt(int value) { + delegate.writeInt(value); + } + + @Override + public void writeLong(long value) { + delegate.writeLong(value); + } + + @Override + public void writeUInt256(UInt256 value) { + delegate.writeUInt256(value); + } + + @Override + public void writeBigInteger(BigInteger value) { + delegate.writeBigInteger(value); + } + + @Override + public void writeString(String str) { + delegate.writeString(str); + } + + @Override + public void writeList(Consumer fn) { + delegate.writeList(fn); + } +} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLP.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLP.java new file mode 100644 index 000000000..af4e1a96c --- /dev/null +++ b/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLP.java @@ -0,0 +1,528 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.rlp.EndOfRLPException; +import org.apache.tuweni.rlp.InvalidRLPEncodingException; +import org.apache.tuweni.rlp.InvalidRLPTypeException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.math.BigInteger; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +/** Recursive Length Prefix (RLP) encoding and decoding. */ +public final class RLP { + private static final byte[] EMPTY_VALUE = new byte[] {(byte) 0x80}; + + private RLP() {} + + /** + * Encode values to a {@link Bytes} value. + * + *

Important: this method does not write any list prefix to the result. If you are writing a + * RLP encoded list of values, you usually want to use {@link #encodeList(Consumer)}. + * + * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encode(Consumer fn) { + requireNonNull(fn); + BytesRLPWriter writer = new BytesRLPWriter(); + fn.accept(writer); + return writer.toBytes(); + } + + /** + * Encode values to a {@link ByteBuffer}. + * + *

Important: this method does not write any list prefix to the result. If you are writing a + * RLP encoded list of values, you usually want to use {@link #encodeList(Consumer)}. + * + * @param buffer The buffer to write into, starting from its current position. + * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. + * @param The type of the buffer. + * @return The buffer. + * @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 static T encodeTo(T buffer, Consumer fn) { + requireNonNull(fn); + ByteBufferRLPWriter writer = new ByteBufferRLPWriter(buffer); + fn.accept(writer); + return buffer; + } + + /** + * Encode a list of values to a {@link Bytes} value. + * + * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeList(Consumer fn) { + requireNonNull(fn); + BytesRLPWriter writer = new BytesRLPWriter(); + writer.writeList(fn); + return writer.toBytes(); + } + + /** + * Encode a list of values to a {@link Bytes} value. + * + * @param elements A list of values to be encoded. + * @param fn A consumer that will be provided with a {@link RLPWriter} and an element of the list. + * @param The type of the list elements. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeList(List elements, BiConsumer fn) { + requireNonNull(fn); + BytesRLPWriter writer = new BytesRLPWriter(); + writer.writeList(elements, fn); + return writer.toBytes(); + } + + /** + * Encode a list of values to a {@link ByteBuffer}. + * + * @param buffer The buffer to write into, starting from its current position. + * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. + * @param The type of the buffer. + * @return The buffer. + * @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 static T encodeListTo(T buffer, Consumer fn) { + requireNonNull(fn); + ByteBufferRLPWriter writer = new ByteBufferRLPWriter(buffer); + writer.writeList(fn); + return buffer; + } + + /** + * Encode a value to a {@link Bytes} value. + * + * @param value The value to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeValue(Bytes value) { + requireNonNull(value); + return encodeValue(value.toArrayUnsafe()); + } + + /** + * Encode a value to a {@link Bytes} value. + * + * @param value The value to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeByteArray(byte[] value) { + requireNonNull(value); + return encodeValue(value); + } + + private static Bytes encodeValue(byte[] value) { + int maxSize = value.length + 5; + ByteBuffer buffer = ByteBuffer.allocate(maxSize); + encodeByteArray(value, buffer::put); + return Bytes.wrap(buffer.array(), 0, buffer.position()); + } + + static void encodeByteArray(byte[] value, Consumer appender) { + requireNonNull(value); + int size = value.length; + if (size == 0) { + appender.accept(EMPTY_VALUE); + return; + } + if (size == 1) { + byte b = value[0]; + if ((b & 0xFF) <= 0x7f) { + appender.accept(value); + return; + } + } + appender.accept(encodeLength(size, 0x80)); + appender.accept(value); + } + + /** + * Encode a integer to a {@link Bytes} value. + * + * @param value The integer to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeInt(int value) { + return encodeLong(value); + } + + /** + * Encode a long to a {@link Bytes} value. + * + * @param value The long to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeLong(long value) { + return Bytes.wrap(encodeNumber(value)); + } + + static byte[] encodeNumber(long value) { + if (value == 0x00) { + return EMPTY_VALUE; + } + if (value <= 0x7f) { + return new byte[] {(byte) (value & 0xFF)}; + } + return encodeLongBytes(value, 0x80); + } + + private static byte[] encodeLongBytes(long value, int offset) { + int zeros = Long.numberOfLeadingZeros(value); + int resultBytes = 8 - (zeros / 8); + + byte[] encoded = new byte[resultBytes + 1]; + encoded[0] = (byte) ((offset + resultBytes) & 0xFF); + + int shift = 0; + for (int i = 0; i < resultBytes; i++) { + encoded[resultBytes - i] = (byte) ((value >> shift) & 0xFF); + shift += 8; + } + return encoded; + } + + /** + * Encode a big integer to a {@link Bytes} value. + * + * @param value The big integer to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeBigInteger(BigInteger value) { + requireNonNull(value); + return encode(writer -> writer.writeBigInteger(value)); + } + + /** + * Encode a string to a {@link Bytes} value. + * + * @param str The string to encode. + * @return The RLP encoding in a {@link Bytes} value. + */ + public static Bytes encodeString(String str) { + requireNonNull(str); + return encodeByteArray(str.getBytes(UTF_8)); + } + + static byte[] encodeLength(int length, int offset) { + if (length <= 55) { + return new byte[] {(byte) ((offset + length) & 0xFF)}; + } + return encodeLongBytes(length, offset + 55); + } + + /** + * Read and decode RLP from a {@link Bytes} value. + * + *

Important: this method does not consume any list prefix from the source data. If you are + * reading a RLP encoded list of values, you usually want to use {@link #decodeList(Bytes, + * Function)}. + * + * @param source The RLP encoded bytes. + * @param fn A function that will be provided a {@link RLPReader}. + * @param The result type of the reading function. + * @return The result from the reading function. + */ + public static T decode(Bytes source, Function fn) { + return decode(source, false, fn); + } + + /** + * Read and decode RLP from a {@link Bytes} value. + * + *

Important: this method does not consume any list prefix from the source data. If you are + * reading a RLP encoded list of values, you usually want to use {@link #decodeList(Bytes, + * Function)}. + * + * @param source The RLP encoded bytes. + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @param fn A function that will be provided a {@link RLPReader}. + * @param The result type of the reading function. + * @return The result from the reading function. + */ + public static T decode(Bytes source, boolean lenient, Function fn) { + requireNonNull(source); + requireNonNull(fn); + return fn.apply(new BytesRLPReader(source, lenient)); + } + + /** + * Read an RLP encoded list of values from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @param fn A function that will be provided a {@link RLPReader}. + * @param The result type of the reading function. + * @return The result from the reading function. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the first RLP value is not a list. + */ + public static T decodeList(Bytes source, Function fn) { + return decodeList(source, false, fn); + } + + /** + * Read an RLP encoded list of values from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @param fn A function that will be provided a {@link RLPReader}. + * @param The result type of the reading function. + * @return The result from the reading function. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the first RLP value is not a list. + */ + public static T decodeList(Bytes source, boolean lenient, Function fn) { + requireNonNull(source); + requireNonNull(fn); + if (source.isEmpty()) { + throw new IllegalArgumentException("source is empty"); + } + return decode(source, lenient, reader -> reader.readList(fn)); + } + + /** + * Read an RLP encoded list of values from a {@link Bytes} value, populating a mutable output + * list. + * + * @param source The RLP encoded bytes. + * @param fn A function that will be provided a {@link RLPReader}. + * @return The list supplied to {@code fn}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the first RLP value is not a list. + */ + public static List decodeToList(Bytes source, BiConsumer> fn) { + return decodeToList(source, false, fn); + } + + /** + * Read an RLP encoded list of values from a {@link Bytes} value, populating a mutable output + * list. + * + * @param source The RLP encoded bytes. + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @param fn A function that will be provided a {@link RLPReader}. + * @return The list supplied to {@code fn}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the first RLP value is not a list. + */ + public static List decodeToList( + Bytes source, boolean lenient, BiConsumer> fn) { + requireNonNull(source); + requireNonNull(fn); + if (source.isEmpty()) { + throw new IllegalArgumentException("source is empty"); + } + return decode(source, lenient, reader -> reader.readList(fn)); + } + + /** + * Read a list of values from the RLP source, populating a list using a function interpreting each + * value. + * + * @param source The RLP encoded bytes. + * @param fn A function creating a new element of the list for each value in the RLP list. + * @return The list supplied to {@code fn}. + * @param The type of the list elements. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the first RLP value is not a list. + */ + public static List decodeToList(Bytes source, Function fn) { + return decodeToList(source, false, fn); + } + + /** + * Read an RLP encoded list of values from a {@link Bytes} value, populating a mutable output + * list. + * + * @param source The RLP encoded bytes. + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @param fn A function creating a new element of the list for each value in the RLP list. + * @param The type of the list elements. + * @return The list supplied to {@code fn}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the first RLP value is not a list. + */ + public static List decodeToList(Bytes source, boolean lenient, Function fn) { + requireNonNull(source); + requireNonNull(fn); + if (source.isEmpty()) { + throw new IllegalArgumentException("source is empty"); + } + return decode(source, lenient, reader -> reader.readListContents(fn)); + } + + /** + * Read an RLP encoded value from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return The bytes for the value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no RLP values to read. + */ + public static Bytes decodeValue(Bytes source) { + return decodeValue(source, false); + } + + /** + * Read an RLP encoded value from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @return The bytes for the value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no RLP values to read. + */ + public static Bytes decodeValue(Bytes source, boolean lenient) { + requireNonNull(source); + return decode(source, lenient, RLPReader::readValue); + } + + /** + * Read an RLP encoded integer from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return An integer. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static int decodeInt(Bytes source) { + return decodeInt(source, false); + } + + /** + * Read an RLP encoded integer from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @return An integer. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static int decodeInt(Bytes source, boolean lenient) { + requireNonNull(source); + if (source.isEmpty()) { + throw new IllegalArgumentException("source is empty"); + } + return decode(source, lenient, RLPReader::readInt); + } + + /** + * Read an RLP encoded long from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return A long. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static long decodeLong(Bytes source) { + return decodeLong(source, false); + } + + /** + * Read an RLP encoded long from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @return A long. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static long decodeLong(Bytes source, boolean lenient) { + requireNonNull(source); + if (source.isEmpty()) { + throw new IllegalArgumentException("source is empty"); + } + return decode(source, lenient, RLPReader::readLong); + } + + /** + * Read an RLP encoded big integer from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return A {@link BigInteger}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static BigInteger decodeBigInteger(Bytes source) { + return decodeBigInteger(source, false); + } + + /** + * Read an RLP encoded big integer from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @return A {@link BigInteger}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static BigInteger decodeBigInteger(Bytes source, boolean lenient) { + requireNonNull(source); + if (source.isEmpty()) { + throw new IllegalArgumentException("source is empty"); + } + return decode(source, lenient, RLPReader::readBigInteger); + } + + /** + * Read an RLP encoded string from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @return A string. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static String decodeString(Bytes source) { + return decodeString(source, false); + } + + /** + * Read an RLP encoded string from a {@link Bytes} value. + * + * @param source The RLP encoded bytes. + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @return A string. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + */ + public static String decodeString(Bytes source, boolean lenient) { + requireNonNull(source); + if (source.isEmpty()) { + throw new IllegalArgumentException("source is empty"); + } + return decode(source, lenient, RLPReader::readString); + } + + /** + * Check if the {@link Bytes} value contains an RLP encoded list. + * + * @param value The value to check. + * @return {@code true} if the value contains a list. + */ + public static boolean isList(Bytes value) { + requireNonNull(value); + if (value.isEmpty()) { + throw new IllegalArgumentException("value is empty"); + } + return decode(value, RLPReader::nextIsList); + } +} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPReader.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPReader.java new file mode 100644 index 000000000..4a009246b --- /dev/null +++ b/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPReader.java @@ -0,0 +1,424 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.rlp.EndOfRLPException; +import org.apache.tuweni.rlp.InvalidRLPEncodingException; +import org.apache.tuweni.rlp.InvalidRLPTypeException; +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** A reader for consuming values from an RLP encoded source. */ +public interface RLPReader { + + /** + * Determine if this reader is lenient by default. + * + *

A non-lenient reader will throw {@link InvalidRLPEncodingException} from any read method if + * the source RLP has not used a minimal encoding format for the value. + * + * @return {@code true} if the reader is lenient, and {@code false} otherwise (default). + */ + boolean isLenient(); + + /** + * Read the next value from the RLP source. + * + * @return The bytes for the next value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default Bytes readValue() { + return readValue(isLenient()); + } + + /** + * Read the next value from the RLP source. + * + * @param lenient If {@code false}, an exception will be thrown if the value is not minimally + * encoded. + * @return The bytes for the next value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + Bytes readValue(boolean lenient); + + /** + * Read a byte array from the RLP source. + * + * @return The byte array for the next value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default byte[] readByteArray() { + return readValue().toArrayUnsafe(); + } + + /** + * Read a byte from the RLP source. + * + * @return The byte for the next value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default byte readByte() { + return readByte(isLenient()); + } + + /** + * Read a byte from the RLP source. + * + * @param lenient If {@code false}, an exception will be thrown if the byte is not minimally + * encoded. + * @return The byte for the next value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default byte readByte(boolean lenient) { + Bytes bytes = readValue(lenient); + if (bytes.size() != 1) { + throw new InvalidRLPTypeException("Value is not a single byte"); + } + return bytes.get(0); + } + + /** + * Read an integer value from the RLP source. + * + * @return An integer. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as an integer. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default int readInt() { + return readInt(isLenient()); + } + + /** + * Read an integer value from the RLP source. + * + * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally + * encoded. + * @return An integer. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the + * integer is not minimally encoded and `lenient` is {@code false}. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as an integer. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default int readInt(boolean lenient) { + Bytes bytes = readValue(); + if (!lenient && bytes.hasLeadingZeroByte()) { + throw new InvalidRLPEncodingException("Integer value was not minimally encoded"); + } + try { + return bytes.toInt(); + } catch (IllegalArgumentException e) { + throw new InvalidRLPTypeException("Value is too large to be represented as an int"); + } + } + + /** + * Read a long value from the RLP source. + * + * @return A long. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default long readLong() { + return readLong(isLenient()); + } + + /** + * Read a long value from the RLP source. + * + * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally + * encoded. + * @return A long. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the + * integer is not minimally encoded and `lenient` is {@code false}. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default long readLong(boolean lenient) { + Bytes bytes = readValue(); + if (!lenient && bytes.hasLeadingZeroByte()) { + throw new InvalidRLPEncodingException("Integer value was not minimally encoded"); + } + try { + return bytes.toLong(); + } catch (IllegalArgumentException e) { + throw new InvalidRLPTypeException("Value is too large to be represented as a long"); + } + } + + /** + * Read a {@link UInt256} value from the RLP source. + * + * @return A {@link UInt256} value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default UInt256 readUInt256() { + return readUInt256(isLenient()); + } + + /** + * Read a {@link UInt256} value from the RLP source. + * + * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally + * encoded. + * @return A {@link UInt256} value. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the + * integer is not minimally encoded and `lenient` is {@code false}. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default UInt256 readUInt256(boolean lenient) { + Bytes bytes = readValue(); + if (!lenient && bytes.hasLeadingZeroByte()) { + throw new InvalidRLPEncodingException("Integer value was not minimally encoded"); + } + try { + return UInt256.fromBytes(bytes); + } catch (IllegalArgumentException e) { + throw new InvalidRLPTypeException("Value is too large to be represented as a UInt256"); + } + } + + /** + * Read a big integer value from the RLP source. + * + * @return A big integer. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a big integer. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default BigInteger readBigInteger() { + return readBigInteger(isLenient()); + } + + /** + * Read a big integer value from the RLP source. + * + * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally + * encoded. + * @return A big integer. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the + * integer is not minimally encoded and `lenient` is {@code false}. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a big integer. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default BigInteger readBigInteger(boolean lenient) { + Bytes bytes = readValue(); + if (!lenient && bytes.hasLeadingZeroByte()) { + throw new InvalidRLPEncodingException("Integer value was not minimally encoded"); + } + return bytes.toUnsignedBigInteger(); + } + + /** + * Read a string value from the RLP source. + * + * @return A string. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a string. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default String readString() { + return readString(isLenient()); + } + + /** + * Read a string value from the RLP source. + * + * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally + * encoded. + * @return A string. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a string. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default String readString(boolean lenient) { + return new String(readValue(lenient).toArrayUnsafe(), UTF_8); + } + + /** + * Check if the next item to be read is a list. + * + * @return {@code true} if the next item to be read is a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + boolean nextIsList(); + + /** + * Check if the next item to be read is empty. + * + * @return {@code true} if the next item to be read is empty. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + boolean nextIsEmpty(); + + /** + * Read a list of values from the RLP source. + * + * @param fn A function that will be provided a {@link RLPReader}. + * @param The result type of the reading function. + * @return The result from the reading function. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value is not a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default T readList(Function fn) { + return readList(isLenient(), fn); + } + + /** + * Read a list of values from the RLP source. + * + * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally + * encoded. + * @param fn A function that will be provided a {@link RLPReader}. + * @param The result type of the reading function. + * @return The result from the reading function. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value is not a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + T readList(boolean lenient, Function fn); + + /** + * Read a list of values from the RLP source, populating a mutable output list. + * + * @param fn A function that will be provided with a {@link RLPReader} and a mutable output list. + * @return The list supplied to {@code fn}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value is not a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default List readList(BiConsumer> fn) { + return readList(isLenient(), fn); + } + + /** + * Read a list of values from the RLP source, populating a list using a function interpreting each + * value. + * + * @param fn A function creating a new element of the list for each value in the RLP list. + * @param The type of the list elements. + * @return The list supplied to {@code fn}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value is not a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default List readListContents(Function fn) { + return readListContents(isLenient(), fn); + } + + /** + * Read a list of values from the RLP source, populating a mutable output list. + * + * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally + * encoded. + * @param fn A function that will be provided with a {@link RLPReader} and a mutable output list. + * @return The list supplied to {@code fn}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value is not a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default List readList(boolean lenient, BiConsumer> fn) { + requireNonNull(fn); + return readList( + lenient, + reader -> { + List list = new ArrayList<>(); + fn.accept(reader, list); + return list; + }); + } + + /** + * Read a list of values from the RLP source, populating a list using a function interpreting each + * value. + * + * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally + * encoded. + * @param fn A function creating a new element of the list for each value in the RLP list. + * @param The type of the list elements. + * @return The list supplied to {@code fn}. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws InvalidRLPTypeException If the next RLP value is not a list. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default List readListContents(boolean lenient, Function fn) { + requireNonNull(fn); + + return readList( + lenient, + reader -> { + List list = new ArrayList(); + while (!reader.isComplete()) { + list.add(fn.apply(reader)); + } + return list; + }); + } + + /** + * Skip the next value or list in the RLP source. + * + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + default void skipNext() { + skipNext(isLenient()); + } + + /** + * Skip the next value or list in the RLP source. + * + * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally + * encoded. + * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. + * @throws EndOfRLPException If there are no more RLP values to read. + */ + void skipNext(boolean lenient); + + /** + * The number of remaining values to read. + * + * @return The number of remaining values to read. + */ + int remaining(); + + /** + * Check if all values have been read. + * + * @return {@code true} if all values have been read. + */ + boolean isComplete(); + + /** + * Returns reader's index + * + * @return current reader position + */ + int position(); + + /** + * Provides the remainder of the bytes that have not been read yet. + * + * @return the remainder of the input at current position + */ + Bytes readRemaining(); +} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPWriter.java new file mode 100644 index 000000000..30cd7d373 --- /dev/null +++ b/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPWriter.java @@ -0,0 +1,128 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; + +import java.math.BigInteger; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** A writer for encoding values to RLP. */ +public interface RLPWriter { + + /** + * Append an already RLP encoded value. + * + *

Note that this method may not validate that {@code value} is a valid RLP sequence. + * Appending an invalid RLP sequence will cause the entire RLP encoding produced by this writer to + * also be invalid. + * + * @param value The RLP encoded bytes to append. + */ + void writeRLP(Bytes value); + + /** + * Encode a {@link Bytes} value to RLP. + * + * @param value The byte array to encode. + */ + void writeValue(Bytes value); + + /** + * Encode a byte array to RLP. + * + * @param value The byte array to encode. + */ + default void writeByteArray(byte[] value) { + writeValue(Bytes.wrap(value)); + } + + /** + * Encode a byte to RLP. + * + * @param value The byte value to encode. + */ + default void writeByte(byte value) { + writeValue(Bytes.of(value)); + } + + /** + * Write an integer to the output. + * + * @param value The integer to write. + */ + default void writeInt(int value) { + writeLong(value); + } + + /** + * Write a long to the output. + * + * @param value The long value to write. + */ + void writeLong(long value); + + /** + * Write a {@link UInt256} to the output. + * + * @param value The {@link UInt256} value to write. + */ + default void writeUInt256(UInt256 value) { + writeValue(value.toMinimalBytes()); + } + + /** + * Write a big integer to the output. + * + * @param value The integer to write. + */ + default void writeBigInteger(BigInteger value) { + if (value.signum() == 0) { + writeInt(0); + return; + } + byte[] byteArray = value.toByteArray(); + if (byteArray[0] == 0) { + writeValue(Bytes.wrap(byteArray).slice(1)); + } else { + writeByteArray(byteArray); + } + } + + /** + * Write a string to the output. + * + * @param str The string to write. + */ + default void writeString(String str) { + writeByteArray(str.getBytes(UTF_8)); + } + + /** + * Write a list of values. + * + * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. + */ + void writeList(Consumer fn); + + /** + * Write a list of values, sending each value to a function to be interpreted. + * + * @param elements the list of elements to write + * @param elementWriter the function called for each element in the list + * @param The type of the list elements. + */ + default void writeList(List elements, BiConsumer elementWriter) { + writeList( + writer -> { + for (T element : elements) { + elementWriter.accept(writer, element); + } + }); + } +} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/package-info.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/package-info.java new file mode 100644 index 000000000..71e929098 --- /dev/null +++ b/rlp/src/main/java/org/apache/tuweni/v2/rlp/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. + * + *

Recursive Length Prefix (RLP) encoding and decoding. + * + *

An implementation of the Ethereum Recursive Length Prefix (RLP) algorithm, as described at https://github.com/ethereum/wiki/wiki/RLP. + * + *

These classes are included in the standard Tuweni distribution, or separately when using the + * gradle dependency 'org.apache.tuweni:tuweni-rlp' (tuweni-rlp.jar). + */ +package org.apache.tuweni.v2.rlp; diff --git a/rlp/src/test/java/org/apache/tuweni/v2/rlp/ByteBufferWriterTest.java b/rlp/src/test/java/org/apache/tuweni/v2/rlp/ByteBufferWriterTest.java new file mode 100644 index 000000000..231bf22c1 --- /dev/null +++ b/rlp/src/test/java/org/apache/tuweni/v2/rlp/ByteBufferWriterTest.java @@ -0,0 +1,163 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ByteBufferWriterTest { + + @ParameterizedTest + @CsvSource({"8203e8, 1000", "830186a0, 100000"}) + void shouldWriteSmallIntegers(String expectedHex, int value) { + ByteBuffer buffer = ByteBuffer.allocate(64); + RLP.encodeTo(buffer, writer -> writer.writeInt(value)); + buffer.flip(); + assertEquals(Bytes.fromHexString(expectedHex), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteLongIntegers() { + ByteBuffer buffer = ByteBuffer.allocate(64); + RLP.encodeTo(buffer, writer -> writer.writeLong(100000L)); + buffer.flip(); + assertEquals(Bytes.fromHexString("830186a0"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteUInt256Integers() { + ByteBuffer buffer = ByteBuffer.allocate(64); + RLP.encodeTo(buffer, writer -> writer.writeUInt256(UInt256.valueOf(100000L))); + buffer.flip(); + assertEquals(Bytes.fromHexString("830186a0"), Bytes.wrapByteBuffer(buffer)); + + buffer.clear(); + RLP.encodeTo( + buffer, + writer -> + writer.writeUInt256( + UInt256.fromHexString( + "0x0400000000000000000000000000000000000000000000000000f100000000ab"))); + buffer.flip(); + assertEquals( + Bytes.fromHexString("a00400000000000000000000000000000000000000000000000000f100000000ab"), + Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteBigIntegers() { + ByteBuffer buffer = ByteBuffer.allocate(64); + RLP.encodeTo(buffer, writer -> writer.writeBigInteger(BigInteger.valueOf(100000))); + buffer.flip(); + assertEquals(Bytes.fromHexString("830186a0"), Bytes.wrapByteBuffer(buffer)); + + buffer.clear(); + RLP.encodeTo(buffer, writer -> writer.writeBigInteger(BigInteger.valueOf(127).pow(16))); + buffer.flip(); + assertEquals( + Bytes.fromHexString("8ee1ceefa5bbd9ed1c97f17a1df801"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteEmptyStrings() { + ByteBuffer buffer = ByteBuffer.allocate(64); + RLP.encodeTo(buffer, writer -> writer.writeString("")); + buffer.flip(); + assertEquals(Bytes.fromHexString("80"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteOneCharactersStrings() { + ByteBuffer buffer = ByteBuffer.allocate(64); + RLP.encodeTo(buffer, writer -> writer.writeString("d")); + buffer.flip(); + assertEquals(Bytes.fromHexString("64"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteStrings() { + ByteBuffer buffer = ByteBuffer.allocate(64); + RLP.encodeTo(buffer, writer -> writer.writeString("dog")); + buffer.flip(); + assertEquals(Bytes.fromHexString("83646f67"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteShortLists() { + List strings = + Arrays.asList( + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); + + ByteBuffer buffer = ByteBuffer.allocate(64); + RLP.encodeListTo(buffer, listWriter -> strings.forEach(listWriter::writeString)); + buffer.flip(); + + assertEquals( + Bytes.fromHexString( + "f784617364668471776572847a78637684617364668471776572847a" + + "78637684617364668471776572847a78637684617364668471776572"), + Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteNestedLists() { + ByteBuffer buffer = ByteBuffer.allocate(1024); + RLP.encodeListTo( + buffer, + listWriter -> { + listWriter.writeString("asdf"); + listWriter.writeString("qwer"); + for (int i = 30; i >= 0; --i) { + listWriter.writeList( + subListWriter -> { + subListWriter.writeString("zxcv"); + subListWriter.writeString("asdf"); + subListWriter.writeString("qwer"); + }); + } + }); + + buffer.flip(); + assertTrue( + RLP.decodeList( + Bytes.wrapByteBuffer(buffer), + listReader -> { + assertEquals("asdf", listReader.readString()); + assertEquals("qwer", listReader.readString()); + + for (int i = 30; i >= 0; --i) { + assertTrue( + listReader.readList( + subListReader -> { + assertEquals("zxcv", subListReader.readString()); + assertEquals("asdf", subListReader.readString()); + assertEquals("qwer", subListReader.readString()); + return true; + })); + } + + return true; + })); + } + + @Test + void shouldWritePreviouslyEncodedValues() { + ByteBuffer buffer = ByteBuffer.allocate(64); + RLP.encodeTo(buffer, writer -> writer.writeRLP(RLP.encodeByteArray("abc".getBytes(UTF_8)))); + buffer.flip(); + assertEquals("abc", RLP.decodeString(Bytes.wrapByteBuffer(buffer))); + } +} diff --git a/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPReaderTest.java b/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPReaderTest.java new file mode 100644 index 000000000..0d083e5c5 --- /dev/null +++ b/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPReaderTest.java @@ -0,0 +1,275 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; +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.rlp.EndOfRLPException; +import org.apache.tuweni.rlp.InvalidRLPEncodingException; +import org.apache.tuweni.rlp.InvalidRLPTypeException; +import org.apache.tuweni.v2.bytes.Bytes; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class BytesRLPReaderTest { + + private static final Bytes SHORT_LIST = + fromHexString( + "f784617364668471776572847a78637684617364668471776572847a78637684617364668471776572847a78637684617364668471776572"); + private static final Bytes LONG_LIST = + fromHexString( + "f90200cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf8461736" + + "4668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572" + + "847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf8" + + "4617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471" + + "776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786" + + "376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364" + + "668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf846173646684717765728" + + "47a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84" + + "617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf846173646684717" + + "76572847a786376cf84617364668471776572847a786376"); + + private static class SomeObject { + private final String name; + private final int number; + private final BigInteger longNumber; + + SomeObject(String name, int number, BigInteger longNumber) { + this.name = name; + this.number = number; + this.longNumber = longNumber; + } + } + + @Test + void shouldParseFullObjects() { + Bytes bytes = fromHexString("83426f620486011F71B70768"); + SomeObject readObject = + RLP.decode( + bytes, + reader -> + new SomeObject(reader.readString(), reader.readInt(), reader.readBigInteger())); + + assertEquals("Bob", readObject.name); + assertEquals(4, readObject.number); + assertEquals(BigInteger.valueOf(1234563434344L), readObject.longNumber); + } + + @ParameterizedTest + @CsvSource({ + "80, 0", + "01, 1", + "10, 16", + "4f, 79", + "7f, 127", + "8180, 128", + "8203e8, 1000", + "830186a0, 100000" + }) + void shouldReadIntegers(String hex, int value) { + assertTrue( + RLP.decode( + fromHexString(hex), + reader -> { + assertEquals(value, reader.readInt()); + return true; + })); + } + + @ParameterizedTest + // @formatter:off + @CsvSource({ + "80, ''", + "00, '\u0000'", + "01, '\u0001'", + "7f, '\u007F'", + "83646f67, dog", + "b74c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c69" + + ", 'Lorem ipsum dolor sit amet, consectetur adipisicing eli'", + "b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974" + + ", 'Lorem ipsum dolor sit amet, consectetur adipisicing elit'", + "b904004c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742e20437572616269747572206d6175726973206d61676e612c20737573636970697420736564207665686963756c61206e6f6e2c20696163756c697320666175636962757320746f72746f722e2050726f696e20737573636970697420756c74726963696573206d616c6573756164612e204475697320746f72746f7220656c69742c2064696374756d2071756973207472697374697175652065752c20756c7472696365732061742072697375732e204d6f72626920612065737420696d70657264696574206d6920756c6c616d636f7270657220616c6971756574207375736369706974206e6563206c6f72656d2e2041656e65616e2071756973206c656f206d6f6c6c69732c2076756c70757461746520656c6974207661726975732c20636f6e73657175617420656e696d2e204e756c6c6120756c74726963657320747572706973206a7573746f2c20657420706f73756572652075726e6120636f6e7365637465747572206e65632e2050726f696e206e6f6e20636f6e76616c6c6973206d657475732e20446f6e65632074656d706f7220697073756d20696e206d617572697320636f6e67756520736f6c6c696369747564696e2e20566573746962756c756d20616e746520697073756d207072696d697320696e206661756369627573206f726369206c756374757320657420756c74726963657320706f737565726520637562696c69612043757261653b2053757370656e646973736520636f6e76616c6c69732073656d2076656c206d617373612066617563696275732c2065676574206c6163696e6961206c616375732074656d706f722e204e756c6c61207175697320756c747269636965732070757275732e2050726f696e20617563746f722072686f6e637573206e69626820636f6e64696d656e74756d206d6f6c6c69732e20416c697175616d20636f6e73657175617420656e696d206174206d65747573206c75637475732c206120656c656966656e6420707572757320656765737461732e20437572616269747572206174206e696268206d657475732e204e616d20626962656e64756d2c206e6571756520617420617563746f72207472697374697175652c206c6f72656d206c696265726f20616c697175657420617263752c206e6f6e20696e74657264756d2074656c6c7573206c65637475732073697420616d65742065726f732e20437261732072686f6e6375732c206d65747573206163206f726e617265206375727375732c20646f6c6f72206a7573746f20756c747269636573206d657475732c20617420756c6c616d636f7270657220766f6c7574706174" + + ", 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat'" + }) + // @formatter:on + void shouldReadStrings(String hex, String value) { + assertTrue( + RLP.decode( + fromHexString(hex), + reader -> { + assertEquals(value, reader.readString()); + return true; + })); + } + + @Test + void shouldThrowWhenInputExhausted() { + EndOfRLPException ex = + assertThrows(EndOfRLPException.class, () -> RLP.decode(Bytes.EMPTY, RLPReader::readInt)); + assertEquals("End of RLP source reached", ex.getMessage()); + } + + @Test + void shouldThrowWhenNextItemIsAList() { + InvalidRLPTypeException ex = + assertThrows( + InvalidRLPTypeException.class, () -> RLP.decode(SHORT_LIST, RLPReader::readInt)); + assertEquals("Attempted to read a value but next item is a list", ex.getMessage()); + } + + @Test + void shouldThrowWheSourceIsTruncated() { + InvalidRLPEncodingException ex = + assertThrows( + InvalidRLPEncodingException.class, + () -> RLP.decode(fromHexString("830186"), RLPReader::readInt)); + assertEquals("Insufficient bytes in RLP encoding: expected 3 but have only 2", ex.getMessage()); + } + + @Test + void shouldThrowWhenLowValueIsntEncodedToSingleByte() { + Bytes bytes1 = fromHexString("8128"); + InvalidRLPEncodingException ex1 = + assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes1)); + assertEquals("Value should have been encoded as a single byte 0x28", ex1.getMessage()); + assertEquals(40, RLP.decodeInt(bytes1, true)); + + Bytes bytes2 = fromHexString("b80128"); + InvalidRLPEncodingException ex2 = + assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes1)); + assertEquals("Value should have been encoded as a single byte 0x28", ex2.getMessage()); + assertEquals(40, RLP.decodeInt(bytes2, true)); + } + + @Test + void shouldThrowWhenValueLengthContainsLeadingZeros() { + Bytes bytes1 = + fromHexString( + "b900384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974"); + InvalidRLPEncodingException ex1 = + assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeString(bytes1)); + assertEquals("RLP value length contains leading zero bytes", ex1.getMessage()); + assertEquals( + "Lorem ipsum dolor sit amet, consectetur adipisicing elit", RLP.decodeString(bytes1, true)); + + Bytes bytes2 = fromHexString("bb0000000028"); + InvalidRLPEncodingException ex2 = + assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes2)); + assertEquals("RLP value length contains leading zero bytes", ex2.getMessage()); + InvalidRLPEncodingException ex3 = + assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes2, true)); + assertEquals("RLP value length is zero", ex3.getMessage()); + + Bytes bytes3 = fromHexString("bd00000000000128"); + InvalidRLPEncodingException ex4 = + assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes3)); + assertEquals("RLP value length contains leading zero bytes", ex4.getMessage()); + assertEquals(40, RLP.decodeInt(bytes3, true)); + } + + @Test + void shouldThrowWhenListLengthContainsLeadingZeros() { + Bytes bytes1 = fromHexString("0xF9000101"); + InvalidRLPEncodingException ex1 = + assertThrows( + InvalidRLPEncodingException.class, () -> RLP.decodeList(bytes1, RLPReader::readInt)); + assertEquals("RLP list length contains leading zero bytes", ex1.getMessage()); + assertEquals(Integer.valueOf(1), RLP.decodeList(bytes1, true, RLPReader::readInt)); + + Bytes bytes2 = fromHexString("0xF80101"); + InvalidRLPEncodingException ex2 = + assertThrows( + InvalidRLPEncodingException.class, () -> RLP.decodeList(bytes2, RLPReader::readInt)); + assertEquals("RLP list length of 1 was not minimally encoded", ex2.getMessage()); + assertEquals(Integer.valueOf(1), RLP.decodeList(bytes2, true, RLPReader::readInt)); + } + + @Test + void shouldThrowWhenLengthIsOversized() { + Bytes bytes1 = fromHexString("bc0aaaaaaaaa28"); + InvalidRLPEncodingException ex1 = + assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes1)); + assertEquals("RLP value length is oversized", ex1.getMessage()); + + Bytes bytes2 = fromHexString("bb8000000128"); + InvalidRLPEncodingException ex2 = + assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes2)); + assertEquals("RLP value length is oversized", ex2.getMessage()); + } + + @Test + void shouldReadShortList() { + List expected = + Arrays.asList( + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); + + List result = + RLP.decodeToList( + SHORT_LIST, + (reader, list) -> { + assertEquals(11, reader.remaining()); + for (int i = 10; i >= 0; --i) { + list.add(reader.readString()); + } + }); + assertEquals(expected, result); + } + + @Test + void shouldReadShortListContents() { + List expected = + Arrays.asList( + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); + + List result = RLP.decodeToList(SHORT_LIST, reader -> reader.readString()); + assertEquals(expected, result); + } + + @Test + void shouldReadLongList() { + List> expected = + Stream.generate(() -> Arrays.asList("asdf", "qwer", "zxcv")) + .limit(31) + .collect(Collectors.toList()); + + List result = + RLP.decodeToList( + LONG_LIST, + (reader, list) -> { + for (int i = 30; i >= 0; --i) { + list.add( + reader.readList( + (subReader, subList) -> { + subList.add(subReader.readString()); + subList.add(subReader.readString()); + subList.add(subReader.readString()); + })); + } + }); + assertEquals(expected, result); + } + + @Test + void shouldReadRemaining() { + Bytes input = Bytes.fromHexString("83646f6783646f6783646f67"); + RLP.decode( + input, + reader -> { + reader.readValue(); + assertEquals(4, reader.position()); + assertEquals(Bytes.fromHexString("83646f6783646f67"), reader.readRemaining()); + return null; + }); + } +} diff --git a/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPWriterTest.java b/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPWriterTest.java new file mode 100644 index 000000000..989054ff5 --- /dev/null +++ b/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPWriterTest.java @@ -0,0 +1,187 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.rlp; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +class BytesRLPWriterTest { + + private static class SomeObject { + private final String name; + private final int number; + private final BigInteger longNumber; + + SomeObject(String name, int number, BigInteger longNumber) { + this.name = name; + this.number = number; + this.longNumber = longNumber; + } + } + + @Test + void shouldWriteFullObjects() { + SomeObject bob = new SomeObject("Bob", 4, BigInteger.valueOf(1234563434344L)); + Bytes bytes = + RLP.encode( + writer -> { + writer.writeString(bob.name); + writer.writeInt(bob.number); + writer.writeBigInteger(bob.longNumber); + }); + + assertTrue( + RLP.decode( + bytes, + reader -> { + assertEquals("Bob", reader.readString()); + assertEquals(4, reader.readInt()); + assertEquals(BigInteger.valueOf(1234563434344L), reader.readBigInteger()); + return true; + })); + } + + @Test + void shouldWriteSmallIntegers() { + assertEquals(fromHexString("80"), RLP.encode(writer -> writer.writeInt(0))); + assertEquals(fromHexString("01"), RLP.encode(writer -> writer.writeInt(1))); + assertEquals(fromHexString("0f"), RLP.encode(writer -> writer.writeInt(15))); + assertEquals(fromHexString("8203e8"), RLP.encode(writer -> writer.writeInt(1000))); + assertEquals(fromHexString("820400"), RLP.encode(writer -> writer.writeInt(1024))); + assertEquals(fromHexString("830186a0"), RLP.encode(writer -> writer.writeInt(100000))); + } + + @Test + void shouldWriteLongIntegers() { + assertEquals(fromHexString("80"), RLP.encode(writer -> writer.writeLong(0L))); + assertEquals(fromHexString("01"), RLP.encode(writer -> writer.writeLong(1))); + assertEquals(fromHexString("0f"), RLP.encode(writer -> writer.writeLong(15))); + assertEquals(fromHexString("8203e8"), RLP.encode(writer -> writer.writeLong(1000))); + assertEquals(fromHexString("820400"), RLP.encode(writer -> writer.writeLong(1024))); + assertEquals(fromHexString("830186a0"), RLP.encode(writer -> writer.writeLong(100000L))); + } + + @Test + void shouldWriteUInt256Integers() { + assertEquals( + fromHexString("80"), RLP.encode(writer -> writer.writeUInt256(UInt256.valueOf(0L)))); + assertEquals( + fromHexString("830186a0"), + RLP.encode(writer -> writer.writeUInt256(UInt256.valueOf(100000L)))); + assertEquals( + fromHexString("a00400000000000000000000000000000000000000000000000000f100000000ab"), + RLP.encode( + writer -> + writer.writeUInt256( + UInt256.fromHexString( + "0x0400000000000000000000000000000000000000000000000000f100000000ab")))); + } + + @Test + void shouldWriteBigIntegers() { + assertEquals( + fromHexString("830186a0"), + RLP.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(100000)))); + assertEquals( + fromHexString("8ee1ceefa5bbd9ed1c97f17a1df801"), + RLP.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(127).pow(16)))); + } + + @Test + void shouldWriteEmptyStrings() { + assertEquals(fromHexString("80"), RLP.encode(writer -> writer.writeString(""))); + } + + @Test + void shouldWriteOneCharactersStrings() { + assertEquals(fromHexString("64"), RLP.encode(writer -> writer.writeString("d"))); + } + + @Test + void shouldWriteStrings() { + assertEquals(fromHexString("83646f67"), RLP.encode(writer -> writer.writeString("dog"))); + } + + @Test + void shouldWriteShortLists() { + List strings = + Arrays.asList( + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); + + assertEquals( + fromHexString( + "f784617364668471776572847a78637684617364668471776572847a" + + "78637684617364668471776572847a78637684617364668471776572"), + RLP.encodeList(listWriter -> strings.forEach(listWriter::writeString))); + } + + @Test + void shouldWriteShortListWithAFunction() { + List strings = + Arrays.asList( + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); + + assertEquals( + fromHexString( + "f784617364668471776572847a78637684617364668471776572847a" + + "78637684617364668471776572847a78637684617364668471776572"), + RLP.encodeList(strings, RLPWriter::writeString)); + } + + @Test + void shouldWriteNestedLists() { + Bytes bytes = + RLP.encodeList( + listWriter -> { + listWriter.writeString("asdf"); + listWriter.writeString("qwer"); + for (int i = 30; i >= 0; --i) { + listWriter.writeList( + subListWriter -> { + subListWriter.writeString("zxcv"); + subListWriter.writeString("asdf"); + subListWriter.writeString("qwer"); + }); + } + }); + + assertTrue( + RLP.decodeList( + bytes, + listReader -> { + assertEquals("asdf", listReader.readString()); + assertEquals("qwer", listReader.readString()); + + for (int i = 30; i >= 0; --i) { + assertTrue( + listReader.readList( + subListReader -> { + assertEquals("zxcv", subListReader.readString()); + assertEquals("asdf", subListReader.readString()); + assertEquals("qwer", subListReader.readString()); + return true; + })); + } + + return true; + })); + } + + @Test + void shouldWritePreviouslyEncodedValues() { + Bytes output = + RLP.encode(writer -> writer.writeRLP(RLP.encodeByteArray("abc".getBytes(UTF_8)))); + assertEquals("abc", RLP.decodeString(output)); + } +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/ByteBufferSSZWriter.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/ByteBufferSSZWriter.java new file mode 100644 index 000000000..455bd4555 --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/ByteBufferSSZWriter.java @@ -0,0 +1,26 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.nio.ByteBuffer; + +final class ByteBufferSSZWriter implements SSZWriter { + + private ByteBuffer buffer; + + ByteBufferSSZWriter(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public void writeSSZ(Bytes value) { + buffer.put(value.toArrayUnsafe()); + } + + @Override + public void writeSSZ(byte[] value) { + buffer.put(value); + } +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZReader.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZReader.java new file mode 100644 index 000000000..a546ae8cd --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZReader.java @@ -0,0 +1,479 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import static java.nio.ByteOrder.LITTLE_ENDIAN; +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.apache.tuweni.ssz.EndOfSSZException; +import org.apache.tuweni.ssz.InvalidSSZTypeException; +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; +import org.apache.tuweni.v2.units.bigints.UInt384; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; +import java.util.function.LongFunction; +import java.util.function.Supplier; + +final class BytesSSZReader implements SSZReader { + + private final Bytes content; + private int index = 0; + + BytesSSZReader(Bytes content) { + this.content = content; + } + + @Override + public Bytes readBytes(int limit) { + int byteLength = 4; + ensureBytes(byteLength, () -> "SSZ encoded data is not a byte array"); + int size; + try { + size = content.getInt(index, LITTLE_ENDIAN); + } catch (IndexOutOfBoundsException e) { + throw new EndOfSSZException(); + } + if (size < 0 || size > limit) { + throw new InvalidSSZTypeException("length of bytes would exceed limit"); + } + index += 4; + if (content.size() - index - size < 0) { + throw new InvalidSSZTypeException( + "SSZ encoded data has insufficient bytes for decoded byte array length"); + } + return consumeBytes(size); + } + + @Override + public Bytes readFixedBytes(int byteLength, int limit) { + ensureBytes(byteLength, () -> "SSZ encoded data is not a fixed-length byte array"); + return consumeBytes(byteLength); + } + + @Override + public int readInt(int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + int byteLength = bitLength / 8; + ensureBytes( + byteLength, + () -> "SSZ encoded data has insufficient length to read a " + bitLength + "-bit integer"); + Bytes bytes = content.slice(index, byteLength); + int zeroBytes = bytes.numberOfTrailingZeroBytes(); + if ((byteLength - zeroBytes) > 4) { + throw new InvalidSSZTypeException("decoded integer is too large for an int"); + } + index += byteLength; + return bytes.slice(0, bytes.size() - zeroBytes).toInt(LITTLE_ENDIAN); + } + + @Override + public long readLong(int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + int byteLength = bitLength / 8; + ensureBytes( + byteLength, + () -> "SSZ encoded data has insufficient length to read a " + bitLength + "-bit integer"); + Bytes bytes = content.slice(index, byteLength); + int zeroBytes = bytes.numberOfTrailingZeroBytes(); + if ((byteLength - zeroBytes) > 8) { + throw new InvalidSSZTypeException("decoded integer is too large for a long"); + } + index += byteLength; + return bytes.slice(0, bytes.size() - zeroBytes).toLong(LITTLE_ENDIAN); + } + + @Override + public BigInteger readBigInteger(int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + int byteLength = bitLength / 8; + ensureBytes( + byteLength, + () -> "SSZ encoded data has insufficient length to read a " + bitLength + "-bit integer"); + return consumeBytes(byteLength).toBigInteger(LITTLE_ENDIAN); + } + + @Override + public BigInteger readUnsignedBigInteger(int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + int byteLength = bitLength / 8; + ensureBytes( + byteLength, + () -> "SSZ encoded data has insufficient length to read a " + bitLength + "-bit integer"); + return consumeBytes(byteLength).toUnsignedBigInteger(LITTLE_ENDIAN); + } + + @Override + public UInt256 readUInt256() { + ensureBytes( + 256 / 8, () -> "SSZ encoded data has insufficient length to read a 256-bit integer"); + return UInt256.fromBytes(consumeBytes(256 / 8).mutableCopy().reverse()); + } + + @Override + public UInt384 readUInt384() { + ensureBytes( + 384 / 8, () -> "SSZ encoded data has insufficient length to read a 384-bit integer"); + return UInt384.fromBytes(consumeBytes(384 / 8).mutableCopy().reverse()); + } + + @Override + public Bytes readAddress() { + ensureBytes(20, () -> "SSZ encoded data has insufficient length to read a 20-byte address"); + return consumeBytes(20); + } + + @Override + public Bytes readHash(int hashLength) { + ensureBytes( + hashLength, + () -> "SSZ encoded data has insufficient length to read a " + hashLength + "-byte hash"); + return consumeBytes(hashLength); + } + + @Override + public List readBytesList(int limit) { + return readList(remaining -> readBytes(limit)); + } + + @Override + public List readVector(long listSize, int limit) { + return readList(listSize, remaining -> readByteArray(limit), Bytes::wrap); + } + + @Override + public List readFixedBytesVector(int listSize, int byteLength, int limit) { + return readFixedList(listSize, remaining -> readFixedByteArray(byteLength, limit), Bytes::wrap); + } + + @Override + public List readFixedBytesList(int byteLength, int limit) { + return readList(byteLength, () -> readFixedBytes(byteLength, limit)); + } + + @Override + public List readStringList(int limit) { + return readList( + remaining -> readBytes(limit), bytes -> new String(bytes.toArrayUnsafe(), UTF_8)); + } + + @Override + public List readIntList(int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + return readList(bitLength / 8, () -> readInt(bitLength)); + } + + @Override + public List readLongIntList(int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + return readList(bitLength / 8, () -> readLong(bitLength)); + } + + @Override + public List readBigIntegerList(int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + return readList(bitLength / 8, () -> readBigInteger(bitLength)); + } + + @Override + public List readUnsignedBigIntegerList(int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + return readList(bitLength / 8, () -> readUnsignedBigInteger(bitLength)); + } + + @Override + public List readUInt256List() { + return readList(256 / 8, this::readUInt256); + } + + @Override + public List readUInt384List() { + return readList(384 / 8, this::readUInt384); + } + + @Override + public List readAddressList() { + return readList(20, this::readAddress); + } + + @Override + public List readHashList(int hashLength) { + return readList(hashLength, () -> readHash(hashLength)); + } + + @Override + public List readBooleanList() { + return readList(1, this::readBoolean); + } + + @Override + public boolean isComplete() { + return index >= content.size(); + } + + private void ensureBytes(int byteLength, Supplier message) { + if (index == content.size()) { + throw new EndOfSSZException(); + } + if (content.size() - index - byteLength < 0) { + throw new InvalidSSZTypeException(message.get()); + } + } + + private Bytes consumeBytes(int size) { + Bytes bytes = content.slice(index, size); + index += size; + return bytes; + } + + @Override + public Bytes consumeRemainingBytes(int limit) { + if (content.size() - index > limit) { + throw new InvalidSSZTypeException("Too many bytes to consume"); + } + return consumeBytes(content.size() - index); + } + + private List readList(LongFunction bytesSupplier) { + ensureBytes(4, () -> "SSZ encoded data is not a list"); + int originalIndex = this.index; + List elements; + try { + // use a long to simulate reading unsigned + long listSize = consumeBytes(4).toLong(LITTLE_ENDIAN); + elements = new ArrayList<>(); + while (listSize > 0) { + Bytes bytes = bytesSupplier.apply(listSize); + elements.add(bytes); + listSize -= bytes.size(); + listSize -= 4; + if (listSize < 0) { + throw new InvalidSSZTypeException( + "SSZ encoded list length does not align with lengths of its elements"); + } + } + } catch (Exception e) { + this.index = originalIndex; + throw e; + } + return elements; + } + + private List readList(LongFunction bytesSupplier, Function converter) { + ensureBytes(4, () -> "SSZ encoded data is not a list"); + int originalIndex = this.index; + List elements; + try { + // use a long to simulate reading unsigned + long listSize = consumeBytes(4).toLong(LITTLE_ENDIAN); + elements = new ArrayList<>(); + while (listSize > 0) { + Bytes bytes = bytesSupplier.apply(listSize); + elements.add(converter.apply(bytes)); + listSize -= bytes.size(); + listSize -= 4; + if (listSize < 0) { + throw new InvalidSSZTypeException( + "SSZ encoded list length does not align with lengths of its elements"); + } + } + } catch (Exception e) { + this.index = originalIndex; + throw e; + } + return elements; + } + + private List readList( + long listSize, LongFunction bytesSupplier, Function converter) { + int originalIndex = this.index; + List elements; + try { + elements = new ArrayList<>(); + while (listSize > 0) { + byte[] bytes = bytesSupplier.apply(listSize); + elements.add(converter.apply(bytes)); + // When lists have lengths passed in, the listSize argument is the number of + // elements in the list, instead of the number of bytes in the list, so + // we only subtract one each time an element is processed in this case. + listSize -= 1; + if (listSize < 0) { + throw new InvalidSSZTypeException( + "SSZ encoded list length does not align with lengths of its elements"); + } + } + } catch (Exception e) { + this.index = originalIndex; + throw e; + } + return elements; + } + + private List readFixedList( + int listSize, LongFunction bytesSupplier, Function converter) { + int originalIndex = this.index; + List elements; + try { + elements = new ArrayList<>(); + while (listSize > 0) { + byte[] bytes = bytesSupplier.apply(listSize); + elements.add(converter.apply(bytes)); + // When lists have lengths passed in, the listSize argument is the number of + // elements in the list, instead of the number of bytes in the list, so + // we only subtract one each time an element is processed in this case. + listSize -= 1; + if (listSize < 0) { + throw new InvalidSSZTypeException( + "SSZ encoded list length does not align with lengths of its elements"); + } + } + } catch (Exception e) { + this.index = originalIndex; + throw e; + } + return elements; + } + + private List readList(int elementSize, Supplier elementSupplier) { + ensureBytes(4, () -> "SSZ encoded data is not a list"); + int originalIndex = this.index; + List bytesList; + try { + int listSize = consumeBytes(4).toInt(LITTLE_ENDIAN); + if ((listSize % elementSize) != 0) { + throw new InvalidSSZTypeException( + "SSZ encoded list length does not align with lengths of its elements"); + } + int nElements = listSize / elementSize; + bytesList = new ArrayList<>(nElements); + for (int i = 0; i < nElements; ++i) { + bytesList.add(elementSupplier.get()); + } + } catch (Exception e) { + this.index = originalIndex; + throw e; + } + return bytesList; + } + + @Override + public void readAsContainer(SSZReadable... elements) { + var variableElements = new ArrayList(); + + for (SSZReadable element : elements) { + if (element.isFixed()) { + element.populateFromReader(this); + } else { + variableElements.add(new ElementOffset(readUInt32(), element)); + } + } + if (variableElements.isEmpty()) { + return; + } + for (int i = 0; i < variableElements.size() - 1; i++) { + if (variableElements.get(i).getOffset() != index) { + throw new InvalidSSZTypeException("Variable elements are not in order"); + } + int length = (int) (variableElements.get(i + 1).offset - variableElements.get(i).offset); + variableElements.get(i).getElement().populateFromReader(this.slice(length)); + this.index += length; + } + variableElements + .get(variableElements.size() - 1) + .getElement() + .populateFromReader(this.slice(content.size() - index)); + } + + private SSZReader slice(int length) { + return new BytesSSZReader(content.slice(index, length)); + } + + private static class ElementOffset { + final long offset; + final SSZReadable element; + + public ElementOffset(long offset, SSZReadable element) { + this.offset = offset; + this.element = element; + } + + public long getOffset() { + return offset; + } + + public SSZReadable getElement() { + return element; + } + } + + @Override + public List readFixedTypedList(int elementSize, Supplier supplier) { + int listSize = (content.size() - index) / elementSize; + return readFixedList( + listSize, + remaining -> readFixedByteArray(elementSize, listSize * elementSize), + bytes -> { + T t = supplier.get(); + t.populateFromReader(new BytesSSZReader(Bytes.wrap(bytes))); + return t; + }); + } + + @Override + public List readTypedVector( + int listSize, int elementSize, Supplier supplier) { + return readFixedList( + listSize, + remaining -> readFixedByteArray(elementSize, listSize * elementSize), + bytes -> { + T t = supplier.get(); + t.populateFromReader(new BytesSSZReader(Bytes.wrap(bytes))); + return t; + }); + } + + @Override + public List readVariableSizeTypeList(Supplier supplier) { + if (content.size() == index) { + return Collections.emptyList(); + } + final long firstOffset = readUInt32(); + int size = (int) (firstOffset / 4) + 1; + + List lengths = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + lengths.add((int) (readUInt32() - firstOffset)); + } + List elements = new ArrayList<>(size); + for (Integer length : lengths) { + T t = supplier.get(); + t.populateFromReader(this.slice(length)); + this.index += length; + elements.add(t); + } + T t = supplier.get(); + t.populateFromReader(this.slice(content.size() - index)); + elements.add(t); + return Collections.unmodifiableList(elements); + } +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZWriter.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZWriter.java new file mode 100644 index 000000000..12a518d26 --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZWriter.java @@ -0,0 +1,25 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import org.apache.tuweni.v2.bytes.Bytes; + +import java.util.ArrayList; +import java.util.List; + +final class BytesSSZWriter implements SSZWriter { + + private final List values = new ArrayList<>(); + + @Override + public void writeSSZ(Bytes value) { + values.add(value); + } + + Bytes toBytes() { + if (values.isEmpty()) { + return Bytes.EMPTY; + } + return Bytes.wrap(values.toArray(new Bytes[0])); + } +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZ.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZ.java new file mode 100644 index 000000000..ba2909381 --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZ.java @@ -0,0 +1,2037 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import static java.nio.ByteOrder.LITTLE_ENDIAN; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +import org.apache.tuweni.ssz.EndOfSSZException; +import org.apache.tuweni.ssz.InvalidSSZTypeException; +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.crypto.Hash; +import org.apache.tuweni.v2.units.bigints.UInt256; +import org.apache.tuweni.v2.units.bigints.UInt384; + +import java.math.BigInteger; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.nio.ReadOnlyBufferException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** Simple Serialize (SSZ) encoding and decoding. */ +public final class SSZ { + + private static final Bytes TRUE = Bytes.of((byte) 1); + private static final Bytes FALSE = Bytes.of((byte) 0); + + private SSZ() {} + + /** + * Create the hash tree root of a set of values + * + * @param bytes 1 value or a list of homogeneous values + * @return the SSZ tree root hash of the values + */ + public static Bytes hashTreeRoot(Bytes... bytes) { + if (bytes.length == 1) { + if (bytes[0].size() > 32) { + return Hash.keccak256(bytes[0]); + } else { + return bytes[0].mutableCopy().rightPad(32); + } + } else { + Bytes hash = merkleHash(new ArrayList<>(Arrays.asList(bytes))); + return hash.mutableCopy().rightPad(32); + } + } + + /** + * Hashes a list of homogeneous values. + * + * @param values a list of homogeneous values + * @return the merkle hash of the list of values + */ + static Bytes merkleHash(List values) { + Bytes littleEndianLength = Bytes.ofUnsignedInt(values.size(), LITTLE_ENDIAN); + Bytes valuesLength = littleEndianLength.mutableCopy().rightPad(32); + + List chunks; + if (values.isEmpty()) { + chunks = new ArrayList<>(); + chunks.add(Bytes.wrap(new byte[128])); + } else if (values.get(0).size() < 128) { + int itemsPerChunk = (int) Math.floor(128 / (double) values.get(0).size()); + chunks = new ArrayList<>(); + + for (int i = 0; i * itemsPerChunk < values.size(); i++) { + Bytes[] chunkItems = + values + .subList(i * itemsPerChunk, Math.min((i + 1) * itemsPerChunk, values.size())) + .toArray(new Bytes[0]); + chunks.add(Bytes.wrap(chunkItems)); + } + } else { + chunks = values; + } + while (chunks.size() > 1) { + if (chunks.size() % 2 == 1) { + chunks.add(Bytes.wrap(new byte[128])); + } + Iterator iterator = chunks.iterator(); + List hashRound = new ArrayList<>(); + while (iterator.hasNext()) { + hashRound.add(Hash.keccak256(Bytes.wrap(iterator.next(), iterator.next()))); + } + chunks = hashRound; + } + + return Hash.keccak256(Bytes.wrap(chunks.getFirst(), valuesLength)); + } + + // Encoding + + /** + * Encode values to a {@link Bytes} value. + * + * @param fn A consumer that will be provided with a {@link SSZWriter} that can consume values. + * @return The SSZ encoding in a {@link Bytes} value. + */ + public static Bytes encode(Consumer fn) { + requireNonNull(fn); + BytesSSZWriter writer = new BytesSSZWriter(); + fn.accept(writer); + return writer.toBytes(); + } + + /** + * Encode values to a {@link ByteBuffer}. + * + * @param buffer The buffer to write into, starting from its current position. + * @param fn A consumer that will be provided with a {@link SSZWriter} that can consume values. + * @param The type of the buffer. + * @return The buffer. + * @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 static T encodeTo(T buffer, Consumer fn) { + requireNonNull(buffer); + requireNonNull(fn); + ByteBufferSSZWriter writer = new ByteBufferSSZWriter(buffer); + fn.accept(writer); + return buffer; + } + + /** + * Encode {@link Bytes}. + * + * @param value The value to encode. + * @return The SSZ encoding in a {@link Bytes} value. + */ + public static Bytes encodeBytes(Bytes value) { + Bytes lengthBytes = encodeLong(value.size(), 32); + return Bytes.wrap(lengthBytes, value); + } + + static void encodeBytesTo(Bytes value, Consumer appender) { + appender.accept(encodeLong(value.size(), 32)); + appender.accept(value); + } + + static void encodeFixedBytesTo(Bytes value, Consumer appender) { + appender.accept(value); + } + + /** + * Encode a value to a {@link Bytes} value. + * + * @param value The value to encode. + * @return The SSZ encoding in a {@link Bytes} value. + */ + public static Bytes encodeByteArray(byte[] value) { + return encodeBytes(Bytes.wrap(value)); + } + + static void encodeByteArrayTo(byte[] value, Consumer appender) { + appender.accept(encodeLongToByteArray(value.length, 32)); + appender.accept(value); + } + + /** + * Encode a string to a {@link Bytes} value. + * + * @param str The string to encode. + * @return The SSZ encoding in a {@link Bytes} value. + */ + public static Bytes encodeString(String str) { + return encodeByteArray(str.getBytes(UTF_8)); + } + + static void encodeStringTo(String str, Consumer appender) { + encodeByteArrayTo(str.getBytes(UTF_8), appender); + } + + /** + * Encode a two's-compliment integer to a {@link Bytes} value. + * + * @param value the integer to encode + * @param bitLength the bit length of the encoded integer value (must be a multiple of 8) + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} + */ + public static Bytes encodeInt(int value, int bitLength) { + return encodeLong(value, bitLength); + } + + /** + * Encode a two's-compliment long integer to a {@link Bytes} value. + * + * @param value the long to encode + * @param bitLength the bit length of the integer value (must be a multiple of 8) + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} + */ + public static Bytes encodeLong(long value, int bitLength) { + return Bytes.wrap(encodeLongToByteArray(value, bitLength)); + } + + static byte[] encodeLongToByteArray(long value, int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + int zeros = + (value >= 0) ? Long.numberOfLeadingZeros(value) : Long.numberOfLeadingZeros(-1 - value) - 1; + int valueBytes = 8 - (zeros / 8); + + int byteLength = bitLength / 8; + if (valueBytes > byteLength) { + throw new IllegalArgumentException("value is too large for the desired bitLength"); + } + + byte[] encoded = new byte[byteLength]; + + int shift = 0; + for (int i = 0; i < valueBytes; i++) { + encoded[i] = (byte) ((value >> shift) & 0xFF); + shift += 8; + } + if (value < 0) { + // Extend the two's-compliment integer by setting all remaining bits to 1. + for (int i = valueBytes; i < byteLength; i++) { + encoded[i] = (byte) 0xFF; + } + } + return encoded; + } + + /** + * Encode a big integer to a {@link Bytes} value. + * + * @param value the big integer to encode + * @param bitLength the bit length of the integer value (must be a multiple of 8) + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} + */ + public static Bytes encodeBigInteger(BigInteger value, int bitLength) { + return Bytes.wrap(encodeBigIntegerToByteArray(value, bitLength)); + } + + public static byte[] encodeBigIntegerToByteArray(BigInteger value, int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + byte[] bytes = value.toByteArray(); + int valueBytes = bytes.length; + int offset = 0; + if (value.signum() >= 0 && bytes[0] == 0) { + valueBytes = bytes.length - 1; + offset = 1; + } + + int byteLength = bitLength / 8; + if (valueBytes > byteLength) { + throw new IllegalArgumentException("value is too large for the desired bitLength"); + } + + byte[] encoded; + if (valueBytes == byteLength && offset == 0) { + encoded = bytes; + } else { + encoded = new byte[byteLength]; + int padLength = byteLength - valueBytes; + System.arraycopy(bytes, offset, encoded, padLength, valueBytes); + if (value.signum() < 0) { + // Extend the two's-compliment integer by setting all leading bits to 1. + for (int i = 0; i < padLength; i++) { + encoded[i] = (byte) 0xFF; + } + } + } + // reverse the array to make it little endian + for (int i = 0; i < (encoded.length / 2); i++) { + byte swapped = encoded[i]; + encoded[i] = encoded[encoded.length - i - 1]; + encoded[encoded.length - i - 1] = swapped; + } + return encoded; + } + + /** + * Encode an 8-bit two's-compliment integer to a {@link Bytes} value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large to be represented in 8 bits + */ + public static Bytes encodeInt8(int value) { + return encodeInt(value, 8); + } + + /** + * Encode a 16-bit two's-compliment integer to a {@link Bytes} value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large to be represented in 16 bits + */ + public static Bytes encodeInt16(int value) { + return encodeInt(value, 16); + } + + /** + * Encode a 32-bit two's-compliment integer to a {@link Bytes} value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeInt32(int value) { + return encodeInt(value, 32); + } + + /** + * Encode a 64-bit two's-compliment integer to a {@link Bytes} value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeInt64(long value) { + return encodeLong(value, 64); + } + + /** + * Encode an unsigned integer to a {@link Bytes} value. + * + *

Note that {@code value} is a native signed int, but will be interpreted as an unsigned + * value. + * + * @param value the integer to encode + * @param bitLength the bit length of the encoded integer value (must be a multiple of 8) + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} + */ + public static Bytes encodeUInt(int value, int bitLength) { + return encodeULong(value, bitLength); + } + + /** + * Encode an unsigned long integer to a {@link Bytes} value. + * + *

Note that {@code value} is a native signed long, but will be interpreted as an unsigned + * value. + * + * @param value the long to encode + * @param bitLength the bit length of the integer value (must be a multiple of 8) + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} + */ + public static Bytes encodeULong(long value, int bitLength) { + return Bytes.wrap(encodeULongToByteArray(value, bitLength)); + } + + static byte[] encodeULongToByteArray(long value, int bitLength) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + int zeros = Long.numberOfLeadingZeros(value); + int valueBytes = 8 - (zeros / 8); + if (zeros != 0 && value < 0) { + throw new IllegalArgumentException("Value must be positive or zero"); + } + + int byteLength = bitLength / 8; + if (valueBytes > byteLength) { + throw new IllegalArgumentException("value is too large for the desired bitLength"); + } + + byte[] encoded = new byte[byteLength]; + + int shift = 0; + for (int i = 0; i < valueBytes; i++) { + encoded[i] = (byte) ((value >> shift) & 0xFF); + shift += 8; + } + return encoded; + } + + /** + * Encode an unsigned big integer to a {@link Bytes} value. + * + * @param value the big integer to encode + * @param bitLength the bit length of the integer value (must be a multiple of 8) + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} + */ + public static Bytes encodeUBigInteger(BigInteger value, int bitLength) { + return Bytes.wrap(encodeUBigIntegerToByteArray(value, bitLength)); + } + + public static byte[] encodeUBigIntegerToByteArray(BigInteger value, int bitLength) { + if (value.compareTo(BigInteger.ZERO) < 0) { + throw new IllegalArgumentException("Value must be positive or zero"); + } + return encodeBigIntegerToByteArray(value, bitLength); + } + + /** + * Encode an 8-bit unsigned integer to a {@link Bytes} value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large to be represented in 8 bits + */ + public static Bytes encodeUInt8(int value) { + return encodeUInt(value, 8); + } + + /** + * Encode a 16-bit unsigned integer to a {@link Bytes} value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if the value is too large to be represented in 16 bits + */ + public static Bytes encodeUInt16(int value) { + return encodeUInt(value, 16); + } + + /** + * Encode a 32-bit unsigned integer to a {@link Bytes} value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt32(long value) { + return encodeULong(value, 32); + } + + /** + * Encode a 64-bit unsigned integer to a {@link Bytes} value. + * + *

Note that {@code value} is a native signed long, but will be interpreted as an unsigned + * value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt64(long value) { + return encodeULong(value, 64); + } + + /** + * Encode a 256-bit unsigned integer to a little-endian {@link Bytes} value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt256(UInt256 value) { + return value.mutableCopy().reverse(); + } + + /** + * Encode a 384-bit unsigned integer to a little-endian {@link Bytes} value. + * + * @param value the integer to encode + * @return the SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt384(UInt384 value) { + return value.mutableCopy().reverse(); + } + + /** + * Encode a boolean to a {@link Bytes} value. + * + * @param value the boolean to encode + * @return the SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeBoolean(boolean value) { + return value ? TRUE : FALSE; + } + + /** + * Encode a 20-byte address to a {@link Bytes} value. + * + * @param address the address (must be exactly 20 bytes) + * @return the SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if {@code address.size != 20} + */ + public static Bytes encodeAddress(Bytes address) { + if (address.size() != 20) { + throw new IllegalArgumentException("address is not 20 bytes"); + } + return address; + } + + /** + * Encode a hash to a {@link Bytes} value. + * + * @param hash the hash + * @return the SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeHash(Bytes hash) { + return hash; + } + + /** + * Encode a list of bytes. + * + * @param elements the bytes to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeBytesList(Bytes... elements) { + ArrayList encoded = new ArrayList<>(elements.length * 2 + 1); + encodeBytesListTo(elements, encoded::add); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of bytes. + * + * @param elements the bytes to write as a list + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeBytesList(List elements) { + ArrayList encoded = new ArrayList<>(elements.size() * 2 + 1); + encodeBytesListTo(elements, encoded::add); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeBytesListTo(Bytes[] elements, Consumer appender) { + // pre-calculate the list size - relies on knowing how encodeBytesTo does its serialization, but + // is worth it + // to avoid having to pre-serialize all the elements + long listSize = 0; + for (Bytes bytes : elements) { + listSize += 4; + listSize += bytes.size(); + if (listSize > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Cannot serialize list: overall length is too large"); + } + } + appender.accept(encodeUInt32(listSize)); + for (Bytes bytes : elements) { + encodeBytesTo(bytes, appender); + } + } + + static void encodeBytesListTo(List elements, Consumer appender) { + // pre-calculate the list size - relies on knowing how encodeBytesTo does its serialization, but + // is worth it + // to avoid having to pre-serialize all the elements + long listSize = 0; + for (Bytes bytes : elements) { + listSize += 4; + listSize += bytes.size(); + if (listSize > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Cannot serialize list: overall length is too large"); + } + } + appender.accept(encodeUInt32(listSize)); + for (Bytes bytes : elements) { + encodeBytesTo(bytes, appender); + } + } + + static void encodeFixedBytesVectorTo(List elements, Consumer appender) { + for (Bytes bytes : elements) { + appender.accept(bytes); + } + } + + static void encodeBytesVectorTo(List elements, Consumer appender) { + for (Bytes bytes : elements) { + appender.accept(encodeLong(bytes.size(), 32)); + appender.accept(bytes); + } + } + + static void encodeFixedBytesListTo(List elements, Consumer appender) { + // pre-calculate the list size - relies on knowing how encodeBytesTo does its serialization, but + // is worth it + // to avoid having to pre-serialize all the elements + long listSize = 0; + for (Bytes bytes : elements) { + listSize += bytes.size(); + if (listSize > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Cannot serialize list: overall length is too large"); + } + } + appender.accept(encodeUInt32(listSize)); + for (Bytes bytes : elements) { + encodeFixedBytesTo(bytes, appender); + } + } + + /** + * Encode a list of strings. + * + * @param elements the strings to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeStringList(String... elements) { + ArrayList encoded = new ArrayList<>(elements.length * 2 + 1); + encodeStringListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of strings + * + * @param elements the list of strings to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeStringList(List elements) { + ArrayList encoded = new ArrayList<>(elements.size() * 2 + 1); + encodeStringListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeStringListTo(String[] elements, Consumer appender) { + Bytes[] elementBytes = new Bytes[elements.length]; + for (int i = 0; i < elements.length; ++i) { + elementBytes[i] = Bytes.wrap(elements[i].getBytes(UTF_8)); + } + encodeBytesListTo(elementBytes, appender); + } + + static void encodeStringListTo(List elements, Consumer appender) { + Bytes[] elementBytes = new Bytes[elements.size()]; + for (int i = 0; i < elements.size(); ++i) { + elementBytes[i] = Bytes.wrap(elements.get(i).getBytes(UTF_8)); + } + encodeBytesListTo(elementBytes, appender); + } + + /** + * Encode a list of two's compliment integers. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large for the specified {@code + * bitLength} + */ + public static Bytes encodeIntList(int bitLength, int... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of two's compliment integers. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the list of Integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large for the specified {@code + * bitLength} + */ + public static Bytes encodeIntList(int bitLength, List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeIntListTo(int bitLength, int[] elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.length, bitLength / 8)); + for (int value : elements) { + appender.accept(encodeLongToByteArray(value, bitLength)); + } + } + + static void encodeIntListTo(int bitLength, List elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); + for (int value : elements) { + appender.accept(encodeLongToByteArray(value, bitLength)); + } + } + + /** + * Encode a list of two's compliment long integers. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large for the specified {@code + * bitLength} + */ + public static Bytes encodeLongIntList(int bitLength, long... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeLongIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of two's compliment long integers. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the list of Longs to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large for the specified {@code + * bitLength} + */ + public static Bytes encodeLongIntList(int bitLength, List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeLongIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeLongIntListTo(int bitLength, long[] elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.length, bitLength / 8)); + for (long value : elements) { + appender.accept(encodeLongToByteArray(value, bitLength)); + } + } + + static void encodeLongIntListTo(int bitLength, List elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); + for (long value : elements) { + appender.accept(encodeLongToByteArray(value, bitLength)); + } + } + + /** + * Encode a list of big integers. + * + * @param bitLength The bit length of the encoded integers (must be a multiple of 8). + * @param elements The integers to write. + * @return SSZ encoding in a {@link Bytes} value. + * @throws IllegalArgumentException If any values are too large for the specified {@code + * bitLength}. + */ + public static Bytes encodeBigIntegerList(int bitLength, BigInteger... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeBigIntegerListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of big integers. + * + * @param bitLength The bit length of the encoded integers (must be a multiple of 8). + * @param elements The list of BigIntegers to write. + * @return SSZ encoding in a {@link Bytes} value. + * @throws IllegalArgumentException If any values are too large for the specified {@code + * bitLength}. + */ + public static Bytes encodeBigIntegerList(int bitLength, List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeBigIntegerListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeBigIntegerListTo( + int bitLength, BigInteger[] elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.length, bitLength / 8)); + for (BigInteger value : elements) { + appender.accept(encodeBigIntegerToByteArray(value, bitLength)); + } + } + + static void encodeBigIntegerListTo( + int bitLength, List elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); + for (BigInteger value : elements) { + appender.accept(encodeBigIntegerToByteArray(value, bitLength)); + } + } + + /** + * Encode a list of 8-bit two's compliment integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 8 bits + */ + public static Bytes encodeInt8List(int... elements) { + return encodeIntList(8, elements); + } + + /** + * Encode a list of 8-bit two's compliment integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 8 bits + */ + public static Bytes encodeInt8List(List elements) { + return encodeIntList(8, elements); + } + + /** + * Encode a list of 16-bit two's compliment integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 16 bits + */ + public static Bytes encodeInt16List(int... elements) { + return encodeIntList(16, elements); + } + + /** + * Encode a list of 16-bit two's compliment integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 16 bits + */ + public static Bytes encodeInt16List(List elements) { + return encodeIntList(16, elements); + } + + /** + * Encode a list of 32-bit two's compliment integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeInt32List(int... elements) { + return encodeIntList(32, elements); + } + + /** + * Encode a list of 32-bit two's compliment integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeInt32List(List elements) { + return encodeIntList(32, elements); + } + + /** + * Encode a list of 64-bit two's compliment integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeInt64List(long... elements) { + return encodeLongIntList(64, elements); + } + + /** + * Encode a list of 64-bit two's compliment integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeInt64List(List elements) { + return encodeLongIntList(64, elements); + } + + /** + * Encode a list of unsigned integers. + * + *

Note that the {@code elements} are native signed ints, but will be interpreted as an + * unsigned values. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large for the specified {@code + * bitLength} + */ + public static Bytes encodeUIntList(int bitLength, int... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeUIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of unsigned integers. + * + *

Note that the {@code elements} are native signed ints, but will be interpreted as an + * unsigned values. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large for the specified {@code + * bitLength} + */ + public static Bytes encodeUIntList(int bitLength, List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeUIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeUIntListTo(int bitLength, int[] elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.length, bitLength / 8)); + for (int value : elements) { + appender.accept(encodeULongToByteArray(value, bitLength)); + } + } + + static void encodeUIntListTo(int bitLength, List elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); + for (int value : elements) { + appender.accept(encodeULongToByteArray(value, bitLength)); + } + } + + /** + * Encode a list of unsigned long integers. + * + *

Note that the {@code elements} are native signed longs, but will be interpreted as an + * unsigned values. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large for the specified {@code + * bitLength} + */ + public static Bytes encodeULongIntList(int bitLength, long... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeULongIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of unsigned long integers. + * + *

Note that the {@code elements} are native signed longs, but will be interpreted as an + * unsigned values. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large for the specified {@code + * bitLength} + */ + public static Bytes encodeULongIntList(int bitLength, List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeULongIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeULongIntListTo(int bitLength, long[] elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.length, bitLength / 8)); + for (long value : elements) { + appender.accept(encodeULongToByteArray(value, bitLength)); + } + } + + static void encodeULongIntListTo(int bitLength, List elements, Consumer appender) { + if (bitLength % 8 != 0) { + throw new IllegalArgumentException("bitLength must be a multiple of 8"); + } + appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); + for (long value : elements) { + appender.accept(encodeULongToByteArray(value, bitLength)); + } + } + + /** + * Encode a list of 8-bit unsigned integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 8 bits + */ + public static Bytes encodeUInt8List(int... elements) { + return encodeUIntList(8, elements); + } + + /** + * Encode a list of 8-bit unsigned integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 8 bits + */ + public static Bytes encodeUInt8List(List elements) { + return encodeUIntList(8, elements); + } + + /** + * Encode a list of 16-bit unsigned integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 16 bits + */ + public static Bytes encodeUInt16List(int... elements) { + return encodeUIntList(16, elements); + } + + /** + * Encode a list of 16-bit unsigned integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 16 bits + */ + public static Bytes encodeUInt16List(List elements) { + return encodeUIntList(16, elements); + } + + /** + * Encode a list of 32-bit unsigned integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 32 bits + */ + public static Bytes encodeUInt32List(long... elements) { + return encodeULongIntList(32, elements); + } + + /** + * Encode a list of 32-bit unsigned integers. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any values are too large to be represented in 32 bits + */ + public static Bytes encodeUInt32List(List elements) { + return encodeULongIntList(32, elements); + } + + /** + * Encode a list of 64-bit unsigned integers. + * + *

Note that the {@code elements} are native signed longs, but will be interpreted as an + * unsigned values. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt64List(long... elements) { + return encodeULongIntList(64, elements); + } + + /** + * Encode a list of 64-bit unsigned integers. + * + *

Note that the {@code elements} are native signed longs, but will be interpreted as an + * unsigned values. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt64List(List elements) { + return encodeULongIntList(64, elements); + } + + /** + * Encode a list of {@link UInt256}. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt256List(UInt256... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeUInt256ListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of {@link UInt256}. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt256List(List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeUInt256ListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeUInt256ListTo(UInt256[] elements, Consumer appender) { + appender.accept(Bytes.wrap(listLengthPrefix(elements.length, 256 / 8))); + for (UInt256 value : elements) { + appender.accept(encodeUInt256(value)); + } + } + + static void encodeUInt256ListTo(List elements, Consumer appender) { + appender.accept(Bytes.wrap(listLengthPrefix(elements.size(), 256 / 8))); + for (UInt256 value : elements) { + appender.accept(encodeUInt256(value)); + } + } + + /** + * Encode a list of {@link UInt384}. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt384List(UInt384... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeUInt384ListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of {@link UInt384}. + * + * @param elements the integers to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeUInt384List(List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeUInt384ListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeUInt384ListTo(UInt384[] elements, Consumer appender) { + appender.accept(Bytes.wrap(listLengthPrefix(elements.length, 256 / 8))); + for (UInt384 value : elements) { + appender.accept(encodeUInt384(value)); + } + } + + static void encodeUInt384ListTo(List elements, Consumer appender) { + appender.accept(Bytes.wrap(listLengthPrefix(elements.size(), 256 / 8))); + for (UInt384 value : elements) { + appender.accept(encodeUInt384(value)); + } + } + + /** + * Encode a list of hashes. + * + * @param elements the hashes to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeHashList(Bytes... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeHashListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of hashes. + * + * @param elements the hashes to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeHashList(List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeHashListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeHashListTo(Bytes[] elements, Consumer appender) { + int hashLength = 0; + for (Bytes bytes : elements) { + if (hashLength == 0) { + hashLength = bytes.size(); + } else { + if (bytes.size() != hashLength) { + throw new IllegalArgumentException("Hashes must be all of the same size"); + } + } + } + appender.accept(Bytes.wrap(listLengthPrefix(elements.length, 32))); + for (Bytes bytes : elements) { + appender.accept(bytes); + } + } + + static void encodeHashListTo(List elements, Consumer appender) { + int hashLength = 0; + for (Bytes bytes : elements) { + if (hashLength == 0) { + hashLength = bytes.size(); + } else { + if (bytes.size() != hashLength) { + throw new IllegalArgumentException("Hashes must be all of the same size"); + } + } + } + appender.accept(Bytes.wrap(listLengthPrefix(elements.size(), 32))); + for (Bytes bytes : elements) { + appender.accept(bytes); + } + } + + /** + * Encode a list of addresses. + * + * @param elements the addresses to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any {@code address.size != 20} + */ + public static Bytes encodeAddressList(Bytes... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeAddressListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of addresses. + * + * @param elements the addresses to write + * @return SSZ encoding in a {@link Bytes} value + * @throws IllegalArgumentException if any {@code address.size != 20} + */ + public static Bytes encodeAddressList(List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeAddressListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeAddressListTo(Bytes[] elements, Consumer appender) { + appender.accept(Bytes.wrap(listLengthPrefix(elements.length, 20))); + for (Bytes bytes : elements) { + appender.accept(encodeAddress(bytes)); + } + } + + static void encodeAddressListTo(List elements, Consumer appender) { + appender.accept(Bytes.wrap(listLengthPrefix(elements.size(), 20))); + for (Bytes bytes : elements) { + appender.accept(encodeAddress(bytes)); + } + } + + /** + * Encode a list of booleans. + * + * @param elements the booleans to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeBooleanList(boolean... elements) { + ArrayList encoded = new ArrayList<>(elements.length + 1); + encodeBooleanListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + /** + * Encode a list of booleans. + * + * @param elements the booleans to write + * @return SSZ encoding in a {@link Bytes} value + */ + public static Bytes encodeBooleanList(List elements) { + ArrayList encoded = new ArrayList<>(elements.size() + 1); + encodeBooleanListTo(elements, b -> encoded.add(Bytes.wrap(b))); + return Bytes.wrap(encoded.toArray(new Bytes[0])); + } + + static void encodeBooleanListTo(boolean[] elements, Consumer appender) { + appender.accept(encodeInt32(elements.length)); + for (boolean value : elements) { + appender.accept(encodeBoolean(value)); + } + } + + static void encodeBooleanListTo(List elements, Consumer appender) { + appender.accept(encodeInt32(elements.size())); + for (boolean value : elements) { + appender.accept(encodeBoolean(value)); + } + } + + public static void encodeAsFixedTypeListTo( + List elements, Consumer appender) { + for (T value : elements) { + appender.accept(SSZ.encode(value::writeTo)); + } + } + + public static void encodeAsFixedTypeVectorTo( + List elements, Consumer appender) { + for (T value : elements) { + appender.accept(SSZ.encode(value::writeTo)); + } + } + + public static void encodeAsContainerTo(Consumer consumer, SSZWritable... elements) { + long written = 0; + long variableOffset = 0; + List> fixedParts = new ArrayList<>(); + List offsetSuppliers = new ArrayList<>(); + List variableParts = new ArrayList<>(); + + for (SSZWritable element : elements) { + BytesSSZWriter fixedWriter = new BytesSSZWriter(); + element.writeTo(fixedWriter); + Bytes bytes = fixedWriter.toBytes(); + if (element.isFixed()) { + fixedParts.add(() -> bytes); + written += bytes.size(); + } else { + var offsetProvider = new OffsetSupplier(variableOffset); + fixedParts.add(offsetProvider); + offsetSuppliers.add(offsetProvider); + written += 4; + variableParts.add(bytes); + variableOffset += bytes.size(); + } + } + for (OffsetSupplier offsetSupplier : offsetSuppliers) { + offsetSupplier.addWritten(written); + } + for (Supplier fixedPart : fixedParts) { + consumer.accept(fixedPart.get()); + } + for (Bytes bytes : variableParts) { + consumer.accept(bytes); + } + } + + private static class OffsetSupplier implements Supplier { + + private final long offset; + private long written; + + public OffsetSupplier(long offset) { + this.offset = offset; + } + + @Override + public Bytes get() { + return SSZ.encodeUInt32(written + offset); + } + + public void addWritten(long written) { + this.written = written; + } + } + + private static byte[] listLengthPrefix(long nElements, int elementBytes) { + long listSize; + try { + listSize = Math.multiplyExact(nElements, (long) elementBytes); + } catch (ArithmeticException e) { + listSize = Long.MAX_VALUE; + } + if (listSize > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Cannot serialize list: overall length is too large"); + } + return encodeLongToByteArray(listSize, 32); + } + + // Decoding + + /** + * Read and decode SSZ from a {@link Bytes} value. + * + * @param source the SSZ encoded bytes + * @param fn a function that will be provided a {@link SSZReader} + * @param the result type of the reading function + * @return the result from the reading function + */ + public static T decode(Bytes source, Function fn) { + requireNonNull(source); + requireNonNull(fn); + return fn.apply(new BytesSSZReader(source)); + } + + /** + * Read a SSZ encoded bytes from a {@link Bytes} value. + * + *

Note: prefer to use {@link #decodeBytes(Bytes, int)} instead, especially when reading + * untrusted data. + * + * @param source the SSZ encoded bytes + * @return the bytes + * @throws InvalidSSZTypeException if the next SSZ value is not a byte array, or is too large + * (greater than 2^32 bytes) + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static Bytes decodeBytes(Bytes source) { + return decode(source, SSZReader::readBytes); + } + + /** + * Read a SSZ encoded bytes from a {@link Bytes} value. + * + * @param source the SSZ encoded bytes + * @param limit the maximum number of bytes to read + * @return the bytes + * @throws InvalidSSZTypeException if the next SSZ value is not a byte array, or would exceed the + * limit + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static Bytes decodeBytes(Bytes source, int limit) { + return decode(source, r -> r.readBytes(limit)); + } + + /** + * Read a SSZ encoded string from a {@link Bytes} value. + * + *

Note: prefer to use {@link #decodeString(Bytes, int)} instead, especially when reading + * untrusted data. + * + * @param source the SSZ encoded bytes + * @return a string + * @throws InvalidSSZTypeException if the next SSZ value is not a byte array, or is too large + * (greater than 2^32 bytes) + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static String decodeString(Bytes source) { + return decode(source, SSZReader::readString); + } + + /** + * Read a SSZ encoded string from a {@link Bytes} value. + * + * @param source the SSZ encoded bytes + * @param limit the maximum number of bytes to read + * @return a string + * @throws InvalidSSZTypeException if the next SSZ value is not a byte array, or would exceed the + * limit + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static String decodeString(Bytes source, int limit) { + return decode(source, r -> r.readString(limit)); + } + + /** + * Read a SSZ encoded two's-compliment integer from a {@link Bytes} value. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integer to read (a multiple of 8) + * @return an int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit + * length, or the decoded value was too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static int decodeInt(Bytes source, int bitLength) { + return decode(source, r -> r.readInt(bitLength)); + } + + /** + * Read a SSZ encoded two's-compliment long integer from a {@link Bytes} value. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integer to read (a multiple of 8) + * @return a long + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit + * length, or the decoded value was too large to fit into a long + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static long decodeLong(Bytes source, int bitLength) { + return decode(source, r -> r.readLong(bitLength)); + } + + /** + * Read a SSZ encoded two's-compliment big integer from a {@link Bytes} value. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integer to read (a multiple of 8) + * @return a string + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit + * length + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static BigInteger decodeBigInteger(Bytes source, int bitLength) { + return decode(source, r -> r.readBigInteger(bitLength)); + } + + /** + * Read an 8-bit two's-compliment integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return an int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for an 8-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static int decodeInt8(Bytes source) { + return decodeInt(source, 8); + } + + /** + * Read a 16-bit two's-compliment integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return an int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 16-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static int decodeInt16(Bytes source) { + return decodeInt(source, 16); + } + + /** + * Read a 32-bit two's-compliment integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return an int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 32-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static int decodeInt32(Bytes source) { + return decodeInt(source, 32); + } + + /** + * Read a 64-bit two's-compliment integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return an int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 64-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static long decodeInt64(Bytes source) { + return decodeLong(source, 64); + } + + /** + * Read a SSZ encoded unsigned integer from a {@link Bytes} value. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integers to read (a multiple of 8) + * @return an unsigned int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit + * length, or the decoded value was too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static int decodeUInt(Bytes source, int bitLength) { + return decode(source, r -> r.readUInt(bitLength)); + } + + /** + * Read a SSZ encoded unsigned long integer from a {@link Bytes} value. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integers to read (a multiple of 8) + * @return an unsigned long + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit + * length, or the decoded value was too large to fit into a long + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static long decodeULong(Bytes source, int bitLength) { + return decode(source, r -> r.readULong(bitLength)); + } + + /** + * Read a SSZ encoded unsigned big integer from a {@link Bytes} value. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integers to read (a multiple of 8) + * @return a string + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit + * length + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static BigInteger decodeUnsignedBigInteger(Bytes source, int bitLength) { + return decode(source, r -> r.readBigInteger(bitLength)); + } + + /** + * Read an 8-bit unsigned integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return an int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for an 8-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static int decodeUInt8(Bytes source) { + return decodeUInt(source, 8); + } + + /** + * Read a 16-bit unsigned integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return an int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 16-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static int decodeUInt16(Bytes source) { + return decodeUInt(source, 16); + } + + /** + * Read a 32-bit unsigned integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return an int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 32-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static long decodeUInt32(Bytes source) { + return decodeULong(source, 32); + } + + /** + * Read a 64-bit unsigned integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return an int + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 64-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static long decodeUInt64(Bytes source) { + return decodeLong(source, 64); + } + + /** + * Read a 256-bit unsigned integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a {@link UInt256} + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 256-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static UInt256 decodeUInt256(Bytes source) { + return decode(source, SSZReader::readUInt256); + } + + /** + * Read a 384-bit unsigned integer from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a {@link UInt384} + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 384-bit int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static UInt384 decodeUInt384(Bytes source) { + return decode(source, SSZReader::readUInt384); + } + + /** + * Read a boolean value from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a boolean + * @throws InvalidSSZTypeException if the decoded value is not a boolean + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static boolean decodeBoolean(Bytes source) { + return decode(source, SSZReader::readBoolean); + } + + /** + * Read a 20-byte address from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return the bytes of the Address + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 20-byte address + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static Bytes decodeAddress(Bytes source) { + return decode(source, SSZReader::readAddress); + } + + /** + * Read a 32-byte hash from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param hashLength the length of the hash (in bytes) + * @return the bytes of the hash + * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 32-byte hash + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static Bytes decodeHash(Bytes source, int hashLength) { + return decode(source, r -> r.readHash(hashLength)); + } + + /** + * Read a list of {@link Bytes} from the SSZ source. + * + *

Note: prefer to use {@link #decodeBytesList(Bytes, int)} instead, especially when reading + * untrusted data. + * + * @param source the SSZ encoded bytes + * @return a list of {@link Bytes} + * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is + * not a byte array, or any byte array is too large (greater than 2^32 bytes) + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeBytesList(Bytes source) { + return decode(source, SSZReader::readBytesList); + } + + /** + * Read a list of {@link Bytes} from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param limit the maximum number of bytes to read for each list element + * @return a list of {@link Bytes} + * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is + * not a byte array, or any byte array is too large (greater than 2^32 bytes) + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeBytesList(Bytes source, int limit) { + return decode(source, r -> r.readBytesList(limit)); + } + + /** + * Read a list of byte arrays from the SSZ source. + * + *

Note: prefer to use {@link #decodeByteArrayList(Bytes, int)} instead, especially when + * reading untrusted data. + * + * @param source the SSZ encoded bytes + * @return a list of byte arrays + * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is + * not a byte array, or any byte array is too large (greater than 2^32 bytes) + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeByteArrayList(Bytes source) { + return decode(source, SSZReader::readByteArrayList); + } + + /** + * Read a list of byte arrays from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param limit The maximum number of bytes to read for each list element. + * @return a list of byte arrays + * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is + * not a byte array, or any byte array is too large (greater than 2^32 bytes) + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeByteArrayList(Bytes source, int limit) { + return decode(source, r -> r.readByteArrayList(limit)); + } + + /** + * Read a list of strings from the SSZ source. + * + *

Note: prefer to use {@link #decodeStringList(Bytes, int)} instead, especially when reading + * untrusted data. + * + * @param source the SSZ encoded bytes + * @return a list of strings + * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is + * not a string, or any string is too large (greater than 2^32 bytes) + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeStringList(Bytes source) { + return decode(source, SSZReader::readStringList); + } + + /** + * Read a list of strings from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param limit The maximum number of bytes to read for each list element. + * @return a list of strings + * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is + * not a string, or any string is too large (greater than 2^32 bytes) + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeStringList(Bytes source, int limit) { + return decode(source, r -> r.readStringList(limit)); + } + + /** + * Read a list of two's-compliment int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integers to read (a multiple of 8) + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeIntList(Bytes source, int bitLength) { + return decode(source, r -> r.readIntList(bitLength)); + } + + /** + * Read a list of two's-compliment long int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integers to read (a multiple of 8) + * @return a list of longs + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into a long + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeLongIntList(Bytes source, int bitLength) { + return decode(source, r -> r.readLongIntList(bitLength)); + } + + /** + * Read a list of two's-compliment big integer values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integers to read (a multiple of 8) + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, or there are insufficient + * encoded bytes for the desired bit length or any value in the list + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeBigIntegerList(Bytes source, int bitLength) { + return decode(source, r -> r.readBigIntegerList(bitLength)); + } + + /** + * Read a list of 8-bit two's-compliment int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeInt8List(Bytes source) { + return decode(source, SSZReader::readInt8List); + } + + /** + * Read a list of 16-bit two's-compliment int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeInt16List(Bytes source) { + return decode(source, SSZReader::readInt16List); + } + + /** + * Read a list of 32-bit two's-compliment int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeInt32List(Bytes source) { + return decode(source, SSZReader::readInt32List); + } + + /** + * Read a list of 64-bit two's-compliment int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeInt64List(Bytes source) { + return decode(source, SSZReader::readInt64List); + } + + /** + * Read a list of unsigned int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integers to read (a multiple of 8) + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeUIntList(Bytes source, int bitLength) { + return decode(source, r -> r.readUIntList(bitLength)); + } + + /** + * Read a list of unsigned long int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integers to read (a multiple of 8) + * @return a list of longs + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into a long + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeULongIntList(Bytes source, int bitLength) { + return decode(source, r -> r.readULongIntList(bitLength)); + } + + /** + * Read a list of unsigned big integer values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param bitLength the bit length of the integers to read (a multiple of 8) + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, or there are insufficient + * encoded bytes for the desired bit length of any value in the list + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeUnsignedBigIntegerList(Bytes source, int bitLength) { + return decode(source, r -> r.readUnsignedBigIntegerList(bitLength)); + } + + /** + * Read a list of 8-bit unsigned int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length of any value in the list, or any decoded value was + * too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeUInt8List(Bytes source) { + return decode(source, SSZReader::readUInt8List); + } + + /** + * Read a list of 16-bit unsigned int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length of any value in the list, or any decoded value was + * too large to fit into an int + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeUInt16List(Bytes source) { + return decode(source, SSZReader::readUInt16List); + } + + /** + * Read a list of 32-bit unsigned int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length of any value in the list, or any decoded value was + * too large to fit into a long + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeUInt32List(Bytes source) { + return decode(source, SSZReader::readUInt32List); + } + + /** + * Read a list of 64-bit unsigned int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of ints + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length of any value in the list, or any decoded value was + * too large to fit into a long + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeUInt64List(Bytes source) { + return decode(source, SSZReader::readUInt64List); + } + + /** + * Read a list of 256-bit unsigned int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of {@link UInt256} + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length of any value in the list, or any decoded value was + * too large to fit into {@link UInt256} + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeUInt256List(Bytes source) { + return decode(source, SSZReader::readUInt256List); + } + + /** + * Read a list of 384-bit unsigned int values from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of {@link UInt384} + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length of any value in the list, or any decoded value was + * too large to fit into a {@link UInt384} + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeUInt384List(Bytes source) { + return decode(source, SSZReader::readUInt384List); + } + + /** + * Read a list of 20-byte addresses from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of 20-byte addresses + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for any address in the list + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeAddressList(Bytes source) { + return decode(source, SSZReader::readAddressList); + } + + /** + * Read a list of 32-byte hashes from the SSZ source. + * + * @param source the SSZ encoded bytes + * @param hashLength The length of the hash (in bytes). + * @return a list of 32-byte hashes + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for any hash in the list + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeHashList(Bytes source, int hashLength) { + return decode(source, r -> r.readHashList(hashLength)); + } + + /** + * Read a list of booleans from the SSZ source. + * + * @param source the SSZ encoded bytes + * @return a list of booleans + * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient + * encoded bytes for all the booleans in the list + * @throws EndOfSSZException if there are no more SSZ values to read + */ + public static List decodeBooleanList(Bytes source) { + return decode(source, SSZReader::readBooleanList); + } +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeTypeList.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeTypeList.java new file mode 100644 index 000000000..20bceb354 --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeTypeList.java @@ -0,0 +1,41 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class SSZFixedSizeTypeList + implements SSZReadable, SSZWritable { + + private final int elementSize; + private final Supplier supplier; + + private final List elements = new ArrayList<>(); + + public SSZFixedSizeTypeList(int elementSize, Supplier supplier) { + this.elementSize = elementSize; + this.supplier = supplier; + } + + // The elements might be fixed, but the overall type is variable + @Override + public boolean isFixed() { + return false; + } + + @Override + public void populateFromReader(SSZReader reader) { + elements.addAll(reader.readFixedTypedList(elementSize, supplier)); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeTypedList(elements); + } + + public List getElements() { + return elements; + } +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeVector.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeVector.java new file mode 100644 index 000000000..d287f8411 --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeVector.java @@ -0,0 +1,42 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class SSZFixedSizeVector + implements SSZReadable, SSZWritable { + + private final int listSize; + private final int elementSize; + private final Supplier supplier; + + private final List elements = new ArrayList<>(); + + public SSZFixedSizeVector(int listSize, int elementSize, Supplier supplier) { + this.listSize = listSize; + this.elementSize = elementSize; + this.supplier = supplier; + } + + @Override + public boolean isFixed() { + return false; + } + + @Override + public void populateFromReader(SSZReader reader) { + elements.addAll(reader.readTypedVector(listSize, elementSize, supplier)); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeTypedVector(elements); + } + + public List getElements() { + return elements; + } +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReadable.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReadable.java new file mode 100644 index 000000000..e06e666af --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReadable.java @@ -0,0 +1,9 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import org.apache.tuweni.ssz.SSZType; + +public interface SSZReadable extends SSZType { + void populateFromReader(SSZReader reader); +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReader.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReader.java new file mode 100644 index 000000000..71d11724e --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReader.java @@ -0,0 +1,799 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import org.apache.tuweni.ssz.EndOfSSZException; +import org.apache.tuweni.ssz.InvalidSSZTypeException; +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; +import org.apache.tuweni.v2.units.bigints.UInt384; + +import java.math.BigInteger; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** A reader for consuming values from an SSZ encoded source. */ +public interface SSZReader { + + /** + * Read bytes from the SSZ source. + * + *

Note: prefer to use {@link #readBytes(int)} instead, especially when reading untrusted data. + * + * @return The bytes for the next value. + * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or is too large + * (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default Bytes readBytes() { + return readBytes(Integer.MAX_VALUE); + } + + /** + * Read bytes from the SSZ source. + * + * @param limit The maximum number of bytes to read. + * @return The bytes for the next value. + * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the + * limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + Bytes readBytes(int limit); + + /** + * Read a known size fixed-length bytes value from the SSZ source. + * + *

Note: prefer to use {@link #readFixedBytes(int, int)} instead, especially when reading + * untrusted data. + * + * @param byteLength The number of fixed-length Bytes (no length mixin) to read. + * @return The bytes for the next value. + * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or is too large + * (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default Bytes readFixedBytes(int byteLength) { + return readFixedBytes(byteLength, Integer.MAX_VALUE); + } + + /** + * Read a known size fixed-length bytes value from the SSZ source. + * + * @param byteLength The number of fixed-length Bytes (no length mixin) to read. + * @param limit The maximum number of bytes to read. + * @return The bytes for the next value. + * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the + * limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + Bytes readFixedBytes(int byteLength, int limit); + + /** + * Read a byte array from the SSZ source. + * + *

Note: prefer to use {@link #readByteArray(int)} instead, especially when reading untrusted + * data. + * + * @return The byte array for the next value. + * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or is too large + * (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default byte[] readByteArray() { + return readByteArray(Integer.MAX_VALUE); + } + + /** + * Read a byte array from the SSZ source. + * + * @param limit The maximum number of bytes to read. + * @return The byte array for the next value. + * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the + * limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default byte[] readByteArray(int limit) { + return readBytes(limit).toArrayUnsafe(); + } + + /** + * Read an array of fixed-length homogenous Bytes from the SSZ source. + * + * @param byteLength The number of fixed-length Bytes per array element. + * @param limit The maximum number of bytes to read. + * @return The byte array for the next value. + * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the + * limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default byte[] readFixedByteArray(int byteLength, int limit) { + return readFixedBytes(byteLength, limit).toArrayUnsafe(); + } + + /** + * Read a string value from the SSZ source. + * + *

Note: prefer to use {@link #readString(int)} instead, especially when reading untrusted + * data. + * + * @return A string. + * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or is too large + * (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default String readString() { + return new String(readByteArray(), UTF_8); + } + + /** + * Read a string value from the SSZ source. + * + * @param limit The maximum number of bytes to read. + * @return A string. + * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the + * limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default String readString(int limit) { + return new String(readByteArray(limit), UTF_8); + } + + /** + * Read a two's-compliment int value from the SSZ source. + * + * @param bitLength The bit length of the integer to read (a multiple of 8). + * @return An int. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit + * length, or the decoded value was too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + int readInt(int bitLength); + + /** + * Read a two's-compliment long value from the SSZ source. + * + * @param bitLength The bit length of the integer to read (a multiple of 8). + * @return A long. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit + * length, or the decoded value was too large to fit into a long. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + long readLong(int bitLength); + + /** + * Read a big integer value from the SSZ source. + * + * @param bitLength The bit length of the integer to read (a multiple of 8). + * @return A big integer. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit + * length. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + BigInteger readBigInteger(int bitLength); + + /** + * Read an 8-bit two's-compliment integer from the SSZ source. + * + * @return An int. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for an 8-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default int readInt8() { + return readInt(8); + } + + /** + * Read a 16-bit two's-compliment integer from the SSZ source. + * + * @return An int. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 16-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default int readInt16() { + return readInt(16); + } + + /** + * Read a 32-bit two's-compliment integer from the SSZ source. + * + * @return An int. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 32-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default int readInt32() { + return readInt(32); + } + + /** + * Read a 64-bit two's-compliment integer from the SSZ source. + * + * @return A long. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 64-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default long readInt64() { + return readLong(64); + } + + /** + * Read an unsigned int value from the SSZ source. + * + * @param bitLength The bit length of the integer to read (a multiple of 8). + * @return An unsigned int. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit + * length, or the decoded value was too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default int readUInt(int bitLength) { + // encoding is the same for unsigned + return readInt(bitLength); + } + + /** + * Read an unsigned long value from the SSZ source. + * + * @param bitLength The bit length of the integer to read (a multiple of 8). + * @return An unsigned long. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit + * length, or the decoded value was too large to fit into a long. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default long readULong(int bitLength) { + // encoding is the same for unsigned + return readLong(bitLength); + } + + /** + * Read an unsigned big integer value from the SSZ source. + * + * @param bitLength The bit length of the integer to read (a multiple of 8). + * @return A big integer. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit + * length. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + BigInteger readUnsignedBigInteger(int bitLength); + + /** + * Read an 8-bit unsigned integer from the SSZ source. + * + * @return An int. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for an 8-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default int readUInt8() { + return readUInt(8); + } + + /** + * Read a 16-bit unsigned integer from the SSZ source. + * + * @return An int. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 16-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default int readUInt16() { + return readUInt(16); + } + + /** + * Read a 32-bit unsigned integer from the SSZ source. + * + * @return An int. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 32-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default long readUInt32() { + return readULong(32); + } + + /** + * Read a 64-bit unsigned integer from the SSZ source. + * + * @return A long. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 64-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default long readUInt64() { + return readULong(64); + } + + /** + * Read a {@link UInt256} from the SSZ source. + * + * @return A {@link UInt256}. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 256-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + UInt256 readUInt256(); + + /** + * Read a {@link UInt384} from the SSZ source. + * + * @return A {@link UInt384}. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 384-bit int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + UInt384 readUInt384(); + + /** + * Read a boolean from the SSZ source. + * + * @return A boolean. + * @throws InvalidSSZTypeException If the decoded value is not a boolean. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default boolean readBoolean() { + int value = readInt(8); + if (value == 0) { + return false; + } else if (value == 1) { + return true; + } else { + throw new InvalidSSZTypeException("decoded value is not a boolean"); + } + } + + /** + * Read a 20-byte address from the SSZ source. + * + * @return The bytes of the Address. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 20-byte address. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + Bytes readAddress(); + + /** + * Read a hash from the SSZ source. + * + * @param hashLength The length of the hash (in bytes). + * @return The bytes of the hash. + * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 32-byte hash. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + Bytes readHash(int hashLength); + + /** + * Read a list of {@link Bytes} from the SSZ source. + * + *

Note: prefer to use {@link #readBytesList(int)} instead, especially when reading untrusted + * data. + * + * @return A list of {@link Bytes}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or any byte array is too large (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readBytesList() { + return readBytesList(Integer.MAX_VALUE); + } + + /** + * Read a known-size fixed-length list of {@link Bytes} from the SSZ source. The list WILL NOT + * have a length mixin, where as the elements WILL. + * + *

Note: prefer to use {@link #readVector(long, int)} instead, especially when reading + * untrusted data. + * + * @param listSize The size of the fixed-length list being read. + * @return A list of {@link Bytes}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or any byte array is too large (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readVector(long listSize) { + return readVector(listSize, Integer.MAX_VALUE); + } + + /** + * Read a known-size fixed length list of known-size fixed length {@link Bytes} from the SSZ + * source. + * + * @param listSize The size of the fixed-length list being read. + * @param byteLength The number of fixed-length Bytes per homogenous List element. + * @param limit The maximum number of bytes to read for each list element. + * @return A list of {@link Bytes}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or the size of any byte array would exceed the limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readFixedBytesVector(int listSize, int byteLength, int limit); + + /** + * Read a known-size fixed length list of known-size fixed length {@link Bytes} from the SSZ + * source. + * + *

Note: prefer to use {@link #readFixedBytesVector(int, int, int)} instead, especially when + * reading untrusted data. + * + * @param listSize The size of the fixed-length list being read. + * @param byteLength The number of fixed-length Bytes per homogenous List element. + * @return A list of {@link Bytes}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or any byte array is too large (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readFixedBytesVector(int listSize, int byteLength) { + return readFixedBytesVector(listSize, byteLength, Integer.MAX_VALUE); + } + + /** + * Read a list of known-size fixed length {@link Bytes} from the SSZ source. A length mixin IS + * expected for the list, but IS NOT expected for the list elements. + * + * @param byteLength The number of fixed-length Bytes per homogenous List element. + * @param limit The maximum number of bytes to read for each list element. + * @return A list of {@link Bytes}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or the size of any byte array would exceed the limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readFixedBytesList(int byteLength, int limit); + + /** + * Read a variable-length list of known-size fixed length {@link Bytes} from the SSZ source. + * + *

Note: prefer to use {@link #readFixedBytesList(int, int)} instead, especially when reading + * untrusted data. + * + * @param byteLength The number of fixed-length Bytes per homogenous List element. + * @return A list of {@link Bytes}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or any byte array is too large (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readFixedBytesList(int byteLength) { + return readFixedBytesList(byteLength, Integer.MAX_VALUE); + } + + /** + * Read a list of {@link Bytes} from the SSZ source. + * + * @param limit The maximum number of bytes to read for each list element. + * @return A list of {@link Bytes}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or the size of any byte array would exceed the limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readBytesList(int limit); + + /** + * Read a known-size fixed-length list of {@link Bytes} from the SSZ source. + * + * @param listSize The size of the fixed-length list being read. + * @param limit The maximum number of bytes to read for each list element. + * @return A list of {@link Bytes}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or the size of any byte array would exceed the limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readVector(long listSize, int limit); + + /** + * Read a list of byte arrays from the SSZ source. + * + *

Note: prefer to use {@link #readByteArrayList(int)} instead, especially when reading + * untrusted data. + * + * @return A list of byte arrays. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or any byte array is too large (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readByteArrayList() { + return readByteArrayList(Integer.MAX_VALUE); + } + + /** + * Read a list of byte arrays from the SSZ source. + * + * @param limit The maximum number of bytes to read for each list element. + * @return A list of byte arrays. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a byte array, or the size of any byte array would exceed the limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readByteArrayList(int limit) { + return readBytesList(limit).stream().map(Bytes::toArrayUnsafe).collect(Collectors.toList()); + } + + /** + * Read a list of strings from the SSZ source. + * + *

Note: prefer to use {@link #readStringList(int)} instead, especially when reading untrusted + * data. + * + * @return A list of strings. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a string, or any string is too large (greater than 2^32 bytes). + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readStringList() { + return readStringList(Integer.MAX_VALUE); + } + + /** + * Read a list of strings from the SSZ source. + * + * @param limit The maximum number of bytes to read for each list element. + * @return A list of strings. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is + * not a string, or the size of any string would exceed the limit. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readStringList(int limit); + + /** + * Read a list of two's-compliment int values from the SSZ source. + * + * @param bitLength The bit length of the integers to read (a multiple of 8). + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readIntList(int bitLength); + + /** + * Read a list of two's-compliment long int values from the SSZ source. + * + * @param bitLength The bit length of the integers to read (a multiple of 8). + * @return A list of longs. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into a long. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readLongIntList(int bitLength); + + /** + * Read a list of two's-compliment big integer values from the SSZ source. + * + * @param bitLength The bit length of the integers to read (a multiple of 8). + * @return A list of big integers. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, or there are insufficient + * encoded bytes for the desired bit length or any value in the list. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readBigIntegerList(int bitLength); + + /** + * Read a list of 8-bit two's-compliment int values from the SSZ source. + * + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readInt8List() { + return readIntList(8); + } + + /** + * Read a list of 16-bit two's-compliment int values from the SSZ source. + * + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readInt16List() { + return readIntList(16); + } + + /** + * Read a list of 32-bit two's-compliment int values from the SSZ source. + * + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readInt32List() { + return readIntList(32); + } + + /** + * Read a list of 64-bit two's-compliment int values from the SSZ source. + * + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readInt64List() { + return readLongIntList(64); + } + + /** + * Read a list of unsigned int values from the SSZ source. + * + * @param bitLength The bit length of the integers to read (a multiple of 8). + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readUIntList(int bitLength) { + // encoding is the same for unsigned + return readIntList(bitLength); + } + + /** + * Read a list of unsigned long int values from the SSZ source. + * + * @param bitLength The bit length of the integers to read (a multiple of 8). + * @return A list of longs. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into a long. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readULongIntList(int bitLength) { + // encoding is the same for unsigned + return readLongIntList(bitLength); + } + + /** + * Read a list of unsigned big integer values from the SSZ source. + * + * @param bitLength The bit length of the integers to read (a multiple of 8). + * @return A list of big integers. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, or there are insufficient + * encoded bytes for the desired bit length or any value in the list. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readUnsignedBigIntegerList(int bitLength); + + /** + * Read a list of 8-bit unsigned int values from the SSZ source. + * + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readUInt8List() { + return readUIntList(8); + } + + /** + * Read a list of 16-bit unsigned int values from the SSZ source. + * + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readUInt16List() { + return readUIntList(16); + } + + /** + * Read a list of 32-bit unsigned int values from the SSZ source. + * + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readUInt32List() { + return readULongIntList(32); + } + + /** + * Read a list of 64-bit unsigned int values from the SSZ source. + * + * @return A list of ints. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + default List readUInt64List() { + return readULongIntList(64); + } + + /** + * Read a list of 256-bit unsigned int values from the SSZ source. + * + * @return A list of {@link UInt256}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readUInt256List(); + + /** + * Read a list of 384-bit unsigned int values from the SSZ source. + * + * @return A list of {@link UInt384}. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for the desired bit length or any value in the list, or any decoded value was + * too large to fit into an int. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readUInt384List(); + + /** + * Read a list of 20-byte addresses from the SSZ source. + * + * @return A list of 20-byte addresses. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for any address in the list. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readAddressList(); + + /** + * Read a list of hashes from the SSZ source. + * + * @param hashLength The length of the hash (in bytes). + * @return A list of hashes. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for any hash in the list. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readHashList(int hashLength); + + /** + * Read a list of booleans from the SSZ source. + * + * @return A list of booleans. + * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient + * encoded bytes for all the booleans in the list. + * @throws EndOfSSZException If there are no more SSZ values to read. + */ + List readBooleanList(); + + /** + * Check if all values have been read. + * + * @return {@code true} if all values have been read. + */ + boolean isComplete(); + + /** + * Read a SSZ container from the SSZ source and calls populate method on the elements with a + * sliced SSZReader in case of variable size elements to provide a way to detect a list size. + */ + void readAsContainer(SSZReadable... elements); + + /** + * Read a List of Fixed size type from the SSZ source. + * + *

The size of the list is not encoded in the SSZ and cannot be calculated from the data + * either. This method will read until the end of the SSZReader. It is the responsibility of the + * caller to properly slice the parent container SSZReader. The Lists are a variable type and + * therefore start of the scope is specified in parent container using an offset. The end is + * offset of the next variable element, or end of the parent scope if this list is the last in the + * scope. + */ + List readFixedTypedList(int elementSize, Supplier supplier); + + /** + * Reads a Vector from the SSZ source, gets a new instance of T from supplier and calls the + * populate method on it. + */ + List readTypedVector( + int listSize, int elementSize, Supplier supplier); + + /** Returns the remaining bytes in the SSZReader without any manipulation or checks. */ + Bytes consumeRemainingBytes(int limit); + + /** Reads a List of Variable size type from the SSZ source. */ + List readVariableSizeTypeList(Supplier supplier); +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZVariableSizeTypeList.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZVariableSizeTypeList.java new file mode 100644 index 000000000..0cce02bf6 --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZVariableSizeTypeList.java @@ -0,0 +1,37 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class SSZVariableSizeTypeList + implements SSZReadable, SSZWritable { + private final Supplier supplier; + + private final List elements = new ArrayList<>(); + + public SSZVariableSizeTypeList(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public boolean isFixed() { + return false; + } + + @Override + public void populateFromReader(SSZReader reader) { + elements.addAll(reader.readVariableSizeTypeList(supplier)); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeTypedList(elements); + } + + public List getElements() { + return elements; + } +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWritable.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWritable.java new file mode 100644 index 000000000..1f7e104b2 --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWritable.java @@ -0,0 +1,10 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import org.apache.tuweni.ssz.SSZType; + +/** An object that can be written to a {@link SSZWriter}. */ +public interface SSZWritable extends SSZType { + void writeTo(SSZWriter writer); +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWriter.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWriter.java new file mode 100644 index 000000000..11a3c9d31 --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWriter.java @@ -0,0 +1,739 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; +import org.apache.tuweni.v2.units.bigints.UInt384; + +import java.math.BigInteger; +import java.util.List; + +/** A writer for encoding values to SSZ. */ +public interface SSZWriter { + + /** + * Append an already SSZ encoded value. + * + *

Note that this method may not validate that {@code value} is a valid SSZ sequence. + * Appending an invalid SSZ sequence will cause the entire SSZ encoding produced by this writer to + * also be invalid. + * + * @param value the SSZ encoded bytes to append + */ + void writeSSZ(Bytes value); + + /** + * Append an already SSZ encoded value. + * + *

Note that this method may not validate that {@code value} is a valid SSZ sequence. + * Appending an invalid SSZ sequence will cause the entire SSZ encoding produced by this writer to + * also be invalid. + * + * @param value the SSZ encoded bytes to append + */ + default void writeSSZ(byte[] value) { + writeSSZ(Bytes.wrap(value)); + } + + /** + * Encode a {@link Bytes} value to SSZ. + * + * @param value the byte array to encode + */ + default void writeBytes(Bytes value) { + SSZ.encodeBytesTo(value, this::writeSSZ); + } + + /** + * Encode a byte array to SSZ. + * + * @param value the byte array to encode + */ + default void writeBytes(byte[] value) { + SSZ.encodeByteArrayTo(value, this::writeSSZ); + } + + /** + * Encode a known fixed-length {@link Bytes} value to SSZ without the length mixin. + * + * @param value the byte array to encode + * @throws IllegalArgumentException if the byteLength is not the same size as value. + */ + default void writeFixedBytes(Bytes value) { + SSZ.encodeFixedBytesTo(value, this::writeSSZ); + } + + /** + * Write a string to the output. + * + * @param str the string to write + */ + default void writeString(String str) { + SSZ.encodeStringTo(str, this::writeSSZ); + } + + /** + * Write a two's-compliment integer to the output. + * + * @param value the integer to write + * @param bitLength the bit length of the integer value + * @throws IllegalArgumentException if the value is too large for the specified bit length + */ + default void writeInt(int value, int bitLength) { + writeSSZ(SSZ.encodeLongToByteArray(value, bitLength)); + } + + /** + * Write a two's-compliment long to the output. + * + * @param value the long value to write + * @param bitLength the bit length of the integer value + * @throws IllegalArgumentException if the value is too large for the specified bit length + */ + default void writeLong(long value, int bitLength) { + writeSSZ(SSZ.encodeLongToByteArray(value, bitLength)); + } + + /** + * Write a big integer to the output. + * + * @param value the integer to write + * @param bitLength the bit length of the integer value + * @throws IllegalArgumentException if the value is too large for the specified bit length + */ + default void writeBigInteger(BigInteger value, int bitLength) { + writeSSZ(SSZ.encodeBigIntegerToByteArray(value, bitLength)); + } + + /** + * Write an 8-bit two's-compliment integer to the output. + * + * @param value the integer to write + * @throws IllegalArgumentException if the value is too large to be represented in 8 bits + */ + default void writeInt8(int value) { + writeInt(value, 8); + } + + /** + * Write a 16-bit two's-compliment integer to the output. + * + * @param value the integer to write + * @throws IllegalArgumentException If the value is too large to be represented in 16 bits + */ + default void writeInt16(int value) { + writeInt(value, 16); + } + + /** + * Write a 32-bit two's-compliment integer to the output. + * + * @param value the integer to write + */ + default void writeInt32(int value) { + writeInt(value, 32); + } + + /** + * Write a 64-bit two's-compliment integer to the output. + * + * @param value the long to write + */ + default void writeInt64(long value) { + writeLong(value, 64); + } + + /** + * Write an unsigned integer to the output. + * + *

Note that the argument {@code value} is a native signed int but will be interpreted as an + * unsigned value. + * + * @param value the integer to write + * @param bitLength the bit length of the integer value + * @throws IllegalArgumentException if the value is too large for the specified bit length + */ + default void writeUInt(int value, int bitLength) { + writeSSZ(SSZ.encodeULongToByteArray(value, bitLength)); + } + + /** + * Write an unsigned long to the output. + * + *

Note that the argument {@code value} is a native signed long but will be interpreted as an + * unsigned value. + * + * @param value the long value to write + * @param bitLength the bit length of the integer value + * @throws IllegalArgumentException if the value is too large for the specified bit length + */ + default void writeULong(long value, int bitLength) { + writeSSZ(SSZ.encodeULongToByteArray(value, bitLength)); + } + + /** + * Write an unsigned big integer to the output. + * + * @param value the integer to write + * @param bitLength the bit length of the integer value + * @throws IllegalArgumentException if the value is too large for the specified bit length or the + * value is negative + */ + default void writeUBigInteger(BigInteger value, int bitLength) { + writeSSZ(SSZ.encodeUBigIntegerToByteArray(value, bitLength)); + } + + /** + * Write an 8-bit unsigned integer to the output. + * + * @param value the integer to write + * @throws IllegalArgumentException if the value is too large to be represented in 8 bits + */ + default void writeUInt8(int value) { + writeUInt(value, 8); + } + + /** + * Write a 16-bit unsigned integer to the output. + * + * @param value the integer to write + * @throws IllegalArgumentException If the value is too large to be represented in 16 bits + */ + default void writeUInt16(int value) { + writeUInt(value, 16); + } + + /** + * Write a 32-bit unsigned integer to the output. + * + * @param value the integer to write + */ + default void writeUInt32(long value) { + writeULong(value, 32); + } + + /** + * Write a 64-bit unsigned integer to the output. + * + *

Note that the argument {@code value} is a native signed long but will be interpreted as an + * unsigned value. + * + * @param value the long to write + */ + default void writeUInt64(long value) { + writeULong(value, 64); + } + + /** + * Write a {@link UInt256} to the output. + * + * @param value the {@link UInt256} to write + */ + default void writeUInt256(UInt256 value) { + writeSSZ(SSZ.encodeUInt256(value)); + } + + /** + * Write a {@link UInt384} to the output. + * + * @param value the {@link UInt384} to write + */ + default void writeUInt384(UInt384 value) { + writeSSZ(SSZ.encodeUInt384(value)); + } + + /** + * Write a boolean to the output. + * + * @param value the boolean value + */ + default void writeBoolean(boolean value) { + writeSSZ(SSZ.encodeBoolean(value)); + } + + /** + * Write an address. + * + * @param address the address (must be exactly 20 bytes) + * @throws IllegalArgumentException if {@code address.size != 20} + */ + default void writeAddress(Bytes address) { + writeSSZ(SSZ.encodeAddress(address)); + } + + /** + * Write a hash. + * + * @param hash the hash + */ + default void writeHash(Bytes hash) { + writeSSZ(SSZ.encodeHash(hash)); + } + + /** + * Write a list of bytes. + * + * @param elements the bytes to write as a list + */ + default void writeBytesList(Bytes... elements) { + SSZ.encodeBytesListTo(elements, this::writeSSZ); + } + + /** + * Write a list of bytes. + * + * @param elements the bytes to write as a list + */ + default void writeBytesList(List elements) { + SSZ.encodeBytesListTo(elements, this::writeSSZ); + } + + /** + * Write a vector of bytes. + * + * @param elements the bytes to write as a list + */ + default void writeVector(List elements) { + SSZ.encodeBytesVectorTo(elements, this::writeSSZ); + } + + /** + * Write a list of known-size homogenous bytes. The list itself WILL have a length mixin, but the + * elements WILL NOT. + * + * @param elements the known-size bytes to write as a list + */ + default void writeFixedBytesList(List elements) { + SSZ.encodeFixedBytesListTo(elements, this::writeSSZ); + } + + /** + * Write a known-size fixed-length list of known-size homogenous bytes. Neither the list nor the + * elements in the list will have a length mixin. + * + * @param elements the bytes to write as a list + */ + default void writeFixedBytesVector(List elements) { + SSZ.encodeFixedBytesVectorTo(elements, this::writeSSZ); + } + + /** + * Write a list of strings, which must be of the same length + * + * @param elements the strings to write as a list + */ + default void writeStringList(String... elements) { + SSZ.encodeStringListTo(elements, this::writeSSZ); + } + + /** + * Write a list of strings, which must be of the same length + * + * @param elements the strings to write as a list + */ + default void writeStringList(List elements) { + SSZ.encodeStringListTo(elements, this::writeSSZ); + } + + /** + * Write a list of two's compliment integers. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large for the specified bit length + */ + default void writeIntList(int bitLength, int... elements) { + SSZ.encodeIntListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of two's compliment integers. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large for the specified bit length + */ + default void writeIntList(int bitLength, List elements) { + SSZ.encodeIntListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of two's compliment long integers. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the long integers to write as a list + * @throws IllegalArgumentException if any values are too large for the specified bit length + */ + default void writeLongIntList(int bitLength, long... elements) { + SSZ.encodeLongIntListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of two's compliment long integers. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the long integers to write as a list + * @throws IllegalArgumentException if any values are too large for the specified bit length + */ + default void writeLongIntList(int bitLength, List elements) { + SSZ.encodeLongIntListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of big integers. + * + * @param bitLength the bit length of each integer + * @param elements the integers to write as a list + * @throws IllegalArgumentException if an integer cannot be stored in the number of bytes provided + */ + default void writeBigIntegerList(int bitLength, BigInteger... elements) { + SSZ.encodeBigIntegerListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of big integers. + * + * @param bitLength the bit length of each integer + * @param elements the integers to write as a list + * @throws IllegalArgumentException if an integer cannot be stored in the number of bytes provided + */ + default void writeBigIntegerList(int bitLength, List elements) { + SSZ.encodeBigIntegerListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of 8-bit two's compliment integers. + * + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large to be represented in 8 bits + */ + default void writeInt8List(int... elements) { + writeIntList(8, elements); + } + + /** + * Write a list of 8-bit two's compliment integers. + * + * @param elements the integers to write as a list. + * @throws IllegalArgumentException if any values are too large to be represented in 8 bits + */ + default void writeInt8List(List elements) { + writeIntList(8, elements); + } + + /** + * Write a list of 16-bit two's compliment integers. + * + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large to be represented in 16 bits + */ + default void writeInt16List(int... elements) { + writeIntList(16, elements); + } + + /** + * Write a list of 16-bit two's compliment integers. + * + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large to be represented in 16 bits + */ + default void writeInt16List(List elements) { + writeIntList(16, elements); + } + + /** + * Write a list of 32-bit two's compliment integers. + * + * @param elements the integers to write as a list + */ + default void writeInt32List(int... elements) { + writeIntList(32, elements); + } + + /** + * Write a list of 32-bit two's compliment integers. + * + * @param elements the integers to write as a list + */ + default void writeInt32List(List elements) { + writeIntList(32, elements); + } + + /** + * Write a list of 64-bit two's compliment integers. + * + * @param elements the integers to write as a list + */ + default void writeInt64List(long... elements) { + writeLongIntList(64, elements); + } + + /** + * Write a list of 64-bit two's compliment integers. + * + * @param elements the integers to write as a list + */ + default void writeInt64List(List elements) { + writeLongIntList(64, elements); + } + + /** + * Write a list of unsigned integers. + * + *

Note that the {@code elements} are native signed ints, but will be interpreted as an + * unsigned values. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large for the specified bit length + */ + default void writeUIntList(int bitLength, int... elements) { + SSZ.encodeUIntListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of unsigned integers. + * + *

Note that the {@code elements} are native signed ints, but will be interpreted as an + * unsigned values. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large for the specified bit length + */ + default void writeUIntList(int bitLength, List elements) { + SSZ.encodeUIntListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of unsigned long integers. + * + *

Note that the {@code elements} are native signed longs, but will be interpreted as an + * unsigned values. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the long integers to write as a list + * @throws IllegalArgumentException if any values are too large for the specified bit length + */ + default void writeULongIntList(int bitLength, long... elements) { + SSZ.encodeULongIntListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of unsigned long integers. + * + *

Note that the {@code elements} are native signed longs, but will be interpreted as an + * unsigned values. + * + * @param bitLength the bit length of the encoded integers (must be a multiple of 8) + * @param elements the long integers to write as a list + * @throws IllegalArgumentException if any values are too large for the specified bit length + */ + default void writeULongIntList(int bitLength, List elements) { + SSZ.encodeULongIntListTo(bitLength, elements, this::writeSSZ); + } + + /** + * Write a list of 8-bit unsigned integers. + * + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large to be represented in 8 bits + */ + default void writeUInt8List(int... elements) { + writeUIntList(8, elements); + } + + /** + * Write a list of 8-bit unsigned integers. + * + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large to be represented in 8 bits + */ + default void writeUInt8List(List elements) { + writeUIntList(8, elements); + } + + /** + * Write a list of 16-bit unsigned integers. + * + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large to be represented in 16 bits + */ + default void writeUInt16List(int... elements) { + writeUIntList(16, elements); + } + + /** + * Write a list of 16-bit unsigned integers. + * + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large to be represented in 16 bits + */ + default void writeUInt16List(List elements) { + writeUIntList(16, elements); + } + + /** + * Write a list of 32-bit unsigned integers. + * + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large to be represented in 32 bits + */ + default void writeUInt32List(long... elements) { + writeULongIntList(32, elements); + } + + /** + * Write a list of 32-bit unsigned integers. + * + * @param elements the integers to write as a list + * @throws IllegalArgumentException if any values are too large to be represented in 32 bits + */ + default void writeUInt32List(List elements) { + writeULongIntList(32, elements); + } + + /** + * Write a list of 64-bit unsigned integers. + * + *

Note that the {@code elements} are native signed longs, but will be interpreted as an + * unsigned values. + * + * @param elements the integers to write as a list + */ + default void writeUInt64List(long... elements) { + writeULongIntList(64, elements); + } + + /** + * Write a list of 64-bit unsigned integers. + * + *

Note that the {@code elements} are native signed longs, but will be interpreted as an + * unsigned values. + * + * @param elements the integers to write as a list + */ + default void writeUInt64List(List elements) { + writeULongIntList(64, elements); + } + + /** + * Write a list of unsigned 256-bit integers. + * + * @param elements the integers to write as a list + */ + default void writeUInt256List(UInt256... elements) { + SSZ.encodeUInt256ListTo(elements, this::writeSSZ); + } + + /** + * Write a list of unsigned 256-bit integers. + * + * @param elements the integers to write as a list + */ + default void writeUInt256List(List elements) { + SSZ.encodeUInt256ListTo(elements, this::writeSSZ); + } + + /** + * Write a list of unsigned 384-bit integers. + * + * @param elements the integers to write as a list + */ + default void writeUInt384List(List elements) { + SSZ.encodeUInt384ListTo(elements, this::writeSSZ); + } + + /** + * Write a list of unsigned 384-bit integers. + * + * @param elements the integers to write as a list + */ + default void writeUInt384List(UInt384... elements) { + SSZ.encodeUInt384ListTo(elements, this::writeSSZ); + } + + /** + * Write a list of hashes. + * + * @param elements the hashes to write as a list + */ + default void writeHashList(Bytes... elements) { + SSZ.encodeHashListTo(elements, this::writeSSZ); + } + + /** + * Write a list of hashes. + * + * @param elements the hashes to write as a list + */ + default void writeHashList(List elements) { + SSZ.encodeHashListTo(elements, this::writeSSZ); + } + + /** + * Write a list of addresses. + * + * @param elements the addresses to write as a list + * @throws IllegalArgumentException if any {@code address.size != 20} + */ + default void writeAddressList(Bytes... elements) { + SSZ.encodeAddressListTo(elements, this::writeSSZ); + } + + /** + * Write a list of addresses. + * + * @param elements the addresses to write as a list + * @throws IllegalArgumentException if any {@code address.size != 20} + */ + default void writeAddressList(List elements) { + SSZ.encodeAddressListTo(elements, this::writeSSZ); + } + + /** + * Write a list of booleans. + * + * @param elements the booleans to write as a list + */ + default void writeBooleanList(boolean... elements) { + SSZ.encodeBooleanListTo(elements, this::writeSSZ); + } + + /** + * Write a list of booleans. + * + * @param elements the booleans to write as a list + */ + default void writeBooleanList(List elements) { + SSZ.encodeBooleanListTo(elements, this::writeSSZ); + } + + /** + * Write a list of SSZWritable elements. + * + * @param elements the elements to write as a list + */ + default void writeAsContainer(SSZWritable... elements) { + SSZ.encodeAsContainerTo(this::writeSSZ, elements); + } + + /** + * Write a list of SSZWritable elements. + * + * @param elements the elements to write as a list + */ + default void writeTypedList(List elements) { + SSZ.encodeAsFixedTypeListTo(elements, this::writeSSZ); + } + + /** + * Write a list of SSZWritable elements. + * + * @param elements the elements to write as a list + */ + default void writeTypedVector(List elements) { + SSZ.encodeAsFixedTypeVectorTo(elements, this::writeSSZ); + } +} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/package-info.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/package-info.java new file mode 100644 index 000000000..cdc9d7055 --- /dev/null +++ b/ssz/src/main/java/org/apache/tuweni/v2/ssz/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. + * + *

Simple Serialize (SSZ) encoding and decoding. + * + *

An implementation of the Ethereum Simple Serialize (SSZ) algorithm, as described at https://github.com/ethereum/eth2.0-specs/blob/master/specs/simple-serialize.md. + * + *

These classes are included in the standard Tuweni distribution, or separately when using the + * gradle dependency 'org.apache.tuweni:tuweni-ssz' (tuweni-ssz.jar). + */ +package org.apache.tuweni.v2.ssz; diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/ByteBufferWriterTest.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/ByteBufferWriterTest.java new file mode 100644 index 000000000..cb6384985 --- /dev/null +++ b/ssz/src/test/java/org/apache/tuweni/v2/ssz/ByteBufferWriterTest.java @@ -0,0 +1,121 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; + +import java.math.BigInteger; +import java.nio.ByteBuffer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class ByteBufferWriterTest { + + @ParameterizedTest + @CsvSource({"E8030000, 1000", "A0860100, 100000"}) + void shouldWriteSmallIntegers(String expectedHex, int value) { + ByteBuffer buffer = ByteBuffer.allocate(64); + SSZ.encodeTo(buffer, writer -> writer.writeInt(value, 32)); + buffer.flip(); + assertEquals(fromHexString(expectedHex), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteLongIntegers() { + ByteBuffer buffer = ByteBuffer.allocate(64); + SSZ.encodeTo(buffer, writer -> writer.writeLong(100000L, 24)); + buffer.flip(); + assertEquals(fromHexString("A08601"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteUInt256Integers() { + ByteBuffer buffer = ByteBuffer.allocate(64); + SSZ.encodeTo(buffer, writer -> writer.writeUInt256(UInt256.valueOf(100000L))); + buffer.flip(); + assertEquals( + fromHexString("A086010000000000000000000000000000000000000000000000000000000000"), + Bytes.wrapByteBuffer(buffer)); + + buffer.clear(); + SSZ.encodeTo( + buffer, + writer -> + writer.writeUInt256( + UInt256.fromHexString( + "0x0400000000000000000000000000000000000000000000000000f100000000ab"))); + buffer.flip(); + assertEquals( + fromHexString("AB00000000F10000000000000000000000000000000000000000000000000004"), + Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteBigIntegers() { + ByteBuffer buffer = ByteBuffer.allocate(64); + SSZ.encodeTo(buffer, writer -> writer.writeBigInteger(BigInteger.valueOf(100000), 24)); + buffer.flip(); + assertEquals(fromHexString("A08601"), Bytes.wrapByteBuffer(buffer)); + + buffer.clear(); + SSZ.encodeTo(buffer, writer -> writer.writeBigInteger(BigInteger.valueOf(127).pow(16), 112)); + buffer.flip(); + assertEquals(fromHexString("01F81D7AF1971CEDD9BBA5EFCEE1"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteEmptyStrings() { + ByteBuffer buffer = ByteBuffer.allocate(64); + SSZ.encodeTo(buffer, writer -> writer.writeString("")); + buffer.flip(); + assertEquals(fromHexString("00000000"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteOneCharactersStrings() { + ByteBuffer buffer = ByteBuffer.allocate(64); + SSZ.encodeTo(buffer, writer -> writer.writeString("d")); + buffer.flip(); + assertEquals(fromHexString("0100000064"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteStrings() { + ByteBuffer buffer = ByteBuffer.allocate(64); + SSZ.encodeTo(buffer, writer -> writer.writeString("dog")); + buffer.flip(); + assertEquals(fromHexString("03000000646F67"), Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWriteShortLists() { + String[] strings = + new String[] { + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer" + }; + + ByteBuffer buffer = ByteBuffer.allocate(256); + SSZ.encodeTo(buffer, w -> w.writeStringList(strings)); + buffer.flip(); + + assertEquals( + fromHexString( + "5800000004000000617364660400000071776572040000007A78637604000000617364660400000071776572040000007A78637604000000617364660400000071776572040000007A78637604000000617364660400000071776572"), + Bytes.wrapByteBuffer(buffer)); + } + + @Test + void shouldWritePreviouslyEncodedValues() { + ByteBuffer buffer = ByteBuffer.allocate(64); + SSZ.encodeTo(buffer, writer -> writer.writeSSZ(SSZ.encodeByteArray("abc".getBytes(UTF_8)))); + buffer.flip(); + assertEquals("abc", SSZ.decodeString(Bytes.wrapByteBuffer(buffer))); + } +} diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZReaderTest.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZReaderTest.java new file mode 100644 index 000000000..54505f842 --- /dev/null +++ b/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZReaderTest.java @@ -0,0 +1,252 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; +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.ssz.EndOfSSZException; +import org.apache.tuweni.ssz.InvalidSSZTypeException; +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes32; +import org.apache.tuweni.v2.bytes.Bytes48; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class BytesSSZReaderTest { + + private static final Bytes SHORT_LIST = + fromHexString( + "5800000004000000617364660400000071776572040000007A78637604000000617364660400000071776572040000007A78637604000000617364660400000071776572040000007A78637604000000617364660400000071776572"); + + private static class SomeObject { + private final String name; + private final int number; + private final BigInteger longNumber; + + SomeObject(String name, int number, BigInteger longNumber) { + this.name = name; + this.number = number; + this.longNumber = longNumber; + } + } + + private static class AnotherObject { + private final long number1; + private final long number2; + + public AnotherObject(long number1, long number2) { + this.number1 = number1; + this.number2 = number2; + } + } + + @Test + void shouldParseFullObjects() { + Bytes bytes = + fromHexString( + "0x03000000426F62046807B7711F010000000000000000000000000000000000000000000000000000"); + SomeObject readObject = + SSZ.decode(bytes, r -> new SomeObject(r.readString(), r.readInt8(), r.readBigInteger(256))); + + assertEquals("Bob", readObject.name); + assertEquals(4, readObject.number); + assertEquals(BigInteger.valueOf(1234563434344L), readObject.longNumber); + } + + @Test + void shouldRoundTripComplexObjects() throws IOException { + String resourceName = "org/apache/tuweni/ssz/signedBlobTransaction.hex"; + InputStream inputStream = + Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(resourceName)); + String inputString = new String(inputStream.readAllBytes(), UTF_8); + Bytes bytes = fromHexString(inputString); + + TransactionNetworkPayload payload = + SSZ.decode( + bytes, + sszReader -> { + var p = new TransactionNetworkPayload(); + p.populateFromReader(sszReader); + return p; + }); + + Bytes encoded = SSZ.encode(payload::writeTo); + assertEquals(inputString, encoded.toString()); + } + + @ParameterizedTest + @CsvSource({ + "00, 0", + "01, 1", + "10, 16", + "4f, 79", + "7f, 127", + "8000, 128", + "e803, 1000", + "a0860100, 100000", + "a08601000000, 100000" + }) + void shouldReadIntegers(String hex, int value) { + assertTrue( + SSZ.decode( + fromHexString(hex), + reader -> { + assertEquals(value, reader.readInt(hex.length() * 4)); + return true; + })); + } + + /** + * Related to the bug when {@link BytesSSZReader#readLong(int)} calculates lead zeroes from + * beginning of whole content instead of the current value + */ + @Test + void shouldCorrectlyParseLongs() { + Bytes bytes = fromHexString("7b00000000000000" + "ffffffffff7f0000"); + AnotherObject readObject = + SSZ.decode(bytes, r -> new AnotherObject(r.readLong(64), r.readLong(64))); + + assertEquals(123, readObject.number1); + assertEquals(140737488355327L, readObject.number2); + } + + @Test + void shouldThrowWhenReadingOversizedInt() { + InvalidSSZTypeException ex = + assertThrows( + InvalidSSZTypeException.class, + () -> SSZ.decode(fromHexString("1122334455667788"), r -> r.readInt(64))); + assertEquals("decoded integer is too large for an int", ex.getMessage()); + } + + @ParameterizedTest + // @formatter:off + @CsvSource({ + "00000000, ''", + "0100000000, '\u0000'", + "0100000001, '\u0001'", + "010000007f, '\u007F'", + "03000000646f67, dog", + "370000004C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E7365637465747572206164697069736963696E6720656C69" + + ", 'Lorem ipsum dolor sit amet, consectetur adipisicing eli'", + "380000004C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E7365637465747572206164697069736963696E6720656C6974" + + ", 'Lorem ipsum dolor sit amet, consectetur adipisicing elit'", + "000400004C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E73656374657475722061646970697363696E6720656C69742E20437572616269747572206D6175726973206D61676E612C20737573636970697420736564207665686963756C61206E6F6E2C20696163756C697320666175636962757320746F72746F722E2050726F696E20737573636970697420756C74726963696573206D616C6573756164612E204475697320746F72746F7220656C69742C2064696374756D2071756973207472697374697175652065752C20756C7472696365732061742072697375732E204D6F72626920612065737420696D70657264696574206D6920756C6C616D636F7270657220616C6971756574207375736369706974206E6563206C6F72656D2E2041656E65616E2071756973206C656F206D6F6C6C69732C2076756C70757461746520656C6974207661726975732C20636F6E73657175617420656E696D2E204E756C6C6120756C74726963657320747572706973206A7573746F2C20657420706F73756572652075726E6120636F6E7365637465747572206E65632E2050726F696E206E6F6E20636F6E76616C6C6973206D657475732E20446F6E65632074656D706F7220697073756D20696E206D617572697320636F6E67756520736F6C6C696369747564696E2E20566573746962756C756D20616E746520697073756D207072696D697320696E206661756369627573206F726369206C756374757320657420756C74726963657320706F737565726520637562696C69612043757261653B2053757370656E646973736520636F6E76616C6C69732073656D2076656C206D617373612066617563696275732C2065676574206C6163696E6961206C616375732074656D706F722E204E756C6C61207175697320756C747269636965732070757275732E2050726F696E20617563746F722072686F6E637573206E69626820636F6E64696D656E74756D206D6F6C6C69732E20416C697175616D20636F6E73657175617420656E696D206174206D65747573206C75637475732C206120656C656966656E6420707572757320656765737461732E20437572616269747572206174206E696268206D657475732E204E616D20626962656E64756D2C206E6571756520617420617563746F72207472697374697175652C206C6F72656D206C696265726F20616C697175657420617263752C206E6F6E20696E74657264756D2074656C6C7573206C65637475732073697420616D65742065726F732E20437261732072686F6E6375732C206D65747573206163206F726E617265206375727375732C20646F6C6F72206A7573746F20756C747269636573206D657475732C20617420756C6C616D636F7270657220766F6C7574706174" + + ", 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat'" + }) + // @formatter:on + void shouldReadStrings(String hex, String value) { + System.out.println(SSZ.encodeString(value)); + assertTrue( + SSZ.decode( + fromHexString(hex), + reader -> { + assertEquals(value, reader.readString()); + return true; + })); + } + + @Test + void shouldThrowWhenInputExhausted() { + EndOfSSZException ex = + assertThrows( + EndOfSSZException.class, () -> SSZ.decode(Bytes.EMPTY, reader -> reader.readInt(16))); + assertEquals("End of SSZ source reached", ex.getMessage()); + } + + @Test + void shouldThrowWheSourceIsTruncated() { + InvalidSSZTypeException ex = + assertThrows( + InvalidSSZTypeException.class, + () -> SSZ.decode(fromHexString("0000000f830186"), SSZReader::readBytes)); + assertEquals( + "SSZ encoded data has insufficient bytes for decoded byte array length", ex.getMessage()); + } + + @Test + void shouldReadShortVaragsList() { + List expected = + Arrays.asList( + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); + + System.out.println( + SSZ.encodeStringList( + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", + "qwer")); + + List result = SSZ.decodeStringList(SHORT_LIST); + assertEquals(expected, result); + } + + @Test + void shouldReadShortUtilList() { + List expected = + Arrays.asList( + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); + + System.out.println( + SSZ.encodeStringList( + Arrays.asList( + "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", + "qwer"))); + + List result = SSZ.decodeStringList(SHORT_LIST); + assertEquals(expected, result); + } + + @Test + void shouldAcceptStringListOfVariableLengths() { + List expected = Arrays.asList("one", "three", "four"); + + List result = + SSZ.decodeStringList( + Bytes.fromHexString("0x18000000030000006F6E6505000000746872656504000000666F7572")); + assertEquals(expected, result); + } + + @Test + void shouldRoundtripBytesVararg() { + List toWrite = + Arrays.asList(Bytes48.fromRandom(), Bytes48.fromRandom(), Bytes48.fromRandom()); + Bytes encoded = SSZ.encode(writer -> writer.writeBytesList(toWrite.toArray(new Bytes[0]))); + assertEquals(toWrite, SSZ.decodeBytesList(encoded)); + } + + @Test + void shouldRoundtripBytesList() { + List toWrite = + Arrays.asList(Bytes48.fromRandom(), Bytes48.fromRandom(), Bytes48.fromRandom()); + Bytes encoded = SSZ.encode(writer -> writer.writeBytesList(toWrite)); + assertEquals(toWrite, SSZ.decodeBytesList(encoded)); + } + + @Test + void shouldRoundtripBytesVector() { + List toWrite = + Arrays.asList(Bytes48.fromRandom(), Bytes48.fromRandom(), Bytes48.fromRandom()); + Bytes encoded = SSZ.encode(writer -> writer.writeFixedBytesVector(toWrite)); + assertEquals(toWrite, SSZ.decode(encoded, reader -> reader.readFixedBytesVector(3, 48))); + } + + @Test + void shouldRoundtripHomogenousBytesList() { + List toWrite = + Arrays.asList(Bytes32.fromRandom(), Bytes32.fromRandom(), Bytes32.fromRandom()); + Bytes encoded = SSZ.encode(writer -> writer.writeFixedBytesList(toWrite)); + assertEquals(toWrite, SSZ.decode(encoded, reader -> reader.readFixedBytesList(32))); + } +} diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZWriterTest.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZWriterTest.java new file mode 100644 index 000000000..cae213ea7 --- /dev/null +++ b/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZWriterTest.java @@ -0,0 +1,539 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; +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.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes32; +import org.apache.tuweni.v2.units.bigints.UInt256; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +import com.google.common.base.Charsets; +import org.junit.jupiter.api.Test; + +class BytesSSZWriterTest { + + private static class SomeObject { + private final String name; + private final int number; + private final BigInteger longNumber; + + SomeObject(String name, int number, BigInteger longNumber) { + this.name = name; + this.number = number; + this.longNumber = longNumber; + } + } + + @Test + void shouldWriteFullObjects() { + SomeObject bob = new SomeObject("Bob", 4, BigInteger.valueOf(1234563434344L)); + + Bytes bytes = + SSZ.encode( + writer -> { + writer.writeString(bob.name); + writer.writeInt(bob.number, 8); + writer.writeBigInteger(bob.longNumber, 256); + }); + + assertTrue( + SSZ.decode( + bytes, + reader -> { + assertEquals("Bob", reader.readString()); + assertEquals(4, reader.readInt(8)); + assertEquals(BigInteger.valueOf(1234563434344L), reader.readBigInteger(256)); + return true; + })); + } + + @Test + void shouldWriteEmptyStrings() { + assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeString(""))); + } + + @Test + void shouldWriteOneCharactersStrings() { + assertEquals(fromHexString("0100000064"), SSZ.encode(writer -> writer.writeString("d"))); + } + + @Test + void shouldWriteStrings() { + assertEquals(fromHexString("03000000646f67"), SSZ.encode(writer -> writer.writeString("dog"))); + } + + @Test + void shouldWriteSignedIntegers() { + assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeInt(0, 8))); + assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeInt8(0))); + + assertEquals(fromHexString("0000"), SSZ.encode(writer -> writer.writeInt(0, 16))); + assertEquals(fromHexString("0000"), SSZ.encode(writer -> writer.writeInt16(0))); + + assertEquals(fromHexString("000000"), SSZ.encode(writer -> writer.writeInt(0, 24))); + + assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeInt(0, 32))); + assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeInt32(0))); + + assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeInt(1, 8))); + assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeInt8(1))); + + assertEquals(fromHexString("0100"), SSZ.encode(writer -> writer.writeInt(1, 16))); + assertEquals(fromHexString("0100"), SSZ.encode(writer -> writer.writeInt16(1))); + + assertEquals(fromHexString("01000000"), SSZ.encode(writer -> writer.writeInt(1, 32))); + assertEquals(fromHexString("01000000"), SSZ.encode(writer -> writer.writeInt32(1))); + + assertEquals(fromHexString("0f"), SSZ.encode(writer -> writer.writeInt8(15))); + assertEquals(fromHexString("0f00"), SSZ.encode(writer -> writer.writeInt16(15))); + assertEquals(fromHexString("E803"), SSZ.encode(writer -> writer.writeInt16(1000))); + assertEquals(fromHexString("0004"), SSZ.encode(writer -> writer.writeInt16(1024))); + assertEquals(fromHexString("A08601"), SSZ.encode(writer -> writer.writeInt(100000, 24))); + + assertEquals(fromHexString("FF"), SSZ.encode(writer -> writer.writeInt(-1, 8))); + assertEquals(fromHexString("FFFF"), SSZ.encode(writer -> writer.writeInt(-1, 16))); + assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeInt(-128, 8))); + assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeInt8(-128))); + assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeInt(-32768, 16))); + assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeInt16(-32768))); + } + + @Test + void shouldWriteSignedLongs() { + assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeLong(0, 8))); + assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeLong(1, 8))); + assertEquals(fromHexString("0f"), SSZ.encode(writer -> writer.writeLong(15, 8))); + + assertEquals(fromHexString("e803"), SSZ.encode(writer -> writer.writeLong(1000, 16))); + assertEquals(fromHexString("0004"), SSZ.encode(writer -> writer.writeLong(1024, 16))); + assertEquals(fromHexString("A08601"), SSZ.encode(writer -> writer.writeLong(100000L, 24))); + assertEquals(fromHexString("A0860100"), SSZ.encode(writer -> writer.writeLong(100000L, 32))); + assertEquals( + fromHexString("A086010000000000"), SSZ.encode(writer -> writer.writeLong(100000L, 64))); + assertEquals( + fromHexString("A086010000000000"), SSZ.encode(writer -> writer.writeInt64(100000L))); + + assertEquals(fromHexString("FF"), SSZ.encode(writer -> writer.writeLong(-1, 8))); + assertEquals(fromHexString("FFFF"), SSZ.encode(writer -> writer.writeLong(-1, 16))); + assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeLong(-128, 8))); + assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeLong(-32768, 16))); + assertEquals( + fromHexString("0000000000000080"), + SSZ.encode(writer -> writer.writeInt64(-9223372036854775808L))); + } + + @Test + void shouldWriteSignedBigIntegers() { + assertEquals( + fromHexString("A08601"), + SSZ.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(100000), 24))); + assertEquals( + fromHexString("16EB"), + SSZ.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(-5354), 16))); + assertEquals( + fromHexString("0080"), + SSZ.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(-32768), 16))); + assertEquals( + fromHexString("01F81D7AF1971CEDD9BBA5EFCEE1"), + SSZ.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(127).pow(16), 112))); + } + + @Test + void shouldWriteUnsignedIntegers() { + assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeUInt(0, 8))); + assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeUInt8(0))); + + assertEquals(fromHexString("0000"), SSZ.encode(writer -> writer.writeUInt(0, 16))); + assertEquals(fromHexString("0000"), SSZ.encode(writer -> writer.writeUInt16(0))); + + assertEquals(fromHexString("000000"), SSZ.encode(writer -> writer.writeUInt(0, 24))); + + assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeUInt(0, 32))); + assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeUInt32(0))); + + assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeUInt(1, 8))); + assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeUInt8(1))); + + assertEquals(fromHexString("0100"), SSZ.encode(writer -> writer.writeUInt(1, 16))); + assertEquals(fromHexString("0100"), SSZ.encode(writer -> writer.writeUInt16(1))); + + assertEquals(fromHexString("01000000"), SSZ.encode(writer -> writer.writeUInt(1, 32))); + assertEquals(fromHexString("01000000"), SSZ.encode(writer -> writer.writeUInt32(1))); + + assertEquals(fromHexString("0f"), SSZ.encode(writer -> writer.writeUInt8(15))); + assertEquals(fromHexString("0f00"), SSZ.encode(writer -> writer.writeUInt16(15))); + assertEquals(fromHexString("e803"), SSZ.encode(writer -> writer.writeUInt16(1000))); + assertEquals(fromHexString("0004"), SSZ.encode(writer -> writer.writeUInt16(1024))); + assertEquals(fromHexString("A08601"), SSZ.encode(writer -> writer.writeUInt(100000, 24))); + + assertEquals(fromHexString("FF"), SSZ.encode(writer -> writer.writeUInt(255, 8))); + assertEquals(fromHexString("FFFF"), SSZ.encode(writer -> writer.writeUInt(65535, 16))); + assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeUInt(128, 8))); + assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeUInt8(128))); + assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeUInt(32768, 16))); + assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeUInt16(32768))); + } + + @Test + void shouldWriteUnsignedLongs() { + assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeULong(0, 8))); + assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeULong(1, 8))); + assertEquals(fromHexString("0f"), SSZ.encode(writer -> writer.writeULong(15, 8))); + + assertEquals(fromHexString("e803"), SSZ.encode(writer -> writer.writeULong(1000, 16))); + assertEquals(fromHexString("0004"), SSZ.encode(writer -> writer.writeULong(1024, 16))); + assertEquals(fromHexString("A08601"), SSZ.encode(writer -> writer.writeULong(100000L, 24))); + assertEquals(fromHexString("A0860100"), SSZ.encode(writer -> writer.writeULong(100000L, 32))); + assertEquals( + fromHexString("A086010000000000"), SSZ.encode(writer -> writer.writeULong(100000L, 64))); + assertEquals( + fromHexString("A086010000000000"), SSZ.encode(writer -> writer.writeUInt64(100000L))); + + assertEquals(fromHexString("FF"), SSZ.encode(writer -> writer.writeULong(255, 8))); + assertEquals(fromHexString("FFFF"), SSZ.encode(writer -> writer.writeULong(65535, 16))); + assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeULong(128, 8))); + assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeULong(32768, 16))); + assertEquals( + fromHexString("0000000000000080"), + SSZ.encode( + writer -> { + writer.writeUInt64(Long.parseUnsignedLong("9223372036854775808")); + })); + } + + @Test + void shouldWriteUInt256Integers() { + assertEquals( + fromHexString("0000000000000000000000000000000000000000000000000000000000000000"), + SSZ.encode(writer -> writer.writeUInt256(UInt256.valueOf(0L)))); + assertEquals( + fromHexString("A086010000000000000000000000000000000000000000000000000000000000"), + SSZ.encode(writer -> writer.writeUInt256(UInt256.valueOf(100000L)))); + assertEquals( + fromHexString("AB00000000F10000000000000000000000000000000000000000000000000004"), + SSZ.encode( + writer -> + writer.writeUInt256( + UInt256.fromHexString( + "0x0400000000000000000000000000000000000000000000000000f100000000ab")))); + } + + @Test + void shouldWriteBooleans() { + assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeBoolean(false))); + assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeBoolean(true))); + } + + @Test + void shouldWriteAddresses() { + assertEquals( + fromHexString("8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), + SSZ.encode( + writer -> + writer.writeAddress( + Bytes.fromHexString("8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97")))); + assertThrows( + IllegalArgumentException.class, + () -> SSZ.encode(writer -> writer.writeAddress(Bytes.fromHexString("beef")))); + } + + @Test + void shouldWriteHashes() { + assertEquals( + fromHexString("ED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), + SSZ.encode( + writer -> + writer.writeHash( + Bytes.fromHexString( + "ED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97")))); + assertThrows( + IllegalArgumentException.class, + () -> SSZ.encode(writer -> writer.writeAddress(Bytes.fromHexString("beef")))); + } + + @Test + void shouldWriteVarargsListsOfInts() { + assertEquals(fromHexString("03000000030405"), SSZ.encodeIntList(8, 3, 4, 5)); + } + + @Test + void shouldWriteUtilListsOfInts() { + assertEquals(fromHexString("03000000030405"), SSZ.encodeIntList(8, Arrays.asList(3, 4, 5))); + } + + @Test + void shouldWriteVarargsListsOfLongInts() { + assertEquals(fromHexString("03000000030405"), SSZ.encodeLongIntList(8, 3, 4, 5)); + } + + @Test + void shouldWriteUtilListsOfLongInts() { + assertEquals( + fromHexString("03000000030405"), + SSZ.encodeLongIntList(8, Arrays.asList((long) 3, (long) 4, (long) 5))); + } + + @Test + void shouldWriteVarargsListsOfBigIntegers() { + assertEquals( + fromHexString("03000000030405"), + SSZ.encodeBigIntegerList( + 8, BigInteger.valueOf(3), BigInteger.valueOf(4), BigInteger.valueOf(5))); + } + + @Test + void shouldWriteUtilListsOfBigIntegers() { + assertEquals( + fromHexString("03000000030405"), + SSZ.encodeBigIntegerList( + 8, Arrays.asList(BigInteger.valueOf(3), BigInteger.valueOf(4), BigInteger.valueOf(5)))); + } + + @Test + void shouldWriteVarargsListsOfUnsignedInts() { + assertEquals(fromHexString("03000000FDFEFF"), SSZ.encodeUIntList(8, 253, 254, 255)); + } + + @Test + void shouldWriteUtilListsOfUnsignedInts() { + assertEquals( + fromHexString("03000000FDFEFF"), SSZ.encodeUIntList(8, Arrays.asList(253, 254, 255))); + } + + @Test + void shouldWriteVarargsListsOfUnsignedLongs() { + assertEquals(fromHexString("03000000FDFEFF"), SSZ.encodeULongIntList(8, 253, 254, 255)); + } + + @Test + void shouldWriteUtilListsOfUnsignedLongs() { + assertEquals( + fromHexString("03000000FDFEFF"), + SSZ.encodeULongIntList(8, Arrays.asList((long) 253, (long) 254, (long) 255))); + } + + @Test + void shouldWriteVarargsListsOfUInt256() { + assertEquals( + fromHexString( + "0x60000000030000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000"), + SSZ.encodeUInt256List(UInt256.valueOf(3L), UInt256.valueOf(4L), UInt256.valueOf(5L))); + } + + @Test + void shouldWriteUtilListsOfUInt256() { + assertEquals( + fromHexString( + "0x60000000030000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000"), + SSZ.encodeUInt256List( + Arrays.asList(UInt256.valueOf(3L), UInt256.valueOf(4L), UInt256.valueOf(5L)))); + } + + @Test + void shouldWriteVaragsListsOfStrings() { + assertEquals( + fromHexString("1800000003000000626F62040000006A616E65050000006A616E6574"), + SSZ.encodeStringList("bob", "jane", "janet")); + } + + @Test + void shouldWriteUtilListsOfStrings() { + assertEquals( + fromHexString("1800000003000000626F62040000006A616E65050000006A616E6574"), + SSZ.encodeStringList(Arrays.asList("bob", "jane", "janet"))); + } + + @Test + void shouldWriteVarargsListsOfBytes() { + assertEquals( + fromHexString("1800000003000000626F62040000006A616E65050000006A616E6574"), + SSZ.encodeBytesList( + Bytes.wrap("bob".getBytes(Charsets.UTF_8)), + Bytes.wrap("jane".getBytes(Charsets.UTF_8)), + Bytes.wrap("janet".getBytes(Charsets.UTF_8)))); + } + + @Test + void shouldWriteUtilListOfBytes() { + assertEquals( + fromHexString("1800000003000000626F62040000006A616E65050000006A616E6574"), + SSZ.encodeBytesList( + Arrays.asList( + Bytes.wrap("bob".getBytes(Charsets.UTF_8)), + Bytes.wrap("jane".getBytes(Charsets.UTF_8)), + Bytes.wrap("janet".getBytes(Charsets.UTF_8))))); + } + + @Test + void shouldWriteUtilListOfExtendedBytesType() { + assertEquals( + fromHexString( + "0x6C000000200000000000000000000000000000000000000000000000000000000000000000626F6220000000000000000000000000000000000000000000000000000000000000006A616E65200000000000000000000000000000000000000000000000000000000000006A616E6574"), + SSZ.encodeBytesList( + Arrays.asList( + Bytes.wrap("bob".getBytes(Charsets.UTF_8)).mutableCopy().leftPad(32), + Bytes.wrap("jane".getBytes(Charsets.UTF_8)).mutableCopy().leftPad(32), + Bytes.wrap("janet".getBytes(Charsets.UTF_8)).mutableCopy().leftPad(32)))); + } + + @Test + void shouldWriteVarargsListsOfHashes() { + assertEquals( + fromHexString( + "0x60000000ED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C978B40CA3893681B062BC06760A4863AFAFA6C4D6226D4C6AFAFA3684A06760CB26B30567B2281FF3BD582B0A633B33A376B95BD3333DB59B673A33B336A0B285D"), + SSZ.encodeHashList( + fromHexString("0xED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), + fromHexString("0x8B40CA3893681B062BC06760A4863AFAFA6C4D6226D4C6AFAFA3684A06760CB2"), + fromHexString("0x6B30567B2281FF3BD582B0A633B33A376B95BD3333DB59B673A33B336A0B285D"))); + } + + @Test + void shouldWriteUtilListsOfHashes() { + assertEquals( + fromHexString( + "0x60000000ED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C978B40CA3893681B062BC06760A4863AFAFA6C4D6226D4C6AFAFA3684A06760CB26B30567B2281FF3BD582B0A633B33A376B95BD3333DB59B673A33B336A0B285D"), + SSZ.encodeHashList( + Arrays.asList( + fromHexString("0xED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), + fromHexString("0x8B40CA3893681B062BC06760A4863AFAFA6C4D6226D4C6AFAFA3684A06760CB2"), + fromHexString( + "0x6B30567B2281FF3BD582B0A633B33A376B95BD3333DB59B673A33B336A0B285D")))); + } + + @Test + void shouldWriteVaragsListsOfAddresses() { + assertEquals( + fromHexString( + "0x3C0000008EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C9779C1DE9DBB5AFEEC1EE8BBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5"), + SSZ.encodeAddressList( + fromHexString("0x8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), + fromHexString("0x8EE1CEEFA5BBD9ED1C9779C1DE9DBB5AFEEC1EE8"), + fromHexString("0xBBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5"))); + } + + @Test + void shouldWriteUtilListsOfAddresses() { + assertEquals( + fromHexString( + "0x3C0000008EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C9779C1DE9DBB5AFEEC1EE8BBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5"), + SSZ.encodeAddressList( + Arrays.asList( + fromHexString("0x8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), + fromHexString("0x8EE1CEEFA5BBD9ED1C9779C1DE9DBB5AFEEC1EE8"), + fromHexString("0xBBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5")))); + } + + @Test + void shouldWriteVaragsListsOfBooleans() { + assertEquals( + fromHexString("0400000000010100"), SSZ.encodeBooleanList(false, true, true, false)); + } + + @Test + void shouldWriteUtilListsOfBooleans() { + assertEquals( + fromHexString("0400000000010100"), + SSZ.encodeBooleanList(Arrays.asList(false, true, true, false))); + } + + @Test + void shouldWriteVectorOfHomogeneousBytes() { + // Per the pre-SOS SSZ spec, neither the vector nor the bytes should have a mixin. + List elements = + Arrays.asList( + Bytes32.fromHexString("0x01"), + Bytes32.fromHexString("0x02"), + Bytes32.fromHexString("0x03"), + Bytes32.fromHexString("0x04"), + Bytes32.fromHexString("0x05")); + assertEquals( + fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000001" + + "0000000000000000000000000000000000000000000000000000000000000002" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "0000000000000000000000000000000000000000000000000000000000000004" + + "0000000000000000000000000000000000000000000000000000000000000005"), + SSZ.encode(writer -> writer.writeFixedBytesVector(elements))); + } + + @Test + void shouldWriteVectorOfNonHomogeneousBytes() { + // Per the pre-SOS SSZ spec, the vector itself should not have a mixin, but the individual bytes + // elements should. + List elements = + Arrays.asList( + Bytes32.fromHexString("0x01"), + Bytes32.fromHexString("0x02"), + Bytes32.fromHexString("0x03"), + Bytes32.fromHexString("0x04"), + Bytes32.fromHexString("0x05")); + assertEquals( + fromHexString( + "0x200000000000000000000000000000000000000000000000000000000000000000000001" + + "200000000000000000000000000000000000000000000000000000000000000000000002" + + "200000000000000000000000000000000000000000000000000000000000000000000003" + + "200000000000000000000000000000000000000000000000000000000000000000000004" + + "200000000000000000000000000000000000000000000000000000000000000000000005"), + SSZ.encode(writer -> writer.writeVector(elements))); + } + + @Test + void shouldWriteListOfHomogeneousBytes() { + // Per the pre-SOS SSZ spec, the list iself should have a mixin, but the bytes elements should + // not. + List elements = + Arrays.asList( + Bytes32.fromHexString("0x01"), + Bytes32.fromHexString("0x02"), + Bytes32.fromHexString("0x03"), + Bytes32.fromHexString("0x04"), + Bytes32.fromHexString("0x05")); + assertEquals( + fromHexString( + "0xA0000000" + + "0000000000000000000000000000000000000000000000000000000000000001" + + "0000000000000000000000000000000000000000000000000000000000000002" + + "0000000000000000000000000000000000000000000000000000000000000003" + + "0000000000000000000000000000000000000000000000000000000000000004" + + "0000000000000000000000000000000000000000000000000000000000000005"), + SSZ.encode(writer -> writer.writeFixedBytesList(elements))); + } + + @Test + void shouldWriteListOfNonHomogeneousBytes() { + // Per the pre-SOS SSZ spec, both the vector itself and the individual bytes elements should + // have a length mixin. + List elements = + Arrays.asList( + Bytes32.fromHexString("0x01"), + Bytes32.fromHexString("0x02"), + Bytes32.fromHexString("0x03"), + Bytes32.fromHexString("0x04"), + Bytes32.fromHexString("0x05")); + assertEquals( + fromHexString( + "0xB4000000" + + "200000000000000000000000000000000000000000000000000000000000000000000001" + + "200000000000000000000000000000000000000000000000000000000000000000000002" + + "200000000000000000000000000000000000000000000000000000000000000000000003" + + "200000000000000000000000000000000000000000000000000000000000000000000004" + + "200000000000000000000000000000000000000000000000000000000000000000000005"), + SSZ.encode(writer -> writer.writeBytesList(elements))); + } + + @Test + void shouldWritePreviouslyEncodedValues() { + Bytes output = + SSZ.encode(writer -> writer.writeSSZ(SSZ.encodeByteArray("abc".getBytes(UTF_8)))); + assertEquals("abc", SSZ.decodeString(output)); + } +} diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/HashTreeRootTest.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/HashTreeRootTest.java new file mode 100644 index 000000000..1726df972 --- /dev/null +++ b/ssz/src/test/java/org/apache/tuweni/v2/ssz/HashTreeRootTest.java @@ -0,0 +1,105 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.apache.tuweni.junit.BouncyCastleExtension; +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.bytes.Bytes32; +import org.apache.tuweni.v2.crypto.Hash; + +import org.bouncycastle.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(BouncyCastleExtension.class) +class HashTreeRootTest { + + @Test + void hashBoolean() { + assertEquals( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000"), + SSZ.hashTreeRoot(SSZ.encodeBoolean(false))); + assertEquals( + Bytes.fromHexString("0x0100000000000000000000000000000000000000000000000000000000000000"), + SSZ.hashTreeRoot(SSZ.encodeBoolean(true))); + } + + @Test + void hashUint8() { + assertEquals( + Bytes.fromHexString("0x0400000000000000000000000000000000000000000000000000000000000000"), + SSZ.hashTreeRoot(SSZ.encodeUInt(4, 8))); + } + + @Test + void hashBytes32() { + Bytes someBytes = Bytes32.fromRandom(); + assertEquals(someBytes, SSZ.hashTreeRoot(someBytes)); + } + + @Test + void hashBytes34() { + Bytes someBytes = Bytes.random(34); + assertEquals(Hash.keccak256(someBytes), SSZ.hashTreeRoot(someBytes)); + } + + @Test + void list() { + Bytes[] list = + new Bytes[] { + Bytes.wrap(new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}), + Bytes.wrap(new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}), + Bytes.wrap(new byte[] {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}), + Bytes.wrap(new byte[] {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}), + Bytes.wrap(new byte[] {5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}), + Bytes.wrap(new byte[] {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}), + Bytes.wrap(new byte[] {7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7}), + Bytes.wrap(new byte[] {8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}), + Bytes.wrap(new byte[] {9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9}), + Bytes.wrap(new byte[] {10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}) + }; + assertEquals( + Bytes.fromHexString("0x839D98509E2EFC53BD1DEA17403921A89856E275BBF4D56C600CC3F6730AAFFA"), + SSZ.hashTreeRoot(list)); + } + + @Test + void list2() { + byte[] _1s = new byte[32]; + byte[] _2s = new byte[32]; + byte[] _3s = new byte[32]; + byte[] _4s = new byte[32]; + byte[] _5s = new byte[32]; + byte[] _6s = new byte[32]; + byte[] _7s = new byte[32]; + byte[] _8s = new byte[32]; + byte[] _9s = new byte[32]; + byte[] _as = new byte[32]; + Arrays.fill(_1s, (byte) 1); + Arrays.fill(_2s, (byte) 2); + Arrays.fill(_3s, (byte) 3); + Arrays.fill(_4s, (byte) 4); + Arrays.fill(_5s, (byte) 5); + Arrays.fill(_6s, (byte) 6); + Arrays.fill(_7s, (byte) 7); + Arrays.fill(_8s, (byte) 8); + Arrays.fill(_9s, (byte) 9); + Arrays.fill(_as, (byte) 10); + + assertEquals( + Bytes.fromHexString("0x55DC6699E7B5713DD9102224C302996F931836C6DAE9A4EC6AB49C966F394685"), + SSZ.hashTreeRoot( + Bytes.wrap(_1s), + Bytes.wrap(_2s), + Bytes.wrap(_3s), + Bytes.wrap(_4s), + Bytes.wrap(_5s), + Bytes.wrap(_6s), + Bytes.wrap(_7s), + Bytes.wrap(_8s), + Bytes.wrap(_9s), + Bytes.wrap(_as))); + } +} diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/TransactionNetworkPayload.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/TransactionNetworkPayload.java new file mode 100644 index 000000000..a10e97837 --- /dev/null +++ b/ssz/src/test/java/org/apache/tuweni/v2/ssz/TransactionNetworkPayload.java @@ -0,0 +1,302 @@ +// Copyright The Tuweni Authors +// SPDX-License-Identifier: Apache-2.0 +package org.apache.tuweni.v2.ssz; + +import org.apache.tuweni.v2.bytes.Bytes; +import org.apache.tuweni.v2.units.bigints.UInt256; + +public class TransactionNetworkPayload implements SSZReadable, SSZWritable { + public static final int KZG_COMMITMENT_SIZE = 48; + public static final int FIELD_ELEMENTS_PER_BLOB = 4096; + public static final int ELEMENT_SIZE = 32; + SingedBlobTransaction signedBlobTransaction = new SingedBlobTransaction(); + SSZFixedSizeTypeList kzgCommitments = + new SSZFixedSizeTypeList<>(KZG_COMMITMENT_SIZE, KZGCommitment::new); + SSZFixedSizeTypeList blobs = + new SSZFixedSizeTypeList<>(FIELD_ELEMENTS_PER_BLOB * ELEMENT_SIZE, Blob::new); + + KZGProof kzgProof = new KZGProof(); + + @Override + public boolean isFixed() { + return false; + } + + @Override + public void populateFromReader(SSZReader reader) { + reader.readAsContainer(signedBlobTransaction, kzgCommitments, blobs, kzgProof); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeAsContainer(signedBlobTransaction, kzgCommitments, blobs, kzgProof); + } + + public SingedBlobTransaction getSignedBlobTransaction() { + return signedBlobTransaction; + } + + public SSZFixedSizeTypeList getKzgCommitments() { + return kzgCommitments; + } + + public SSZFixedSizeTypeList getBlobs() { + return blobs; + } + + public KZGProof getKzgProof() { + return kzgProof; + } + + public static class SingedBlobTransaction implements SSZReadable, SSZWritable { + private final BlobTransaction message = new BlobTransaction(); + private final ECDSASignature signature = new ECDSASignature(); + + @Override + public boolean isFixed() { + return false; + } + + @Override + public void populateFromReader(SSZReader reader) { + reader.readAsContainer(message, signature); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeAsContainer(message, signature); + } + + public BlobTransaction getMessage() { + return message; + } + + public static class BlobTransaction implements SSZReadable, SSZWritable { + final Data data = new Data(); + UInt256 chainId; + long nonce; + UInt256 maxPriorityFeePerGas; + UInt256 maxFeePerGas; + long gas; + AddressUnion address = new AddressUnion(); + UInt256 value; + SSZVariableSizeTypeList accessList = + new SSZVariableSizeTypeList<>(AccessTuple::new); + UInt256 maxFeePerData; + + SSZFixedSizeTypeList blobVersionedHashes = + new SSZFixedSizeTypeList<>(32, VersionedHash::new); + + @Override + public boolean isFixed() { + return false; + } + + @Override + public void populateFromReader(SSZReader reader) { + reader.readAsContainer( + r -> chainId = r.readUInt256(), + r -> nonce = r.readUInt64(), + r -> maxPriorityFeePerGas = r.readUInt256(), + r -> maxFeePerGas = r.readUInt256(), + r -> gas = r.readUInt64(), + address, + r -> value = r.readUInt256(), + data, + accessList, + r -> maxFeePerData = r.readUInt256(), + blobVersionedHashes); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeAsContainer( + w -> w.writeUInt256(chainId), + w -> w.writeUInt64(nonce), + w -> w.writeUInt256(maxPriorityFeePerGas), + w -> w.writeUInt256(maxFeePerGas), + w -> w.writeUInt64(gas), + address, + w -> w.writeUInt256(value), + data, + accessList, + w -> w.writeUInt256(maxFeePerData), + blobVersionedHashes); + } + } + + public static class AddressUnion implements SSZReadable, SSZWritable { + private Bytes address; + + @Override + public boolean isFixed() { + return false; + } + + @Override + public void writeTo(SSZWriter writer) { + if (address == null) { + writer.writeUInt8(0); + } else { + writer.writeUInt8(1); + writer.writeAddress(address); + } + } + + @Override + public void populateFromReader(SSZReader reader) { + final int type = reader.readUInt8(); + if (type == 1) { + address = reader.readAddress(); + } + } + } + + public static class Data implements SSZReadable, SSZWritable { + public static final int MAX_CALL_DATA_SIZE = 16777216; // 2**24 + + Bytes data; + + @Override + public boolean isFixed() { + return false; + } + + @Override + public void populateFromReader(SSZReader reader) { + if (reader.isComplete()) { + return; + } + data = reader.consumeRemainingBytes(MAX_CALL_DATA_SIZE); + } + + @Override + public void writeTo(SSZWriter writer) { + if (data != null) { + writer.writeBytes(data); + } + } + } + + public static class AccessTuple implements SSZReadable, SSZWritable { + Bytes address; + SSZFixedSizeTypeList storageKeys = + new SSZFixedSizeTypeList<>(ELEMENT_SIZE, SSZUInt256Wrapper::new); + + @Override + public boolean isFixed() { + return false; + } + + @Override + public void populateFromReader(SSZReader reader) { + reader.readAsContainer(r -> address = r.readAddress(), storageKeys); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeAsContainer(w -> w.writeAddress(address), storageKeys); + } + } + + public static class VersionedHash implements SSZReadable, SSZWritable { + private Bytes bytes; + + @Override + public boolean isFixed() { + return true; + } + + @Override + public void populateFromReader(SSZReader reader) { + reader.readAsContainer(r -> bytes = r.readFixedBytes(32)); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeAsContainer(w -> w.writeFixedBytes(bytes)); + } + } + + public static class ECDSASignature implements SSZReadable, SSZWritable { + boolean parity; + UInt256 r; + UInt256 s; + + @Override + public void populateFromReader(SSZReader reader) { + parity = reader.readBoolean(); + r = reader.readUInt256(); + s = reader.readUInt256(); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeBoolean(parity); + writer.writeUInt256(r); + writer.writeUInt256(s); + } + } + } + + public static class KZGCommitment implements SSZReadable, SSZWritable { + Bytes data; + + @Override + public void populateFromReader(SSZReader reader) { + data = reader.readFixedBytes(KZG_COMMITMENT_SIZE); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeFixedBytes(data); + } + } + + public static class Blob implements SSZReadable, SSZWritable { + SSZFixedSizeVector vector = + new SSZFixedSizeVector<>(FIELD_ELEMENTS_PER_BLOB, ELEMENT_SIZE, SSZUInt256Wrapper::new); + + @Override + public void populateFromReader(SSZReader reader) { + vector.populateFromReader(reader); + } + + @Override + public void writeTo(SSZWriter writer) { + vector.writeTo(writer); + } + } + + public static class SSZUInt256Wrapper implements SSZReadable, SSZWritable { + UInt256 data; + + @Override + public void populateFromReader(SSZReader reader) { + data = reader.readUInt256(); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeUInt256(data); + } + } + + public static class KZGProof implements SSZReadable, SSZWritable { + Bytes bytes; + + @Override + public void populateFromReader(SSZReader reader) { + bytes = reader.readFixedBytes(48); + } + + @Override + public void writeTo(SSZWriter writer) { + writer.writeFixedBytes(bytes); + } + + public Bytes getBytes() { + return bytes; + } + } +} diff --git a/units/.factorypath b/units/.factorypath new file mode 100644 index 000000000..62faf29fd --- /dev/null +++ b/units/.factorypath @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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()); + } +} From 974909629f60452d3ce6105f023052d873dbe245 Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Thu, 20 Nov 2025 16:44:35 +0000 Subject: [PATCH 2/7] Make MutableBytes inherit from ArrayWrappingBytes Signed-off-by: Luis Pinto --- .../tuweni/v2/bytes/ArrayWrappingBytes.java | 6 +- .../apache/tuweni/v2/bytes/MutableBytes.java | 202 ++++++------------ 2 files changed, 75 insertions(+), 133 deletions(-) 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 index 2b7d7e317..5a50c2011 100644 --- a/bytes/src/main/java/org/apache/tuweni/v2/bytes/ArrayWrappingBytes.java +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/ArrayWrappingBytes.java @@ -14,9 +14,13 @@ class ArrayWrappingBytes extends Bytes { - protected final byte[] 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; 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 index a000d7448..685aabf16 100644 --- a/bytes/src/main/java/org/apache/tuweni/v2/bytes/MutableBytes.java +++ b/bytes/src/main/java/org/apache/tuweni/v2/bytes/MutableBytes.java @@ -15,24 +15,20 @@ import io.vertx.core.buffer.Buffer; /** A class for doing modifications on a {@link Bytes} value without modifying the original. */ -public class MutableBytes extends Bytes { - private byte[] bytesArray; +public class MutableBytes extends ArrayWrappingBytes { MutableBytes(int size) { - super(size); - this.bytesArray = new byte[size]; + super(new byte[size]); } MutableBytes(byte[] bytesArray) { - super(bytesArray.length); - this.bytesArray = new byte[size]; - System.arraycopy(bytesArray, 0, this.bytesArray, 0, size); + super(new byte[bytesArray.length]); + System.arraycopy(bytesArray, 0, bytes, 0, bytesArray.length); } MutableBytes(byte[] bytesArray, int offset, int length) { - super(length); - this.bytesArray = new byte[length]; - System.arraycopy(bytesArray, offset, this.bytesArray, 0, length); + super(new byte[length]); + System.arraycopy(bytesArray, offset, bytes, 0, length); } /** @@ -146,7 +142,7 @@ public static MutableBytes fromByteBuf(ByteBuf byteBuf) { return MutableBytes.create(0); } MutableBytes mutableBytes = MutableBytes.create(byteBuf.capacity()); - byteBuf.getBytes(0, mutableBytes.bytesArray); + byteBuf.getBytes(0, mutableBytes.bytes); return mutableBytes; } @@ -177,7 +173,7 @@ public static MutableBytes fromByteBuf(ByteBuf byteBuf, int offset, int length) return MutableBytes.create(0); } MutableBytes mutableBytes = MutableBytes.create(length); - byteBuf.getBytes(offset, mutableBytes.bytesArray); + byteBuf.getBytes(offset, mutableBytes.bytes); return mutableBytes; } @@ -195,7 +191,7 @@ public static MutableBytes fromByteBuffer(ByteBuffer byteBuffer) { return MutableBytes.create(0); } MutableBytes mutableBytes = MutableBytes.create(byteBuffer.limit()); - byteBuffer.get(0, mutableBytes.bytesArray); + byteBuffer.get(0, mutableBytes.bytes); return mutableBytes; } @@ -226,7 +222,7 @@ public static MutableBytes fromByteBuffer(ByteBuffer byteBuffer, int offset, int return MutableBytes.create(0); } MutableBytes mutableBytes = MutableBytes.create(length); - byteBuffer.get(offset, mutableBytes.bytesArray); + byteBuffer.get(offset, mutableBytes.bytes); return mutableBytes; } @@ -273,7 +269,7 @@ public void set(int index, Bytes bytes) { return; } checkElementIndex(index, size); - checkLength(bytesArray.length, index, bytes.size()); + checkLength(this.bytes.length, index, bytes.size()); for (int i = 0; i < bytes.size(); i++) { set(i + index, bytes.get(i)); } @@ -293,7 +289,7 @@ public void set(int index, byte[] bytes) { return; } checkElementIndex(index, size); - checkLength(bytesArray.length, index, bytes.length); + checkLength(this.bytes.length, index, bytes.length); for (int i = 0; i < bytes.length; i++) { set(i + index, bytes[i]); } @@ -356,7 +352,8 @@ public void setLong(int index, long value) { */ public void set(int index, byte b) { checkElementIndex(index, size); - bytesArray[index] = b; + bytes[index] = b; + hashCode = null; } /** @@ -369,14 +366,15 @@ public void set(int index, byte b) { */ public MutableBytes increment() { for (int i = size - 1; i >= 0; --i) { - if (bytesArray[i] == (byte) 0xFF) { - bytesArray[i] = (byte) 0x00; + if (bytes[i] == (byte) 0xFF) { + bytes[i] = (byte) 0x00; } else { - byte currentValue = bytesArray[i]; - bytesArray[i] = ++currentValue; + byte currentValue = bytes[i]; + bytes[i] = ++currentValue; break; } } + hashCode = null; return this; } @@ -390,14 +388,15 @@ public MutableBytes increment() { */ public MutableBytes decrement() { for (int i = size - 1; i >= 0; --i) { - if (bytesArray[i] == (byte) 0x00) { - bytesArray[i] = (byte) 0xFF; + if (bytes[i] == (byte) 0x00) { + bytes[i] = (byte) 0xFF; } else { - byte currentValue = bytesArray[i]; - bytesArray[i] = --currentValue; + byte currentValue = bytes[i]; + bytes[i] = --currentValue; break; } } + hashCode = null; return this; } @@ -409,8 +408,9 @@ public MutableBytes decrement() { */ public MutableBytes fill(byte b) { for (int i = 0; i < size; i++) { - bytesArray[i] = b; + bytes[i] = b; } + hashCode = null; return this; } @@ -432,9 +432,10 @@ public MutableBytes clear() { public MutableBytes reverse() { byte[] reverse = new byte[size]; for (int i = 0; i < size; i++) { - reverse[size - 1 - i] = bytesArray[i]; + reverse[size - 1 - i] = bytes[i]; } - bytesArray = reverse; + bytes = reverse; + hashCode = null; return this; } @@ -448,29 +449,26 @@ public MutableBytes and(Bytes other) { checkNotNull(other); int otherSize = other.size(); if (size == otherSize) { - other.and(bytesArray, 0, size); + other.and(bytes, 0, size); + hashCode = null; return this; } int otherOffset = 0; if (size < otherSize) { byte[] newBytesArray = new byte[otherSize]; - System.arraycopy(bytesArray, 0, newBytesArray, otherSize - size, size); - bytesArray = newBytesArray; + System.arraycopy(bytes, 0, newBytesArray, otherSize - size, size); + bytes = newBytesArray; size = otherSize; } else { - Arrays.fill(bytesArray, 0, size - otherSize, (byte) 0); + Arrays.fill(bytes, 0, size - otherSize, (byte) 0); otherOffset = size - otherSize; } - other.and(bytesArray, otherOffset, otherSize); + other.and(bytes, otherOffset, otherSize); + hashCode = null; return this; } - @Override - protected void and(byte[] bytesArray, int offset, int length) { - Utils.and(this.bytesArray, 0, bytesArray, offset, length); - } - /** * Calculate a bit-wise OR of these bytes and the supplied bytes. * @@ -481,28 +479,25 @@ public MutableBytes or(Bytes other) { checkNotNull(other); int otherSize = other.size(); if (size == otherSize) { - other.or(bytesArray, 0, size); + other.or(bytes, 0, size); + hashCode = null; return this; } int otherOffset = 0; if (size < otherSize) { byte[] newBytesArray = new byte[otherSize]; - System.arraycopy(bytesArray, 0, newBytesArray, otherSize - size, size); - bytesArray = newBytesArray; + System.arraycopy(bytes, 0, newBytesArray, otherSize - size, size); + bytes = newBytesArray; size = otherSize; } else { otherOffset = size - otherSize; } - other.or(bytesArray, otherOffset, otherSize); + other.or(bytes, otherOffset, otherSize); + hashCode = null; return this; } - @Override - protected void or(byte[] bytesArray, int offset, int length) { - Utils.or(this.bytesArray, 0, bytesArray, offset, length); - } - /** * Calculate a bit-wise XOR of these bytes and the supplied bytes. * @@ -513,28 +508,25 @@ public MutableBytes xor(Bytes other) { checkNotNull(other); int otherSize = other.size(); if (size == otherSize) { - other.xor(bytesArray, 0, size); + other.xor(bytes, 0, size); + hashCode = null; return this; } int otherOffset = 0; if (size < otherSize) { byte[] newBytesArray = new byte[otherSize]; - System.arraycopy(bytesArray, 0, newBytesArray, otherSize - size, size); - bytesArray = newBytesArray; + System.arraycopy(bytes, 0, newBytesArray, otherSize - size, size); + bytes = newBytesArray; size = otherSize; } else { otherOffset = size - otherSize; } - other.xor(bytesArray, otherOffset, otherSize); + other.xor(bytes, otherOffset, otherSize); + hashCode = null; return this; } - @Override - protected void xor(byte[] bytesArray, int offset, int length) { - Utils.xor(this.bytesArray, 0, bytesArray, offset, length); - } - /** * Calculate a bit-wise NOT of these bytes. * @@ -542,8 +534,9 @@ protected void xor(byte[] bytesArray, int offset, int length) { */ public MutableBytes not() { for (int i = 0; i < size; i++) { - bytesArray[i] = (byte) ~bytesArray[i]; + bytes[i] = (byte) ~bytes[i]; } + hashCode = null; return this; } @@ -555,7 +548,7 @@ public MutableBytes not() { */ public MutableBytes shiftRight(int distance) { checkArgument(distance >= 0, "Invalid negative distance"); - if (distance == 0) { + if (distance == 0 || size() == 0) { return this; } distance = Math.min(distance, size * 8); @@ -564,20 +557,21 @@ public MutableBytes shiftRight(int distance) { if (byteShift > 0) { for (int i = size - 1; i >= 0; i--) { - byte previousByte = (i < byteShift) ? 0 : bytesArray[i - byteShift]; - bytesArray[i] = previousByte; + byte previousByte = (i < byteShift) ? 0 : bytes[i - byteShift]; + bytes[i] = previousByte; } } if (bitShift > 0) { for (int i = size - 1; i >= 0; i--) { - byte currentByte = bytesArray[i]; - byte previousByte = (i == 0) ? 0 : bytesArray[i - 1]; + byte currentByte = bytes[i]; + byte previousByte = (i == 0) ? 0 : bytes[i - 1]; int rightSide = (currentByte & 0XFF) >>> bitShift; int leftSide = previousByte << (8 - bitShift); - bytesArray[i] = (byte) (leftSide | rightSide); + bytes[i] = (byte) (leftSide | rightSide); } } + hashCode = null; return this; } @@ -589,7 +583,7 @@ public MutableBytes shiftRight(int distance) { */ public MutableBytes shiftLeft(int distance) { checkArgument(distance >= 0, "Invalid negative distance"); - if (distance == 0) { + if (distance == 0 || size() == 0) { return this; } distance = Math.min(distance, size * 8); @@ -598,20 +592,21 @@ public MutableBytes shiftLeft(int distance) { if (byteShift > 0) { for (int i = 0; i < size; i++) { - byte nextByte = (i + byteShift < size) ? bytesArray[i + byteShift] : 0; - bytesArray[i] = nextByte; + byte nextByte = (i + byteShift < size) ? bytes[i + byteShift] : 0; + bytes[i] = nextByte; } } if (bitShift > 0) { for (int i = 0; i < size; i++) { - byte currentByte = bytesArray[i]; - byte nextByte = (i == size - 1) ? 0 : bytesArray[i + 1]; + byte currentByte = bytes[i]; + byte nextByte = (i == size - 1) ? 0 : bytes[i + 1]; int leftSide = currentByte << bitShift; int rightSide = (nextByte & 0XFF) >>> (8 - bitShift); - bytesArray[i] = (byte) (leftSide | rightSide); + bytes[i] = (byte) (leftSide | rightSide); } } + hashCode = null; return this; } @@ -630,9 +625,10 @@ public MutableBytes leftPad(int length) { return this; } byte[] newBytesArray = new byte[length]; - System.arraycopy(bytesArray, 0, newBytesArray, length - size, size); - bytesArray = newBytesArray; + System.arraycopy(bytes, 0, newBytesArray, length - size, size); + bytes = newBytesArray; size = length; + hashCode = null; return this; } @@ -651,75 +647,17 @@ public MutableBytes rightPad(int length) { return this; } byte[] newBytesArray = new byte[length]; - System.arraycopy(bytesArray, 0, newBytesArray, 0, size); - bytesArray = newBytesArray; + System.arraycopy(bytes, 0, newBytesArray, 0, size); + bytes = newBytesArray; size = length; + hashCode = null; return this; } - @Override - public Bytes slice(int i, int length) { - checkArgument(length >= 0, "Invalid negative length"); - if (bytesArray.length > 0) { - checkElementIndex(i, bytesArray.length); - } - checkLength(bytesArray.length, i, length); - if (length == size) { - return this; - } - return new ArrayWrappingBytes(this.bytesArray, i, length); - } - - @Override - public MutableBytes mutableCopy() { - return new MutableBytes(bytesArray); - } - - @Override - public byte[] toArrayUnsafe() { - return bytesArray; - } - public byte[] toArray() { return toArrayUnsafe(); } - @Override - public byte get(int i) { - return bytesArray[i]; - } - - @Override - public int hashCode() { - int result = 1; - for (int i = 0; i < size; i++) { - result = 31 * result + bytesArray[i]; - } - return result; - } - - @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 (bytesArray[i] != other.get(i)) { - return false; - } - } - - return true; - } - /** * Parse a hexadecimal string into a {@link MutableBytes} value. * From a6462713d8c6abbf598ccbd2df04423925419e5f Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Thu, 27 Nov 2025 16:36:23 +0000 Subject: [PATCH 3/7] revert changes to any other projects than not bytes This is for easy reviewing by making the PR much shorter. Signed-off-by: Luis Pinto --- .../org/apache/tuweni/v2/crypto/Hash.java | 296 -- .../apache/tuweni/v2/crypto/SECP256K1.java | 1045 ------ .../blake2bf/Blake2bfMessageDigest.java | 233 -- .../tuweni/v2/crypto/mikuli/AtePairing.java | 22 - .../tuweni/v2/crypto/mikuli/BLS12381.java | 122 - .../tuweni/v2/crypto/mikuli/G1Point.java | 88 - .../tuweni/v2/crypto/mikuli/G2Point.java | 85 - .../tuweni/v2/crypto/mikuli/GTPoint.java | 33 - .../apache/tuweni/v2/crypto/mikuli/Group.java | 11 - .../tuweni/v2/crypto/mikuli/KeyPair.java | 46 - .../tuweni/v2/crypto/mikuli/PublicKey.java | 101 - .../tuweni/v2/crypto/mikuli/Scalar.java | 34 - .../tuweni/v2/crypto/mikuli/SecretKey.java | 63 - .../tuweni/v2/crypto/mikuli/Signature.java | 94 - .../crypto/mikuli/SignatureAndPublicKey.java | 62 - .../tuweni/v2/crypto/mikuli/package-info.java | 23 - .../apache/tuweni/v2/crypto/package-info.java | 20 - .../tuweni/v2/crypto/sodium/AES256GCM.java | 1046 ------ .../tuweni/v2/crypto/sodium/Allocated.java | 127 - .../apache/tuweni/v2/crypto/sodium/Auth.java | 228 -- .../apache/tuweni/v2/crypto/sodium/Box.java | 1225 ------- .../tuweni/v2/crypto/sodium/Concatenate.java | 135 - .../DefaultDetachedEncryptionResult.java | 36 - .../sodium/DetachedEncryptionResult.java | 37 - .../tuweni/v2/crypto/sodium/DiffieHelman.java | 463 --- .../tuweni/v2/crypto/sodium/GenericHash.java | 325 -- .../tuweni/v2/crypto/sodium/HMACSHA256.java | 196 - .../tuweni/v2/crypto/sodium/HMACSHA512.java | 194 - .../v2/crypto/sodium/HMACSHA512256.java | 195 - .../v2/crypto/sodium/KeyDerivation.java | 305 -- .../tuweni/v2/crypto/sodium/KeyExchange.java | 697 ---- .../tuweni/v2/crypto/sodium/PasswordHash.java | 1061 ------ .../tuweni/v2/crypto/sodium/SHA256Hash.java | 232 -- .../tuweni/v2/crypto/sodium/SHA512Hash.java | 232 -- .../tuweni/v2/crypto/sodium/SecretBox.java | 1841 ---------- .../crypto/sodium/SecretDecryptionStream.java | 36 - .../crypto/sodium/SecretEncryptionStream.java | 87 - .../tuweni/v2/crypto/sodium/Signature.java | 683 ---- .../tuweni/v2/crypto/sodium/Sodium.java | 3194 ----------------- .../v2/crypto/sodium/SodiumVersion.java | 47 - .../v2/crypto/sodium/XChaCha20Poly1305.java | 923 ----- .../tuweni/v2/crypto/sodium/package-info.java | 30 - .../org/apache/tuweni/v2/crypto/HashTest.java | 166 - .../tuweni/v2/crypto/SECP256K1Test.java | 388 -- .../blake2bf/Blake2bfMessageDigestTest.java | 124 - .../tuweni/v2/crypto/mikuli/GTPointTest.java | 23 - .../v2/crypto/mikuli/SignatureTest.java | 164 - .../v2/crypto/sodium/AllocatedTest.java | 37 - .../tuweni/v2/crypto/sodium/BoxTest.java | 272 -- .../v2/crypto/sodium/ConcatenateTest.java | 33 - .../v2/crypto/sodium/DiffieHelmanTest.java | 97 - .../v2/crypto/sodium/GenericHashTest.java | 39 - .../v2/crypto/sodium/HMACSHA256Test.java | 49 - .../v2/crypto/sodium/HMACSHA512256Test.java | 49 - .../v2/crypto/sodium/HMACSHA512Test.java | 49 - .../v2/crypto/sodium/KeyDerivationTest.java | 34 - .../v2/crypto/sodium/PasswordHashTest.java | 119 - .../v2/crypto/sodium/SHA256HashTest.java | 66 - .../sodium/SecretDecryptionStreamTest.java | 29 - .../v2/crypto/sodium/SignatureTest.java | 54 - .../v2/devp2p/v5/ConnectTwoServersTest.kt | 67 - .../kotlin/org/apache/tuweni/devp2p/Packet.kt | 2 +- .../tuweni/v2/devp2p/DiscoveryService.kt | 922 ----- .../org/apache/tuweni/v2/devp2p/Endpoint.kt | 99 - .../tuweni/v2/devp2p/EnodeUriComponents.kt | 55 - .../tuweni/v2/devp2p/EthereumNodeRecord.kt | 329 -- .../org/apache/tuweni/v2/devp2p/Node.kt | 31 - .../org/apache/tuweni/v2/devp2p/Packet.kt | 477 --- .../org/apache/tuweni/v2/devp2p/PacketType.kt | 115 - .../org/apache/tuweni/v2/devp2p/Peer.kt | 98 - .../apache/tuweni/v2/devp2p/PeerRepository.kt | 210 -- .../tuweni/v2/devp2p/PeerRoutingTable.kt | 96 - .../tuweni/v2/devp2p/v5/DiscoveryV5Service.kt | 316 -- .../apache/tuweni/v2/devp2p/v5/ENRStorage.kt | 50 - .../tuweni/v2/devp2p/v5/HandshakeSession.kt | 201 -- .../org/apache/tuweni/v2/devp2p/v5/Message.kt | 75 - .../apache/tuweni/v2/devp2p/v5/Messages.kt | 305 -- .../tuweni/v2/devp2p/v5/RoutingTable.kt | 62 - .../org/apache/tuweni/v2/devp2p/v5/Session.kt | 333 -- .../tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt | 58 - .../tuweni/v2/devp2p/v5/encrypt/SessionKey.kt | 11 - .../devp2p/v5/encrypt/SessionKeyGenerator.kt | 40 - .../tuweni/v2/devp2p/v5/topic/Ticket.kt | 70 - .../apache/tuweni/v2/devp2p/v5/topic/Topic.kt | 12 - .../tuweni/v2/devp2p/v5/topic/TopicTable.kt | 95 - .../tuweni/devp2p/v5/HandshakeSessionTest.kt | 11 +- .../tuweni/v2/devp2p/DiscoveryServiceTest.kt | 403 --- .../tuweni/v2/devp2p/ENRResponsePacketTest.kt | 44 - .../v2/devp2p/EthereumNodeRecordTest.kt | 62 - .../tuweni/v2/devp2p/FindNodePacketTest.kt | 48 - .../tuweni/v2/devp2p/NeighborsPacketTest.kt | 66 - .../apache/tuweni/v2/devp2p/PingPacketTest.kt | 107 - .../apache/tuweni/v2/devp2p/PongPacketTest.kt | 73 - .../v5/DefaultDiscoveryV5ServiceTest.kt | 67 - .../v2/devp2p/v5/HandshakeSessionTest.kt | 88 - .../v2/devp2p/v5/PeerRoutingTableTest.kt | 23 - .../v2/devp2p/v5/encrypt/AES128GCMTest.kt | 42 - .../v5/encrypt/SessionKeyGeneratorTest.kt | 28 - .../devp2p/v5/packet/FindNodeMessageTest.kt | 27 - .../tuweni/v2/devp2p/v5/packet/MessageTest.kt | 66 - .../v2/devp2p/v5/packet/NodesMessageTest.kt | 39 - .../v2/devp2p/v5/packet/PingMessageTest.kt | 24 - .../v2/devp2p/v5/packet/PongMessageTest.kt | 26 - .../v2/devp2p/v5/packet/RandomMessageTest.kt | 40 - .../v5/packet/RegConfirmationMessageTest.kt | 25 - .../devp2p/v5/packet/RegTopicMessageTest.kt | 35 - .../v2/devp2p/v5/packet/TicketMessageTest.kt | 26 - .../devp2p/v5/packet/TopicQueryMessageTest.kt | 25 - .../devp2p/v5/packet/WhoAreYouMessageTest.kt | 23 - .../tuweni/v2/devp2p/v5/topic/TicketTest.kt | 20 - .../java/org/apache/tuweni/v2/io/Base32.java | 59 - .../java/org/apache/tuweni/v2/io/Base58.java | 56 - .../org/apache/tuweni/v2/io/Base58Codec.java | 95 - .../java/org/apache/tuweni/v2/io/Base64.java | 56 - .../apache/tuweni/v2/io/Base64URLSafe.java | 56 - .../org/apache/tuweni/v2/io/package-info.java | 23 - .../org/apache/tuweni/v2/io/Base32Test.java | 37 - .../org/apache/tuweni/v2/io/Base58Test.java | 77 - .../org/apache/tuweni/v2/io/Base64Test.java | 43 - .../tuweni/v2/io/Base64URLSafeTest.java | 43 - .../apache/tuweni/v2/net/package-info.java | 20 - .../tls/ClientFingerprintTrustManager.java | 123 - .../tls/FileBackedFingerprintRepository.java | 167 - .../v2/net/tls/FingerprintRepository.java | 35 - .../tls/ServerFingerprintTrustManager.java | 121 - .../org/apache/tuweni/v2/net/tls/TLS.java | 219 -- .../v2/net/tls/TLSEnvironmentException.java | 14 - .../tuweni/v2/net/tls/package-info.java | 17 - .../FileBackedFingerprintRepositoryTest.java | 98 - .../tuweni/v2/rlp/AccumulatingRLPWriter.java | 87 - .../tuweni/v2/rlp/ByteBufferRLPWriter.java | 70 - .../apache/tuweni/v2/rlp/BytesRLPReader.java | 313 -- .../apache/tuweni/v2/rlp/BytesRLPWriter.java | 22 - .../tuweni/v2/rlp/DelegatingRLPWriter.java | 68 - .../java/org/apache/tuweni/v2/rlp/RLP.java | 528 --- .../org/apache/tuweni/v2/rlp/RLPReader.java | 424 --- .../org/apache/tuweni/v2/rlp/RLPWriter.java | 128 - .../apache/tuweni/v2/rlp/package-info.java | 23 - .../tuweni/v2/rlp/ByteBufferWriterTest.java | 163 - .../tuweni/v2/rlp/BytesRLPReaderTest.java | 275 -- .../tuweni/v2/rlp/BytesRLPWriterTest.java | 187 - .../tuweni/v2/ssz/ByteBufferSSZWriter.java | 26 - .../apache/tuweni/v2/ssz/BytesSSZReader.java | 479 --- .../apache/tuweni/v2/ssz/BytesSSZWriter.java | 25 - .../java/org/apache/tuweni/v2/ssz/SSZ.java | 2037 ----------- .../tuweni/v2/ssz/SSZFixedSizeTypeList.java | 41 - .../tuweni/v2/ssz/SSZFixedSizeVector.java | 42 - .../org/apache/tuweni/v2/ssz/SSZReadable.java | 9 - .../org/apache/tuweni/v2/ssz/SSZReader.java | 799 ----- .../v2/ssz/SSZVariableSizeTypeList.java | 37 - .../org/apache/tuweni/v2/ssz/SSZWritable.java | 10 - .../org/apache/tuweni/v2/ssz/SSZWriter.java | 739 ---- .../apache/tuweni/v2/ssz/package-info.java | 23 - .../tuweni/v2/ssz/ByteBufferWriterTest.java | 121 - .../tuweni/v2/ssz/BytesSSZReaderTest.java | 252 -- .../tuweni/v2/ssz/BytesSSZWriterTest.java | 539 --- .../tuweni/v2/ssz/HashTreeRootTest.java | 105 - .../v2/ssz/TransactionNetworkPayload.java | 302 -- units/.factorypath | 25 - .../apache/tuweni/units/bigints/Utils.java | 47 - .../tuweni/v2/units/bigints/UInt256.java | 1051 ------ .../tuweni/v2/units/bigints/UInt32.java | 483 --- .../tuweni/v2/units/bigints/UInt384.java | 877 ----- .../tuweni/v2/units/bigints/UInt64.java | 541 --- .../apache/tuweni/v2/units/bigints/Utils.java | 47 - .../tuweni/v2/units/bigints/package-info.java | 20 - .../apache/tuweni/v2/units/package-info.java | 20 - .../tuweni/v2/units/bigints/UInt256Test.java | 1245 ------- .../tuweni/v2/units/bigints/UInt32Test.java | 832 ----- .../tuweni/v2/units/bigints/UInt384Test.java | 1144 ------ .../tuweni/v2/units/bigints/UInt64Test.java | 820 ----- 171 files changed, 3 insertions(+), 39694 deletions(-) delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/Hash.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/SECP256K1.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigest.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/AtePairing.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/BLS12381.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G1Point.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G2Point.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/GTPoint.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Group.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/KeyPair.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/PublicKey.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Scalar.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SecretKey.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Signature.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SignatureAndPublicKey.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/package-info.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/package-info.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/AES256GCM.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Allocated.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Auth.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Box.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Concatenate.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DefaultDetachedEncryptionResult.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DetachedEncryptionResult.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelman.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/GenericHash.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivation.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyExchange.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/PasswordHash.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA256Hash.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA512Hash.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretBox.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStream.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretEncryptionStream.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Signature.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Sodium.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SodiumVersion.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/XChaCha20Poly1305.java delete mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/package-info.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/HashTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/SECP256K1Test.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigestTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/GTPointTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/SignatureTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/AllocatedTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/BoxTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/ConcatenateTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelmanTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/GenericHashTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256Test.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256Test.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512Test.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivationTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/PasswordHashTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SHA256HashTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStreamTest.java delete mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SignatureTest.java delete mode 100644 devp2p/src/integrationTest/kotlin/org/apache/tuweni/v2/devp2p/v5/ConnectTwoServersTest.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryService.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Endpoint.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EnodeUriComponents.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecord.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Node.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Packet.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PacketType.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Peer.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRepository.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRoutingTable.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/DiscoveryV5Service.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/ENRStorage.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSession.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Message.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Messages.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/RoutingTable.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Session.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKey.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGenerator.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Ticket.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Topic.kt delete mode 100644 devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TopicTable.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryServiceTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/ENRResponsePacketTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecordTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/FindNodePacketTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/NeighborsPacketTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PingPacketTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PongPacketTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/DefaultDiscoveryV5ServiceTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSessionTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/PeerRoutingTableTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCMTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/FindNodeMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/MessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/NodesMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PingMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PongMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RandomMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegConfirmationMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegTopicMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TicketMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TopicQueryMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/WhoAreYouMessageTest.kt delete mode 100644 devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TicketTest.kt delete mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base32.java delete mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base58.java delete mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base58Codec.java delete mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base64.java delete mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base64URLSafe.java delete mode 100644 io/src/main/java/org/apache/tuweni/v2/io/package-info.java delete mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base32Test.java delete mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base58Test.java delete mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base64Test.java delete mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base64URLSafeTest.java delete mode 100644 net/src/main/java/org/apache/tuweni/v2/net/package-info.java delete mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/ClientFingerprintTrustManager.java delete mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepository.java delete mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/FingerprintRepository.java delete mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/ServerFingerprintTrustManager.java delete mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/TLS.java delete mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/TLSEnvironmentException.java delete mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/package-info.java delete mode 100644 net/src/test/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepositoryTest.java delete mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/AccumulatingRLPWriter.java delete mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/ByteBufferRLPWriter.java delete mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPReader.java delete mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPWriter.java delete mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/DelegatingRLPWriter.java delete mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/RLP.java delete mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPReader.java delete mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPWriter.java delete mode 100644 rlp/src/main/java/org/apache/tuweni/v2/rlp/package-info.java delete mode 100644 rlp/src/test/java/org/apache/tuweni/v2/rlp/ByteBufferWriterTest.java delete mode 100644 rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPReaderTest.java delete mode 100644 rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPWriterTest.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/ByteBufferSSZWriter.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZReader.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZWriter.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZ.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeTypeList.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeVector.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReadable.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReader.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZVariableSizeTypeList.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWritable.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWriter.java delete mode 100644 ssz/src/main/java/org/apache/tuweni/v2/ssz/package-info.java delete mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/ByteBufferWriterTest.java delete mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZReaderTest.java delete mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZWriterTest.java delete mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/HashTreeRootTest.java delete mode 100644 ssz/src/test/java/org/apache/tuweni/v2/ssz/TransactionNetworkPayload.java delete mode 100644 units/.factorypath delete mode 100644 units/src/main/java/org/apache/tuweni/units/bigints/Utils.java delete mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt256.java delete mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt32.java delete mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt384.java delete mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt64.java delete mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/Utils.java delete mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/package-info.java delete mode 100644 units/src/main/java/org/apache/tuweni/v2/units/package-info.java delete mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt256Test.java delete mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt32Test.java delete mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt384Test.java delete mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt64Test.java 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 deleted file mode 100644 index 7bfa5ceda..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/Hash.java +++ /dev/null @@ -1,296 +0,0 @@ -// 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 deleted file mode 100644 index c19922395..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/SECP256K1.java +++ /dev/null @@ -1,1045 +0,0 @@ -/* - * 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 deleted file mode 100644 index 2295e914f..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigest.java +++ /dev/null @@ -1,233 +0,0 @@ -// 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 deleted file mode 100644 index 3e6dc4222..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/AtePairing.java +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index 8dd7f0349..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/BLS12381.java +++ /dev/null @@ -1,122 +0,0 @@ -// 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 deleted file mode 100644 index 6f1cb3368..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G1Point.java +++ /dev/null @@ -1,88 +0,0 @@ -// 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 deleted file mode 100644 index ccb0c84cc..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G2Point.java +++ /dev/null @@ -1,85 +0,0 @@ -// 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 deleted file mode 100644 index 1108f8a9b..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/GTPoint.java +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index 45dda1907..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Group.java +++ /dev/null @@ -1,11 +0,0 @@ -// 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 deleted file mode 100644 index 20eed1cc5..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/KeyPair.java +++ /dev/null @@ -1,46 +0,0 @@ -// 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 deleted file mode 100644 index 56872efcb..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/PublicKey.java +++ /dev/null @@ -1,101 +0,0 @@ -// 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 deleted file mode 100644 index 3f0c7edfc..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Scalar.java +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index e5d290807..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SecretKey.java +++ /dev/null @@ -1,63 +0,0 @@ -// 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 deleted file mode 100644 index 1fbfd951c..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Signature.java +++ /dev/null @@ -1,94 +0,0 @@ -// 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 deleted file mode 100644 index 20adab496..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SignatureAndPublicKey.java +++ /dev/null @@ -1,62 +0,0 @@ -// 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 deleted file mode 100644 index 397b33ca7..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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 deleted file mode 100644 index 1a01bdd6a..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * 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 deleted file mode 100644 index 2bdb9833f..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/AES256GCM.java +++ /dev/null @@ -1,1046 +0,0 @@ -// 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 deleted file mode 100644 index 2013b4f11..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Allocated.java +++ /dev/null @@ -1,127 +0,0 @@ -// 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 deleted file mode 100644 index b2bd3aca3..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Auth.java +++ /dev/null @@ -1,228 +0,0 @@ -// 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 deleted file mode 100644 index 3ff9b82e2..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Box.java +++ /dev/null @@ -1,1225 +0,0 @@ -// 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 deleted file mode 100644 index 329293fea..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Concatenate.java +++ /dev/null @@ -1,135 +0,0 @@ -// 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 deleted file mode 100644 index e3d1d4854..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DefaultDetachedEncryptionResult.java +++ /dev/null @@ -1,36 +0,0 @@ -// 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 deleted file mode 100644 index f261fbc6e..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DetachedEncryptionResult.java +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index 78ac936ec..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelman.java +++ /dev/null @@ -1,463 +0,0 @@ -// 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 deleted file mode 100644 index 8fdd86fd2..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/GenericHash.java +++ /dev/null @@ -1,325 +0,0 @@ -// 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 deleted file mode 100644 index 9d33a5ac2..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256.java +++ /dev/null @@ -1,196 +0,0 @@ -// 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 deleted file mode 100644 index 3c0e5304c..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512.java +++ /dev/null @@ -1,194 +0,0 @@ -// 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 deleted file mode 100644 index 497ca2a66..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256.java +++ /dev/null @@ -1,195 +0,0 @@ -// 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 deleted file mode 100644 index e93fafd97..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivation.java +++ /dev/null @@ -1,305 +0,0 @@ -// 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 deleted file mode 100644 index 7331f32e8..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyExchange.java +++ /dev/null @@ -1,697 +0,0 @@ -// 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 deleted file mode 100644 index 81abf895f..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/PasswordHash.java +++ /dev/null @@ -1,1061 +0,0 @@ -// 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 deleted file mode 100644 index 5aaf3c648..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA256Hash.java +++ /dev/null @@ -1,232 +0,0 @@ -// 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 deleted file mode 100644 index 0aebc69f8..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA512Hash.java +++ /dev/null @@ -1,232 +0,0 @@ -// 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 deleted file mode 100644 index decd5d3c5..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretBox.java +++ /dev/null @@ -1,1841 +0,0 @@ -// 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 deleted file mode 100644 index a46584f31..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStream.java +++ /dev/null @@ -1,36 +0,0 @@ -// 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 deleted file mode 100644 index 80d86864a..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretEncryptionStream.java +++ /dev/null @@ -1,87 +0,0 @@ -// 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 deleted file mode 100644 index 2dde5f451..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Signature.java +++ /dev/null @@ -1,683 +0,0 @@ -// 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 deleted file mode 100644 index 52eeb20f3..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Sodium.java +++ /dev/null @@ -1,3194 +0,0 @@ -// 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 deleted file mode 100644 index ff1a8cc84..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SodiumVersion.java +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 60ca76f51..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/XChaCha20Poly1305.java +++ /dev/null @@ -1,923 +0,0 @@ -// 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 deleted file mode 100644 index 871886603..000000000 --- a/crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/package-info.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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 deleted file mode 100644 index 76f5fd73f..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/HashTest.java +++ /dev/null @@ -1,166 +0,0 @@ -// 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 deleted file mode 100644 index 63cf879ba..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/SECP256K1Test.java +++ /dev/null @@ -1,388 +0,0 @@ -// 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 deleted file mode 100644 index f746b3da7..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigestTest.java +++ /dev/null @@ -1,124 +0,0 @@ -// 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 deleted file mode 100644 index 61689fc73..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/GTPointTest.java +++ /dev/null @@ -1,23 +0,0 @@ -// 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 deleted file mode 100644 index 67cd2ee1c..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/SignatureTest.java +++ /dev/null @@ -1,164 +0,0 @@ -// 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 deleted file mode 100644 index 8ca740765..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/AllocatedTest.java +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index fdd478318..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/BoxTest.java +++ /dev/null @@ -1,272 +0,0 @@ -// 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 deleted file mode 100644 index b31817eb5..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/ConcatenateTest.java +++ /dev/null @@ -1,33 +0,0 @@ -// 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 deleted file mode 100644 index c7adea397..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelmanTest.java +++ /dev/null @@ -1,97 +0,0 @@ -// 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 deleted file mode 100644 index 15ec4d3f9..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/GenericHashTest.java +++ /dev/null @@ -1,39 +0,0 @@ -// 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 deleted file mode 100644 index 0c4d5ae87..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256Test.java +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index 275ee5cb8..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256Test.java +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index 272a64600..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512Test.java +++ /dev/null @@ -1,49 +0,0 @@ -// 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 deleted file mode 100644 index 840b3ffc2..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivationTest.java +++ /dev/null @@ -1,34 +0,0 @@ -// 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 deleted file mode 100644 index a944a09ee..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/PasswordHashTest.java +++ /dev/null @@ -1,119 +0,0 @@ -// 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 deleted file mode 100644 index 02a5efa83..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SHA256HashTest.java +++ /dev/null @@ -1,66 +0,0 @@ -// 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 deleted file mode 100644 index 128f3e7fa..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStreamTest.java +++ /dev/null @@ -1,29 +0,0 @@ -// 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 deleted file mode 100644 index ca4febd89..000000000 --- a/crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SignatureTest.java +++ /dev/null @@ -1,54 +0,0 @@ -// 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/devp2p/src/integrationTest/kotlin/org/apache/tuweni/v2/devp2p/v5/ConnectTwoServersTest.kt b/devp2p/src/integrationTest/kotlin/org/apache/tuweni/v2/devp2p/v5/ConnectTwoServersTest.kt deleted file mode 100644 index 10e8340b6..000000000 --- a/devp2p/src/integrationTest/kotlin/org/apache/tuweni/v2/devp2p/v5/ConnectTwoServersTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import io.vertx.core.Vertx -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.apache.tuweni.concurrent.coroutines.await -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.junit.VertxExtension -import org.apache.tuweni.junit.VertxInstance -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.Hash -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import java.util.concurrent.ConcurrentHashMap - -internal class SimpleTestENRStorage : ENRStorage { - - val storage: MutableMap = ConcurrentHashMap() - - override fun find(nodeId: Bytes): EthereumNodeRecord? = storage[nodeId] - - override fun put(nodeId: Bytes, enr: EthereumNodeRecord) { - storage[nodeId] = enr - } -} - -@ExtendWith(BouncyCastleExtension::class, VertxExtension::class) -class ConnectTwoServersTest { - - @Test - fun testConnectTwoServers(@VertxInstance vertx: Vertx) = runBlocking { - val storage = SimpleTestENRStorage() - val service = DiscoveryService.open( - vertx, - SECP256K1.KeyPair.random(), - localPort = 40002, - bootstrapENRList = emptyList(), - enrStorage = storage, - ) - service.start().await() - - val otherStorage = SimpleTestENRStorage() - val otherService = DiscoveryService.open( - vertx, - SECP256K1.KeyPair.random(), - localPort = 40003, - bootstrapENRList = emptyList(), - enrStorage = otherStorage, - ) - otherService.start().await() - otherService.addPeer(service.enr()).await() - delay(500) - assertEquals(1, storage.storage.size) - assertEquals(1, otherStorage.storage.size) - assertNotNull(otherStorage.find(Hash.sha2_256(service.enr().toRLP()))) - assertNotNull(storage.find(Hash.sha2_256(otherService.enr().toRLP()))) - - service.terminate() - otherService.terminate() - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/Packet.kt b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/Packet.kt index 8bd1fd130..40266dec5 100644 --- a/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/Packet.kt +++ b/devp2p/src/main/kotlin/org/apache/tuweni/devp2p/Packet.kt @@ -398,7 +398,7 @@ internal class ENRRequestPacket private constructor( fun create(keyPair: SECP256K1.KeyPair, now: Long): Packet { val expiration = expirationFor(now) val sigHash = createSignature( - PacketType.ENRREQUEST, + PacketType.ENRRESPONSE, keyPair, ) { writer -> encodeTo(writer, expiration) diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryService.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryService.kt deleted file mode 100644 index bdf1960f5..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryService.kt +++ /dev/null @@ -1,922 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import com.google.common.cache.Cache -import com.google.common.cache.CacheBuilder -import io.vertx.core.Vertx -import io.vertx.core.buffer.Buffer -import io.vertx.core.datagram.DatagramPacket -import io.vertx.core.net.SocketAddress -import io.vertx.kotlin.coroutines.coAwait -import io.vertx.kotlin.coroutines.dispatcher -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout -import kotlinx.coroutines.withTimeoutOrNull -import org.apache.tuweni.concurrent.AsyncCompletion -import org.apache.tuweni.concurrent.AsyncResult -import org.apache.tuweni.concurrent.coroutines.asyncCompletion -import org.apache.tuweni.concurrent.coroutines.asyncResult -import org.apache.tuweni.concurrent.coroutines.await -import org.apache.tuweni.devp2p.getValue -import org.apache.tuweni.devp2p.setValue -import org.apache.tuweni.kademlia.orderedInsert -import org.apache.tuweni.kademlia.xorDistCmp -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.slf4j.LoggerFactory -import java.net.InetAddress -import java.net.URI -import java.nio.ByteBuffer -import java.time.Instant -import java.util.Collections -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicLong -import kotlin.coroutines.CoroutineContext - -internal const val PACKET_EXPIRATION_PERIOD_MS = (20 * 1000).toLong() // 20 seconds -internal const val PACKET_EXPIRATION_CHECK_GRACE_MS = (5 * 1000).toLong() // 5 seconds -internal const val PEER_VERIFICATION_TIMEOUT_MS = (22 * 1000).toLong() // 22 seconds (packet expiration + a little) -internal const val ENR_REQUEST_TIMEOUT_MS = (22 * 1000).toLong() // 22 seconds (packet expiration + a little) -internal const val PEER_VERIFICATION_RETRY_DELAY_MS = (5 * 60 * 1000).toLong() // 5 minutes -internal const val ENR_REQUEST_RETRY_DELAY_MS = (5 * 60 * 1000).toLong() // 5 minutes -internal const val BOOTSTRAP_PEER_VERIFICATION_TIMEOUT_MS = (2 * 60 * 1000).toLong() // 2 minutes -internal const val REFRESH_INTERVAL_MS = (60 * 1000).toLong() // 1 minute -internal const val PING_RETRIES: Int = 20 -internal const val RESEND_DELAY_MS = 1000.toLong() // 1 second -internal const val RESEND_DELAY_INCREASE_MS = 500.toLong() // 500 milliseconds -internal const val RESEND_MAX_DELAY_MS = (30 * 1000).toLong() // 30 seconds -internal const val ENDPOINT_PROOF_LONGEVITY_MS = (12 * 60 * 60 * 1000).toLong() // 12 hours -internal const val FIND_NODES_CACHE_EXPIRY = (3 * 60 * 1000).toLong() // 3 minutes -internal const val FIND_NODES_QUERY_GAP_MS = (30 * 1000).toLong() // 30 seconds -internal const val LOOKUP_RESPONSE_TIMEOUT_MS = 500.toLong() // 500 milliseconds - -/** - * An Ethereum ÐΞVp2p discovery service. - * - * This service supports devp2p discovery v4, alongside support for EIP-868. - * http://eips.ethereum.org/EIPS/eip-868 - * - */ -interface DiscoveryService { - - companion object { - internal val CURRENT_TIME_SUPPLIER: () -> Long = { System.currentTimeMillis() } - - /** - * Start the discovery service. - * - * @param vertx Vert.x instance - * @param keyPair the local node's keypair - * @param port the port to listen on (defaults to `0`, which will cause a random free port to be chosen) - * @param host the host name or IP address of the interface to bind to (defaults to `null`, which will cause the - * service to listen on all interfaces - * @param seq the sequence number of the Ethereum Node Record - * @param enrData the additional key/value pair entries to broadcast as an Ethereum Node Record (ENR). - * @param bootstrapURIs the URIs for bootstrap nodes - * @param peerRepository a [PeerRepository] for obtaining [Peer] instances - * @param advertiseAddress the IP address to advertise to peers, or `null` if the address of the first bound - * interface should be used. - * @param advertiseUdpPort the UDP port to advertise to peers, or `null` if the bound port should to be used. - * @param advertiseTcpPort the TCP port to advertise to peers, or `null` if it should be the same as the UDP port. - * @param routingTable a [PeerRoutingTable] which handles the ÐΞVp2p routing table - * @param packetFilter a filter for incoming packets - \ * @param bufferAllocator a [ByteBuffer] allocator, which must return buffers of size 1280 bytes or larger - * @param timeSupplier a function supplying the current time, in milliseconds since the epoch - */ - @JvmOverloads - fun open( - vertx: Vertx, - keyPair: SECP256K1.KeyPair, - port: Int = 0, - host: String? = null, - seq: Long = Instant.now().toEpochMilli(), - enrData: Map = emptyMap(), - bootstrapURIs: List = emptyList(), - peerRepository: PeerRepository = EphemeralPeerRepository(), - advertiseAddress: String? = null, - advertiseUdpPort: Int? = null, - advertiseTcpPort: Int? = null, - routingTable: PeerRoutingTable = DevP2PPeerRoutingTable(keyPair.publicKey()), - packetFilter: ((SECP256K1.PublicKey, SocketAddress) -> Boolean)? = null, - timeSupplier: () -> Long = CURRENT_TIME_SUPPLIER, - ): DiscoveryService { - val bindAddress = - if (host == null) { - SocketAddress.inetSocketAddress(port, "127.0.0.1") - } else { - SocketAddress.inetSocketAddress( - port, - host, - ) - } - return open( - vertx, - keyPair, - bindAddress, - seq, - enrData, - bootstrapURIs, - peerRepository, - advertiseAddress, - advertiseUdpPort, - advertiseTcpPort, - routingTable, - packetFilter, - timeSupplier, - ) - } - - /** - * Start the discovery service. - * - * @param vertx Vert.x instance - * @param keyPair the local node's keypair - * @param bindAddress the address to listen on - * @param seq the sequence number of the Ethereum Node Record - * @param enrData the additional key/value pair entries to broadcast as an Ethereum Node Record (ENR). - * @param bootstrapURIs the URIs for bootstrap nodes - * @param peerRepository a [PeerRepository] for obtaining [Peer] instances - * @param advertiseAddress the IP address to advertise for incoming packets - * @param advertiseUdpPort the UDP port to advertise to peers, or `null` if the bound port should to be used. - * @param advertiseTcpPort the TCP port to advertise to peers, or `null` if it should be the same as the UDP port. - * @param routingTable a [PeerRoutingTable] which handles the ÐΞVp2p routing table - * @param packetFilter a filter for incoming packets - * @param timeSupplier a function supplying the current time, in milliseconds since the epoch - */ - @JvmOverloads - fun open( - vertx: Vertx, - keyPair: SECP256K1.KeyPair, - bindAddress: SocketAddress, - seq: Long = Instant.now().toEpochMilli(), - enrData: Map = emptyMap(), - bootstrapURIs: List = emptyList(), - peerRepository: PeerRepository = EphemeralPeerRepository(), - advertiseAddress: String? = null, - advertiseUdpPort: Int? = null, - advertiseTcpPort: Int? = null, - routingTable: PeerRoutingTable = DevP2PPeerRoutingTable(keyPair.publicKey()), - packetFilter: ((SECP256K1.PublicKey, SocketAddress) -> Boolean)? = null, - timeSupplier: () -> Long = CURRENT_TIME_SUPPLIER, - ): DiscoveryService { - return CoroutineDiscoveryService( - vertx, - keyPair, seq, enrData, bindAddress, bootstrapURIs, advertiseAddress, advertiseUdpPort, advertiseTcpPort, - peerRepository, routingTable, packetFilter, timeSupplier, - ) - } - } - - /** - * `true` if the service has been shutdown - */ - val isShutdown: Boolean - - /** - * the UDP port that the service is listening on - */ - val localPort: Int - - /** - * the node id for this node (i.e. it's public key) - */ - val nodeId: SECP256K1.PublicKey - - /** - * Suspend until the bootstrap peers have been reached, or failed. - * - */ - suspend fun awaitBootstrap() - - /** - * Suspend until the bootstrap peers have been reached, or failed. - * - */ - fun awaitBootstrapAsync(): AsyncCompletion - - /** - * Attempt to find a specific peer, or peers close to it. - * - * @param target the node-id to search for - * @return a list of 16 peers, ordered by their distance to the target node-id. - */ - suspend fun lookup(target: SECP256K1.PublicKey): List - - /** - * Attempt to find a specific peer, or peers close to it asynchronously. - * - * @param target the node-id to search for - * @return a future of a list of 16 peers, ordered by their distance to the target node-id. - */ - fun lookupAsync(target: SECP256K1.PublicKey): AsyncResult> - - /** - * Shuts down this service. - */ - suspend fun shutdown() - - /** - * Suspend until this service has terminated. - */ - fun shutdownAsync(): AsyncCompletion - - /** - * Counter of invalid packets - */ - val invalidPackets: Long - - /** - * Counter of packets sent to self - */ - val selfPackets: Long - - /** - * Counter of expired packets - */ - val expiredPackets: Long - - /** - * Counter of filtered packets - */ - val filteredPackets: Long - - /** - * Counter of unvalidated peer packets - */ - val unvalidatedPeerPackets: Long - - /** - * Counter of unexpected pongs - */ - val unexpectedPongs: Long - - /** - * Counter of unexpected NEIGHBORS messages - */ - val unexpectedNeighbors: Long - - /** - * Counter of unexpected ENRResponse messages - */ - val unexpectedENRResponses: Long -} - -internal class CoroutineDiscoveryService( - vertx: Vertx, - private val keyPair: SECP256K1.KeyPair, - private val seq: Long = Instant.now().toEpochMilli(), - private val enrData: Map, - private val bindAddress: SocketAddress, - private val bootstrapURIs: List = emptyList(), - private val advertiseAddress: String? = null, - private val advertiseUdpPort: Int? = null, - private val advertiseTcpPort: Int? = null, - private val peerRepository: PeerRepository = EphemeralPeerRepository(), - private val routingTable: PeerRoutingTable = DevP2PPeerRoutingTable(keyPair.publicKey()), - private val packetFilter: ((SECP256K1.PublicKey, SocketAddress) -> Boolean)? = null, - private val timeSupplier: () -> Long = DiscoveryService.CURRENT_TIME_SUPPLIER, - override val coroutineContext: CoroutineContext = vertx.dispatcher() + CoroutineExceptionHandler { _, _ -> }, -) : DiscoveryService, CoroutineScope { - - companion object { - internal val logger = LoggerFactory.getLogger(DiscoveryService::class.java) - } - - private val serviceDescriptor = "ÐΞVp2p discovery " + System.identityHashCode(this) - private var selfEndpoint: Endpoint? = null - private var enr: Bytes? = null - - private val shutdown = AtomicBoolean(false) - private val bootstrapped = AsyncCompletion.incomplete() - private var refreshLoop: Job? = null - private val server = vertx.createDatagramSocket() - - override val isShutdown: Boolean - get() = shutdown.get() - - override val localPort: Int - get() = server.localAddress().port() - - override val nodeId: SECP256K1.PublicKey - get() = keyPair.publicKey() - - private val verifyingEndpoints: Cache = - CacheBuilder.newBuilder().expireAfterAccess(PEER_VERIFICATION_RETRY_DELAY_MS, TimeUnit.MILLISECONDS).build() - private val requestingENRs: Cache = - CacheBuilder.newBuilder().expireAfterAccess(ENR_REQUEST_RETRY_DELAY_MS, TimeUnit.MILLISECONDS).build() - private val awaitingPongs = ConcurrentHashMap() - private val awaitingENRs = ConcurrentHashMap() - private val findNodeStates: Cache = - CacheBuilder.newBuilder().expireAfterAccess(FIND_NODES_CACHE_EXPIRY, TimeUnit.MILLISECONDS) - .removalListener { it.value?.close() } - .build() - - override var invalidPackets: Long by AtomicLong(0) - override var selfPackets: Long by AtomicLong(0) - override var expiredPackets: Long by AtomicLong(0) - override var filteredPackets: Long by AtomicLong(0) - override var unvalidatedPeerPackets: Long by AtomicLong(0) - override var unexpectedPongs: Long by AtomicLong(0) - override var unexpectedENRResponses: Long by AtomicLong(0) - override var unexpectedNeighbors: Long by AtomicLong(0) - - init { - start() - } - - private fun start() = launch { - server.handler { receiveDatagram(it) }.listen(bindAddress.port(), bindAddress.host()).coAwait() - val endpoint = Endpoint( - advertiseAddress ?: (server.localAddress()).host(), - advertiseUdpPort ?: server.localAddress().port(), - advertiseTcpPort, - ) - enr = EthereumNodeRecord.toRLP( - keyPair, - seq, - enrData, - null, - InetAddress.getByName(endpoint.address), - endpoint.tcpPort, - endpoint.udpPort, - ) - selfEndpoint = endpoint - refreshLoop = launch { - while (true) { - delay(REFRESH_INTERVAL_MS) - refresh() - } - } - launch { - AsyncCompletion.allOf( - bootstrapURIs.map { uri -> - asyncCompletion { bootstrapFrom(uri) } - }, - ).thenRun { - bootstrapped.complete() - }.await() - } - - logger.info("{}: started, listening on {}", serviceDescriptor, server.localAddress()) - } - - private suspend fun bootstrapFrom(uri: URI): Boolean { - logger.info("Starting verification of bootnode $uri") - val peer = peerRepository.get(uri) - logger.info("Starting verification of bootnode $uri") - try { - val result = withTimeout(BOOTSTRAP_PEER_VERIFICATION_TIMEOUT_MS) { - endpointVerification(peer.endpoint, peer).verifyWithRetries() - } ?: return false - if (result.peer != peer) { - logger.warn( - "{}: ignoring bootstrap peer {} - responding node used a different node-id", - serviceDescriptor, - uri, - ) - return false - } - logger.info("{}: verified bootstrap peer {}", serviceDescriptor, uri) - addToRoutingTable(peer) - findNodes(peer, nodeId).thenRun { - logger.info("{}: completed bootstrapping from {}", serviceDescriptor, uri) - } - return true - } catch (_: TimeoutCancellationException) { - logger.warn("{}: timeout verifying bootstrap node {}", serviceDescriptor, uri) - return false - } - } - - private fun receiveDatagram(packet: DatagramPacket) { - // do quick sanity checks and discard bad packets before launching a co-routine - if (packet.data().length() < Packet.MIN_SIZE) { - logger.debug("{}: ignoring under-sized packet with source {}", serviceDescriptor, packet.sender()) - ++invalidPackets - return - } - if (packet.data().length() > Packet.MAX_SIZE) { - logger.debug("{}: ignoring over-sized packet with source {}", serviceDescriptor, packet.sender()) - ++invalidPackets - return - } - - val arrivalTime = timeSupplier() - launch { - try { - receivePacket(packet.data(), packet.sender(), arrivalTime) - } catch (e: Throwable) { - logger.error("$serviceDescriptor: unexpected error during packet handling", e) - } - } - } - - override suspend fun awaitBootstrap() = bootstrapped.await() - - override fun awaitBootstrapAsync(): AsyncCompletion = bootstrapped - - override suspend fun shutdown() { - if (shutdown.compareAndSet(false, true)) { - logger.info("{}: shutdown", serviceDescriptor) - server.close().coAwait() - for (pending in awaitingPongs.values) { - pending.complete(null) - } - awaitingPongs.clear() - verifyingEndpoints.invalidateAll() - verifyingEndpoints.cleanUp() - findNodeStates.invalidateAll() - findNodeStates.cleanUp() - refreshLoop?.cancel() - } - } - - override fun shutdownAsync(): AsyncCompletion = asyncCompletion { shutdown() } - - override suspend fun lookup(target: SECP256K1.PublicKey): List { - val targetId = target.bytesArray() - val results = neighbors(target).toMutableList() - logger.debug("Initial neighbors query {}", results) - - // maybe add ourselves to the set - val selfPeer = peerRepository.get(selfEndpoint!!.address, selfEndpoint!!.udpPort, nodeId) - results.orderedInsert(selfPeer) { a, _ -> targetId.xorDistCmp(a.nodeId.bytesArray(), nodeId.bytesArray()) } - results.removeAt(results.lastIndex) - - withTimeout(LOOKUP_RESPONSE_TIMEOUT_MS) { - results.map { nodeToQuery -> - async { - val nodes = findNodes(nodeToQuery, target).await() - for (node in ArrayList(nodes)) { - val peer = peerRepository.get(selfEndpoint!!.address, selfEndpoint!!.udpPort, node.nodeId) - if (!results.contains(peer)) { - results.orderedInsert(peer) { a, _ -> - targetId.xorDistCmp( - a.nodeId.bytesArray(), - node.nodeId.bytesArray(), - ) - } - results.removeAt(results.lastIndex) - } - } - } - } - }.awaitAll() - return results - } - - override fun lookupAsync(target: SECP256K1.PublicKey) = asyncResult { lookup(target) } - - private suspend fun refresh() { - logger.debug("{}: table refresh triggered", serviceDescriptor) - // TODO: instead of a random target, choose a target to optimally fill the peer table - lookup(SECP256K1.KeyPair.random().publicKey()) - } - - private suspend fun receivePacket(datagram: Buffer, address: SocketAddress, arrivalTime: Long) { - val packet: Packet - try { - packet = Packet.decodeFrom(Bytes.wrap(datagram.bytes)) - } catch (e: DecodingException) { - logger.debug("{}: ignoring invalid packet from {}", serviceDescriptor, address) - ++invalidPackets - return - } - - if (packet.nodeId == nodeId) { - logger.debug("{}: ignoring packet from self", serviceDescriptor) - ++selfPackets - return - } - - if (packet.isExpired(arrivalTime - PACKET_EXPIRATION_CHECK_GRACE_MS)) { - logger.debug("{}: ignoring expired packet", serviceDescriptor) - ++expiredPackets - return - } - - if (packetFilter?.invoke(packet.nodeId, address) == false) { - logger.debug("{}: packet rejected by filter", serviceDescriptor) - ++filteredPackets - return - } - - when (packet) { - is PingPacket -> handlePing(packet, address, arrivalTime) - is PongPacket -> handlePong(packet, address, arrivalTime) - is FindNodePacket -> handleFindNode(packet, address, arrivalTime) - is NeighborsPacket -> handleNeighbors(packet, address) - is ENRRequestPacket -> handleENRRequest(packet, address, arrivalTime) - is ENRResponsePacket -> handleENRResponse(packet, address, arrivalTime) - }.let {} // guarantees "when" matching is exhaustive - } - - private suspend fun handlePing(packet: PingPacket, from: SocketAddress, arrivalTime: Long) { - // COMPATIBILITY: The ping packet should contain the canonical endpoint for the peer, yet it is often observed to - // be incorrect (using private-subnet addresses, wildcard addresses, etc). So instead, respond to the source - // address of the packet itself. - val fromEndpoint = Endpoint(from, packet.from.tcpPort) - val peer = peerRepository.get(from.host(), from.port(), packet.nodeId) - // update the endpoint if the peer does not have one that's been proven - var currentEndpoint = peer.updateEndpoint(fromEndpoint, arrivalTime, arrivalTime - ENDPOINT_PROOF_LONGEVITY_MS) - if (currentEndpoint.tcpPort != packet.from.tcpPort) { - currentEndpoint = peer.updateEndpoint( - Endpoint(currentEndpoint.address, currentEndpoint.udpPort, packet.from.tcpPort), - arrivalTime, - ) - } - - val pong = PongPacket.create(keyPair, timeSupplier(), currentEndpoint, packet.hash, seq) - sendPacket(from, pong) - // https://github.com/ethereum/devp2p/blob/master/discv4.md#ping-packet-0x01 also suggests sending a ping - // packet if the peer is unknown, however sending two packets in response to a single incoming would allow a - // traffic amplification attack - } - - private suspend fun handlePong(packet: PongPacket, from: SocketAddress, arrivalTime: Long) { - val pending = awaitingPongs.remove(packet.pingHash) ?: run { - logger.debug("{}: received unexpected or late pong from {}", serviceDescriptor, from) - ++unexpectedPongs - return - } - - val sender = pending.peer - // COMPATIBILITY: If the node-id's don't match, the pong should probably be rejected. However, if a different - // peer is listening at the same address, it will respond to the ping with its node-id. Instead of rejecting, - // accept the pong and update the new peer record with the proven endpoint, preferring to keep its current - // tcpPort and otherwise keeping the tcpPort of the original peer. - val peer = - if (sender.nodeId == packet.nodeId) sender else peerRepository.get(from.host(), from.port(), packet.nodeId) - val endpoint = if (peer.verifyEndpoint(pending.endpoint, arrivalTime)) { - pending.endpoint - } else { - val endpoint = peer.endpoint.tcpPort?.let { port -> - Endpoint(pending.endpoint.address, pending.endpoint.udpPort, port) - } ?: pending.endpoint - peer.updateEndpoint(endpoint, arrivalTime) - peer.verifyEndpoint(endpoint, arrivalTime) - endpoint - } - - if (sender.nodeId == packet.nodeId) { - logger.debug("{}: verified peer endpoint {} (node-id: {})", serviceDescriptor, endpoint.address, peer.nodeId) - } else { - logger.debug( - "{}: verified peer endpoint {} (node-id: {} - changed from {})", - serviceDescriptor, - endpoint.address, - peer.nodeId, - sender.nodeId, - ) - } - - pending.complete(VerificationResult(peer, endpoint)) - - if (packet.enrSeq != null) { - if (peer.enr == null || peer.enr!!.seq() < packet.enrSeq) { - val now = timeSupplier() - withTimeoutOrNull(ENR_REQUEST_TIMEOUT_MS) { enrRequest(endpoint, peer).verify(now) } - } - } - } - - private suspend fun handleFindNode(packet: FindNodePacket, from: SocketAddress, arrivalTime: Long) { - // if the peer has not been validated, delay sending neighbors until it is - val peer = peerRepository.get(from.host(), from.port(), packet.nodeId) - val (_, endpoint) = ensurePeerIsValid(peer, from, arrivalTime) ?: run { - logger.debug("{}: received findNode from {} which cannot be validated", serviceDescriptor, from) - ++unvalidatedPeerPackets - return - } - - logger.debug("{}: received findNode from {} for target-id {}", serviceDescriptor, from, packet.target) - val nodes = neighbors(packet.target).map { p -> p.toNode() } - - val address = endpoint.udpSocketAddress - NeighborsPacket.createRequired(keyPair, timeSupplier(), nodes).forEach { p -> - logger.debug("{}: sending {} neighbors to {}", serviceDescriptor, p.nodes.size, address) - sendPacket(address, p) - } - } - - private fun handleNeighbors(packet: NeighborsPacket, from: SocketAddress) { - findNodeStates.getIfPresent(packet.nodeId)?.let { state -> - for (node in packet.nodes) { - launch { - logger.debug("{}: received neighbour {} from {}", serviceDescriptor, node.endpoint.address, from) - val neighbor = peerRepository.get(from.host(), from.port(), node.nodeId) - val now = timeSupplier() - neighbor.updateEndpoint(node.endpoint, now, now - ENDPOINT_PROOF_LONGEVITY_MS) - - withTimeoutOrNull(PEER_VERIFICATION_TIMEOUT_MS) { - endpointVerification(node.endpoint, neighbor).verify(now) - }?.let { result -> - logger.debug( - "{}: adding {} to the routing table (node-id: {})", - serviceDescriptor, - result.endpoint.address, - result.peer.nodeId, - ) - addToRoutingTable(result.peer) - } - } - } - state.receive(packet.nodes) - } ?: run { - logger.debug("{}: received unexpected or late neighbors packet from {}", serviceDescriptor, from) - ++unexpectedNeighbors - } - } - - private suspend fun handleENRRequest(packet: ENRRequestPacket, from: SocketAddress, arrivalTime: Long) { - val peer = peerRepository.get(from.host(), from.port(), packet.nodeId) - val (_, endpoint) = ensurePeerIsValid(peer, from, arrivalTime) ?: run { - logger.debug("{}: received enrRequest from {} which cannot be validated", serviceDescriptor, from) - ++unvalidatedPeerPackets - return - } - - logger.debug("{}: received enrRequest from {}", serviceDescriptor, from) - - val address = endpoint.udpSocketAddress - sendPacket(address, ENRResponsePacket.create(keyPair, timeSupplier(), packet.hash, enr!!)) - } - - private suspend fun handleENRResponse(packet: ENRResponsePacket, from: SocketAddress, arrivalTime: Long) { - val pending = awaitingENRs.remove(packet.requestHash) ?: run { - logger.debug("{}: received unexpected or late enr response from {}", serviceDescriptor, from) - ++unexpectedENRResponses - return - } - packet.requestHash - val sender = pending.peer - // COMPATIBILITY: If the node-id's don't match, the pong should probably be rejected. However, if a different - // peer is listening at the same address, it will respond to the ping with its node-id. Instead of rejecting, - // accept the pong and update the new peer record with the proven endpoint, preferring to keep its current - // tcpPort and otherwise keeping the tcpPort of the original peer. - val peer = - if (sender.nodeId == packet.nodeId) sender else peerRepository.get(from.host(), from.port(), packet.nodeId) - - val enr = EthereumNodeRecord.fromRLP(packet.enr) - try { - enr.validate() - } catch (e: InvalidNodeRecordException) { - logger.debug("Invalid ENR", e) - return - } - - peer.updateENR(enr, arrivalTime) - - pending.complete(ENRResult(peer, enr)) - } - - private fun addToRoutingTable(peer: Peer) { - routingTable.add(peer)?.let { contested -> - launch { - contested.endpoint.let { endpoint -> - withTimeoutOrNull(PEER_VERIFICATION_TIMEOUT_MS) { endpointVerification(endpoint, contested).verify() } - } ?: routingTable.evict(contested) - } - } - } - - private suspend fun ensurePeerIsValid( - peer: Peer, - address: SocketAddress, - arrivalTime: Long, - ): VerificationResult? { - val now = timeSupplier() - peer.getEndpoint(now - ENDPOINT_PROOF_LONGEVITY_MS)?.let { endpoint -> - // already valid - return VerificationResult(peer, endpoint) - } - - val endpoint = peer.updateEndpoint( - Endpoint(address, peer.endpoint.tcpPort), - arrivalTime, - now - ENDPOINT_PROOF_LONGEVITY_MS, - ) - return withTimeoutOrNull(PEER_VERIFICATION_TIMEOUT_MS) { endpointVerification(endpoint, peer).verify(now) } - } - - private fun endpointVerification(endpoint: Endpoint, peer: Peer) = - verifyingEndpoints.get(endpoint.udpSocketAddress) { EndpointVerification(endpoint, peer) } - - // a representation of the state and current action for verifying an endpoint, - // to avoid concurrent attempts to verify the same endpoint - private inner class EndpointVerification(val endpoint: Endpoint, val peer: Peer) { - private val deferred = CompletableDeferred() - - @Volatile - private var active: Job? = null - private var nextPingMs: Long = 0 - private var retryDelay: Long = 0 - - suspend fun verify(now: Long = timeSupplier()): VerificationResult? { - if (!deferred.isCompleted) { - // if not already actively pinging and enough time has passed since the last ping, send a single ping - synchronized(this) { - if (active?.isCompleted != false && now >= nextPingMs) { - nextPingMs = now + RESEND_DELAY_MS - launch { sendPing(now) } - } - } - } - return deferred.await() - } - - suspend fun verifyWithRetries(): VerificationResult? { - if (!deferred.isCompleted) { - // if not already actively pinging, start pinging with retries - synchronized(this) { - if (active?.isCompleted != false) { - active = launch { - repeat(PING_RETRIES) { - delay(nextPingMs - timeSupplier()) - nextPingMs = timeSupplier() + RESEND_DELAY_MS - retryDelay += RESEND_DELAY_INCREASE_MS - if (retryDelay > RESEND_MAX_DELAY_MS) { - retryDelay = RESEND_MAX_DELAY_MS - } - sendPing() - } - } - } - } - } - return deferred.await() - } - - private suspend fun sendPing(now: Long = timeSupplier()) { - val pingPacket = PingPacket.create(keyPair, now, selfEndpoint!!, endpoint, seq) - - // create local references to be captured in the closure, rather than the whole packet instance - val hash = pingPacket.hash - val timeout = pingPacket.expiration - now - - // very unlikely that there is another ping packet created with the same hash yet a different EndpointVerification - // instance, but if there is then the first will be waiting on a deferred that never completes and will - // eventually time out - if (awaitingPongs.put(hash, this) != this) { - launch { - delay(timeout) - awaitingPongs.remove(hash) - } - sendPacket(endpoint.udpSocketAddress, pingPacket) - } - } - - fun complete(result: VerificationResult?): Boolean { - active?.cancel() - return deferred.complete(result) - } - } - - private data class VerificationResult( - /** The peer that responded to the verification request. */ - val peer: Peer, - /** - * The endpoint that was verified. - * - * This will typically be the same as peer.endpoint, but may not be due to concurrent updates. - */ - val endpoint: Endpoint, - ) - - private fun enrRequest(endpoint: Endpoint, peer: Peer) = - requestingENRs.get(endpoint.udpSocketAddress) { ENRRequest(endpoint, peer) } - - // a representation of the state and current action for querying an ENR from a peer, - // to avoid concurrent attempts to request the same information. - private inner class ENRRequest(val endpoint: Endpoint, val peer: Peer) { - private val deferred = CompletableDeferred() - - @Volatile - private var active: Job? = null - private var nextENRRequest: Long = 0 - - suspend fun verify(now: Long = timeSupplier()): ENRResult? { - if (!deferred.isCompleted) { - // if not already actively requesting and enough time has passed since the last request, send a single request - synchronized(this) { - if (active?.isCompleted != false && now >= nextENRRequest) { - nextENRRequest = now + RESEND_DELAY_MS - launch { sendENRRequest(now) } - } - } - } - return deferred.await() - } - - private suspend fun sendENRRequest(now: Long = timeSupplier()) { - val enrRequestPacket = ENRRequestPacket.create(keyPair, now) - - // create local references to be captured in the closure, rather than the whole packet instance - val hash = enrRequestPacket.hash - val timeout = enrRequestPacket.expiration - now - - // very unlikely that there is another ping packet created with the same hash yet a different ENRRequest - // instance, but if there is then the first will be waiting on a deferred that never completes and will - // eventually time out - if (awaitingENRs.put(hash, this) != this) { - launch { - delay(timeout) - awaitingENRs.remove(hash) - } - sendPacket(endpoint.udpSocketAddress, enrRequestPacket) - } - } - - fun complete(result: ENRResult?): Boolean { - active?.cancel() - return deferred.complete(result) - } - } - - private data class ENRResult( - val peer: Peer, - val enr: EthereumNodeRecord, - ) - - private suspend fun findNodes(peer: Peer, target: SECP256K1.PublicKey): AsyncResult> { - if (peer.nodeId == nodeId) { - // for queries to self, respond directly - return AsyncResult.completed(neighbors(target).map { p -> p.toNode() }) - } - return findNodeStates.get(peer.nodeId) { FindNodeState(peer) }.findNodes(target) - } - - private fun neighbors(target: SECP256K1.PublicKey) = routingTable.nearest(target, DEVP2P_BUCKET_SIZE) - - private data class FindNodeRequest(val target: SECP256K1.PublicKey) - - private inner class FindNodeState(val peer: Peer) { - - private val nodesCollected = Collections.synchronizedList(ArrayList()) - private val result = AsyncResult.incomplete>() - private val start = timeSupplier() - - @Volatile - private var lastReceive: Long = 0 - - private suspend fun send(request: FindNodeRequest) { - try { - val endpoint = peer.endpoint - val now = timeSupplier() - lastReceive = now - val findNodePacket = FindNodePacket.create(keyPair, now, request.target) - sendPacket(endpoint.udpSocketAddress, findNodePacket) - logger.debug("{}: sent findNode to {} for {}", serviceDescriptor, endpoint.udpSocketAddress, request.target) - - // issue a "get" on the state cache, to indicate that this state is still in use - val state = findNodeStates.getIfPresent(peer.nodeId) - if (state != this) { - logger.warn("{}: findNode state for {} has been replaced", serviceDescriptor, state) - close() - } - } catch (e: TimeoutCancellationException) { - logger.debug( - "$serviceDescriptor: Timeout while sending FindNode requests for peer ${peer.nodeId}", - e, - ) - } catch (e: Exception) { - logger.error( - "$serviceDescriptor: Error while sending FindNode requests for peer ${peer.nodeId}", - e, - ) - } - } - - suspend fun findNodes(target: SECP256K1.PublicKey): AsyncResult> { - send(FindNodeRequest(target)) - return result - } - - fun receive(nodes: List) { - lastReceive = timeSupplier() - nodesCollected.addAll(nodes) - if (lastReceive - start > FIND_NODES_QUERY_GAP_MS) { - close() - } - } - - fun close() { - result.complete(nodesCollected) - } - } - - private suspend fun sendPacket(address: SocketAddress, packet: Packet) { - server.send(Buffer.buffer(packet.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Endpoint.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Endpoint.kt deleted file mode 100644 index 9f646edf6..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Endpoint.kt +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import io.vertx.core.net.SocketAddress -import org.apache.tuweni.rlp.RLPException -import org.apache.tuweni.v2.rlp.RLPReader -import org.apache.tuweni.v2.rlp.RLPWriter -import java.net.InetAddress -import java.net.UnknownHostException - -/** - * An Ethereum node endpoint. - * - * @constructor Create a new endpoint. - * @param address the InetAddress - * @param udpPort the UDP port for the endpoint - * @param tcpPort the TCP port for the endpoint or `null` if no TCP port is known - * @throws IllegalArgumentException if either port is out of range - */ -data class Endpoint( - val address: String, - val udpPort: Int = DEFAULT_PORT, - val tcpPort: Int? = null, -) { - - /** - * Create a new endpoint. - * - * @param address a SocketAddress, containing the IP address the UDP port - */ - constructor(address: SocketAddress, tcpPort: Int? = null) : this(address.host(), address.port(), tcpPort) - - companion object { - - /** - * The default port used by Ethereum DevP2P. - */ - const val DEFAULT_PORT = 30303 - - /** - * Create an Endpoint by reading fields from the RLP input stream. - * - * If the fields are wrapped into an RLP list, use `reader.readList` to unwrap before calling this method. - * - * @param reader the RLP input stream from which to read - * @return the decoded endpoint - * @throws RLPException if the RLP source does not decode to a valid endpoint - */ - fun readFrom(reader: RLPReader): Endpoint { - val addr: InetAddress - try { - addr = InetAddress.getByAddress(reader.readValue().toArrayUnsafe()) - } catch (e: UnknownHostException) { - throw RLPException(e) - } - - var udpPort = reader.readInt() - if (udpPort == 0) { // this is an invalid port number we see in the wild. Use DEFAULT_PORT instead. - udpPort = DEFAULT_PORT - } - // Some implementations seem to send packets that either do not have the TCP port field, or to have an - // RLP NULL value for it. - var tcpPort: Int? = null - if (!reader.isComplete) { - tcpPort = reader.readInt() - if (tcpPort == 0) { - tcpPort = null - } - } - - return Endpoint(addr.hostAddress, udpPort, tcpPort) - } - } - - init { - require(udpPort in 1..65535) { "udpPort should be between 1 and 65535, got $udpPort" } - require(tcpPort == null || tcpPort in 1..65535) { "tcpPort should be between 1 and 65535, got $tcpPort" } - } - - /** - * UDP socket address of the endpoint - */ - val udpSocketAddress: SocketAddress = SocketAddress.inetSocketAddress(udpPort, address) - - /** - * Write this endpoint to an RLP output. - * - * @param writer the RLP writer - */ - internal fun writeTo(writer: RLPWriter) { - writer.writeByteArray(InetAddress.getByName(address).address) - writer.writeInt(udpPort) - writer.writeInt(tcpPort ?: 0) - } - - // rough over-estimate, assuming maximum size encoding for the port numbers - internal fun rlpSize(): Int = 1 + InetAddress.getByName(address).address.size + 2 * (1 + 2) -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EnodeUriComponents.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EnodeUriComponents.kt deleted file mode 100644 index 9e7bcb409..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EnodeUriComponents.kt +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import java.net.URI -import java.util.regex.Pattern - -private val DISCPORT_QUERY_STRING_REGEX = Pattern.compile(".*discport=([^&]+).*") - -/** - * The components of an enode URI. - * @param nodeId the public key of the node - * @param endpoint the ndoe endpoint - */ -data class EnodeUriComponents(val nodeId: SECP256K1.PublicKey, val endpoint: Endpoint) - -/** - * Parse an enode URI. - * - * @param uri the URI to parse - * @return the node id and the endpoint - * @throws IllegalArgumentException if the uri is not a valid enode URI - */ -fun parseEnodeUri(uri: URI): EnodeUriComponents { - require("enode" == uri.scheme) { "URI must be an enode:// uri" } - require(uri.userInfo != null) { "URI must have a node id" } - val nodeId = SECP256K1.PublicKey.fromBytes(Bytes.fromHexString(uri.userInfo)) - - var tcpPort = Endpoint.DEFAULT_PORT - if (uri.port >= 0) { - tcpPort = uri.port - } - - // If TCP and UDP ports differ, expect a query param 'discport' with the UDP port. - // See https://github.com/ethereum/wiki/wiki/enode-url-format - var udpPort = tcpPort - val query = uri.query - if (query != null) { - val matcher = DISCPORT_QUERY_STRING_REGEX.matcher(query) - if (matcher.matches()) { - try { - udpPort = Integer.parseInt(matcher.group(1)) - } catch (e: NumberFormatException) { - throw IllegalArgumentException("Invalid discport query parameter") - } - } - } - - return EnodeUriComponents( - nodeId, - Endpoint(uri.host, udpPort, tcpPort), - ) -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecord.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecord.kt deleted file mode 100644 index 4c0bf2604..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecord.kt +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.bytes.MutableBytes -import org.apache.tuweni.v2.crypto.Hash -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.rlp.RLP -import org.apache.tuweni.v2.rlp.RLPReader -import org.apache.tuweni.v2.rlp.RLPWriter -import org.apache.tuweni.v2.units.bigints.UInt256 -import java.lang.IllegalArgumentException -import java.lang.RuntimeException -import java.net.InetAddress -import java.time.Instant - -/** - * Ethereum Node Record (ENR) as described in [EIP-778](https://eips.ethereum.org/EIPS/eip-778). - * - * @param signature the record signature - * @param seq the sequence of the record, its revision number - * @param data the arbitrary data of the record - * @param listData the arbitrary data of the record as list - */ -class EthereumNodeRecord( - val signature: Bytes, - val seq: Long, - val data: Map, - val listData: Map> = emptyMap(), - val rlp: Bytes, -) { - - companion object { - - /** - * Derives the public key of an ethereum node record into a unique 32 bytes hash. - * @param publicKey the public key to hash - * @return the hash of the public key - * @throws IllegalArgumentException if the public key is not valid. - */ - fun nodeId(publicKey: SECP256K1.PublicKey): Bytes { - val pt = publicKey.asEcPoint() - val xPart = UInt256.valueOf(pt.xCoord.toBigInteger()) - val yPart = UInt256.valueOf(pt.yCoord.toBigInteger()) - return Hash.keccak256(Bytes.wrap(xPart, yPart)) - } - - /** - * Creates an ENR from its serialized form as a RLP list - * @param rlp the serialized form of the ENR - * @return the ENR - * @throws IllegalArgumentException if the rlp bytes length is longer than 300 bytes - */ - @JvmStatic - fun fromRLP(rlp: Bytes): EthereumNodeRecord { - if (rlp.size() > 300) { - throw IllegalArgumentException("Record too long") - } - return RLP.decodeList(rlp) { fromRLP(it, rlp) } - } - - /** - * Creates an ENR from its serialized form as a RLP list - * @param reader the RLP reader - * @return the ENR - * @throws IllegalArgumentException if the rlp bytes length is longer than 300 bytes - */ - @JvmStatic - fun fromRLP(reader: RLPReader): EthereumNodeRecord { - val tempRecord = fromRLP(reader, Bytes.EMPTY) - val encoded = RLP.encodeList { - it.writeValue(tempRecord.signature) - encode(data = tempRecord.data, seq = tempRecord.seq, writer = it) - } - - return fromRLP(encoded) - } - - /** - * Creates an ENR from its serialized form as a RLP list - * @param reader the RLP reader - * @return the ENR - * @throws IllegalArgumentException if the rlp bytes length is longer than 300 bytes - */ - @JvmStatic - fun fromRLP(reader: RLPReader, rlp: Bytes): EthereumNodeRecord { - val sig = reader.readValue() - - val seq = reader.readLong() - - val data = mutableMapOf() - val listData = mutableMapOf>() - while (!reader.isComplete) { - val key = reader.readString() - if (reader.nextIsList()) { - listData[key] = reader.readListContents { listreader -> - if (listreader.nextIsList()) { - // TODO complex structures not supported - listreader.skipNext() - null - } else { - listreader.readValue() - } - }.filterNotNull() - } else { - val value = reader.readValue() - data[key] = value - } - } - - return EthereumNodeRecord(sig, seq, data, listData, rlp) - } - - fun encode( - signatureKeyPair: SECP256K1.KeyPair? = null, - seq: Long = Instant.now().toEpochMilli(), - ip: InetAddress? = null, - tcp: Int? = null, - udp: Int? = null, - data: Map? = null, - listData: Map>? = null, - writer: RLPWriter, - ) { - writer.writeLong(seq) - val mutableData = data?.toMutableMap() ?: mutableMapOf() - mutableData["id"] = Bytes.wrap("v4".toByteArray()) - signatureKeyPair?.let { - mutableData["secp256k1"] = Bytes.wrap(it.publicKey().asEcPoint().getEncoded(true)) - } - ip?.let { - mutableData["ip"] = Bytes.wrap(it.address) - } - tcp?.let { - mutableData["tcp"] = Bytes.ofUnsignedShort(it) - } - udp?.let { - mutableData["udp"] = Bytes.ofUnsignedShort(it) - } - val keys = mutableListOf() - keys.addAll(mutableData.keys) - listData?.let { keys.addAll(it.keys) } - keys.sorted().forEach { key -> - mutableData[key]?.let { value -> - writer.writeString(key) - writer.writeValue(value) - } - listData?.get(key)?.let { value -> - writer.writeString(key) - writer.writeList(value) { writer, v -> writer.writeValue(v) } - } - } - } - - /** - * Creates the serialized form of a ENR - * @param signatureKeyPair the key pair to use to sign the ENR - * @param seq the sequence number for the ENR. It should be higher than the previous time the ENR was generated. It defaults to the current time since epoch in milliseconds. - * @param data the key pairs to encode in the ENR - * @param listData the key pairs of list values to encode in the ENR - * @param ip the IP address of the host - * @param tcp an optional parameter to a TCP port used for the wire protocol - * @param udp an optional parameter to a UDP port used for discovery - * @return the ENR - */ - @JvmOverloads - @JvmStatic - fun create( - signatureKeyPair: SECP256K1.KeyPair, - seq: Long = Instant.now().toEpochMilli(), - data: Map? = null, - listData: Map>? = null, - ip: InetAddress, - tcp: Int? = null, - udp: Int? = null, - ): EthereumNodeRecord { - return fromRLP(toRLP(signatureKeyPair, seq, data, listData, ip, tcp, udp)) - } - - /** - * Creates the serialized form of a ENR - * @param signatureKeyPair the key pair to use to sign the ENR - * @param seq the sequence number for the ENR. It should be higher than the previous time the ENR was generated. It defaults to the current time since epoch in milliseconds. - * @param data the key pairs to encode in the ENR - * @param listData the key pairs of list values to encode in the ENR - * @param ip the IP address of the host - * @param tcp an optional parameter to a TCP port used for the wire protocol - * @param udp an optional parameter to a UDP port used for discovery - * @return the serialized form of the ENR as a RLP-encoded list - */ - @JvmOverloads - @JvmStatic - fun toRLP( - signatureKeyPair: SECP256K1.KeyPair, - seq: Long = Instant.now().toEpochMilli(), - data: Map? = null, - listData: Map>? = null, - ip: InetAddress, - tcp: Int? = null, - udp: Int? = null, - ): Bytes { - val encoded = RLP.encodeList { writer -> - encode(signatureKeyPair, seq, ip, tcp, udp, data, listData, writer) - } - val signature = SECP256K1.sign(encoded, signatureKeyPair) - val sigBytes = MutableBytes.create(64) - sigBytes.set(0, UInt256.valueOf(signature.r())) - sigBytes.set(32, UInt256.valueOf(signature.s())) - - val completeEncoding = RLP.encodeList { writer -> - writer.writeValue(sigBytes) - encode(signatureKeyPair, seq, ip, tcp, udp, data, listData, writer) - } - return completeEncoding - } - } - - /** - * Validates an ENR to check that it conforms to a valid ENR scheme. - * - * Only the v4 scheme is supported at this time. - */ - fun validate() { - if (Bytes.wrap("v4".toByteArray()) != data["id"]) { - throw InvalidNodeRecordException("id attribute is not set to v4") - } - - val encoded = RLP.encodeList { - encode(data = data, seq = seq, writer = it) - } - - val sig = SECP256K1.Signature.create( - 1, - signature.slice(0, 32).toUnsignedBigInteger(), - signature.slice(32).toUnsignedBigInteger(), - ) - - val pubKey = publicKey() - val recovered = SECP256K1.PublicKey.recoverFromSignature(encoded, sig) - - if (pubKey != recovered) { - val sig0 = SECP256K1.Signature.create( - 0, - signature.slice(0, 32).toUnsignedBigInteger(), - signature.slice(32).toUnsignedBigInteger(), - ) - val recovered0 = SECP256K1.PublicKey.recoverFromSignature(encoded, sig0) - if (pubKey != recovered0) { - throw InvalidNodeRecordException("Public key does not match signature") - } - } - } - - /** - * The ENR public key entry - * @return the ENR public key - */ - fun publicKey(): SECP256K1.PublicKey { - return SECP256K1.PublicKey.fromBytes(publicKeyBytes()) - } - - /** - * The ENR public key entry bytes - * @return the ENR public key bytes - */ - private fun publicKeyBytes(): Bytes { - val keyBytes = data["secp256k1"] ?: throw InvalidNodeRecordException("Missing secp256k1 entry") - val ecPoint = SECP256K1.Parameters.CURVE.curve.decodePoint(keyBytes.toArrayUnsafe()) - return Bytes.wrap(ecPoint.getEncoded(false)).slice(1) - } - - /** - * Derives the public key of an ethereum node record into a unique 32 bytes hash. - * @return the hash of the public key - */ - fun nodeId() = nodeId(publicKey()) - - /** - * The ip associated with the ENR - * @return The IP adress of the ENR - */ - fun ip(): InetAddress { - return data["ip"]?.let { InetAddress.getByAddress(it.toArrayUnsafe()) } ?: InetAddress.getLoopbackAddress() - } - - /** - * The TCP port of the ENR - * @return the TCP port associated with this ENR - */ - fun tcp(): Int? { - return data["tcp"]?.toInt() - } - - /** - * The UDP port of the ENR - * @return the UDP port associated with this ENR - */ - fun udp(): Int? { - return data["udp"]?.toInt() ?: tcp() - } - - fun seq(): Long { - return seq - } - - /** - * @return the ENR as a URI - */ - override fun toString(): String { - return "enr:${ip()}:${tcp()}?udp=${udp()}" - } - - fun toRLP(): Bytes = rlp - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as EthereumNodeRecord - - return rlp == other.rlp - } - - override fun hashCode(): Int { - return rlp.hashCode() - } -} - -internal class InvalidNodeRecordException(message: String?) : RuntimeException(message) diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Node.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Node.kt deleted file mode 100644 index 6d4d0000f..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Node.kt +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.rlp.RLPReader -import org.apache.tuweni.v2.rlp.RLPWriter - -internal data class Node( - val endpoint: Endpoint, - val nodeId: SECP256K1.PublicKey, -) { - - companion object { - fun readFrom(reader: RLPReader): Node { - val endpoint = Endpoint.readFrom(reader) - val nodeId = SECP256K1.PublicKey.fromBytes(reader.readValue()) - return Node(endpoint, nodeId) - } - } - - internal fun writeTo(writer: RLPWriter) { - endpoint.writeTo(writer) - writer.writeValue(nodeId.bytes()) - } - - internal fun rlpSize(): Int = 1 + endpoint.rlpSize() + 3 + 64 -} - -internal fun Peer.toNode(): Node = - Node(endpoint, nodeId) diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Packet.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Packet.kt deleted file mode 100644 index 91de3ed45..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Packet.kt +++ /dev/null @@ -1,477 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.rlp.RLPException -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.Hash -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.rlp.RLP -import org.apache.tuweni.v2.rlp.RLPWriter -import java.nio.ByteBuffer - -internal class DecodingException(message: String, cause: Throwable? = null) : Exception(message, cause) - -internal data class SigHash(val signature: SECP256K1.Signature, val hash: Bytes) - -private fun msecToSec(time: Long) = (time + 999) / 1000 -private fun secToMsec(time: Long) = time * 1000 - -internal sealed class Packet( - val nodeId: SECP256K1.PublicKey, - private val signature: SECP256K1.Signature, - val hash: Bytes, - val expiration: Long, -) { - - companion object { - const val MIN_SIZE = 104 - const val MAX_SIZE = 1280 - - private const val HASH_INDEX = 0 - private const val SIGNATURE_INDEX = 32 - private const val PACKET_TYPE_INDEX = 97 - private const val PACKET_DATA_INDEX = 98 - - fun decodeFrom(datagram: ByteBuffer) = - decodeFrom(Bytes.wrapByteBuffer(datagram)) - - fun decodeFrom(datagram: Bytes): Packet { - val typeByte = datagram.get(PACKET_TYPE_INDEX) - val packetType = PacketType.forType(typeByte) ?: throw DecodingException("Unrecognized packet type: $typeByte") - - val signature = SECP256K1.Signature.fromBytes( - datagram.slice(SIGNATURE_INDEX, PACKET_TYPE_INDEX - SIGNATURE_INDEX), - ) - - val publicKey = SECP256K1.PublicKey.recoverFromSignature( - datagram.slice(PACKET_TYPE_INDEX, datagram.size() - PACKET_TYPE_INDEX), - signature, - ) ?: throw DecodingException("Invalid packet signature") - - val hash = Bytes.wrap( - datagram.slice( - HASH_INDEX, - SIGNATURE_INDEX, - ), - ) - if (Hash.keccak256(datagram.slice(SIGNATURE_INDEX)) != hash) { - throw DecodingException("Invalid packet hash") - } - - return packetType.decode(datagram.slice(PACKET_DATA_INDEX), hash, publicKey, signature) - } - - @JvmStatic - protected fun expirationFor(now: Long) = now + PACKET_EXPIRATION_PERIOD_MS - - @JvmStatic - protected fun createSignature( - packetType: PacketType, - keyPair: SECP256K1.KeyPair, - encoder: (RLPWriter) -> Unit, - ): SigHash { - val typeByte = Bytes.of(packetType.typeId) - val dataBytes = RLP.encodeList { writer -> encoder(writer) } - val payloadBytes = Bytes.wrap(typeByte, dataBytes) - val signature = SECP256K1.sign(payloadBytes, keyPair) - val hash = Hash.keccak256(Bytes.wrap(signature.bytes(), payloadBytes)) - return SigHash(signature, hash) - } - } - - fun isExpired(now: Long): Boolean = expiration <= now - - abstract fun encode(): Bytes - - protected fun encodeTo(packetType: PacketType, contentWriter: (RLPWriter) -> Unit): Bytes { - return Bytes.wrap(hash, signature.bytes(), Bytes.of(packetType.typeId), RLP.encodeList(contentWriter)) - } -} - -internal class PingPacket private constructor( - nodeId: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - hash: Bytes, - val from: Endpoint, - val to: Endpoint, - expiration: Long, - val enrSeq: Long?, -) : Packet(nodeId, signature, hash, expiration) { - - companion object { - private const val VERSION = 4 - - fun create(keyPair: SECP256K1.KeyPair, now: Long, from: Endpoint, to: Endpoint, seq: Long?): PingPacket { - val expiration = expirationFor(now) - val sigHash = createSignature( - PacketType.PING, - keyPair, - ) { writer -> - encodeTo(writer, from, to, expiration, seq) - } - return PingPacket( - keyPair.publicKey(), - sigHash.signature, - sigHash.hash, - from, - to, - expiration, - seq, - ) - } - - fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ): PingPacket { - try { - return RLP.decodeList(payload) { reader -> - val version = reader.readInt() - val from = reader.readList { r -> Endpoint.readFrom(r) } - val to = reader.readList { r -> Endpoint.readFrom(r) } - val expiration = reader.readLong() // seconds - val seq: Long? = if (!reader.isComplete) { - reader.readLong() - } else { - null - } - - if (version < VERSION) { - throw DecodingException("Unexpected version $VERSION in ping") - } - PingPacket(publicKey, signature, hash, from, to, secToMsec(expiration), seq) - } - } catch (e: RLPException) { - throw DecodingException("Invalid ping packet", e) - } - } - - private fun encodeTo(writer: RLPWriter, from: Endpoint, to: Endpoint, expiration: Long, seq: Long?) { - writer.writeInt(VERSION) - writer.writeList { w -> from.writeTo(w) } - writer.writeList { w -> to.writeTo(w) } - writer.writeLong(msecToSec(expiration)) // write in seconds - seq?.let { - writer.writeLong(it) - } - } - } - - override fun encode() = encodeTo(PacketType.PING) { writer -> - encodeTo(writer, from, to, expiration, enrSeq) - } -} - -internal class PongPacket private constructor( - nodeId: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - hash: Bytes, - val to: Endpoint, - val pingHash: Bytes, - expiration: Long, - val enrSeq: Long?, -) : Packet(nodeId, signature, hash, expiration) { - - companion object { - fun create(keyPair: SECP256K1.KeyPair, now: Long, to: Endpoint, pingHash: Bytes, enrSeq: Long?): PongPacket { - val expiration = expirationFor(now) - val sigHash = createSignature( - PacketType.PONG, - keyPair, - ) { writer -> - encodeTo(writer, to, pingHash, expiration, enrSeq) - } - return PongPacket( - keyPair.publicKey(), - sigHash.signature, - sigHash.hash, - to, - pingHash, - expiration, - enrSeq, - ) - } - - fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ): PongPacket { - try { - return RLP.decodeList(payload) { reader -> - val to = reader.readList { r -> Endpoint.readFrom(r) } - val pingHash = Bytes.wrap(reader.readValue()) - val expiration = reader.readLong() // seconds - val seq: Long? = if (!reader.isComplete) { - reader.readLong() - } else { - null - } - PongPacket(publicKey, signature, hash, to, pingHash, secToMsec(expiration), seq) - } - } catch (e: RLPException) { - throw DecodingException("Invalid pong packet", e) - } - } - - private fun encodeTo(writer: RLPWriter, to: Endpoint, pingHash: Bytes, expiration: Long, enrSeq: Long?) { - writer.writeList { w -> to.writeTo(w) } - writer.writeValue(pingHash) - writer.writeLong(msecToSec(expiration)) - enrSeq?.let { writer.writeLong(it) } - } - } - - override fun encode() = encodeTo(PacketType.PONG) { writer -> - encodeTo(writer, to, pingHash, expiration, enrSeq) - } -} - -internal class FindNodePacket private constructor( - nodeId: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - hash: Bytes, - val target: SECP256K1.PublicKey, - expiration: Long, -) : Packet(nodeId, signature, hash, expiration) { - - companion object { - fun create(keyPair: SECP256K1.KeyPair, now: Long, target: SECP256K1.PublicKey): FindNodePacket { - val expiration = expirationFor(now) - val sigHash = createSignature( - PacketType.FIND_NODE, - keyPair, - ) { writer -> - encodeTo(writer, target, expiration) - } - return FindNodePacket( - keyPair.publicKey(), - sigHash.signature, - sigHash.hash, - target, - expiration, - ) - } - - fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ): FindNodePacket { - try { - return RLP.decodeList(payload) { reader -> - val target = SECP256K1.PublicKey.fromBytes(reader.readValue()) - val expiration = reader.readLong() - FindNodePacket(publicKey, signature, hash, target, secToMsec(expiration)) - } - } catch (e: RLPException) { - throw DecodingException("Invalid find nodes packet", e) - } - } - - private fun encodeTo(writer: RLPWriter, target: SECP256K1.PublicKey, expiration: Long) { - writer.writeValue(target.bytes()) - writer.writeLong(msecToSec(expiration)) - } - } - - override fun encode() = encodeTo(PacketType.FIND_NODE) { writer -> - encodeTo(writer, target, expiration) - } -} - -internal class NeighborsPacket private constructor( - nodeId: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - hash: Bytes, - val nodes: List, - expiration: Long, -) : Packet(nodeId, signature, hash, expiration) { - - companion object { - // an over-estimate of the minimum size, based on a full 64-bit expiration time and a full list length prefix - internal const val RLP_MIN_SIZE = 109 - - fun create(keyPair: SECP256K1.KeyPair, now: Long, nodes: List): NeighborsPacket { - val expiration = expirationFor(now) - val sigHash = createSignature( - PacketType.NEIGHBORS, - keyPair, - ) { writer -> - encodeTo(writer, nodes, expiration) - } - return NeighborsPacket( - keyPair.publicKey(), - sigHash.signature, - sigHash.hash, - nodes, - expiration, - ) - } - - fun createRequired(keyPair: SECP256K1.KeyPair, now: Long, nodes: List): List { - val result = mutableListOf() - var nodeSubset = mutableListOf() - var size = RLP_MIN_SIZE - for (node in nodes) { - val nodeSize = node.rlpSize() - size += nodeSize - if (size > MAX_SIZE) { - result.add(create(keyPair, now, nodeSubset)) - nodeSubset = mutableListOf() - size = RLP_MIN_SIZE + nodeSize - } - nodeSubset.add(node) - } - result.add(create(keyPair, now, nodeSubset)) - return result - } - - fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ): NeighborsPacket { - try { - return RLP.decodeList(payload) { reader -> - val nodes = mutableListOf() - reader.readList { r -> - while (!r.isComplete) { - val node = r.readList { nr -> Node.readFrom(nr) } - nodes.add(node) - } - } - val expiration = reader.readLong() - NeighborsPacket(publicKey, signature, hash, nodes, secToMsec(expiration)) - } - } catch (e: RLPException) { - throw DecodingException("Invalid nodes packet", e) - } - } - - private fun encodeTo(writer: RLPWriter, nodes: List, expiration: Long) { - writer.writeList { w -> nodes.forEach { node -> w.writeList { nw -> node.writeTo(nw) } } } - writer.writeLong(msecToSec(expiration)) - } - } - - override fun encode() = encodeTo(PacketType.NEIGHBORS) { writer -> - encodeTo(writer, nodes, expiration) - } -} - -internal class ENRRequestPacket private constructor( - nodeId: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - hash: Bytes, - expiration: Long, -) : Packet(nodeId, signature, hash, expiration) { - - companion object { - fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ): ENRRequestPacket { - try { - return RLP.decodeList(payload) { reader -> - val expiration = reader.readLong() - ENRRequestPacket(publicKey, signature, hash, secToMsec(expiration)) - } - } catch (e: RLPException) { - throw DecodingException("Invalid enr request packet", e) - } - } - - private fun encodeTo(writer: RLPWriter, expiration: Long) { - writer.writeLong(msecToSec(expiration)) - } - - fun create(keyPair: SECP256K1.KeyPair, now: Long): Packet { - val expiration = expirationFor(now) - val sigHash = createSignature( - PacketType.ENRREQUEST, - keyPair, - ) { writer -> - encodeTo(writer, expiration) - } - return ENRRequestPacket( - keyPair.publicKey(), - sigHash.signature, - sigHash.hash, - expiration, - ) - } - } - - override fun encode() = encodeTo(PacketType.ENRREQUEST) { writer -> - encodeTo(writer, expiration) - } -} - -internal class ENRResponsePacket private constructor( - nodeId: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - hash: Bytes, - expiration: Long, - val requestHash: Bytes, - val enr: Bytes, -) : Packet(nodeId, signature, hash, expiration) { - - companion object { - - fun create(keyPair: SECP256K1.KeyPair, now: Long, requestHash: Bytes, enr: Bytes): ENRResponsePacket { - val expiration = expirationFor(now) - val sigHash = createSignature( - PacketType.ENRRESPONSE, - keyPair, - ) { writer -> - encodeTo(writer, requestHash, enr, expiration) - } - return ENRResponsePacket( - keyPair.publicKey(), - sigHash.signature, - sigHash.hash, - expiration, - requestHash, - enr, - ) - } - - fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ): ENRResponsePacket { - try { - return RLP.decodeList(payload) { reader -> - // request-hash, ENR - val requestHash = reader.readValue() - val enr = reader.readValue() - val expiration = reader.readLong() - ENRResponsePacket(publicKey, signature, hash, secToMsec(expiration), requestHash, enr) - } - } catch (e: RLPException) { - throw DecodingException("Invalid enr response packet", e) - } - } - - private fun encodeTo(writer: RLPWriter, requestHash: Bytes, enr: Bytes, expiration: Long) { - writer.writeValue(requestHash) - writer.writeValue(enr) - writer.writeLong(msecToSec(expiration)) - } - } - - override fun encode() = encodeTo(PacketType.ENRRESPONSE) { writer -> - encodeTo(writer, requestHash, enr, expiration) - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PacketType.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PacketType.kt deleted file mode 100644 index 576fa423e..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PacketType.kt +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 - -/** - * DevP2P discovery packet types - * @param typeId the byte representing the type - */ -internal enum class PacketType( - val typeId: Byte, -) { - /** - * Ping packet - */ - PING(0x01) { - override fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ) = PingPacket.decode(payload, hash, publicKey, signature) as Packet - }, - - /** - * Pong packet as response to pings - */ - PONG(0x02) { - override fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ) = PongPacket.decode(payload, hash, publicKey, signature) as Packet - }, - - /** - * FindNode packet - */ - FIND_NODE(0x03) { - override fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ) = FindNodePacket.decode(payload, hash, publicKey, signature) as Packet - }, - - /** - * Neighbors packet response - */ - NEIGHBORS(0x04) { - override fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ) = NeighborsPacket.decode(payload, hash, publicKey, signature) as Packet - }, - - /** - * ENR request packet - */ - ENRREQUEST(0x05) { - override fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ) = ENRRequestPacket.decode(payload, hash, publicKey, signature) as Packet - }, - - /** - * ENR response packet - */ - ENRRESPONSE(0x06) { - override fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ) = ENRResponsePacket.decode(payload, hash, publicKey, signature) as Packet - }, ; - - companion object { - private const val MAX_VALUE: Byte = 0x7f - private val INDEX = arrayOfNulls(MAX_VALUE.toInt()) - - init { - // populate an array by packet type id for index-based lookup in `forType(Byte)` - entries.forEach { type -> INDEX[type.typeId.toInt()] = type } - } - - fun forType(typeId: Byte): PacketType? { - return INDEX[typeId.toInt()] - } - } - - init { - checkTypeId() - } - - private fun checkTypeId() { - require(typeId <= PacketType.MAX_VALUE) { "Packet typeId must be in range [0x00, 0x80]" } - } - - abstract fun decode( - payload: Bytes, - hash: Bytes, - publicKey: SECP256K1.PublicKey, - signature: SECP256K1.Signature, - ): Packet -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Peer.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Peer.kt deleted file mode 100644 index cf673fbae..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/Peer.kt +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.v2.crypto.SECP256K1 - -/** - * An Ethereum P2P network peer. - */ -interface Peer { - - /** - * The nodeId for this peer. - */ - val nodeId: SECP256K1.PublicKey - - /** - * The endpoint for communicating with this peer, if known. - */ - val endpoint: Endpoint - - /** - * The Ethereum Node Record associated with the peer, if known. - */ - val enr: EthereumNodeRecord? - - /** - * The last time the current endpoint of this peer was verified, in milliseconds since the epoch. - * - * Endpoint is verified by a ping/pong cycle: https://github.com/ethereum/devp2p/blob/master/discv4.md#endpoint-proof - */ - val lastVerified: Long? - - /** - * The time this peer was last seen at its current endpoint, in milliseconds since the epoch. - */ - val lastSeen: Long? - - /** - * Get the endpoint for this peer, if it has been verified on or after a specified time. - * - * @param ifVerifiedOnOrAfter the earliest time, in milliseconds since the epoch, when the - * endpoint of this peer must have been verified - * @return the endpoint of this peer, if it has been verified on or after the specified time - */ - fun getEndpoint(ifVerifiedOnOrAfter: Long): Endpoint? - - /** - * Update the peer with a new endpoint. - * - * If successful, the [endpoint] property will be set, the [lastSeen] timestamp will be updated. If the IP address - * or UDP port of the endpoint was changed, then the [lastVerified] timestamp will be cleared. - * - * @param endpoint the endpoint for the peer - * @param time the time this endpoint information was determined, in milliseconds since the epoch - * @param ifVerifiedBefore the latest allowable time, in milliseconds since the epoch, when this peer was last - * verified at its current endpoint. If this peers endpoint was verified after this time, the endpoint - * will not be updated. If `null`, then no check will be made and the endpoint will always be updated. - * @return the resulting endpoint of the peer - */ - fun updateEndpoint(endpoint: Endpoint, time: Long, ifVerifiedBefore: Long? = null): Endpoint - - /** - * Set the [lastVerified] and [lastSeen] time to the provided time, if the endpoint matches. - * - * Will only update [lastVerified] and/or [lastSeen] if the new time is more recent than their current values. - * - * @param endpoint the endpoint that was verified, which must match this peer's endpoint - * @param time the time when the endpoint was verified, in milliseconds since the epoch - * @return `true` if the endpoint matched the endpoint of this peer - */ - fun verifyEndpoint(endpoint: Endpoint, time: Long): Boolean - - /** - * Set the [lastSeen] time to the current time. - * - * Will only update [lastSeen] if the new time is more recent than their current values. - * - * @param time the time when the endpoint was verified, in milliseconds since the epoch - * @throws IllegalStateException if there is no endpoint for this peer - */ - fun seenAt(time: Long) - - /** - * Update the peer's ENR. - * - * Will only update if the `seq` is larger than the one associated with the peer. - * - * @param record the ENR record associated with the peer - * @param time the time this endpoint information was determined, in milliseconds since the epoch - */ - fun updateENR(record: EthereumNodeRecord, time: Long) - - fun uri(): String { - return "enode://${nodeId.toHexString()}@${endpoint.address}:${endpoint.tcpPort - ?: 30303}?discPort=${endpoint.udpPort}" - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRepository.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRepository.kt deleted file mode 100644 index a263b6835..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRepository.kt +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import org.apache.tuweni.concurrent.AsyncResult -import org.apache.tuweni.concurrent.coroutines.asyncResult -import org.apache.tuweni.v2.crypto.SECP256K1 -import java.net.URI -import java.util.concurrent.ConcurrentHashMap -import kotlin.coroutines.CoroutineContext - -/** - * A repository of peers in an Ethereum network. - * - * Conceptually, this repository stores information about all peers in an Ethereum network, hence the - * retrieval methods always return a valid [Peer]. However, the [Peer] objects are only generated on demand and - * may be purged from underlying storage if they can be recreated easily. - */ -interface PeerRepository { - - /** - * Adds a listener to the repository, which will consume peer entries whenever they are added to the repository. - */ - fun addListener(listener: (Peer) -> Unit) - - /** - * Get a Peer based on a URI components. - * - * The returned peer will use the endpoint from the URI, unless the peer is already active, in - * which case its endpoint will be unchanged. - * - * @param host the peer host - * @param port the peer port - * @param nodeId the public key associated with the peer - */ - suspend fun get(host: String, port: Int, nodeId: SECP256K1.PublicKey): Peer - - /** - * Get a Peer based on a URI. - * - * The returned peer will use the endpoint from the URI, unless the peer is already active, in - * which case its endpoint will be unchanged. - * - * @param uri the enode URI - * @return the peer - * @throws IllegalArgumentException if the URI is not a valid enode URI - */ - suspend fun get(uri: URI): Peer - - /** - * Get a Peer based on a URI. - * - * The returned peer will use the endpoint from the URI, unless the peer is already active, in - * which case its endpoint will be unchanged. - * - * @param uri the enode URI - * @return the peer - * @throws IllegalArgumentException if the URI is not a valid enode URI - */ - fun getAsync(uri: URI): AsyncResult - - /** - * Get a Peer based on a URI string. - * - * The returned peer will use the endpoint from the URI, unless the peer is already active, in - * which case its endpoint will be unchanged. - * - * @param uri the enode URI - * @return the peer - * @throws IllegalArgumentException if the URI is not a valid enode URI - */ - suspend fun get(uri: String) = get(URI.create(uri)) - - /** - * Get a Peer based on a URI string. - * - * The returned peer will use the endpoint from the URI, unless the peer is already active, in - * which case its endpoint will be unchanged. - * - * @param uri the enode URI - * @return the peer - * @throws IllegalArgumentException if the URI is not a valid enode URI - */ - fun getAsync(uri: String): AsyncResult -} - -/** - * An in-memory peer repository. - * - * Note: as the storage is in-memory, no retrieval methods in this implementation will suspend. - */ -class EphemeralPeerRepository( - private val peers: MutableMap = ConcurrentHashMap(), - override val coroutineContext: CoroutineContext = Dispatchers.Default, -) : - PeerRepository, CoroutineScope { - - private val listeners = mutableListOf<(Peer) -> Unit>() - - override fun addListener(listener: (Peer) -> Unit) { - listeners.add(listener) - } - - /** - * Get a peer from node ID and endpoint information - * @param nodeId the peer public key - * @param endpoint the peer endpoint - * @return the peer - */ - fun get(nodeId: SECP256K1.PublicKey, endpoint: Endpoint) = - peers.compute(nodeId) { _, peer -> - if (peer == null) { - val newPeer = EphemeralPeer(nodeId, endpoint) - listeners.let { - for (listener in listeners) { - listener(newPeer) - } - } - newPeer - } else { - peer - } - } as Peer - - override suspend fun get(host: String, port: Int, nodeId: SECP256K1.PublicKey): Peer { - return get(nodeId, Endpoint(host, port)) - } - - override suspend fun get(uri: URI): Peer { - val (nodeId, endpoint) = parseEnodeUri(uri) - return get(nodeId, endpoint) - } - - override fun getAsync(uri: URI): AsyncResult = asyncResult { get(uri) } - - override fun getAsync(uri: String): AsyncResult = asyncResult { get(uri) } - - private inner class EphemeralPeer( - override val nodeId: SECP256K1.PublicKey, - knownEndpoint: Endpoint, - ) : Peer { - @Volatile - override var endpoint: Endpoint = knownEndpoint - - override var enr: EthereumNodeRecord? = null - - @Synchronized - override fun getEndpoint(ifVerifiedOnOrAfter: Long): Endpoint? { - if ((lastVerified ?: 0) >= ifVerifiedOnOrAfter) { - return this.endpoint - } - return null - } - - @Volatile - override var lastVerified: Long? = null - - @Volatile - override var lastSeen: Long? = null - - @Synchronized - override fun updateEndpoint(endpoint: Endpoint, time: Long, ifVerifiedBefore: Long?): Endpoint { - val currentEndpoint = this.endpoint - if (currentEndpoint == endpoint) { - this.seenAt(time) - return currentEndpoint - } - - if (ifVerifiedBefore == null || (lastVerified ?: 0) < ifVerifiedBefore) { - if (currentEndpoint.address != endpoint.address || currentEndpoint.udpPort != endpoint.udpPort) { - lastVerified = null - } - this.endpoint = endpoint - this.seenAt(time) - return endpoint - } - - return currentEndpoint - } - - @Synchronized - override fun verifyEndpoint(endpoint: Endpoint, time: Long): Boolean { - if (endpoint != this.endpoint) { - return false - } - seenAt(time) - if ((lastVerified ?: 0) < time) { - lastVerified = time - } - return true - } - - @Synchronized - override fun seenAt(time: Long) { - if ((lastSeen ?: 0) < time) { - lastSeen = time - } - } - - @Synchronized - override fun updateENR(record: EthereumNodeRecord, time: Long) { - if (enr == null || enr!!.seq() < record.seq()) { - enr = record - updateEndpoint(Endpoint(record.ip().hostAddress, record.udp()!!, record.tcp()), time) - } - } - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRoutingTable.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRoutingTable.kt deleted file mode 100644 index 2095dcb89..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/PeerRoutingTable.kt +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import com.google.common.cache.Cache -import com.google.common.cache.CacheBuilder -import com.google.common.util.concurrent.UncheckedExecutionException -import org.apache.tuweni.kademlia.KademliaRoutingTable -import org.apache.tuweni.v2.crypto.SECP256K1 -import java.lang.IllegalArgumentException - -/** - * A routing table for ÐΞVp2p peers. - */ -interface PeerRoutingTable : Set { - - /** - * Return the nearest nodes to a target id, in order from closest to furthest. - * - * The sort order is the log distance from the target to the node-id's of the Peers. - * - * @param targetId the id to find nodes nearest to - * @param limit the maximum number of nodes to return - * @return a list of nodes from the routing table - */ - fun nearest(targetId: SECP256K1.PublicKey, limit: Int): List - - /** - * Add a node to the table. - * - * @param node the node to add - * @return `null` if the node was successfully added to the table (or already in the table). Otherwise, a node - * will be returned that is a suitable candidate for eviction, and the provided node will be stored in - * the replacements list. - */ - fun add(node: Peer): Peer? - - /** - * Remove a node from the table, potentially adding an alternative from the replacement cache. - * - * @param node the node to evict - * @param `true` if the node was removed - */ - fun evict(node: Peer): Boolean -} - -internal const val DEVP2P_BUCKET_SIZE = 16 - -/** - * A Peer routing table for the Ethereum ÐΞVp2p network. - * - * This is an implementation of a [KademliaRoutingTable] using keccak256 hashed node ids and a k-bucket size of 6. - * - * @constructor Create a new ÐΞVp2p routing table. - * @param selfId the ID of the local node - */ -internal class DevP2PPeerRoutingTable(selfId: SECP256K1.PublicKey) : PeerRoutingTable { - - private val idHashCache: Cache = - CacheBuilder.newBuilder().maximumSize((1L + 256) * 16).weakKeys().build() - - private val table = KademliaRoutingTable( - selfId = hashForId(selfId)!!, - k = DEVP2P_BUCKET_SIZE, - nodeId = { p -> hashForId(p.nodeId)!! }, - ) - - override val size: Int - get() = table.size - - override fun contains(element: Peer): Boolean = table.contains(element) - - override fun containsAll(elements: Collection): Boolean = table.containsAll(elements) - - override fun isEmpty(): Boolean = table.isEmpty() - - override fun iterator(): Iterator = table.iterator() - - override fun nearest(targetId: SECP256K1.PublicKey, limit: Int): List = - hashForId(targetId)?.let { table.nearest(it, limit) } ?: listOf() - - override fun add(node: Peer): Peer? = table.add(node) - - override fun evict(node: Peer): Boolean = table.evict(node) - - private fun hashForId(id: SECP256K1.PublicKey): ByteArray? { - try { - return idHashCache.get(id) { EthereumNodeRecord.nodeId(id).toArrayUnsafe() } - } catch (e: UncheckedExecutionException) { - if (e.cause is IllegalArgumentException) { - return null - } - throw e - } - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/DiscoveryV5Service.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/DiscoveryV5Service.kt deleted file mode 100644 index 376162c61..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/DiscoveryV5Service.kt +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import io.vertx.core.Vertx -import io.vertx.core.buffer.Buffer -import io.vertx.core.datagram.DatagramPacket -import io.vertx.core.net.SocketAddress -import io.vertx.kotlin.coroutines.coAwait -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.launch -import org.apache.tuweni.concurrent.AsyncCompletion -import org.apache.tuweni.concurrent.AsyncResult -import org.apache.tuweni.concurrent.ExpiringMap -import org.apache.tuweni.concurrent.coroutines.asyncCompletion -import org.apache.tuweni.concurrent.coroutines.asyncResult -import org.apache.tuweni.concurrent.coroutines.await -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.Hash -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import org.apache.tuweni.v2.devp2p.Packet -import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKey -import org.apache.tuweni.v2.devp2p.v5.topic.TopicTable -import org.apache.tuweni.v2.io.Base64URLSafe -import org.slf4j.LoggerFactory -import java.net.InetAddress -import java.net.InetSocketAddress -import java.nio.ByteBuffer -import java.time.Instant -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.coroutines.CoroutineContext - -/** - * A creator of discovery service objects. - */ -object DiscoveryService { - - /** - * Creates a new discovery service, generating the node ENR and configuring the UDP connector. - * @param vertx Vert.x instance - * @param keyPair the key pair identifying the node running the service. - * @param bindAddress the address to bind the node to. - * @param enrSeq the sequence of the ENR of the node - * @param bootstrapENRList the list of other nodes to connect to on bootstrap. - * @param enrStorage the permanent storage of ENRs. Defaults to an in-memory store. - * @param coroutineContext the coroutine context associated with the store. - */ - @JvmStatic - @JvmOverloads - fun open( - vertx: Vertx, - keyPair: SECP256K1.KeyPair, - localPort: Int, - bindAddress: InetSocketAddress = InetSocketAddress(InetAddress.getLoopbackAddress(), localPort), - enrSeq: Long = Instant.now().toEpochMilli(), - bootstrapENRList: List = emptyList(), - enrStorage: ENRStorage = DefaultENRStorage(), - coroutineContext: CoroutineContext = Dispatchers.Default, - ): DiscoveryV5Service { - val selfENR = EthereumNodeRecord.create( - keyPair, - enrSeq, - emptyMap(), - emptyMap(), - bindAddress.address, - bindAddress.port, // TODO allow override - bindAddress.port, - ) - // val connector = UdpConnector(bindAddress, keyPair, selfENR, enrStorage) - return DefaultDiscoveryV5Service( - vertx, - bindAddress, - bootstrapENRList, - enrStorage, - keyPair, - selfENR, - coroutineContext = coroutineContext, - ) - } -} - -/** - * Service executes network discovery, according to discv5 specification - * (https://github.com/ethereum/devp2p/blob/master/discv5/discv5.md) - */ -interface DiscoveryV5Service : CoroutineScope { - - /** - * Starts the node discovery service. - */ - suspend fun start(): AsyncCompletion - - /** - * Stops the node discovery service. - */ - suspend fun terminate() - - /** - * Starts the discovery service, providing a handle to the completion of the start operation. - */ - fun startAsync() = asyncCompletion { start() } - - /** - * Stops the node discovery service, providing a handle to the completion of the shutdown operation. - */ - fun terminateAsync() = asyncCompletion { terminate() } - - /** - * Provides the ENR identifying the service. - */ - fun enr(): EthereumNodeRecord - - /** - * Adds a peer to the routing table. - * - * @param rlpENR the RLP representation of the peer ENR. - */ - suspend fun addPeer(rlpENR: Bytes): AsyncCompletion { - val enr: EthereumNodeRecord = EthereumNodeRecord.fromRLP(rlpENR) - return addPeer(enr) - } - - /** - * Adds a peer to the routing table. - * - * @param enr the peer Ethereum Node Record - * @param address optionally, the UDP address to call for this peer. - */ - suspend fun addPeer( - enr: EthereumNodeRecord, - address: SocketAddress = SocketAddress.inetSocketAddress(enr.udp()!!, enr.ip().hostAddress), - ): AsyncCompletion - - /** - * Requests nodes from all connected peers. - * - * @param distance the distance between the node and the peer. Helps pick a Kademlia bucket. - * @param maxSecondsToWait number of seconds to wait for a response. - */ - suspend fun requestNodes( - distance: Int = 1, - maxSecondsToWait: Long = 10, - ): AsyncResult>> -} - -internal class DefaultDiscoveryV5Service( - vertx: Vertx, - private val bindAddress: InetSocketAddress, - private val bootstrapENRList: List, - private val enrStorage: ENRStorage, - private val keyPair: SECP256K1.KeyPair, - private val selfEnr: EthereumNodeRecord, - private val routingTable: RoutingTable = RoutingTable(selfEnr), - private val topicTable: TopicTable = TopicTable(), - override val coroutineContext: CoroutineContext = Dispatchers.Default, -) : DiscoveryV5Service { - - companion object { - private val logger = LoggerFactory.getLogger(DefaultDiscoveryV5Service::class.java) - } - - private val server = vertx.createDatagramSocket() - private val handshakes = ExpiringMap() - private val sessions = ConcurrentHashMap() - private val started = AtomicBoolean(false) - private val nodeId = EthereumNodeRecord.nodeId(keyPair.publicKey()) - private val whoAreYouHeader = Hash.sha2_256(Bytes.wrap(nodeId, Bytes.wrap("WHOAREYOU".toByteArray()))) - - private lateinit var receiveJob: Job - - override suspend fun start(): AsyncCompletion { - server.handler(this::receiveDatagram).listen(bindAddress.port, bindAddress.hostString).coAwait() - return bootstrap() - } - - override suspend fun terminate() { - if (started.compareAndSet(true, false)) { - receiveJob.cancel() - server.close().coAwait() - } - } - - override fun enr(): EthereumNodeRecord = selfEnr - - override suspend fun addPeer(enr: EthereumNodeRecord, address: SocketAddress): AsyncCompletion { - val session = sessions[address] - if (session == null) { - logger.trace("Creating new session for peer {}", enr) - val handshakeSession = handshakes.computeIfAbsent(address) { addr -> createHandshake(addr, enr.publicKey(), enr) } - return asyncCompletion { - logger.trace("Handshake connection start {}", enr) - handshakeSession.connect().await() - logger.trace("Handshake connection done {}", enr) - } - } else { - logger.trace("Session found for peer {}", enr) - return AsyncCompletion.completed() - } - } - - private fun send(addr: SocketAddress, message: Bytes) { - launch { - server.send(Buffer.buffer(message.toArrayUnsafe()), addr.port(), addr.host()).coAwait() - } - } - - private suspend fun bootstrap(): AsyncCompletion = AsyncCompletion.allOf( - bootstrapENRList.map { - logger.trace("Connecting to bootstrap peer {}", it) - var encodedEnr = it - if (it.startsWith("enr:")) { - encodedEnr = it.substringAfter("enr:") - } - val rlpENR = Base64URLSafe.decode(encodedEnr) - addPeer(rlpENR) - }, - ) - - private fun receiveDatagram(packet: DatagramPacket) { - var session = sessions[packet.sender()] - val size = Packet.MAX_SIZE.coerceAtMost(packet.data().length()) - val buffer = ByteBuffer.allocate(size) - buffer.put(packet.data().bytes) - buffer.flip() - val message = Bytes.wrapByteBuffer(buffer) - if (message.slice(0, 32) == whoAreYouHeader && session != null) { - sessions.remove(packet.sender()) - session = null - } - if (session == null) { - val handshakeSession = - handshakes.computeIfAbsent(packet.sender()) { createHandshake(it) } - launch { - handshakeSession.processMessage(message) - } - } else { - launch { - session.processMessage(message) - } - } - } - - private fun createHandshake( - address: SocketAddress, - publicKey: SECP256K1.PublicKey? = null, - receivedEnr: EthereumNodeRecord? = null, - ): HandshakeSession { - logger.trace("Creating new handshake with {}", address) - val newSession = HandshakeSession(keyPair, address, publicKey, this::send, this::enr, coroutineContext) - newSession.awaitConnection().thenAccept { - val peerEnr = receivedEnr ?: newSession.receivedEnr!! - logger.trace("Handshake connection done {}", peerEnr) - val session = createSession(newSession, address, it, peerEnr) - newSession.requestId?.let { requestId -> - session.activeFindNodes[requestId] = AsyncResult.incomplete() - } - }.exceptionally { logger.error("Error during connection", it) } - return newSession - } - - private fun createSession( - newSession: HandshakeSession, - address: SocketAddress, - sessionKey: SessionKey, - receivedEnr: EthereumNodeRecord, - ): Session { - val session = Session( - receivedEnr, - keyPair, - newSession.nodeId, - newSession.tag(), - sessionKey, - address, - this::send, - this::enr, - routingTable, - topicTable, - { missedPings -> - missedPings > 5 - }, - coroutineContext, - ) - logger.trace("Adding ENR discovered by connecting to peer") - enrStorage.set(receivedEnr) - sessions[address] = session - return session - } - - override suspend fun requestNodes( - distance: Int, - maxSecondsToWait: Long, - ): AsyncResult>> = - asyncResult { - val results = ConcurrentHashMap>() - logger.debug("Requesting from ${sessions.size} sessions with distance $distance") - sessions.values.map { session -> - async { - try { - val oneResult = session.sendFindNodes(distance).get(maxSecondsToWait, TimeUnit.SECONDS) - logger.debug("Received ${oneResult!!.size} results") - results.put(session.enr, oneResult) - } catch (e: Exception) { - logger.debug("Timeout waiting for nodes") - } - } - }.awaitAll() - results - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/ENRStorage.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/ENRStorage.kt deleted file mode 100644 index c5be90dc9..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/ENRStorage.kt +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.Hash -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import java.util.concurrent.ConcurrentHashMap - -/** - * Storage of node records - */ -interface ENRStorage { - - /** - * Add an ENR record to the store - * - * @param enr node record - */ - fun set(enr: EthereumNodeRecord) { - val nodeId = Hash.sha2_256(enr.toRLP()) - put(nodeId, enr) - } - - /** - * Store an ENR record associated with a nodeId in the store. - */ - fun put(nodeId: Bytes, enr: EthereumNodeRecord) - - /** - * Find a stored node record - * - * @param nodeId node identifier - * - * @return node record, if present. - */ - fun find(nodeId: Bytes): EthereumNodeRecord? -} - -/** - * Default storage for Ethereum Node Records, backed by an in-memory hash map. - */ -internal class DefaultENRStorage : ENRStorage { - - private val storage: MutableMap = ConcurrentHashMap() - - override fun find(nodeId: Bytes): EthereumNodeRecord? = storage[nodeId] - - override fun put(nodeId: Bytes, enr: EthereumNodeRecord) { storage.put(nodeId, enr) } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSession.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSession.kt deleted file mode 100644 index b12e601d3..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSession.kt +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import io.vertx.core.net.SocketAddress -import kotlinx.coroutines.CoroutineScope -import org.apache.tuweni.concurrent.AsyncResult -import org.apache.tuweni.concurrent.CompletableAsyncResult -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.Hash -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import org.apache.tuweni.v2.devp2p.v5.encrypt.AES128GCM -import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKey -import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKeyGenerator -import org.apache.tuweni.v2.rlp.RLP -import org.apache.tuweni.v2.rlp.RLPReader -import org.apache.tuweni.v2.units.bigints.UInt256 -import org.slf4j.LoggerFactory -import kotlin.coroutines.CoroutineContext - -private val DISCOVERY_ID_NONCE: Bytes = Bytes.wrap("discovery-id-nonce".toByteArray()) - -internal class HandshakeSession( - private val keyPair: SECP256K1.KeyPair, - private val address: SocketAddress, - private var publicKey: SECP256K1.PublicKey? = null, - private val sendFn: (SocketAddress, Bytes) -> Unit, - private val enr: () -> EthereumNodeRecord, - override val coroutineContext: CoroutineContext, -) : CoroutineScope { - - var requestId: Bytes? = null - private val connected: CompletableAsyncResult = AsyncResult.incomplete() - var receivedEnr: EthereumNodeRecord? = null - val nodeId = EthereumNodeRecord.nodeId(keyPair.publicKey()) - private val whoAreYouHeader = Hash.sha2_256(Bytes.wrap(nodeId, Bytes.wrap("WHOAREYOU".toByteArray()))) - - private val tokens = ArrayList() - - companion object { - private val logger = LoggerFactory.getLogger(HandshakeSession::class.java) - } - - suspend fun connect(): AsyncResult { - val message = RandomMessage() - tokens.add(message.authTag) - val tag = tag() - val rlpAuthTag = RLP.encodeValue(message.authTag) - val content = Bytes.wrap(tag, rlpAuthTag, message.toRLP()) - logger.trace("Sending random packet {} {}", address, content) - sendFn(address, content) - return connected - } - - suspend fun processMessage(messageBytes: Bytes) { - if (messageBytes.size() > Message.MAX_UDP_MESSAGE_SIZE) { - logger.trace("Message too long, dropping from {}", address) - return - } - if (messageBytes.size() < 32) { - logger.trace("Message too short, dropping from {}", address) - } - - logger.trace("Received message from {}", address) - val tag = messageBytes.slice(0, 32) - val content = messageBytes.slice(32) - // it's either a WHOAREYOU or a RANDOM message. - if (whoAreYouHeader == tag) { - logger.trace("Identified a WHOAREYOU message") - val message = WhoAreYouMessage.create(tag, content) - if (!this.tokens.contains(message.token)) { - // We were not expecting this WHOAREYOU. - logger.trace("Unexpected WHOAREYOU packet {}", message.token) - return - } - // Use the WHOAREYOU info to send handshake. - // Generate ephemeral key pair - val ephemeralKeyPair = SECP256K1.KeyPair.random() - val ephemeralKey = ephemeralKeyPair.secretKey() - - val destNodeId = EthereumNodeRecord.nodeId(publicKey!!) - val secret = SECP256K1.deriveECDHKeyAgreement(ephemeralKey.bytes(), publicKey!!.bytes()) - - // Derive keys - val newSession = SessionKeyGenerator.generate(nodeId, destNodeId, secret, message.idNonce) - val signValue = Bytes.wrap(DISCOVERY_ID_NONCE, message.idNonce, ephemeralKeyPair.publicKey().bytes()) - val signature = SECP256K1.signHashed(Hash.sha2_256(signValue), keyPair) - val plain = RLP.encodeList { writer -> - writer.writeInt(5) - writer.writeValue( - Bytes.wrap( - UInt256.valueOf(signature.r()), - UInt256.valueOf(signature.s()), - ), - ) - writer.writeRLP(enr().toRLP()) - } - val zeroNonce = Bytes.wrap(ByteArray(12)) - val authResponse = AES128GCM.encrypt(newSession.authRespKey, zeroNonce, plain, Bytes.EMPTY) - val authTag = Message.authTag() - val newTag = tag() - val findNode = FindNodeMessage() - requestId = findNode.requestId - val encryptedMessage = AES128GCM.encrypt( - newSession.initiatorKey, - authTag, - Bytes.wrap(Bytes.of(MessageType.FINDNODE.byte()), findNode.toRLP()), - newTag, - ) - val response = Bytes.wrap( - newTag, - RLP.encodeList { - it.writeValue(authTag) - it.writeValue(message.idNonce) - it.writeValue(Bytes.wrap("gcm".toByteArray())) - it.writeValue(ephemeralKeyPair.publicKey().bytes()) - it.writeValue(authResponse) - }, - encryptedMessage, - ) - logger.trace("Sending handshake FindNode {}", response) - connected.complete(newSession) - sendFn(address, response) - } else { - // connection initiated by the peer. - // try to see if this a message with a header we can read: - val hasHeader = RLP.decode(content, RLPReader::nextIsList) - if (hasHeader) { - logger.trace("Identified a valid message") - RLP.decodeList(content) { - it.skipNext() - val idNonce = it.readValue() - it.skipNext() - val ephemeralPublicKey = SECP256K1.PublicKey.fromBytes(it.readValue()) - val authResponse = it.readValue() - - val secret = SECP256K1.deriveECDHKeyAgreement(keyPair.secretKey().bytes(), ephemeralPublicKey.bytes()) - val senderNodeId = Message.getSourceFromTag(tag, nodeId) - val sessionKey = SessionKeyGenerator.generate(senderNodeId, nodeId, secret, idNonce) - val decryptedAuthResponse = - Bytes.wrap(AES128GCM.decrypt(sessionKey.authRespKey, Bytes.wrap(ByteArray(12)), authResponse, Bytes.EMPTY)) - RLP.decodeList(decryptedAuthResponse) { reader -> - reader.skipNext() - val signatureBytes = reader.readValue() - val enr = reader.readList { enrReader -> EthereumNodeRecord.fromRLP(enrReader) } - receivedEnr = enr - publicKey = enr.publicKey() - val signatureVerified = verifySignature(signatureBytes, idNonce, ephemeralPublicKey, enr.publicKey()) - if (!signatureVerified) { - throw IllegalArgumentException("Signature is not verified") - } - logger.trace("Finalized handshake") - connected.complete(sessionKey) - } - } - } else { - logger.trace("Identified a RANDOM message") - val token = RLP.decodeValue(content) - val peerNodeId = Message.getSourceFromTag(tag, nodeId) - logger.trace("Found peerNodeId $peerNodeId") - // Build a WHOAREYOU message with the tag of the random message. - val whoAreYouTag = Hash.sha2_256(Bytes.wrap(peerNodeId, Bytes.wrap("WHOAREYOU".toByteArray()))) - val response = WhoAreYouMessage(whoAreYouTag, token, Message.idNonce(), enr().seq()) - this.tokens.add(token) - logger.trace("Sending WHOAREYOU to {}", address) - sendFn(address, response.toRLP()) - } - } - } - - private fun verifySignature( - signatureBytes: Bytes, - idNonce: Bytes, - ephemeralPublicKey: SECP256K1.PublicKey, - publicKey: SECP256K1.PublicKey, - ): Boolean { - val signature = SECP256K1.Signature.create( - 1, - signatureBytes.slice(0, 32).toUnsignedBigInteger(), - signatureBytes.slice(32).toUnsignedBigInteger(), - ) - - val signValue = Bytes.wrap(DISCOVERY_ID_NONCE, idNonce, ephemeralPublicKey.bytes()) - val hashedSignValue = Hash.sha2_256(signValue) - if (!SECP256K1.verifyHashed(hashedSignValue, signature, publicKey)) { - val signature0 = SECP256K1.Signature.create( - 0, - signatureBytes.slice(0, 32).toUnsignedBigInteger(), - signatureBytes.slice(32).toUnsignedBigInteger(), - ) - return SECP256K1.verifyHashed(hashedSignValue, signature0, publicKey) - } else { - return true - } - } - - fun awaitConnection(): AsyncResult = connected - - fun tag() = Message.tag(nodeId, EthereumNodeRecord.nodeId(publicKey!!)) -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Message.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Message.kt deleted file mode 100644 index 0aa35916e..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Message.kt +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.Hash - -/** - * Discovery message sent over UDP. - */ -internal interface Message { - - companion object { - - const val MAX_UDP_MESSAGE_SIZE = 1280 - const val TAG_LENGTH: Int = 32 - const val AUTH_TAG_LENGTH: Int = 12 - const val RANDOM_DATA_LENGTH: Int = 44 - const val ID_NONCE_LENGTH: Int = 32 - const val REQUEST_ID_LENGTH: Int = 8 - - private val WHO_ARE_YOU: Bytes = Bytes.wrap("WHOAREYOU".toByteArray()) - - fun magic(dest: Bytes): Bytes { - return Hash.sha2_256(Bytes.wrap(dest, WHO_ARE_YOU)) - } - - fun tag(src: Bytes, dest: Bytes): Bytes { - val encodedDestKey = Hash.sha2_256(dest) - return encodedDestKey.mutableCopy().xor(src) - } - - fun getSourceFromTag(tag: Bytes, dest: Bytes): Bytes { - val encodedDestKey = Hash.sha2_256(dest) - return encodedDestKey.mutableCopy().xor(tag) - } - - fun requestId(): Bytes = Bytes.random(REQUEST_ID_LENGTH) - - fun authTag(): Bytes = Bytes.random(AUTH_TAG_LENGTH) - - fun idNonce(): Bytes = Bytes.random(ID_NONCE_LENGTH) - } - - fun toRLP(): Bytes - - fun type(): MessageType -} - -internal enum class MessageType(val code: Int) { - RANDOM(0), - WHOAREYOU(0), - FINDNODE(3), - NODES(4), - PING(1), - PONG(2), - REGTOPIC(5), - REGCONFIRM(7), - TICKET(6), - TOPICQUERY(8), - ; - - fun byte(): Byte = code.toByte() - - companion object { - fun valueOf(code: Int): MessageType { - for (messageType in MessageType.values()) { - if (messageType.code == code) { - return messageType - } - } - throw IllegalArgumentException("No known message with code $code") - } - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Messages.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Messages.kt deleted file mode 100644 index bef28ebbc..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Messages.kt +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import org.apache.tuweni.v2.rlp.RLP -import java.net.InetAddress - -internal class FindNodeMessage( - val requestId: Bytes = Message.requestId(), - val distance: Int = 0, -) : Message { - - private val encodedMessageType: Bytes = Bytes.fromHexString("0x03") - - override fun toRLP(): Bytes { - return RLP.encodeList { writer -> - writer.writeValue(requestId) - writer.writeInt(distance) - } - } - - override fun type(): MessageType = MessageType.FINDNODE - - companion object { - fun create(content: Bytes): FindNodeMessage { - return RLP.decodeList(content) { reader -> - val requestId = reader.readValue() - val distance = reader.readInt() - return@decodeList FindNodeMessage(requestId, distance) - } - } - } -} - -internal class NodesMessage( - val requestId: Bytes = Message.requestId(), - val total: Int, - val nodeRecords: List, -) : Message { - - private val encodedMessageType: Bytes = Bytes.fromHexString("0x04") - - override fun type(): MessageType = MessageType.NODES - - override fun toRLP(): Bytes { - return RLP.encodeList { writer -> - writer.writeValue(requestId) - writer.writeInt(total) - writer.writeList(nodeRecords) { listWriter, it -> - listWriter.writeRLP(it.toRLP()) - } - } - } - - companion object { - fun create(content: Bytes): NodesMessage { - return RLP.decodeList(content) { reader -> - val requestId = reader.readValue() - val total = reader.readInt() - val nodeRecords = reader.readListContents { listReader -> - listReader.readList { enrReader -> - EthereumNodeRecord.fromRLP(enrReader) - } - } - return@decodeList NodesMessage(requestId, total, nodeRecords) - } - } - } -} - -internal class PingMessage( - val requestId: Bytes = Message.requestId(), - val enrSeq: Long = 0, -) : Message { - - private val encodedMessageType: Bytes = Bytes.fromHexString("0x01") - - override fun type(): MessageType = MessageType.PING - - override fun toRLP(): Bytes { - return RLP.encodeList { reader -> - reader.writeValue(requestId) - reader.writeLong(enrSeq) - } - } - - companion object { - fun create(content: Bytes): PingMessage { - return RLP.decodeList(content) { reader -> - val requestId = reader.readValue() - val enrSeq = reader.readLong() - return@decodeList PingMessage(requestId, enrSeq) - } - } - } -} - -internal class RandomMessage( - val authTag: Bytes = Message.authTag(), - val data: Bytes = randomData(), -) : Message { - - companion object { - fun randomData(): Bytes = Bytes.random(Message.RANDOM_DATA_LENGTH) - - fun create(authTag: Bytes, content: Bytes = randomData()): RandomMessage { - return RandomMessage(authTag, content) - } - } - - override fun type(): MessageType = MessageType.RANDOM - - override fun toRLP(): Bytes { - return data - } -} - -internal class TicketMessage( - val requestId: Bytes = Message.requestId(), - val ticket: Bytes, - val waitTime: Long, -) : Message { - - override fun type(): MessageType = MessageType.TICKET - - override fun toRLP(): Bytes { - return RLP.encodeList { writer -> - writer.writeValue(requestId) - writer.writeValue(ticket) - writer.writeLong(waitTime) - } - } - - companion object { - fun create(content: Bytes): TicketMessage { - return RLP.decodeList(content) { reader -> - val requestId = reader.readValue() - val ticket = reader.readValue() - val waitTime = reader.readLong() - return@decodeList TicketMessage(requestId, ticket, waitTime) - } - } - } -} - -internal class WhoAreYouMessage( - val magic: Bytes, - val token: Bytes, - val idNonce: Bytes, - val enrSeq: Long = 0, -) : Message { - - companion object { - fun create(magic: Bytes, content: Bytes): WhoAreYouMessage { - return RLP.decodeList(content) { r -> - val token = r.readValue() - val idNonce = r.readValue() - val enrSeq = r.readValue() - WhoAreYouMessage(magic = magic, token = token, idNonce = idNonce, enrSeq = enrSeq.toLong()) - } - } - } - - override fun type(): MessageType = MessageType.WHOAREYOU - - override fun toRLP(): Bytes { - return Bytes.wrap( - magic, - RLP.encodeList { w -> - w.writeValue(token) - w.writeValue(idNonce) - w.writeLong(enrSeq) - }, - ) - } -} - -internal class TopicQueryMessage( - val requestId: Bytes = Message.requestId(), - val topic: Bytes, -) : Message { - - private val encodedMessageType: Bytes = Bytes.fromHexString("0x08") - - override fun type(): MessageType = MessageType.TOPICQUERY - - override fun toRLP(): Bytes { - return RLP.encodeList { writer -> - writer.writeValue(requestId) - writer.writeValue(topic) - } - } - - companion object { - fun create(content: Bytes): TopicQueryMessage { - return RLP.decodeList(content) { reader -> - val requestId = reader.readValue() - val topic = reader.readValue() - return@decodeList TopicQueryMessage(requestId, topic) - } - } - } -} - -/** - * Message to register a topic. - */ -internal class RegTopicMessage( - val requestId: Bytes = Message.requestId(), - val nodeRecord: EthereumNodeRecord, - val topic: Bytes, - val ticket: Bytes, -) : Message { - - private val encodedMessageType: Bytes = Bytes.fromHexString("0x05") - - override fun type(): MessageType = MessageType.REGTOPIC - - override fun toRLP(): Bytes { - return RLP.encodeList { writer -> - writer.writeValue(requestId) - writer.writeRLP(nodeRecord.toRLP()) - writer.writeValue(topic) - writer.writeValue(ticket) - } - } - - companion object { - fun create(content: Bytes): RegTopicMessage { - return RLP.decodeList(content) { reader -> - val requestId = reader.readValue() - val nodeRecord = reader.readList { enrReader -> - EthereumNodeRecord.fromRLP(enrReader) - } - val topic = reader.readValue() - val ticket = reader.readValue() - return@decodeList RegTopicMessage(requestId, nodeRecord, topic, ticket) - } - } - } -} - -internal class PongMessage( - val requestId: Bytes = Message.requestId(), - val enrSeq: Long = 0, - val recipientIp: String, - val recipientPort: Int, -) : Message { - - private val encodedMessageType: Bytes = Bytes.fromHexString("0x02") - - override fun type(): MessageType = MessageType.PONG - - override fun toRLP(): Bytes { - return RLP.encodeList { writer -> - writer.writeValue(requestId) - writer.writeLong(enrSeq) - - val bytesIp = Bytes.wrap(InetAddress.getByName(recipientIp).address) - writer.writeValue(bytesIp) - writer.writeInt(recipientPort) - } - } - - companion object { - fun create(content: Bytes): PongMessage { - return RLP.decodeList(content) { reader -> - val requestId = reader.readValue() - val enrSeq = reader.readLong() - val address = InetAddress.getByAddress(reader.readValue().toArrayUnsafe()) - val recipientPort = reader.readInt() - return@decodeList PongMessage(requestId, enrSeq, address.hostAddress, recipientPort) - } - } - } -} - -internal class RegConfirmationMessage( - val requestId: Bytes = Message.requestId(), - val topic: Bytes, -) : Message { - - private val encodedMessageType: Bytes = Bytes.fromHexString("0x07") - - override fun type(): MessageType = MessageType.REGCONFIRM - - override fun toRLP(): Bytes { - return RLP.encodeList { writer -> - writer.writeValue(requestId) - writer.writeValue(topic) - } - } - - companion object { - fun create(content: Bytes): RegConfirmationMessage { - return RLP.decodeList(content) { reader -> - val requestId = reader.readValue() - val topic = reader.readValue() - return@decodeList RegConfirmationMessage(requestId, topic) - } - } - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/RoutingTable.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/RoutingTable.kt deleted file mode 100644 index 5dbc252da..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/RoutingTable.kt +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import com.google.common.math.IntMath -import org.apache.tuweni.kademlia.KademliaRoutingTable -import org.apache.tuweni.kademlia.xorDist -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import java.math.RoundingMode - -internal class RoutingTable( - private val selfEnr: EthereumNodeRecord, -) { - - private val selfNodeId = EthereumNodeRecord.nodeId(selfEnr.publicKey()).toArrayUnsafe() - private val nodeIdCalculation: (Bytes) -> ByteArray = { enr -> key(enr) } - - private val table = KademliaRoutingTable( - selfId = selfNodeId, - k = BUCKET_SIZE, - nodeId = nodeIdCalculation, - distanceToSelf = { - val xorResult = key(it) xorDist selfNodeId - if (xorResult == 0) 0 else IntMath.log2(xorResult, RoundingMode.FLOOR) - }, - ) - - val size: Int - get() = table.size - - fun getSelfEnr(): EthereumNodeRecord = selfEnr - - fun add(enr: EthereumNodeRecord) { - add(enr.toRLP()) - } - - fun add(enr: Bytes) { - if (enr != selfEnr.toRLP()) { - table.add(enr) - } - } - - fun distanceToSelf(targetId: Bytes): Int = table.logDistToSelf(targetId) - - fun evict(enr: Bytes): Boolean = table.evict(enr) - - fun random(): Bytes = table.getRandom() - - fun isEmpty(): Boolean = table.isEmpty() - - fun nodesOfDistance(distance: Int): List = - table.peersOfDistance(distance).map { EthereumNodeRecord.fromRLP(it) } - - fun clear() = table.clear() - - private fun key(enr: Bytes): ByteArray = EthereumNodeRecord.fromRLP(enr).nodeId().toArrayUnsafe() - - companion object { - private const val BUCKET_SIZE: Int = 16 - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Session.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Session.kt deleted file mode 100644 index 338ab7308..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/Session.kt +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import io.vertx.core.net.SocketAddress -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.apache.tuweni.concurrent.AsyncCompletion -import org.apache.tuweni.concurrent.AsyncResult -import org.apache.tuweni.concurrent.CompletableAsyncCompletion -import org.apache.tuweni.concurrent.CompletableAsyncResult -import org.apache.tuweni.concurrent.ExpiringMap -import org.apache.tuweni.rlp.InvalidRLPTypeException -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.devp2p.DiscoveryService -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import org.apache.tuweni.v2.devp2p.v5.encrypt.AES128GCM -import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKey -import org.apache.tuweni.v2.devp2p.v5.topic.Ticket -import org.apache.tuweni.v2.devp2p.v5.topic.Topic -import org.apache.tuweni.v2.devp2p.v5.topic.TopicTable -import org.apache.tuweni.v2.rlp.RLP -import org.apache.tuweni.v2.rlp.RLPReader -import org.slf4j.LoggerFactory -import java.lang.UnsupportedOperationException -import kotlin.coroutines.CoroutineContext - -private const val MAX_NODES_IN_RESPONSE: Int = 4 -private const val WHO_ARE_YOU_MESSAGE_LENGTH = 48 -private const val SEND_REGTOPIC_DELAY_MS = 15 * 60 * 1000L // 15 min - -/** - * Tracks a session with another peer. - */ -internal class Session( - val enr: EthereumNodeRecord, - private val keyPair: SECP256K1.KeyPair, - private val nodeId: Bytes, - private val tag: Bytes, - private val sessionKey: SessionKey, - private val address: SocketAddress, - private val sendFn: (SocketAddress, Bytes) -> Unit, - private val ourENR: () -> EthereumNodeRecord, - private val routingTable: RoutingTable, - private val topicTable: TopicTable, - private val failedPingsListener: (missedPings: Int) -> Boolean, - override val coroutineContext: CoroutineContext, -) : CoroutineScope { - - companion object { - private val logger = LoggerFactory.getLogger(Session::class.java) - - const val PING_REFRESH = 10000L - } - - val activeFindNodes = HashMap>>() - private var activePing: CompletableAsyncCompletion? = null - private val chunkedNodeResults = ExpiringMap>() - private var missedPings = 0 - private val ticketHolder = HashMap() - private var peerSeq: Long = -1 - - private fun launchPing() { - launch { - delay(PING_REFRESH) - sendPing() - } - } - - private suspend fun sendPing(): AsyncCompletion { - activePing?.let { - if (!it.isDone) { - it.cancel() - } - } - val newPing = AsyncCompletion.incomplete() - newPing.exceptionally { - missedPings++ - if (!failedPingsListener(missedPings)) { - launchPing() - } - } - newPing.thenRun { - this.missedPings = 0 - launchPing() - } - activePing = newPing - send(PingMessage()) - return newPing - } - - suspend fun sendFindNodes(distance: Int): AsyncResult> { - val message = FindNodeMessage(distance = distance) - val result: CompletableAsyncResult> = AsyncResult.incomplete() - activeFindNodes[message.requestId] = result - send(message) - return result - } - - suspend fun processMessage(messageBytes: Bytes) { - if (messageBytes.size() > Message.MAX_UDP_MESSAGE_SIZE) { - logger.trace("Message too long, dropping from {}", address) - return - } - logger.trace("Received message from {}", address) - - var message: Message - try { - message = decode(messageBytes) - } catch (e: InvalidRLPTypeException) { - logger.trace("Bad message content, dropping from {}: {}", address, messageBytes) - return - } - logger.trace("Received message of type {}", message.type()) - when (message.type()) { - MessageType.FINDNODE -> handleFindNode(message as FindNodeMessage) - MessageType.NODES -> handleNodes(message as NodesMessage) - MessageType.PING -> handlePing(message as PingMessage) - MessageType.PONG -> handlePong(message as PongMessage) - MessageType.REGTOPIC -> handleRegTopic( - message as RegTopicMessage, - ) - MessageType.REGCONFIRM -> handleRegConfirmation( - message as RegConfirmationMessage, - ) - MessageType.TICKET -> handleTicket(message as TicketMessage) - MessageType.TOPICQUERY -> handleTopicQuery(message as TopicQueryMessage) - MessageType.RANDOM -> throw UnsupportedOperationException("random") - MessageType.WHOAREYOU -> throw UnsupportedOperationException("whoareyou") - } - } - - private suspend fun handleTopicQuery(message: TopicQueryMessage) { - val nodes = topicTable.getNodes(Topic(message.topic.toHexString())) - - for (chunk in nodes.chunked(MAX_NODES_IN_RESPONSE)) { - val response = NodesMessage(message.requestId, nodes.size, chunk) - send(response) - } - } - - private suspend fun handlePong( - message: PongMessage, - ) { - if (activePing?.isDone == true) { - logger.trace("Received pong when no ping was active") - return - } - if (peerSeq != message.enrSeq) { - val request = FindNodeMessage(message.requestId) - send(request) - } - activePing?.complete() - } - - private suspend fun handlePing( - message: PingMessage, - ) { - activePing = AsyncCompletion.incomplete() - val response = - PongMessage(message.requestId, ourENR().seq(), address.host(), address.port()) - send(response) - } - - private val now: () -> Long = DiscoveryService.CURRENT_TIME_SUPPLIER - - private suspend fun handleNodes(message: NodesMessage) { - if (activeFindNodes[message.requestId] == null) { - logger.trace("Received NODES message but no matching FINDNODES present. Dropping") - return - } - val enrs = message.nodeRecords - val records = chunkedNodeResults.computeIfAbsent(message.requestId) { mutableListOf() } - records.addAll(enrs) - logger.debug("Received ${enrs.size} for ${records.size}/${message.total}") - - // there seems to be a bug where no nodes are sent yet the total is above 0. - if ((records.size == 0 && message.total != 0) || records.size >= message.total) { - activeFindNodes[message.requestId]?.let { - it.complete(chunkedNodeResults[message.requestId]) - chunkedNodeResults.remove(message.requestId) - activeFindNodes.remove(message.requestId) - } - } - } - - private suspend fun handleTicket(message: TicketMessage) { - ticketHolder.put(message.requestId, message.ticket) - - if (message.waitTime != 0L) { - val ticket = Ticket.decrypt(message.ticket, sessionKey.initiatorKey) - delayRegTopic(message.requestId, ticket.topic, message.waitTime) - } - } - - private suspend fun handleRegTopic( - message: RegTopicMessage, - ) { - val topic = Topic(message.topic.toHexString()) - - val existingTicket = if (!message.ticket.isEmpty) { - val ticket = Ticket.decrypt(message.ticket, sessionKey.initiatorKey) - ticket.validate(nodeId, address.host(), now(), message.topic) - ticket - } else { - null - } - - // Create new ticket - val waitTime = topicTable.put(topic, message.nodeRecord) - val cumTime = (existingTicket?.cumTime ?: waitTime) + waitTime - val ticket = Ticket(message.topic, nodeId, address.host(), now(), waitTime, cumTime) - val encryptedTicket = ticket.encrypt(sessionKey.initiatorKey) - - // Send ticket - val response = TicketMessage(message.requestId, encryptedTicket, waitTime) - sendFn(address, response.toRLP()) - - // Send confirmation if topic was placed - if (waitTime == 0L) { - val confirmation = RegConfirmationMessage(message.requestId, message.topic) - send(confirmation) - } - } - - private suspend fun handleRegConfirmation(message: RegConfirmationMessage) { - ticketHolder.remove(message.requestId) - registerTopic(message.topic, true) - } - - private suspend fun send(message: Message) { - logger.trace("Sending an encrypted message of type {}", message.type()) - val messagePlain = Bytes.wrap(Bytes.of(message.type().byte()), message.toRLP()) - val authTag = Message.authTag() - val encryptionResult = AES128GCM.encrypt(sessionKey.initiatorKey, authTag, messagePlain, tag) - sendFn(address, Bytes.wrap(tag, RLP.encodeValue(authTag), encryptionResult)) - } - - private suspend fun handleFindNode(message: FindNodeMessage) { - if (0 == message.distance) { - val response = NodesMessage(message.requestId, 1, listOf(ourENR())) - send(response) - return - } - - val nodes = routingTable.nodesOfDistance(message.distance) - - for (chunk in nodes.chunked(MAX_NODES_IN_RESPONSE)) { - val response = NodesMessage(message.requestId, nodes.size, chunk) - send(response) - } - } - - fun decode(message: Bytes): Message { - val tag = message.slice(0, Message.TAG_LENGTH) - val contentWithHeader = message.slice(Message.TAG_LENGTH) - val decodedMessage = RLP.decode(contentWithHeader) { reader -> read(tag, contentWithHeader, reader) } - return decodedMessage - } - - internal fun read(tag: Bytes, contentWithHeader: Bytes, reader: RLPReader): Message { - if (reader.nextIsList()) { - // this looks like a WHOAREYOU. - } - val authTag = reader.readValue() - - val encryptedContent = contentWithHeader.slice(reader.position()) - val decryptionKey = sessionKey.recipientKey - val decryptedContent = AES128GCM.decrypt(decryptionKey, authTag, encryptedContent, tag) - val type = decryptedContent.slice(0, 1) - val message = decryptedContent.slice(1) - - // Retrieve result - val messageType = MessageType.valueOf(type.toInt()) - return when (messageType) { - MessageType.PING -> PingMessage.create(message) - MessageType.PONG -> PongMessage.create(message) - MessageType.FINDNODE -> FindNodeMessage.create(message) - MessageType.NODES -> NodesMessage.create(message) - MessageType.REGTOPIC -> RegTopicMessage.create(message) - MessageType.TICKET -> TicketMessage.create(message) - MessageType.REGCONFIRM -> RegConfirmationMessage.create(message) - MessageType.TOPICQUERY -> TopicQueryMessage.create(message) - else -> throw IllegalArgumentException("Unsupported message type $messageType") - } - } - - suspend fun delayRegTopic(requestId: Bytes, topic: Bytes, waitTime: Long) { - delay(waitTime) - - val ticket = ticketHolder.get(requestId) - ticket?.let { - sendRegTopic(topic, ticket, requestId) - } - } - - suspend fun registerTopic(topic: Bytes, withDelay: Boolean = false) { - if (withDelay) { - delay(SEND_REGTOPIC_DELAY_MS) - } - - sendRegTopic(topic, Bytes.EMPTY) - } - - private suspend fun sendRegTopic( - topic: Bytes, - ticket: Bytes, - requestId: Bytes = Message.requestId(), - ) { - TODO("" + topic + ticket + requestId) - } - -// private suspend fun sendRegTopic( -// topic: Bytes, -// ticket: Bytes, -// requestId: Bytes = Message.requestId() -// ) { -// val nodeEnr = enr().toRLP() -// //val message = RegTopicMessage(requestId, nodeEnr, topic, ticket) -// -// val distance = 1 -// val receivers = routingTable.nodesOfDistance(distance) -// receivers.forEach { rlp -> -// val receiver = EthereumNodeRecord.fromRLP(rlp) -// val address = InetSocketAddress(receiver.ip(), receiver.udp()) -// val nodeId = Hash.sha2_256(rlp) -// TODO("" +address + nodeId) -// //send(address, message, nodeId) -// } -// } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt deleted file mode 100644 index 149a5951e..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCM.kt +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.encrypt - -import org.apache.tuweni.v2.bytes.Bytes -import javax.crypto.Cipher -import javax.crypto.spec.GCMParameterSpec -import javax.crypto.spec.SecretKeySpec - -/** - * Util dedicated for AES-GCM encoding with key size equal 16 bytes - */ -object AES128GCM { - - private const val ALGO_NAME: String = "AES" - private const val CIPHER_NAME: String = "AES/GCM/NoPadding" - private const val KEY_SIZE: Int = 128 - - /** - * AES128GCM encryption function - * - * @param key 16-byte encryption key - * @param nonce initialization vector - * @param message content for encryption - * @param data encryption metadata - */ - fun encrypt(privateKey: Bytes, nonce: Bytes, message: Bytes, additionalAuthenticatedData: Bytes): Bytes { - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - cipher.init( - Cipher.ENCRYPT_MODE, - SecretKeySpec(privateKey.toArrayUnsafe(), "AES"), - GCMParameterSpec(128, nonce.toArrayUnsafe()), - ) - cipher.updateAAD(additionalAuthenticatedData.toArrayUnsafe()) - val result = Bytes.wrap(cipher.doFinal(message.toArrayUnsafe())) - return result - } - - /** - * AES128GCM decryption function - * - * @param privateKey the key to use for decryption - * @param nonce the nonce of the encrypted data - * @param encoded the encrypted content - * @param additionalAuthenticatedData the AAD that should be decrypted alongside - * @return the decrypted data - */ - fun decrypt(privateKey: Bytes, nonce: Bytes, encoded: Bytes, additionalAuthenticatedData: Bytes): Bytes { - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - cipher.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(privateKey.toArrayUnsafe(), "AES"), - GCMParameterSpec(128, nonce.toArrayUnsafe()), - ) - cipher.updateAAD(additionalAuthenticatedData.toArrayUnsafe()) - return Bytes.wrap(cipher.doFinal(encoded.toArrayUnsafe())) - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKey.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKey.kt deleted file mode 100644 index 57c5b86c8..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKey.kt +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.encrypt - -import org.apache.tuweni.v2.bytes.Bytes - -internal data class SessionKey( - val initiatorKey: Bytes, - val recipientKey: Bytes, - val authRespKey: Bytes, -) diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGenerator.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGenerator.kt deleted file mode 100644 index 96f84406c..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGenerator.kt +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.encrypt - -import org.apache.tuweni.v2.bytes.Bytes -import org.bouncycastle.crypto.digests.SHA256Digest -import org.bouncycastle.crypto.generators.HKDFBytesGenerator -import org.bouncycastle.crypto.params.HKDFParameters - -/** - * Generates session keys on handshake, using HKDF key derivation function - */ -internal object SessionKeyGenerator { - - private const val DERIVED_KEY_SIZE: Int = 16 - private val INFO_PREFIX = Bytes.wrap("discovery v5 key agreement".toByteArray()) - - /** - * Executes session keys generation - * - * @param srcNodeId sender node identifier - * @param destNodeId receiver node identifier - * @param secret the input keying material or seed - * @param idNonce nonce used as salt - */ - fun generate(srcNodeId: Bytes, destNodeId: Bytes, secret: Bytes, idNonce: Bytes): SessionKey { - val info = Bytes.wrap(INFO_PREFIX, srcNodeId, destNodeId) - - val hkdf = HKDFBytesGenerator(SHA256Digest()) - val params = HKDFParameters(secret.toArrayUnsafe(), idNonce.toArrayUnsafe(), info.toArrayUnsafe()) - hkdf.init(params) - val output = Bytes.wrap(ByteArray(DERIVED_KEY_SIZE * 3)) - hkdf.generateBytes(output.toArrayUnsafe(), 0, output.size()) - return SessionKey( - output.slice(0, DERIVED_KEY_SIZE), - output.slice(DERIVED_KEY_SIZE, DERIVED_KEY_SIZE), - output.slice(DERIVED_KEY_SIZE * 2), - ) - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Ticket.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Ticket.kt deleted file mode 100644 index bfea72b53..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Ticket.kt +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.topic - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.devp2p.v5.encrypt.AES128GCM -import org.apache.tuweni.v2.rlp.RLP -import java.net.InetAddress - -internal data class Ticket( - val topic: Bytes, - val srcNodeId: Bytes, - val srcIp: String, - val requestTime: Long, - val waitTime: Long, - val cumTime: Long, -) { - - companion object { - private const val ZERO_NONCE_SIZE: Int = 12 - internal const val TIME_WINDOW_MS: Int = 10000 // 10 seconds - internal const val TICKET_INVALID_MSG = "Ticket is invalid" - - fun create(content: Bytes): Ticket { - return RLP.decodeList(content) { reader -> - val topic = reader.readValue() - val srcNodeId = reader.readValue() - val srcIp = InetAddress.getByAddress(reader.readValue().toArrayUnsafe()) - val requestTime = reader.readLong() - val waitTime = reader.readLong() - val cumTime = reader.readLong() - return@decodeList Ticket(topic, srcNodeId, srcIp.hostAddress, requestTime, waitTime, cumTime) - } - } - - fun decrypt(encrypted: Bytes, key: Bytes): Ticket { - val decrypted = AES128GCM.decrypt(key, Bytes.wrap(ByteArray(ZERO_NONCE_SIZE)), encrypted, Bytes.EMPTY) - return create(decrypted) - } - } - - fun encode(): Bytes { - return RLP.encodeList { writer -> - writer.writeValue(topic) - writer.writeValue(srcNodeId) - writer.writeValue(Bytes.wrap(InetAddress.getByName(srcIp).address)) - writer.writeLong(requestTime) - writer.writeLong(waitTime) - writer.writeLong(cumTime) - } - } - - fun encrypt(key: Bytes): Bytes { - val ticketBytes = encode() - return AES128GCM.encrypt(key, Bytes.wrap(ByteArray(ZERO_NONCE_SIZE)), ticketBytes, Bytes.EMPTY) - } - - fun validate( - srcNodeId: Bytes, - srcIp: String, - now: Long, - topic: Bytes, - ) { - require(this.srcNodeId == srcNodeId) { TICKET_INVALID_MSG } - require(this.srcIp == srcIp) { TICKET_INVALID_MSG } - require(this.topic == topic) { TICKET_INVALID_MSG } - val windowStart = this.requestTime + this.waitTime - require(now >= windowStart && now <= windowStart + TIME_WINDOW_MS) { TICKET_INVALID_MSG } - } -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Topic.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Topic.kt deleted file mode 100644 index 1d584fff5..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/Topic.kt +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.topic - -import org.apache.tuweni.v2.bytes.Bytes - -internal data class Topic( - val content: String, -) { - - fun toBytes(): Bytes = Bytes.fromHexString(content) -} diff --git a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TopicTable.kt b/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TopicTable.kt deleted file mode 100644 index ab7cb5369..000000000 --- a/devp2p/src/main/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TopicTable.kt +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.topic - -import com.google.common.cache.Cache -import com.google.common.cache.CacheBuilder -import org.apache.tuweni.v2.devp2p.DiscoveryService -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import java.util.concurrent.TimeUnit - -internal class TopicTable( - private val tableCapacity: Int = MAX_TABLE_CAPACITY, - private val queueCapacity: Int = MAX_ENTRIES_PER_TOPIC, -) { - - private val timeSupplier: () -> Long = DiscoveryService.CURRENT_TIME_SUPPLIER - private val table: HashMap> = HashMap(tableCapacity) - - init { - require(tableCapacity > 0) { "Table capacity value must be positive" } - require(queueCapacity > 0) { "Queue capacity value must be positive" } - } - - fun getNodes(topic: Topic): List { - val values = table[topic] - return values?.let { values.asMap().values.map { it.enr } } ?: emptyList() - } - - /** - * Puts a topic in a table - * - * @return wait time for registrant node (0 is topic was putted immediately, -1 in case of error) - */ - @Synchronized - fun put(topic: Topic, enr: EthereumNodeRecord): Long { - gcTable() - - val topicQueue = table[topic] - val nodeId = enr.nodeId().toHexString() - - if (null != topicQueue) { - if (topicQueue.size() < queueCapacity) { - topicQueue.put(nodeId, TargetAd(timeSupplier(), enr)) - return 0 // put immediately - } else { - // Queue if full (wait time = target-ad-lifetime - oldest ad lifetime in queue) - return TARGET_AD_LIFETIME_MS - (timeSupplier() - topicQueue.oldest().regTime) - } - } - - if (table.size < tableCapacity) { - table[topic] = createNewQueue().apply { put(nodeId, TargetAd(timeSupplier(), enr)) } - return 0 // put immediately - } else { - // table is full (wait time = target-ad-lifetime - oldest in table of youngest in queue) - val oldestInTable = table.entries.map { it.value.youngest().regTime }.minOrNull() ?: -1 - return TARGET_AD_LIFETIME_MS - (timeSupplier() - oldestInTable) - } - } - - fun contains(topic: Topic): Boolean = table.containsKey(topic) - - fun isEmpty(): Boolean = table.isEmpty() - - fun clear() = table.clear() - - private fun createNewQueue(): Cache { - return CacheBuilder.newBuilder() - .expireAfterWrite(TARGET_AD_LIFETIME_MS, TimeUnit.MILLISECONDS) - .initialCapacity(queueCapacity) - .build() - } - - private fun gcTable() { - table.entries.removeIf { it.value.size() == 0L } - } - - private fun Cache.oldest(): TargetAd { - return asMap().values.minByOrNull { it.regTime } ?: throw IllegalArgumentException(QUEUE_EMPTY_MSG) - } - - private fun Cache.youngest(): TargetAd { - return asMap().values.maxByOrNull { it.regTime } ?: throw IllegalArgumentException(QUEUE_EMPTY_MSG) - } - - companion object { - internal const val MAX_ENTRIES_PER_TOPIC: Int = 100 - private const val MAX_TABLE_CAPACITY: Int = 500 - private const val TARGET_AD_LIFETIME_MS = (15 * 60 * 1000).toLong() // 15 min - - private const val QUEUE_EMPTY_MSG = "Queue is empty." - } -} - -internal class TargetAd(val regTime: Long, val enr: EthereumNodeRecord) diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeSessionTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeSessionTest.kt index db9302098..3a7d15380 100644 --- a/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeSessionTest.kt +++ b/devp2p/src/test/kotlin/org/apache/tuweni/devp2p/v5/HandshakeSessionTest.kt @@ -6,11 +6,8 @@ import io.vertx.core.net.SocketAddress import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import org.apache.tuweni.bytes.Bytes -import org.apache.tuweni.bytes.Bytes32 import org.apache.tuweni.concurrent.coroutines.await import org.apache.tuweni.crypto.SECP256K1 -import org.apache.tuweni.crypto.SECP256K1.PublicKey -import org.apache.tuweni.crypto.SECP256K1.SecretKey import org.apache.tuweni.devp2p.EthereumNodeRecord import org.apache.tuweni.devp2p.v5.encrypt.AES128GCM import org.apache.tuweni.devp2p.v5.encrypt.SessionKeyGenerator @@ -26,12 +23,8 @@ class HandshakeSessionTest { @Test fun testConnectTwoClients() = runBlocking { - val secretKey = SecretKey.fromBytes(Bytes32.fromHexString("0x01")) - val publicKey = PublicKey.fromSecretKey(secretKey) - val keyPair = SECP256K1.KeyPair.create(secretKey, publicKey) - val peerSecretKey = SecretKey.fromBytes(Bytes32.fromHexString("0x02")) - val peerPublicKey = PublicKey.fromSecretKey(peerSecretKey) - val peerKeyPair = SECP256K1.KeyPair.create(peerSecretKey, peerPublicKey) + val keyPair = SECP256K1.KeyPair.random() + val peerKeyPair = SECP256K1.KeyPair.random() val address = SocketAddress.inetSocketAddress(1234, "localhost") val peerAddress = SocketAddress.inetSocketAddress(1235, "localhost") val enr = EthereumNodeRecord.create(keyPair, ip = InetAddress.getLoopbackAddress(), udp = 1234) diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryServiceTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryServiceTest.kt deleted file mode 100644 index a8c97065b..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/DiscoveryServiceTest.kt +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import io.vertx.core.Vertx -import io.vertx.core.buffer.Buffer -import io.vertx.core.net.SocketAddress -import io.vertx.kotlin.coroutines.coAwait -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import org.apache.tuweni.concurrent.AsyncResult -import org.apache.tuweni.concurrent.CompletableAsyncResult -import org.apache.tuweni.concurrent.coroutines.await -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.junit.VertxExtension -import org.apache.tuweni.junit.VertxInstance -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.extension.ExtendWith -import java.net.URI -import java.nio.ByteBuffer -import java.util.concurrent.atomic.AtomicReference - -@Timeout(10) -@ExtendWith(BouncyCastleExtension::class, VertxExtension::class) -internal class DiscoveryServiceTest { - - @Test - fun shouldStartAndShutdownService(@VertxInstance vertx: Vertx) = runBlocking { - val discoveryService = DiscoveryService.open( - vertx, - host = "127.0.0.1", - keyPair = SECP256K1.KeyPair.random(), - ) - discoveryService.awaitBootstrap() - assertFalse(discoveryService.isShutdown) - discoveryService.shutdown() - assertTrue(discoveryService.isShutdown) - } - - @Test - fun shouldRespondToPingAndRecordEndpoint(@VertxInstance vertx: Vertx): Unit = runBlocking { - val peerRepository = EphemeralPeerRepository() - val discoveryService = DiscoveryService.open( - vertx, - host = "127.0.0.1", - keyPair = SECP256K1.KeyPair.random(), - peerRepository = peerRepository, - ) - discoveryService.awaitBootstrap() - val address = SocketAddress.inetSocketAddress(discoveryService.localPort, "127.0.0.1") - val clientKeyPair = SECP256K1.KeyPair.random() - val reference = AsyncResult.incomplete() - val client = vertx.createDatagramSocket().handler { res -> - reference.complete(res.data()) - }.listen(0, "localhost").coAwait() - val clientEndpoint = Endpoint("192.168.1.1", 5678, 7654) - val ping = PingPacket.create( - clientKeyPair, - System.currentTimeMillis(), - clientEndpoint, - Endpoint(address), - null, - ) - client.send(Buffer.buffer(ping.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() - val datagram = reference.await() - val buffer = ByteBuffer.allocate(datagram.length()) - buffer.put(datagram.bytes) - val pong = Packet.decodeFrom(buffer) as PongPacket - assertEquals(discoveryService.nodeId, pong.nodeId) - assertEquals(ping.hash, pong.pingHash) - - val peer = peerRepository.get(URI("enode://" + clientKeyPair.publicKey().toHexString() + "@127.0.0.1:5678")) - assertNotNull(peer.endpoint) - assertEquals(clientEndpoint.tcpPort, peer.endpoint.tcpPort) - discoveryService.shutdown() - client.close() - } - - @Test - fun shouldPingBootstrapNodeAndValidate(@VertxInstance vertx: Vertx): Unit = runBlocking { - val bootstrapKeyPair = SECP256K1.KeyPair.random() - val reference = AtomicReference>() - reference.set(AsyncResult.incomplete()) - val bootstrapClient = vertx.createDatagramSocket().handler { res -> - reference.get().complete(res.data()) - }.listen(0, "127.0.0.1").coAwait() - - val serviceKeyPair = SECP256K1.KeyPair.random() - val peerRepository = EphemeralPeerRepository() - val routingTable = DevP2PPeerRoutingTable(serviceKeyPair.publicKey()) - val discoveryService = DiscoveryService.open( - vertx, - host = "127.0.0.1", - keyPair = serviceKeyPair, - bootstrapURIs = listOf( - URI( - "enode://" + bootstrapKeyPair.publicKey().toHexString() + "@127.0.0.1:" + bootstrapClient.localAddress() - .port(), - ), - ), - peerRepository = peerRepository, - routingTable = routingTable, - ) - - val datagram = reference.get().await() - val buffer = ByteBuffer.allocate(datagram.length()) - buffer.put(datagram.bytes) - val ping = Packet.decodeFrom(buffer) as PingPacket - assertEquals(discoveryService.nodeId, ping.nodeId) - assertEquals( - ping.to, - Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), - ) - assertEquals(discoveryService.localPort, ping.from.udpPort) - assertNull(ping.from.tcpPort) - - val pong = PongPacket.create( - bootstrapKeyPair, - System.currentTimeMillis(), - ping.from, - ping.hash, - null, - ) - reference.set(AsyncResult.incomplete()) - val address = SocketAddress.inetSocketAddress(discoveryService.localPort, "127.0.0.1") - bootstrapClient.send(Buffer.buffer(pong.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() - - val findNodesDatagram = reference.get().await() - - val findNodes = Packet.decodeFrom(Bytes.wrap(findNodesDatagram.bytes)) as FindNodePacket - assertEquals(discoveryService.nodeId, findNodes.nodeId) - assertEquals(discoveryService.nodeId, findNodes.target) - - val bootstrapPeer = - peerRepository.get( - URI( - "enode://" + bootstrapKeyPair.publicKey().toHexString() + - "@127.0.0.1:" + bootstrapClient.localAddress().port(), - ), - ) - assertNotNull(bootstrapPeer.lastVerified) - assertNotNull(bootstrapPeer.endpoint) - assertEquals(bootstrapClient.localAddress().port(), bootstrapPeer.endpoint.tcpPort) - - assertTrue(routingTable.contains(bootstrapPeer)) - - discoveryService.shutdown() - bootstrapClient.close() - } - - @Test - fun shouldIgnoreBootstrapNodeRespondingWithDifferentNodeId(@VertxInstance vertx: Vertx): Unit = runBlocking { - println("foo") - val bootstrapKeyPair = SECP256K1.KeyPair.random() - val reference = AsyncResult.incomplete() - val bootstrapClient = vertx.createDatagramSocket().handler { res -> - reference.complete(res.data()) - }.listen(0, "localhost").coAwait() - - val serviceKeyPair = SECP256K1.KeyPair.random() - val peerRepository = EphemeralPeerRepository() - val routingTable = DevP2PPeerRoutingTable(serviceKeyPair.publicKey()) - val discoveryService = DiscoveryService.open( - vertx, - host = "127.0.0.1", - keyPair = serviceKeyPair, - bootstrapURIs = listOf( - URI( - "enode://" + bootstrapKeyPair.publicKey().toHexString() + "@127.0.0.1:" + bootstrapClient.localAddress() - .port(), - ), - ), - peerRepository = peerRepository, - routingTable = routingTable, - ) - val datagram = reference.await() - val buffer = ByteBuffer.allocate(datagram.length()) - buffer.put(datagram.bytes) - val ping = Packet.decodeFrom(buffer) as PingPacket - assertEquals(discoveryService.nodeId, ping.nodeId) - assertEquals( - ping.to, - Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), - ) - assertEquals(discoveryService.localPort, ping.from.udpPort) - assertNull(ping.from.tcpPort) - - val pong = PongPacket.create( - SECP256K1.KeyPair.random(), - System.currentTimeMillis(), - ping.from, - ping.hash, - null, - ) - val address = SocketAddress.inetSocketAddress(discoveryService.localPort, "127.0.0.1") - bootstrapClient.send(Buffer.buffer(pong.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() - - delay(1000) - val bootstrapPeer = - peerRepository.get( - URI( - "enode://" + bootstrapKeyPair.publicKey().toHexString() + - "@127.0.0.1:" + bootstrapClient.localAddress().port(), - ), - ) - assertNull(bootstrapPeer.lastVerified) - assertFalse(routingTable.contains(bootstrapPeer)) - - discoveryService.shutdown() - bootstrapClient.close() - } - - @Test - fun shouldPingBootstrapNodeWithAdvertisedAddress(@VertxInstance vertx: Vertx): Unit = runBlocking { - val bootstrapKeyPair = SECP256K1.KeyPair.random() - val reference = AsyncResult.incomplete() - val bootstrapClient = vertx.createDatagramSocket().handler { res -> - reference.complete(res.data()) - }.listen(0, "localhost").coAwait() - - val discoveryService = DiscoveryService.open( - vertx, - host = "127.0.0.1", - keyPair = SECP256K1.KeyPair.random(), - bootstrapURIs = listOf( - URI( - "enode://" + bootstrapKeyPair.publicKey().bytes() - .toHexString() + "@127.0.0.1:" + bootstrapClient.localAddress().port(), - ), - ), - advertiseAddress = "192.168.66.55", - advertiseUdpPort = 3836, - advertiseTcpPort = 8765, - ) - - val datagram = reference.await() - val buffer = ByteBuffer.allocate(datagram.length()) - buffer.put(datagram.bytes) - val ping = Packet.decodeFrom(buffer) as PingPacket - assertEquals(discoveryService.nodeId, ping.nodeId) - assertEquals( - Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), - ping.to, - ) - assertEquals(Endpoint("192.168.66.55", 3836, 8765), ping.from) - - discoveryService.shutdown() - bootstrapClient.close() - } - - @Test - fun shouldRetryPingsToBootstrapNodes(@VertxInstance vertx: Vertx): Unit = runBlocking { - val bootstrapKeyPair = SECP256K1.KeyPair.random() - val reference = AtomicReference>() - reference.set(AsyncResult.incomplete()) - val bootstrapClient = vertx.createDatagramSocket().handler { res -> - reference.get().complete(res.data()) - }.listen(0, "localhost").coAwait() - - val discoveryService = DiscoveryService.open( - vertx, - host = "127.0.0.1", - keyPair = SECP256K1.KeyPair.random(), - bootstrapURIs = listOf( - URI( - "enode://" + bootstrapKeyPair.publicKey().bytes() - .toHexString() + "@127.0.0.1:" + bootstrapClient.localAddress().port(), - ), - ), - ) - val datagram1 = reference.get().await() - reference.set(AsyncResult.incomplete()) - val buffer1 = ByteBuffer.allocate(datagram1.length()) - buffer1.put(datagram1.bytes) - val ping1 = Packet.decodeFrom(buffer1) as PingPacket - assertEquals(discoveryService.nodeId, ping1.nodeId) - assertEquals( - Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), - ping1.to, - ) - val datagram2 = reference.get().await() - reference.set(AsyncResult.incomplete()) - val buffer2 = ByteBuffer.allocate(datagram2.length()) - buffer2.put(datagram2.bytes) - val ping2 = Packet.decodeFrom(buffer2) as PingPacket - assertEquals(discoveryService.nodeId, ping2.nodeId) - assertEquals( - Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), - ping2.to, - ) - val datagram3 = reference.get().await() - reference.set(AsyncResult.incomplete()) - val buffer3 = ByteBuffer.allocate(datagram3.length()) - buffer3.put(datagram3.bytes) - val ping3 = Packet.decodeFrom(buffer3) as PingPacket - assertEquals(discoveryService.nodeId, ping3.nodeId) - assertEquals( - Endpoint("127.0.0.1", bootstrapClient.localAddress().port(), bootstrapClient.localAddress().port()), - ping3.to, - ) - discoveryService.shutdown() - bootstrapClient.close() - } - - @Test - fun shouldRequirePingPongBeforeRespondingToFindNodesUnverifiedPeer(@VertxInstance vertx: Vertx): Unit = runBlocking { - val peerRepository = EphemeralPeerRepository() - val discoveryService = DiscoveryService.open( - vertx, - host = "127.0.0.1", - keyPair = SECP256K1.KeyPair.random(), - peerRepository = peerRepository, - ) - discoveryService.awaitBootstrap() - val address = SocketAddress.inetSocketAddress(discoveryService.localPort, "127.0.0.1") - - val clientKeyPair = SECP256K1.KeyPair.random() - val reference = AtomicReference>() - reference.set(AsyncResult.incomplete()) - val client = vertx.createDatagramSocket().handler { res -> - reference.get().complete(res.data()) - }.listen(0, "localhost").coAwait() - val findNodes = - FindNodePacket.create( - clientKeyPair, - System.currentTimeMillis(), - SECP256K1.KeyPair.random().publicKey(), - ) - client.send(Buffer.buffer(findNodes.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() - - val datagram = reference.get().await() - val buffer = ByteBuffer.allocate(datagram.length()) - buffer.put(datagram.bytes) - val ping = Packet.decodeFrom(buffer) as PingPacket - assertEquals(discoveryService.nodeId, ping.nodeId) - - // check it didn't immediately send neighbors - delay(500) - - val pong = PongPacket.create( - clientKeyPair, - System.currentTimeMillis(), - ping.from, - ping.hash, - null, - ) - - reference.set(AsyncResult.incomplete()) - client.send(Buffer.buffer(pong.encode().toArrayUnsafe()), address.port(), address.host()).coAwait() - - val datagram2 = reference.get().await() - val buffer2 = ByteBuffer.allocate(datagram2.length()) - buffer2.put(datagram2.bytes) - val neighbors = Packet.decodeFrom(buffer2) as NeighborsPacket - assertEquals(discoveryService.nodeId, neighbors.nodeId) - - val peer = - peerRepository.get( - URI( - "enode://" + clientKeyPair.publicKey().toHexString() + - "@127.0.0.1:" + discoveryService.localPort, - ), - ) - assertNotNull(peer.lastVerified) - assertNotNull(peer.endpoint) - - discoveryService.shutdown() - client.close() - } - - @Disabled - @Test - fun shouldConnectToNetworkAndDoALookup(@VertxInstance vertx: Vertx) { - /* ktlint-disable */ - val boostrapNodes = listOf( - "enode://6332792c4a00e3e4ee0926ed89e0d27ef985424d97b6a45bf0f23e51f0dcb5e66b875777506458aea7af6f9e4ffb69f43f3778ee73c81ed9d34c51c4b16b0b0f@52.232.243.152:30303" -// "enode://94c15d1b9e2fe7ce56e458b9a3b672ef11894ddedd0c6f247e0f1d3487f52b66208fb4aeb8179fce6e3a749ea93ed147c37976d67af557508d199d9594c35f09@192.81.208.223:30303" - ).map { s -> URI.create(s) } - /* ktlint-enable */ - val discoveryService = DiscoveryService.open( - vertx, - host = "127.0.0.1", - keyPair = SECP256K1.KeyPair.random(), - bootstrapURIs = boostrapNodes, - ) - - runBlocking { - discoveryService.awaitBootstrap() - val result = discoveryService.lookup(SECP256K1.KeyPair.random().publicKey()) - assertTrue(result.isNotEmpty()) - discoveryService.shutdown() - } - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/ENRResponsePacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/ENRResponsePacketTest.kt deleted file mode 100644 index 38c5d574c..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/ENRResponsePacketTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes32 -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import java.net.InetAddress - -@ExtendWith(BouncyCastleExtension::class) -internal class ENRResponsePacketTest { - - @Test - fun shouldEncodeThenDecodePacket() { - val keyPair = SECP256K1.KeyPair.random() - - val requestHash = Bytes32.fromRandom() - val enr = EthereumNodeRecord.toRLP( - SECP256K1.KeyPair.random(), - 2, - emptyMap(), - emptyMap(), - InetAddress.getByName("localhost"), - 3000, - 12000, - ) - val now = System.currentTimeMillis() - val pong = ENRResponsePacket.create(keyPair, now, requestHash, enr) - - val datagram = pong.encode() - val packet = Packet.decodeFrom(datagram) - assertTrue(packet is ENRResponsePacket) - - val responsePacket = packet as ENRResponsePacket - assertEquals(keyPair.publicKey(), responsePacket.nodeId) - assertEquals(enr, responsePacket.enr) - assertEquals(requestHash, responsePacket.requestHash) - assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, responsePacket.expiration) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecordTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecordTest.kt deleted file mode 100644 index 67ae15b3b..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/EthereumNodeRecordTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtendWith -import java.net.InetAddress - -@ExtendWith(BouncyCastleExtension::class) -class EthereumNodeRecordTest { - - @Test - fun tooLong() { - val tooLong = Bytes.random(312) - val exception: IllegalArgumentException = assertThrows { - EthereumNodeRecord.fromRLP(tooLong) - } - assertEquals("Record too long", exception.message) - } - - @Test - fun readFromRLP() { - val keyPair = SECP256K1.KeyPair.random() - val enr = EthereumNodeRecord.create( - keyPair, - 1, - emptyMap(), - emptyMap(), - InetAddress.getLoopbackAddress(), - null, - 10000, - ) - enr.validate() - assertEquals(1L, enr.seq()) - assertEquals(Bytes.wrap("v4".toByteArray()), enr.data["id"]) - assertEquals(Bytes.fromHexString("7f000001"), enr.data["ip"]) - } - - @Test - fun toRLP() { - val keypair = SECP256K1.KeyPair.random() - val rlp = EthereumNodeRecord.toRLP( - keypair, - seq = 1L, - data = mutableMapOf(Pair("key", Bytes.fromHexString("deadbeef"))), - listData = mutableMapOf(Pair("foo", listOf(Bytes.fromHexString("deadbeef")))), - ip = InetAddress.getByName("127.0.0.1"), - ) - val record = EthereumNodeRecord.fromRLP(rlp) - assertEquals(1L, record.seq()) - assertEquals(Bytes.wrap("v4".toByteArray()), record.data["id"]) - assertEquals(Bytes.fromHexString("7f000001"), record.data["ip"]) - assertEquals(keypair.publicKey(), record.publicKey()) - assertEquals(Bytes.fromHexString("deadbeef"), record.data["key"]) - assertEquals(Bytes.fromHexString("deadbeef"), (record.listData["foo"] ?: error("None"))[0]) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/FindNodePacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/FindNodePacketTest.kt deleted file mode 100644 index a3e16aa27..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/FindNodePacketTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(BouncyCastleExtension::class) -internal class FindNodePacketTest { - - @Test - fun shouldEncodeThenDecodePacket() { - val keyPair = SECP256K1.KeyPair.random() - val target = SECP256K1.KeyPair.random().publicKey() - val now = System.currentTimeMillis() - val pong = FindNodePacket.create(keyPair, now, target) - - val datagram = pong.encode() - val packet = Packet.decodeFrom(datagram) - assertTrue(packet is FindNodePacket) - - val findNodePacket = packet as FindNodePacket - assertEquals(keyPair.publicKey(), findNodePacket.nodeId) - assertEquals(target, findNodePacket.target) - assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, findNodePacket.expiration) - } - - @Test - fun decodeReferencePacket1() { - // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md - val datagram = Bytes.fromHexString( - "c7c44041b9f7c7e41934417ebac9a8e1a4c6298f74553f2fcfdcae6ed6fe53163eb3d2b52e39fe91" + - "831b8a927bf4fc222c3902202027e5e9eb812195f95d20061ef5cd31d502e47ecb61183f74a504fe" + - "04c51e73df81f25c4d506b26db4517490103f84eb840ca634cae0d49acb401d8a4c6b6fe8c55b70d" + - "115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be0081290476" + - "7bf5ccd1fc7f8443b9a35582999983999999280dc62cc8255c73471e0a61da0c89acdc0e035e260a" + - "dd7fc0c04ad9ebf3919644c91cb247affc82b69bd2ca235c71eab8e49737c937a2c396", - ) - val packet = Packet.decodeFrom(datagram) - - assertTrue(packet is FindNodePacket) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/NeighborsPacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/NeighborsPacketTest.kt deleted file mode 100644 index a31d62607..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/NeighborsPacketTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(BouncyCastleExtension::class) -internal class NeighborsPacketTest { - - @Test - fun shouldHaveExpectedMinimumSize() { - val packet = - NeighborsPacket.create(SECP256K1.KeyPair.random(), System.currentTimeMillis(), emptyList()) - val buffer = packet.encode() - // the minimum also includes a list length prefix of 4 bytes - assertEquals(NeighborsPacket.RLP_MIN_SIZE, buffer.size() + 4) - } - - @Test - fun shouldEncodeThenDecodePacket() { - val keyPair = SECP256K1.KeyPair.random() - val neighbors = listOf( - Node(Endpoint("10.0.0.54", 6543, 6543), SECP256K1.KeyPair.random().publicKey()), - Node(Endpoint("192.168.34.65", 9832, 1453), SECP256K1.KeyPair.random().publicKey()), - ) - val now = System.currentTimeMillis() - val pong = NeighborsPacket.create(keyPair, now, neighbors) - - val datagram = pong.encode() - val packet = Packet.decodeFrom(datagram) - assertTrue(packet is NeighborsPacket) - - val neighborsPacket = packet as NeighborsPacket - assertEquals(keyPair.publicKey(), neighborsPacket.nodeId) - assertEquals(neighbors, neighborsPacket.nodes) - assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, neighborsPacket.expiration) - } - - @Test - fun decodeReferencePacket1() { - // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md - val datagram = Bytes.fromHexString( - "c679fc8fe0b8b12f06577f2e802d34f6fa257e6137a995f6f4cbfc9ee50ed3710faf6e66f932c4c8" + - "d81d64343f429651328758b47d3dbc02c4042f0fff6946a50f4a49037a72bb550f3a7872363a83e1" + - "b9ee6469856c24eb4ef80b7535bcf99c0004f9015bf90150f84d846321163782115c82115db84031" + - "55e1427f85f10a5c9a7755877748041af1bcd8d474ec065eb33df57a97babf54bfd2103575fa8291" + - "15d224c523596b401065a97f74010610fce76382c0bf32f84984010203040101b840312c55512422" + - "cf9b8a4097e9a6ad79402e87a15ae909a4bfefa22398f03d20951933beea1e4dfa6f968212385e82" + - "9f04c2d314fc2d4e255e0d3bc08792b069dbf8599020010db83c4d001500000000abcdef12820d05" + - "820d05b84038643200b172dcfef857492156971f0e6aa2c538d8b74010f8e140811d53b98c765dd2" + - "d96126051913f44582e8c199ad7c6d6819e9a56483f637feaac9448aacf8599020010db885a308d3" + - "13198a2e037073488203e78203e8b8408dcab8618c3253b558d459da53bd8fa68935a719aff8b811" + - "197101a4b2b47dd2d47295286fc00cc081bb542d760717d1bdd6bec2c37cd72eca367d6dd3b9df73" + - "8443b9a355010203b525a138aa34383fec3d2719a0", - ) - val packet = Packet.decodeFrom(datagram) - - assertTrue(packet is NeighborsPacket) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PingPacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PingPacketTest.kt deleted file mode 100644 index 26eb5d01b..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PingPacketTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(BouncyCastleExtension::class) -internal class PingPacketTest { - - @Test - fun shouldEncodeThenDecodePacket() { - val keyPair = SECP256K1.KeyPair.random() - val from = Endpoint("10.0.0.54", 6543, 6543) - val to = Endpoint("192.168.34.65", 9832, 1453) - val now = System.currentTimeMillis() - val ping = PingPacket.create(keyPair, now, from, to, null) - - val datagram = ping.encode() - val packet = Packet.decodeFrom(datagram) - assertTrue(packet is PingPacket) - - val pingPacket = packet as PingPacket - assertEquals(keyPair.publicKey(), pingPacket.nodeId) - assertEquals(Endpoint("10.0.0.54", 6543, 6543), pingPacket.from) - assertEquals(Endpoint("192.168.34.65", 9832, 1453), pingPacket.to) - assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pingPacket.expiration) - } - - @Test - fun shouldEncodeThenDecodePacketWithEnrSeq() { - val keyPair = SECP256K1.KeyPair.random() - val from = Endpoint("10.0.0.54", 6543, 6543) - val to = Endpoint("192.168.34.65", 9832, 1453) - val now = System.currentTimeMillis() - val ping = PingPacket.create(keyPair, now, from, to, 64) - - val datagram = ping.encode() - val packet = Packet.decodeFrom(datagram) - assertTrue(packet is PingPacket) - - val pingPacket = packet as PingPacket - assertEquals(keyPair.publicKey(), pingPacket.nodeId) - assertEquals(Endpoint("10.0.0.54", 6543, 6543), pingPacket.from) - assertEquals(Endpoint("192.168.34.65", 9832, 1453), pingPacket.to) - assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pingPacket.expiration) - assertEquals(64L, pingPacket.enrSeq) - } - - @Test - fun shouldDecodePingPacketWithMissingEndpoint() { - val keyPair = SECP256K1.KeyPair.random() - val from = Endpoint("10.0.0.54", 6543, 6543) - val to = Endpoint("192.168.34.65", 9832, 1453) - val now = System.currentTimeMillis() - val ping = PingPacket.create(keyPair, now, from, to, null) - - val datagram = ping.encode() - val packet = Packet.decodeFrom(datagram) - assertTrue(packet is PingPacket) - - val pingPacket = packet as PingPacket - assertEquals(keyPair.publicKey(), pingPacket.nodeId) - assertEquals(Endpoint("10.0.0.54", 6543, 6543), pingPacket.from) - assertEquals(Endpoint("192.168.34.65", 9832, 1453), pingPacket.to) - assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pingPacket.expiration) - } - - @Test - fun decodeReferencePacket1() { - // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md - val datagram = Bytes.fromHexString( - "e9614ccfd9fc3e74360018522d30e1419a143407ffcce748de3e22116b7e8dc92ff74788c0b6663a" + - "aa3d67d641936511c8f8d6ad8698b820a7cf9e1be7155e9a241f556658c55428ec0563514365799a" + - "4be2be5a685a80971ddcfa80cb422cdd0101ec04cb847f000001820cfa8215a8d790000000000000" + - "000000000000000000018208ae820d058443b9a3550102", - ) - val packet = Packet.decodeFrom(datagram) - - assertTrue(packet is PingPacket) - } - - @Disabled("EIP-868 supercedes EIP-8 behavior") - @Test - fun decodeReferencePacket2() { - // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md - val datagram = Bytes.fromHexString( - "577be4349c4dd26768081f58de4c6f375a7a22f3f7adda654d1428637412c3d7fe917cadc56d4e5e" + - "7ffae1dbe3efffb9849feb71b262de37977e7c7a44e677295680e9e38ab26bee2fcbae207fba3ff3" + - "d74069a50b902a82c9903ed37cc993c50001f83e82022bd79020010db83c4d001500000000abcdef" + - "12820cfa8215a8d79020010db885a308d313198a2e037073488208ae82823a8443b9a355c5010203" + - "040531b9019afde696e582a78fa8d95ea13ce3297d4afb8ba6433e4154caa5ac6431af1b80ba7602" + - "3fa4090c408f6b4bc3701562c031041d4702971d102c9ab7fa5eed4cd6bab8f7af956f7d565ee191" + - "7084a95398b6a21eac920fe3dd1345ec0a7ef39367ee69ddf092cbfe5b93e5e568ebc491983c09c7" + - "6d922dc3", - ) - val packet = Packet.decodeFrom(datagram) - - assertTrue(packet is PingPacket) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PongPacketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PongPacketTest.kt deleted file mode 100644 index 2c8daead8..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/PongPacketTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p - -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.bytes.Bytes32 -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(BouncyCastleExtension::class) -internal class PongPacketTest { - - @Test - fun shouldEncodeThenDecodePacket() { - val keyPair = SECP256K1.KeyPair.random() - val to = Endpoint("10.0.0.54", 6543, 6543) - val pingHash = Bytes32.fromRandom() - val now = System.currentTimeMillis() - val pong = PongPacket.create(keyPair, now, to, pingHash, null) - - val datagram = pong.encode() - val packet = Packet.decodeFrom(datagram) - assertTrue(packet is PongPacket) - - val pongPacket = packet as PongPacket - assertEquals(keyPair.publicKey(), pongPacket.nodeId) - assertEquals(Endpoint("10.0.0.54", 6543, 6543), pongPacket.to) - assertEquals(pingHash, pongPacket.pingHash) - assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pongPacket.expiration) - } - - @Test - fun shouldEncodeThenDecodePacketWithSeq() { - val keyPair = SECP256K1.KeyPair.random() - val to = Endpoint("10.0.0.54", 6543, 6543) - val pingHash = Bytes32.fromRandom() - val now = System.currentTimeMillis() - val pong = PongPacket.create(keyPair, now, to, pingHash, 32) - - val datagram = pong.encode() - val packet = Packet.decodeFrom(datagram) - assertTrue(packet is PongPacket) - - val pongPacket = packet as PongPacket - assertEquals(keyPair.publicKey(), pongPacket.nodeId) - assertEquals(Endpoint("10.0.0.54", 6543, 6543), pongPacket.to) - assertEquals(pingHash, pongPacket.pingHash) - assertEquals(((now + PACKET_EXPIRATION_PERIOD_MS + 999) / 1000) * 1000, pongPacket.expiration) - assertEquals(32L, pongPacket.enrSeq) - } - - @Disabled("EIP-868 supercedes EIP-8 behavior") - @Test - fun decodeReferencePacket1() { - // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-8.md - val datagram = Bytes.fromHexString( - "09b2428d83348d27cdf7064ad9024f526cebc19e4958f0fdad87c15eb598dd61d08423e0bf66b206" + - "9869e1724125f820d851c136684082774f870e614d95a2855d000f05d1648b2d5945470bc187c2d2" + - "216fbe870f43ed0909009882e176a46b0102f846d79020010db885a308d313198a2e037073488208" + - "ae82823aa0fbc914b16819237dcd8801d7e53f69e9719adecb3cc0e790c57e91ca4461c9548443b9" + - "a355c6010203c2040506a0c969a58f6f9095004c0177a6b47f451530cab38966a25cca5cb58f0555" + - "42124e", - ) - val packet = Packet.decodeFrom(datagram) - - assertTrue(packet is PongPacket) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/DefaultDiscoveryV5ServiceTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/DefaultDiscoveryV5ServiceTest.kt deleted file mode 100644 index 4cd4f30b2..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/DefaultDiscoveryV5ServiceTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import io.vertx.core.Vertx -import io.vertx.core.buffer.Buffer -import io.vertx.kotlin.coroutines.coAwait -import kotlinx.coroutines.runBlocking -import org.apache.tuweni.concurrent.AsyncResult -import org.apache.tuweni.concurrent.coroutines.await -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.junit.VertxExtension -import org.apache.tuweni.junit.VertxInstance -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import org.apache.tuweni.v2.io.Base64URLSafe -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.Timeout -import org.junit.jupiter.api.extension.ExtendWith -import java.net.InetAddress -import java.nio.ByteBuffer - -@Timeout(10) -@ExtendWith(BouncyCastleExtension::class, VertxExtension::class) -class DefaultDiscoveryV5ServiceTest { - - private val recipientKeyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random() - private val recipientEnr: Bytes = - EthereumNodeRecord.toRLP(recipientKeyPair, ip = InetAddress.getLoopbackAddress(), udp = 19003) - private val encodedEnr: String = "enr:${Base64URLSafe.encode(recipientEnr)}" - private val keyPair: SECP256K1.KeyPair = SECP256K1.KeyPair.random() - private val localPort: Int = 19002 - private val bootstrapENRList: List = listOf(encodedEnr) - - @Test - fun startInitializesConnectorAndBootstraps(@VertxInstance vertx: Vertx): Unit = runBlocking { - val reference = AsyncResult.incomplete() - val client = vertx.createDatagramSocket().handler { res -> - reference.complete(res.data()) - }.listen(19003, "localhost").coAwait() - val discoveryV5Service: DiscoveryV5Service = - DiscoveryService.open( - vertx, - keyPair, - localPort, - bootstrapENRList = bootstrapENRList, - ) - discoveryV5Service.start() - - val datagram = reference.await() - val buffer = ByteBuffer.allocate(datagram.length()) - buffer.put(datagram.bytes) - buffer.flip() - val receivedBytes = Bytes.wrapByteBuffer(buffer) - val content = receivedBytes.slice(45) - - val message = RandomMessage.create( - Message.authTag(), - content, - ) - assertEquals(message.data.size(), Message.RANDOM_DATA_LENGTH) - discoveryV5Service.terminate() - client.close() - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSessionTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSessionTest.kt deleted file mode 100644 index 9ee702a7f..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/HandshakeSessionTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import io.vertx.core.net.SocketAddress -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import org.apache.tuweni.concurrent.coroutines.await -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.bytes.MutableBytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.crypto.SECP256K1.PublicKey -import org.apache.tuweni.v2.crypto.SECP256K1.SecretKey -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import org.apache.tuweni.v2.devp2p.v5.encrypt.AES128GCM -import org.apache.tuweni.v2.devp2p.v5.encrypt.SessionKeyGenerator -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import java.net.InetAddress - -@ExtendWith(BouncyCastleExtension::class) -class HandshakeSessionTest { - - @Test - fun testConnectTwoClients() = - runBlocking { - val secretKey = SecretKey.fromBytes(MutableBytes.fromHexString("0x01").leftPad(32)) - val publicKey = PublicKey.fromSecretKey(secretKey) - val keyPair = SECP256K1.KeyPair.create(secretKey, publicKey) - val peerSecretKey = SecretKey.fromBytes(MutableBytes.fromHexString("0x02").leftPad(32)) - val peerPublicKey = PublicKey.fromSecretKey(peerSecretKey) - val peerKeyPair = SECP256K1.KeyPair.create(peerSecretKey, peerPublicKey) - - val address = SocketAddress.inetSocketAddress(1234, "localhost") - val peerAddress = SocketAddress.inetSocketAddress(1235, "localhost") - val enr = EthereumNodeRecord.create(keyPair, ip = InetAddress.getLoopbackAddress(), udp = 1234) - val peerEnr = EthereumNodeRecord.create(peerKeyPair, ip = InetAddress.getLoopbackAddress(), udp = 1235) - var peerSession: HandshakeSession? = null - - val session = - HandshakeSession( - keyPair, - peerAddress, - peerKeyPair.publicKey(), - { _, message -> runBlocking { peerSession!!.processMessage(message) } }, - { enr }, - Dispatchers.Default, - ) - peerSession = - HandshakeSession( - peerKeyPair, - address, - keyPair.publicKey(), - { _, message -> runBlocking { session.processMessage(message) } }, - { peerEnr }, - Dispatchers.Default, - ) - - val key = session.connect().await() - val peerKey = peerSession.awaitConnection().await() - assertEquals(key, peerKey) - } - - @Test - fun testInitiatorAndRecipientKey() { - val keyPair = SECP256K1.KeyPair.random() - val peerKeyPair = SECP256K1.KeyPair.random() - val ephemeralKeyPair = SECP256K1.KeyPair.random() - val enr = EthereumNodeRecord.create(keyPair, ip = InetAddress.getLoopbackAddress(), udp = 1234) - val peerEnr = EthereumNodeRecord.create(peerKeyPair, ip = InetAddress.getLoopbackAddress(), udp = 1235) - val secret = SECP256K1.deriveECDHKeyAgreement(ephemeralKeyPair.secretKey().bytes(), keyPair.publicKey().bytes()) - val nonce = Bytes.random(12) - val session = SessionKeyGenerator.generate(enr.nodeId(), peerEnr.nodeId(), secret, nonce) - val peerSession = SessionKeyGenerator.generate(enr.nodeId(), peerEnr.nodeId(), secret, nonce) - val authTag = Message.authTag() - val token = Message.authTag() - val encryptedMessage = AES128GCM.encrypt( - session.initiatorKey, - authTag, - Bytes.wrap("hello world".toByteArray()), - token, - ) - val decryptedMessage = AES128GCM.decrypt(peerSession.initiatorKey, authTag, encryptedMessage, token) - assertEquals(Bytes.wrap("hello world".toByteArray()), decryptedMessage) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/PeerRoutingTableTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/PeerRoutingTableTest.kt deleted file mode 100644 index 16071c3bd..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/PeerRoutingTableTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5 - -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.devp2p.DevP2PPeerRoutingTable -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(BouncyCastleExtension::class) -class PeerRoutingTableTest { - - @Test - fun invalidPublicKey() { - val routingTable = DevP2PPeerRoutingTable(SECP256K1.KeyPair.random().publicKey()) - val invalidPublicKey = SECP256K1.PublicKey.fromBytes(Bytes.repeat(0, 64)) - val peers = routingTable.nearest(invalidPublicKey, 3) - assertEquals(0, peers.size) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCMTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCMTest.kt deleted file mode 100644 index c6da864b8..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/AES128GCMTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.encrypt - -import org.apache.tuweni.v2.bytes.Bytes -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class AES128GCMTest { - - @Test - fun encryptPerformsAES128GCMEncryption() { - val expectedResult = Bytes.fromHexString( - "0x943dab6b1f5a0b13e83c41964f818ab8a51d6d30550bae8b33a952aa1b68" + - "18ab88b66dbd60f5e016fa546808d983b70d", - ) - - val key = Bytes.fromHexString("0xA924872EAE2DA2C0057ED6DEBD8CAAB8") - val nonce = Bytes.fromHexString("0x7FC4FDB0E50ACBDA9CD993CFD3A3752104935B91F61B2AF2602C2DC4EFD97AFB") - val data = Bytes.fromHexString("0x19F23925525AF4C2697C1BED166EEB37B5381C10E508A27BCAA02CE661E62A2B") - - val result = AES128GCM.encrypt(key, nonce, data, Bytes.EMPTY) - - assertEquals(expectedResult, result) - } - - @Test - fun decryptPerformsAES128GCMDecryption() { - val expectedResult = Bytes.fromHexString("0x19F23925525AF4C2697C1BED166EEB37B5381C10E508A27BCAA02CE661E62A2B") - val nonce = Bytes.fromHexString("0x7FC4FDB0E50ACBDA9CD993CFD3A3752104935B91F61B2AF2602C2DC4EFD97AFB") - - val encryptedData = Bytes.fromHexString( - "0x943dab6b1f5a0b13e83c41964f818ab8a51d6d30550bae8b33a952aa1b6818a" + - "b88b66dbd60f5e016fa546808d983b70d", - ) - val key = Bytes.fromHexString("0xA924872EAE2DA2C0057ED6DEBD8CAAB8") - - val result = AES128GCM.decrypt(key, nonce, encryptedData, Bytes.EMPTY) - - assertEquals(expectedResult, result) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt deleted file mode 100644 index f97fc967e..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/encrypt/SessionKeyGeneratorTest.kt +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.encrypt - -import org.apache.tuweni.v2.bytes.Bytes -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class SessionKeyGeneratorTest { - - @Test - fun generateCreatesSessionKey() { - val expectedAuthRespKey = Bytes.fromHexString("0xDC999F3F7EF11907F6762497476117C9") - val expectedInitiatorKey = Bytes.fromHexString("0xBBBE757DCE9687BBE5E90CBF9C776163") - val expectedRecipientKey = Bytes.fromHexString("0xE83FC3ED3B32DEE7D81D706FECA6174F") - - val srcNodeId = Bytes.fromHexString("0x9CE70B8F317791EB4E775FF9314B9B7B2CD01D90FF5D0E1979B2EBEB92DCB48D") - val destNodeId = Bytes.fromHexString("0x0B1E82724DB4D17089EF64A441A2C367683EAC448E6AB7F6F8B3094D2B1B2229") - val secret = Bytes.fromHexString("0xAB285AD41C712A917DAC83DE8AAD963285067ED84BAC37052A32BB74DCC75AA5") - val idNonce = Bytes.fromHexString("0x630222D6CD1253BF40CB800F230759F117EC1890CD76792135BBC4D7AAD0B4C1") - - val result = SessionKeyGenerator.generate(srcNodeId, destNodeId, secret, idNonce) - - assertEquals(result.authRespKey, expectedAuthRespKey) - assertEquals(result.initiatorKey, expectedInitiatorKey) - assertEquals(result.recipientKey, expectedRecipientKey) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/FindNodeMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/FindNodeMessageTest.kt deleted file mode 100644 index 0b5dc42ce..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/FindNodeMessageTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.devp2p.v5.FindNodeMessage -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class FindNodeMessageTest { - - @Test - fun encodeCreatesValidBytesSequence() { - val expectedEncodingResult = "0xca88c6e32c5e89caa75480" - - val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") - val message = FindNodeMessage(requestId) - - val encodingResult = message.toRLP() - assertEquals(encodingResult.toHexString(), expectedEncodingResult) - - val decodingResult = FindNodeMessage.create(encodingResult) - - assertEquals(decodingResult.requestId, requestId) - assertEquals(decodingResult.distance, 0) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/MessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/MessageTest.kt deleted file mode 100644 index 9d7d1c037..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/MessageTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.devp2p.v5.Message -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotEquals -import org.junit.jupiter.api.Test - -class MessageTest { - - @Test - fun magicCreatesSha256OfDestNodeIdAndConstantString() { - val destId = Bytes.fromHexString("0xA5CFE10E0EFC543CBE023560B2900E2243D798FAFD0EA46267DDD20D283CE13C") - val expected = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640") - - val result = Message.magic(destId) - - assertEquals(expected, result) - } - - @Test - fun tagHashesSourceAndDestNodeIdCorrectly() { - val srcId = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640") - val destId = Bytes.fromHexString("0xA5CFE10E0EFC543CBE023560B2900E2243D798FAFD0EA46267DDD20D283CE13C") - val expected = Bytes.fromHexString("0xB7A0D7CA8BD37611315DA0882FF479DE14B442FD30AE0EFBE6FC6344D55DC632") - - val result = Message.tag(srcId, destId) - - assertEquals(expected, result) - } - - @Test - fun getSourceFromTagFetchesSrcNodeId() { - val srcId = Bytes.fromHexString("0x98EB6D611291FA21F6169BFF382B9369C33D997FE4DC93410987E27796360640") - val destId = Bytes.fromHexString("0xA5CFE10E0EFC543CBE023560B2900E2243D798FAFD0EA46267DDD20D283CE13C") - val tag = Message.tag(srcId, destId) - - val result = Message.getSourceFromTag(tag, destId) - - assertEquals(srcId, result) - } - - @Test - fun authTagGivesRandom12Bytes() { - val firstResult = Message.authTag() - - assertEquals(Message.AUTH_TAG_LENGTH, firstResult.size()) - - val secondResult = Message.authTag() - - assertNotEquals(secondResult, firstResult) - } - - @Test - fun idNonceGivesRandom32Bytes() { - val firstResult = Message.idNonce() - - assertEquals(Message.ID_NONCE_LENGTH, firstResult.size()) - - val secondResult = Message.idNonce() - - assertNotEquals(secondResult, firstResult) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/NodesMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/NodesMessageTest.kt deleted file mode 100644 index c326fe002..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/NodesMessageTest.kt +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import org.apache.tuweni.v2.devp2p.v5.NodesMessage -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import java.net.InetAddress - -@ExtendWith(BouncyCastleExtension::class) -class NodesMessageTest { - - @Test - fun encodeCreatesValidBytesSequence() { - val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") - val total = 10 - val nodeRecords = listOf( - EthereumNodeRecord.create(SECP256K1.KeyPair.random(), ip = InetAddress.getLoopbackAddress(), udp = 9090), - EthereumNodeRecord.create(SECP256K1.KeyPair.random(), ip = InetAddress.getLoopbackAddress(), udp = 9091), - EthereumNodeRecord.create(SECP256K1.KeyPair.random(), ip = InetAddress.getLoopbackAddress(), udp = 9092), - ) - val message = NodesMessage(requestId, total, nodeRecords) - - val encodingResult = message.toRLP() - - val decodingResult = NodesMessage.create(encodingResult) - - assertEquals(decodingResult.requestId, requestId) - assertEquals(decodingResult.total, 10) - assertEquals(decodingResult.nodeRecords[0].udp(), 9090) - assertEquals(decodingResult.nodeRecords[1].udp(), 9091) - assertEquals(decodingResult.nodeRecords[2].udp(), 9092) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PingMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PingMessageTest.kt deleted file mode 100644 index 79b17afbc..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PingMessageTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.devp2p.v5.PingMessage -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class PingMessageTest { - - @Test - fun encodeCreatesValidBytesSequence() { - val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") - val message = PingMessage(requestId) - - val encodingResult = message.toRLP() - - val decodingResult = PingMessage.create(encodingResult) - - assertEquals(decodingResult.requestId, requestId) - assertEquals(decodingResult.enrSeq, message.enrSeq) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PongMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PongMessageTest.kt deleted file mode 100644 index 77b779905..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/PongMessageTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.devp2p.v5.PongMessage -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class PongMessageTest { - - @Test - fun encodeCreatesValidBytesSequence() { - val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") - val message = PongMessage(requestId, 0, "127.0.0.1", 9090) - - val encodingResult = message.toRLP() - - val decodingResult = PongMessage.create(encodingResult) - - assertEquals(decodingResult.requestId, requestId) - assertEquals(decodingResult.enrSeq, message.enrSeq) - assertEquals(decodingResult.recipientIp, message.recipientIp) - assertEquals(decodingResult.recipientPort, message.recipientPort) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RandomMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RandomMessageTest.kt deleted file mode 100644 index 123e4dab3..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RandomMessageTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.devp2p.v5.Message -import org.apache.tuweni.v2.devp2p.v5.RandomMessage -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotEquals -import org.junit.jupiter.api.Test - -class RandomMessageTest { - - @Test - fun encodeCreatesValidBytesSequence() { - val expectedEncodingResult = - "0xb53ccf732982b8e950836d1e02898c8b38cfdbfdf86bc65c8826506b454e14618ea73612a0f5582c130ff666" - - val data = Bytes.fromHexString(expectedEncodingResult) - val message = RandomMessage(Message.authTag(), data) - - val encodingResult = message.toRLP() - assertEquals(encodingResult.toHexString(), expectedEncodingResult) - - val decodingResult = RandomMessage.create(Message.authTag(), encodingResult) - - assertEquals(decodingResult.data, data) - } - - @Test - fun randomDataGivesRandom44Bytes() { - val firstResult = RandomMessage.randomData() - - assertEquals(Message.RANDOM_DATA_LENGTH, firstResult.size()) - - val secondResult = RandomMessage.randomData() - - assertNotEquals(secondResult, firstResult) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegConfirmationMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegConfirmationMessageTest.kt deleted file mode 100644 index 835cde810..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegConfirmationMessageTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.bytes.Bytes32 -import org.apache.tuweni.v2.devp2p.v5.RegConfirmationMessage -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class RegConfirmationMessageTest { - - @Test - fun encodeCreatesValidBytesSequence() { - val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") - val message = RegConfirmationMessage(requestId, Bytes32.fromRandom()) - - val encodingResult = message.toRLP() - - val decodingResult = RegConfirmationMessage.create(encodingResult) - - assertEquals(decodingResult.requestId, requestId) - assertEquals(decodingResult.topic, message.topic) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegTopicMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegTopicMessageTest.kt deleted file mode 100644 index 0f4794180..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/RegTopicMessageTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.bytes.Bytes32 -import org.apache.tuweni.v2.crypto.SECP256K1 -import org.apache.tuweni.v2.devp2p.EthereumNodeRecord -import org.apache.tuweni.v2.devp2p.v5.RegTopicMessage -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import java.net.InetAddress - -class RegTopicMessageTest { - - @Test - fun encodeCreatesValidBytesSequence() { - val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") - val message = - RegTopicMessage( - requestId, - EthereumNodeRecord.create(SECP256K1.KeyPair.random(), ip = InetAddress.getLoopbackAddress()), - Bytes32.fromRandom(), - Bytes.random(16), - ) - - val encodingResult = message.toRLP() - - val decodingResult = RegTopicMessage.create(encodingResult) - - assertEquals(decodingResult.requestId, requestId) - assertEquals(decodingResult.ticket, message.ticket) - assertEquals(decodingResult.nodeRecord, message.nodeRecord) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TicketMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TicketMessageTest.kt deleted file mode 100644 index 9929c8e5b..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TicketMessageTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.bytes.Bytes32 -import org.apache.tuweni.v2.devp2p.v5.TicketMessage -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class TicketMessageTest { - - @Test - fun encodeCreatesValidBytesSequence() { - val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") - val message = TicketMessage(requestId, Bytes32.fromRandom(), 1000) - - val encodingResult = message.toRLP() - - val decodingResult = TicketMessage.create(encodingResult) - - assertEquals(decodingResult.requestId, requestId) - assertEquals(decodingResult.ticket, message.ticket) - assertEquals(decodingResult.waitTime, message.waitTime) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TopicQueryMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TopicQueryMessageTest.kt deleted file mode 100644 index facc61285..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/TopicQueryMessageTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.bytes.Bytes32 -import org.apache.tuweni.v2.devp2p.v5.TopicQueryMessage -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class TopicQueryMessageTest { - - @Test - fun encodeCreatesValidBytesSequence() { - val requestId = Bytes.fromHexString("0xC6E32C5E89CAA754") - val message = TopicQueryMessage(requestId, Bytes32.fromRandom()) - - val encodingResult = message.toRLP() - - val decodingResult = TopicQueryMessage.create(encodingResult) - - assertEquals(decodingResult.requestId, requestId) - assertEquals(decodingResult.topic, message.topic) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/WhoAreYouMessageTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/WhoAreYouMessageTest.kt deleted file mode 100644 index 592be8493..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/packet/WhoAreYouMessageTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.packet - -import org.apache.tuweni.junit.BouncyCastleExtension -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.devp2p.v5.WhoAreYouMessage -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith - -@ExtendWith(BouncyCastleExtension::class) -class WhoAreYouMessageTest { - - @Test - fun decodeSelf() { - val bytes = - Bytes.fromHexString( - "0x282E641D415A892C05FD03F0AE716BDD92D1569116FDC7C7D3DB39AC5F79B0F7EF8C" + - "E56EDC7BB967899B4C48EEA6A0E838C9091B71DADB98C59508306275AE37A1916EF2517E77CFE09FA006909FE880", - ) - WhoAreYouMessage.create(magic = bytes.slice(0, 32), content = bytes.slice(32)) - } -} diff --git a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TicketTest.kt b/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TicketTest.kt deleted file mode 100644 index 53f88e365..000000000 --- a/devp2p/src/test/kotlin/org/apache/tuweni/v2/devp2p/v5/topic/TicketTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.devp2p.v5.topic - -import org.apache.tuweni.v2.bytes.Bytes -import org.apache.tuweni.v2.bytes.Bytes32 -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class TicketTest { - - @Test - fun roundtrip() { - val ticket = - Ticket(Bytes.wrap("hello world".toByteArray()), Bytes32.fromRandom(), "127.0.0.1", 0L, 0L, 0L) - val key = Bytes.random(16) - val encrypted = ticket.encrypt(key) - assertEquals(Ticket.decrypt(encrypted, key), ticket) - } -} 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 deleted file mode 100644 index 783195c16..000000000 --- a/io/src/main/java/org/apache/tuweni/v2/io/Base32.java +++ /dev/null @@ -1,59 +0,0 @@ -// 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 deleted file mode 100644 index c317f0ae4..000000000 --- a/io/src/main/java/org/apache/tuweni/v2/io/Base58.java +++ /dev/null @@ -1,56 +0,0 @@ -// 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 deleted file mode 100644 index d1af7ab89..000000000 --- a/io/src/main/java/org/apache/tuweni/v2/io/Base58Codec.java +++ /dev/null @@ -1,95 +0,0 @@ -// 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 deleted file mode 100644 index 32c4e2945..000000000 --- a/io/src/main/java/org/apache/tuweni/v2/io/Base64.java +++ /dev/null @@ -1,56 +0,0 @@ -// 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 deleted file mode 100644 index db22b2244..000000000 --- a/io/src/main/java/org/apache/tuweni/v2/io/Base64URLSafe.java +++ /dev/null @@ -1,56 +0,0 @@ -// 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 deleted file mode 100644 index 08aa51e7b..000000000 --- a/io/src/main/java/org/apache/tuweni/v2/io/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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 deleted file mode 100644 index 90c6b10e5..000000000 --- a/io/src/test/java/org/apache/tuweni/v2/io/Base32Test.java +++ /dev/null @@ -1,37 +0,0 @@ -// 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 deleted file mode 100644 index 78b7f2448..000000000 --- a/io/src/test/java/org/apache/tuweni/v2/io/Base58Test.java +++ /dev/null @@ -1,77 +0,0 @@ -// 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 deleted file mode 100644 index a485397e7..000000000 --- a/io/src/test/java/org/apache/tuweni/v2/io/Base64Test.java +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index 0d8ff0814..000000000 --- a/io/src/test/java/org/apache/tuweni/v2/io/Base64URLSafeTest.java +++ /dev/null @@ -1,43 +0,0 @@ -// 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 deleted file mode 100644 index e7c321139..000000000 --- a/net/src/main/java/org/apache/tuweni/v2/net/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * 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 deleted file mode 100644 index 2df434e64..000000000 --- a/net/src/main/java/org/apache/tuweni/v2/net/tls/ClientFingerprintTrustManager.java +++ /dev/null @@ -1,123 +0,0 @@ -// 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 deleted file mode 100644 index eec5ab75e..000000000 --- a/net/src/main/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepository.java +++ /dev/null @@ -1,167 +0,0 @@ -// 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 deleted file mode 100644 index 7931ade26..000000000 --- a/net/src/main/java/org/apache/tuweni/v2/net/tls/FingerprintRepository.java +++ /dev/null @@ -1,35 +0,0 @@ -// 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 deleted file mode 100644 index cf580363b..000000000 --- a/net/src/main/java/org/apache/tuweni/v2/net/tls/ServerFingerprintTrustManager.java +++ /dev/null @@ -1,121 +0,0 @@ -// 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 deleted file mode 100644 index 16b0934e2..000000000 --- a/net/src/main/java/org/apache/tuweni/v2/net/tls/TLS.java +++ /dev/null @@ -1,219 +0,0 @@ -// 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 deleted file mode 100644 index c1b982308..000000000 --- a/net/src/main/java/org/apache/tuweni/v2/net/tls/TLSEnvironmentException.java +++ /dev/null @@ -1,14 +0,0 @@ -// 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 deleted file mode 100644 index 7c3a08257..000000000 --- a/net/src/main/java/org/apache/tuweni/v2/net/tls/package-info.java +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 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 deleted file mode 100644 index b907d8f3e..000000000 --- a/net/src/test/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepositoryTest.java +++ /dev/null @@ -1,98 +0,0 @@ -// 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/rlp/src/main/java/org/apache/tuweni/v2/rlp/AccumulatingRLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/AccumulatingRLPWriter.java deleted file mode 100644 index 6d336d2ff..000000000 --- a/rlp/src/main/java/org/apache/tuweni/v2/rlp/AccumulatingRLPWriter.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import static java.util.Objects.requireNonNull; -import static org.apache.tuweni.v2.rlp.RLP.encodeByteArray; -import static org.apache.tuweni.v2.rlp.RLP.encodeLength; -import static org.apache.tuweni.v2.rlp.RLP.encodeNumber; - -import org.apache.tuweni.v2.bytes.Bytes; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.function.Consumer; - -final class AccumulatingRLPWriter implements RLPWriter { - - private static final int COMBINE_THRESHOLD = 32; - - private ArrayDeque values = new ArrayDeque<>(); - - Deque values() { - return values; - } - - @Override - public void writeRLP(Bytes value) { - requireNonNull(value); - appendBytes(value.toArrayUnsafe()); - } - - @Override - public void writeValue(Bytes value) { - requireNonNull(value); - writeByteArray(value.toArrayUnsafe()); - } - - @Override - public void writeByteArray(byte[] value) { - encodeByteArray(value, this::appendBytes); - } - - @Override - public void writeByte(byte value) { - encodeByteArray(new byte[] {value}, this::appendBytes); - } - - @Override - public void writeLong(long value) { - appendBytes(encodeNumber(value)); - } - - @Override - public void writeList(Consumer fn) { - requireNonNull(fn); - AccumulatingRLPWriter listWriter = new AccumulatingRLPWriter(); - fn.accept(listWriter); - int totalSize = 0; - for (byte[] value : listWriter.values) { - try { - totalSize = Math.addExact(totalSize, value.length); - } catch (ArithmeticException e) { - throw new IllegalArgumentException( - "Combined length of values is too long (> Integer.MAX_VALUE)"); - } - } - appendBytes(encodeLength(totalSize, 0xc0)); - this.values.addAll(listWriter.values); - } - - private void appendBytes(byte[] bytes) { - if (bytes.length < COMBINE_THRESHOLD) { - if (!values.isEmpty()) { - byte[] last = values.getLast(); - if (last.length <= (COMBINE_THRESHOLD - bytes.length)) { - byte[] combined = new byte[last.length + bytes.length]; - System.arraycopy(last, 0, combined, 0, last.length); - System.arraycopy(bytes, 0, combined, last.length, bytes.length); - values.pollLast(); - values.add(combined); - return; - } - } - } - values.add(bytes); - } -} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/ByteBufferRLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/ByteBufferRLPWriter.java deleted file mode 100644 index 4eca78f5a..000000000 --- a/rlp/src/main/java/org/apache/tuweni/v2/rlp/ByteBufferRLPWriter.java +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import static java.util.Objects.requireNonNull; -import static org.apache.tuweni.v2.rlp.RLP.encodeByteArray; -import static org.apache.tuweni.v2.rlp.RLP.encodeLength; -import static org.apache.tuweni.v2.rlp.RLP.encodeNumber; - -import org.apache.tuweni.v2.bytes.Bytes; - -import java.nio.BufferOverflowException; -import java.nio.ByteBuffer; -import java.util.Deque; -import java.util.function.Consumer; - -final class ByteBufferRLPWriter implements RLPWriter { - - private ByteBuffer buffer; - - ByteBufferRLPWriter(ByteBuffer buffer) { - this.buffer = buffer; - } - - @Override - public void writeRLP(Bytes value) { - buffer.put(value.toArrayUnsafe()); - } - - @Override - public void writeValue(Bytes value) { - encodeByteArray(value.toArrayUnsafe(), buffer::put); - } - - @Override - public void writeByteArray(byte[] value) { - encodeByteArray(value, buffer::put); - } - - @Override - public void writeByte(byte value) { - encodeByteArray(new byte[] {value}, buffer::put); - } - - @Override - public void writeLong(long value) { - buffer.put(encodeNumber(value)); - } - - @Override - public void writeList(Consumer fn) { - requireNonNull(fn); - AccumulatingRLPWriter listWriter = new AccumulatingRLPWriter(); - fn.accept(listWriter); - writeEncodedValuesAsList(listWriter.values()); - } - - private void writeEncodedValuesAsList(Deque values) { - int totalSize = 0; - for (byte[] value : values) { - try { - totalSize = Math.addExact(totalSize, value.length); - } catch (ArithmeticException e) { - throw new BufferOverflowException(); - } - } - buffer.put(encodeLength(totalSize, 0xc0)); - values.forEach(bytes -> buffer.put(bytes)); - } -} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPReader.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPReader.java deleted file mode 100644 index b54f77707..000000000 --- a/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPReader.java +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import org.apache.tuweni.rlp.EndOfRLPException; -import org.apache.tuweni.rlp.InvalidRLPEncodingException; -import org.apache.tuweni.rlp.InvalidRLPTypeException; -import org.apache.tuweni.v2.bytes.Bytes; - -import java.util.function.Function; - -final class BytesRLPReader implements RLPReader { - - private final Bytes content; - private boolean lenient; - private int index = 0; - - BytesRLPReader(Bytes content, boolean lenient) { - this.content = content; - this.lenient = lenient; - } - - @Override - public boolean isLenient() { - return lenient; - } - - @Override - public Bytes readRemaining() { - int remaining = content.size() - index; - if (remaining == 0) { - return Bytes.EMPTY; - } - return content.slice(index++, remaining); - } - - @Override - public Bytes readValue(boolean lenient) { - int remaining = content.size() - index; - if (remaining == 0) { - throw new EndOfRLPException(); - } - int prefix = (((int) content.get(index)) & 0xFF); - if (prefix <= 0x7f) { - return content.slice(index++, 1); - } - remaining--; - - if (prefix <= 0xb7) { - int length = prefix - 0x80; - if (remaining < length) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + length - + " but have only " - + remaining); - } - Bytes bytes = content.slice(index + 1, length); - if (!lenient && length == 1 && (bytes.get(0) & 0xFF) <= 0x7f) { - throw new InvalidRLPEncodingException( - "Value should have been encoded as a single byte " + bytes.toHexString()); - } - index += 1 + length; - return bytes; - } - if (prefix <= 0xbf) { - int lengthOfLength = prefix - 0xb7; - if (remaining < lengthOfLength) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + lengthOfLength - + " but have only " - + remaining); - } - - remaining -= lengthOfLength; - int length = getLength(lengthOfLength, lenient, "value"); - - if (remaining < length) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + length - + " but have only " - + remaining); - } - - index += 1 + lengthOfLength; - Bytes bytes = content.slice(index, length); - index += length; - return bytes; - } - throw new InvalidRLPTypeException("Attempted to read a value but next item is a list"); - } - - @Override - public boolean nextIsList() { - int remaining = content.size() - index; - if (remaining == 0) { - throw new EndOfRLPException(); - } - int prefix = (((int) content.get(index)) & 0xFF); - return prefix > 0xbf; - } - - @Override - public boolean nextIsEmpty() { - int remaining = content.size() - index; - if (remaining == 0) { - throw new EndOfRLPException(); - } - int prefix = (((int) content.get(index)) & 0xFF); - return prefix == 0x80; - } - - @Override - public T readList(boolean lenient, Function fn) { - return fn.apply(new BytesRLPReader(readList(lenient), lenient)); - } - - @Override - public void skipNext(boolean lenient) { - int remaining = content.size() - index; - if (remaining == 0) { - throw new EndOfRLPException(); - } - int prefix = (((int) content.get(index)) & 0xFF); - if (prefix <= 0x7f) { - index++; - return; - } - remaining--; - - if (prefix <= 0xb7) { - int length = prefix - 0x80; - if (remaining < length) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + length - + " but have only " - + remaining); - } - if (!lenient && length == 1 && (content.get(index + 1) & 0xFF) <= 0x7f) { - throw new InvalidRLPEncodingException( - "Value should have been encoded as a single byte " - + content.slice(index + 1, 1).toHexString()); - } - index += 1 + length; - return; - } - if (prefix <= 0xbf) { - int lengthOfLength = prefix - 0xb7; - if (remaining < lengthOfLength) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + lengthOfLength - + " but have only " - + remaining); - } - - remaining -= lengthOfLength; - int length = getLength(lengthOfLength, lenient, "value"); - - if (remaining < length) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + length - + " but have only " - + remaining); - } - - index += 1 + lengthOfLength + length; - return; - } - if (prefix <= 0xf7) { - int length = prefix - 0xc0; - if (remaining < length) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + length - + " but have only " - + remaining); - } - index += 1 + length; - return; - } - - int lengthOfLength = prefix - 0xf7; - if (remaining < lengthOfLength) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + lengthOfLength - + " but have only " - + remaining); - } - - remaining -= lengthOfLength; - int length = getLength(lengthOfLength, lenient, "list"); - - if (remaining < length) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + lengthOfLength - + " but have only " - + remaining); - } - - index += 1 + lengthOfLength + length; - } - - @Override - public int remaining() { - int oldIndex = index; - try { - int count = 0; - while (!isComplete()) { - count++; - skipNext(); - } - return count; - } finally { - index = oldIndex; - } - } - - @Override - public boolean isComplete() { - return (content.size() - index) == 0; - } - - @Override - public int position() { - return index; - } - - private Bytes readList(boolean lenient) { - int remaining = content.size() - index; - if (remaining == 0) { - throw new EndOfRLPException(); - } - int prefix = (((int) content.get(index)) & 0xFF); - if (prefix <= 0xbf) { - throw new InvalidRLPTypeException("Attempted to read a list but next item is a value"); - } - remaining--; - - if (prefix <= 0xf7) { - int length = prefix - 0xc0; - if (remaining < length) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + length - + " but have only " - + remaining); - } - index++; - Bytes bytes = content.slice(index, length); - index += length; - return bytes; - } - - int lengthOfLength = prefix - 0xf7; - if (remaining < lengthOfLength) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " - + lengthOfLength - + " but have only " - + remaining); - } - - remaining -= lengthOfLength; - int length = getLength(lengthOfLength, lenient, "list"); - - if (remaining < length) { - throw new InvalidRLPEncodingException( - "Insufficient bytes in RLP encoding: expected " + length + " but have only " + remaining); - } - - index += 1 + lengthOfLength; - Bytes bytes = content.slice(index, length); - index += length; - return bytes; - } - - private int getLength(int lengthOfLength, boolean lenient, String type) { - Bytes lengthBytes = content.slice(index + 1, lengthOfLength); - if (!lenient) { - if (lengthBytes.hasLeadingZeroByte()) { - throw new InvalidRLPEncodingException( - "RLP " + type + " length contains leading zero bytes"); - } - } else { - lengthBytes = lengthBytes.trimLeadingZeros(); - } - if (lengthBytes.size() == 0) { - throw new InvalidRLPEncodingException("RLP " + type + " length is zero"); - } - // Check if the length is greater than a 4 byte integer - if (lengthBytes.size() > 4) { - throw new InvalidRLPEncodingException("RLP " + type + " length is oversized"); - } - int length = lengthBytes.toInt(); - if (length < 0) { - // Java ints are two's compliment, so this was oversized - throw new InvalidRLPEncodingException("RLP " + type + " length is oversized"); - } - assert length > 0; - if (!lenient && length <= 55) { - throw new InvalidRLPEncodingException( - "RLP " + type + " length of " + length + " was not minimally encoded"); - } - return length; - } -} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPWriter.java deleted file mode 100644 index edbe5eeb9..000000000 --- a/rlp/src/main/java/org/apache/tuweni/v2/rlp/BytesRLPWriter.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import org.apache.tuweni.v2.bytes.Bytes; - -import java.util.Deque; - -final class BytesRLPWriter extends DelegatingRLPWriter { - - BytesRLPWriter() { - super(new AccumulatingRLPWriter()); - } - - Bytes toBytes() { - Deque values = delegate.values(); - if (values.isEmpty()) { - return Bytes.EMPTY; - } - return Bytes.wrap(values.stream().map(Bytes::wrap).toArray(Bytes[]::new)); - } -} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/DelegatingRLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/DelegatingRLPWriter.java deleted file mode 100644 index f0c9ba3b0..000000000 --- a/rlp/src/main/java/org/apache/tuweni/v2/rlp/DelegatingRLPWriter.java +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; - -import java.math.BigInteger; -import java.util.function.Consumer; - -class DelegatingRLPWriter implements RLPWriter { - - T delegate; - - DelegatingRLPWriter(T delegate) { - this.delegate = delegate; - } - - @Override - public void writeRLP(Bytes value) { - delegate.writeRLP(value); - } - - @Override - public void writeValue(Bytes value) { - delegate.writeValue(value); - } - - @Override - public void writeByteArray(byte[] value) { - delegate.writeByteArray(value); - } - - @Override - public void writeByte(byte value) { - delegate.writeByte(value); - } - - @Override - public void writeInt(int value) { - delegate.writeInt(value); - } - - @Override - public void writeLong(long value) { - delegate.writeLong(value); - } - - @Override - public void writeUInt256(UInt256 value) { - delegate.writeUInt256(value); - } - - @Override - public void writeBigInteger(BigInteger value) { - delegate.writeBigInteger(value); - } - - @Override - public void writeString(String str) { - delegate.writeString(str); - } - - @Override - public void writeList(Consumer fn) { - delegate.writeList(fn); - } -} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLP.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLP.java deleted file mode 100644 index af4e1a96c..000000000 --- a/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLP.java +++ /dev/null @@ -1,528 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Objects.requireNonNull; - -import org.apache.tuweni.rlp.EndOfRLPException; -import org.apache.tuweni.rlp.InvalidRLPEncodingException; -import org.apache.tuweni.rlp.InvalidRLPTypeException; -import org.apache.tuweni.v2.bytes.Bytes; - -import java.math.BigInteger; -import java.nio.BufferOverflowException; -import java.nio.ByteBuffer; -import java.nio.ReadOnlyBufferException; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; - -/** Recursive Length Prefix (RLP) encoding and decoding. */ -public final class RLP { - private static final byte[] EMPTY_VALUE = new byte[] {(byte) 0x80}; - - private RLP() {} - - /** - * Encode values to a {@link Bytes} value. - * - *

Important: this method does not write any list prefix to the result. If you are writing a - * RLP encoded list of values, you usually want to use {@link #encodeList(Consumer)}. - * - * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. - * @return The RLP encoding in a {@link Bytes} value. - */ - public static Bytes encode(Consumer fn) { - requireNonNull(fn); - BytesRLPWriter writer = new BytesRLPWriter(); - fn.accept(writer); - return writer.toBytes(); - } - - /** - * Encode values to a {@link ByteBuffer}. - * - *

Important: this method does not write any list prefix to the result. If you are writing a - * RLP encoded list of values, you usually want to use {@link #encodeList(Consumer)}. - * - * @param buffer The buffer to write into, starting from its current position. - * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. - * @param The type of the buffer. - * @return The buffer. - * @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 static T encodeTo(T buffer, Consumer fn) { - requireNonNull(fn); - ByteBufferRLPWriter writer = new ByteBufferRLPWriter(buffer); - fn.accept(writer); - return buffer; - } - - /** - * Encode a list of values to a {@link Bytes} value. - * - * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. - * @return The RLP encoding in a {@link Bytes} value. - */ - public static Bytes encodeList(Consumer fn) { - requireNonNull(fn); - BytesRLPWriter writer = new BytesRLPWriter(); - writer.writeList(fn); - return writer.toBytes(); - } - - /** - * Encode a list of values to a {@link Bytes} value. - * - * @param elements A list of values to be encoded. - * @param fn A consumer that will be provided with a {@link RLPWriter} and an element of the list. - * @param The type of the list elements. - * @return The RLP encoding in a {@link Bytes} value. - */ - public static Bytes encodeList(List elements, BiConsumer fn) { - requireNonNull(fn); - BytesRLPWriter writer = new BytesRLPWriter(); - writer.writeList(elements, fn); - return writer.toBytes(); - } - - /** - * Encode a list of values to a {@link ByteBuffer}. - * - * @param buffer The buffer to write into, starting from its current position. - * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. - * @param The type of the buffer. - * @return The buffer. - * @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 static T encodeListTo(T buffer, Consumer fn) { - requireNonNull(fn); - ByteBufferRLPWriter writer = new ByteBufferRLPWriter(buffer); - writer.writeList(fn); - return buffer; - } - - /** - * Encode a value to a {@link Bytes} value. - * - * @param value The value to encode. - * @return The RLP encoding in a {@link Bytes} value. - */ - public static Bytes encodeValue(Bytes value) { - requireNonNull(value); - return encodeValue(value.toArrayUnsafe()); - } - - /** - * Encode a value to a {@link Bytes} value. - * - * @param value The value to encode. - * @return The RLP encoding in a {@link Bytes} value. - */ - public static Bytes encodeByteArray(byte[] value) { - requireNonNull(value); - return encodeValue(value); - } - - private static Bytes encodeValue(byte[] value) { - int maxSize = value.length + 5; - ByteBuffer buffer = ByteBuffer.allocate(maxSize); - encodeByteArray(value, buffer::put); - return Bytes.wrap(buffer.array(), 0, buffer.position()); - } - - static void encodeByteArray(byte[] value, Consumer appender) { - requireNonNull(value); - int size = value.length; - if (size == 0) { - appender.accept(EMPTY_VALUE); - return; - } - if (size == 1) { - byte b = value[0]; - if ((b & 0xFF) <= 0x7f) { - appender.accept(value); - return; - } - } - appender.accept(encodeLength(size, 0x80)); - appender.accept(value); - } - - /** - * Encode a integer to a {@link Bytes} value. - * - * @param value The integer to encode. - * @return The RLP encoding in a {@link Bytes} value. - */ - public static Bytes encodeInt(int value) { - return encodeLong(value); - } - - /** - * Encode a long to a {@link Bytes} value. - * - * @param value The long to encode. - * @return The RLP encoding in a {@link Bytes} value. - */ - public static Bytes encodeLong(long value) { - return Bytes.wrap(encodeNumber(value)); - } - - static byte[] encodeNumber(long value) { - if (value == 0x00) { - return EMPTY_VALUE; - } - if (value <= 0x7f) { - return new byte[] {(byte) (value & 0xFF)}; - } - return encodeLongBytes(value, 0x80); - } - - private static byte[] encodeLongBytes(long value, int offset) { - int zeros = Long.numberOfLeadingZeros(value); - int resultBytes = 8 - (zeros / 8); - - byte[] encoded = new byte[resultBytes + 1]; - encoded[0] = (byte) ((offset + resultBytes) & 0xFF); - - int shift = 0; - for (int i = 0; i < resultBytes; i++) { - encoded[resultBytes - i] = (byte) ((value >> shift) & 0xFF); - shift += 8; - } - return encoded; - } - - /** - * Encode a big integer to a {@link Bytes} value. - * - * @param value The big integer to encode. - * @return The RLP encoding in a {@link Bytes} value. - */ - public static Bytes encodeBigInteger(BigInteger value) { - requireNonNull(value); - return encode(writer -> writer.writeBigInteger(value)); - } - - /** - * Encode a string to a {@link Bytes} value. - * - * @param str The string to encode. - * @return The RLP encoding in a {@link Bytes} value. - */ - public static Bytes encodeString(String str) { - requireNonNull(str); - return encodeByteArray(str.getBytes(UTF_8)); - } - - static byte[] encodeLength(int length, int offset) { - if (length <= 55) { - return new byte[] {(byte) ((offset + length) & 0xFF)}; - } - return encodeLongBytes(length, offset + 55); - } - - /** - * Read and decode RLP from a {@link Bytes} value. - * - *

Important: this method does not consume any list prefix from the source data. If you are - * reading a RLP encoded list of values, you usually want to use {@link #decodeList(Bytes, - * Function)}. - * - * @param source The RLP encoded bytes. - * @param fn A function that will be provided a {@link RLPReader}. - * @param The result type of the reading function. - * @return The result from the reading function. - */ - public static T decode(Bytes source, Function fn) { - return decode(source, false, fn); - } - - /** - * Read and decode RLP from a {@link Bytes} value. - * - *

Important: this method does not consume any list prefix from the source data. If you are - * reading a RLP encoded list of values, you usually want to use {@link #decodeList(Bytes, - * Function)}. - * - * @param source The RLP encoded bytes. - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @param fn A function that will be provided a {@link RLPReader}. - * @param The result type of the reading function. - * @return The result from the reading function. - */ - public static T decode(Bytes source, boolean lenient, Function fn) { - requireNonNull(source); - requireNonNull(fn); - return fn.apply(new BytesRLPReader(source, lenient)); - } - - /** - * Read an RLP encoded list of values from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @param fn A function that will be provided a {@link RLPReader}. - * @param The result type of the reading function. - * @return The result from the reading function. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the first RLP value is not a list. - */ - public static T decodeList(Bytes source, Function fn) { - return decodeList(source, false, fn); - } - - /** - * Read an RLP encoded list of values from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @param fn A function that will be provided a {@link RLPReader}. - * @param The result type of the reading function. - * @return The result from the reading function. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the first RLP value is not a list. - */ - public static T decodeList(Bytes source, boolean lenient, Function fn) { - requireNonNull(source); - requireNonNull(fn); - if (source.isEmpty()) { - throw new IllegalArgumentException("source is empty"); - } - return decode(source, lenient, reader -> reader.readList(fn)); - } - - /** - * Read an RLP encoded list of values from a {@link Bytes} value, populating a mutable output - * list. - * - * @param source The RLP encoded bytes. - * @param fn A function that will be provided a {@link RLPReader}. - * @return The list supplied to {@code fn}. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the first RLP value is not a list. - */ - public static List decodeToList(Bytes source, BiConsumer> fn) { - return decodeToList(source, false, fn); - } - - /** - * Read an RLP encoded list of values from a {@link Bytes} value, populating a mutable output - * list. - * - * @param source The RLP encoded bytes. - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @param fn A function that will be provided a {@link RLPReader}. - * @return The list supplied to {@code fn}. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the first RLP value is not a list. - */ - public static List decodeToList( - Bytes source, boolean lenient, BiConsumer> fn) { - requireNonNull(source); - requireNonNull(fn); - if (source.isEmpty()) { - throw new IllegalArgumentException("source is empty"); - } - return decode(source, lenient, reader -> reader.readList(fn)); - } - - /** - * Read a list of values from the RLP source, populating a list using a function interpreting each - * value. - * - * @param source The RLP encoded bytes. - * @param fn A function creating a new element of the list for each value in the RLP list. - * @return The list supplied to {@code fn}. - * @param The type of the list elements. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the first RLP value is not a list. - */ - public static List decodeToList(Bytes source, Function fn) { - return decodeToList(source, false, fn); - } - - /** - * Read an RLP encoded list of values from a {@link Bytes} value, populating a mutable output - * list. - * - * @param source The RLP encoded bytes. - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @param fn A function creating a new element of the list for each value in the RLP list. - * @param The type of the list elements. - * @return The list supplied to {@code fn}. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the first RLP value is not a list. - */ - public static List decodeToList(Bytes source, boolean lenient, Function fn) { - requireNonNull(source); - requireNonNull(fn); - if (source.isEmpty()) { - throw new IllegalArgumentException("source is empty"); - } - return decode(source, lenient, reader -> reader.readListContents(fn)); - } - - /** - * Read an RLP encoded value from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @return The bytes for the value. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws EndOfRLPException If there are no RLP values to read. - */ - public static Bytes decodeValue(Bytes source) { - return decodeValue(source, false); - } - - /** - * Read an RLP encoded value from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @return The bytes for the value. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws EndOfRLPException If there are no RLP values to read. - */ - public static Bytes decodeValue(Bytes source, boolean lenient) { - requireNonNull(source); - return decode(source, lenient, RLPReader::readValue); - } - - /** - * Read an RLP encoded integer from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @return An integer. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - */ - public static int decodeInt(Bytes source) { - return decodeInt(source, false); - } - - /** - * Read an RLP encoded integer from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @return An integer. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - */ - public static int decodeInt(Bytes source, boolean lenient) { - requireNonNull(source); - if (source.isEmpty()) { - throw new IllegalArgumentException("source is empty"); - } - return decode(source, lenient, RLPReader::readInt); - } - - /** - * Read an RLP encoded long from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @return A long. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - */ - public static long decodeLong(Bytes source) { - return decodeLong(source, false); - } - - /** - * Read an RLP encoded long from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @return A long. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - */ - public static long decodeLong(Bytes source, boolean lenient) { - requireNonNull(source); - if (source.isEmpty()) { - throw new IllegalArgumentException("source is empty"); - } - return decode(source, lenient, RLPReader::readLong); - } - - /** - * Read an RLP encoded big integer from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @return A {@link BigInteger}. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - */ - public static BigInteger decodeBigInteger(Bytes source) { - return decodeBigInteger(source, false); - } - - /** - * Read an RLP encoded big integer from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @return A {@link BigInteger}. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - */ - public static BigInteger decodeBigInteger(Bytes source, boolean lenient) { - requireNonNull(source); - if (source.isEmpty()) { - throw new IllegalArgumentException("source is empty"); - } - return decode(source, lenient, RLPReader::readBigInteger); - } - - /** - * Read an RLP encoded string from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @return A string. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - */ - public static String decodeString(Bytes source) { - return decodeString(source, false); - } - - /** - * Read an RLP encoded string from a {@link Bytes} value. - * - * @param source The RLP encoded bytes. - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @return A string. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - */ - public static String decodeString(Bytes source, boolean lenient) { - requireNonNull(source); - if (source.isEmpty()) { - throw new IllegalArgumentException("source is empty"); - } - return decode(source, lenient, RLPReader::readString); - } - - /** - * Check if the {@link Bytes} value contains an RLP encoded list. - * - * @param value The value to check. - * @return {@code true} if the value contains a list. - */ - public static boolean isList(Bytes value) { - requireNonNull(value); - if (value.isEmpty()) { - throw new IllegalArgumentException("value is empty"); - } - return decode(value, RLPReader::nextIsList); - } -} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPReader.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPReader.java deleted file mode 100644 index 4a009246b..000000000 --- a/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPReader.java +++ /dev/null @@ -1,424 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Objects.requireNonNull; - -import org.apache.tuweni.rlp.EndOfRLPException; -import org.apache.tuweni.rlp.InvalidRLPEncodingException; -import org.apache.tuweni.rlp.InvalidRLPTypeException; -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Function; - -/** A reader for consuming values from an RLP encoded source. */ -public interface RLPReader { - - /** - * Determine if this reader is lenient by default. - * - *

A non-lenient reader will throw {@link InvalidRLPEncodingException} from any read method if - * the source RLP has not used a minimal encoding format for the value. - * - * @return {@code true} if the reader is lenient, and {@code false} otherwise (default). - */ - boolean isLenient(); - - /** - * Read the next value from the RLP source. - * - * @return The bytes for the next value. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default Bytes readValue() { - return readValue(isLenient()); - } - - /** - * Read the next value from the RLP source. - * - * @param lenient If {@code false}, an exception will be thrown if the value is not minimally - * encoded. - * @return The bytes for the next value. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - Bytes readValue(boolean lenient); - - /** - * Read a byte array from the RLP source. - * - * @return The byte array for the next value. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default byte[] readByteArray() { - return readValue().toArrayUnsafe(); - } - - /** - * Read a byte from the RLP source. - * - * @return The byte for the next value. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default byte readByte() { - return readByte(isLenient()); - } - - /** - * Read a byte from the RLP source. - * - * @param lenient If {@code false}, an exception will be thrown if the byte is not minimally - * encoded. - * @return The byte for the next value. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default byte readByte(boolean lenient) { - Bytes bytes = readValue(lenient); - if (bytes.size() != 1) { - throw new InvalidRLPTypeException("Value is not a single byte"); - } - return bytes.get(0); - } - - /** - * Read an integer value from the RLP source. - * - * @return An integer. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as an integer. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default int readInt() { - return readInt(isLenient()); - } - - /** - * Read an integer value from the RLP source. - * - * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally - * encoded. - * @return An integer. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the - * integer is not minimally encoded and `lenient` is {@code false}. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as an integer. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default int readInt(boolean lenient) { - Bytes bytes = readValue(); - if (!lenient && bytes.hasLeadingZeroByte()) { - throw new InvalidRLPEncodingException("Integer value was not minimally encoded"); - } - try { - return bytes.toInt(); - } catch (IllegalArgumentException e) { - throw new InvalidRLPTypeException("Value is too large to be represented as an int"); - } - } - - /** - * Read a long value from the RLP source. - * - * @return A long. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default long readLong() { - return readLong(isLenient()); - } - - /** - * Read a long value from the RLP source. - * - * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally - * encoded. - * @return A long. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the - * integer is not minimally encoded and `lenient` is {@code false}. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default long readLong(boolean lenient) { - Bytes bytes = readValue(); - if (!lenient && bytes.hasLeadingZeroByte()) { - throw new InvalidRLPEncodingException("Integer value was not minimally encoded"); - } - try { - return bytes.toLong(); - } catch (IllegalArgumentException e) { - throw new InvalidRLPTypeException("Value is too large to be represented as a long"); - } - } - - /** - * Read a {@link UInt256} value from the RLP source. - * - * @return A {@link UInt256} value. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default UInt256 readUInt256() { - return readUInt256(isLenient()); - } - - /** - * Read a {@link UInt256} value from the RLP source. - * - * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally - * encoded. - * @return A {@link UInt256} value. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the - * integer is not minimally encoded and `lenient` is {@code false}. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a long. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default UInt256 readUInt256(boolean lenient) { - Bytes bytes = readValue(); - if (!lenient && bytes.hasLeadingZeroByte()) { - throw new InvalidRLPEncodingException("Integer value was not minimally encoded"); - } - try { - return UInt256.fromBytes(bytes); - } catch (IllegalArgumentException e) { - throw new InvalidRLPTypeException("Value is too large to be represented as a UInt256"); - } - } - - /** - * Read a big integer value from the RLP source. - * - * @return A big integer. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a big integer. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default BigInteger readBigInteger() { - return readBigInteger(isLenient()); - } - - /** - * Read a big integer value from the RLP source. - * - * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally - * encoded. - * @return A big integer. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source, or the - * integer is not minimally encoded and `lenient` is {@code false}. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a big integer. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default BigInteger readBigInteger(boolean lenient) { - Bytes bytes = readValue(); - if (!lenient && bytes.hasLeadingZeroByte()) { - throw new InvalidRLPEncodingException("Integer value was not minimally encoded"); - } - return bytes.toUnsignedBigInteger(); - } - - /** - * Read a string value from the RLP source. - * - * @return A string. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a string. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default String readString() { - return readString(isLenient()); - } - - /** - * Read a string value from the RLP source. - * - * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally - * encoded. - * @return A string. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value cannot be represented as a string. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default String readString(boolean lenient) { - return new String(readValue(lenient).toArrayUnsafe(), UTF_8); - } - - /** - * Check if the next item to be read is a list. - * - * @return {@code true} if the next item to be read is a list. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - boolean nextIsList(); - - /** - * Check if the next item to be read is empty. - * - * @return {@code true} if the next item to be read is empty. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - boolean nextIsEmpty(); - - /** - * Read a list of values from the RLP source. - * - * @param fn A function that will be provided a {@link RLPReader}. - * @param The result type of the reading function. - * @return The result from the reading function. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value is not a list. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default T readList(Function fn) { - return readList(isLenient(), fn); - } - - /** - * Read a list of values from the RLP source. - * - * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally - * encoded. - * @param fn A function that will be provided a {@link RLPReader}. - * @param The result type of the reading function. - * @return The result from the reading function. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value is not a list. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - T readList(boolean lenient, Function fn); - - /** - * Read a list of values from the RLP source, populating a mutable output list. - * - * @param fn A function that will be provided with a {@link RLPReader} and a mutable output list. - * @return The list supplied to {@code fn}. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value is not a list. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default List readList(BiConsumer> fn) { - return readList(isLenient(), fn); - } - - /** - * Read a list of values from the RLP source, populating a list using a function interpreting each - * value. - * - * @param fn A function creating a new element of the list for each value in the RLP list. - * @param The type of the list elements. - * @return The list supplied to {@code fn}. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value is not a list. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default List readListContents(Function fn) { - return readListContents(isLenient(), fn); - } - - /** - * Read a list of values from the RLP source, populating a mutable output list. - * - * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally - * encoded. - * @param fn A function that will be provided with a {@link RLPReader} and a mutable output list. - * @return The list supplied to {@code fn}. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value is not a list. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default List readList(boolean lenient, BiConsumer> fn) { - requireNonNull(fn); - return readList( - lenient, - reader -> { - List list = new ArrayList<>(); - fn.accept(reader, list); - return list; - }); - } - - /** - * Read a list of values from the RLP source, populating a list using a function interpreting each - * value. - * - * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally - * encoded. - * @param fn A function creating a new element of the list for each value in the RLP list. - * @param The type of the list elements. - * @return The list supplied to {@code fn}. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws InvalidRLPTypeException If the next RLP value is not a list. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default List readListContents(boolean lenient, Function fn) { - requireNonNull(fn); - - return readList( - lenient, - reader -> { - List list = new ArrayList(); - while (!reader.isComplete()) { - list.add(fn.apply(reader)); - } - return list; - }); - } - - /** - * Skip the next value or list in the RLP source. - * - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - default void skipNext() { - skipNext(isLenient()); - } - - /** - * Skip the next value or list in the RLP source. - * - * @param lenient If {@code false}, an exception will be thrown if the integer is not minimally - * encoded. - * @throws InvalidRLPEncodingException If there is an error decoding the RLP source. - * @throws EndOfRLPException If there are no more RLP values to read. - */ - void skipNext(boolean lenient); - - /** - * The number of remaining values to read. - * - * @return The number of remaining values to read. - */ - int remaining(); - - /** - * Check if all values have been read. - * - * @return {@code true} if all values have been read. - */ - boolean isComplete(); - - /** - * Returns reader's index - * - * @return current reader position - */ - int position(); - - /** - * Provides the remainder of the bytes that have not been read yet. - * - * @return the remainder of the input at current position - */ - Bytes readRemaining(); -} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPWriter.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPWriter.java deleted file mode 100644 index 30cd7d373..000000000 --- a/rlp/src/main/java/org/apache/tuweni/v2/rlp/RLPWriter.java +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; - -import java.math.BigInteger; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -/** A writer for encoding values to RLP. */ -public interface RLPWriter { - - /** - * Append an already RLP encoded value. - * - *

Note that this method may not validate that {@code value} is a valid RLP sequence. - * Appending an invalid RLP sequence will cause the entire RLP encoding produced by this writer to - * also be invalid. - * - * @param value The RLP encoded bytes to append. - */ - void writeRLP(Bytes value); - - /** - * Encode a {@link Bytes} value to RLP. - * - * @param value The byte array to encode. - */ - void writeValue(Bytes value); - - /** - * Encode a byte array to RLP. - * - * @param value The byte array to encode. - */ - default void writeByteArray(byte[] value) { - writeValue(Bytes.wrap(value)); - } - - /** - * Encode a byte to RLP. - * - * @param value The byte value to encode. - */ - default void writeByte(byte value) { - writeValue(Bytes.of(value)); - } - - /** - * Write an integer to the output. - * - * @param value The integer to write. - */ - default void writeInt(int value) { - writeLong(value); - } - - /** - * Write a long to the output. - * - * @param value The long value to write. - */ - void writeLong(long value); - - /** - * Write a {@link UInt256} to the output. - * - * @param value The {@link UInt256} value to write. - */ - default void writeUInt256(UInt256 value) { - writeValue(value.toMinimalBytes()); - } - - /** - * Write a big integer to the output. - * - * @param value The integer to write. - */ - default void writeBigInteger(BigInteger value) { - if (value.signum() == 0) { - writeInt(0); - return; - } - byte[] byteArray = value.toByteArray(); - if (byteArray[0] == 0) { - writeValue(Bytes.wrap(byteArray).slice(1)); - } else { - writeByteArray(byteArray); - } - } - - /** - * Write a string to the output. - * - * @param str The string to write. - */ - default void writeString(String str) { - writeByteArray(str.getBytes(UTF_8)); - } - - /** - * Write a list of values. - * - * @param fn A consumer that will be provided with a {@link RLPWriter} that can consume values. - */ - void writeList(Consumer fn); - - /** - * Write a list of values, sending each value to a function to be interpreted. - * - * @param elements the list of elements to write - * @param elementWriter the function called for each element in the list - * @param The type of the list elements. - */ - default void writeList(List elements, BiConsumer elementWriter) { - writeList( - writer -> { - for (T element : elements) { - elementWriter.accept(writer, element); - } - }); - } -} diff --git a/rlp/src/main/java/org/apache/tuweni/v2/rlp/package-info.java b/rlp/src/main/java/org/apache/tuweni/v2/rlp/package-info.java deleted file mode 100644 index 71e929098..000000000 --- a/rlp/src/main/java/org/apache/tuweni/v2/rlp/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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. - * - *

Recursive Length Prefix (RLP) encoding and decoding. - * - *

An implementation of the Ethereum Recursive Length Prefix (RLP) algorithm, as described at https://github.com/ethereum/wiki/wiki/RLP. - * - *

These classes are included in the standard Tuweni distribution, or separately when using the - * gradle dependency 'org.apache.tuweni:tuweni-rlp' (tuweni-rlp.jar). - */ -package org.apache.tuweni.v2.rlp; diff --git a/rlp/src/test/java/org/apache/tuweni/v2/rlp/ByteBufferWriterTest.java b/rlp/src/test/java/org/apache/tuweni/v2/rlp/ByteBufferWriterTest.java deleted file mode 100644 index 231bf22c1..000000000 --- a/rlp/src/test/java/org/apache/tuweni/v2/rlp/ByteBufferWriterTest.java +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.List; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -class ByteBufferWriterTest { - - @ParameterizedTest - @CsvSource({"8203e8, 1000", "830186a0, 100000"}) - void shouldWriteSmallIntegers(String expectedHex, int value) { - ByteBuffer buffer = ByteBuffer.allocate(64); - RLP.encodeTo(buffer, writer -> writer.writeInt(value)); - buffer.flip(); - assertEquals(Bytes.fromHexString(expectedHex), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteLongIntegers() { - ByteBuffer buffer = ByteBuffer.allocate(64); - RLP.encodeTo(buffer, writer -> writer.writeLong(100000L)); - buffer.flip(); - assertEquals(Bytes.fromHexString("830186a0"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteUInt256Integers() { - ByteBuffer buffer = ByteBuffer.allocate(64); - RLP.encodeTo(buffer, writer -> writer.writeUInt256(UInt256.valueOf(100000L))); - buffer.flip(); - assertEquals(Bytes.fromHexString("830186a0"), Bytes.wrapByteBuffer(buffer)); - - buffer.clear(); - RLP.encodeTo( - buffer, - writer -> - writer.writeUInt256( - UInt256.fromHexString( - "0x0400000000000000000000000000000000000000000000000000f100000000ab"))); - buffer.flip(); - assertEquals( - Bytes.fromHexString("a00400000000000000000000000000000000000000000000000000f100000000ab"), - Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteBigIntegers() { - ByteBuffer buffer = ByteBuffer.allocate(64); - RLP.encodeTo(buffer, writer -> writer.writeBigInteger(BigInteger.valueOf(100000))); - buffer.flip(); - assertEquals(Bytes.fromHexString("830186a0"), Bytes.wrapByteBuffer(buffer)); - - buffer.clear(); - RLP.encodeTo(buffer, writer -> writer.writeBigInteger(BigInteger.valueOf(127).pow(16))); - buffer.flip(); - assertEquals( - Bytes.fromHexString("8ee1ceefa5bbd9ed1c97f17a1df801"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteEmptyStrings() { - ByteBuffer buffer = ByteBuffer.allocate(64); - RLP.encodeTo(buffer, writer -> writer.writeString("")); - buffer.flip(); - assertEquals(Bytes.fromHexString("80"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteOneCharactersStrings() { - ByteBuffer buffer = ByteBuffer.allocate(64); - RLP.encodeTo(buffer, writer -> writer.writeString("d")); - buffer.flip(); - assertEquals(Bytes.fromHexString("64"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteStrings() { - ByteBuffer buffer = ByteBuffer.allocate(64); - RLP.encodeTo(buffer, writer -> writer.writeString("dog")); - buffer.flip(); - assertEquals(Bytes.fromHexString("83646f67"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteShortLists() { - List strings = - Arrays.asList( - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); - - ByteBuffer buffer = ByteBuffer.allocate(64); - RLP.encodeListTo(buffer, listWriter -> strings.forEach(listWriter::writeString)); - buffer.flip(); - - assertEquals( - Bytes.fromHexString( - "f784617364668471776572847a78637684617364668471776572847a" - + "78637684617364668471776572847a78637684617364668471776572"), - Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteNestedLists() { - ByteBuffer buffer = ByteBuffer.allocate(1024); - RLP.encodeListTo( - buffer, - listWriter -> { - listWriter.writeString("asdf"); - listWriter.writeString("qwer"); - for (int i = 30; i >= 0; --i) { - listWriter.writeList( - subListWriter -> { - subListWriter.writeString("zxcv"); - subListWriter.writeString("asdf"); - subListWriter.writeString("qwer"); - }); - } - }); - - buffer.flip(); - assertTrue( - RLP.decodeList( - Bytes.wrapByteBuffer(buffer), - listReader -> { - assertEquals("asdf", listReader.readString()); - assertEquals("qwer", listReader.readString()); - - for (int i = 30; i >= 0; --i) { - assertTrue( - listReader.readList( - subListReader -> { - assertEquals("zxcv", subListReader.readString()); - assertEquals("asdf", subListReader.readString()); - assertEquals("qwer", subListReader.readString()); - return true; - })); - } - - return true; - })); - } - - @Test - void shouldWritePreviouslyEncodedValues() { - ByteBuffer buffer = ByteBuffer.allocate(64); - RLP.encodeTo(buffer, writer -> writer.writeRLP(RLP.encodeByteArray("abc".getBytes(UTF_8)))); - buffer.flip(); - assertEquals("abc", RLP.decodeString(Bytes.wrapByteBuffer(buffer))); - } -} diff --git a/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPReaderTest.java b/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPReaderTest.java deleted file mode 100644 index 0d083e5c5..000000000 --- a/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPReaderTest.java +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; -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.rlp.EndOfRLPException; -import org.apache.tuweni.rlp.InvalidRLPEncodingException; -import org.apache.tuweni.rlp.InvalidRLPTypeException; -import org.apache.tuweni.v2.bytes.Bytes; - -import java.math.BigInteger; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -class BytesRLPReaderTest { - - private static final Bytes SHORT_LIST = - fromHexString( - "f784617364668471776572847a78637684617364668471776572847a78637684617364668471776572847a78637684617364668471776572"); - private static final Bytes LONG_LIST = - fromHexString( - "f90200cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf8461736" - + "4668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572" - + "847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf8" - + "4617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471" - + "776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786" - + "376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364" - + "668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf846173646684717765728" - + "47a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84" - + "617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf846173646684717" - + "76572847a786376cf84617364668471776572847a786376"); - - private static class SomeObject { - private final String name; - private final int number; - private final BigInteger longNumber; - - SomeObject(String name, int number, BigInteger longNumber) { - this.name = name; - this.number = number; - this.longNumber = longNumber; - } - } - - @Test - void shouldParseFullObjects() { - Bytes bytes = fromHexString("83426f620486011F71B70768"); - SomeObject readObject = - RLP.decode( - bytes, - reader -> - new SomeObject(reader.readString(), reader.readInt(), reader.readBigInteger())); - - assertEquals("Bob", readObject.name); - assertEquals(4, readObject.number); - assertEquals(BigInteger.valueOf(1234563434344L), readObject.longNumber); - } - - @ParameterizedTest - @CsvSource({ - "80, 0", - "01, 1", - "10, 16", - "4f, 79", - "7f, 127", - "8180, 128", - "8203e8, 1000", - "830186a0, 100000" - }) - void shouldReadIntegers(String hex, int value) { - assertTrue( - RLP.decode( - fromHexString(hex), - reader -> { - assertEquals(value, reader.readInt()); - return true; - })); - } - - @ParameterizedTest - // @formatter:off - @CsvSource({ - "80, ''", - "00, '\u0000'", - "01, '\u0001'", - "7f, '\u007F'", - "83646f67, dog", - "b74c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c69" - + ", 'Lorem ipsum dolor sit amet, consectetur adipisicing eli'", - "b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974" - + ", 'Lorem ipsum dolor sit amet, consectetur adipisicing elit'", - "b904004c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742e20437572616269747572206d6175726973206d61676e612c20737573636970697420736564207665686963756c61206e6f6e2c20696163756c697320666175636962757320746f72746f722e2050726f696e20737573636970697420756c74726963696573206d616c6573756164612e204475697320746f72746f7220656c69742c2064696374756d2071756973207472697374697175652065752c20756c7472696365732061742072697375732e204d6f72626920612065737420696d70657264696574206d6920756c6c616d636f7270657220616c6971756574207375736369706974206e6563206c6f72656d2e2041656e65616e2071756973206c656f206d6f6c6c69732c2076756c70757461746520656c6974207661726975732c20636f6e73657175617420656e696d2e204e756c6c6120756c74726963657320747572706973206a7573746f2c20657420706f73756572652075726e6120636f6e7365637465747572206e65632e2050726f696e206e6f6e20636f6e76616c6c6973206d657475732e20446f6e65632074656d706f7220697073756d20696e206d617572697320636f6e67756520736f6c6c696369747564696e2e20566573746962756c756d20616e746520697073756d207072696d697320696e206661756369627573206f726369206c756374757320657420756c74726963657320706f737565726520637562696c69612043757261653b2053757370656e646973736520636f6e76616c6c69732073656d2076656c206d617373612066617563696275732c2065676574206c6163696e6961206c616375732074656d706f722e204e756c6c61207175697320756c747269636965732070757275732e2050726f696e20617563746f722072686f6e637573206e69626820636f6e64696d656e74756d206d6f6c6c69732e20416c697175616d20636f6e73657175617420656e696d206174206d65747573206c75637475732c206120656c656966656e6420707572757320656765737461732e20437572616269747572206174206e696268206d657475732e204e616d20626962656e64756d2c206e6571756520617420617563746f72207472697374697175652c206c6f72656d206c696265726f20616c697175657420617263752c206e6f6e20696e74657264756d2074656c6c7573206c65637475732073697420616d65742065726f732e20437261732072686f6e6375732c206d65747573206163206f726e617265206375727375732c20646f6c6f72206a7573746f20756c747269636573206d657475732c20617420756c6c616d636f7270657220766f6c7574706174" - + ", 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat'" - }) - // @formatter:on - void shouldReadStrings(String hex, String value) { - assertTrue( - RLP.decode( - fromHexString(hex), - reader -> { - assertEquals(value, reader.readString()); - return true; - })); - } - - @Test - void shouldThrowWhenInputExhausted() { - EndOfRLPException ex = - assertThrows(EndOfRLPException.class, () -> RLP.decode(Bytes.EMPTY, RLPReader::readInt)); - assertEquals("End of RLP source reached", ex.getMessage()); - } - - @Test - void shouldThrowWhenNextItemIsAList() { - InvalidRLPTypeException ex = - assertThrows( - InvalidRLPTypeException.class, () -> RLP.decode(SHORT_LIST, RLPReader::readInt)); - assertEquals("Attempted to read a value but next item is a list", ex.getMessage()); - } - - @Test - void shouldThrowWheSourceIsTruncated() { - InvalidRLPEncodingException ex = - assertThrows( - InvalidRLPEncodingException.class, - () -> RLP.decode(fromHexString("830186"), RLPReader::readInt)); - assertEquals("Insufficient bytes in RLP encoding: expected 3 but have only 2", ex.getMessage()); - } - - @Test - void shouldThrowWhenLowValueIsntEncodedToSingleByte() { - Bytes bytes1 = fromHexString("8128"); - InvalidRLPEncodingException ex1 = - assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes1)); - assertEquals("Value should have been encoded as a single byte 0x28", ex1.getMessage()); - assertEquals(40, RLP.decodeInt(bytes1, true)); - - Bytes bytes2 = fromHexString("b80128"); - InvalidRLPEncodingException ex2 = - assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes1)); - assertEquals("Value should have been encoded as a single byte 0x28", ex2.getMessage()); - assertEquals(40, RLP.decodeInt(bytes2, true)); - } - - @Test - void shouldThrowWhenValueLengthContainsLeadingZeros() { - Bytes bytes1 = - fromHexString( - "b900384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974"); - InvalidRLPEncodingException ex1 = - assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeString(bytes1)); - assertEquals("RLP value length contains leading zero bytes", ex1.getMessage()); - assertEquals( - "Lorem ipsum dolor sit amet, consectetur adipisicing elit", RLP.decodeString(bytes1, true)); - - Bytes bytes2 = fromHexString("bb0000000028"); - InvalidRLPEncodingException ex2 = - assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes2)); - assertEquals("RLP value length contains leading zero bytes", ex2.getMessage()); - InvalidRLPEncodingException ex3 = - assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes2, true)); - assertEquals("RLP value length is zero", ex3.getMessage()); - - Bytes bytes3 = fromHexString("bd00000000000128"); - InvalidRLPEncodingException ex4 = - assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes3)); - assertEquals("RLP value length contains leading zero bytes", ex4.getMessage()); - assertEquals(40, RLP.decodeInt(bytes3, true)); - } - - @Test - void shouldThrowWhenListLengthContainsLeadingZeros() { - Bytes bytes1 = fromHexString("0xF9000101"); - InvalidRLPEncodingException ex1 = - assertThrows( - InvalidRLPEncodingException.class, () -> RLP.decodeList(bytes1, RLPReader::readInt)); - assertEquals("RLP list length contains leading zero bytes", ex1.getMessage()); - assertEquals(Integer.valueOf(1), RLP.decodeList(bytes1, true, RLPReader::readInt)); - - Bytes bytes2 = fromHexString("0xF80101"); - InvalidRLPEncodingException ex2 = - assertThrows( - InvalidRLPEncodingException.class, () -> RLP.decodeList(bytes2, RLPReader::readInt)); - assertEquals("RLP list length of 1 was not minimally encoded", ex2.getMessage()); - assertEquals(Integer.valueOf(1), RLP.decodeList(bytes2, true, RLPReader::readInt)); - } - - @Test - void shouldThrowWhenLengthIsOversized() { - Bytes bytes1 = fromHexString("bc0aaaaaaaaa28"); - InvalidRLPEncodingException ex1 = - assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes1)); - assertEquals("RLP value length is oversized", ex1.getMessage()); - - Bytes bytes2 = fromHexString("bb8000000128"); - InvalidRLPEncodingException ex2 = - assertThrows(InvalidRLPEncodingException.class, () -> RLP.decodeInt(bytes2)); - assertEquals("RLP value length is oversized", ex2.getMessage()); - } - - @Test - void shouldReadShortList() { - List expected = - Arrays.asList( - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); - - List result = - RLP.decodeToList( - SHORT_LIST, - (reader, list) -> { - assertEquals(11, reader.remaining()); - for (int i = 10; i >= 0; --i) { - list.add(reader.readString()); - } - }); - assertEquals(expected, result); - } - - @Test - void shouldReadShortListContents() { - List expected = - Arrays.asList( - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); - - List result = RLP.decodeToList(SHORT_LIST, reader -> reader.readString()); - assertEquals(expected, result); - } - - @Test - void shouldReadLongList() { - List> expected = - Stream.generate(() -> Arrays.asList("asdf", "qwer", "zxcv")) - .limit(31) - .collect(Collectors.toList()); - - List result = - RLP.decodeToList( - LONG_LIST, - (reader, list) -> { - for (int i = 30; i >= 0; --i) { - list.add( - reader.readList( - (subReader, subList) -> { - subList.add(subReader.readString()); - subList.add(subReader.readString()); - subList.add(subReader.readString()); - })); - } - }); - assertEquals(expected, result); - } - - @Test - void shouldReadRemaining() { - Bytes input = Bytes.fromHexString("83646f6783646f6783646f67"); - RLP.decode( - input, - reader -> { - reader.readValue(); - assertEquals(4, reader.position()); - assertEquals(Bytes.fromHexString("83646f6783646f67"), reader.readRemaining()); - return null; - }); - } -} diff --git a/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPWriterTest.java b/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPWriterTest.java deleted file mode 100644 index 989054ff5..000000000 --- a/rlp/src/test/java/org/apache/tuweni/v2/rlp/BytesRLPWriterTest.java +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.rlp; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; - -import java.math.BigInteger; -import java.util.Arrays; -import java.util.List; - -import org.junit.jupiter.api.Test; - -class BytesRLPWriterTest { - - private static class SomeObject { - private final String name; - private final int number; - private final BigInteger longNumber; - - SomeObject(String name, int number, BigInteger longNumber) { - this.name = name; - this.number = number; - this.longNumber = longNumber; - } - } - - @Test - void shouldWriteFullObjects() { - SomeObject bob = new SomeObject("Bob", 4, BigInteger.valueOf(1234563434344L)); - Bytes bytes = - RLP.encode( - writer -> { - writer.writeString(bob.name); - writer.writeInt(bob.number); - writer.writeBigInteger(bob.longNumber); - }); - - assertTrue( - RLP.decode( - bytes, - reader -> { - assertEquals("Bob", reader.readString()); - assertEquals(4, reader.readInt()); - assertEquals(BigInteger.valueOf(1234563434344L), reader.readBigInteger()); - return true; - })); - } - - @Test - void shouldWriteSmallIntegers() { - assertEquals(fromHexString("80"), RLP.encode(writer -> writer.writeInt(0))); - assertEquals(fromHexString("01"), RLP.encode(writer -> writer.writeInt(1))); - assertEquals(fromHexString("0f"), RLP.encode(writer -> writer.writeInt(15))); - assertEquals(fromHexString("8203e8"), RLP.encode(writer -> writer.writeInt(1000))); - assertEquals(fromHexString("820400"), RLP.encode(writer -> writer.writeInt(1024))); - assertEquals(fromHexString("830186a0"), RLP.encode(writer -> writer.writeInt(100000))); - } - - @Test - void shouldWriteLongIntegers() { - assertEquals(fromHexString("80"), RLP.encode(writer -> writer.writeLong(0L))); - assertEquals(fromHexString("01"), RLP.encode(writer -> writer.writeLong(1))); - assertEquals(fromHexString("0f"), RLP.encode(writer -> writer.writeLong(15))); - assertEquals(fromHexString("8203e8"), RLP.encode(writer -> writer.writeLong(1000))); - assertEquals(fromHexString("820400"), RLP.encode(writer -> writer.writeLong(1024))); - assertEquals(fromHexString("830186a0"), RLP.encode(writer -> writer.writeLong(100000L))); - } - - @Test - void shouldWriteUInt256Integers() { - assertEquals( - fromHexString("80"), RLP.encode(writer -> writer.writeUInt256(UInt256.valueOf(0L)))); - assertEquals( - fromHexString("830186a0"), - RLP.encode(writer -> writer.writeUInt256(UInt256.valueOf(100000L)))); - assertEquals( - fromHexString("a00400000000000000000000000000000000000000000000000000f100000000ab"), - RLP.encode( - writer -> - writer.writeUInt256( - UInt256.fromHexString( - "0x0400000000000000000000000000000000000000000000000000f100000000ab")))); - } - - @Test - void shouldWriteBigIntegers() { - assertEquals( - fromHexString("830186a0"), - RLP.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(100000)))); - assertEquals( - fromHexString("8ee1ceefa5bbd9ed1c97f17a1df801"), - RLP.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(127).pow(16)))); - } - - @Test - void shouldWriteEmptyStrings() { - assertEquals(fromHexString("80"), RLP.encode(writer -> writer.writeString(""))); - } - - @Test - void shouldWriteOneCharactersStrings() { - assertEquals(fromHexString("64"), RLP.encode(writer -> writer.writeString("d"))); - } - - @Test - void shouldWriteStrings() { - assertEquals(fromHexString("83646f67"), RLP.encode(writer -> writer.writeString("dog"))); - } - - @Test - void shouldWriteShortLists() { - List strings = - Arrays.asList( - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); - - assertEquals( - fromHexString( - "f784617364668471776572847a78637684617364668471776572847a" - + "78637684617364668471776572847a78637684617364668471776572"), - RLP.encodeList(listWriter -> strings.forEach(listWriter::writeString))); - } - - @Test - void shouldWriteShortListWithAFunction() { - List strings = - Arrays.asList( - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); - - assertEquals( - fromHexString( - "f784617364668471776572847a78637684617364668471776572847a" - + "78637684617364668471776572847a78637684617364668471776572"), - RLP.encodeList(strings, RLPWriter::writeString)); - } - - @Test - void shouldWriteNestedLists() { - Bytes bytes = - RLP.encodeList( - listWriter -> { - listWriter.writeString("asdf"); - listWriter.writeString("qwer"); - for (int i = 30; i >= 0; --i) { - listWriter.writeList( - subListWriter -> { - subListWriter.writeString("zxcv"); - subListWriter.writeString("asdf"); - subListWriter.writeString("qwer"); - }); - } - }); - - assertTrue( - RLP.decodeList( - bytes, - listReader -> { - assertEquals("asdf", listReader.readString()); - assertEquals("qwer", listReader.readString()); - - for (int i = 30; i >= 0; --i) { - assertTrue( - listReader.readList( - subListReader -> { - assertEquals("zxcv", subListReader.readString()); - assertEquals("asdf", subListReader.readString()); - assertEquals("qwer", subListReader.readString()); - return true; - })); - } - - return true; - })); - } - - @Test - void shouldWritePreviouslyEncodedValues() { - Bytes output = - RLP.encode(writer -> writer.writeRLP(RLP.encodeByteArray("abc".getBytes(UTF_8)))); - assertEquals("abc", RLP.decodeString(output)); - } -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/ByteBufferSSZWriter.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/ByteBufferSSZWriter.java deleted file mode 100644 index 455bd4555..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/ByteBufferSSZWriter.java +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import org.apache.tuweni.v2.bytes.Bytes; - -import java.nio.ByteBuffer; - -final class ByteBufferSSZWriter implements SSZWriter { - - private ByteBuffer buffer; - - ByteBufferSSZWriter(ByteBuffer buffer) { - this.buffer = buffer; - } - - @Override - public void writeSSZ(Bytes value) { - buffer.put(value.toArrayUnsafe()); - } - - @Override - public void writeSSZ(byte[] value) { - buffer.put(value); - } -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZReader.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZReader.java deleted file mode 100644 index a546ae8cd..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZReader.java +++ /dev/null @@ -1,479 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import static java.nio.ByteOrder.LITTLE_ENDIAN; -import static java.nio.charset.StandardCharsets.UTF_8; - -import org.apache.tuweni.ssz.EndOfSSZException; -import org.apache.tuweni.ssz.InvalidSSZTypeException; -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; -import org.apache.tuweni.v2.units.bigints.UInt384; - -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; -import java.util.function.LongFunction; -import java.util.function.Supplier; - -final class BytesSSZReader implements SSZReader { - - private final Bytes content; - private int index = 0; - - BytesSSZReader(Bytes content) { - this.content = content; - } - - @Override - public Bytes readBytes(int limit) { - int byteLength = 4; - ensureBytes(byteLength, () -> "SSZ encoded data is not a byte array"); - int size; - try { - size = content.getInt(index, LITTLE_ENDIAN); - } catch (IndexOutOfBoundsException e) { - throw new EndOfSSZException(); - } - if (size < 0 || size > limit) { - throw new InvalidSSZTypeException("length of bytes would exceed limit"); - } - index += 4; - if (content.size() - index - size < 0) { - throw new InvalidSSZTypeException( - "SSZ encoded data has insufficient bytes for decoded byte array length"); - } - return consumeBytes(size); - } - - @Override - public Bytes readFixedBytes(int byteLength, int limit) { - ensureBytes(byteLength, () -> "SSZ encoded data is not a fixed-length byte array"); - return consumeBytes(byteLength); - } - - @Override - public int readInt(int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - int byteLength = bitLength / 8; - ensureBytes( - byteLength, - () -> "SSZ encoded data has insufficient length to read a " + bitLength + "-bit integer"); - Bytes bytes = content.slice(index, byteLength); - int zeroBytes = bytes.numberOfTrailingZeroBytes(); - if ((byteLength - zeroBytes) > 4) { - throw new InvalidSSZTypeException("decoded integer is too large for an int"); - } - index += byteLength; - return bytes.slice(0, bytes.size() - zeroBytes).toInt(LITTLE_ENDIAN); - } - - @Override - public long readLong(int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - int byteLength = bitLength / 8; - ensureBytes( - byteLength, - () -> "SSZ encoded data has insufficient length to read a " + bitLength + "-bit integer"); - Bytes bytes = content.slice(index, byteLength); - int zeroBytes = bytes.numberOfTrailingZeroBytes(); - if ((byteLength - zeroBytes) > 8) { - throw new InvalidSSZTypeException("decoded integer is too large for a long"); - } - index += byteLength; - return bytes.slice(0, bytes.size() - zeroBytes).toLong(LITTLE_ENDIAN); - } - - @Override - public BigInteger readBigInteger(int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - int byteLength = bitLength / 8; - ensureBytes( - byteLength, - () -> "SSZ encoded data has insufficient length to read a " + bitLength + "-bit integer"); - return consumeBytes(byteLength).toBigInteger(LITTLE_ENDIAN); - } - - @Override - public BigInteger readUnsignedBigInteger(int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - int byteLength = bitLength / 8; - ensureBytes( - byteLength, - () -> "SSZ encoded data has insufficient length to read a " + bitLength + "-bit integer"); - return consumeBytes(byteLength).toUnsignedBigInteger(LITTLE_ENDIAN); - } - - @Override - public UInt256 readUInt256() { - ensureBytes( - 256 / 8, () -> "SSZ encoded data has insufficient length to read a 256-bit integer"); - return UInt256.fromBytes(consumeBytes(256 / 8).mutableCopy().reverse()); - } - - @Override - public UInt384 readUInt384() { - ensureBytes( - 384 / 8, () -> "SSZ encoded data has insufficient length to read a 384-bit integer"); - return UInt384.fromBytes(consumeBytes(384 / 8).mutableCopy().reverse()); - } - - @Override - public Bytes readAddress() { - ensureBytes(20, () -> "SSZ encoded data has insufficient length to read a 20-byte address"); - return consumeBytes(20); - } - - @Override - public Bytes readHash(int hashLength) { - ensureBytes( - hashLength, - () -> "SSZ encoded data has insufficient length to read a " + hashLength + "-byte hash"); - return consumeBytes(hashLength); - } - - @Override - public List readBytesList(int limit) { - return readList(remaining -> readBytes(limit)); - } - - @Override - public List readVector(long listSize, int limit) { - return readList(listSize, remaining -> readByteArray(limit), Bytes::wrap); - } - - @Override - public List readFixedBytesVector(int listSize, int byteLength, int limit) { - return readFixedList(listSize, remaining -> readFixedByteArray(byteLength, limit), Bytes::wrap); - } - - @Override - public List readFixedBytesList(int byteLength, int limit) { - return readList(byteLength, () -> readFixedBytes(byteLength, limit)); - } - - @Override - public List readStringList(int limit) { - return readList( - remaining -> readBytes(limit), bytes -> new String(bytes.toArrayUnsafe(), UTF_8)); - } - - @Override - public List readIntList(int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - return readList(bitLength / 8, () -> readInt(bitLength)); - } - - @Override - public List readLongIntList(int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - return readList(bitLength / 8, () -> readLong(bitLength)); - } - - @Override - public List readBigIntegerList(int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - return readList(bitLength / 8, () -> readBigInteger(bitLength)); - } - - @Override - public List readUnsignedBigIntegerList(int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - return readList(bitLength / 8, () -> readUnsignedBigInteger(bitLength)); - } - - @Override - public List readUInt256List() { - return readList(256 / 8, this::readUInt256); - } - - @Override - public List readUInt384List() { - return readList(384 / 8, this::readUInt384); - } - - @Override - public List readAddressList() { - return readList(20, this::readAddress); - } - - @Override - public List readHashList(int hashLength) { - return readList(hashLength, () -> readHash(hashLength)); - } - - @Override - public List readBooleanList() { - return readList(1, this::readBoolean); - } - - @Override - public boolean isComplete() { - return index >= content.size(); - } - - private void ensureBytes(int byteLength, Supplier message) { - if (index == content.size()) { - throw new EndOfSSZException(); - } - if (content.size() - index - byteLength < 0) { - throw new InvalidSSZTypeException(message.get()); - } - } - - private Bytes consumeBytes(int size) { - Bytes bytes = content.slice(index, size); - index += size; - return bytes; - } - - @Override - public Bytes consumeRemainingBytes(int limit) { - if (content.size() - index > limit) { - throw new InvalidSSZTypeException("Too many bytes to consume"); - } - return consumeBytes(content.size() - index); - } - - private List readList(LongFunction bytesSupplier) { - ensureBytes(4, () -> "SSZ encoded data is not a list"); - int originalIndex = this.index; - List elements; - try { - // use a long to simulate reading unsigned - long listSize = consumeBytes(4).toLong(LITTLE_ENDIAN); - elements = new ArrayList<>(); - while (listSize > 0) { - Bytes bytes = bytesSupplier.apply(listSize); - elements.add(bytes); - listSize -= bytes.size(); - listSize -= 4; - if (listSize < 0) { - throw new InvalidSSZTypeException( - "SSZ encoded list length does not align with lengths of its elements"); - } - } - } catch (Exception e) { - this.index = originalIndex; - throw e; - } - return elements; - } - - private List readList(LongFunction bytesSupplier, Function converter) { - ensureBytes(4, () -> "SSZ encoded data is not a list"); - int originalIndex = this.index; - List elements; - try { - // use a long to simulate reading unsigned - long listSize = consumeBytes(4).toLong(LITTLE_ENDIAN); - elements = new ArrayList<>(); - while (listSize > 0) { - Bytes bytes = bytesSupplier.apply(listSize); - elements.add(converter.apply(bytes)); - listSize -= bytes.size(); - listSize -= 4; - if (listSize < 0) { - throw new InvalidSSZTypeException( - "SSZ encoded list length does not align with lengths of its elements"); - } - } - } catch (Exception e) { - this.index = originalIndex; - throw e; - } - return elements; - } - - private List readList( - long listSize, LongFunction bytesSupplier, Function converter) { - int originalIndex = this.index; - List elements; - try { - elements = new ArrayList<>(); - while (listSize > 0) { - byte[] bytes = bytesSupplier.apply(listSize); - elements.add(converter.apply(bytes)); - // When lists have lengths passed in, the listSize argument is the number of - // elements in the list, instead of the number of bytes in the list, so - // we only subtract one each time an element is processed in this case. - listSize -= 1; - if (listSize < 0) { - throw new InvalidSSZTypeException( - "SSZ encoded list length does not align with lengths of its elements"); - } - } - } catch (Exception e) { - this.index = originalIndex; - throw e; - } - return elements; - } - - private List readFixedList( - int listSize, LongFunction bytesSupplier, Function converter) { - int originalIndex = this.index; - List elements; - try { - elements = new ArrayList<>(); - while (listSize > 0) { - byte[] bytes = bytesSupplier.apply(listSize); - elements.add(converter.apply(bytes)); - // When lists have lengths passed in, the listSize argument is the number of - // elements in the list, instead of the number of bytes in the list, so - // we only subtract one each time an element is processed in this case. - listSize -= 1; - if (listSize < 0) { - throw new InvalidSSZTypeException( - "SSZ encoded list length does not align with lengths of its elements"); - } - } - } catch (Exception e) { - this.index = originalIndex; - throw e; - } - return elements; - } - - private List readList(int elementSize, Supplier elementSupplier) { - ensureBytes(4, () -> "SSZ encoded data is not a list"); - int originalIndex = this.index; - List bytesList; - try { - int listSize = consumeBytes(4).toInt(LITTLE_ENDIAN); - if ((listSize % elementSize) != 0) { - throw new InvalidSSZTypeException( - "SSZ encoded list length does not align with lengths of its elements"); - } - int nElements = listSize / elementSize; - bytesList = new ArrayList<>(nElements); - for (int i = 0; i < nElements; ++i) { - bytesList.add(elementSupplier.get()); - } - } catch (Exception e) { - this.index = originalIndex; - throw e; - } - return bytesList; - } - - @Override - public void readAsContainer(SSZReadable... elements) { - var variableElements = new ArrayList(); - - for (SSZReadable element : elements) { - if (element.isFixed()) { - element.populateFromReader(this); - } else { - variableElements.add(new ElementOffset(readUInt32(), element)); - } - } - if (variableElements.isEmpty()) { - return; - } - for (int i = 0; i < variableElements.size() - 1; i++) { - if (variableElements.get(i).getOffset() != index) { - throw new InvalidSSZTypeException("Variable elements are not in order"); - } - int length = (int) (variableElements.get(i + 1).offset - variableElements.get(i).offset); - variableElements.get(i).getElement().populateFromReader(this.slice(length)); - this.index += length; - } - variableElements - .get(variableElements.size() - 1) - .getElement() - .populateFromReader(this.slice(content.size() - index)); - } - - private SSZReader slice(int length) { - return new BytesSSZReader(content.slice(index, length)); - } - - private static class ElementOffset { - final long offset; - final SSZReadable element; - - public ElementOffset(long offset, SSZReadable element) { - this.offset = offset; - this.element = element; - } - - public long getOffset() { - return offset; - } - - public SSZReadable getElement() { - return element; - } - } - - @Override - public List readFixedTypedList(int elementSize, Supplier supplier) { - int listSize = (content.size() - index) / elementSize; - return readFixedList( - listSize, - remaining -> readFixedByteArray(elementSize, listSize * elementSize), - bytes -> { - T t = supplier.get(); - t.populateFromReader(new BytesSSZReader(Bytes.wrap(bytes))); - return t; - }); - } - - @Override - public List readTypedVector( - int listSize, int elementSize, Supplier supplier) { - return readFixedList( - listSize, - remaining -> readFixedByteArray(elementSize, listSize * elementSize), - bytes -> { - T t = supplier.get(); - t.populateFromReader(new BytesSSZReader(Bytes.wrap(bytes))); - return t; - }); - } - - @Override - public List readVariableSizeTypeList(Supplier supplier) { - if (content.size() == index) { - return Collections.emptyList(); - } - final long firstOffset = readUInt32(); - int size = (int) (firstOffset / 4) + 1; - - List lengths = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - lengths.add((int) (readUInt32() - firstOffset)); - } - List elements = new ArrayList<>(size); - for (Integer length : lengths) { - T t = supplier.get(); - t.populateFromReader(this.slice(length)); - this.index += length; - elements.add(t); - } - T t = supplier.get(); - t.populateFromReader(this.slice(content.size() - index)); - elements.add(t); - return Collections.unmodifiableList(elements); - } -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZWriter.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZWriter.java deleted file mode 100644 index 12a518d26..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/BytesSSZWriter.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import org.apache.tuweni.v2.bytes.Bytes; - -import java.util.ArrayList; -import java.util.List; - -final class BytesSSZWriter implements SSZWriter { - - private final List values = new ArrayList<>(); - - @Override - public void writeSSZ(Bytes value) { - values.add(value); - } - - Bytes toBytes() { - if (values.isEmpty()) { - return Bytes.EMPTY; - } - return Bytes.wrap(values.toArray(new Bytes[0])); - } -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZ.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZ.java deleted file mode 100644 index ba2909381..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZ.java +++ /dev/null @@ -1,2037 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import static java.nio.ByteOrder.LITTLE_ENDIAN; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Objects.requireNonNull; - -import org.apache.tuweni.ssz.EndOfSSZException; -import org.apache.tuweni.ssz.InvalidSSZTypeException; -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.crypto.Hash; -import org.apache.tuweni.v2.units.bigints.UInt256; -import org.apache.tuweni.v2.units.bigints.UInt384; - -import java.math.BigInteger; -import java.nio.BufferOverflowException; -import java.nio.ByteBuffer; -import java.nio.ReadOnlyBufferException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -/** Simple Serialize (SSZ) encoding and decoding. */ -public final class SSZ { - - private static final Bytes TRUE = Bytes.of((byte) 1); - private static final Bytes FALSE = Bytes.of((byte) 0); - - private SSZ() {} - - /** - * Create the hash tree root of a set of values - * - * @param bytes 1 value or a list of homogeneous values - * @return the SSZ tree root hash of the values - */ - public static Bytes hashTreeRoot(Bytes... bytes) { - if (bytes.length == 1) { - if (bytes[0].size() > 32) { - return Hash.keccak256(bytes[0]); - } else { - return bytes[0].mutableCopy().rightPad(32); - } - } else { - Bytes hash = merkleHash(new ArrayList<>(Arrays.asList(bytes))); - return hash.mutableCopy().rightPad(32); - } - } - - /** - * Hashes a list of homogeneous values. - * - * @param values a list of homogeneous values - * @return the merkle hash of the list of values - */ - static Bytes merkleHash(List values) { - Bytes littleEndianLength = Bytes.ofUnsignedInt(values.size(), LITTLE_ENDIAN); - Bytes valuesLength = littleEndianLength.mutableCopy().rightPad(32); - - List chunks; - if (values.isEmpty()) { - chunks = new ArrayList<>(); - chunks.add(Bytes.wrap(new byte[128])); - } else if (values.get(0).size() < 128) { - int itemsPerChunk = (int) Math.floor(128 / (double) values.get(0).size()); - chunks = new ArrayList<>(); - - for (int i = 0; i * itemsPerChunk < values.size(); i++) { - Bytes[] chunkItems = - values - .subList(i * itemsPerChunk, Math.min((i + 1) * itemsPerChunk, values.size())) - .toArray(new Bytes[0]); - chunks.add(Bytes.wrap(chunkItems)); - } - } else { - chunks = values; - } - while (chunks.size() > 1) { - if (chunks.size() % 2 == 1) { - chunks.add(Bytes.wrap(new byte[128])); - } - Iterator iterator = chunks.iterator(); - List hashRound = new ArrayList<>(); - while (iterator.hasNext()) { - hashRound.add(Hash.keccak256(Bytes.wrap(iterator.next(), iterator.next()))); - } - chunks = hashRound; - } - - return Hash.keccak256(Bytes.wrap(chunks.getFirst(), valuesLength)); - } - - // Encoding - - /** - * Encode values to a {@link Bytes} value. - * - * @param fn A consumer that will be provided with a {@link SSZWriter} that can consume values. - * @return The SSZ encoding in a {@link Bytes} value. - */ - public static Bytes encode(Consumer fn) { - requireNonNull(fn); - BytesSSZWriter writer = new BytesSSZWriter(); - fn.accept(writer); - return writer.toBytes(); - } - - /** - * Encode values to a {@link ByteBuffer}. - * - * @param buffer The buffer to write into, starting from its current position. - * @param fn A consumer that will be provided with a {@link SSZWriter} that can consume values. - * @param The type of the buffer. - * @return The buffer. - * @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 static T encodeTo(T buffer, Consumer fn) { - requireNonNull(buffer); - requireNonNull(fn); - ByteBufferSSZWriter writer = new ByteBufferSSZWriter(buffer); - fn.accept(writer); - return buffer; - } - - /** - * Encode {@link Bytes}. - * - * @param value The value to encode. - * @return The SSZ encoding in a {@link Bytes} value. - */ - public static Bytes encodeBytes(Bytes value) { - Bytes lengthBytes = encodeLong(value.size(), 32); - return Bytes.wrap(lengthBytes, value); - } - - static void encodeBytesTo(Bytes value, Consumer appender) { - appender.accept(encodeLong(value.size(), 32)); - appender.accept(value); - } - - static void encodeFixedBytesTo(Bytes value, Consumer appender) { - appender.accept(value); - } - - /** - * Encode a value to a {@link Bytes} value. - * - * @param value The value to encode. - * @return The SSZ encoding in a {@link Bytes} value. - */ - public static Bytes encodeByteArray(byte[] value) { - return encodeBytes(Bytes.wrap(value)); - } - - static void encodeByteArrayTo(byte[] value, Consumer appender) { - appender.accept(encodeLongToByteArray(value.length, 32)); - appender.accept(value); - } - - /** - * Encode a string to a {@link Bytes} value. - * - * @param str The string to encode. - * @return The SSZ encoding in a {@link Bytes} value. - */ - public static Bytes encodeString(String str) { - return encodeByteArray(str.getBytes(UTF_8)); - } - - static void encodeStringTo(String str, Consumer appender) { - encodeByteArrayTo(str.getBytes(UTF_8), appender); - } - - /** - * Encode a two's-compliment integer to a {@link Bytes} value. - * - * @param value the integer to encode - * @param bitLength the bit length of the encoded integer value (must be a multiple of 8) - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} - */ - public static Bytes encodeInt(int value, int bitLength) { - return encodeLong(value, bitLength); - } - - /** - * Encode a two's-compliment long integer to a {@link Bytes} value. - * - * @param value the long to encode - * @param bitLength the bit length of the integer value (must be a multiple of 8) - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} - */ - public static Bytes encodeLong(long value, int bitLength) { - return Bytes.wrap(encodeLongToByteArray(value, bitLength)); - } - - static byte[] encodeLongToByteArray(long value, int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - int zeros = - (value >= 0) ? Long.numberOfLeadingZeros(value) : Long.numberOfLeadingZeros(-1 - value) - 1; - int valueBytes = 8 - (zeros / 8); - - int byteLength = bitLength / 8; - if (valueBytes > byteLength) { - throw new IllegalArgumentException("value is too large for the desired bitLength"); - } - - byte[] encoded = new byte[byteLength]; - - int shift = 0; - for (int i = 0; i < valueBytes; i++) { - encoded[i] = (byte) ((value >> shift) & 0xFF); - shift += 8; - } - if (value < 0) { - // Extend the two's-compliment integer by setting all remaining bits to 1. - for (int i = valueBytes; i < byteLength; i++) { - encoded[i] = (byte) 0xFF; - } - } - return encoded; - } - - /** - * Encode a big integer to a {@link Bytes} value. - * - * @param value the big integer to encode - * @param bitLength the bit length of the integer value (must be a multiple of 8) - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} - */ - public static Bytes encodeBigInteger(BigInteger value, int bitLength) { - return Bytes.wrap(encodeBigIntegerToByteArray(value, bitLength)); - } - - public static byte[] encodeBigIntegerToByteArray(BigInteger value, int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - byte[] bytes = value.toByteArray(); - int valueBytes = bytes.length; - int offset = 0; - if (value.signum() >= 0 && bytes[0] == 0) { - valueBytes = bytes.length - 1; - offset = 1; - } - - int byteLength = bitLength / 8; - if (valueBytes > byteLength) { - throw new IllegalArgumentException("value is too large for the desired bitLength"); - } - - byte[] encoded; - if (valueBytes == byteLength && offset == 0) { - encoded = bytes; - } else { - encoded = new byte[byteLength]; - int padLength = byteLength - valueBytes; - System.arraycopy(bytes, offset, encoded, padLength, valueBytes); - if (value.signum() < 0) { - // Extend the two's-compliment integer by setting all leading bits to 1. - for (int i = 0; i < padLength; i++) { - encoded[i] = (byte) 0xFF; - } - } - } - // reverse the array to make it little endian - for (int i = 0; i < (encoded.length / 2); i++) { - byte swapped = encoded[i]; - encoded[i] = encoded[encoded.length - i - 1]; - encoded[encoded.length - i - 1] = swapped; - } - return encoded; - } - - /** - * Encode an 8-bit two's-compliment integer to a {@link Bytes} value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large to be represented in 8 bits - */ - public static Bytes encodeInt8(int value) { - return encodeInt(value, 8); - } - - /** - * Encode a 16-bit two's-compliment integer to a {@link Bytes} value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large to be represented in 16 bits - */ - public static Bytes encodeInt16(int value) { - return encodeInt(value, 16); - } - - /** - * Encode a 32-bit two's-compliment integer to a {@link Bytes} value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeInt32(int value) { - return encodeInt(value, 32); - } - - /** - * Encode a 64-bit two's-compliment integer to a {@link Bytes} value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeInt64(long value) { - return encodeLong(value, 64); - } - - /** - * Encode an unsigned integer to a {@link Bytes} value. - * - *

Note that {@code value} is a native signed int, but will be interpreted as an unsigned - * value. - * - * @param value the integer to encode - * @param bitLength the bit length of the encoded integer value (must be a multiple of 8) - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} - */ - public static Bytes encodeUInt(int value, int bitLength) { - return encodeULong(value, bitLength); - } - - /** - * Encode an unsigned long integer to a {@link Bytes} value. - * - *

Note that {@code value} is a native signed long, but will be interpreted as an unsigned - * value. - * - * @param value the long to encode - * @param bitLength the bit length of the integer value (must be a multiple of 8) - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} - */ - public static Bytes encodeULong(long value, int bitLength) { - return Bytes.wrap(encodeULongToByteArray(value, bitLength)); - } - - static byte[] encodeULongToByteArray(long value, int bitLength) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - int zeros = Long.numberOfLeadingZeros(value); - int valueBytes = 8 - (zeros / 8); - if (zeros != 0 && value < 0) { - throw new IllegalArgumentException("Value must be positive or zero"); - } - - int byteLength = bitLength / 8; - if (valueBytes > byteLength) { - throw new IllegalArgumentException("value is too large for the desired bitLength"); - } - - byte[] encoded = new byte[byteLength]; - - int shift = 0; - for (int i = 0; i < valueBytes; i++) { - encoded[i] = (byte) ((value >> shift) & 0xFF); - shift += 8; - } - return encoded; - } - - /** - * Encode an unsigned big integer to a {@link Bytes} value. - * - * @param value the big integer to encode - * @param bitLength the bit length of the integer value (must be a multiple of 8) - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large for the specified {@code bitLength} - */ - public static Bytes encodeUBigInteger(BigInteger value, int bitLength) { - return Bytes.wrap(encodeUBigIntegerToByteArray(value, bitLength)); - } - - public static byte[] encodeUBigIntegerToByteArray(BigInteger value, int bitLength) { - if (value.compareTo(BigInteger.ZERO) < 0) { - throw new IllegalArgumentException("Value must be positive or zero"); - } - return encodeBigIntegerToByteArray(value, bitLength); - } - - /** - * Encode an 8-bit unsigned integer to a {@link Bytes} value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large to be represented in 8 bits - */ - public static Bytes encodeUInt8(int value) { - return encodeUInt(value, 8); - } - - /** - * Encode a 16-bit unsigned integer to a {@link Bytes} value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if the value is too large to be represented in 16 bits - */ - public static Bytes encodeUInt16(int value) { - return encodeUInt(value, 16); - } - - /** - * Encode a 32-bit unsigned integer to a {@link Bytes} value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt32(long value) { - return encodeULong(value, 32); - } - - /** - * Encode a 64-bit unsigned integer to a {@link Bytes} value. - * - *

Note that {@code value} is a native signed long, but will be interpreted as an unsigned - * value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt64(long value) { - return encodeULong(value, 64); - } - - /** - * Encode a 256-bit unsigned integer to a little-endian {@link Bytes} value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt256(UInt256 value) { - return value.mutableCopy().reverse(); - } - - /** - * Encode a 384-bit unsigned integer to a little-endian {@link Bytes} value. - * - * @param value the integer to encode - * @return the SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt384(UInt384 value) { - return value.mutableCopy().reverse(); - } - - /** - * Encode a boolean to a {@link Bytes} value. - * - * @param value the boolean to encode - * @return the SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeBoolean(boolean value) { - return value ? TRUE : FALSE; - } - - /** - * Encode a 20-byte address to a {@link Bytes} value. - * - * @param address the address (must be exactly 20 bytes) - * @return the SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if {@code address.size != 20} - */ - public static Bytes encodeAddress(Bytes address) { - if (address.size() != 20) { - throw new IllegalArgumentException("address is not 20 bytes"); - } - return address; - } - - /** - * Encode a hash to a {@link Bytes} value. - * - * @param hash the hash - * @return the SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeHash(Bytes hash) { - return hash; - } - - /** - * Encode a list of bytes. - * - * @param elements the bytes to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeBytesList(Bytes... elements) { - ArrayList encoded = new ArrayList<>(elements.length * 2 + 1); - encodeBytesListTo(elements, encoded::add); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of bytes. - * - * @param elements the bytes to write as a list - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeBytesList(List elements) { - ArrayList encoded = new ArrayList<>(elements.size() * 2 + 1); - encodeBytesListTo(elements, encoded::add); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeBytesListTo(Bytes[] elements, Consumer appender) { - // pre-calculate the list size - relies on knowing how encodeBytesTo does its serialization, but - // is worth it - // to avoid having to pre-serialize all the elements - long listSize = 0; - for (Bytes bytes : elements) { - listSize += 4; - listSize += bytes.size(); - if (listSize > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Cannot serialize list: overall length is too large"); - } - } - appender.accept(encodeUInt32(listSize)); - for (Bytes bytes : elements) { - encodeBytesTo(bytes, appender); - } - } - - static void encodeBytesListTo(List elements, Consumer appender) { - // pre-calculate the list size - relies on knowing how encodeBytesTo does its serialization, but - // is worth it - // to avoid having to pre-serialize all the elements - long listSize = 0; - for (Bytes bytes : elements) { - listSize += 4; - listSize += bytes.size(); - if (listSize > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Cannot serialize list: overall length is too large"); - } - } - appender.accept(encodeUInt32(listSize)); - for (Bytes bytes : elements) { - encodeBytesTo(bytes, appender); - } - } - - static void encodeFixedBytesVectorTo(List elements, Consumer appender) { - for (Bytes bytes : elements) { - appender.accept(bytes); - } - } - - static void encodeBytesVectorTo(List elements, Consumer appender) { - for (Bytes bytes : elements) { - appender.accept(encodeLong(bytes.size(), 32)); - appender.accept(bytes); - } - } - - static void encodeFixedBytesListTo(List elements, Consumer appender) { - // pre-calculate the list size - relies on knowing how encodeBytesTo does its serialization, but - // is worth it - // to avoid having to pre-serialize all the elements - long listSize = 0; - for (Bytes bytes : elements) { - listSize += bytes.size(); - if (listSize > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Cannot serialize list: overall length is too large"); - } - } - appender.accept(encodeUInt32(listSize)); - for (Bytes bytes : elements) { - encodeFixedBytesTo(bytes, appender); - } - } - - /** - * Encode a list of strings. - * - * @param elements the strings to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeStringList(String... elements) { - ArrayList encoded = new ArrayList<>(elements.length * 2 + 1); - encodeStringListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of strings - * - * @param elements the list of strings to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeStringList(List elements) { - ArrayList encoded = new ArrayList<>(elements.size() * 2 + 1); - encodeStringListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeStringListTo(String[] elements, Consumer appender) { - Bytes[] elementBytes = new Bytes[elements.length]; - for (int i = 0; i < elements.length; ++i) { - elementBytes[i] = Bytes.wrap(elements[i].getBytes(UTF_8)); - } - encodeBytesListTo(elementBytes, appender); - } - - static void encodeStringListTo(List elements, Consumer appender) { - Bytes[] elementBytes = new Bytes[elements.size()]; - for (int i = 0; i < elements.size(); ++i) { - elementBytes[i] = Bytes.wrap(elements.get(i).getBytes(UTF_8)); - } - encodeBytesListTo(elementBytes, appender); - } - - /** - * Encode a list of two's compliment integers. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large for the specified {@code - * bitLength} - */ - public static Bytes encodeIntList(int bitLength, int... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of two's compliment integers. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the list of Integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large for the specified {@code - * bitLength} - */ - public static Bytes encodeIntList(int bitLength, List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeIntListTo(int bitLength, int[] elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.length, bitLength / 8)); - for (int value : elements) { - appender.accept(encodeLongToByteArray(value, bitLength)); - } - } - - static void encodeIntListTo(int bitLength, List elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); - for (int value : elements) { - appender.accept(encodeLongToByteArray(value, bitLength)); - } - } - - /** - * Encode a list of two's compliment long integers. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large for the specified {@code - * bitLength} - */ - public static Bytes encodeLongIntList(int bitLength, long... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeLongIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of two's compliment long integers. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the list of Longs to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large for the specified {@code - * bitLength} - */ - public static Bytes encodeLongIntList(int bitLength, List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeLongIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeLongIntListTo(int bitLength, long[] elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.length, bitLength / 8)); - for (long value : elements) { - appender.accept(encodeLongToByteArray(value, bitLength)); - } - } - - static void encodeLongIntListTo(int bitLength, List elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); - for (long value : elements) { - appender.accept(encodeLongToByteArray(value, bitLength)); - } - } - - /** - * Encode a list of big integers. - * - * @param bitLength The bit length of the encoded integers (must be a multiple of 8). - * @param elements The integers to write. - * @return SSZ encoding in a {@link Bytes} value. - * @throws IllegalArgumentException If any values are too large for the specified {@code - * bitLength}. - */ - public static Bytes encodeBigIntegerList(int bitLength, BigInteger... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeBigIntegerListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of big integers. - * - * @param bitLength The bit length of the encoded integers (must be a multiple of 8). - * @param elements The list of BigIntegers to write. - * @return SSZ encoding in a {@link Bytes} value. - * @throws IllegalArgumentException If any values are too large for the specified {@code - * bitLength}. - */ - public static Bytes encodeBigIntegerList(int bitLength, List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeBigIntegerListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeBigIntegerListTo( - int bitLength, BigInteger[] elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.length, bitLength / 8)); - for (BigInteger value : elements) { - appender.accept(encodeBigIntegerToByteArray(value, bitLength)); - } - } - - static void encodeBigIntegerListTo( - int bitLength, List elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); - for (BigInteger value : elements) { - appender.accept(encodeBigIntegerToByteArray(value, bitLength)); - } - } - - /** - * Encode a list of 8-bit two's compliment integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 8 bits - */ - public static Bytes encodeInt8List(int... elements) { - return encodeIntList(8, elements); - } - - /** - * Encode a list of 8-bit two's compliment integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 8 bits - */ - public static Bytes encodeInt8List(List elements) { - return encodeIntList(8, elements); - } - - /** - * Encode a list of 16-bit two's compliment integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 16 bits - */ - public static Bytes encodeInt16List(int... elements) { - return encodeIntList(16, elements); - } - - /** - * Encode a list of 16-bit two's compliment integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 16 bits - */ - public static Bytes encodeInt16List(List elements) { - return encodeIntList(16, elements); - } - - /** - * Encode a list of 32-bit two's compliment integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeInt32List(int... elements) { - return encodeIntList(32, elements); - } - - /** - * Encode a list of 32-bit two's compliment integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeInt32List(List elements) { - return encodeIntList(32, elements); - } - - /** - * Encode a list of 64-bit two's compliment integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeInt64List(long... elements) { - return encodeLongIntList(64, elements); - } - - /** - * Encode a list of 64-bit two's compliment integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeInt64List(List elements) { - return encodeLongIntList(64, elements); - } - - /** - * Encode a list of unsigned integers. - * - *

Note that the {@code elements} are native signed ints, but will be interpreted as an - * unsigned values. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large for the specified {@code - * bitLength} - */ - public static Bytes encodeUIntList(int bitLength, int... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeUIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of unsigned integers. - * - *

Note that the {@code elements} are native signed ints, but will be interpreted as an - * unsigned values. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large for the specified {@code - * bitLength} - */ - public static Bytes encodeUIntList(int bitLength, List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeUIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeUIntListTo(int bitLength, int[] elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.length, bitLength / 8)); - for (int value : elements) { - appender.accept(encodeULongToByteArray(value, bitLength)); - } - } - - static void encodeUIntListTo(int bitLength, List elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); - for (int value : elements) { - appender.accept(encodeULongToByteArray(value, bitLength)); - } - } - - /** - * Encode a list of unsigned long integers. - * - *

Note that the {@code elements} are native signed longs, but will be interpreted as an - * unsigned values. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large for the specified {@code - * bitLength} - */ - public static Bytes encodeULongIntList(int bitLength, long... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeULongIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of unsigned long integers. - * - *

Note that the {@code elements} are native signed longs, but will be interpreted as an - * unsigned values. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large for the specified {@code - * bitLength} - */ - public static Bytes encodeULongIntList(int bitLength, List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeULongIntListTo(bitLength, elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeULongIntListTo(int bitLength, long[] elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.length, bitLength / 8)); - for (long value : elements) { - appender.accept(encodeULongToByteArray(value, bitLength)); - } - } - - static void encodeULongIntListTo(int bitLength, List elements, Consumer appender) { - if (bitLength % 8 != 0) { - throw new IllegalArgumentException("bitLength must be a multiple of 8"); - } - appender.accept(listLengthPrefix(elements.size(), bitLength / 8)); - for (long value : elements) { - appender.accept(encodeULongToByteArray(value, bitLength)); - } - } - - /** - * Encode a list of 8-bit unsigned integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 8 bits - */ - public static Bytes encodeUInt8List(int... elements) { - return encodeUIntList(8, elements); - } - - /** - * Encode a list of 8-bit unsigned integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 8 bits - */ - public static Bytes encodeUInt8List(List elements) { - return encodeUIntList(8, elements); - } - - /** - * Encode a list of 16-bit unsigned integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 16 bits - */ - public static Bytes encodeUInt16List(int... elements) { - return encodeUIntList(16, elements); - } - - /** - * Encode a list of 16-bit unsigned integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 16 bits - */ - public static Bytes encodeUInt16List(List elements) { - return encodeUIntList(16, elements); - } - - /** - * Encode a list of 32-bit unsigned integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 32 bits - */ - public static Bytes encodeUInt32List(long... elements) { - return encodeULongIntList(32, elements); - } - - /** - * Encode a list of 32-bit unsigned integers. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any values are too large to be represented in 32 bits - */ - public static Bytes encodeUInt32List(List elements) { - return encodeULongIntList(32, elements); - } - - /** - * Encode a list of 64-bit unsigned integers. - * - *

Note that the {@code elements} are native signed longs, but will be interpreted as an - * unsigned values. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt64List(long... elements) { - return encodeULongIntList(64, elements); - } - - /** - * Encode a list of 64-bit unsigned integers. - * - *

Note that the {@code elements} are native signed longs, but will be interpreted as an - * unsigned values. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt64List(List elements) { - return encodeULongIntList(64, elements); - } - - /** - * Encode a list of {@link UInt256}. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt256List(UInt256... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeUInt256ListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of {@link UInt256}. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt256List(List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeUInt256ListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeUInt256ListTo(UInt256[] elements, Consumer appender) { - appender.accept(Bytes.wrap(listLengthPrefix(elements.length, 256 / 8))); - for (UInt256 value : elements) { - appender.accept(encodeUInt256(value)); - } - } - - static void encodeUInt256ListTo(List elements, Consumer appender) { - appender.accept(Bytes.wrap(listLengthPrefix(elements.size(), 256 / 8))); - for (UInt256 value : elements) { - appender.accept(encodeUInt256(value)); - } - } - - /** - * Encode a list of {@link UInt384}. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt384List(UInt384... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeUInt384ListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of {@link UInt384}. - * - * @param elements the integers to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeUInt384List(List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeUInt384ListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeUInt384ListTo(UInt384[] elements, Consumer appender) { - appender.accept(Bytes.wrap(listLengthPrefix(elements.length, 256 / 8))); - for (UInt384 value : elements) { - appender.accept(encodeUInt384(value)); - } - } - - static void encodeUInt384ListTo(List elements, Consumer appender) { - appender.accept(Bytes.wrap(listLengthPrefix(elements.size(), 256 / 8))); - for (UInt384 value : elements) { - appender.accept(encodeUInt384(value)); - } - } - - /** - * Encode a list of hashes. - * - * @param elements the hashes to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeHashList(Bytes... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeHashListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of hashes. - * - * @param elements the hashes to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeHashList(List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeHashListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeHashListTo(Bytes[] elements, Consumer appender) { - int hashLength = 0; - for (Bytes bytes : elements) { - if (hashLength == 0) { - hashLength = bytes.size(); - } else { - if (bytes.size() != hashLength) { - throw new IllegalArgumentException("Hashes must be all of the same size"); - } - } - } - appender.accept(Bytes.wrap(listLengthPrefix(elements.length, 32))); - for (Bytes bytes : elements) { - appender.accept(bytes); - } - } - - static void encodeHashListTo(List elements, Consumer appender) { - int hashLength = 0; - for (Bytes bytes : elements) { - if (hashLength == 0) { - hashLength = bytes.size(); - } else { - if (bytes.size() != hashLength) { - throw new IllegalArgumentException("Hashes must be all of the same size"); - } - } - } - appender.accept(Bytes.wrap(listLengthPrefix(elements.size(), 32))); - for (Bytes bytes : elements) { - appender.accept(bytes); - } - } - - /** - * Encode a list of addresses. - * - * @param elements the addresses to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any {@code address.size != 20} - */ - public static Bytes encodeAddressList(Bytes... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeAddressListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of addresses. - * - * @param elements the addresses to write - * @return SSZ encoding in a {@link Bytes} value - * @throws IllegalArgumentException if any {@code address.size != 20} - */ - public static Bytes encodeAddressList(List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeAddressListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeAddressListTo(Bytes[] elements, Consumer appender) { - appender.accept(Bytes.wrap(listLengthPrefix(elements.length, 20))); - for (Bytes bytes : elements) { - appender.accept(encodeAddress(bytes)); - } - } - - static void encodeAddressListTo(List elements, Consumer appender) { - appender.accept(Bytes.wrap(listLengthPrefix(elements.size(), 20))); - for (Bytes bytes : elements) { - appender.accept(encodeAddress(bytes)); - } - } - - /** - * Encode a list of booleans. - * - * @param elements the booleans to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeBooleanList(boolean... elements) { - ArrayList encoded = new ArrayList<>(elements.length + 1); - encodeBooleanListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - /** - * Encode a list of booleans. - * - * @param elements the booleans to write - * @return SSZ encoding in a {@link Bytes} value - */ - public static Bytes encodeBooleanList(List elements) { - ArrayList encoded = new ArrayList<>(elements.size() + 1); - encodeBooleanListTo(elements, b -> encoded.add(Bytes.wrap(b))); - return Bytes.wrap(encoded.toArray(new Bytes[0])); - } - - static void encodeBooleanListTo(boolean[] elements, Consumer appender) { - appender.accept(encodeInt32(elements.length)); - for (boolean value : elements) { - appender.accept(encodeBoolean(value)); - } - } - - static void encodeBooleanListTo(List elements, Consumer appender) { - appender.accept(encodeInt32(elements.size())); - for (boolean value : elements) { - appender.accept(encodeBoolean(value)); - } - } - - public static void encodeAsFixedTypeListTo( - List elements, Consumer appender) { - for (T value : elements) { - appender.accept(SSZ.encode(value::writeTo)); - } - } - - public static void encodeAsFixedTypeVectorTo( - List elements, Consumer appender) { - for (T value : elements) { - appender.accept(SSZ.encode(value::writeTo)); - } - } - - public static void encodeAsContainerTo(Consumer consumer, SSZWritable... elements) { - long written = 0; - long variableOffset = 0; - List> fixedParts = new ArrayList<>(); - List offsetSuppliers = new ArrayList<>(); - List variableParts = new ArrayList<>(); - - for (SSZWritable element : elements) { - BytesSSZWriter fixedWriter = new BytesSSZWriter(); - element.writeTo(fixedWriter); - Bytes bytes = fixedWriter.toBytes(); - if (element.isFixed()) { - fixedParts.add(() -> bytes); - written += bytes.size(); - } else { - var offsetProvider = new OffsetSupplier(variableOffset); - fixedParts.add(offsetProvider); - offsetSuppliers.add(offsetProvider); - written += 4; - variableParts.add(bytes); - variableOffset += bytes.size(); - } - } - for (OffsetSupplier offsetSupplier : offsetSuppliers) { - offsetSupplier.addWritten(written); - } - for (Supplier fixedPart : fixedParts) { - consumer.accept(fixedPart.get()); - } - for (Bytes bytes : variableParts) { - consumer.accept(bytes); - } - } - - private static class OffsetSupplier implements Supplier { - - private final long offset; - private long written; - - public OffsetSupplier(long offset) { - this.offset = offset; - } - - @Override - public Bytes get() { - return SSZ.encodeUInt32(written + offset); - } - - public void addWritten(long written) { - this.written = written; - } - } - - private static byte[] listLengthPrefix(long nElements, int elementBytes) { - long listSize; - try { - listSize = Math.multiplyExact(nElements, (long) elementBytes); - } catch (ArithmeticException e) { - listSize = Long.MAX_VALUE; - } - if (listSize > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Cannot serialize list: overall length is too large"); - } - return encodeLongToByteArray(listSize, 32); - } - - // Decoding - - /** - * Read and decode SSZ from a {@link Bytes} value. - * - * @param source the SSZ encoded bytes - * @param fn a function that will be provided a {@link SSZReader} - * @param the result type of the reading function - * @return the result from the reading function - */ - public static T decode(Bytes source, Function fn) { - requireNonNull(source); - requireNonNull(fn); - return fn.apply(new BytesSSZReader(source)); - } - - /** - * Read a SSZ encoded bytes from a {@link Bytes} value. - * - *

Note: prefer to use {@link #decodeBytes(Bytes, int)} instead, especially when reading - * untrusted data. - * - * @param source the SSZ encoded bytes - * @return the bytes - * @throws InvalidSSZTypeException if the next SSZ value is not a byte array, or is too large - * (greater than 2^32 bytes) - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static Bytes decodeBytes(Bytes source) { - return decode(source, SSZReader::readBytes); - } - - /** - * Read a SSZ encoded bytes from a {@link Bytes} value. - * - * @param source the SSZ encoded bytes - * @param limit the maximum number of bytes to read - * @return the bytes - * @throws InvalidSSZTypeException if the next SSZ value is not a byte array, or would exceed the - * limit - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static Bytes decodeBytes(Bytes source, int limit) { - return decode(source, r -> r.readBytes(limit)); - } - - /** - * Read a SSZ encoded string from a {@link Bytes} value. - * - *

Note: prefer to use {@link #decodeString(Bytes, int)} instead, especially when reading - * untrusted data. - * - * @param source the SSZ encoded bytes - * @return a string - * @throws InvalidSSZTypeException if the next SSZ value is not a byte array, or is too large - * (greater than 2^32 bytes) - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static String decodeString(Bytes source) { - return decode(source, SSZReader::readString); - } - - /** - * Read a SSZ encoded string from a {@link Bytes} value. - * - * @param source the SSZ encoded bytes - * @param limit the maximum number of bytes to read - * @return a string - * @throws InvalidSSZTypeException if the next SSZ value is not a byte array, or would exceed the - * limit - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static String decodeString(Bytes source, int limit) { - return decode(source, r -> r.readString(limit)); - } - - /** - * Read a SSZ encoded two's-compliment integer from a {@link Bytes} value. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integer to read (a multiple of 8) - * @return an int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit - * length, or the decoded value was too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static int decodeInt(Bytes source, int bitLength) { - return decode(source, r -> r.readInt(bitLength)); - } - - /** - * Read a SSZ encoded two's-compliment long integer from a {@link Bytes} value. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integer to read (a multiple of 8) - * @return a long - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit - * length, or the decoded value was too large to fit into a long - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static long decodeLong(Bytes source, int bitLength) { - return decode(source, r -> r.readLong(bitLength)); - } - - /** - * Read a SSZ encoded two's-compliment big integer from a {@link Bytes} value. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integer to read (a multiple of 8) - * @return a string - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit - * length - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static BigInteger decodeBigInteger(Bytes source, int bitLength) { - return decode(source, r -> r.readBigInteger(bitLength)); - } - - /** - * Read an 8-bit two's-compliment integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return an int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for an 8-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static int decodeInt8(Bytes source) { - return decodeInt(source, 8); - } - - /** - * Read a 16-bit two's-compliment integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return an int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 16-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static int decodeInt16(Bytes source) { - return decodeInt(source, 16); - } - - /** - * Read a 32-bit two's-compliment integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return an int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 32-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static int decodeInt32(Bytes source) { - return decodeInt(source, 32); - } - - /** - * Read a 64-bit two's-compliment integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return an int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 64-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static long decodeInt64(Bytes source) { - return decodeLong(source, 64); - } - - /** - * Read a SSZ encoded unsigned integer from a {@link Bytes} value. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integers to read (a multiple of 8) - * @return an unsigned int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit - * length, or the decoded value was too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static int decodeUInt(Bytes source, int bitLength) { - return decode(source, r -> r.readUInt(bitLength)); - } - - /** - * Read a SSZ encoded unsigned long integer from a {@link Bytes} value. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integers to read (a multiple of 8) - * @return an unsigned long - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit - * length, or the decoded value was too large to fit into a long - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static long decodeULong(Bytes source, int bitLength) { - return decode(source, r -> r.readULong(bitLength)); - } - - /** - * Read a SSZ encoded unsigned big integer from a {@link Bytes} value. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integers to read (a multiple of 8) - * @return a string - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for the desired bit - * length - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static BigInteger decodeUnsignedBigInteger(Bytes source, int bitLength) { - return decode(source, r -> r.readBigInteger(bitLength)); - } - - /** - * Read an 8-bit unsigned integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return an int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for an 8-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static int decodeUInt8(Bytes source) { - return decodeUInt(source, 8); - } - - /** - * Read a 16-bit unsigned integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return an int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 16-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static int decodeUInt16(Bytes source) { - return decodeUInt(source, 16); - } - - /** - * Read a 32-bit unsigned integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return an int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 32-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static long decodeUInt32(Bytes source) { - return decodeULong(source, 32); - } - - /** - * Read a 64-bit unsigned integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return an int - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 64-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static long decodeUInt64(Bytes source) { - return decodeLong(source, 64); - } - - /** - * Read a 256-bit unsigned integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a {@link UInt256} - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 256-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static UInt256 decodeUInt256(Bytes source) { - return decode(source, SSZReader::readUInt256); - } - - /** - * Read a 384-bit unsigned integer from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a {@link UInt384} - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 384-bit int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static UInt384 decodeUInt384(Bytes source) { - return decode(source, SSZReader::readUInt384); - } - - /** - * Read a boolean value from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a boolean - * @throws InvalidSSZTypeException if the decoded value is not a boolean - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static boolean decodeBoolean(Bytes source) { - return decode(source, SSZReader::readBoolean); - } - - /** - * Read a 20-byte address from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return the bytes of the Address - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 20-byte address - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static Bytes decodeAddress(Bytes source) { - return decode(source, SSZReader::readAddress); - } - - /** - * Read a 32-byte hash from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param hashLength the length of the hash (in bytes) - * @return the bytes of the hash - * @throws InvalidSSZTypeException if there are insufficient encoded bytes for a 32-byte hash - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static Bytes decodeHash(Bytes source, int hashLength) { - return decode(source, r -> r.readHash(hashLength)); - } - - /** - * Read a list of {@link Bytes} from the SSZ source. - * - *

Note: prefer to use {@link #decodeBytesList(Bytes, int)} instead, especially when reading - * untrusted data. - * - * @param source the SSZ encoded bytes - * @return a list of {@link Bytes} - * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is - * not a byte array, or any byte array is too large (greater than 2^32 bytes) - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeBytesList(Bytes source) { - return decode(source, SSZReader::readBytesList); - } - - /** - * Read a list of {@link Bytes} from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param limit the maximum number of bytes to read for each list element - * @return a list of {@link Bytes} - * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is - * not a byte array, or any byte array is too large (greater than 2^32 bytes) - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeBytesList(Bytes source, int limit) { - return decode(source, r -> r.readBytesList(limit)); - } - - /** - * Read a list of byte arrays from the SSZ source. - * - *

Note: prefer to use {@link #decodeByteArrayList(Bytes, int)} instead, especially when - * reading untrusted data. - * - * @param source the SSZ encoded bytes - * @return a list of byte arrays - * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is - * not a byte array, or any byte array is too large (greater than 2^32 bytes) - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeByteArrayList(Bytes source) { - return decode(source, SSZReader::readByteArrayList); - } - - /** - * Read a list of byte arrays from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param limit The maximum number of bytes to read for each list element. - * @return a list of byte arrays - * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is - * not a byte array, or any byte array is too large (greater than 2^32 bytes) - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeByteArrayList(Bytes source, int limit) { - return decode(source, r -> r.readByteArrayList(limit)); - } - - /** - * Read a list of strings from the SSZ source. - * - *

Note: prefer to use {@link #decodeStringList(Bytes, int)} instead, especially when reading - * untrusted data. - * - * @param source the SSZ encoded bytes - * @return a list of strings - * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is - * not a string, or any string is too large (greater than 2^32 bytes) - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeStringList(Bytes source) { - return decode(source, SSZReader::readStringList); - } - - /** - * Read a list of strings from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param limit The maximum number of bytes to read for each list element. - * @return a list of strings - * @throws InvalidSSZTypeException if the next SSZ value is not a list, any value in the list is - * not a string, or any string is too large (greater than 2^32 bytes) - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeStringList(Bytes source, int limit) { - return decode(source, r -> r.readStringList(limit)); - } - - /** - * Read a list of two's-compliment int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integers to read (a multiple of 8) - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeIntList(Bytes source, int bitLength) { - return decode(source, r -> r.readIntList(bitLength)); - } - - /** - * Read a list of two's-compliment long int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integers to read (a multiple of 8) - * @return a list of longs - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into a long - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeLongIntList(Bytes source, int bitLength) { - return decode(source, r -> r.readLongIntList(bitLength)); - } - - /** - * Read a list of two's-compliment big integer values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integers to read (a multiple of 8) - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, or there are insufficient - * encoded bytes for the desired bit length or any value in the list - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeBigIntegerList(Bytes source, int bitLength) { - return decode(source, r -> r.readBigIntegerList(bitLength)); - } - - /** - * Read a list of 8-bit two's-compliment int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeInt8List(Bytes source) { - return decode(source, SSZReader::readInt8List); - } - - /** - * Read a list of 16-bit two's-compliment int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeInt16List(Bytes source) { - return decode(source, SSZReader::readInt16List); - } - - /** - * Read a list of 32-bit two's-compliment int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeInt32List(Bytes source) { - return decode(source, SSZReader::readInt32List); - } - - /** - * Read a list of 64-bit two's-compliment int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeInt64List(Bytes source) { - return decode(source, SSZReader::readInt64List); - } - - /** - * Read a list of unsigned int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integers to read (a multiple of 8) - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeUIntList(Bytes source, int bitLength) { - return decode(source, r -> r.readUIntList(bitLength)); - } - - /** - * Read a list of unsigned long int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integers to read (a multiple of 8) - * @return a list of longs - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into a long - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeULongIntList(Bytes source, int bitLength) { - return decode(source, r -> r.readULongIntList(bitLength)); - } - - /** - * Read a list of unsigned big integer values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param bitLength the bit length of the integers to read (a multiple of 8) - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, or there are insufficient - * encoded bytes for the desired bit length of any value in the list - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeUnsignedBigIntegerList(Bytes source, int bitLength) { - return decode(source, r -> r.readUnsignedBigIntegerList(bitLength)); - } - - /** - * Read a list of 8-bit unsigned int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length of any value in the list, or any decoded value was - * too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeUInt8List(Bytes source) { - return decode(source, SSZReader::readUInt8List); - } - - /** - * Read a list of 16-bit unsigned int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length of any value in the list, or any decoded value was - * too large to fit into an int - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeUInt16List(Bytes source) { - return decode(source, SSZReader::readUInt16List); - } - - /** - * Read a list of 32-bit unsigned int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length of any value in the list, or any decoded value was - * too large to fit into a long - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeUInt32List(Bytes source) { - return decode(source, SSZReader::readUInt32List); - } - - /** - * Read a list of 64-bit unsigned int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of ints - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length of any value in the list, or any decoded value was - * too large to fit into a long - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeUInt64List(Bytes source) { - return decode(source, SSZReader::readUInt64List); - } - - /** - * Read a list of 256-bit unsigned int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of {@link UInt256} - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length of any value in the list, or any decoded value was - * too large to fit into {@link UInt256} - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeUInt256List(Bytes source) { - return decode(source, SSZReader::readUInt256List); - } - - /** - * Read a list of 384-bit unsigned int values from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of {@link UInt384} - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length of any value in the list, or any decoded value was - * too large to fit into a {@link UInt384} - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeUInt384List(Bytes source) { - return decode(source, SSZReader::readUInt384List); - } - - /** - * Read a list of 20-byte addresses from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of 20-byte addresses - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for any address in the list - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeAddressList(Bytes source) { - return decode(source, SSZReader::readAddressList); - } - - /** - * Read a list of 32-byte hashes from the SSZ source. - * - * @param source the SSZ encoded bytes - * @param hashLength The length of the hash (in bytes). - * @return a list of 32-byte hashes - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for any hash in the list - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeHashList(Bytes source, int hashLength) { - return decode(source, r -> r.readHashList(hashLength)); - } - - /** - * Read a list of booleans from the SSZ source. - * - * @param source the SSZ encoded bytes - * @return a list of booleans - * @throws InvalidSSZTypeException if the next SSZ value is not a list, there are insufficient - * encoded bytes for all the booleans in the list - * @throws EndOfSSZException if there are no more SSZ values to read - */ - public static List decodeBooleanList(Bytes source) { - return decode(source, SSZReader::readBooleanList); - } -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeTypeList.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeTypeList.java deleted file mode 100644 index 20bceb354..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeTypeList.java +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Supplier; - -public class SSZFixedSizeTypeList - implements SSZReadable, SSZWritable { - - private final int elementSize; - private final Supplier supplier; - - private final List elements = new ArrayList<>(); - - public SSZFixedSizeTypeList(int elementSize, Supplier supplier) { - this.elementSize = elementSize; - this.supplier = supplier; - } - - // The elements might be fixed, but the overall type is variable - @Override - public boolean isFixed() { - return false; - } - - @Override - public void populateFromReader(SSZReader reader) { - elements.addAll(reader.readFixedTypedList(elementSize, supplier)); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeTypedList(elements); - } - - public List getElements() { - return elements; - } -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeVector.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeVector.java deleted file mode 100644 index d287f8411..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZFixedSizeVector.java +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Supplier; - -public class SSZFixedSizeVector - implements SSZReadable, SSZWritable { - - private final int listSize; - private final int elementSize; - private final Supplier supplier; - - private final List elements = new ArrayList<>(); - - public SSZFixedSizeVector(int listSize, int elementSize, Supplier supplier) { - this.listSize = listSize; - this.elementSize = elementSize; - this.supplier = supplier; - } - - @Override - public boolean isFixed() { - return false; - } - - @Override - public void populateFromReader(SSZReader reader) { - elements.addAll(reader.readTypedVector(listSize, elementSize, supplier)); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeTypedVector(elements); - } - - public List getElements() { - return elements; - } -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReadable.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReadable.java deleted file mode 100644 index e06e666af..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReadable.java +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import org.apache.tuweni.ssz.SSZType; - -public interface SSZReadable extends SSZType { - void populateFromReader(SSZReader reader); -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReader.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReader.java deleted file mode 100644 index 71d11724e..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZReader.java +++ /dev/null @@ -1,799 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import org.apache.tuweni.ssz.EndOfSSZException; -import org.apache.tuweni.ssz.InvalidSSZTypeException; -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; -import org.apache.tuweni.v2.units.bigints.UInt384; - -import java.math.BigInteger; -import java.util.List; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -/** A reader for consuming values from an SSZ encoded source. */ -public interface SSZReader { - - /** - * Read bytes from the SSZ source. - * - *

Note: prefer to use {@link #readBytes(int)} instead, especially when reading untrusted data. - * - * @return The bytes for the next value. - * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or is too large - * (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default Bytes readBytes() { - return readBytes(Integer.MAX_VALUE); - } - - /** - * Read bytes from the SSZ source. - * - * @param limit The maximum number of bytes to read. - * @return The bytes for the next value. - * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the - * limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - Bytes readBytes(int limit); - - /** - * Read a known size fixed-length bytes value from the SSZ source. - * - *

Note: prefer to use {@link #readFixedBytes(int, int)} instead, especially when reading - * untrusted data. - * - * @param byteLength The number of fixed-length Bytes (no length mixin) to read. - * @return The bytes for the next value. - * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or is too large - * (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default Bytes readFixedBytes(int byteLength) { - return readFixedBytes(byteLength, Integer.MAX_VALUE); - } - - /** - * Read a known size fixed-length bytes value from the SSZ source. - * - * @param byteLength The number of fixed-length Bytes (no length mixin) to read. - * @param limit The maximum number of bytes to read. - * @return The bytes for the next value. - * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the - * limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - Bytes readFixedBytes(int byteLength, int limit); - - /** - * Read a byte array from the SSZ source. - * - *

Note: prefer to use {@link #readByteArray(int)} instead, especially when reading untrusted - * data. - * - * @return The byte array for the next value. - * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or is too large - * (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default byte[] readByteArray() { - return readByteArray(Integer.MAX_VALUE); - } - - /** - * Read a byte array from the SSZ source. - * - * @param limit The maximum number of bytes to read. - * @return The byte array for the next value. - * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the - * limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default byte[] readByteArray(int limit) { - return readBytes(limit).toArrayUnsafe(); - } - - /** - * Read an array of fixed-length homogenous Bytes from the SSZ source. - * - * @param byteLength The number of fixed-length Bytes per array element. - * @param limit The maximum number of bytes to read. - * @return The byte array for the next value. - * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the - * limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default byte[] readFixedByteArray(int byteLength, int limit) { - return readFixedBytes(byteLength, limit).toArrayUnsafe(); - } - - /** - * Read a string value from the SSZ source. - * - *

Note: prefer to use {@link #readString(int)} instead, especially when reading untrusted - * data. - * - * @return A string. - * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or is too large - * (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default String readString() { - return new String(readByteArray(), UTF_8); - } - - /** - * Read a string value from the SSZ source. - * - * @param limit The maximum number of bytes to read. - * @return A string. - * @throws InvalidSSZTypeException If the next SSZ value is not a byte array, or would exceed the - * limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default String readString(int limit) { - return new String(readByteArray(limit), UTF_8); - } - - /** - * Read a two's-compliment int value from the SSZ source. - * - * @param bitLength The bit length of the integer to read (a multiple of 8). - * @return An int. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit - * length, or the decoded value was too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - int readInt(int bitLength); - - /** - * Read a two's-compliment long value from the SSZ source. - * - * @param bitLength The bit length of the integer to read (a multiple of 8). - * @return A long. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit - * length, or the decoded value was too large to fit into a long. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - long readLong(int bitLength); - - /** - * Read a big integer value from the SSZ source. - * - * @param bitLength The bit length of the integer to read (a multiple of 8). - * @return A big integer. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit - * length. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - BigInteger readBigInteger(int bitLength); - - /** - * Read an 8-bit two's-compliment integer from the SSZ source. - * - * @return An int. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for an 8-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default int readInt8() { - return readInt(8); - } - - /** - * Read a 16-bit two's-compliment integer from the SSZ source. - * - * @return An int. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 16-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default int readInt16() { - return readInt(16); - } - - /** - * Read a 32-bit two's-compliment integer from the SSZ source. - * - * @return An int. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 32-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default int readInt32() { - return readInt(32); - } - - /** - * Read a 64-bit two's-compliment integer from the SSZ source. - * - * @return A long. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 64-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default long readInt64() { - return readLong(64); - } - - /** - * Read an unsigned int value from the SSZ source. - * - * @param bitLength The bit length of the integer to read (a multiple of 8). - * @return An unsigned int. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit - * length, or the decoded value was too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default int readUInt(int bitLength) { - // encoding is the same for unsigned - return readInt(bitLength); - } - - /** - * Read an unsigned long value from the SSZ source. - * - * @param bitLength The bit length of the integer to read (a multiple of 8). - * @return An unsigned long. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit - * length, or the decoded value was too large to fit into a long. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default long readULong(int bitLength) { - // encoding is the same for unsigned - return readLong(bitLength); - } - - /** - * Read an unsigned big integer value from the SSZ source. - * - * @param bitLength The bit length of the integer to read (a multiple of 8). - * @return A big integer. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for the desired bit - * length. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - BigInteger readUnsignedBigInteger(int bitLength); - - /** - * Read an 8-bit unsigned integer from the SSZ source. - * - * @return An int. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for an 8-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default int readUInt8() { - return readUInt(8); - } - - /** - * Read a 16-bit unsigned integer from the SSZ source. - * - * @return An int. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 16-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default int readUInt16() { - return readUInt(16); - } - - /** - * Read a 32-bit unsigned integer from the SSZ source. - * - * @return An int. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 32-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default long readUInt32() { - return readULong(32); - } - - /** - * Read a 64-bit unsigned integer from the SSZ source. - * - * @return A long. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 64-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default long readUInt64() { - return readULong(64); - } - - /** - * Read a {@link UInt256} from the SSZ source. - * - * @return A {@link UInt256}. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 256-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - UInt256 readUInt256(); - - /** - * Read a {@link UInt384} from the SSZ source. - * - * @return A {@link UInt384}. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 384-bit int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - UInt384 readUInt384(); - - /** - * Read a boolean from the SSZ source. - * - * @return A boolean. - * @throws InvalidSSZTypeException If the decoded value is not a boolean. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default boolean readBoolean() { - int value = readInt(8); - if (value == 0) { - return false; - } else if (value == 1) { - return true; - } else { - throw new InvalidSSZTypeException("decoded value is not a boolean"); - } - } - - /** - * Read a 20-byte address from the SSZ source. - * - * @return The bytes of the Address. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 20-byte address. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - Bytes readAddress(); - - /** - * Read a hash from the SSZ source. - * - * @param hashLength The length of the hash (in bytes). - * @return The bytes of the hash. - * @throws InvalidSSZTypeException If there are insufficient encoded bytes for a 32-byte hash. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - Bytes readHash(int hashLength); - - /** - * Read a list of {@link Bytes} from the SSZ source. - * - *

Note: prefer to use {@link #readBytesList(int)} instead, especially when reading untrusted - * data. - * - * @return A list of {@link Bytes}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or any byte array is too large (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readBytesList() { - return readBytesList(Integer.MAX_VALUE); - } - - /** - * Read a known-size fixed-length list of {@link Bytes} from the SSZ source. The list WILL NOT - * have a length mixin, where as the elements WILL. - * - *

Note: prefer to use {@link #readVector(long, int)} instead, especially when reading - * untrusted data. - * - * @param listSize The size of the fixed-length list being read. - * @return A list of {@link Bytes}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or any byte array is too large (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readVector(long listSize) { - return readVector(listSize, Integer.MAX_VALUE); - } - - /** - * Read a known-size fixed length list of known-size fixed length {@link Bytes} from the SSZ - * source. - * - * @param listSize The size of the fixed-length list being read. - * @param byteLength The number of fixed-length Bytes per homogenous List element. - * @param limit The maximum number of bytes to read for each list element. - * @return A list of {@link Bytes}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or the size of any byte array would exceed the limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readFixedBytesVector(int listSize, int byteLength, int limit); - - /** - * Read a known-size fixed length list of known-size fixed length {@link Bytes} from the SSZ - * source. - * - *

Note: prefer to use {@link #readFixedBytesVector(int, int, int)} instead, especially when - * reading untrusted data. - * - * @param listSize The size of the fixed-length list being read. - * @param byteLength The number of fixed-length Bytes per homogenous List element. - * @return A list of {@link Bytes}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or any byte array is too large (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readFixedBytesVector(int listSize, int byteLength) { - return readFixedBytesVector(listSize, byteLength, Integer.MAX_VALUE); - } - - /** - * Read a list of known-size fixed length {@link Bytes} from the SSZ source. A length mixin IS - * expected for the list, but IS NOT expected for the list elements. - * - * @param byteLength The number of fixed-length Bytes per homogenous List element. - * @param limit The maximum number of bytes to read for each list element. - * @return A list of {@link Bytes}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or the size of any byte array would exceed the limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readFixedBytesList(int byteLength, int limit); - - /** - * Read a variable-length list of known-size fixed length {@link Bytes} from the SSZ source. - * - *

Note: prefer to use {@link #readFixedBytesList(int, int)} instead, especially when reading - * untrusted data. - * - * @param byteLength The number of fixed-length Bytes per homogenous List element. - * @return A list of {@link Bytes}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or any byte array is too large (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readFixedBytesList(int byteLength) { - return readFixedBytesList(byteLength, Integer.MAX_VALUE); - } - - /** - * Read a list of {@link Bytes} from the SSZ source. - * - * @param limit The maximum number of bytes to read for each list element. - * @return A list of {@link Bytes}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or the size of any byte array would exceed the limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readBytesList(int limit); - - /** - * Read a known-size fixed-length list of {@link Bytes} from the SSZ source. - * - * @param listSize The size of the fixed-length list being read. - * @param limit The maximum number of bytes to read for each list element. - * @return A list of {@link Bytes}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or the size of any byte array would exceed the limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readVector(long listSize, int limit); - - /** - * Read a list of byte arrays from the SSZ source. - * - *

Note: prefer to use {@link #readByteArrayList(int)} instead, especially when reading - * untrusted data. - * - * @return A list of byte arrays. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or any byte array is too large (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readByteArrayList() { - return readByteArrayList(Integer.MAX_VALUE); - } - - /** - * Read a list of byte arrays from the SSZ source. - * - * @param limit The maximum number of bytes to read for each list element. - * @return A list of byte arrays. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a byte array, or the size of any byte array would exceed the limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readByteArrayList(int limit) { - return readBytesList(limit).stream().map(Bytes::toArrayUnsafe).collect(Collectors.toList()); - } - - /** - * Read a list of strings from the SSZ source. - * - *

Note: prefer to use {@link #readStringList(int)} instead, especially when reading untrusted - * data. - * - * @return A list of strings. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a string, or any string is too large (greater than 2^32 bytes). - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readStringList() { - return readStringList(Integer.MAX_VALUE); - } - - /** - * Read a list of strings from the SSZ source. - * - * @param limit The maximum number of bytes to read for each list element. - * @return A list of strings. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, any value in the list is - * not a string, or the size of any string would exceed the limit. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readStringList(int limit); - - /** - * Read a list of two's-compliment int values from the SSZ source. - * - * @param bitLength The bit length of the integers to read (a multiple of 8). - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readIntList(int bitLength); - - /** - * Read a list of two's-compliment long int values from the SSZ source. - * - * @param bitLength The bit length of the integers to read (a multiple of 8). - * @return A list of longs. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into a long. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readLongIntList(int bitLength); - - /** - * Read a list of two's-compliment big integer values from the SSZ source. - * - * @param bitLength The bit length of the integers to read (a multiple of 8). - * @return A list of big integers. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, or there are insufficient - * encoded bytes for the desired bit length or any value in the list. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readBigIntegerList(int bitLength); - - /** - * Read a list of 8-bit two's-compliment int values from the SSZ source. - * - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readInt8List() { - return readIntList(8); - } - - /** - * Read a list of 16-bit two's-compliment int values from the SSZ source. - * - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readInt16List() { - return readIntList(16); - } - - /** - * Read a list of 32-bit two's-compliment int values from the SSZ source. - * - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readInt32List() { - return readIntList(32); - } - - /** - * Read a list of 64-bit two's-compliment int values from the SSZ source. - * - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readInt64List() { - return readLongIntList(64); - } - - /** - * Read a list of unsigned int values from the SSZ source. - * - * @param bitLength The bit length of the integers to read (a multiple of 8). - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readUIntList(int bitLength) { - // encoding is the same for unsigned - return readIntList(bitLength); - } - - /** - * Read a list of unsigned long int values from the SSZ source. - * - * @param bitLength The bit length of the integers to read (a multiple of 8). - * @return A list of longs. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into a long. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readULongIntList(int bitLength) { - // encoding is the same for unsigned - return readLongIntList(bitLength); - } - - /** - * Read a list of unsigned big integer values from the SSZ source. - * - * @param bitLength The bit length of the integers to read (a multiple of 8). - * @return A list of big integers. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, or there are insufficient - * encoded bytes for the desired bit length or any value in the list. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readUnsignedBigIntegerList(int bitLength); - - /** - * Read a list of 8-bit unsigned int values from the SSZ source. - * - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readUInt8List() { - return readUIntList(8); - } - - /** - * Read a list of 16-bit unsigned int values from the SSZ source. - * - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readUInt16List() { - return readUIntList(16); - } - - /** - * Read a list of 32-bit unsigned int values from the SSZ source. - * - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readUInt32List() { - return readULongIntList(32); - } - - /** - * Read a list of 64-bit unsigned int values from the SSZ source. - * - * @return A list of ints. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - default List readUInt64List() { - return readULongIntList(64); - } - - /** - * Read a list of 256-bit unsigned int values from the SSZ source. - * - * @return A list of {@link UInt256}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readUInt256List(); - - /** - * Read a list of 384-bit unsigned int values from the SSZ source. - * - * @return A list of {@link UInt384}. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for the desired bit length or any value in the list, or any decoded value was - * too large to fit into an int. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readUInt384List(); - - /** - * Read a list of 20-byte addresses from the SSZ source. - * - * @return A list of 20-byte addresses. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for any address in the list. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readAddressList(); - - /** - * Read a list of hashes from the SSZ source. - * - * @param hashLength The length of the hash (in bytes). - * @return A list of hashes. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for any hash in the list. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readHashList(int hashLength); - - /** - * Read a list of booleans from the SSZ source. - * - * @return A list of booleans. - * @throws InvalidSSZTypeException If the next SSZ value is not a list, there are insufficient - * encoded bytes for all the booleans in the list. - * @throws EndOfSSZException If there are no more SSZ values to read. - */ - List readBooleanList(); - - /** - * Check if all values have been read. - * - * @return {@code true} if all values have been read. - */ - boolean isComplete(); - - /** - * Read a SSZ container from the SSZ source and calls populate method on the elements with a - * sliced SSZReader in case of variable size elements to provide a way to detect a list size. - */ - void readAsContainer(SSZReadable... elements); - - /** - * Read a List of Fixed size type from the SSZ source. - * - *

The size of the list is not encoded in the SSZ and cannot be calculated from the data - * either. This method will read until the end of the SSZReader. It is the responsibility of the - * caller to properly slice the parent container SSZReader. The Lists are a variable type and - * therefore start of the scope is specified in parent container using an offset. The end is - * offset of the next variable element, or end of the parent scope if this list is the last in the - * scope. - */ - List readFixedTypedList(int elementSize, Supplier supplier); - - /** - * Reads a Vector from the SSZ source, gets a new instance of T from supplier and calls the - * populate method on it. - */ - List readTypedVector( - int listSize, int elementSize, Supplier supplier); - - /** Returns the remaining bytes in the SSZReader without any manipulation or checks. */ - Bytes consumeRemainingBytes(int limit); - - /** Reads a List of Variable size type from the SSZ source. */ - List readVariableSizeTypeList(Supplier supplier); -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZVariableSizeTypeList.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZVariableSizeTypeList.java deleted file mode 100644 index 0cce02bf6..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZVariableSizeTypeList.java +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Supplier; - -public class SSZVariableSizeTypeList - implements SSZReadable, SSZWritable { - private final Supplier supplier; - - private final List elements = new ArrayList<>(); - - public SSZVariableSizeTypeList(Supplier supplier) { - this.supplier = supplier; - } - - @Override - public boolean isFixed() { - return false; - } - - @Override - public void populateFromReader(SSZReader reader) { - elements.addAll(reader.readVariableSizeTypeList(supplier)); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeTypedList(elements); - } - - public List getElements() { - return elements; - } -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWritable.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWritable.java deleted file mode 100644 index 1f7e104b2..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWritable.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import org.apache.tuweni.ssz.SSZType; - -/** An object that can be written to a {@link SSZWriter}. */ -public interface SSZWritable extends SSZType { - void writeTo(SSZWriter writer); -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWriter.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWriter.java deleted file mode 100644 index 11a3c9d31..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/SSZWriter.java +++ /dev/null @@ -1,739 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; -import org.apache.tuweni.v2.units.bigints.UInt384; - -import java.math.BigInteger; -import java.util.List; - -/** A writer for encoding values to SSZ. */ -public interface SSZWriter { - - /** - * Append an already SSZ encoded value. - * - *

Note that this method may not validate that {@code value} is a valid SSZ sequence. - * Appending an invalid SSZ sequence will cause the entire SSZ encoding produced by this writer to - * also be invalid. - * - * @param value the SSZ encoded bytes to append - */ - void writeSSZ(Bytes value); - - /** - * Append an already SSZ encoded value. - * - *

Note that this method may not validate that {@code value} is a valid SSZ sequence. - * Appending an invalid SSZ sequence will cause the entire SSZ encoding produced by this writer to - * also be invalid. - * - * @param value the SSZ encoded bytes to append - */ - default void writeSSZ(byte[] value) { - writeSSZ(Bytes.wrap(value)); - } - - /** - * Encode a {@link Bytes} value to SSZ. - * - * @param value the byte array to encode - */ - default void writeBytes(Bytes value) { - SSZ.encodeBytesTo(value, this::writeSSZ); - } - - /** - * Encode a byte array to SSZ. - * - * @param value the byte array to encode - */ - default void writeBytes(byte[] value) { - SSZ.encodeByteArrayTo(value, this::writeSSZ); - } - - /** - * Encode a known fixed-length {@link Bytes} value to SSZ without the length mixin. - * - * @param value the byte array to encode - * @throws IllegalArgumentException if the byteLength is not the same size as value. - */ - default void writeFixedBytes(Bytes value) { - SSZ.encodeFixedBytesTo(value, this::writeSSZ); - } - - /** - * Write a string to the output. - * - * @param str the string to write - */ - default void writeString(String str) { - SSZ.encodeStringTo(str, this::writeSSZ); - } - - /** - * Write a two's-compliment integer to the output. - * - * @param value the integer to write - * @param bitLength the bit length of the integer value - * @throws IllegalArgumentException if the value is too large for the specified bit length - */ - default void writeInt(int value, int bitLength) { - writeSSZ(SSZ.encodeLongToByteArray(value, bitLength)); - } - - /** - * Write a two's-compliment long to the output. - * - * @param value the long value to write - * @param bitLength the bit length of the integer value - * @throws IllegalArgumentException if the value is too large for the specified bit length - */ - default void writeLong(long value, int bitLength) { - writeSSZ(SSZ.encodeLongToByteArray(value, bitLength)); - } - - /** - * Write a big integer to the output. - * - * @param value the integer to write - * @param bitLength the bit length of the integer value - * @throws IllegalArgumentException if the value is too large for the specified bit length - */ - default void writeBigInteger(BigInteger value, int bitLength) { - writeSSZ(SSZ.encodeBigIntegerToByteArray(value, bitLength)); - } - - /** - * Write an 8-bit two's-compliment integer to the output. - * - * @param value the integer to write - * @throws IllegalArgumentException if the value is too large to be represented in 8 bits - */ - default void writeInt8(int value) { - writeInt(value, 8); - } - - /** - * Write a 16-bit two's-compliment integer to the output. - * - * @param value the integer to write - * @throws IllegalArgumentException If the value is too large to be represented in 16 bits - */ - default void writeInt16(int value) { - writeInt(value, 16); - } - - /** - * Write a 32-bit two's-compliment integer to the output. - * - * @param value the integer to write - */ - default void writeInt32(int value) { - writeInt(value, 32); - } - - /** - * Write a 64-bit two's-compliment integer to the output. - * - * @param value the long to write - */ - default void writeInt64(long value) { - writeLong(value, 64); - } - - /** - * Write an unsigned integer to the output. - * - *

Note that the argument {@code value} is a native signed int but will be interpreted as an - * unsigned value. - * - * @param value the integer to write - * @param bitLength the bit length of the integer value - * @throws IllegalArgumentException if the value is too large for the specified bit length - */ - default void writeUInt(int value, int bitLength) { - writeSSZ(SSZ.encodeULongToByteArray(value, bitLength)); - } - - /** - * Write an unsigned long to the output. - * - *

Note that the argument {@code value} is a native signed long but will be interpreted as an - * unsigned value. - * - * @param value the long value to write - * @param bitLength the bit length of the integer value - * @throws IllegalArgumentException if the value is too large for the specified bit length - */ - default void writeULong(long value, int bitLength) { - writeSSZ(SSZ.encodeULongToByteArray(value, bitLength)); - } - - /** - * Write an unsigned big integer to the output. - * - * @param value the integer to write - * @param bitLength the bit length of the integer value - * @throws IllegalArgumentException if the value is too large for the specified bit length or the - * value is negative - */ - default void writeUBigInteger(BigInteger value, int bitLength) { - writeSSZ(SSZ.encodeUBigIntegerToByteArray(value, bitLength)); - } - - /** - * Write an 8-bit unsigned integer to the output. - * - * @param value the integer to write - * @throws IllegalArgumentException if the value is too large to be represented in 8 bits - */ - default void writeUInt8(int value) { - writeUInt(value, 8); - } - - /** - * Write a 16-bit unsigned integer to the output. - * - * @param value the integer to write - * @throws IllegalArgumentException If the value is too large to be represented in 16 bits - */ - default void writeUInt16(int value) { - writeUInt(value, 16); - } - - /** - * Write a 32-bit unsigned integer to the output. - * - * @param value the integer to write - */ - default void writeUInt32(long value) { - writeULong(value, 32); - } - - /** - * Write a 64-bit unsigned integer to the output. - * - *

Note that the argument {@code value} is a native signed long but will be interpreted as an - * unsigned value. - * - * @param value the long to write - */ - default void writeUInt64(long value) { - writeULong(value, 64); - } - - /** - * Write a {@link UInt256} to the output. - * - * @param value the {@link UInt256} to write - */ - default void writeUInt256(UInt256 value) { - writeSSZ(SSZ.encodeUInt256(value)); - } - - /** - * Write a {@link UInt384} to the output. - * - * @param value the {@link UInt384} to write - */ - default void writeUInt384(UInt384 value) { - writeSSZ(SSZ.encodeUInt384(value)); - } - - /** - * Write a boolean to the output. - * - * @param value the boolean value - */ - default void writeBoolean(boolean value) { - writeSSZ(SSZ.encodeBoolean(value)); - } - - /** - * Write an address. - * - * @param address the address (must be exactly 20 bytes) - * @throws IllegalArgumentException if {@code address.size != 20} - */ - default void writeAddress(Bytes address) { - writeSSZ(SSZ.encodeAddress(address)); - } - - /** - * Write a hash. - * - * @param hash the hash - */ - default void writeHash(Bytes hash) { - writeSSZ(SSZ.encodeHash(hash)); - } - - /** - * Write a list of bytes. - * - * @param elements the bytes to write as a list - */ - default void writeBytesList(Bytes... elements) { - SSZ.encodeBytesListTo(elements, this::writeSSZ); - } - - /** - * Write a list of bytes. - * - * @param elements the bytes to write as a list - */ - default void writeBytesList(List elements) { - SSZ.encodeBytesListTo(elements, this::writeSSZ); - } - - /** - * Write a vector of bytes. - * - * @param elements the bytes to write as a list - */ - default void writeVector(List elements) { - SSZ.encodeBytesVectorTo(elements, this::writeSSZ); - } - - /** - * Write a list of known-size homogenous bytes. The list itself WILL have a length mixin, but the - * elements WILL NOT. - * - * @param elements the known-size bytes to write as a list - */ - default void writeFixedBytesList(List elements) { - SSZ.encodeFixedBytesListTo(elements, this::writeSSZ); - } - - /** - * Write a known-size fixed-length list of known-size homogenous bytes. Neither the list nor the - * elements in the list will have a length mixin. - * - * @param elements the bytes to write as a list - */ - default void writeFixedBytesVector(List elements) { - SSZ.encodeFixedBytesVectorTo(elements, this::writeSSZ); - } - - /** - * Write a list of strings, which must be of the same length - * - * @param elements the strings to write as a list - */ - default void writeStringList(String... elements) { - SSZ.encodeStringListTo(elements, this::writeSSZ); - } - - /** - * Write a list of strings, which must be of the same length - * - * @param elements the strings to write as a list - */ - default void writeStringList(List elements) { - SSZ.encodeStringListTo(elements, this::writeSSZ); - } - - /** - * Write a list of two's compliment integers. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large for the specified bit length - */ - default void writeIntList(int bitLength, int... elements) { - SSZ.encodeIntListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of two's compliment integers. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large for the specified bit length - */ - default void writeIntList(int bitLength, List elements) { - SSZ.encodeIntListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of two's compliment long integers. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the long integers to write as a list - * @throws IllegalArgumentException if any values are too large for the specified bit length - */ - default void writeLongIntList(int bitLength, long... elements) { - SSZ.encodeLongIntListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of two's compliment long integers. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the long integers to write as a list - * @throws IllegalArgumentException if any values are too large for the specified bit length - */ - default void writeLongIntList(int bitLength, List elements) { - SSZ.encodeLongIntListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of big integers. - * - * @param bitLength the bit length of each integer - * @param elements the integers to write as a list - * @throws IllegalArgumentException if an integer cannot be stored in the number of bytes provided - */ - default void writeBigIntegerList(int bitLength, BigInteger... elements) { - SSZ.encodeBigIntegerListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of big integers. - * - * @param bitLength the bit length of each integer - * @param elements the integers to write as a list - * @throws IllegalArgumentException if an integer cannot be stored in the number of bytes provided - */ - default void writeBigIntegerList(int bitLength, List elements) { - SSZ.encodeBigIntegerListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of 8-bit two's compliment integers. - * - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large to be represented in 8 bits - */ - default void writeInt8List(int... elements) { - writeIntList(8, elements); - } - - /** - * Write a list of 8-bit two's compliment integers. - * - * @param elements the integers to write as a list. - * @throws IllegalArgumentException if any values are too large to be represented in 8 bits - */ - default void writeInt8List(List elements) { - writeIntList(8, elements); - } - - /** - * Write a list of 16-bit two's compliment integers. - * - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large to be represented in 16 bits - */ - default void writeInt16List(int... elements) { - writeIntList(16, elements); - } - - /** - * Write a list of 16-bit two's compliment integers. - * - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large to be represented in 16 bits - */ - default void writeInt16List(List elements) { - writeIntList(16, elements); - } - - /** - * Write a list of 32-bit two's compliment integers. - * - * @param elements the integers to write as a list - */ - default void writeInt32List(int... elements) { - writeIntList(32, elements); - } - - /** - * Write a list of 32-bit two's compliment integers. - * - * @param elements the integers to write as a list - */ - default void writeInt32List(List elements) { - writeIntList(32, elements); - } - - /** - * Write a list of 64-bit two's compliment integers. - * - * @param elements the integers to write as a list - */ - default void writeInt64List(long... elements) { - writeLongIntList(64, elements); - } - - /** - * Write a list of 64-bit two's compliment integers. - * - * @param elements the integers to write as a list - */ - default void writeInt64List(List elements) { - writeLongIntList(64, elements); - } - - /** - * Write a list of unsigned integers. - * - *

Note that the {@code elements} are native signed ints, but will be interpreted as an - * unsigned values. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large for the specified bit length - */ - default void writeUIntList(int bitLength, int... elements) { - SSZ.encodeUIntListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of unsigned integers. - * - *

Note that the {@code elements} are native signed ints, but will be interpreted as an - * unsigned values. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large for the specified bit length - */ - default void writeUIntList(int bitLength, List elements) { - SSZ.encodeUIntListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of unsigned long integers. - * - *

Note that the {@code elements} are native signed longs, but will be interpreted as an - * unsigned values. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the long integers to write as a list - * @throws IllegalArgumentException if any values are too large for the specified bit length - */ - default void writeULongIntList(int bitLength, long... elements) { - SSZ.encodeULongIntListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of unsigned long integers. - * - *

Note that the {@code elements} are native signed longs, but will be interpreted as an - * unsigned values. - * - * @param bitLength the bit length of the encoded integers (must be a multiple of 8) - * @param elements the long integers to write as a list - * @throws IllegalArgumentException if any values are too large for the specified bit length - */ - default void writeULongIntList(int bitLength, List elements) { - SSZ.encodeULongIntListTo(bitLength, elements, this::writeSSZ); - } - - /** - * Write a list of 8-bit unsigned integers. - * - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large to be represented in 8 bits - */ - default void writeUInt8List(int... elements) { - writeUIntList(8, elements); - } - - /** - * Write a list of 8-bit unsigned integers. - * - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large to be represented in 8 bits - */ - default void writeUInt8List(List elements) { - writeUIntList(8, elements); - } - - /** - * Write a list of 16-bit unsigned integers. - * - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large to be represented in 16 bits - */ - default void writeUInt16List(int... elements) { - writeUIntList(16, elements); - } - - /** - * Write a list of 16-bit unsigned integers. - * - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large to be represented in 16 bits - */ - default void writeUInt16List(List elements) { - writeUIntList(16, elements); - } - - /** - * Write a list of 32-bit unsigned integers. - * - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large to be represented in 32 bits - */ - default void writeUInt32List(long... elements) { - writeULongIntList(32, elements); - } - - /** - * Write a list of 32-bit unsigned integers. - * - * @param elements the integers to write as a list - * @throws IllegalArgumentException if any values are too large to be represented in 32 bits - */ - default void writeUInt32List(List elements) { - writeULongIntList(32, elements); - } - - /** - * Write a list of 64-bit unsigned integers. - * - *

Note that the {@code elements} are native signed longs, but will be interpreted as an - * unsigned values. - * - * @param elements the integers to write as a list - */ - default void writeUInt64List(long... elements) { - writeULongIntList(64, elements); - } - - /** - * Write a list of 64-bit unsigned integers. - * - *

Note that the {@code elements} are native signed longs, but will be interpreted as an - * unsigned values. - * - * @param elements the integers to write as a list - */ - default void writeUInt64List(List elements) { - writeULongIntList(64, elements); - } - - /** - * Write a list of unsigned 256-bit integers. - * - * @param elements the integers to write as a list - */ - default void writeUInt256List(UInt256... elements) { - SSZ.encodeUInt256ListTo(elements, this::writeSSZ); - } - - /** - * Write a list of unsigned 256-bit integers. - * - * @param elements the integers to write as a list - */ - default void writeUInt256List(List elements) { - SSZ.encodeUInt256ListTo(elements, this::writeSSZ); - } - - /** - * Write a list of unsigned 384-bit integers. - * - * @param elements the integers to write as a list - */ - default void writeUInt384List(List elements) { - SSZ.encodeUInt384ListTo(elements, this::writeSSZ); - } - - /** - * Write a list of unsigned 384-bit integers. - * - * @param elements the integers to write as a list - */ - default void writeUInt384List(UInt384... elements) { - SSZ.encodeUInt384ListTo(elements, this::writeSSZ); - } - - /** - * Write a list of hashes. - * - * @param elements the hashes to write as a list - */ - default void writeHashList(Bytes... elements) { - SSZ.encodeHashListTo(elements, this::writeSSZ); - } - - /** - * Write a list of hashes. - * - * @param elements the hashes to write as a list - */ - default void writeHashList(List elements) { - SSZ.encodeHashListTo(elements, this::writeSSZ); - } - - /** - * Write a list of addresses. - * - * @param elements the addresses to write as a list - * @throws IllegalArgumentException if any {@code address.size != 20} - */ - default void writeAddressList(Bytes... elements) { - SSZ.encodeAddressListTo(elements, this::writeSSZ); - } - - /** - * Write a list of addresses. - * - * @param elements the addresses to write as a list - * @throws IllegalArgumentException if any {@code address.size != 20} - */ - default void writeAddressList(List elements) { - SSZ.encodeAddressListTo(elements, this::writeSSZ); - } - - /** - * Write a list of booleans. - * - * @param elements the booleans to write as a list - */ - default void writeBooleanList(boolean... elements) { - SSZ.encodeBooleanListTo(elements, this::writeSSZ); - } - - /** - * Write a list of booleans. - * - * @param elements the booleans to write as a list - */ - default void writeBooleanList(List elements) { - SSZ.encodeBooleanListTo(elements, this::writeSSZ); - } - - /** - * Write a list of SSZWritable elements. - * - * @param elements the elements to write as a list - */ - default void writeAsContainer(SSZWritable... elements) { - SSZ.encodeAsContainerTo(this::writeSSZ, elements); - } - - /** - * Write a list of SSZWritable elements. - * - * @param elements the elements to write as a list - */ - default void writeTypedList(List elements) { - SSZ.encodeAsFixedTypeListTo(elements, this::writeSSZ); - } - - /** - * Write a list of SSZWritable elements. - * - * @param elements the elements to write as a list - */ - default void writeTypedVector(List elements) { - SSZ.encodeAsFixedTypeVectorTo(elements, this::writeSSZ); - } -} diff --git a/ssz/src/main/java/org/apache/tuweni/v2/ssz/package-info.java b/ssz/src/main/java/org/apache/tuweni/v2/ssz/package-info.java deleted file mode 100644 index cdc9d7055..000000000 --- a/ssz/src/main/java/org/apache/tuweni/v2/ssz/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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. - * - *

Simple Serialize (SSZ) encoding and decoding. - * - *

An implementation of the Ethereum Simple Serialize (SSZ) algorithm, as described at https://github.com/ethereum/eth2.0-specs/blob/master/specs/simple-serialize.md. - * - *

These classes are included in the standard Tuweni distribution, or separately when using the - * gradle dependency 'org.apache.tuweni:tuweni-ssz' (tuweni-ssz.jar). - */ -package org.apache.tuweni.v2.ssz; diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/ByteBufferWriterTest.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/ByteBufferWriterTest.java deleted file mode 100644 index cb6384985..000000000 --- a/ssz/src/test/java/org/apache/tuweni/v2/ssz/ByteBufferWriterTest.java +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; - -import java.math.BigInteger; -import java.nio.ByteBuffer; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -class ByteBufferWriterTest { - - @ParameterizedTest - @CsvSource({"E8030000, 1000", "A0860100, 100000"}) - void shouldWriteSmallIntegers(String expectedHex, int value) { - ByteBuffer buffer = ByteBuffer.allocate(64); - SSZ.encodeTo(buffer, writer -> writer.writeInt(value, 32)); - buffer.flip(); - assertEquals(fromHexString(expectedHex), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteLongIntegers() { - ByteBuffer buffer = ByteBuffer.allocate(64); - SSZ.encodeTo(buffer, writer -> writer.writeLong(100000L, 24)); - buffer.flip(); - assertEquals(fromHexString("A08601"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteUInt256Integers() { - ByteBuffer buffer = ByteBuffer.allocate(64); - SSZ.encodeTo(buffer, writer -> writer.writeUInt256(UInt256.valueOf(100000L))); - buffer.flip(); - assertEquals( - fromHexString("A086010000000000000000000000000000000000000000000000000000000000"), - Bytes.wrapByteBuffer(buffer)); - - buffer.clear(); - SSZ.encodeTo( - buffer, - writer -> - writer.writeUInt256( - UInt256.fromHexString( - "0x0400000000000000000000000000000000000000000000000000f100000000ab"))); - buffer.flip(); - assertEquals( - fromHexString("AB00000000F10000000000000000000000000000000000000000000000000004"), - Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteBigIntegers() { - ByteBuffer buffer = ByteBuffer.allocate(64); - SSZ.encodeTo(buffer, writer -> writer.writeBigInteger(BigInteger.valueOf(100000), 24)); - buffer.flip(); - assertEquals(fromHexString("A08601"), Bytes.wrapByteBuffer(buffer)); - - buffer.clear(); - SSZ.encodeTo(buffer, writer -> writer.writeBigInteger(BigInteger.valueOf(127).pow(16), 112)); - buffer.flip(); - assertEquals(fromHexString("01F81D7AF1971CEDD9BBA5EFCEE1"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteEmptyStrings() { - ByteBuffer buffer = ByteBuffer.allocate(64); - SSZ.encodeTo(buffer, writer -> writer.writeString("")); - buffer.flip(); - assertEquals(fromHexString("00000000"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteOneCharactersStrings() { - ByteBuffer buffer = ByteBuffer.allocate(64); - SSZ.encodeTo(buffer, writer -> writer.writeString("d")); - buffer.flip(); - assertEquals(fromHexString("0100000064"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteStrings() { - ByteBuffer buffer = ByteBuffer.allocate(64); - SSZ.encodeTo(buffer, writer -> writer.writeString("dog")); - buffer.flip(); - assertEquals(fromHexString("03000000646F67"), Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWriteShortLists() { - String[] strings = - new String[] { - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer" - }; - - ByteBuffer buffer = ByteBuffer.allocate(256); - SSZ.encodeTo(buffer, w -> w.writeStringList(strings)); - buffer.flip(); - - assertEquals( - fromHexString( - "5800000004000000617364660400000071776572040000007A78637604000000617364660400000071776572040000007A78637604000000617364660400000071776572040000007A78637604000000617364660400000071776572"), - Bytes.wrapByteBuffer(buffer)); - } - - @Test - void shouldWritePreviouslyEncodedValues() { - ByteBuffer buffer = ByteBuffer.allocate(64); - SSZ.encodeTo(buffer, writer -> writer.writeSSZ(SSZ.encodeByteArray("abc".getBytes(UTF_8)))); - buffer.flip(); - assertEquals("abc", SSZ.decodeString(Bytes.wrapByteBuffer(buffer))); - } -} diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZReaderTest.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZReaderTest.java deleted file mode 100644 index 54505f842..000000000 --- a/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZReaderTest.java +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; -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.ssz.EndOfSSZException; -import org.apache.tuweni.ssz.InvalidSSZTypeException; -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.bytes.Bytes32; -import org.apache.tuweni.v2.bytes.Bytes48; - -import java.io.IOException; -import java.io.InputStream; -import java.math.BigInteger; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -class BytesSSZReaderTest { - - private static final Bytes SHORT_LIST = - fromHexString( - "5800000004000000617364660400000071776572040000007A78637604000000617364660400000071776572040000007A78637604000000617364660400000071776572040000007A78637604000000617364660400000071776572"); - - private static class SomeObject { - private final String name; - private final int number; - private final BigInteger longNumber; - - SomeObject(String name, int number, BigInteger longNumber) { - this.name = name; - this.number = number; - this.longNumber = longNumber; - } - } - - private static class AnotherObject { - private final long number1; - private final long number2; - - public AnotherObject(long number1, long number2) { - this.number1 = number1; - this.number2 = number2; - } - } - - @Test - void shouldParseFullObjects() { - Bytes bytes = - fromHexString( - "0x03000000426F62046807B7711F010000000000000000000000000000000000000000000000000000"); - SomeObject readObject = - SSZ.decode(bytes, r -> new SomeObject(r.readString(), r.readInt8(), r.readBigInteger(256))); - - assertEquals("Bob", readObject.name); - assertEquals(4, readObject.number); - assertEquals(BigInteger.valueOf(1234563434344L), readObject.longNumber); - } - - @Test - void shouldRoundTripComplexObjects() throws IOException { - String resourceName = "org/apache/tuweni/ssz/signedBlobTransaction.hex"; - InputStream inputStream = - Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(resourceName)); - String inputString = new String(inputStream.readAllBytes(), UTF_8); - Bytes bytes = fromHexString(inputString); - - TransactionNetworkPayload payload = - SSZ.decode( - bytes, - sszReader -> { - var p = new TransactionNetworkPayload(); - p.populateFromReader(sszReader); - return p; - }); - - Bytes encoded = SSZ.encode(payload::writeTo); - assertEquals(inputString, encoded.toString()); - } - - @ParameterizedTest - @CsvSource({ - "00, 0", - "01, 1", - "10, 16", - "4f, 79", - "7f, 127", - "8000, 128", - "e803, 1000", - "a0860100, 100000", - "a08601000000, 100000" - }) - void shouldReadIntegers(String hex, int value) { - assertTrue( - SSZ.decode( - fromHexString(hex), - reader -> { - assertEquals(value, reader.readInt(hex.length() * 4)); - return true; - })); - } - - /** - * Related to the bug when {@link BytesSSZReader#readLong(int)} calculates lead zeroes from - * beginning of whole content instead of the current value - */ - @Test - void shouldCorrectlyParseLongs() { - Bytes bytes = fromHexString("7b00000000000000" + "ffffffffff7f0000"); - AnotherObject readObject = - SSZ.decode(bytes, r -> new AnotherObject(r.readLong(64), r.readLong(64))); - - assertEquals(123, readObject.number1); - assertEquals(140737488355327L, readObject.number2); - } - - @Test - void shouldThrowWhenReadingOversizedInt() { - InvalidSSZTypeException ex = - assertThrows( - InvalidSSZTypeException.class, - () -> SSZ.decode(fromHexString("1122334455667788"), r -> r.readInt(64))); - assertEquals("decoded integer is too large for an int", ex.getMessage()); - } - - @ParameterizedTest - // @formatter:off - @CsvSource({ - "00000000, ''", - "0100000000, '\u0000'", - "0100000001, '\u0001'", - "010000007f, '\u007F'", - "03000000646f67, dog", - "370000004C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E7365637465747572206164697069736963696E6720656C69" - + ", 'Lorem ipsum dolor sit amet, consectetur adipisicing eli'", - "380000004C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E7365637465747572206164697069736963696E6720656C6974" - + ", 'Lorem ipsum dolor sit amet, consectetur adipisicing elit'", - "000400004C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E73656374657475722061646970697363696E6720656C69742E20437572616269747572206D6175726973206D61676E612C20737573636970697420736564207665686963756C61206E6F6E2C20696163756C697320666175636962757320746F72746F722E2050726F696E20737573636970697420756C74726963696573206D616C6573756164612E204475697320746F72746F7220656C69742C2064696374756D2071756973207472697374697175652065752C20756C7472696365732061742072697375732E204D6F72626920612065737420696D70657264696574206D6920756C6C616D636F7270657220616C6971756574207375736369706974206E6563206C6F72656D2E2041656E65616E2071756973206C656F206D6F6C6C69732C2076756C70757461746520656C6974207661726975732C20636F6E73657175617420656E696D2E204E756C6C6120756C74726963657320747572706973206A7573746F2C20657420706F73756572652075726E6120636F6E7365637465747572206E65632E2050726F696E206E6F6E20636F6E76616C6C6973206D657475732E20446F6E65632074656D706F7220697073756D20696E206D617572697320636F6E67756520736F6C6C696369747564696E2E20566573746962756C756D20616E746520697073756D207072696D697320696E206661756369627573206F726369206C756374757320657420756C74726963657320706F737565726520637562696C69612043757261653B2053757370656E646973736520636F6E76616C6C69732073656D2076656C206D617373612066617563696275732C2065676574206C6163696E6961206C616375732074656D706F722E204E756C6C61207175697320756C747269636965732070757275732E2050726F696E20617563746F722072686F6E637573206E69626820636F6E64696D656E74756D206D6F6C6C69732E20416C697175616D20636F6E73657175617420656E696D206174206D65747573206C75637475732C206120656C656966656E6420707572757320656765737461732E20437572616269747572206174206E696268206D657475732E204E616D20626962656E64756D2C206E6571756520617420617563746F72207472697374697175652C206C6F72656D206C696265726F20616C697175657420617263752C206E6F6E20696E74657264756D2074656C6C7573206C65637475732073697420616D65742065726F732E20437261732072686F6E6375732C206D65747573206163206F726E617265206375727375732C20646F6C6F72206A7573746F20756C747269636573206D657475732C20617420756C6C616D636F7270657220766F6C7574706174" - + ", 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat'" - }) - // @formatter:on - void shouldReadStrings(String hex, String value) { - System.out.println(SSZ.encodeString(value)); - assertTrue( - SSZ.decode( - fromHexString(hex), - reader -> { - assertEquals(value, reader.readString()); - return true; - })); - } - - @Test - void shouldThrowWhenInputExhausted() { - EndOfSSZException ex = - assertThrows( - EndOfSSZException.class, () -> SSZ.decode(Bytes.EMPTY, reader -> reader.readInt(16))); - assertEquals("End of SSZ source reached", ex.getMessage()); - } - - @Test - void shouldThrowWheSourceIsTruncated() { - InvalidSSZTypeException ex = - assertThrows( - InvalidSSZTypeException.class, - () -> SSZ.decode(fromHexString("0000000f830186"), SSZReader::readBytes)); - assertEquals( - "SSZ encoded data has insufficient bytes for decoded byte array length", ex.getMessage()); - } - - @Test - void shouldReadShortVaragsList() { - List expected = - Arrays.asList( - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); - - System.out.println( - SSZ.encodeStringList( - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", - "qwer")); - - List result = SSZ.decodeStringList(SHORT_LIST); - assertEquals(expected, result); - } - - @Test - void shouldReadShortUtilList() { - List expected = - Arrays.asList( - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"); - - System.out.println( - SSZ.encodeStringList( - Arrays.asList( - "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", - "qwer"))); - - List result = SSZ.decodeStringList(SHORT_LIST); - assertEquals(expected, result); - } - - @Test - void shouldAcceptStringListOfVariableLengths() { - List expected = Arrays.asList("one", "three", "four"); - - List result = - SSZ.decodeStringList( - Bytes.fromHexString("0x18000000030000006F6E6505000000746872656504000000666F7572")); - assertEquals(expected, result); - } - - @Test - void shouldRoundtripBytesVararg() { - List toWrite = - Arrays.asList(Bytes48.fromRandom(), Bytes48.fromRandom(), Bytes48.fromRandom()); - Bytes encoded = SSZ.encode(writer -> writer.writeBytesList(toWrite.toArray(new Bytes[0]))); - assertEquals(toWrite, SSZ.decodeBytesList(encoded)); - } - - @Test - void shouldRoundtripBytesList() { - List toWrite = - Arrays.asList(Bytes48.fromRandom(), Bytes48.fromRandom(), Bytes48.fromRandom()); - Bytes encoded = SSZ.encode(writer -> writer.writeBytesList(toWrite)); - assertEquals(toWrite, SSZ.decodeBytesList(encoded)); - } - - @Test - void shouldRoundtripBytesVector() { - List toWrite = - Arrays.asList(Bytes48.fromRandom(), Bytes48.fromRandom(), Bytes48.fromRandom()); - Bytes encoded = SSZ.encode(writer -> writer.writeFixedBytesVector(toWrite)); - assertEquals(toWrite, SSZ.decode(encoded, reader -> reader.readFixedBytesVector(3, 48))); - } - - @Test - void shouldRoundtripHomogenousBytesList() { - List toWrite = - Arrays.asList(Bytes32.fromRandom(), Bytes32.fromRandom(), Bytes32.fromRandom()); - Bytes encoded = SSZ.encode(writer -> writer.writeFixedBytesList(toWrite)); - assertEquals(toWrite, SSZ.decode(encoded, reader -> reader.readFixedBytesList(32))); - } -} diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZWriterTest.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZWriterTest.java deleted file mode 100644 index cae213ea7..000000000 --- a/ssz/src/test/java/org/apache/tuweni/v2/ssz/BytesSSZWriterTest.java +++ /dev/null @@ -1,539 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.tuweni.v2.bytes.Bytes.fromHexString; -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.v2.bytes.Bytes; -import org.apache.tuweni.v2.bytes.Bytes32; -import org.apache.tuweni.v2.units.bigints.UInt256; - -import java.math.BigInteger; -import java.util.Arrays; -import java.util.List; - -import com.google.common.base.Charsets; -import org.junit.jupiter.api.Test; - -class BytesSSZWriterTest { - - private static class SomeObject { - private final String name; - private final int number; - private final BigInteger longNumber; - - SomeObject(String name, int number, BigInteger longNumber) { - this.name = name; - this.number = number; - this.longNumber = longNumber; - } - } - - @Test - void shouldWriteFullObjects() { - SomeObject bob = new SomeObject("Bob", 4, BigInteger.valueOf(1234563434344L)); - - Bytes bytes = - SSZ.encode( - writer -> { - writer.writeString(bob.name); - writer.writeInt(bob.number, 8); - writer.writeBigInteger(bob.longNumber, 256); - }); - - assertTrue( - SSZ.decode( - bytes, - reader -> { - assertEquals("Bob", reader.readString()); - assertEquals(4, reader.readInt(8)); - assertEquals(BigInteger.valueOf(1234563434344L), reader.readBigInteger(256)); - return true; - })); - } - - @Test - void shouldWriteEmptyStrings() { - assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeString(""))); - } - - @Test - void shouldWriteOneCharactersStrings() { - assertEquals(fromHexString("0100000064"), SSZ.encode(writer -> writer.writeString("d"))); - } - - @Test - void shouldWriteStrings() { - assertEquals(fromHexString("03000000646f67"), SSZ.encode(writer -> writer.writeString("dog"))); - } - - @Test - void shouldWriteSignedIntegers() { - assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeInt(0, 8))); - assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeInt8(0))); - - assertEquals(fromHexString("0000"), SSZ.encode(writer -> writer.writeInt(0, 16))); - assertEquals(fromHexString("0000"), SSZ.encode(writer -> writer.writeInt16(0))); - - assertEquals(fromHexString("000000"), SSZ.encode(writer -> writer.writeInt(0, 24))); - - assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeInt(0, 32))); - assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeInt32(0))); - - assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeInt(1, 8))); - assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeInt8(1))); - - assertEquals(fromHexString("0100"), SSZ.encode(writer -> writer.writeInt(1, 16))); - assertEquals(fromHexString("0100"), SSZ.encode(writer -> writer.writeInt16(1))); - - assertEquals(fromHexString("01000000"), SSZ.encode(writer -> writer.writeInt(1, 32))); - assertEquals(fromHexString("01000000"), SSZ.encode(writer -> writer.writeInt32(1))); - - assertEquals(fromHexString("0f"), SSZ.encode(writer -> writer.writeInt8(15))); - assertEquals(fromHexString("0f00"), SSZ.encode(writer -> writer.writeInt16(15))); - assertEquals(fromHexString("E803"), SSZ.encode(writer -> writer.writeInt16(1000))); - assertEquals(fromHexString("0004"), SSZ.encode(writer -> writer.writeInt16(1024))); - assertEquals(fromHexString("A08601"), SSZ.encode(writer -> writer.writeInt(100000, 24))); - - assertEquals(fromHexString("FF"), SSZ.encode(writer -> writer.writeInt(-1, 8))); - assertEquals(fromHexString("FFFF"), SSZ.encode(writer -> writer.writeInt(-1, 16))); - assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeInt(-128, 8))); - assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeInt8(-128))); - assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeInt(-32768, 16))); - assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeInt16(-32768))); - } - - @Test - void shouldWriteSignedLongs() { - assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeLong(0, 8))); - assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeLong(1, 8))); - assertEquals(fromHexString("0f"), SSZ.encode(writer -> writer.writeLong(15, 8))); - - assertEquals(fromHexString("e803"), SSZ.encode(writer -> writer.writeLong(1000, 16))); - assertEquals(fromHexString("0004"), SSZ.encode(writer -> writer.writeLong(1024, 16))); - assertEquals(fromHexString("A08601"), SSZ.encode(writer -> writer.writeLong(100000L, 24))); - assertEquals(fromHexString("A0860100"), SSZ.encode(writer -> writer.writeLong(100000L, 32))); - assertEquals( - fromHexString("A086010000000000"), SSZ.encode(writer -> writer.writeLong(100000L, 64))); - assertEquals( - fromHexString("A086010000000000"), SSZ.encode(writer -> writer.writeInt64(100000L))); - - assertEquals(fromHexString("FF"), SSZ.encode(writer -> writer.writeLong(-1, 8))); - assertEquals(fromHexString("FFFF"), SSZ.encode(writer -> writer.writeLong(-1, 16))); - assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeLong(-128, 8))); - assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeLong(-32768, 16))); - assertEquals( - fromHexString("0000000000000080"), - SSZ.encode(writer -> writer.writeInt64(-9223372036854775808L))); - } - - @Test - void shouldWriteSignedBigIntegers() { - assertEquals( - fromHexString("A08601"), - SSZ.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(100000), 24))); - assertEquals( - fromHexString("16EB"), - SSZ.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(-5354), 16))); - assertEquals( - fromHexString("0080"), - SSZ.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(-32768), 16))); - assertEquals( - fromHexString("01F81D7AF1971CEDD9BBA5EFCEE1"), - SSZ.encode(writer -> writer.writeBigInteger(BigInteger.valueOf(127).pow(16), 112))); - } - - @Test - void shouldWriteUnsignedIntegers() { - assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeUInt(0, 8))); - assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeUInt8(0))); - - assertEquals(fromHexString("0000"), SSZ.encode(writer -> writer.writeUInt(0, 16))); - assertEquals(fromHexString("0000"), SSZ.encode(writer -> writer.writeUInt16(0))); - - assertEquals(fromHexString("000000"), SSZ.encode(writer -> writer.writeUInt(0, 24))); - - assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeUInt(0, 32))); - assertEquals(fromHexString("00000000"), SSZ.encode(writer -> writer.writeUInt32(0))); - - assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeUInt(1, 8))); - assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeUInt8(1))); - - assertEquals(fromHexString("0100"), SSZ.encode(writer -> writer.writeUInt(1, 16))); - assertEquals(fromHexString("0100"), SSZ.encode(writer -> writer.writeUInt16(1))); - - assertEquals(fromHexString("01000000"), SSZ.encode(writer -> writer.writeUInt(1, 32))); - assertEquals(fromHexString("01000000"), SSZ.encode(writer -> writer.writeUInt32(1))); - - assertEquals(fromHexString("0f"), SSZ.encode(writer -> writer.writeUInt8(15))); - assertEquals(fromHexString("0f00"), SSZ.encode(writer -> writer.writeUInt16(15))); - assertEquals(fromHexString("e803"), SSZ.encode(writer -> writer.writeUInt16(1000))); - assertEquals(fromHexString("0004"), SSZ.encode(writer -> writer.writeUInt16(1024))); - assertEquals(fromHexString("A08601"), SSZ.encode(writer -> writer.writeUInt(100000, 24))); - - assertEquals(fromHexString("FF"), SSZ.encode(writer -> writer.writeUInt(255, 8))); - assertEquals(fromHexString("FFFF"), SSZ.encode(writer -> writer.writeUInt(65535, 16))); - assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeUInt(128, 8))); - assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeUInt8(128))); - assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeUInt(32768, 16))); - assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeUInt16(32768))); - } - - @Test - void shouldWriteUnsignedLongs() { - assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeULong(0, 8))); - assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeULong(1, 8))); - assertEquals(fromHexString("0f"), SSZ.encode(writer -> writer.writeULong(15, 8))); - - assertEquals(fromHexString("e803"), SSZ.encode(writer -> writer.writeULong(1000, 16))); - assertEquals(fromHexString("0004"), SSZ.encode(writer -> writer.writeULong(1024, 16))); - assertEquals(fromHexString("A08601"), SSZ.encode(writer -> writer.writeULong(100000L, 24))); - assertEquals(fromHexString("A0860100"), SSZ.encode(writer -> writer.writeULong(100000L, 32))); - assertEquals( - fromHexString("A086010000000000"), SSZ.encode(writer -> writer.writeULong(100000L, 64))); - assertEquals( - fromHexString("A086010000000000"), SSZ.encode(writer -> writer.writeUInt64(100000L))); - - assertEquals(fromHexString("FF"), SSZ.encode(writer -> writer.writeULong(255, 8))); - assertEquals(fromHexString("FFFF"), SSZ.encode(writer -> writer.writeULong(65535, 16))); - assertEquals(fromHexString("80"), SSZ.encode(writer -> writer.writeULong(128, 8))); - assertEquals(fromHexString("0080"), SSZ.encode(writer -> writer.writeULong(32768, 16))); - assertEquals( - fromHexString("0000000000000080"), - SSZ.encode( - writer -> { - writer.writeUInt64(Long.parseUnsignedLong("9223372036854775808")); - })); - } - - @Test - void shouldWriteUInt256Integers() { - assertEquals( - fromHexString("0000000000000000000000000000000000000000000000000000000000000000"), - SSZ.encode(writer -> writer.writeUInt256(UInt256.valueOf(0L)))); - assertEquals( - fromHexString("A086010000000000000000000000000000000000000000000000000000000000"), - SSZ.encode(writer -> writer.writeUInt256(UInt256.valueOf(100000L)))); - assertEquals( - fromHexString("AB00000000F10000000000000000000000000000000000000000000000000004"), - SSZ.encode( - writer -> - writer.writeUInt256( - UInt256.fromHexString( - "0x0400000000000000000000000000000000000000000000000000f100000000ab")))); - } - - @Test - void shouldWriteBooleans() { - assertEquals(fromHexString("00"), SSZ.encode(writer -> writer.writeBoolean(false))); - assertEquals(fromHexString("01"), SSZ.encode(writer -> writer.writeBoolean(true))); - } - - @Test - void shouldWriteAddresses() { - assertEquals( - fromHexString("8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), - SSZ.encode( - writer -> - writer.writeAddress( - Bytes.fromHexString("8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97")))); - assertThrows( - IllegalArgumentException.class, - () -> SSZ.encode(writer -> writer.writeAddress(Bytes.fromHexString("beef")))); - } - - @Test - void shouldWriteHashes() { - assertEquals( - fromHexString("ED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), - SSZ.encode( - writer -> - writer.writeHash( - Bytes.fromHexString( - "ED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97")))); - assertThrows( - IllegalArgumentException.class, - () -> SSZ.encode(writer -> writer.writeAddress(Bytes.fromHexString("beef")))); - } - - @Test - void shouldWriteVarargsListsOfInts() { - assertEquals(fromHexString("03000000030405"), SSZ.encodeIntList(8, 3, 4, 5)); - } - - @Test - void shouldWriteUtilListsOfInts() { - assertEquals(fromHexString("03000000030405"), SSZ.encodeIntList(8, Arrays.asList(3, 4, 5))); - } - - @Test - void shouldWriteVarargsListsOfLongInts() { - assertEquals(fromHexString("03000000030405"), SSZ.encodeLongIntList(8, 3, 4, 5)); - } - - @Test - void shouldWriteUtilListsOfLongInts() { - assertEquals( - fromHexString("03000000030405"), - SSZ.encodeLongIntList(8, Arrays.asList((long) 3, (long) 4, (long) 5))); - } - - @Test - void shouldWriteVarargsListsOfBigIntegers() { - assertEquals( - fromHexString("03000000030405"), - SSZ.encodeBigIntegerList( - 8, BigInteger.valueOf(3), BigInteger.valueOf(4), BigInteger.valueOf(5))); - } - - @Test - void shouldWriteUtilListsOfBigIntegers() { - assertEquals( - fromHexString("03000000030405"), - SSZ.encodeBigIntegerList( - 8, Arrays.asList(BigInteger.valueOf(3), BigInteger.valueOf(4), BigInteger.valueOf(5)))); - } - - @Test - void shouldWriteVarargsListsOfUnsignedInts() { - assertEquals(fromHexString("03000000FDFEFF"), SSZ.encodeUIntList(8, 253, 254, 255)); - } - - @Test - void shouldWriteUtilListsOfUnsignedInts() { - assertEquals( - fromHexString("03000000FDFEFF"), SSZ.encodeUIntList(8, Arrays.asList(253, 254, 255))); - } - - @Test - void shouldWriteVarargsListsOfUnsignedLongs() { - assertEquals(fromHexString("03000000FDFEFF"), SSZ.encodeULongIntList(8, 253, 254, 255)); - } - - @Test - void shouldWriteUtilListsOfUnsignedLongs() { - assertEquals( - fromHexString("03000000FDFEFF"), - SSZ.encodeULongIntList(8, Arrays.asList((long) 253, (long) 254, (long) 255))); - } - - @Test - void shouldWriteVarargsListsOfUInt256() { - assertEquals( - fromHexString( - "0x60000000030000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000"), - SSZ.encodeUInt256List(UInt256.valueOf(3L), UInt256.valueOf(4L), UInt256.valueOf(5L))); - } - - @Test - void shouldWriteUtilListsOfUInt256() { - assertEquals( - fromHexString( - "0x60000000030000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000"), - SSZ.encodeUInt256List( - Arrays.asList(UInt256.valueOf(3L), UInt256.valueOf(4L), UInt256.valueOf(5L)))); - } - - @Test - void shouldWriteVaragsListsOfStrings() { - assertEquals( - fromHexString("1800000003000000626F62040000006A616E65050000006A616E6574"), - SSZ.encodeStringList("bob", "jane", "janet")); - } - - @Test - void shouldWriteUtilListsOfStrings() { - assertEquals( - fromHexString("1800000003000000626F62040000006A616E65050000006A616E6574"), - SSZ.encodeStringList(Arrays.asList("bob", "jane", "janet"))); - } - - @Test - void shouldWriteVarargsListsOfBytes() { - assertEquals( - fromHexString("1800000003000000626F62040000006A616E65050000006A616E6574"), - SSZ.encodeBytesList( - Bytes.wrap("bob".getBytes(Charsets.UTF_8)), - Bytes.wrap("jane".getBytes(Charsets.UTF_8)), - Bytes.wrap("janet".getBytes(Charsets.UTF_8)))); - } - - @Test - void shouldWriteUtilListOfBytes() { - assertEquals( - fromHexString("1800000003000000626F62040000006A616E65050000006A616E6574"), - SSZ.encodeBytesList( - Arrays.asList( - Bytes.wrap("bob".getBytes(Charsets.UTF_8)), - Bytes.wrap("jane".getBytes(Charsets.UTF_8)), - Bytes.wrap("janet".getBytes(Charsets.UTF_8))))); - } - - @Test - void shouldWriteUtilListOfExtendedBytesType() { - assertEquals( - fromHexString( - "0x6C000000200000000000000000000000000000000000000000000000000000000000000000626F6220000000000000000000000000000000000000000000000000000000000000006A616E65200000000000000000000000000000000000000000000000000000000000006A616E6574"), - SSZ.encodeBytesList( - Arrays.asList( - Bytes.wrap("bob".getBytes(Charsets.UTF_8)).mutableCopy().leftPad(32), - Bytes.wrap("jane".getBytes(Charsets.UTF_8)).mutableCopy().leftPad(32), - Bytes.wrap("janet".getBytes(Charsets.UTF_8)).mutableCopy().leftPad(32)))); - } - - @Test - void shouldWriteVarargsListsOfHashes() { - assertEquals( - fromHexString( - "0x60000000ED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C978B40CA3893681B062BC06760A4863AFAFA6C4D6226D4C6AFAFA3684A06760CB26B30567B2281FF3BD582B0A633B33A376B95BD3333DB59B673A33B336A0B285D"), - SSZ.encodeHashList( - fromHexString("0xED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), - fromHexString("0x8B40CA3893681B062BC06760A4863AFAFA6C4D6226D4C6AFAFA3684A06760CB2"), - fromHexString("0x6B30567B2281FF3BD582B0A633B33A376B95BD3333DB59B673A33B336A0B285D"))); - } - - @Test - void shouldWriteUtilListsOfHashes() { - assertEquals( - fromHexString( - "0x60000000ED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C978B40CA3893681B062BC06760A4863AFAFA6C4D6226D4C6AFAFA3684A06760CB26B30567B2281FF3BD582B0A633B33A376B95BD3333DB59B673A33B336A0B285D"), - SSZ.encodeHashList( - Arrays.asList( - fromHexString("0xED1C978EE1CEEFA5BBD9ED1C8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), - fromHexString("0x8B40CA3893681B062BC06760A4863AFAFA6C4D6226D4C6AFAFA3684A06760CB2"), - fromHexString( - "0x6B30567B2281FF3BD582B0A633B33A376B95BD3333DB59B673A33B336A0B285D")))); - } - - @Test - void shouldWriteVaragsListsOfAddresses() { - assertEquals( - fromHexString( - "0x3C0000008EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C9779C1DE9DBB5AFEEC1EE8BBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5"), - SSZ.encodeAddressList( - fromHexString("0x8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), - fromHexString("0x8EE1CEEFA5BBD9ED1C9779C1DE9DBB5AFEEC1EE8"), - fromHexString("0xBBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5"))); - } - - @Test - void shouldWriteUtilListsOfAddresses() { - assertEquals( - fromHexString( - "0x3C0000008EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C9779C1DE9DBB5AFEEC1EE8BBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5"), - SSZ.encodeAddressList( - Arrays.asList( - fromHexString("0x8EE1CEEFA5BBD9ED1C978EE1CEEFA5BBD9ED1C97"), - fromHexString("0x8EE1CEEFA5BBD9ED1C9779C1DE9DBB5AFEEC1EE8"), - fromHexString("0xBBD9ED1C978EE1CEEFA5BBD9ED1C978EE1CEEFA5")))); - } - - @Test - void shouldWriteVaragsListsOfBooleans() { - assertEquals( - fromHexString("0400000000010100"), SSZ.encodeBooleanList(false, true, true, false)); - } - - @Test - void shouldWriteUtilListsOfBooleans() { - assertEquals( - fromHexString("0400000000010100"), - SSZ.encodeBooleanList(Arrays.asList(false, true, true, false))); - } - - @Test - void shouldWriteVectorOfHomogeneousBytes() { - // Per the pre-SOS SSZ spec, neither the vector nor the bytes should have a mixin. - List elements = - Arrays.asList( - Bytes32.fromHexString("0x01"), - Bytes32.fromHexString("0x02"), - Bytes32.fromHexString("0x03"), - Bytes32.fromHexString("0x04"), - Bytes32.fromHexString("0x05")); - assertEquals( - fromHexString( - "0x0000000000000000000000000000000000000000000000000000000000000001" - + "0000000000000000000000000000000000000000000000000000000000000002" - + "0000000000000000000000000000000000000000000000000000000000000003" - + "0000000000000000000000000000000000000000000000000000000000000004" - + "0000000000000000000000000000000000000000000000000000000000000005"), - SSZ.encode(writer -> writer.writeFixedBytesVector(elements))); - } - - @Test - void shouldWriteVectorOfNonHomogeneousBytes() { - // Per the pre-SOS SSZ spec, the vector itself should not have a mixin, but the individual bytes - // elements should. - List elements = - Arrays.asList( - Bytes32.fromHexString("0x01"), - Bytes32.fromHexString("0x02"), - Bytes32.fromHexString("0x03"), - Bytes32.fromHexString("0x04"), - Bytes32.fromHexString("0x05")); - assertEquals( - fromHexString( - "0x200000000000000000000000000000000000000000000000000000000000000000000001" - + "200000000000000000000000000000000000000000000000000000000000000000000002" - + "200000000000000000000000000000000000000000000000000000000000000000000003" - + "200000000000000000000000000000000000000000000000000000000000000000000004" - + "200000000000000000000000000000000000000000000000000000000000000000000005"), - SSZ.encode(writer -> writer.writeVector(elements))); - } - - @Test - void shouldWriteListOfHomogeneousBytes() { - // Per the pre-SOS SSZ spec, the list iself should have a mixin, but the bytes elements should - // not. - List elements = - Arrays.asList( - Bytes32.fromHexString("0x01"), - Bytes32.fromHexString("0x02"), - Bytes32.fromHexString("0x03"), - Bytes32.fromHexString("0x04"), - Bytes32.fromHexString("0x05")); - assertEquals( - fromHexString( - "0xA0000000" - + "0000000000000000000000000000000000000000000000000000000000000001" - + "0000000000000000000000000000000000000000000000000000000000000002" - + "0000000000000000000000000000000000000000000000000000000000000003" - + "0000000000000000000000000000000000000000000000000000000000000004" - + "0000000000000000000000000000000000000000000000000000000000000005"), - SSZ.encode(writer -> writer.writeFixedBytesList(elements))); - } - - @Test - void shouldWriteListOfNonHomogeneousBytes() { - // Per the pre-SOS SSZ spec, both the vector itself and the individual bytes elements should - // have a length mixin. - List elements = - Arrays.asList( - Bytes32.fromHexString("0x01"), - Bytes32.fromHexString("0x02"), - Bytes32.fromHexString("0x03"), - Bytes32.fromHexString("0x04"), - Bytes32.fromHexString("0x05")); - assertEquals( - fromHexString( - "0xB4000000" - + "200000000000000000000000000000000000000000000000000000000000000000000001" - + "200000000000000000000000000000000000000000000000000000000000000000000002" - + "200000000000000000000000000000000000000000000000000000000000000000000003" - + "200000000000000000000000000000000000000000000000000000000000000000000004" - + "200000000000000000000000000000000000000000000000000000000000000000000005"), - SSZ.encode(writer -> writer.writeBytesList(elements))); - } - - @Test - void shouldWritePreviouslyEncodedValues() { - Bytes output = - SSZ.encode(writer -> writer.writeSSZ(SSZ.encodeByteArray("abc".getBytes(UTF_8)))); - assertEquals("abc", SSZ.decodeString(output)); - } -} diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/HashTreeRootTest.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/HashTreeRootTest.java deleted file mode 100644 index 1726df972..000000000 --- a/ssz/src/test/java/org/apache/tuweni/v2/ssz/HashTreeRootTest.java +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.apache.tuweni.junit.BouncyCastleExtension; -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.bytes.Bytes32; -import org.apache.tuweni.v2.crypto.Hash; - -import org.bouncycastle.util.Arrays; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -@ExtendWith(BouncyCastleExtension.class) -class HashTreeRootTest { - - @Test - void hashBoolean() { - assertEquals( - Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000"), - SSZ.hashTreeRoot(SSZ.encodeBoolean(false))); - assertEquals( - Bytes.fromHexString("0x0100000000000000000000000000000000000000000000000000000000000000"), - SSZ.hashTreeRoot(SSZ.encodeBoolean(true))); - } - - @Test - void hashUint8() { - assertEquals( - Bytes.fromHexString("0x0400000000000000000000000000000000000000000000000000000000000000"), - SSZ.hashTreeRoot(SSZ.encodeUInt(4, 8))); - } - - @Test - void hashBytes32() { - Bytes someBytes = Bytes32.fromRandom(); - assertEquals(someBytes, SSZ.hashTreeRoot(someBytes)); - } - - @Test - void hashBytes34() { - Bytes someBytes = Bytes.random(34); - assertEquals(Hash.keccak256(someBytes), SSZ.hashTreeRoot(someBytes)); - } - - @Test - void list() { - Bytes[] list = - new Bytes[] { - Bytes.wrap(new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}), - Bytes.wrap(new byte[] {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}), - Bytes.wrap(new byte[] {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}), - Bytes.wrap(new byte[] {4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4}), - Bytes.wrap(new byte[] {5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}), - Bytes.wrap(new byte[] {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}), - Bytes.wrap(new byte[] {7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7}), - Bytes.wrap(new byte[] {8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}), - Bytes.wrap(new byte[] {9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9}), - Bytes.wrap(new byte[] {10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10}) - }; - assertEquals( - Bytes.fromHexString("0x839D98509E2EFC53BD1DEA17403921A89856E275BBF4D56C600CC3F6730AAFFA"), - SSZ.hashTreeRoot(list)); - } - - @Test - void list2() { - byte[] _1s = new byte[32]; - byte[] _2s = new byte[32]; - byte[] _3s = new byte[32]; - byte[] _4s = new byte[32]; - byte[] _5s = new byte[32]; - byte[] _6s = new byte[32]; - byte[] _7s = new byte[32]; - byte[] _8s = new byte[32]; - byte[] _9s = new byte[32]; - byte[] _as = new byte[32]; - Arrays.fill(_1s, (byte) 1); - Arrays.fill(_2s, (byte) 2); - Arrays.fill(_3s, (byte) 3); - Arrays.fill(_4s, (byte) 4); - Arrays.fill(_5s, (byte) 5); - Arrays.fill(_6s, (byte) 6); - Arrays.fill(_7s, (byte) 7); - Arrays.fill(_8s, (byte) 8); - Arrays.fill(_9s, (byte) 9); - Arrays.fill(_as, (byte) 10); - - assertEquals( - Bytes.fromHexString("0x55DC6699E7B5713DD9102224C302996F931836C6DAE9A4EC6AB49C966F394685"), - SSZ.hashTreeRoot( - Bytes.wrap(_1s), - Bytes.wrap(_2s), - Bytes.wrap(_3s), - Bytes.wrap(_4s), - Bytes.wrap(_5s), - Bytes.wrap(_6s), - Bytes.wrap(_7s), - Bytes.wrap(_8s), - Bytes.wrap(_9s), - Bytes.wrap(_as))); - } -} diff --git a/ssz/src/test/java/org/apache/tuweni/v2/ssz/TransactionNetworkPayload.java b/ssz/src/test/java/org/apache/tuweni/v2/ssz/TransactionNetworkPayload.java deleted file mode 100644 index a10e97837..000000000 --- a/ssz/src/test/java/org/apache/tuweni/v2/ssz/TransactionNetworkPayload.java +++ /dev/null @@ -1,302 +0,0 @@ -// Copyright The Tuweni Authors -// SPDX-License-Identifier: Apache-2.0 -package org.apache.tuweni.v2.ssz; - -import org.apache.tuweni.v2.bytes.Bytes; -import org.apache.tuweni.v2.units.bigints.UInt256; - -public class TransactionNetworkPayload implements SSZReadable, SSZWritable { - public static final int KZG_COMMITMENT_SIZE = 48; - public static final int FIELD_ELEMENTS_PER_BLOB = 4096; - public static final int ELEMENT_SIZE = 32; - SingedBlobTransaction signedBlobTransaction = new SingedBlobTransaction(); - SSZFixedSizeTypeList kzgCommitments = - new SSZFixedSizeTypeList<>(KZG_COMMITMENT_SIZE, KZGCommitment::new); - SSZFixedSizeTypeList blobs = - new SSZFixedSizeTypeList<>(FIELD_ELEMENTS_PER_BLOB * ELEMENT_SIZE, Blob::new); - - KZGProof kzgProof = new KZGProof(); - - @Override - public boolean isFixed() { - return false; - } - - @Override - public void populateFromReader(SSZReader reader) { - reader.readAsContainer(signedBlobTransaction, kzgCommitments, blobs, kzgProof); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeAsContainer(signedBlobTransaction, kzgCommitments, blobs, kzgProof); - } - - public SingedBlobTransaction getSignedBlobTransaction() { - return signedBlobTransaction; - } - - public SSZFixedSizeTypeList getKzgCommitments() { - return kzgCommitments; - } - - public SSZFixedSizeTypeList getBlobs() { - return blobs; - } - - public KZGProof getKzgProof() { - return kzgProof; - } - - public static class SingedBlobTransaction implements SSZReadable, SSZWritable { - private final BlobTransaction message = new BlobTransaction(); - private final ECDSASignature signature = new ECDSASignature(); - - @Override - public boolean isFixed() { - return false; - } - - @Override - public void populateFromReader(SSZReader reader) { - reader.readAsContainer(message, signature); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeAsContainer(message, signature); - } - - public BlobTransaction getMessage() { - return message; - } - - public static class BlobTransaction implements SSZReadable, SSZWritable { - final Data data = new Data(); - UInt256 chainId; - long nonce; - UInt256 maxPriorityFeePerGas; - UInt256 maxFeePerGas; - long gas; - AddressUnion address = new AddressUnion(); - UInt256 value; - SSZVariableSizeTypeList accessList = - new SSZVariableSizeTypeList<>(AccessTuple::new); - UInt256 maxFeePerData; - - SSZFixedSizeTypeList blobVersionedHashes = - new SSZFixedSizeTypeList<>(32, VersionedHash::new); - - @Override - public boolean isFixed() { - return false; - } - - @Override - public void populateFromReader(SSZReader reader) { - reader.readAsContainer( - r -> chainId = r.readUInt256(), - r -> nonce = r.readUInt64(), - r -> maxPriorityFeePerGas = r.readUInt256(), - r -> maxFeePerGas = r.readUInt256(), - r -> gas = r.readUInt64(), - address, - r -> value = r.readUInt256(), - data, - accessList, - r -> maxFeePerData = r.readUInt256(), - blobVersionedHashes); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeAsContainer( - w -> w.writeUInt256(chainId), - w -> w.writeUInt64(nonce), - w -> w.writeUInt256(maxPriorityFeePerGas), - w -> w.writeUInt256(maxFeePerGas), - w -> w.writeUInt64(gas), - address, - w -> w.writeUInt256(value), - data, - accessList, - w -> w.writeUInt256(maxFeePerData), - blobVersionedHashes); - } - } - - public static class AddressUnion implements SSZReadable, SSZWritable { - private Bytes address; - - @Override - public boolean isFixed() { - return false; - } - - @Override - public void writeTo(SSZWriter writer) { - if (address == null) { - writer.writeUInt8(0); - } else { - writer.writeUInt8(1); - writer.writeAddress(address); - } - } - - @Override - public void populateFromReader(SSZReader reader) { - final int type = reader.readUInt8(); - if (type == 1) { - address = reader.readAddress(); - } - } - } - - public static class Data implements SSZReadable, SSZWritable { - public static final int MAX_CALL_DATA_SIZE = 16777216; // 2**24 - - Bytes data; - - @Override - public boolean isFixed() { - return false; - } - - @Override - public void populateFromReader(SSZReader reader) { - if (reader.isComplete()) { - return; - } - data = reader.consumeRemainingBytes(MAX_CALL_DATA_SIZE); - } - - @Override - public void writeTo(SSZWriter writer) { - if (data != null) { - writer.writeBytes(data); - } - } - } - - public static class AccessTuple implements SSZReadable, SSZWritable { - Bytes address; - SSZFixedSizeTypeList storageKeys = - new SSZFixedSizeTypeList<>(ELEMENT_SIZE, SSZUInt256Wrapper::new); - - @Override - public boolean isFixed() { - return false; - } - - @Override - public void populateFromReader(SSZReader reader) { - reader.readAsContainer(r -> address = r.readAddress(), storageKeys); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeAsContainer(w -> w.writeAddress(address), storageKeys); - } - } - - public static class VersionedHash implements SSZReadable, SSZWritable { - private Bytes bytes; - - @Override - public boolean isFixed() { - return true; - } - - @Override - public void populateFromReader(SSZReader reader) { - reader.readAsContainer(r -> bytes = r.readFixedBytes(32)); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeAsContainer(w -> w.writeFixedBytes(bytes)); - } - } - - public static class ECDSASignature implements SSZReadable, SSZWritable { - boolean parity; - UInt256 r; - UInt256 s; - - @Override - public void populateFromReader(SSZReader reader) { - parity = reader.readBoolean(); - r = reader.readUInt256(); - s = reader.readUInt256(); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeBoolean(parity); - writer.writeUInt256(r); - writer.writeUInt256(s); - } - } - } - - public static class KZGCommitment implements SSZReadable, SSZWritable { - Bytes data; - - @Override - public void populateFromReader(SSZReader reader) { - data = reader.readFixedBytes(KZG_COMMITMENT_SIZE); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeFixedBytes(data); - } - } - - public static class Blob implements SSZReadable, SSZWritable { - SSZFixedSizeVector vector = - new SSZFixedSizeVector<>(FIELD_ELEMENTS_PER_BLOB, ELEMENT_SIZE, SSZUInt256Wrapper::new); - - @Override - public void populateFromReader(SSZReader reader) { - vector.populateFromReader(reader); - } - - @Override - public void writeTo(SSZWriter writer) { - vector.writeTo(writer); - } - } - - public static class SSZUInt256Wrapper implements SSZReadable, SSZWritable { - UInt256 data; - - @Override - public void populateFromReader(SSZReader reader) { - data = reader.readUInt256(); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeUInt256(data); - } - } - - public static class KZGProof implements SSZReadable, SSZWritable { - Bytes bytes; - - @Override - public void populateFromReader(SSZReader reader) { - bytes = reader.readFixedBytes(48); - } - - @Override - public void writeTo(SSZWriter writer) { - writer.writeFixedBytes(bytes); - } - - public Bytes getBytes() { - return bytes; - } - } -} diff --git a/units/.factorypath b/units/.factorypath deleted file mode 100644 index 62faf29fd..000000000 --- a/units/.factorypath +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - 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 deleted file mode 100644 index 8cf1fd97f..000000000 --- a/units/src/main/java/org/apache/tuweni/units/bigints/Utils.java +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 64ead5e58..000000000 --- a/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt256.java +++ /dev/null @@ -1,1051 +0,0 @@ -// 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 deleted file mode 100644 index 0f94334e5..000000000 --- a/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt32.java +++ /dev/null @@ -1,483 +0,0 @@ -// 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 deleted file mode 100644 index 3b02eedd8..000000000 --- a/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt384.java +++ /dev/null @@ -1,877 +0,0 @@ -// 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 deleted file mode 100644 index 1a581ad3e..000000000 --- a/units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt64.java +++ /dev/null @@ -1,541 +0,0 @@ -// 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 deleted file mode 100644 index cbd062374..000000000 --- a/units/src/main/java/org/apache/tuweni/v2/units/bigints/Utils.java +++ /dev/null @@ -1,47 +0,0 @@ -// 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 deleted file mode 100644 index 261b20a94..000000000 --- a/units/src/main/java/org/apache/tuweni/v2/units/bigints/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * 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 deleted file mode 100644 index 61c9fa3b8..000000000 --- a/units/src/main/java/org/apache/tuweni/v2/units/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * 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 deleted file mode 100644 index af8a526df..000000000 --- a/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt256Test.java +++ /dev/null @@ -1,1245 +0,0 @@ -// 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 deleted file mode 100644 index 7b5af69d9..000000000 --- a/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt32Test.java +++ /dev/null @@ -1,832 +0,0 @@ -// 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 deleted file mode 100644 index 918c20360..000000000 --- a/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt384Test.java +++ /dev/null @@ -1,1144 +0,0 @@ -// 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 deleted file mode 100644 index 798659a46..000000000 --- a/units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt64Test.java +++ /dev/null @@ -1,820 +0,0 @@ -// 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()); - } -} From 0e919797f8251d25ec7202f454e5a0dd3130ab40 Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Thu, 27 Nov 2025 16:57:56 +0000 Subject: [PATCH 4/7] Add v2 version for io project to accomodate new bytes API Signed-off-by: Luis Pinto --- .../java/org/apache/tuweni/v2/io/Base32.java | 59 ++++++++++++ .../java/org/apache/tuweni/v2/io/Base58.java | 56 +++++++++++ .../org/apache/tuweni/v2/io/Base58Codec.java | 95 +++++++++++++++++++ .../java/org/apache/tuweni/v2/io/Base64.java | 56 +++++++++++ .../apache/tuweni/v2/io/Base64URLSafe.java | 56 +++++++++++ .../org/apache/tuweni/v2/io/package-info.java | 23 +++++ .../org/apache/tuweni/v2/io/Base32Test.java | 37 ++++++++ .../org/apache/tuweni/v2/io/Base58Test.java | 77 +++++++++++++++ .../org/apache/tuweni/v2/io/Base64Test.java | 43 +++++++++ .../tuweni/v2/io/Base64URLSafeTest.java | 43 +++++++++ 10 files changed, 545 insertions(+) create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base32.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base58.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base58Codec.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base64.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/Base64URLSafe.java create mode 100644 io/src/main/java/org/apache/tuweni/v2/io/package-info.java create mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base32Test.java create mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base58Test.java create mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base64Test.java create mode 100644 io/src/test/java/org/apache/tuweni/v2/io/Base64URLSafeTest.java 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); + } +} From 708c7a52fa69802ba671fbd9727e01f50679e440 Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Thu, 27 Nov 2025 17:10:22 +0000 Subject: [PATCH 5/7] Add v2 version for units project to accomodate new bytes API Signed-off-by: Luis Pinto --- .../apache/tuweni/units/bigints/Utils.java | 47 + .../tuweni/v2/units/bigints/UInt256.java | 1051 ++++++++++++++ .../tuweni/v2/units/bigints/UInt32.java | 483 +++++++ .../tuweni/v2/units/bigints/UInt384.java | 877 ++++++++++++ .../tuweni/v2/units/bigints/UInt64.java | 541 +++++++ .../apache/tuweni/v2/units/bigints/Utils.java | 47 + .../tuweni/v2/units/bigints/package-info.java | 20 + .../apache/tuweni/v2/units/package-info.java | 20 + .../tuweni/v2/units/bigints/UInt256Test.java | 1245 +++++++++++++++++ .../tuweni/v2/units/bigints/UInt32Test.java | 832 +++++++++++ .../tuweni/v2/units/bigints/UInt384Test.java | 1144 +++++++++++++++ .../tuweni/v2/units/bigints/UInt64Test.java | 820 +++++++++++ 12 files changed, 7127 insertions(+) create mode 100644 units/src/main/java/org/apache/tuweni/units/bigints/Utils.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt256.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt32.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt384.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/UInt64.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/Utils.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/bigints/package-info.java create mode 100644 units/src/main/java/org/apache/tuweni/v2/units/package-info.java create mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt256Test.java create mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt32Test.java create mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt384Test.java create mode 100644 units/src/test/java/org/apache/tuweni/v2/units/bigints/UInt64Test.java 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()); + } +} From f0c8db908a778d322276ac39eb9ce8e60353871f Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Thu, 27 Nov 2025 17:23:54 +0000 Subject: [PATCH 6/7] Add v2 version for crypto project to accomodate new bytes API Signed-off-by: Luis Pinto --- .../org/apache/tuweni/v2/crypto/Hash.java | 296 ++ .../apache/tuweni/v2/crypto/SECP256K1.java | 1045 ++++++ .../blake2bf/Blake2bfMessageDigest.java | 233 ++ .../tuweni/v2/crypto/mikuli/AtePairing.java | 22 + .../tuweni/v2/crypto/mikuli/BLS12381.java | 122 + .../tuweni/v2/crypto/mikuli/G1Point.java | 88 + .../tuweni/v2/crypto/mikuli/G2Point.java | 85 + .../tuweni/v2/crypto/mikuli/GTPoint.java | 33 + .../apache/tuweni/v2/crypto/mikuli/Group.java | 11 + .../tuweni/v2/crypto/mikuli/KeyPair.java | 46 + .../tuweni/v2/crypto/mikuli/PublicKey.java | 101 + .../tuweni/v2/crypto/mikuli/Scalar.java | 34 + .../tuweni/v2/crypto/mikuli/SecretKey.java | 63 + .../tuweni/v2/crypto/mikuli/Signature.java | 94 + .../crypto/mikuli/SignatureAndPublicKey.java | 62 + .../tuweni/v2/crypto/mikuli/package-info.java | 23 + .../apache/tuweni/v2/crypto/package-info.java | 20 + .../tuweni/v2/crypto/sodium/AES256GCM.java | 1046 ++++++ .../tuweni/v2/crypto/sodium/Allocated.java | 127 + .../apache/tuweni/v2/crypto/sodium/Auth.java | 228 ++ .../apache/tuweni/v2/crypto/sodium/Box.java | 1225 +++++++ .../tuweni/v2/crypto/sodium/Concatenate.java | 135 + .../DefaultDetachedEncryptionResult.java | 36 + .../sodium/DetachedEncryptionResult.java | 37 + .../tuweni/v2/crypto/sodium/DiffieHelman.java | 463 +++ .../tuweni/v2/crypto/sodium/GenericHash.java | 325 ++ .../tuweni/v2/crypto/sodium/HMACSHA256.java | 196 + .../tuweni/v2/crypto/sodium/HMACSHA512.java | 194 + .../v2/crypto/sodium/HMACSHA512256.java | 195 + .../v2/crypto/sodium/KeyDerivation.java | 305 ++ .../tuweni/v2/crypto/sodium/KeyExchange.java | 697 ++++ .../tuweni/v2/crypto/sodium/PasswordHash.java | 1061 ++++++ .../tuweni/v2/crypto/sodium/SHA256Hash.java | 232 ++ .../tuweni/v2/crypto/sodium/SHA512Hash.java | 232 ++ .../tuweni/v2/crypto/sodium/SecretBox.java | 1841 ++++++++++ .../crypto/sodium/SecretDecryptionStream.java | 36 + .../crypto/sodium/SecretEncryptionStream.java | 87 + .../tuweni/v2/crypto/sodium/Signature.java | 683 ++++ .../tuweni/v2/crypto/sodium/Sodium.java | 3194 +++++++++++++++++ .../v2/crypto/sodium/SodiumVersion.java | 47 + .../v2/crypto/sodium/XChaCha20Poly1305.java | 923 +++++ .../tuweni/v2/crypto/sodium/package-info.java | 30 + .../org/apache/tuweni/v2/crypto/HashTest.java | 166 + .../tuweni/v2/crypto/SECP256K1Test.java | 388 ++ .../blake2bf/Blake2bfMessageDigestTest.java | 124 + .../tuweni/v2/crypto/mikuli/GTPointTest.java | 23 + .../v2/crypto/mikuli/SignatureTest.java | 164 + .../v2/crypto/sodium/AllocatedTest.java | 37 + .../tuweni/v2/crypto/sodium/BoxTest.java | 272 ++ .../v2/crypto/sodium/ConcatenateTest.java | 33 + .../v2/crypto/sodium/DiffieHelmanTest.java | 97 + .../v2/crypto/sodium/GenericHashTest.java | 39 + .../v2/crypto/sodium/HMACSHA256Test.java | 49 + .../v2/crypto/sodium/HMACSHA512256Test.java | 49 + .../v2/crypto/sodium/HMACSHA512Test.java | 49 + .../v2/crypto/sodium/KeyDerivationTest.java | 34 + .../v2/crypto/sodium/PasswordHashTest.java | 119 + .../v2/crypto/sodium/SHA256HashTest.java | 66 + .../sodium/SecretDecryptionStreamTest.java | 29 + .../v2/crypto/sodium/SignatureTest.java | 54 + 60 files changed, 17745 insertions(+) create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/Hash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/SECP256K1.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigest.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/AtePairing.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/BLS12381.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G1Point.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/G2Point.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/GTPoint.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Group.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/KeyPair.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/PublicKey.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Scalar.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SecretKey.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/Signature.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/SignatureAndPublicKey.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/mikuli/package-info.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/package-info.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/AES256GCM.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Allocated.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Auth.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Box.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Concatenate.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DefaultDetachedEncryptionResult.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DetachedEncryptionResult.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelman.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/GenericHash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivation.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/KeyExchange.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/PasswordHash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA256Hash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SHA512Hash.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretBox.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStream.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SecretEncryptionStream.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Signature.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/Sodium.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/SodiumVersion.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/XChaCha20Poly1305.java create mode 100644 crypto/src/main/java/org/apache/tuweni/v2/crypto/sodium/package-info.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/HashTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/SECP256K1Test.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/blake2bf/Blake2bfMessageDigestTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/GTPointTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/mikuli/SignatureTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/AllocatedTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/BoxTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/ConcatenateTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/DiffieHelmanTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/GenericHashTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA256Test.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512256Test.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/HMACSHA512Test.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/KeyDerivationTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/PasswordHashTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SHA256HashTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SecretDecryptionStreamTest.java create mode 100644 crypto/src/test/java/org/apache/tuweni/v2/crypto/sodium/SignatureTest.java 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()); + } +} From 1c10324027c10faac31b57bb2e490a3ef280694a Mon Sep 17 00:00:00 2001 From: Luis Pinto Date: Thu, 27 Nov 2025 17:45:10 +0000 Subject: [PATCH 7/7] Add v2 version for net project to accomodate new bytes API Signed-off-by: Luis Pinto --- .../apache/tuweni/v2/net/package-info.java | 20 ++ .../tls/ClientFingerprintTrustManager.java | 123 ++++++++++ .../tls/FileBackedFingerprintRepository.java | 167 +++++++++++++ .../v2/net/tls/FingerprintRepository.java | 35 +++ .../tls/ServerFingerprintTrustManager.java | 121 ++++++++++ .../org/apache/tuweni/v2/net/tls/TLS.java | 219 ++++++++++++++++++ .../v2/net/tls/TLSEnvironmentException.java | 14 ++ .../tuweni/v2/net/tls/package-info.java | 17 ++ .../FileBackedFingerprintRepositoryTest.java | 98 ++++++++ 9 files changed, 814 insertions(+) create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/package-info.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/ClientFingerprintTrustManager.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepository.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/FingerprintRepository.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/ServerFingerprintTrustManager.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/TLS.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/TLSEnvironmentException.java create mode 100644 net/src/main/java/org/apache/tuweni/v2/net/tls/package-info.java create mode 100644 net/src/test/java/org/apache/tuweni/v2/net/tls/FileBackedFingerprintRepositoryTest.java 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)); + } +}