Skip to content

Commit

Permalink
implement sealed box
Browse files Browse the repository at this point in the history
  • Loading branch information
muzzammilshahid committed Feb 28, 2024
1 parent 6c5b0ca commit 4989077
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 4 deletions.
79 changes: 79 additions & 0 deletions src/main/java/io/xconn/cryptobox/HSalsa20.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright © 2017 Coda Hale ([email protected])
*
* 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);
}
}
19 changes: 19 additions & 0 deletions src/main/java/io/xconn/cryptobox/KeyPair.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.xconn.cryptobox;

public class KeyPair<PublicKey, PrivateKey> {
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;
}
}
89 changes: 89 additions & 0 deletions src/main/java/io/xconn/cryptobox/SealedBox.java
Original file line number Diff line number Diff line change
@@ -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<byte[], byte[]> 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);
}
}
4 changes: 2 additions & 2 deletions src/main/java/io/xconn/cryptobox/SecretBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/io/xconn/cryptobox/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,42 @@

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];
SecureRandom random = new SecureRandom();
random.nextBytes(randomBytes);
return randomBytes;
}

public static byte[] getX25519PublicKey(byte[] privateKeyRaw) {
X25519PrivateKeyParameters privateKey = new X25519PrivateKeyParameters(privateKeyRaw, 0);

return privateKey.generatePublicKey().getEncoded();
}

public static KeyPair<byte[], byte[]> 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());
}
}
77 changes: 77 additions & 0 deletions src/test/java/io/xconn/cryptobox/SealedBoxTest.java
Original file line number Diff line number Diff line change
@@ -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));
}

}
2 changes: 1 addition & 1 deletion src/test/java/io/xconn/cryptobox/SecretBoxTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 30 additions & 1 deletion src/test/java/io/xconn/cryptobox/UtilTest.java
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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<byte[], byte[]> 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);
}
}

0 comments on commit 4989077

Please sign in to comment.