Test harness for running JSON format vector tests

This commit is contained in:
Rhys Weatherley 2016-06-28 14:31:07 +10:00
parent cb9ea54fd7
commit cb4e98cdd6
10 changed files with 933 additions and 18 deletions

View File

@ -267,7 +267,7 @@ class AESGCMOnCtrCipherState implements CipherState {
int temp = 0;
for (int index = 0; index < 16; ++index)
temp |= (hashKey[index] ^ iv[index] ^ ciphertext[ciphertextOffset + dataLen + index]);
if (temp != 0)
if ((temp & 0xFF) != 0)
throw new AEADBadTagException();
try {
int result = cipher.update(ciphertext, ciphertextOffset, dataLen, plaintext, plaintextOffset);

View File

@ -270,7 +270,7 @@ class ChaChaPolyCipherState implements CipherState {
int temp = 0;
for (int index = 0; index < 16; ++index)
temp |= (polyKey[index] ^ ciphertext[ciphertextOffset + dataLen + index]);
if (temp != 0)
if ((temp & 0xFF) != 0)
throw new AEADBadTagException();
encrypt(ciphertext, ciphertextOffset, plaintext, plaintextOffset, dataLen);
return dataLen;

View File

@ -96,7 +96,7 @@ class Curve25519DHState implements DHState {
@Override
public void setPrivateKey(byte[] key, int offset) {
System.arraycopy(key, offset, publicKey, 0, 32);
System.arraycopy(key, offset, privateKey, 0, 32);
Curve25519.eval(publicKey, 0, privateKey, null);
mode = 0x03;
}

View File

@ -96,7 +96,7 @@ class Curve448DHState implements DHState {
@Override
public void setPrivateKey(byte[] key, int offset) {
System.arraycopy(key, offset, publicKey, 0, 56);
System.arraycopy(key, offset, privateKey, 0, 56);
Curve448.eval(publicKey, 0, privateKey, null);
mode = 0x03;
}

View File

@ -437,7 +437,7 @@ public class HandshakeState implements Destroyable {
throw new IllegalStateException("Remote static key required");
}
if ((requirements & PSK_REQUIRED) != 0) {
if (preSharedKey != null)
if (preSharedKey == null)
throw new IllegalStateException("Pre-shared key required");
}
@ -468,7 +468,7 @@ public class HandshakeState implements Destroyable {
symmetric.mixPublicKey(localEphemeral);
}
// The handshake has official started - set the first action.
// The handshake has officially started - set the first action.
if (isInitiator)
action = WRITE_MESSAGE;
else
@ -552,7 +552,7 @@ public class HandshakeState implements Destroyable {
// Reverse the pattern flags so that the responder is "local".
flags = Pattern.reverseFlags(flags);
}
requirements = computeRequirements(flags, components[0], isInitiator ? INITIATOR : RESPONDER, false);
requirements = computeRequirements(flags, components[0], isInitiator ? INITIATOR : RESPONDER, true);
}
/**
@ -952,14 +952,11 @@ public class HandshakeState implements Destroyable {
*
* @return The handshake hash. This must not be modified by the caller.
*
* The handshake hash value is only of use to the application after
* split() has been called.
*
* @throws IllegalStateException The action is not COMPLETE.
* @throws IllegalStateException The action is not SPLIT or COMPLETE.
*/
public byte[] getHandshakeHash()
{
if (action != COMPLETE) {
if (action != SPLIT && action != COMPLETE) {
throw new IllegalStateException
("Handshake has not completed");
}

View File

@ -34,13 +34,14 @@ import javax.crypto.ShortBufferException;
/**
* Symmetric state for helping manage a Noise handshake.
*/
public class SymmetricState implements Destroyable {
class SymmetricState implements Destroyable {
private String name;
private CipherState cipher;
private MessageDigest hash;
private byte[] ck;
private byte[] h;
private byte[] prev_h;
/**
* Constructs a new symmetric state object.
@ -57,10 +58,10 @@ public class SymmetricState implements Destroyable {
name = protocolName;
cipher = Noise.createCipher(cipherName);
hash = Noise.createHash(hashName);
int keyLength = cipher.getKeyLength();
ck = new byte [keyLength];
int hashLength = hash.getDigestLength();
ck = new byte [hashLength];
h = new byte [hashLength];
prev_h = new byte [hashLength];
byte[] protocolNameBytes;
try {
@ -77,7 +78,7 @@ public class SymmetricState implements Destroyable {
hashOne(protocolNameBytes, 0, protocolNameBytes.length, h, 0, h.length);
}
System.arraycopy(h, 0, ck, 0, keyLength);
System.arraycopy(h, 0, ck, 0, hashLength);
}
/**
@ -218,8 +219,9 @@ public class SymmetricState implements Destroyable {
*/
public int decryptAndHash(byte[] ciphertext, int ciphertextOffset, byte[] plaintext, int plaintextOffset, int length) throws ShortBufferException, AEADBadTagException
{
System.arraycopy(h, 0, prev_h, 0, h.length);
mixHash(ciphertext, ciphertextOffset, length);
return cipher.decryptWithAd(h, ciphertext, ciphertextOffset, plaintext, plaintextOffset, length);
return cipher.decryptWithAd(prev_h, ciphertext, ciphertextOffset, plaintext, plaintextOffset, length);
}
/**
@ -314,6 +316,10 @@ public class SymmetricState implements Destroyable {
Noise.destroy(h);
h = null;
}
if (prev_h != null) {
Noise.destroy(prev_h);
prev_h = null;
}
}
/**
@ -406,7 +412,7 @@ public class SymmetricState implements Destroyable {
hash.reset();
hash.update(block, 0, blockLength);
hash.update(data, dataOffset, dataLength);
hash.digest(output, outputOffset, outputLength);
hash.digest(output, outputOffset, hashLength);
for (index = 0; index < blockLength; ++index)
block[index] ^= (byte)(0x36 ^ 0x5C);
hash.reset();

View File

@ -0,0 +1,370 @@
/*
* Copyright (C) 2013 Southern Storm Software, Pty Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.southernstorm.json;
import java.io.IOException;
import java.io.Reader;
/**
* Recursive-descent parser for JSON streams.
*
* Intentionally compatible with android.util.JsonReader.
*/
public class JsonReader {
private Reader in;
private JsonToken token;
private boolean lenient;
private boolean booleanValue;
private String stringValue;
private int ungetCh;
public JsonReader(Reader in) {
this.in = in;
this.token = JsonToken.READ_FIRST;
this.lenient = false;
this.ungetCh = -2;
}
public void beginArray() throws IOException {
expectNext(JsonToken.BEGIN_ARRAY, "JSON begin array expected");
}
public void beginObject() throws IOException {
expectNext(JsonToken.BEGIN_OBJECT, "JSON begin object expected");
}
public void close() throws IOException {
in.close();
}
public void endArray() throws IOException {
expectNext(JsonToken.END_ARRAY, "JSON end array expected");
}
public void endObject() throws IOException {
expectNext(JsonToken.END_OBJECT, "JSON end array expected");
}
public boolean hasNext() throws IOException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
while (this.token == JsonToken.COMMA)
nextToken(); // Very lenient - multiple separating commas allowed.
return this.token != JsonToken.END_ARRAY
&& this.token != JsonToken.END_OBJECT;
}
public boolean isLenient() {
return this.lenient;
}
public void setLenient(boolean lenient) {
// Lenient mode as described in the Android documentation is not implemented.
this.lenient = lenient;
}
public boolean nextBoolean() throws IOException, IllegalStateException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
if (this.token != JsonToken.BOOLEAN)
throw new IllegalStateException("JSON boolean value expected");
boolean value = booleanValue;
nextToken();
return value;
}
public double nextDouble() throws IOException, IllegalStateException, NumberFormatException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
double value;
if (this.token == JsonToken.STRING || token == JsonToken.NUMBER) {
value = Double.parseDouble(stringValue);
} else {
throw new IllegalStateException("JSON double value expected");
}
nextToken();
return value;
}
public int nextInt() throws IOException, IllegalStateException, NumberFormatException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
int value;
if (this.token == JsonToken.STRING || this.token == JsonToken.NUMBER) {
value = Integer.parseInt(stringValue);
} else {
throw new IllegalStateException("JSON int value expected");
}
nextToken();
return value;
}
public long nextLong() throws IOException, IllegalStateException, NumberFormatException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
long value;
if (this.token == JsonToken.STRING || this.token == JsonToken.NUMBER) {
value = Long.parseLong(stringValue);
} else {
throw new MalformedJsonException("JSON long value expected");
}
nextToken();
return value;
}
public String nextName() throws IOException, IllegalStateException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
if (this.token != JsonToken.NAME)
throw new IllegalStateException("JSON property name expected");
String value = stringValue;
nextToken();
return value;
}
public void nextNull() throws IOException, IllegalStateException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
if (this.token != JsonToken.NULL)
throw new IllegalStateException("JSON null value expected");
nextToken();
}
public String nextString() throws IOException, IllegalStateException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
String value;
if (this.token == JsonToken.STRING || this.token == JsonToken.NUMBER)
value = stringValue;
else if (this.token == JsonToken.BOOLEAN)
value = Boolean.toString(booleanValue);
else if (this.token == JsonToken.NULL)
value = null;
else
throw new IllegalStateException("JSON string value expected");
nextToken();
return value;
}
public JsonToken peek() throws IOException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
return this.token;
}
public void skipValue() throws IOException {
switch (peek()) {
case BEGIN_ARRAY:
beginArray();
while (hasNext())
skipValue();
endArray();
break;
case BEGIN_OBJECT:
beginObject();
while (hasNext()) {
nextName();
skipValue();
}
endObject();
break;
case END_DOCUMENT:
break;
default:
nextToken();
break;
}
}
private void parseNumber(int ch) throws IOException {
StringBuilder builder = new StringBuilder();
builder.append((char)ch);
ch = in.read();
while ((ch >= '0' && ch <= '9') || ch == '-' || ch == '+' || ch == 'e' || ch == 'E' || ch == '.') {
builder.append((char)ch);
ch = in.read();
}
ungetCh = ch;
stringValue = builder.toString();
}
private void parseString() throws IOException {
StringBuilder builder = new StringBuilder();
int ch = in.read();
outer: while (ch != '"') {
if (ch == -1) {
ungetCh = ch;
break;
} else if (ch == '\\') {
ch = in.read();
if (ch == 'b')
builder.append('\u0008');
else if (ch == 'f')
builder.append('\u000C');
else if (ch == 'n')
builder.append('\n');
else if (ch == 'r')
builder.append('\r');
else if (ch == 't')
builder.append('\t');
else if (ch != 'u')
builder.append((char)ch);
else if (ch == -1) {
ungetCh = ch;
break;
} else {
int value = 0;
int digits = 0;
while (digits < 4) {
ch = in.read();
if (ch >= '0' && ch <= '9')
value = value * 16 + (ch - '0');
else if (ch >= 'A' && ch <= 'F')
value = value * 16 + (ch - 'A' + 10);
else if (ch >= 'a' && ch <= 'f')
value = value * 16 + (ch - 'a' + 10);
else {
builder.append((char)value);
continue outer;
}
++digits;
}
builder.append((char)value);
}
} else {
builder.append((char)ch);
}
ch = in.read();
}
stringValue = builder.toString();
}
private boolean checkForColon() throws IOException {
int ch = ungetCh;
if (ch == -2)
ch = in.read();
while (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n')
ch = in.read();
if (ch == ':') {
ungetCh = -2;
return true;
} else {
ungetCh = ch;
return false;
}
}
private void checkNamedToken(String name) throws IOException {
int index = 1;
int ch = in.read();
for (; index < name.length(); ++index) {
if (ch != name.charAt(index))
break;
ch = in.read();
}
if (index < name.length() || (ch >= 'a' && ch <= 'z')
|| (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')
|| ch == '_') {
throw new MalformedJsonException("Invalid keyword, expected: '"
+ name + "'");
}
ungetCh = ch;
}
private void nextToken() throws IOException {
int ch = ungetCh;
if (ch == -2)
ch = in.read();
while (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n')
ch = in.read();
ungetCh = -2;
switch (ch) {
case -1:
token = JsonToken.END_DOCUMENT;
ungetCh = -1;
break;
case '{':
token = JsonToken.BEGIN_OBJECT;
break;
case '}':
token = JsonToken.END_OBJECT;
break;
case '[':
token = JsonToken.BEGIN_ARRAY;
break;
case ']':
token = JsonToken.END_ARRAY;
break;
case ',':
token = JsonToken.COMMA;
break;
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
parseNumber(ch);
token = JsonToken.NUMBER;
break;
case '"':
parseString();
if (checkForColon())
token = JsonToken.NAME;
else
token = JsonToken.STRING;
break;
case 'n':
checkNamedToken("null");
token = JsonToken.NULL;
break;
case 't':
checkNamedToken("true");
token = JsonToken.BOOLEAN;
booleanValue = true;
break;
case 'f':
checkNamedToken("false");
token = JsonToken.BOOLEAN;
booleanValue = false;
break;
default:
throw new MalformedJsonException(
"Invalid character in JSON stream: '" + (char) ch + "'");
}
}
private void expectNext(JsonToken token, String message) throws IOException {
if (this.token == JsonToken.READ_FIRST)
nextToken();
if (this.token != token)
throw new MalformedJsonException(message);
nextToken();
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2013 Southern Storm Software, Pty Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.southernstorm.json;
/**
* Types of JSON tokens.
*
* Intentionally compatible with android.util.JsonToken.
*/
public enum JsonToken {
BEGIN_ARRAY,
BEGIN_OBJECT,
BOOLEAN,
END_ARRAY,
END_DOCUMENT,
END_OBJECT,
NAME,
NULL,
NUMBER,
STRING,
// Internal to this implementation of JsonReader - not part of the public interface.
READ_FIRST,
COMMA
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2013 Southern Storm Software, Pty Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.southernstorm.json;
import java.io.IOException;
public class MalformedJsonException extends IOException {
private static final long serialVersionUID = -1645564375062756336L;
public MalformedJsonException(String detailMessage) {
super(detailMessage);
}
}

View File

@ -0,0 +1,462 @@
/*
* Copyright (C) 2016 Southern Storm Software, Pty Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
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.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import javax.crypto.AEADBadTagException;
import javax.crypto.ShortBufferException;
import javax.xml.bind.DatatypeConverter;
import com.southernstorm.json.JsonReader;
import com.southernstorm.noise.protocol.CipherState;
import com.southernstorm.noise.protocol.CipherStatePair;
import com.southernstorm.noise.protocol.HandshakeState;
/**
* 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
{
public byte[] payload;
public byte[] ciphertext;
}
/**
* Information about a Noise test vector that was parsed from a JSON stream.
*/
private class TestVector
{
public String name;
public String pattern;
public String dh;
public String cipher;
public String hash;
public byte[] init_prologue;
public byte[] init_ephemeral;
public byte[] init_static;
public byte[] init_remote_static;
public byte[] init_psk;
public byte[] init_ssk;
public byte[] resp_prologue;
public byte[] resp_ephemeral;
public byte[] resp_static;
public byte[] resp_remote_static;
public byte[] resp_psk;
public byte[] resp_ssk;
public byte[] handshake_hash;
public boolean failure_expected;
public boolean fallback_expected;
public TestMessage[] messages;
public void addMessage(TestMessage msg)
{
TestMessage[] newMessages;
if (messages != null) {
newMessages = new TestMessage [messages.length + 1];
System.arraycopy(messages, 0, newMessages, 0, messages.length);
newMessages[messages.length] = msg;
} else {
newMessages = new TestMessage [1];
newMessages[0] = msg;
}
messages = newMessages;
}
}
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]);
}
/**
* 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, AEADBadTagException, NoSuchAlgorithmException
{
// Set all keys and special values that we need.
if (vec.init_prologue != null)
initiator.setPrologue(vec.init_prologue, 0, vec.init_prologue.length);
if (vec.init_static != null)
initiator.getLocalKeyPair().setPrivateKey(vec.init_static, 0);
if (vec.init_remote_static != null)
initiator.getRemotePublicKey().setPublicKey(vec.init_remote_static, 0);
if (vec.init_ephemeral != null)
initiator.getFixedEphemeralKey().setPrivateKey(vec.init_ephemeral, 0);
if (vec.init_psk != null)
initiator.setPreSharedKey(vec.init_psk, 0, vec.init_psk.length);
if (vec.resp_prologue != null)
responder.setPrologue(vec.resp_prologue, 0, vec.resp_prologue.length);
if (vec.resp_static != null)
responder.getLocalKeyPair().setPrivateKey(vec.resp_static, 0);
if (vec.resp_remote_static != null)
responder.getRemotePublicKey().setPublicKey(vec.resp_remote_static, 0);
if (vec.resp_ephemeral != null) {
// Note: The test data contains responder ephemeral keys for one-way
// patterns which doesn't actually make sense. Ignore those keys.
if (vec.pattern.length() != 1)
responder.getFixedEphemeralKey().setPrivateKey(vec.resp_ephemeral, 0);
}
if (vec.resp_psk != null)
responder.setPreSharedKey(vec.resp_psk, 0, vec.resp_psk.length);
// Start both sides of the handshake.
assertEquals(HandshakeState.NO_ACTION, initiator.getAction());
assertEquals(HandshakeState.NO_ACTION, responder.getAction());
initiator.start();
responder.start();
assertEquals(HandshakeState.WRITE_MESSAGE, initiator.getAction());
assertEquals(HandshakeState.READ_MESSAGE, responder.getAction());
// 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 [1024];
byte[] plaintext = new byte [1024];
for (; index < vec.messages.length; ++index) {
if (initiator.getAction() == HandshakeState.SPLIT &&
responder.getAction() == HandshakeState.SPLIT) {
break;
}
if (role == HandshakeState.INITIATOR) {
// Send on the initiator, receive on the responder.
send = initiator;
recv = responder;
if (!isOneWay)
role = HandshakeState.RESPONDER;
} else {
// Send on the responder, receive on the initiator.
send = responder;
recv = initiator;
role = HandshakeState.INITIATOR;
}
assertEquals(HandshakeState.WRITE_MESSAGE, send.getAction());
assertEquals(HandshakeState.READ_MESSAGE, recv.getAction());
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);
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 (AEADBadTagException e) {
// Success!
}
// Initiate fallback on both sides.
initiator.fallback();
responder.fallback();
// Restart the protocols.
initiator.start();
responder.start();
// Only need to fallback once.
fallback = false;
} else {
int plen = recv.readMessage(message, 0, len, plaintext, 0);
assertEquals(msg.payload.length, plen);
assertSubArrayEquals(Integer.toString(index) + ": payload", msg.payload, plaintext);
}
}
if (vec.fallback_expected) {
// The roles will have reversed during the handshake.
assertEquals(HandshakeState.RESPONDER, initiator.getRole());
assertEquals(HandshakeState.INITIATOR, responder.getRole());
} else {
assertEquals(HandshakeState.INITIATOR, initiator.getRole());
assertEquals(HandshakeState.RESPONDER, responder.getRole());
}
// Handshake finished. Check the handshake hash values.
if (vec.handshake_hash != null) {
assertArrayEquals(vec.handshake_hash, initiator.getHandshakeHash());
assertArrayEquals(vec.handshake_hash, responder.getHandshakeHash());
}
// Split the two sides to get the transport ciphers.
CipherStatePair initPair;
CipherStatePair respPair;
assertEquals(HandshakeState.SPLIT, initiator.getAction());
assertEquals(HandshakeState.SPLIT, responder.getAction());
if (vec.init_ssk != null)
initPair = initiator.split(vec.init_ssk, 0, vec.init_ssk.length);
else
initPair = initiator.split();
if (vec.resp_ssk != null)
respPair = responder.split(vec.resp_ssk, 0, vec.resp_ssk.length);
else
respPair = responder.split();
assertEquals(HandshakeState.COMPLETE, initiator.getAction());
assertEquals(HandshakeState.COMPLETE, responder.getAction());
// Now handle the data transport.
CipherState csend, crecv;
for (; index < vec.messages.length; ++index) {
TestMessage msg = vec.messages[index];
if (role == HandshakeState.INITIATOR) {
// Send on the initiator, receive on the responder.
csend = initPair.getSender();
crecv = respPair.getReceiver();
if (!isOneWay)
role = HandshakeState.RESPONDER;
} else {
// Send on the responder, receive on the initiator.
csend = respPair.getSender();
crecv = initPair.getReceiver();
role = HandshakeState.INITIATOR;
}
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);
int plen = crecv.decryptWithAd(null, message, 0, plaintext, 0, len);
assertEquals(msg.payload.length, plen);
assertSubArrayEquals(Integer.toString(index) + ": payload", msg.payload, plaintext);
}
// Clean up.
initiator.destroy();
responder.destroy();
initPair.destroy();
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
{
// Parse the contents of the test vector.
TestVector vec = new TestVector();
while (reader.hasNext()) {
String name = reader.nextName();
if (name.equals("name"))
vec.name = reader.nextString();
else if (name.equals("pattern"))
vec.pattern = reader.nextString();
else if (name.equals("dh"))
vec.dh = reader.nextString();
else if (name.equals("cipher"))
vec.cipher = reader.nextString();
else if (name.equals("hash"))
vec.hash = reader.nextString();
else if (name.equals("init_prologue"))
vec.init_prologue = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("init_ephemeral"))
vec.init_ephemeral = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("init_static"))
vec.init_static = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("init_remote_static"))
vec.init_remote_static = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("init_psk"))
vec.init_psk = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("init_ssk"))
vec.init_ssk = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("resp_prologue"))
vec.resp_prologue = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("resp_ephemeral"))
vec.resp_ephemeral = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("resp_static"))
vec.resp_static = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("resp_remote_static"))
vec.resp_remote_static = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("resp_psk"))
vec.resp_psk = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("resp_ssk"))
vec.resp_ssk = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("handshake_hash"))
vec.handshake_hash = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("fail"))
vec.failure_expected = reader.nextBoolean();
else if (name.equals("fallback"))
vec.fallback_expected = reader.nextBoolean();
else if (name.equals("messages")) {
reader.beginArray();
while (reader.hasNext()) {
TestMessage msg = new TestMessage();
reader.beginObject();
while (reader.hasNext()) {
name = reader.nextName();
if (name.equals("payload"))
msg.payload = DatatypeConverter.parseHexBinary(reader.nextString());
else if (name.equals("ciphertext"))
msg.ciphertext = DatatypeConverter.parseHexBinary(reader.nextString());
else
reader.skipValue();
}
vec.addMessage(msg);
reader.endObject();
}
reader.endArray();
} else {
reader.skipValue();
}
}
// Format the complete protocol name.
String protocolName = "Noise";
if (vec.init_psk != null || vec.resp_psk != null)
protocolName += "PSK";
protocolName += "_" + vec.pattern + "_" + vec.dh + "_" + vec.cipher + "_" + vec.hash;
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");
} 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");
}
}
}
/**
* Processes a file from the command-line.
*
* @param filename The name of the file to process.
*/
public void processFile(String filename)
{
total = 0;
skipped = 0;
failed = 0;
try {
FileInputStream fileStream = new FileInputStream(filename);
try {
InputStreamReader streamReader = new InputStreamReader(fileStream);
BufferedReader bufferedReader = new BufferedReader(streamReader);
JsonReader reader = new JsonReader(bufferedReader);
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();
} finally {
reader.close();
}
} finally {
fileStream.close();
}
} catch (FileNotFoundException e) {
System.err.println(filename + ": File not found");
} catch (IOException e) {
System.err.println("Exception while parsing JSON: " + e.toString());
e.printStackTrace();
}
System.out.print(filename + ": ");
System.out.print(total);
System.out.print(" tests, ");
System.out.print(skipped);
System.out.print(" skipped, ");
System.out.print(failed);
System.out.println(" failed");
}
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("Usage: VectorTests file1 file2 ...");
return;
}
VectorTests app = new VectorTests();
for (String filename : args)
app.processFile(filename);
}
}