From 498907777fbca0312d87d6cacaef0f6ff4519430 Mon Sep 17 00:00:00 2001 From: Muzzammil Shahid Date: Wed, 28 Feb 2024 23:33:23 +0500 Subject: [PATCH] implement sealed box --- .../java/io/xconn/cryptobox/HSalsa20.java | 79 ++++++++++++++++ src/main/java/io/xconn/cryptobox/KeyPair.java | 19 ++++ .../java/io/xconn/cryptobox/SealedBox.java | 89 +++++++++++++++++++ .../java/io/xconn/cryptobox/SecretBox.java | 4 +- src/main/java/io/xconn/cryptobox/Util.java | 28 ++++++ .../io/xconn/cryptobox/SealedBoxTest.java | 77 ++++++++++++++++ .../io/xconn/cryptobox/SecretBoxTest.java | 2 +- .../java/io/xconn/cryptobox/UtilTest.java | 31 ++++++- 8 files changed, 325 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/xconn/cryptobox/HSalsa20.java create mode 100644 src/main/java/io/xconn/cryptobox/KeyPair.java create mode 100644 src/main/java/io/xconn/cryptobox/SealedBox.java create mode 100644 src/test/java/io/xconn/cryptobox/SealedBoxTest.java diff --git a/src/main/java/io/xconn/cryptobox/HSalsa20.java b/src/main/java/io/xconn/cryptobox/HSalsa20.java new file mode 100644 index 0000000..fb25a1a --- /dev/null +++ b/src/main/java/io/xconn/cryptobox/HSalsa20.java @@ -0,0 +1,79 @@ +/* + * Copyright © 2017 Coda Hale (coda.hale@gmail.com) + * + * 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 io.xconn.cryptobox; + +import java.nio.charset.StandardCharsets; + +import org.bouncycastle.crypto.engines.Salsa20Engine; +import org.bouncycastle.util.Pack; + +/** + * An implementation of the HSalsa20 hash based on the Bouncy Castle Salsa20 core. + */ +class HSalsa20 { + + private static final byte[] SIGMA = "expand 32-byte k".getBytes(StandardCharsets.US_ASCII); + private static final int SIGMA_0 = Pack.littleEndianToInt(SIGMA, 0); + private static final int SIGMA_4 = Pack.littleEndianToInt(SIGMA, 4); + private static final int SIGMA_8 = Pack.littleEndianToInt(SIGMA, 8); + private static final int SIGMA_12 = Pack.littleEndianToInt(SIGMA, 12); + + static void hsalsa20(byte[] out, byte[] in, byte[] k) { + final int[] x = new int[16]; + + final int in0 = Pack.littleEndianToInt(in, 0); + final int in4 = Pack.littleEndianToInt(in, 4); + final int in8 = Pack.littleEndianToInt(in, 8); + final int in12 = Pack.littleEndianToInt(in, 12); + + x[0] = SIGMA_0; + x[1] = Pack.littleEndianToInt(k, 0); + x[2] = Pack.littleEndianToInt(k, 4); + x[3] = Pack.littleEndianToInt(k, 8); + x[4] = Pack.littleEndianToInt(k, 12); + x[5] = SIGMA_4; + x[6] = in0; + x[7] = in4; + x[8] = in8; + x[9] = in12; + x[10] = SIGMA_8; + x[11] = Pack.littleEndianToInt(k, 16); + x[12] = Pack.littleEndianToInt(k, 20); + x[13] = Pack.littleEndianToInt(k, 24); + x[14] = Pack.littleEndianToInt(k, 28); + x[15] = SIGMA_12; + + Salsa20Engine.salsaCore(20, x, x); + + x[0] -= SIGMA_0; + x[5] -= SIGMA_4; + x[10] -= SIGMA_8; + x[15] -= SIGMA_12; + x[6] -= in0; + x[7] -= in4; + x[8] -= in8; + x[9] -= in12; + + Pack.intToLittleEndian(x[0], out, 0); + Pack.intToLittleEndian(x[5], out, 4); + Pack.intToLittleEndian(x[10], out, 8); + Pack.intToLittleEndian(x[15], out, 12); + Pack.intToLittleEndian(x[6], out, 16); + Pack.intToLittleEndian(x[7], out, 20); + Pack.intToLittleEndian(x[8], out, 24); + Pack.intToLittleEndian(x[9], out, 28); + } +} diff --git a/src/main/java/io/xconn/cryptobox/KeyPair.java b/src/main/java/io/xconn/cryptobox/KeyPair.java new file mode 100644 index 0000000..f8cc4e7 --- /dev/null +++ b/src/main/java/io/xconn/cryptobox/KeyPair.java @@ -0,0 +1,19 @@ +package io.xconn.cryptobox; + +public class KeyPair { + private final PublicKey publicKey; + private final PrivateKey privateKey; + + public KeyPair(PublicKey publicKey, PrivateKey privateKey) { + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + public PublicKey getPublicKey() { + return publicKey; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } +} diff --git a/src/main/java/io/xconn/cryptobox/SealedBox.java b/src/main/java/io/xconn/cryptobox/SealedBox.java new file mode 100644 index 0000000..acbc383 --- /dev/null +++ b/src/main/java/io/xconn/cryptobox/SealedBox.java @@ -0,0 +1,89 @@ +package io.xconn.cryptobox; + +import org.bouncycastle.crypto.digests.Blake2bDigest; +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 org.bouncycastle.math.ec.rfc7748.X25519; +import org.bouncycastle.util.Arrays; + +import static io.xconn.cryptobox.SecretBox.boxOpen; +import static io.xconn.cryptobox.Util.MAC_SIZE; +import static io.xconn.cryptobox.Util.PUBLIC_KEY_BYTES; +import static io.xconn.cryptobox.Util.getX25519PublicKey; + +public class SealedBox { + private static final byte[] HSALSA20_SEED = new byte[16]; + + public static byte[] seal(byte[] message, byte[] recipientPublicKey) { + byte[] cipherText = new byte[message.length + PUBLIC_KEY_BYTES + MAC_SIZE]; + seal(cipherText, message, recipientPublicKey); + return cipherText; + } + + public static void seal(byte[] output, byte[] message, byte[] recipientPublicKey) { + KeyPair keyPair = Util.generateX25519KeyPair(); + byte[] nonce = createNonce(keyPair.getPublicKey(), recipientPublicKey); + byte[] sharedSecret = computeSharedSecret(recipientPublicKey, keyPair.getPrivateKey()); + + XSalsa20Engine cipher = new XSalsa20Engine(); + ParametersWithIV params = new ParametersWithIV(new KeyParameter(sharedSecret), nonce); + cipher.init(true, params); + + byte[] sk = new byte[Util.SECRET_KEY_LEN]; + cipher.processBytes(sk, 0, sk.length, sk, 0); + + // encrypt the message + byte[] ciphertext = new byte[message.length]; + cipher.processBytes(message, 0, message.length, ciphertext, 0); + + // create the MAC + Poly1305 mac = new Poly1305(); + byte[] macBuf = new byte[mac.getMacSize()]; + mac.init(new KeyParameter(sk)); + mac.update(ciphertext, 0, ciphertext.length); + mac.doFinal(macBuf, 0); + + System.arraycopy(keyPair.getPublicKey(), 0, output, 0, keyPair.getPublicKey().length); + System.arraycopy(macBuf, 0, output, keyPair.getPublicKey().length, macBuf.length); + System.arraycopy(ciphertext, 0, output, keyPair.getPublicKey().length + macBuf.length, ciphertext.length); + } + + static byte[] createNonce(byte[] ephemeralPublicKey, byte[] recipientPublicKey) { + Blake2bDigest blake2b = new Blake2bDigest(Util.NONCE_SIZE * 8); + byte[] nonce = new byte[blake2b.getDigestSize()]; + + blake2b.update(ephemeralPublicKey, 0, ephemeralPublicKey.length); + blake2b.update(recipientPublicKey, 0, recipientPublicKey.length); + + blake2b.doFinal(nonce, 0); + + return nonce; + } + + static byte[] computeSharedSecret(byte[] publicKey, byte[] privateKey) { + byte[] sharedSecret = new byte[32]; + // compute the raw shared secret + X25519.scalarMult(privateKey, 0, publicKey, 0, sharedSecret, 0); + // encrypt the shared secret + byte[] key = new byte[32]; + HSalsa20.hsalsa20(key, HSALSA20_SEED, sharedSecret); + return key; + } + + public static byte[] sealOpen(byte[] message, byte[] privateKey) { + byte[] plainText = new byte[message.length - PUBLIC_KEY_BYTES - MAC_SIZE]; + sealOpen(plainText, message, privateKey); + return plainText; + } + + public static void sealOpen(byte[] output, byte[] message, byte[] privateKey) { + byte[] ephemeralPublicKey = Arrays.copyOf(message, PUBLIC_KEY_BYTES); + byte[] ciphertext = Arrays.copyOfRange(message, PUBLIC_KEY_BYTES, message.length); + byte[] nonce = createNonce(ephemeralPublicKey, getX25519PublicKey(privateKey)); + byte[] sharedSecret = computeSharedSecret(ephemeralPublicKey, privateKey); + + boxOpen(output, nonce, ciphertext, sharedSecret); + } +} diff --git a/src/main/java/io/xconn/cryptobox/SecretBox.java b/src/main/java/io/xconn/cryptobox/SecretBox.java index 250788e..957998f 100644 --- a/src/main/java/io/xconn/cryptobox/SecretBox.java +++ b/src/main/java/io/xconn/cryptobox/SecretBox.java @@ -8,9 +8,9 @@ import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; -public class SecretBox { +import static io.xconn.cryptobox.Util.MAC_SIZE; - static int MAC_SIZE = 16; +public class SecretBox { public static byte[] box(byte[] message, byte[] privateKey) { checkLength(privateKey, Util.SECRET_KEY_LEN); diff --git a/src/main/java/io/xconn/cryptobox/Util.java b/src/main/java/io/xconn/cryptobox/Util.java index 53aa1aa..8b448cd 100644 --- a/src/main/java/io/xconn/cryptobox/Util.java +++ b/src/main/java/io/xconn/cryptobox/Util.java @@ -2,9 +2,17 @@ import java.security.SecureRandom; +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; + public class Util { public static final int NONCE_SIZE = 24; public static final int SECRET_KEY_LEN = 32; + public static final int PUBLIC_KEY_BYTES = 32; + public static int MAC_SIZE = 16; static byte[] generateRandomBytesArray(int size) { byte[] randomBytes = new byte[size]; @@ -12,4 +20,24 @@ static byte[] generateRandomBytesArray(int size) { random.nextBytes(randomBytes); return randomBytes; } + + public static byte[] getX25519PublicKey(byte[] privateKeyRaw) { + X25519PrivateKeyParameters privateKey = new X25519PrivateKeyParameters(privateKeyRaw, 0); + + return privateKey.generatePublicKey().getEncoded(); + } + + public static KeyPair 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 KeyPair<>(publicKeyParams.getEncoded(), privateKeyParams.getEncoded()); + } } diff --git a/src/test/java/io/xconn/cryptobox/SealedBoxTest.java b/src/test/java/io/xconn/cryptobox/SealedBoxTest.java new file mode 100644 index 0000000..899654e --- /dev/null +++ b/src/test/java/io/xconn/cryptobox/SealedBoxTest.java @@ -0,0 +1,77 @@ +package io.xconn.cryptobox; + +import org.bouncycastle.util.encoders.Hex; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import static io.xconn.cryptobox.SealedBox.computeSharedSecret; +import static io.xconn.cryptobox.SealedBox.createNonce; +import static io.xconn.cryptobox.SealedBox.sealOpen; +import static io.xconn.cryptobox.SealedBox.seal; +import static io.xconn.cryptobox.Util.MAC_SIZE; +import static io.xconn.cryptobox.Util.PUBLIC_KEY_BYTES; + +public class SealedBoxTest { + + private static byte[] publicKey; + private static byte[] privateKey; + + @BeforeAll + public static void setUp() { + publicKey = Hex.decode("e146721761cf7378cb2e007adc1a51b70fa40abfb87652c645d8e86be19c2b1e"); + privateKey = Hex.decode("3817e2630237d569188a02a06354d9e9f61ee9cdd0cc8b5388c56013b7b5654a"); + } + + @Test + public void testEncryptAndDecrypt() { + String message = "Hello, world!"; + byte[] encrypted = SealedBox.seal(message.getBytes(), publicKey); + byte[] decrypted = SealedBox.sealOpen(encrypted, privateKey); + + assertArrayEquals(message.getBytes(), decrypted); + } + + @Test + public void testEncryptAndDecryptOutput() { + String message = "Hello, world!"; + + byte[] encrypted = new byte[message.getBytes().length + PUBLIC_KEY_BYTES + MAC_SIZE]; + seal(encrypted, message.getBytes(), publicKey); + byte[] decrypted = new byte[message.length()]; + sealOpen(decrypted, encrypted, privateKey); + + assertArrayEquals(message.getBytes(), decrypted); + } + + @Test + void testCreateNonce() { + byte[] nonce = createNonce(new byte[32], new byte[32]); + assertNotNull(nonce); + assertEquals(Util.NONCE_SIZE, nonce.length); + } + + @Test + public void testComputeSharedSecret() { + byte[] sharedSecret = computeSharedSecret(publicKey, privateKey); + + byte[] expectedSharedSecret = Hex.decode("544b3aea8fcebe9e986a1628e517927526407c100d09e17c5dc7dd81149325e1"); + assertArrayEquals(expectedSharedSecret, sharedSecret); + } + + + @Test + public void testInvalidDecrypt() { + byte[] encrypted = SealedBox.seal("Hello, world!".getBytes(), publicKey); + + // Using a different private key for decryption + byte[] wrongPrivateKey = Hex.decode("0000000000000000000000000000000000000000000000000000000000000000"); + + assertThrows(IllegalArgumentException.class, () -> SealedBox.sealOpen(encrypted, wrongPrivateKey)); + } + +} diff --git a/src/test/java/io/xconn/cryptobox/SecretBoxTest.java b/src/test/java/io/xconn/cryptobox/SecretBoxTest.java index 8168298..d92a04b 100644 --- a/src/test/java/io/xconn/cryptobox/SecretBoxTest.java +++ b/src/test/java/io/xconn/cryptobox/SecretBoxTest.java @@ -7,7 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static io.xconn.cryptobox.SecretBox.MAC_SIZE; +import static io.xconn.cryptobox.Util.MAC_SIZE; import static io.xconn.cryptobox.SecretBox.box; import static io.xconn.cryptobox.SecretBox.boxOpen; import static io.xconn.cryptobox.SecretBox.checkLength; diff --git a/src/test/java/io/xconn/cryptobox/UtilTest.java b/src/test/java/io/xconn/cryptobox/UtilTest.java index 3b246e4..6c8df26 100644 --- a/src/test/java/io/xconn/cryptobox/UtilTest.java +++ b/src/test/java/io/xconn/cryptobox/UtilTest.java @@ -1,10 +1,16 @@ package io.xconn.cryptobox; +import java.security.SecureRandom; + +import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import org.junit.jupiter.api.Test; +import static io.xconn.cryptobox.Util.PUBLIC_KEY_BYTES; +import static io.xconn.cryptobox.Util.generateX25519KeyPair; +import static io.xconn.cryptobox.Util.getX25519PublicKey; + public class UtilTest { @@ -16,4 +22,27 @@ public void testGenerateRandomBytesArray() { assertNotNull(randomBytes); assertEquals(size, randomBytes.length); } + + @Test + void testGetX25519PublicKey() { + SecureRandom random = new SecureRandom(); + byte[] privateKeyRaw = new byte[32]; + random.nextBytes(privateKeyRaw); + + byte[] publicKey = getX25519PublicKey(privateKeyRaw); + + assertNotNull(publicKey); + assertEquals(PUBLIC_KEY_BYTES, publicKey.length); + } + + @Test + void testGenerateX25519KeyPair() { + KeyPair keyPair = generateX25519KeyPair(); + + assertNotNull(keyPair); + assertNotNull(keyPair.getPublicKey()); + assertNotNull(keyPair.getPrivateKey()); + assertEquals(Util.PUBLIC_KEY_BYTES, keyPair.getPublicKey().length); + assertEquals(Util.SECRET_KEY_LEN, keyPair.getPrivateKey().length); + } }