diff --git a/autobahn/src/main/java/xbr/network/KeySeries.java b/autobahn/src/main/java/xbr/network/KeySeries.java index d48d0cc1..118088e8 100644 --- a/autobahn/src/main/java/xbr/network/KeySeries.java +++ b/autobahn/src/main/java/xbr/network/KeySeries.java @@ -15,8 +15,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; -import org.libsodium.jni.SodiumConstants; -import org.libsodium.jni.crypto.Random; import org.web3j.utils.Numeric; import java.math.BigInteger; @@ -94,8 +92,7 @@ byte[] getPrice() { } Map encrypt(Object payload) throws JsonProcessingException { - byte[] nonce = new Random().randomBytes( - SodiumConstants.XSALSA20_POLY1305_SECRETBOX_NONCEBYTES); + byte[] nonce = Util.generateRandomBytesArray(Util.NONCE_SIZE); Map data = new HashMap<>(); data.put("id", mID); @@ -112,8 +109,8 @@ byte[] encryptKey(byte[] keyID, byte[] buyerPubKey) { } private void onRotate() { - mID = new Random().randomBytes(16); - mKey = new Random().randomBytes(SodiumConstants.XSALSA20_POLY1305_SECRETBOX_KEYBYTES); + mID = Util.generateRandomBytesArray(16); + mKey = Util.generateRandomBytesArray(Util.SECRET_KEY_LEN); mBox = new SecretBox(mKey); Map data = new HashMap<>(); diff --git a/autobahn/src/main/java/xbr/network/SimpleBuyer.java b/autobahn/src/main/java/xbr/network/SimpleBuyer.java index 4b22695e..3d45bbd8 100644 --- a/autobahn/src/main/java/xbr/network/SimpleBuyer.java +++ b/autobahn/src/main/java/xbr/network/SimpleBuyer.java @@ -15,7 +15,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.cbor.CBORFactory; -import org.libsodium.jni.SodiumConstants; import org.web3j.crypto.Credentials; import org.web3j.crypto.ECKeyPair; import org.web3j.utils.Numeric; @@ -29,6 +28,7 @@ import io.crossbar.autobahn.utils.ABLogger; import io.crossbar.autobahn.utils.IABLogger; +import io.crossbar.autobahn.utils.Pair; import io.crossbar.autobahn.wamp.Session; import io.crossbar.autobahn.wamp.exceptions.ApplicationError; import xbr.network.crypto.SealedBox; @@ -36,8 +36,6 @@ import xbr.network.pojo.Quote; import xbr.network.pojo.Receipt; -import static org.libsodium.jni.NaCl.sodium; - public class SimpleBuyer { private static final IABLogger LOGGER = ABLogger.getLogger(SimpleBuyer.class.getName()); @@ -69,9 +67,9 @@ public SimpleBuyer(String marketMakerAddr, String buyerKey, BigInteger maxPrice) mEthPublicKey = mECKey.getPublicKey().toByteArray(); mEthAddr = Numeric.hexStringToByteArray(Credentials.create(mECKey).getAddress()); - mPrivateKey = new byte[SodiumConstants.SECRETKEY_BYTES]; - mPublicKey = new byte[SodiumConstants.PUBLICKEY_BYTES]; - sodium().crypto_box_keypair(mPublicKey, mPrivateKey); + Pair pubPriKeyPair = Util.generateX25519KeyPair(); + mPublicKey = pubPriKeyPair.first; + mPrivateKey = pubPriKeyPair.second; mMaxPrice = maxPrice; mKeys = new HashMap<>(); diff --git a/autobahn/src/main/java/xbr/network/SimpleSeller.java b/autobahn/src/main/java/xbr/network/SimpleSeller.java index 2bac117a..3e5c9ae8 100644 --- a/autobahn/src/main/java/xbr/network/SimpleSeller.java +++ b/autobahn/src/main/java/xbr/network/SimpleSeller.java @@ -14,7 +14,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import org.libsodium.jni.crypto.Random; import org.web3j.crypto.ECKeyPair; import org.web3j.crypto.Keys; import org.web3j.utils.Numeric; @@ -93,7 +92,7 @@ public byte[] getPublicKey() { private void onRotate(KeySeries series) { mKeysMap.put(Numeric.toHexString(series.getID()), series); long validFrom = Math.round(System.nanoTime() - 10 * Math.pow(10, 9)); - byte[] signature = new Random().randomBytes(65); + byte[] signature = Util.generateRandomBytesArray(65); List args = new ArrayList<>(); args.add(series.getID()); diff --git a/autobahn/src/main/java/xbr/network/Util.java b/autobahn/src/main/java/xbr/network/Util.java index b99f98fb..03757ae0 100644 --- a/autobahn/src/main/java/xbr/network/Util.java +++ b/autobahn/src/main/java/xbr/network/Util.java @@ -11,6 +11,11 @@ package xbr.network; +import org.bouncycastle.crypto.AsymmetricCipherKeyPair; +import org.bouncycastle.crypto.generators.X25519KeyPairGenerator; +import org.bouncycastle.crypto.params.X25519KeyGenerationParameters; +import org.bouncycastle.crypto.params.X25519PrivateKeyParameters; +import org.bouncycastle.crypto.params.X25519PublicKeyParameters; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -22,11 +27,17 @@ import java.io.IOException; import java.math.BigInteger; +import java.security.SecureRandom; import java.util.Arrays; import java.util.concurrent.CompletableFuture; +import io.crossbar.autobahn.utils.Pair; + public class Util { + public static final int NONCE_SIZE = 24; + public static final int SECRET_KEY_LEN = 32; + public static BigInteger toXBR(int xbr) { return BigInteger.valueOf(xbr).multiply(BigInteger.valueOf(10).pow(18)); } @@ -169,4 +180,25 @@ static CompletableFuture recoverEIP712Signer(int chainId, String verifyi return future; } + + public static byte[] generateRandomBytesArray(int size) { + byte[] randomBytes = new byte[size]; + SecureRandom random = new SecureRandom(); + random.nextBytes(randomBytes); + return randomBytes; + } + + public static Pair generateX25519KeyPair() { + SecureRandom random = new SecureRandom(); + X25519KeyGenerationParameters params = new X25519KeyGenerationParameters(random); + X25519KeyPairGenerator generator = new X25519KeyPairGenerator(); + generator.init(params); + + AsymmetricCipherKeyPair keyPair = generator.generateKeyPair(); + + X25519PrivateKeyParameters privateKeyParams = (X25519PrivateKeyParameters) keyPair.getPrivate(); + X25519PublicKeyParameters publicKeyParams = (X25519PublicKeyParameters) keyPair.getPublic(); + + return new Pair<>(publicKeyParams.getEncoded(), privateKeyParams.getEncoded()); + } } diff --git a/autobahn/src/main/java/xbr/network/crypto/SecretBox.java b/autobahn/src/main/java/xbr/network/crypto/SecretBox.java index acdb3045..c2c548b0 100644 --- a/autobahn/src/main/java/xbr/network/crypto/SecretBox.java +++ b/autobahn/src/main/java/xbr/network/crypto/SecretBox.java @@ -1,46 +1,49 @@ package xbr.network.crypto; -import org.libsodium.jni.crypto.Random; -import org.libsodium.jni.crypto.Util; -import org.libsodium.jni.encoders.Encoder; +import static xbr.network.Util.NONCE_SIZE; +import static xbr.network.Util.SECRET_KEY_LEN; +import static xbr.network.Util.generateRandomBytesArray; -import java.util.Arrays; - -import static org.libsodium.jni.NaCl.sodium; -import static org.libsodium.jni.SodiumConstants.BOXZERO_BYTES; -import static org.libsodium.jni.SodiumConstants.XSALSA20_POLY1305_SECRETBOX_KEYBYTES; -import static org.libsodium.jni.SodiumConstants.XSALSA20_POLY1305_SECRETBOX_NONCEBYTES; -import static org.libsodium.jni.SodiumConstants.ZERO_BYTES; -import static org.libsodium.jni.crypto.Util.checkLength; -import static org.libsodium.jni.crypto.Util.isValid; -import static org.libsodium.jni.crypto.Util.removeZeros; +import org.bouncycastle.crypto.engines.XSalsa20Engine; +import org.bouncycastle.crypto.macs.Poly1305; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import java.security.MessageDigest; +import java.util.Arrays; public class SecretBox { - - private byte[] mKey; - private Encoder mEncoder; + private final byte[] mKey; public SecretBox(byte[] key) { - checkLength(key, XSALSA20_POLY1305_SECRETBOX_KEYBYTES); - mEncoder = Encoder.RAW; - mKey = key; + checkLength(key, SECRET_KEY_LEN); + mKey = Arrays.copyOf(key, key.length); } public byte[] encrypt(byte[] message) { - byte[] nonce = new Random().randomBytes(XSALSA20_POLY1305_SECRETBOX_NONCEBYTES); + byte[] nonce = generateRandomBytesArray(NONCE_SIZE); return encrypt(nonce, message); } - public byte[] encrypt(byte[] nonce, byte[] message) { - checkLength(nonce, XSALSA20_POLY1305_SECRETBOX_NONCEBYTES); - byte[] msg = org.libsodium.jni.crypto.Util.prependZeros(ZERO_BYTES, message); - byte[] ct = org.libsodium.jni.crypto.Util.zeros(msg.length); - isValid(sodium().crypto_secretbox_xsalsa20poly1305(ct, msg, msg.length, - nonce, mKey), "Encryption failed"); - byte[] cipherWithoutNonce = removeZeros(BOXZERO_BYTES, ct); + public byte[] encrypt(byte[] nonce, byte[] plaintext) { + checkLength(nonce, NONCE_SIZE); + + XSalsa20Engine xsalsa20 = new XSalsa20Engine(); + Poly1305 poly1305 = new Poly1305(); + + xsalsa20.init(true, new ParametersWithIV(new KeyParameter(mKey), nonce)); + byte[] subKey = new byte[SECRET_KEY_LEN]; + xsalsa20.processBytes(subKey, 0, SECRET_KEY_LEN, subKey, 0); + byte[] cipherWithoutNonce = new byte[plaintext.length + poly1305.getMacSize()]; + xsalsa20.processBytes(plaintext, 0, plaintext.length, cipherWithoutNonce, poly1305.getMacSize()); + + // hash ciphertext and prepend mac to ciphertext + poly1305.init(new KeyParameter(subKey)); + poly1305.update(cipherWithoutNonce, poly1305.getMacSize(), plaintext.length); + poly1305.doFinal(cipherWithoutNonce, 0); + byte[] ciphertext = new byte[cipherWithoutNonce.length + - XSALSA20_POLY1305_SECRETBOX_NONCEBYTES]; + NONCE_SIZE]; System.arraycopy(nonce, 0, ciphertext, 0, nonce.length); System.arraycopy(cipherWithoutNonce, 0, ciphertext, nonce.length, cipherWithoutNonce.length); @@ -48,18 +51,46 @@ public byte[] encrypt(byte[] nonce, byte[] message) { } public byte[] decrypt(byte[] ciphertext) { - byte[] nonce = Arrays.copyOfRange(ciphertext, 0, XSALSA20_POLY1305_SECRETBOX_NONCEBYTES); - byte[] message = Arrays.copyOfRange(ciphertext, XSALSA20_POLY1305_SECRETBOX_NONCEBYTES, + byte[] nonce = Arrays.copyOfRange(ciphertext, 0, NONCE_SIZE); + byte[] message = Arrays.copyOfRange(ciphertext, NONCE_SIZE, ciphertext.length); return decrypt(nonce, message); } - public byte[] decrypt(byte[] nonce, byte[] ciphertext) { - checkLength(nonce, XSALSA20_POLY1305_SECRETBOX_NONCEBYTES); - byte[] ct = org.libsodium.jni.crypto.Util.prependZeros(BOXZERO_BYTES, ciphertext); - byte[] message = Util.zeros(ct.length); - isValid(sodium().crypto_secretbox_xsalsa20poly1305_open(message, ct, - ct.length, nonce, mKey), "Decryption failed. Ciphertext failed verification"); - return removeZeros(ZERO_BYTES, message); + + private byte[] decrypt(byte[] nonce, byte[] ciphertext) { + checkLength(nonce, NONCE_SIZE); + + XSalsa20Engine xsalsa20 = new XSalsa20Engine(); + Poly1305 poly1305 = new Poly1305(); + + xsalsa20.init(false, new ParametersWithIV(new KeyParameter(mKey), nonce)); + byte[] sk = new byte[SECRET_KEY_LEN]; + xsalsa20.processBytes(sk, 0, sk.length, sk, 0); + + // hash ciphertext + poly1305.init(new KeyParameter(sk)); + int len = Math.max(ciphertext.length - poly1305.getMacSize(), 0); + poly1305.update(ciphertext, poly1305.getMacSize(), len); + byte[] calculatedMAC = new byte[poly1305.getMacSize()]; + poly1305.doFinal(calculatedMAC, 0); + + // extract mac + final byte[] presentedMAC = new byte[poly1305.getMacSize()]; + System.arraycopy( + ciphertext, 0, presentedMAC, 0, Math.min(ciphertext.length, poly1305.getMacSize())); + + if (!MessageDigest.isEqual(calculatedMAC, presentedMAC)) { + throw new IllegalArgumentException("Invalid MAC"); + } + + byte[] plaintext = new byte[len]; + xsalsa20.processBytes(ciphertext, poly1305.getMacSize(), plaintext.length, plaintext, 0); + return plaintext; + } + + private void checkLength(byte[] data, int size) { + if (data == null || data.length != size) + throw new IllegalArgumentException("Invalid size: " + data.length); } -} +} \ No newline at end of file diff --git a/autobahn/src/test/java/io/crossbar/autobahn/wamp/auth/CryptosignAuthTests.java b/autobahn/src/test/java/io/crossbar/autobahn/wamp/auth/CryptosignAuthTests.java index bf47c142..e047d397 100644 --- a/autobahn/src/test/java/io/crossbar/autobahn/wamp/auth/CryptosignAuthTests.java +++ b/autobahn/src/test/java/io/crossbar/autobahn/wamp/auth/CryptosignAuthTests.java @@ -109,4 +109,4 @@ public void testGetAuthMethod() { CryptosignAuth auth = new CryptosignAuth(TestAuthID, TestPrivateKey); assertEquals("cryptosign", auth.getAuthMethod()); } -} \ No newline at end of file +} diff --git a/autobahn/src/test/java/xbr/network/UtilTest.java b/autobahn/src/test/java/xbr/network/UtilTest.java new file mode 100644 index 00000000..9ed109be --- /dev/null +++ b/autobahn/src/test/java/xbr/network/UtilTest.java @@ -0,0 +1,32 @@ +package xbr.network; + +import static junit.framework.TestCase.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +import io.crossbar.autobahn.utils.Pair; + +public class UtilTest { + + @Test + public void testGenerateRandomBytesArray() { + int size = 32; + byte[] randomBytes = Util.generateRandomBytesArray(size); + + assertNotNull(randomBytes); + assertEquals(size, randomBytes.length); + } + + @Test + public void testGenerateKeyPair() { + Pair keyPair = Util.generateX25519KeyPair(); + + assertNotNull(keyPair); + assertNotNull(keyPair.first); + assertNotNull(keyPair.second); + + assertEquals(32, keyPair.first.length); + assertEquals(32, keyPair.second.length); + } +} \ No newline at end of file diff --git a/autobahn/src/test/java/xbr/network/crypto/SecretBoxTest.java b/autobahn/src/test/java/xbr/network/crypto/SecretBoxTest.java new file mode 100644 index 00000000..4b562955 --- /dev/null +++ b/autobahn/src/test/java/xbr/network/crypto/SecretBoxTest.java @@ -0,0 +1,71 @@ +package xbr.network.crypto; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; +import static xbr.network.Util.NONCE_SIZE; +import static xbr.network.Util.generateRandomBytesArray; + +import org.junit.Test; + +public class SecretBoxTest { + + @Test + public void testConstructor() { + // test with valid key + new SecretBox(new byte[32]); + + // test with invalid key + assertThrows(IllegalArgumentException.class, () -> new SecretBox(new byte[16])); + + // test with null key + assertThrows(NullPointerException.class, () -> new SecretBox(null)); + } + + @Test + public void testEncryptAndDecrypt() { + SecretBox secretBox = new SecretBox(new byte[32]); + byte[] message = "Hello, World!".getBytes(); + byte[] encrypted = secretBox.encrypt(message); + byte[] decrypted = secretBox.decrypt(encrypted); + assertArrayEquals(message, decrypted); + } + + @Test + public void testEncryptAndDecryptWithNonce() { + SecretBox secretBox = new SecretBox(new byte[32]); + byte[] nonce = generateRandomBytesArray(NONCE_SIZE); + byte[] message = "Hello, World!".getBytes(); + byte[] encrypted = secretBox.encrypt(nonce, message); + byte[] decrypted = secretBox.decrypt(encrypted); + assertArrayEquals(message, decrypted); + } + + @Test + public void testEncryptAndDecryptWithInvalidMAC() { + SecretBox secretBox = new SecretBox(new byte[32]); + byte[] message = "Hello, World!".getBytes(); + byte[] encrypted = secretBox.encrypt(message); + encrypted[encrypted.length - 1] ^= 0xFF; // Modify last byte + assertThrows(IllegalArgumentException.class, () -> secretBox.decrypt(encrypted)); + } + + @Test + public void testEncryptAndDecryptWithInvalidNonce() { + SecretBox secretBox = new SecretBox(new byte[32]); + byte[] message = "Hello, World!".getBytes(); + byte[] encrypted = secretBox.encrypt(message); + encrypted[0] ^= 0xFF; // Modify first byte + assertThrows(IllegalArgumentException.class, () -> secretBox.decrypt(encrypted)); + } + + @Test + public void testEncryptAndDecryptWithModifiedCiphertext() { + byte[] key = new byte[32]; + SecretBox secretBox = new SecretBox(key); + byte[] message = "Hello, World!".getBytes(); + byte[] encrypted = secretBox.encrypt(message); + encrypted[NONCE_SIZE + 1] ^= 0xFF; // Modify the byte next to nonce + assertThrows(IllegalArgumentException.class, () -> secretBox.decrypt(encrypted)); + } + +}