-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
f7e8ff6
commit 6c5b0ca
Showing
5 changed files
with
234 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |