From cb4e98cdd67f6cfa1d4c9e29f3994b9279994e2b Mon Sep 17 00:00:00 2001 From: Rhys Weatherley Date: Tue, 28 Jun 2016 14:31:07 +1000 Subject: [PATCH] Test harness for running JSON format vector tests --- .../protocol/AESGCMOnCtrCipherState.java | 2 +- .../noise/protocol/ChaChaPolyCipherState.java | 2 +- .../noise/protocol/Curve25519DHState.java | 2 +- .../noise/protocol/Curve448DHState.java | 2 +- .../noise/protocol/HandshakeState.java | 13 +- .../noise/protocol/SymmetricState.java | 18 +- .../com/southernstorm/json/JsonReader.java | 370 ++++++++++++++ .../src/com/southernstorm/json/JsonToken.java | 45 ++ .../json/MalformedJsonException.java | 35 ++ .../noise/tests/VectorTests.java | 462 ++++++++++++++++++ 10 files changed, 933 insertions(+), 18 deletions(-) create mode 100644 NoiseJavaTests/src/com/southernstorm/json/JsonReader.java create mode 100644 NoiseJavaTests/src/com/southernstorm/json/JsonToken.java create mode 100644 NoiseJavaTests/src/com/southernstorm/json/MalformedJsonException.java create mode 100644 NoiseJavaTests/src/com/southernstorm/noise/tests/VectorTests.java diff --git a/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMOnCtrCipherState.java b/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMOnCtrCipherState.java index 2172b19..07e86ee 100644 --- a/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMOnCtrCipherState.java +++ b/NoiseJava/src/com/southernstorm/noise/protocol/AESGCMOnCtrCipherState.java @@ -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); diff --git a/NoiseJava/src/com/southernstorm/noise/protocol/ChaChaPolyCipherState.java b/NoiseJava/src/com/southernstorm/noise/protocol/ChaChaPolyCipherState.java index 806022d..ddf597e 100644 --- a/NoiseJava/src/com/southernstorm/noise/protocol/ChaChaPolyCipherState.java +++ b/NoiseJava/src/com/southernstorm/noise/protocol/ChaChaPolyCipherState.java @@ -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; diff --git a/NoiseJava/src/com/southernstorm/noise/protocol/Curve25519DHState.java b/NoiseJava/src/com/southernstorm/noise/protocol/Curve25519DHState.java index e6741b5..771faa2 100644 --- a/NoiseJava/src/com/southernstorm/noise/protocol/Curve25519DHState.java +++ b/NoiseJava/src/com/southernstorm/noise/protocol/Curve25519DHState.java @@ -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; } diff --git a/NoiseJava/src/com/southernstorm/noise/protocol/Curve448DHState.java b/NoiseJava/src/com/southernstorm/noise/protocol/Curve448DHState.java index 3900934..b55bfdd 100644 --- a/NoiseJava/src/com/southernstorm/noise/protocol/Curve448DHState.java +++ b/NoiseJava/src/com/southernstorm/noise/protocol/Curve448DHState.java @@ -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; } diff --git a/NoiseJava/src/com/southernstorm/noise/protocol/HandshakeState.java b/NoiseJava/src/com/southernstorm/noise/protocol/HandshakeState.java index abe069d..01fdcc3 100644 --- a/NoiseJava/src/com/southernstorm/noise/protocol/HandshakeState.java +++ b/NoiseJava/src/com/southernstorm/noise/protocol/HandshakeState.java @@ -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"); } diff --git a/NoiseJava/src/com/southernstorm/noise/protocol/SymmetricState.java b/NoiseJava/src/com/southernstorm/noise/protocol/SymmetricState.java index 4146a43..363dba4 100644 --- a/NoiseJava/src/com/southernstorm/noise/protocol/SymmetricState.java +++ b/NoiseJava/src/com/southernstorm/noise/protocol/SymmetricState.java @@ -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(); diff --git a/NoiseJavaTests/src/com/southernstorm/json/JsonReader.java b/NoiseJavaTests/src/com/southernstorm/json/JsonReader.java new file mode 100644 index 0000000..7a70b87 --- /dev/null +++ b/NoiseJavaTests/src/com/southernstorm/json/JsonReader.java @@ -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(); + } +} diff --git a/NoiseJavaTests/src/com/southernstorm/json/JsonToken.java b/NoiseJavaTests/src/com/southernstorm/json/JsonToken.java new file mode 100644 index 0000000..0b392fc --- /dev/null +++ b/NoiseJavaTests/src/com/southernstorm/json/JsonToken.java @@ -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 +} diff --git a/NoiseJavaTests/src/com/southernstorm/json/MalformedJsonException.java b/NoiseJavaTests/src/com/southernstorm/json/MalformedJsonException.java new file mode 100644 index 0000000..5e6a5f6 --- /dev/null +++ b/NoiseJavaTests/src/com/southernstorm/json/MalformedJsonException.java @@ -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); + } + +} diff --git a/NoiseJavaTests/src/com/southernstorm/noise/tests/VectorTests.java b/NoiseJavaTests/src/com/southernstorm/noise/tests/VectorTests.java new file mode 100644 index 0000000..d4f537e --- /dev/null +++ b/NoiseJavaTests/src/com/southernstorm/noise/tests/VectorTests.java @@ -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); + } + +}