Compare commits

...

9 Commits

Author SHA1 Message Date
Chris Eager
49af72520c CI: update Java matrix to LTS releases
Some checks failed
Build/test / JDK ${{ matrix.java }} (11, ubuntu-22.04) (push) Has been cancelled
Build/test / JDK ${{ matrix.java }} (17, ubuntu-22.04) (push) Has been cancelled
Build/test / JDK ${{ matrix.java }} (21, ubuntu-22.04) (push) Has been cancelled
Build/test / JDK ${{ matrix.java }} (8, ubuntu-22.04) (push) Has been cancelled
2024-02-29 16:43:45 -06:00
Chris Eager
36c1079322 CI: update matrix.os to ubuntu-22.04 2024-02-29 16:43:45 -06:00
Chris Eager
3f0fba316d CI: update actions/checkout and actions/setup-java to the latest versions 2024-02-29 16:43:45 -06:00
Jon Chambers
a529bd0143 [maven-release-plugin] prepare for next development iteration 2024-02-26 17:20:03 -05:00
Jon Chambers
9ba25253c9 [maven-release-plugin] prepare release noise-java-0.1.1
Some checks failed
Build/test / JDK ${{ matrix.java }} (11, ubuntu-20.04) (push) Has been cancelled
Build/test / JDK ${{ matrix.java }} (13, ubuntu-20.04) (push) Has been cancelled
Build/test / JDK ${{ matrix.java }} (15, ubuntu-20.04) (push) Has been cancelled
Build/test / JDK ${{ matrix.java }} (8, ubuntu-20.04) (push) Has been cancelled
2024-02-26 17:20:01 -05:00
Jon Chambers
ed3f6064a2 Parameterize vector-based tests 2024-02-26 13:38:23 -05:00
Jon Chambers
87b27851aa Update to JUnit5 2024-02-26 13:38:23 -05:00
Jon Chambers
c111aa7e9d Don't increment nonces within a CipherState unless the encryption/decryption operation succeeds 2024-02-26 13:22:24 -05:00
Jon Chambers
f43de4a734 [maven-release-plugin] prepare for next development iteration 2022-08-03 12:44:10 -04:00
15 changed files with 156 additions and 334 deletions

View File

@ -7,23 +7,18 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04]
java: [8, 11, 13, 15]
os: [ubuntu-22.04]
java: [8, 11, 17, 21]
fail-fast: false
name: JDK ${{ matrix.java }}
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Set up JDK
uses: actions/setup-java@v1
uses: actions/setup-java@9704b39bf258b59bc04b50fa2dd55e9ed76b47a8 # v4.1.0
with:
java-version: ${{ matrix.java }}
- name: Cache local Maven repository
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
distribution: 'temurin'
cache: maven
- name: Test with Maven
run: mvn verify -B --file pom.xml

10
pom.xml
View File

@ -5,7 +5,7 @@
<groupId>org.signal.forks</groupId>
<artifactId>noise-java</artifactId>
<version>0.1.0</version>
<version>0.1.2-SNAPSHOT</version>
<name>Noise-Java</name>
<description>Plain Java implementation of the Noise protocol</description>
@ -16,7 +16,7 @@
<connection>scm:git:https://github.com/signalapp/noise-java.git</connection>
<developerConnection>scm:git:git@github.com:signalapp/noise-java.git</developerConnection>
<url>https://github.com/signalapp/noise-java</url>
<tag>noise-java-0.1.0</tag>
<tag>HEAD</tag>
</scm>
<developers>
@ -45,9 +45,9 @@
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@ -130,8 +130,7 @@ class AESGCMFallbackCipherState implements CipherState {
iv[13] = 0;
iv[14] = 0;
iv[15] = 1;
++n;
// Encrypt a block of zeroes to generate the hash key to XOR
// the GHASH tag with at the end of the encrypt/decrypt operation.
Arrays.fill(hashKey, (byte)0);
@ -207,6 +206,7 @@ class AESGCMFallbackCipherState implements CipherState {
ghash.finish(ciphertext, ciphertextOffset + length, 16);
for (int index = 0; index < 16; ++index)
ciphertext[ciphertextOffset + length + index] ^= hashKey[index];
n += 1;
return length + 16;
}
@ -247,6 +247,7 @@ class AESGCMFallbackCipherState implements CipherState {
if ((temp & 0xFF) != 0)
Noise.throwBadTagException();
encryptCTR(ciphertext, ciphertextOffset, plaintext, plaintextOffset, dataLen);
n += 1;
return dataLen;
}

View File

@ -190,8 +190,7 @@ class AESGCMOnCtrCipherState implements CipherState {
iv[13] = 0;
iv[14] = 0;
iv[15] = 1;
++n;
// Initialize the CTR mode cipher with the key and IV.
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv));
@ -255,6 +254,7 @@ class AESGCMOnCtrCipherState implements CipherState {
ghash.finish(ciphertext, ciphertextOffset + length, 16);
for (int index = 0; index < 16; ++index)
ciphertext[ciphertextOffset + length + index] ^= hashKey[index];
n += 1;
return length + 16;
}
@ -312,6 +312,7 @@ class AESGCMOnCtrCipherState implements CipherState {
// Shouldn't happen.
throw new IllegalStateException(e);
}
n += 1;
return dataLen;
}

