Skip to content

Commit

Permalink
Implement SecretBox (#7)
Browse files Browse the repository at this point in the history
* Add secret box

* add tests

* improve error messages

* refactor code

* add output variant of box and boxOpen

* suggested renaming

* sort imports

* renaming tweaks
  • Loading branch information
muzzammilshahid authored Feb 28, 2024
1 parent f7e8ff6 commit 6c5b0ca
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 0 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ repositories {
}

dependencies {
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
Expand Down
114 changes: 114 additions & 0 deletions src/main/java/io/xconn/cryptobox/SecretBox.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package io.xconn.cryptobox;

import java.security.MessageDigest;
import java.util.Arrays;

import org.bouncycastle.crypto.engines.XSalsa20Engine;
import org.bouncycastle.crypto.macs.Poly1305;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

public class SecretBox {

static int MAC_SIZE = 16;

public static byte[] box(byte[] message, byte[] privateKey) {
checkLength(privateKey, Util.SECRET_KEY_LEN);

byte[] nonce = Util.generateRandomBytesArray(Util.NONCE_SIZE);
byte[] output = new byte[message.length + MAC_SIZE + Util.NONCE_SIZE];
box(output, nonce, message, privateKey);

return output;
}

public static void box(byte[] output, byte[] message, byte[] privateKey) {
checkLength(privateKey, Util.SECRET_KEY_LEN);

byte[] nonce = Util.generateRandomBytesArray(Util.NONCE_SIZE);
box(output, nonce, message, privateKey);
}

static void box(byte[] output, byte[] nonce, byte[] plaintext, byte[] privateKey) {
checkLength(nonce, Util.NONCE_SIZE);

XSalsa20Engine cipher = new XSalsa20Engine();
Poly1305 mac = new Poly1305();

cipher.init(true, new ParametersWithIV(new KeyParameter(privateKey), nonce));
byte[] subKey = new byte[Util.SECRET_KEY_LEN];
cipher.processBytes(subKey, 0, Util.SECRET_KEY_LEN, subKey, 0);
byte[] cipherWithoutNonce = new byte[plaintext.length + mac.getMacSize()];
cipher.processBytes(plaintext, 0, plaintext.length, cipherWithoutNonce, mac.getMacSize());

// hash the ciphertext
mac.init(new KeyParameter(subKey));
mac.update(cipherWithoutNonce, mac.getMacSize(), plaintext.length);
mac.doFinal(cipherWithoutNonce, 0);

System.arraycopy(nonce, 0, output, 0, nonce.length);
System.arraycopy(cipherWithoutNonce, 0, output, nonce.length, cipherWithoutNonce.length);
}


public static byte[] boxOpen(byte[] ciphertext, byte[] privateKey) {
checkLength(privateKey, Util.SECRET_KEY_LEN);

byte[] nonce = Arrays.copyOfRange(ciphertext, 0, Util.NONCE_SIZE);
byte[] message = Arrays.copyOfRange(ciphertext, Util.NONCE_SIZE,
ciphertext.length);
byte[] plainText = new byte[message.length - MAC_SIZE];
boxOpen(plainText, nonce, message, privateKey);

return plainText;
}

public static void boxOpen(byte[] output, byte[] ciphertext, byte[] privateKey) {
checkLength(privateKey, Util.SECRET_KEY_LEN);

byte[] nonce = Arrays.copyOfRange(ciphertext, 0, Util.NONCE_SIZE);
byte[] message = Arrays.copyOfRange(ciphertext, Util.NONCE_SIZE,
ciphertext.length);

boxOpen(output, nonce, message, privateKey);
}

static void boxOpen(byte[] output, byte[] nonce, byte[] ciphertext, byte[] privateKey) {
checkLength(nonce, Util.NONCE_SIZE);

XSalsa20Engine cipher = new XSalsa20Engine();
Poly1305 mac = new Poly1305();

cipher.init(false, new ParametersWithIV(new KeyParameter(privateKey), nonce));
byte[] sk = new byte[Util.SECRET_KEY_LEN];
cipher.processBytes(sk, 0, sk.length, sk, 0);

// hash ciphertext
mac.init(new KeyParameter(sk));
int len = Math.max(ciphertext.length - mac.getMacSize(), 0);
mac.update(ciphertext, mac.getMacSize(), len);
byte[] calculatedMAC = new byte[mac.getMacSize()];
mac.doFinal(calculatedMAC, 0);

// extract mac
final byte[] presentedMAC = new byte[mac.getMacSize()];
System.arraycopy(
ciphertext, 0, presentedMAC, 0, Math.min(ciphertext.length, mac.getMacSize()));

if (!MessageDigest.isEqual(calculatedMAC, presentedMAC)) {
throw new IllegalArgumentException("Invalid MAC");
}


cipher.processBytes(ciphertext, mac.getMacSize(), output.length, output, 0);
}

static void checkLength(byte[] data, int size) {
if (data == null)
throw new NullPointerException("Input array is null.");
else if (data.length != size) {
throw new IllegalArgumentException("Invalid array length: " + data.length +
". Length should be " + size);
}
}
}
15 changes: 15 additions & 0 deletions src/main/java/io/xconn/cryptobox/Util.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.xconn.cryptobox;

import java.security.SecureRandom;

public class Util {
public static final int NONCE_SIZE = 24;
public static final int SECRET_KEY_LEN = 32;

static byte[] generateRandomBytesArray(int size) {
byte[] randomBytes = new byte[size];
SecureRandom random = new SecureRandom();
random.nextBytes(randomBytes);
return randomBytes;
}
}
85 changes: 85 additions & 0 deletions src/test/java/io/xconn/cryptobox/SecretBoxTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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.assertThrows;

import static io.xconn.cryptobox.SecretBox.MAC_SIZE;
import static io.xconn.cryptobox.SecretBox.box;
import static io.xconn.cryptobox.SecretBox.boxOpen;
import static io.xconn.cryptobox.SecretBox.checkLength;

public class SecretBoxTest {

private static byte[] privateKey;

@BeforeAll
public static void setUp() {
privateKey = Hex.decode("cd281cb85a967c5fc249b31c1c6503a181841526182d4f6e63c81e4213a45fb7");
}

@Test
public void testEncryptAndDecrypt() {
byte[] message = "Hello, World!".getBytes();
byte[] encrypted = box(message, privateKey);
byte[] decrypted = boxOpen(encrypted, privateKey);
assertArrayEquals(message, decrypted);
}

@Test
public void testEncryptAndDecryptOutput() {
byte[] message = "Hello, World!".getBytes();
byte[] encrypted = new byte[Util.NONCE_SIZE + MAC_SIZE + message.length];
box(encrypted, message, privateKey);
byte[] decrypted = new byte[message.length];
boxOpen(decrypted, encrypted, privateKey);
assertArrayEquals(message, decrypted);
}

@Test
public void testEncryptAndDecryptWithNonce() {
byte[] nonce = Util.generateRandomBytesArray(Util.NONCE_SIZE);
byte[] message = "Hello, World!".getBytes();
byte[] encrypted = new byte[message.length + Util.NONCE_SIZE + MAC_SIZE];
box(encrypted, nonce, message, privateKey);
byte[] decrypted = boxOpen(encrypted, privateKey);
assertArrayEquals(message, decrypted);
}

@Test
public void testEncryptAndDecryptWithInvalidMAC() {
byte[] message = "Hello, World!".getBytes();
byte[] encrypted = box(message, privateKey);
encrypted[encrypted.length - 1] ^= 0xFF; // Modify last byte
assertThrows(IllegalArgumentException.class, () -> boxOpen(encrypted, privateKey));
}

@Test
public void testEncryptAndDecryptWithInvalidNonce() {
byte[] message = "Hello, World!".getBytes();
byte[] encrypted = box(message, privateKey);
encrypted[0] ^= 0xFF; // Modify first byte
assertThrows(IllegalArgumentException.class, () -> boxOpen(encrypted, privateKey));
}

@Test
public void testEncryptAndDecryptWithModifiedCiphertext() {
byte[] message = "Hello, World!".getBytes();
byte[] encrypted = box(message, privateKey);
encrypted[Util.NONCE_SIZE + 1] ^= 0xFF; // Modify the byte next to nonce
assertThrows(IllegalArgumentException.class, () -> boxOpen(encrypted, privateKey));
}

@Test
void testCheckLength() {
assertThrows(NullPointerException.class, () -> checkLength(null, 16));

byte[] data = new byte[16];
checkLength(data, 16);

assertThrows(IllegalArgumentException.class, () -> checkLength(data, 32));
}
}
19 changes: 19 additions & 0 deletions src/test/java/io/xconn/cryptobox/UtilTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.xconn.cryptobox;


import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.junit.jupiter.api.Test;

public class UtilTest {

@Test
public void testGenerateRandomBytesArray() {
int size = 32;
byte[] randomBytes = Util.generateRandomBytesArray(size);

assertNotNull(randomBytes);
assertEquals(size, randomBytes.length);
}
}

0 comments on commit 6c5b0ca

Please sign in to comment.