View File

@ -138,7 +138,7 @@ class ChaChaPolyCipherState implements CipherState {
{
if (n == -1L)
throw new IllegalStateException("Nonce has wrapped around");
ChaChaCore.initIV(input, n++);
ChaChaCore.initIV(input, n);
ChaChaCore.hash(output, input);
Arrays.fill(polyKey, (byte)0);
xorBlock(polyKey, 0, polyKey, 0, 32, output);
@ -234,6 +234,7 @@ class ChaChaPolyCipherState implements CipherState {
poly.update(ciphertext, ciphertextOffset, length);
finish(ad, length);
System.arraycopy(polyKey, 0, ciphertext, ciphertextOffset + length, 16);
n += 1;
return length + 16;
}
@ -273,6 +274,7 @@ class ChaChaPolyCipherState implements CipherState {
if ((temp & 0xFF) != 0)
Noise.throwBadTagException();
encrypt(ciphertext, ciphertextOffset, plaintext, plaintextOffset, dataLen);
n += 1;
return dataLen;
}

View File

@ -30,7 +30,7 @@ import java.io.Reader;
*
* Intentionally compatible with android.util.JsonReader.
*/
public class JsonReader {
public class JsonReader implements AutoCloseable {
private Reader in;
private JsonToken token;
@ -54,6 +54,7 @@ public class JsonReader {
expectNext(JsonToken.BEGIN_OBJECT, "JSON begin object expected");
}
@Override
public void close() throws IOException {
in.close();
}

View File

@ -22,18 +22,17 @@
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.ShortBufferException;
import org.junit.Test;
import com.southernstorm.noise.protocol.CipherState;
import com.southernstorm.noise.protocol.Noise;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Perform tests on the cipher algorithms used by Noise.
@ -43,51 +42,35 @@ public class CipherStateTests {
private void testCipher(String name, int keyLen, int macLen,
String key, long nonce, String ad,
String plaintext, String ciphertext,
String mac, boolean forceFallbacks)
{
String mac, boolean forceFallbacks) throws NoSuchAlgorithmException, ShortBufferException, BadPaddingException {
byte[] keyBytes = TestUtils.stringToData(key);
byte[] adBytes = TestUtils.stringToData(ad);
byte[] plaintextBytes = TestUtils.stringToData(plaintext);
byte[] ciphertextBytes;
byte[] buffer;
if (ciphertext.length() > 0)
ciphertextBytes = TestUtils.stringToData(ciphertext + mac.substring(2));
else
ciphertextBytes = TestUtils.stringToData(mac);
final byte[] plaintextBuffer = new byte[plaintextBytes.length];
final byte[] ciphertextBuffer = new byte[ciphertextBytes.length];
// Create the cipher object and check its properties.
CipherState cipher = null;
Noise.setForceFallbacks(forceFallbacks);
try {
cipher = Noise.createCipher(name);
} catch (NoSuchAlgorithmException e) {
fail(name + " cipher is not supported");
}
final CipherState cipher = Noise.createCipher(name);
assertEquals(name, cipher.getCipherName());
assertEquals(keyLen, cipher.getKeyLength());
assertEquals(0, cipher.getMACLength()); // Key has not been set yet.
// Try to encrypt. Because the key is not set yet, this will
// return the plaintext as-is.
try {
buffer = new byte [plaintextBytes.length];
Arrays.fill(buffer, (byte)0xAA);
assertEquals(plaintextBytes.length, cipher.encryptWithAd(adBytes, plaintextBytes, 0, buffer, 0, plaintextBytes.length));
assertArrayEquals(plaintextBytes, buffer);
} catch (ShortBufferException e) {
fail("Buffer should have been big enough");
}
Arrays.fill(plaintextBuffer, (byte)0xAA);
assertEquals(plaintextBytes.length, cipher.encryptWithAd(adBytes, plaintextBytes, 0, plaintextBuffer, 0, plaintextBytes.length));
assertArrayEquals(plaintextBytes, plaintextBuffer);
// Try to decrypt. Will return the ciphertext and MAC as-is.
buffer = new byte [ciphertextBytes.length];
Arrays.fill(buffer, (byte)0xAA);
try {
assertEquals(ciphertextBytes.length, cipher.decryptWithAd(adBytes, ciphertextBytes, 0, buffer, 0, ciphertextBytes.length));
} catch (BadPaddingException e) {
fail();
} catch (ShortBufferException e) {
fail();
}
Arrays.fill(ciphertextBuffer, (byte)0xAA);
assertEquals(ciphertextBytes.length, cipher.decryptWithAd(adBytes, ciphertextBytes, 0, ciphertextBuffer, 0, ciphertextBytes.length));
// Set the key and fast-forward the nonce.
cipher.initializeKey(keyBytes, 0);
@ -95,39 +78,24 @@ public class CipherStateTests {
assertEquals(macLen, cipher.getMACLength());
// Encrypt the data.
try {
buffer = new byte [ciphertextBytes.length];
Arrays.fill(buffer, (byte)0xAA);
assertEquals(ciphertextBytes.length, cipher.encryptWithAd(adBytes, plaintextBytes, 0, buffer, 0, plaintextBytes.length));
assertArrayEquals(ciphertextBytes, buffer);
} catch (ShortBufferException e) {
fail("Buffer should have been big enough");
}
Arrays.fill(ciphertextBuffer, (byte)0xAA);
assertEquals(ciphertextBytes.length, cipher.encryptWithAd(adBytes, plaintextBytes, 0, ciphertextBuffer, 0, plaintextBytes.length));
assertArrayEquals(ciphertextBytes, ciphertextBuffer);
// Try to decrypt. The MAC check should fail because the internal
// nonce was incremented and no longer matches the parameter.
try {
cipher.decryptWithAd(adBytes, ciphertextBytes, 0, buffer, 0, ciphertextBytes.length);
fail();
} catch (BadPaddingException e) {
// Success!
} catch (ShortBufferException e) {
fail();
}
assertThrows(BadPaddingException.class, () ->
cipher.decryptWithAd(adBytes, ciphertextBytes, 0, ciphertextBuffer, 0, ciphertextBytes.length));
// Fast-forward the nonce to just before the rollover. We will be able
// to encrypt one more block, and then the next request will be rejected.
cipher.setNonce(-2L);
try {
buffer = new byte [ciphertextBytes.length];
Arrays.fill(buffer, (byte)0xAA);
cipher.encryptWithAd(adBytes, plaintextBytes, 0, buffer, 0, plaintextBytes.length);
try {
cipher.encryptWithAd(adBytes, plaintextBytes, 0, buffer, 0, plaintextBytes.length);
fail();
} catch (IllegalStateException e) {
// Success!
}
Arrays.fill(ciphertextBuffer, (byte)0xAA);
cipher.encryptWithAd(adBytes, plaintextBytes, 0, ciphertextBuffer, 0, plaintextBytes.length);
assertThrows(IllegalStateException.class, () ->
cipher.encryptWithAd(adBytes, plaintextBytes, 0, ciphertextBuffer, 0, plaintextBytes.length));
} catch (ShortBufferException e) {
fail("Buffer should have been big enough");
}
@ -138,53 +106,34 @@ public class CipherStateTests {
assertEquals(macLen, cipher.getMACLength());
// Decrypt the test ciphertext and MAC.
try {
buffer = new byte [plaintextBytes.length];
Arrays.fill(buffer, (byte)0xAA);
assertEquals(plaintextBytes.length, cipher.decryptWithAd(adBytes, ciphertextBytes, 0, buffer, 0, ciphertextBytes.length));
assertArrayEquals(plaintextBytes, buffer);
} catch (BadPaddingException e) {
fail();
} catch (ShortBufferException e) {
fail();
}
Arrays.fill(plaintextBuffer, (byte)0xAA);
assertEquals(plaintextBytes.length, cipher.decryptWithAd(adBytes, ciphertextBytes, 0, plaintextBuffer, 0, ciphertextBytes.length));
assertArrayEquals(plaintextBytes, plaintextBuffer);
// Fast-forward the nonce to just before the rollover. We will be able
// to decrypt one more block, and then the next request will be rejected.
// Fast-forward the nonce to just before the rollover. We will be able
// to decrypt one more block, and then the next request will be rejected.
cipher.setNonce(-2L);
try {
buffer = new byte [plaintextBytes.length];
Arrays.fill(buffer, (byte)0xAA);
try {
cipher.decryptWithAd(adBytes, ciphertextBytes, 0, buffer, 0, ciphertextBytes.length);
fail();
} catch (BadPaddingException e) {
// Success!
}
try {
cipher.decryptWithAd(adBytes, ciphertextBytes, 0, buffer, 0, ciphertextBytes.length);
fail();
} catch (IllegalStateException e) {
// Success!
} catch (BadPaddingException e) {
fail();
}
} catch (ShortBufferException e) {
fail("Buffer should have been big enough");
}
Arrays.fill(plaintextBuffer, (byte)0xAA);
assertThrows(BadPaddingException.class, () ->
cipher.decryptWithAd(adBytes, ciphertextBytes, 0, plaintextBuffer, 0, ciphertextBytes.length));
cipher.setNonce(-1L);
assertThrows(IllegalStateException.class, () ->
cipher.decryptWithAd(adBytes, ciphertextBytes, 0, plaintextBuffer, 0, ciphertextBytes.length));
}
private void testCipher(String name, int keyLen, int macLen,
String key, long nonce, String ad,
String plaintext, String ciphertext,
String mac)
{
String mac) throws ShortBufferException, NoSuchAlgorithmException, BadPaddingException {
testCipher(name, keyLen, macLen, key, nonce, ad, plaintext, ciphertext, mac, true);
testCipher(name, keyLen, macLen, key, nonce, ad, plaintext, ciphertext, mac, false);
}
@Test
public void AESGCM() {
public void AESGCM() throws ShortBufferException, NoSuchAlgorithmException, BadPaddingException {
/* Test vectors for AES in GCM mode from Appendix B of:
http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf
We can only use a few of the vectors because most of the IV's in the
@ -212,7 +161,7 @@ public class CipherStateTests {
}
@Test
public void ChaChaPoly() {
public void ChaChaPoly() throws ShortBufferException, NoSuchAlgorithmException, BadPaddingException {
// ChaChaPoly test vectors from Appendix A.5 of RFC 7539.
testCipher
("ChaChaPoly", 32, 16,

View File

@ -22,13 +22,12 @@
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.junit.Test;
import com.southernstorm.noise.crypto.Curve25519;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
public class Curve25519Tests {

View File

@ -22,13 +22,12 @@
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.junit.Test;
import com.southernstorm.noise.crypto.Curve448;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
public class Curve448Tests {

View File

@ -22,13 +22,12 @@
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.junit.Test;
import com.southernstorm.noise.crypto.GHASH;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
public class GHASHTests {

View File

@ -22,23 +22,21 @@
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import java.security.DigestException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.junit.Test;
import com.southernstorm.noise.protocol.Noise;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
/**
* Perform tests on the hash algorithms used by Noise.
*/
public class HashTests {
private void testHash(MessageDigest digest, String input, String hash)
{
private void testHash(MessageDigest digest, String input, String hash) throws DigestException {
byte[] inputBytes = TestUtils.stringToData(input);
byte[] hashBytes = TestUtils.stringToData(hash);
byte[] result = new byte [digest.getDigestLength()];
@ -46,50 +44,36 @@ public class HashTests {
// Hash the entire input in one request.
digest.reset();
digest.update(inputBytes);
try {
digest.digest(result, 0, result.length);
} catch (DigestException e) {
fail("digest failed");
}
digest.digest(result, 0, result.length);
assertArrayEquals(hashBytes, result);
// Hash the input in pieces to test split requests.
digest.reset();
digest.update(inputBytes, 0, inputBytes.length / 2);
digest.update(inputBytes, inputBytes.length / 2, inputBytes.length - (inputBytes.length / 2));
try {
digest.digest(result, 0, result.length);
} catch (DigestException e) {
fail("digest failed");
}
digest.digest(result, 0, result.length);
assertArrayEquals(hashBytes, result);
}
private void testHash(String name, String input, String hash)
{
private void testHash(String name, String input, String hash) throws NoSuchAlgorithmException, DigestException {
MessageDigest digest = null;
Noise.setForceFallbacks(true);
try {
digest = Noise.createHash(name);
} catch (NoSuchAlgorithmException e) {
fail("No crypto provider for " + name);
} finally {
Noise.setForceFallbacks(false);
}
testHash(digest, input, hash);
Noise.setForceFallbacks(false);
try {
digest = Noise.createHash(name);
} catch (NoSuchAlgorithmException e) {
fail("No crypto provider for " + name);
}
digest = Noise.createHash(name);
testHash(digest, input, hash);
}
@Test
public void blake2b() {
public void blake2b() throws DigestException, NoSuchAlgorithmException {
testHash("BLAKE2b", "", "0x786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce");
testHash("BLAKE2b", "abc", "0xba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923");
testHash("BLAKE2b", "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "0x7285ff3e8bd768d69be62b3bf18765a325917fa9744ac2f582a20850bc2b1141ed1b3e4528595acc90772bdf2d37dc8a47130b44f33a02e8730e5ad8e166e888");
@ -97,7 +81,7 @@ public class HashTests {
}
@Test
public void blake2s() {
public void blake2s() throws DigestException, NoSuchAlgorithmException {
testHash("BLAKE2s", "", "0x69217a3079908094e11121d042354a7c1f55b6482ca1a51e1b250dfd1ed0eef9");
testHash("BLAKE2s", "abc", "0x508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982");
testHash("BLAKE2s", "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "0x6f4df5116a6f332edab1d9e10ee87df6557beab6259d7663f3bcd5722c13f189");
@ -105,14 +89,14 @@ public class HashTests {
}
@Test
public void sha256() {
public void sha256() throws DigestException, NoSuchAlgorithmException {
testHash("SHA256", "", "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
testHash("SHA256", "abc", "0xba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
testHash("SHA256", "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "0x248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
}
@Test
public void sha512() {
public void sha512() throws DigestException, NoSuchAlgorithmException {
testHash("SHA512", "", "0xcf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e");
testHash("SHA512", "abc", "0xddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f");
testHash("SHA512", "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", "0x8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909");

View File

@ -22,13 +22,12 @@
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.junit.Test;
import com.southernstorm.noise.crypto.Poly1305;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
/**
* Perform tests on the Poly1305 implementation in isolation from ChaChaPoly.

View File

@ -22,13 +22,12 @@
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import java.util.Arrays;
import org.junit.Test;
import com.southernstorm.noise.crypto.RijndaelAES;
import org.junit.jupiter.api.Test;
/**
* AES test cases to verify the fallback RijndaelAES implementation.

View File

@ -1,17 +0,0 @@
package com.southernstorm.noise.tests;
import java.io.InputStream;
import org.junit.Assert;
import org.junit.Test;
public class UnitVectorTests {
@Test
public void testBasicVector() throws Exception {
try (final InputStream stream = getClass().getResourceAsStream("test-vectors.json")) {
VectorTests vectorTests = new VectorTests();
vectorTests.processInputStream(stream);
Assert.assertEquals(vectorTests.getFailed(), 0);
}
}
}

View File

@ -22,16 +22,14 @@
package com.southernstorm.noise.tests;
import static org.junit.Assert.*;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import javax.crypto.BadPaddingException;
import javax.crypto.ShortBufferException;
@ -41,27 +39,22 @@ import com.southernstorm.json.JsonReader;
import com.southernstorm.noise.protocol.CipherState;
import com.southernstorm.noise.protocol.CipherStatePair;
import com.southernstorm.noise.protocol.HandshakeState;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.*;
/**
* Executes Noise vector tests in JSON format.
*/
public class VectorTests {
private int total;
private int failed;
private int skipped;
public VectorTests()
{
total = 0;
failed = 0;
skipped = 0;
}
/**
* Information about a handshake or transport message.
*/
private class TestMessage
private static class TestMessage
{
public byte[] payload;
public byte[] ciphertext;
@ -70,7 +63,7 @@ public class VectorTests {
/**
* Information about a Noise test vector that was parsed from a JSON stream.
*/
private class TestVector
private static class TestVector
{
public String name;
public String pattern;
@ -97,7 +90,7 @@ public class VectorTests {
public boolean failure_expected;
public boolean fallback_expected;
public TestMessage[] messages;
public void addMessage(TestMessage msg)
{
TestMessage[] newMessages;
@ -116,18 +109,20 @@ public class VectorTests {
private void assertSubArrayEquals(String msg, byte[] expected, byte[] actual)
{
for (int index = 0; index < expected.length; ++index)
assertEquals(msg + "[" + Integer.toString(index) + "]", expected[index], actual[index]);
assertEquals(expected[index], actual[index], msg + "[" + index + "]");
}
/**
* Runs a Noise test vector.
*
* @param vec The test vector.
* @param initiator Handshake object for the initiator.
* @param responder Handshake object for the responder.
*/
private void runTest(TestVector vec, HandshakeState initiator, HandshakeState responder) throws ShortBufferException, BadPaddingException, NoSuchAlgorithmException
@ParameterizedTest
@MethodSource
void testVectors(TestVector vec) throws ShortBufferException, BadPaddingException, NoSuchAlgorithmException
{
HandshakeState initiator = new HandshakeState(vec.name, HandshakeState.INITIATOR);
HandshakeState responder = new HandshakeState(vec.name, HandshakeState.RESPONDER);
assertEquals(HandshakeState.INITIATOR, initiator.getRole());
assertEquals(HandshakeState.RESPONDER, responder.getRole());
assertEquals(vec.name, initiator.getProtocolName());
assertEquals(vec.name, responder.getProtocolName());
// Set all keys and special values that we need.
if (vec.init_prologue != null)
initiator.setPrologue(vec.init_prologue, 0, vec.init_prologue.length);
@ -169,12 +164,13 @@ public class VectorTests {
// Work through the messages one by one until both sides "split".
int role = HandshakeState.INITIATOR;
int index = 0;
HandshakeState send, recv;
boolean isOneWay = (vec.pattern.length() == 1);
boolean fallback = vec.fallback_expected;
byte[] message = new byte [8192];
byte[] plaintext = new byte [8192];
for (; index < vec.messages.length; ++index) {
final HandshakeState send, recv;
if (initiator.getAction() == HandshakeState.SPLIT &&
responder.getAction() == HandshakeState.SPLIT) {
break;
@ -196,15 +192,11 @@ public class VectorTests {
TestMessage msg = vec.messages[index];
int len = send.writeMessage(message, 0, msg.payload, 0, msg.payload.length);
assertEquals(msg.ciphertext.length, len);
assertSubArrayEquals(Integer.toString(index) + ": ciphertext", msg.ciphertext, message);
assertSubArrayEquals(index + ": ciphertext", msg.ciphertext, message);
if (fallback) {
// Perform a read on the responder, which will fail.
try {
recv.readMessage(message, 0, len, plaintext, 0);
fail("read should have triggered fallback");
} catch (BadPaddingException e) {
// Success!
}
assertThrows(BadPaddingException.class, () -> recv.readMessage(message, 0, len, plaintext, 0),
"read should have triggered fallback");
// Look up the pattern to fall back to.
String pattern = vec.fallback_pattern;
@ -224,7 +216,7 @@ public class VectorTests {
} else {
int plen = recv.readMessage(message, 0, len, plaintext, 0);
assertEquals(msg.payload.length, plen);
assertSubArrayEquals(Integer.toString(index) + ": payload", msg.payload, plaintext);
assertSubArrayEquals(index + ": payload", msg.payload, plaintext);
}
}
if (vec.fallback_expected) {
@ -276,10 +268,10 @@ public class VectorTests {
}
int len = csend.encryptWithAd(null, msg.payload, 0, message, 0, msg.payload.length);
assertEquals(msg.ciphertext.length, len);
assertSubArrayEquals(Integer.toString(index) + ": ciphertext", msg.ciphertext, message);
assertSubArrayEquals(index + ": ciphertext", msg.ciphertext, message);
int plen = crecv.decryptWithAd(null, message, 0, plaintext, 0, len);
assertEquals(msg.payload.length, plen);
assertSubArrayEquals(Integer.toString(index) + ": payload", msg.payload, plaintext);
assertSubArrayEquals(index + ": payload", msg.payload, plaintext);
}
// Clean up.
@ -289,16 +281,20 @@ public class VectorTests {
respPair.destroy();
}
/**
* Processes a single test vector from an input stream.
*
* @param reader The JSON reader for the input stream.
*
* The reader is positioned on the first field of the vector object.
*/
private void processVector(JsonReader reader) throws IOException
private static Stream<Arguments> testVectors() throws IOException {
try (InputStream testVectorInputStream = VectorTests.class.getResourceAsStream("test-vectors.json")) {
if (testVectorInputStream == null) {
throw new IOException("Could not load test vectors");
}
return loadTestVectors(testVectorInputStream).stream()
.map(testVector -> Arguments.of(Named.of(testVector.name, testVector)));
}
}
private static TestVector getNextVector(final JsonReader reader) throws IOException
{
boolean res = true;
boolean res = true;
// Parse the contents of the test vector.
TestVector vec = new TestVector();
while (reader.hasNext()) {
@ -385,116 +381,31 @@ public class VectorTests {
if (vec.name == null)
vec.name = protocolName;
// Execute the test vector.
++total;
System.out.print(vec.name);
System.out.print(" ... ");
System.out.flush();
try {
HandshakeState initiator = new HandshakeState(protocolName, HandshakeState.INITIATOR);
HandshakeState responder = new HandshakeState(protocolName, HandshakeState.RESPONDER);
assertEquals(HandshakeState.INITIATOR, initiator.getRole());
assertEquals(HandshakeState.RESPONDER, responder.getRole());
assertEquals(protocolName, initiator.getProtocolName());
assertEquals(protocolName, responder.getProtocolName());
runTest(vec, initiator, responder);
if (!vec.failure_expected) {
System.out.println("ok");
return vec;
}
private static List<TestVector> loadTestVectors(InputStream jsonInputStream) throws IOException {
List<TestVector> testVectors = new ArrayList<>();
try (JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader(jsonInputStream)))) {
jsonReader.beginObject();
while (jsonReader.hasNext()) {
String name = jsonReader.nextName();
if (name.equals("vectors")) {
jsonReader.beginArray();
while (jsonReader.hasNext()) {
jsonReader.beginObject();
testVectors.add(getNextVector(jsonReader));
jsonReader.endObject();
}
jsonReader.endArray();
} else {
System.out.println("failure expected");
++failed;
}
} catch (NoSuchAlgorithmException e) {
System.out.println("unsupported");
++skipped;
} catch (AssertionError e) {
System.out.println(e.getMessage());
e.printStackTrace(System.out);
++failed;
} catch (Exception e) {
if (!vec.failure_expected) {
System.out.println("failed");
e.printStackTrace(System.out);
++failed;
} else {
System.out.println("ok");
jsonReader.skipValue();
}
}
}
public void processFile(String filename) throws IOException {
try {
try (FileInputStream fileStream = new FileInputStream(filename)) {
System.out.print(filename + ": ");
processInputStream(fileStream);
}
} catch (FileNotFoundException e) {
System.err.println(filename + ": File not found");
}
}
public void processInputStream(InputStream jsonInputStream) throws IOException {
try(Reader streamReader = new BufferedReader(new InputStreamReader(jsonInputStream))) {
processReader(streamReader);
}
}
public void processReader(Reader jsonStream) throws IOException {
total = 0;
skipped = 0;
failed = 0;
JsonReader reader = new JsonReader(jsonStream);
try {
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if (name.equals("vectors")) {
reader.beginArray();
while (reader.hasNext() /*&& total < 50*/) {
reader.beginObject();
processVector(reader);
reader.endObject();
}
reader.endArray();
} else {
reader.skipValue();
}
}
reader.endObject();
} catch (IOException e) {
System.err.println("Exception while parsing JSON: " + e.toString());
e.printStackTrace();
} finally {
reader.close();
}
System.out.print(total);
System.out.print(" tests, ");
System.out.print(skipped);
System.out.print(" skipped, ");
System.out.print(failed);
System.out.println(" failed");
jsonReader.endObject();
}
public int getTotal() {
return total;
return testVectors;
}
public int getFailed() {
return failed;
}
public int getSkipped() {
return skipped;
}
public static void main(String[] args) throws IOException {
if (args.length == 0) {
System.out.println("Usage: VectorTests file1 file2 ...");
return;
}
VectorTests app = new VectorTests();
for (String filename : args)
app.processFile(filename);
}
}