From 70f018eb87fc4b135b853f0b034ff14ed8096920 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 26 Jun 2018 16:01:35 +0000 Subject: [PATCH] NTCP2: Noise lib from: https://github.com/rweather/noise-java Revision db4855c on Oct 8, 2016 Copyright (C) 2016 Southern Storm Software, Pty Ltd. MIT license Unmodified, as base for future merges Only classes we need, this rev will not compile --- .../noise/crypto/ChaChaCore.java | 200 +++ .../noise/crypto/Curve25519.java | 531 +++++++ .../southernstorm/noise/crypto/Poly1305.java | 327 +++++ .../noise/crypto/package-info.java | 12 + .../noise/protocol/ChaChaPolyCipherState.java | 290 ++++ .../noise/protocol/CipherState.java | 158 +++ .../noise/protocol/CipherStatePair.java | 106 ++ .../noise/protocol/Curve25519DHState.java | 156 ++ .../southernstorm/noise/protocol/DHState.java | 156 ++ .../noise/protocol/Destroyable.java | 43 + .../noise/protocol/HandshakeState.java | 1260 +++++++++++++++++ .../southernstorm/noise/protocol/Noise.java | 237 ++++ .../southernstorm/noise/protocol/Pattern.java | 835 +++++++++++ .../noise/protocol/SymmetricState.java | 484 +++++++ .../noise/protocol/package-info.java | 7 + 15 files changed, 4802 insertions(+) create mode 100644 router/java/src/com/southernstorm/noise/crypto/ChaChaCore.java create mode 100644 router/java/src/com/southernstorm/noise/crypto/Curve25519.java create mode 100644 router/java/src/com/southernstorm/noise/crypto/Poly1305.java create mode 100644 router/java/src/com/southernstorm/noise/crypto/package-info.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/ChaChaPolyCipherState.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/CipherState.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/CipherStatePair.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/Curve25519DHState.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/DHState.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/Destroyable.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/HandshakeState.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/Noise.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/Pattern.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/SymmetricState.java create mode 100644 router/java/src/com/southernstorm/noise/protocol/package-info.java diff --git a/router/java/src/com/southernstorm/noise/crypto/ChaChaCore.java b/router/java/src/com/southernstorm/noise/crypto/ChaChaCore.java new file mode 100644 index 000000000..4f38c4503 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/crypto/ChaChaCore.java @@ -0,0 +1,200 @@ +/* + * 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.crypto; + +/** + * Implementation of the ChaCha20 core hash transformation. + */ +public final class ChaChaCore { + + private ChaChaCore() {} + + /** + * Hashes an input block with ChaCha20. + * + * @param output The output block, which must contain at least 16 + * elements and must not overlap with the input. + * @param input The input block, which must contain at least 16 + * elements. + */ + public static void hash(int[] output, int[] input) + { + int index; + + // Copy the input to the output to start with. + for (index = 0; index < 16; ++index) + output[index] = input[index]; + + // Perform the 20 ChaCha rounds in groups of two. + for (index = 0; index < 20; index += 2) { + // Column round. + quarterRound(output, 0, 4, 8, 12); + quarterRound(output, 1, 5, 9, 13); + quarterRound(output, 2, 6, 10, 14); + quarterRound(output, 3, 7, 11, 15); + + // Diagonal round. + quarterRound(output, 0, 5, 10, 15); + quarterRound(output, 1, 6, 11, 12); + quarterRound(output, 2, 7, 8, 13); + quarterRound(output, 3, 4, 9, 14); + } + + // Add the input block to the output. + for (index = 0; index < 16; ++index) + output[index] += input[index]; + } + + private static int char4(char c1, char c2, char c3, char c4) + { + return (((int)c1) & 0xFF) | ((((int)c2) & 0xFF) << 8) | ((((int)c3) & 0xFF) << 16) | ((((int)c4) & 0xFF) << 24); + } + + private static int fromLittleEndian(byte[] key, int offset) + { + return (key[offset] & 0xFF) | ((key[offset + 1] & 0xFF) << 8) | ((key[offset + 2] & 0xFF) << 16) | ((key[offset + 3] & 0xFF) << 24); + } + + /** + * Initializes a ChaCha20 block with a 128-bit key. + * + * @param output The output block, which must consist of at + * least 16 words. + * @param key The buffer containing the key. + * @param offset Offset of the key in the buffer. + */ + public static void initKey128(int[] output, byte[] key, int offset) + { + output[0] = char4('e', 'x', 'p', 'a'); + output[1] = char4('n', 'd', ' ', '1'); + output[2] = char4('6', '-', 'b', 'y'); + output[3] = char4('t', 'e', ' ', 'k'); + output[4] = fromLittleEndian(key, offset); + output[5] = fromLittleEndian(key, offset + 4); + output[6] = fromLittleEndian(key, offset + 8); + output[7] = fromLittleEndian(key, offset + 12); + output[8] = output[4]; + output[9] = output[5]; + output[10] = output[6]; + output[11] = output[7]; + output[12] = 0; + output[13] = 0; + output[14] = 0; + output[15] = 0; + } + + /** + * Initializes a ChaCha20 block with a 256-bit key. + * + * @param output The output block, which must consist of at + * least 16 words. + * @param key The buffer containing the key. + * @param offset Offset of the key in the buffer. + */ + public static void initKey256(int[] output, byte[] key, int offset) + { + output[0] = char4('e', 'x', 'p', 'a'); + output[1] = char4('n', 'd', ' ', '3'); + output[2] = char4('2', '-', 'b', 'y'); + output[3] = char4('t', 'e', ' ', 'k'); + output[4] = fromLittleEndian(key, offset); + output[5] = fromLittleEndian(key, offset + 4); + output[6] = fromLittleEndian(key, offset + 8); + output[7] = fromLittleEndian(key, offset + 12); + output[8] = fromLittleEndian(key, offset + 16); + output[9] = fromLittleEndian(key, offset + 20); + output[10] = fromLittleEndian(key, offset + 24); + output[11] = fromLittleEndian(key, offset + 28); + output[12] = 0; + output[13] = 0; + output[14] = 0; + output[15] = 0; + } + + /** + * Initializes the 64-bit initialization vector in a ChaCha20 block. + * + * @param output The output block, which must consist of at + * least 16 words and must have been initialized by initKey256() + * or initKey128(). + * @param iv The 64-bit initialization vector value. + * + * The counter portion of the output block is set to zero. + */ + public static void initIV(int[] output, long iv) + { + output[12] = 0; + output[13] = 0; + output[14] = (int)iv; + output[15] = (int)(iv >> 32); + } + + /** + * Initializes the 64-bit initialization vector and counter in a ChaCha20 block. + * + * @param output The output block, which must consist of at + * least 16 words and must have been initialized by initKey256() + * or initKey128(). + * @param iv The 64-bit initialization vector value. + * @param counter The 64-bit counter value. + */ + public static void initIV(int[] output, long iv, long counter) + { + output[12] = (int)counter; + output[13] = (int)(counter >> 32); + output[14] = (int)iv; + output[15] = (int)(iv >> 32); + } + + private static int leftRotate16(int v) + { + return v << 16 | (v >>> 16); + } + + private static int leftRotate12(int v) + { + return v << 12 | (v >>> 20); + } + + private static int leftRotate8(int v) + { + return v << 8 | (v >>> 24); + } + + private static int leftRotate7(int v) + { + return v << 7 | (v >>> 25); + } + + private static void quarterRound(int[] v, int a, int b, int c, int d) + { + v[a] += v[b]; + v[d] = leftRotate16(v[d] ^ v[a]); + v[c] += v[d]; + v[b] = leftRotate12(v[b] ^ v[c]); + v[a] += v[b]; + v[d] = leftRotate8(v[d] ^ v[a]); + v[c] += v[d]; + v[b] = leftRotate7(v[b] ^ v[c]); + } +} diff --git a/router/java/src/com/southernstorm/noise/crypto/Curve25519.java b/router/java/src/com/southernstorm/noise/crypto/Curve25519.java new file mode 100644 index 000000000..5183e2260 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/crypto/Curve25519.java @@ -0,0 +1,531 @@ +/* + * 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.crypto; + +import java.util.Arrays; + +/** + * Implementation of the Curve25519 elliptic curve algorithm. + * + * This implementation is based on that from arduinolibs: + * https://github.com/rweather/arduinolibs + * + * Differences in this version are due to using 26-bit limbs for the + * representation instead of the 8/16/32-bit limbs in the original. + * + * References: http://cr.yp.to/ecdh.html, RFC 7748 + */ +public final class Curve25519 { + + // Numbers modulo 2^255 - 19 are broken up into ten 26-bit words. + private static final int NUM_LIMBS_255BIT = 10; + private static final int NUM_LIMBS_510BIT = 20; + private int[] x_1; + private int[] x_2; + private int[] x_3; + private int[] z_2; + private int[] z_3; + private int[] A; + private int[] B; + private int[] C; + private int[] D; + private int[] E; + private int[] AA; + private int[] BB; + private int[] DA; + private int[] CB; + private long[] t1; + private int[] t2; + + /** + * Constructs the temporary state holder for Curve25519 evaluation. + */ + private Curve25519() + { + // Allocate memory for all of the temporary variables we will need. + x_1 = new int [NUM_LIMBS_255BIT]; + x_2 = new int [NUM_LIMBS_255BIT]; + x_3 = new int [NUM_LIMBS_255BIT]; + z_2 = new int [NUM_LIMBS_255BIT]; + z_3 = new int [NUM_LIMBS_255BIT]; + A = new int [NUM_LIMBS_255BIT]; + B = new int [NUM_LIMBS_255BIT]; + C = new int [NUM_LIMBS_255BIT]; + D = new int [NUM_LIMBS_255BIT]; + E = new int [NUM_LIMBS_255BIT]; + AA = new int [NUM_LIMBS_255BIT]; + BB = new int [NUM_LIMBS_255BIT]; + DA = new int [NUM_LIMBS_255BIT]; + CB = new int [NUM_LIMBS_255BIT]; + t1 = new long [NUM_LIMBS_510BIT]; + t2 = new int [NUM_LIMBS_510BIT]; + } + + + /** + * Destroy all sensitive data in this object. + */ + private void destroy() { + // Destroy all temporary variables. + Arrays.fill(x_1, 0); + Arrays.fill(x_2, 0); + Arrays.fill(x_3, 0); + Arrays.fill(z_2, 0); + Arrays.fill(z_3, 0); + Arrays.fill(A, 0); + Arrays.fill(B, 0); + Arrays.fill(C, 0); + Arrays.fill(D, 0); + Arrays.fill(E, 0); + Arrays.fill(AA, 0); + Arrays.fill(BB, 0); + Arrays.fill(DA, 0); + Arrays.fill(CB, 0); + Arrays.fill(t1, 0L); + Arrays.fill(t2, 0); + } + + /** + * Reduces a number modulo 2^255 - 19 where it is known that the + * number can be reduced with only 1 trial subtraction. + * + * @param x The number to reduce, and the result. + */ + private void reduceQuick(int[] x) + { + int index, carry; + + // Perform a trial subtraction of (2^255 - 19) from "x" which is + // equivalent to adding 19 and subtracting 2^255. We add 19 here; + // the subtraction of 2^255 occurs in the next step. + carry = 19; + for (index = 0; index < NUM_LIMBS_255BIT; ++index) { + carry += x[index]; + t2[index] = carry & 0x03FFFFFF; + carry >>= 26; + } + + // If there was a borrow, then the original "x" is the correct answer. + // If there was no borrow, then "t2" is the correct answer. Select the + // correct answer but do it in a way that instruction timing will not + // reveal which value was selected. Borrow will occur if bit 21 of + // "t2" is zero. Turn the bit into a selection mask. + int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01); + int nmask = ~mask; + t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; + for (index = 0; index < NUM_LIMBS_255BIT; ++index) + x[index] = (x[index] & nmask) | (t2[index] & mask); + } + + /** + * Reduce a number modulo 2^255 - 19. + * + * @param result The result. + * @param x The value to be reduced. This array will be + * modified during the reduction. + * @param size The number of limbs in the high order half of x. + */ + private void reduce(int[] result, int[] x, int size) + { + int index, limb, carry; + + // Calculate (x mod 2^255) + ((x / 2^255) * 19) which will + // either produce the answer we want or it will produce a + // value of the form "answer + j * (2^255 - 19)". There are + // 5 left-over bits in the top-most limb of the bottom half. + carry = 0; + limb = x[NUM_LIMBS_255BIT - 1] >> 21; + x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; + for (index = 0; index < size; ++index) { + limb += x[NUM_LIMBS_255BIT + index] << 5; + carry += (limb & 0x03FFFFFF) * 19 + x[index]; + x[index] = carry & 0x03FFFFFF; + limb >>= 26; + carry >>= 26; + } + if (size < NUM_LIMBS_255BIT) { + // The high order half of the number is short; e.g. for mulA24(). + // Propagate the carry through the rest of the low order part. + for (index = size; index < NUM_LIMBS_255BIT; ++index) { + carry += x[index]; + x[index] = carry & 0x03FFFFFF; + carry >>= 26; + } + } + + // The "j" value may still be too large due to the final carry-out. + // We must repeat the reduction. If we already have the answer, + // then this won't do any harm but we must still do the calculation + // to preserve the overall timing. The "j" value will be between + // 0 and 19, which means that the carry we care about is in the + // top 5 bits of the highest limb of the bottom half. + carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19; + x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; + for (index = 0; index < NUM_LIMBS_255BIT; ++index) { + carry += x[index]; + result[index] = carry & 0x03FFFFFF; + carry >>= 26; + } + + // At this point "x" will either be the answer or it will be the + // answer plus (2^255 - 19). Perform a trial subtraction to + // complete the reduction process. + reduceQuick(result); + } + + /** + * Multiplies two numbers modulo 2^255 - 19. + * + * @param result The result. + * @param x The first number to multiply. + * @param y The second number to multiply. + */ + private void mul(int[] result, int[] x, int[] y) + { + int i, j; + + // Multiply the two numbers to create the intermediate result. + long v = x[0]; + for (i = 0; i < NUM_LIMBS_255BIT; ++i) { + t1[i] = v * y[i]; + } + for (i = 1; i < NUM_LIMBS_255BIT; ++i) { + v = x[i]; + for (j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) { + t1[i + j] += v * y[j]; + } + t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1]; + } + + // Propagate carries and convert back into 26-bit words. + v = t1[0]; + t2[0] = ((int)v) & 0x03FFFFFF; + for (i = 1; i < NUM_LIMBS_510BIT; ++i) { + v = (v >> 26) + t1[i]; + t2[i] = ((int)v) & 0x03FFFFFF; + } + + // Reduce the result modulo 2^255 - 19. + reduce(result, t2, NUM_LIMBS_255BIT); + } + + /** + * Squares a number modulo 2^255 - 19. + * + * @param result The result. + * @param x The number to square. + */ + private void square(int[] result, int[] x) + { + mul(result, x, x); + } + + /** + * Multiplies a number by the a24 constant, modulo 2^255 - 19. + * + * @param result The result. + * @param x The number to multiply by a24. + */ + private void mulA24(int[] result, int[] x) + { + long a24 = 121665; + long carry = 0; + int index; + for (index = 0; index < NUM_LIMBS_255BIT; ++index) { + carry += a24 * x[index]; + t2[index] = ((int)carry) & 0x03FFFFFF; + carry >>= 26; + } + t2[NUM_LIMBS_255BIT] = ((int)carry) & 0x03FFFFFF; + reduce(result, t2, 1); + } + + /** + * Adds two numbers modulo 2^255 - 19. + * + * @param result The result. + * @param x The first number to add. + * @param y The second number to add. + */ + private void add(int[] result, int[] x, int[] y) + { + int index, carry; + carry = x[0] + y[0]; + result[0] = carry & 0x03FFFFFF; + for (index = 1; index < NUM_LIMBS_255BIT; ++index) { + carry = (carry >> 26) + x[index] + y[index]; + result[index] = carry & 0x03FFFFFF; + } + reduceQuick(result); + } + + /** + * Subtracts two numbers modulo 2^255 - 19. + * + * @param result The result. + * @param x The first number to subtract. + * @param y The second number to subtract. + */ + private void sub(int[] result, int[] x, int[] y) + { + int index, borrow; + + // Subtract y from x to generate the intermediate result. + borrow = 0; + for (index = 0; index < NUM_LIMBS_255BIT; ++index) { + borrow = x[index] - y[index] - ((borrow >> 26) & 0x01); + result[index] = borrow & 0x03FFFFFF; + } + + // If we had a borrow, then the result has gone negative and we + // have to add 2^255 - 19 to the result to make it positive again. + // The top bits of "borrow" will be all 1's if there is a borrow + // or it will be all 0's if there was no borrow. Easiest is to + // conditionally subtract 19 and then mask off the high bits. + borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19); + result[0] = borrow & 0x03FFFFFF; + for (index = 1; index < NUM_LIMBS_255BIT; ++index) { + borrow = result[index] - ((borrow >> 26) & 0x01); + result[index] = borrow & 0x03FFFFFF; + } + result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF; + } + + /** + * Conditional swap of two values. + * + * @param select Set to 1 to swap, 0 to leave as-is. + * @param x The first value. + * @param y The second value. + */ + private static void cswap(int select, int[] x, int[] y) + { + int dummy; + select = -select; + for (int index = 0; index < NUM_LIMBS_255BIT; ++index) { + dummy = select & (x[index] ^ y[index]); + x[index] ^= dummy; + y[index] ^= dummy; + } + } + + /** + * Raise x to the power of (2^250 - 1). + * + * @param result The result. Must not overlap with x. + * @param x The argument. + */ + private void pow250(int[] result, int[] x) + { + int i, j; + + // The big-endian hexadecimal expansion of (2^250 - 1) is: + // 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF + // + // The naive implementation needs to do 2 multiplications per 1 bit and + // 1 multiplication per 0 bit. We can improve upon this by creating a + // pattern 0000000001 ... 0000000001. If we square and multiply the + // pattern by itself we can turn the pattern into the partial results + // 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc. + // This averages out to about 1.1 multiplications per 1 bit instead of 2. + + // Build a pattern of 250 bits in length of repeated copies of 0000000001. + square(A, x); + for (j = 0; j < 9; ++j) + square(A, A); + mul(result, A, x); + for (i = 0; i < 23; ++i) { + for (j = 0; j < 10; ++j) + square(A, A); + mul(result, result, A); + } + + // Multiply bit-shifted versions of the 0000000001 pattern into + // the result to "fill in" the gaps in the pattern. + square(A, result); + mul(result, result, A); + for (j = 0; j < 8; ++j) { + square(A, A); + mul(result, result, A); + } + } + + /** + * Computes the reciprocal of a number modulo 2^255 - 19. + * + * @param result The result. Must not overlap with x. + * @param x The argument. + */ + private void recip(int[] result, int[] x) + { + // The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19. + // The big-endian hexadecimal expansion of (p - 2) is: + // 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB + // Start with the 250 upper bits of the expansion of (p - 2). + pow250(result, x); + + // Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest. + square(result, result); + square(result, result); + mul(result, result, x); + square(result, result); + square(result, result); + mul(result, result, x); + square(result, result); + mul(result, result, x); + } + + /** + * Evaluates the curve for every bit in a secret key. + * + * @param s The 32-byte secret key. + */ + private void evalCurve(byte[] s) + { + int sposn = 31; + int sbit = 6; + int svalue = s[sposn] | 0x40; + int swap = 0; + int select; + + // Iterate over all 255 bits of "s" from the highest to the lowest. + // We ignore the high bit of the 256-bit representation of "s". + for (;;) { + // Conditional swaps on entry to this bit but only if we + // didn't swap on the previous bit. + select = (svalue >> sbit) & 0x01; + swap ^= select; + cswap(swap, x_2, x_3); + cswap(swap, z_2, z_3); + swap = select; + + // Evaluate the curve. + add(A, x_2, z_2); // A = x_2 + z_2 + square(AA, A); // AA = A^2 + sub(B, x_2, z_2); // B = x_2 - z_2 + square(BB, B); // BB = B^2 + sub(E, AA, BB); // E = AA - BB + add(C, x_3, z_3); // C = x_3 + z_3 + sub(D, x_3, z_3); // D = x_3 - z_3 + mul(DA, D, A); // DA = D * A + mul(CB, C, B); // CB = C * B + add(x_3, DA, CB); // x_3 = (DA + CB)^2 + square(x_3, x_3); + sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2 + square(z_3, z_3); + mul(z_3, z_3, x_1); + mul(x_2, AA, BB); // x_2 = AA * BB + mulA24(z_2, E); // z_2 = E * (AA + a24 * E) + add(z_2, z_2, AA); + mul(z_2, z_2, E); + + // Move onto the next lower bit of "s". + if (sbit > 0) { + --sbit; + } else if (sposn == 0) { + break; + } else if (sposn == 1) { + --sposn; + svalue = s[sposn] & 0xF8; + sbit = 7; + } else { + --sposn; + svalue = s[sposn]; + sbit = 7; + } + } + + // Final conditional swaps. + cswap(swap, x_2, x_3); + cswap(swap, z_2, z_3); + } + + /** + * Evaluates the Curve25519 curve. + * + * @param result Buffer to place the result of the evaluation into. + * @param offset Offset into the result buffer. + * @param privateKey The private key to use in the evaluation. + * @param publicKey The public key to use in the evaluation, or null + * if the base point of the curve should be used. + */ + public static void eval(byte[] result, int offset, byte[] privateKey, byte[] publicKey) + { + Curve25519 state = new Curve25519(); + try { + // Unpack the public key value. If null, use 9 as the base point. + Arrays.fill(state.x_1, 0); + if (publicKey != null) { + // Convert the input value from little-endian into 26-bit limbs. + for (int index = 0; index < 32; ++index) { + int bit = (index * 8) % 26; + int word = (index * 8) / 26; + int value = publicKey[index] & 0xFF; + if (bit <= (26 - 8)) { + state.x_1[word] |= value << bit; + } else { + state.x_1[word] |= value << bit; + state.x_1[word] &= 0x03FFFFFF; + state.x_1[word + 1] |= value >> (26 - bit); + } + } + + // Just in case, we reduce the number modulo 2^255 - 19 to + // make sure that it is in range of the field before we start. + // This eliminates values between 2^255 - 19 and 2^256 - 1. + state.reduceQuick(state.x_1); + state.reduceQuick(state.x_1); + } else { + state.x_1[0] = 9; + } + + // Initialize the other temporary variables. + Arrays.fill(state.x_2, 0); // x_2 = 1 + state.x_2[0] = 1; + Arrays.fill(state.z_2, 0); // z_2 = 0 + System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1 + Arrays.fill(state.z_3, 0); // z_3 = 1 + state.z_3[0] = 1; + + // Evaluate the curve for every bit of the private key. + state.evalCurve(privateKey); + + // Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19. + state.recip(state.z_3, state.z_2); + state.mul(state.x_2, state.x_2, state.z_3); + + // Convert x_2 into little-endian in the result buffer. + for (int index = 0; index < 32; ++index) { + int bit = (index * 8) % 26; + int word = (index * 8) / 26; + if (bit <= (26 - 8)) + result[offset + index] = (byte)(state.x_2[word] >> bit); + else + result[offset + index] = (byte)((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit))); + } + } finally { + // Clean up all temporary state before we exit. + state.destroy(); + } + } +} diff --git a/router/java/src/com/southernstorm/noise/crypto/Poly1305.java b/router/java/src/com/southernstorm/noise/crypto/Poly1305.java new file mode 100644 index 000000000..6a2a6c3ee --- /dev/null +++ b/router/java/src/com/southernstorm/noise/crypto/Poly1305.java @@ -0,0 +1,327 @@ +/* + * 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.crypto; + +import java.util.Arrays; + +import com.southernstorm.noise.protocol.Destroyable; + +/** + * Simple implementation of the Poly1305 message authenticator. + */ +public final class Poly1305 implements Destroyable { + + // The 130-bit intermediate values are broken up into five 26-bit words. + private byte[] nonce; + private byte[] block; + private int[] h; + private int[] r; + private int[] c; + private long[] t; + private int posn; + + /** + * Constructs a new Poly1305 message authenticator. + */ + public Poly1305() + { + nonce = new byte [16]; + block = new byte [16]; + h = new int [5]; + r = new int [5]; + c = new int [5]; + t = new long [10]; + posn = 0; + } + + /** + * Resets the message authenticator with a new key. + * + * @param key The buffer containing the 32 byte key. + * @param offset The offset into the buffer of the first key byte. + */ + public void reset(byte[] key, int offset) + { + System.arraycopy(key, offset + 16, nonce, 0, 16); + Arrays.fill(h, 0); + posn = 0; + + // Convert the first 16 bytes of the key into a 130-bit + // "r" value while masking off the bits that we don't need. + r[0] = ((key[offset] & 0xFF)) | + ((key[offset + 1] & 0xFF) << 8) | + ((key[offset + 2] & 0xFF) << 16) | + ((key[offset + 3] & 0x03) << 24); + r[1] = ((key[offset + 3] & 0x0C) >> 2) | + ((key[offset + 4] & 0xFC) << 6) | + ((key[offset + 5] & 0xFF) << 14) | + ((key[offset + 6] & 0x0F) << 22); + r[2] = ((key[offset + 6] & 0xF0) >> 4) | + ((key[offset + 7] & 0x0F) << 4) | + ((key[offset + 8] & 0xFC) << 12) | + ((key[offset + 9] & 0x3F) << 20); + r[3] = ((key[offset + 9] & 0xC0) >> 6) | + ((key[offset + 10] & 0xFF) << 2) | + ((key[offset + 11] & 0x0F) << 10) | + ((key[offset + 12] & 0xFC) << 18); + r[4] = ((key[offset + 13] & 0xFF)) | + ((key[offset + 14] & 0xFF) << 8) | + ((key[offset + 15] & 0x0F) << 16); + } + + /** + * Updates the message authenticator with more input data. + * + * @param data The buffer containing the input data. + * @param offset The offset of the first byte of input. + * @param length The number of bytes of input. + */ + public void update(byte[] data, int offset, int length) + { + while (length > 0) { + if (posn == 0 && length >= 16) { + // We can process the chunk directly out of the input buffer. + processChunk(data, offset, false); + offset += 16; + length -= 16; + } else { + // Collect up partial bytes in the block buffer. + int temp = 16 - posn; + if (temp > length) + temp = length; + System.arraycopy(data, offset, block, posn, temp); + offset += temp; + length -= temp; + posn += temp; + if (posn >= 16) { + processChunk(block, 0, false); + posn = 0; + } + } + } + } + + /** + * Pads the input with zeroes to a multiple of 16 bytes. + */ + public void pad() + { + if (posn != 0) { + Arrays.fill(block, posn, 16, (byte)0); + processChunk(block, 0, false); + posn = 0; + } + } + + /** + * Finishes the message authenticator and returns the 16-byte token. + * + * @param token The buffer to receive the token. + * @param offset The offset of the token in the buffer. + */ + public void finish(byte[] token, int offset) + { + // Pad and flush the final chunk. + if (posn != 0) { + block[posn] = (byte)1; + Arrays.fill(block, posn + 1, 16, (byte)0); + processChunk(block, 0, true); + } + + // At this point, processChunk() has left h as a partially reduced + // result that is less than (2^130 - 5) * 6. Perform one more + // reduction and a trial subtraction to produce the final result. + + // Multiply the high bits of h by 5 and add them to the 130 low bits. + int carry = (h[4] >> 26) * 5 + h[0]; + h[0] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[1]; + h[1] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[2]; + h[2] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[3]; + h[3] = carry & 0x03FFFFFF; + h[4] = (carry >> 26) + (h[4] & 0x03FFFFFF); + + // Subtract (2^130 - 5) from h by computing c = h + 5 - 2^130. + // The "minus 2^130" step is implicit. + carry = 5 + h[0]; + c[0] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[1]; + c[1] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[2]; + c[2] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[3]; + c[3] = carry & 0x03FFFFFF; + c[4] = (carry >> 26) + h[4]; + + // Borrow occurs if bit 2^130 of the previous c result is zero. + // Carefully turn this into a selection mask so we can select either + // h or c as the final result. + int mask = -((c[4] >> 26) & 0x01); + int nmask = ~mask; + h[0] = (h[0] & nmask) | (c[0] & mask); + h[1] = (h[1] & nmask) | (c[1] & mask); + h[2] = (h[2] & nmask) | (c[2] & mask); + h[3] = (h[3] & nmask) | (c[3] & mask); + h[4] = (h[4] & nmask) | (c[4] & mask); + + // Convert h into little-endian in the block buffer. + block[0] = (byte)(h[0]); + block[1] = (byte)(h[0] >> 8); + block[2] = (byte)(h[0] >> 16); + block[3] = (byte)((h[0] >> 24) | (h[1] << 2)); + block[4] = (byte)(h[1] >> 6); + block[5] = (byte)(h[1] >> 14); + block[6] = (byte)((h[1] >> 22) | (h[2] << 4)); + block[7] = (byte)(h[2] >> 4); + block[8] = (byte)(h[2] >> 12); + block[9] = (byte)((h[2] >> 20) | (h[3] << 6)); + block[10] = (byte)(h[3] >> 2); + block[11] = (byte)(h[3] >> 10); + block[12] = (byte)(h[3] >> 18); + block[13] = (byte)(h[4]); + block[14] = (byte)(h[4] >> 8); + block[15] = (byte)(h[4] >> 16); + + // Add the nonce and write the final result to the token. + carry = (nonce[0] & 0xFF) + (block[0] & 0xFF); + token[offset] = (byte)carry; + for (int x = 1; x < 16; ++x) { + carry = (carry >> 8) + (nonce[x] & 0xFF) + (block[x] & 0xFF); + token[offset + x] = (byte)carry; + } + } + + /** + * Processes the next chunk of input data. + * + * @param chunk Buffer containing the input data chunk. + * @param offset Offset of the first byte of the 16-byte chunk. + * @param finalChunk Set to true if this is the final chunk. + */ + private void processChunk(byte[] chunk, int offset, boolean finalChunk) + { + int x; + + // Unpack the 128-bit chunk into a 130-bit value in "c". + c[0] = ((chunk[offset] & 0xFF)) | + ((chunk[offset + 1] & 0xFF) << 8) | + ((chunk[offset + 2] & 0xFF) << 16) | + ((chunk[offset + 3] & 0x03) << 24); + c[1] = ((chunk[offset + 3] & 0xFC) >> 2) | + ((chunk[offset + 4] & 0xFF) << 6) | + ((chunk[offset + 5] & 0xFF) << 14) | + ((chunk[offset + 6] & 0x0F) << 22); + c[2] = ((chunk[offset + 6] & 0xF0) >> 4) | + ((chunk[offset + 7] & 0xFF) << 4) | + ((chunk[offset + 8] & 0xFF) << 12) | + ((chunk[offset + 9] & 0x3F) << 20); + c[3] = ((chunk[offset + 9] & 0xC0) >> 6) | + ((chunk[offset + 10] & 0xFF) << 2) | + ((chunk[offset + 11] & 0xFF) << 10) | + ((chunk[offset + 12] & 0xFF) << 18); + c[4] = ((chunk[offset + 13] & 0xFF)) | + ((chunk[offset + 14] & 0xFF) << 8) | + ((chunk[offset + 15] & 0xFF) << 16); + if (!finalChunk) + c[4] |= (1 << 24); + + // Compute h = ((h + c) * r) mod (2^130 - 5) + + // Start with h += c. We assume that h is less than (2^130 - 5) * 6 + // and that c is less than 2^129, so the result will be less than 2^133. + h[0] += c[0]; + h[1] += c[1]; + h[2] += c[2]; + h[3] += c[3]; + h[4] += c[4]; + + // Multiply h by r. We know that r is less than 2^124 because the + // top 4 bits were AND-ed off by reset(). That makes h * r less + // than 2^257. Which is less than the (2^130 - 6)^2 we want for + // the modulo reduction step that follows. The intermediate limbs + // are 52 bits in size, which allows us to collect up carries in the + // extra bits of the 64 bit longs and propagate them later. + long hv = h[0]; + t[0] = hv * r[0]; + t[1] = hv * r[1]; + t[2] = hv * r[2]; + t[3] = hv * r[3]; + t[4] = hv * r[4]; + for (x = 1; x < 5; ++x) { + hv = h[x]; + t[x] += hv * r[0]; + t[x + 1] += hv * r[1]; + t[x + 2] += hv * r[2]; + t[x + 3] += hv * r[3]; + t[x + 4] = hv * r[4]; + } + + // Propagate carries to convert the t limbs from 52-bit back to 26-bit. + // The low bits are placed into h and the high bits are placed into c. + h[0] = ((int)t[0]) & 0x03FFFFFF; + hv = t[1] + (t[0] >> 26); + h[1] = ((int)hv) & 0x03FFFFFF; + hv = t[2] + (hv >> 26); + h[2] = ((int)hv) & 0x03FFFFFF; + hv = t[3] + (hv >> 26); + h[3] = ((int)hv) & 0x03FFFFFF; + hv = t[4] + (hv >> 26); + h[4] = ((int)hv) & 0x03FFFFFF; + hv = t[5] + (hv >> 26); + c[0] = ((int)hv) & 0x03FFFFFF; + hv = t[6] + (hv >> 26); + c[1] = ((int)hv) & 0x03FFFFFF; + hv = t[7] + (hv >> 26); + c[2] = ((int)hv) & 0x03FFFFFF; + hv = t[8] + (hv >> 26); + c[3] = ((int)hv) & 0x03FFFFFF; + hv = t[9] + (hv >> 26); + c[4] = ((int)hv); + + // Reduce h * r modulo (2^130 - 5) by multiplying the high 130 bits by 5 + // and adding them to the low 130 bits. This will leave the result at + // most 5 subtractions away from the answer we want. + int carry = h[0] + c[0] * 5; + h[0] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[1] + c[1] * 5; + h[1] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[2] + c[2] * 5; + h[2] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[3] + c[3] * 5; + h[3] = carry & 0x03FFFFFF; + carry = (carry >> 26) + h[4] + c[4] * 5; + h[4] = carry; + } + + @Override + public void destroy() { + Arrays.fill(nonce, (byte)0); + Arrays.fill(block, (byte)0); + Arrays.fill(h, (int)0); + Arrays.fill(r, (int)0); + Arrays.fill(c, (int)0); + Arrays.fill(t, (long)0); + } +} diff --git a/router/java/src/com/southernstorm/noise/crypto/package-info.java b/router/java/src/com/southernstorm/noise/crypto/package-info.java new file mode 100644 index 000000000..8552009a1 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/crypto/package-info.java @@ -0,0 +1,12 @@ + +/** + * Fallback implementations of cryptographic primitives. + * + * This package provides plain Java implementations of the + * cryptographic primitives that Noise requires which do not + * normally come with standard JDK's. + * + * Applications that use Noise won't normally use these classes + * directly. + */ +package com.southernstorm.noise.crypto; diff --git a/router/java/src/com/southernstorm/noise/protocol/ChaChaPolyCipherState.java b/router/java/src/com/southernstorm/noise/protocol/ChaChaPolyCipherState.java new file mode 100644 index 000000000..25b1a4c0a --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/ChaChaPolyCipherState.java @@ -0,0 +1,290 @@ +/* + * 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.protocol; + +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.ShortBufferException; + +import com.southernstorm.noise.crypto.ChaChaCore; +import com.southernstorm.noise.crypto.Poly1305; + +/** + * Implements the ChaChaPoly cipher for Noise. + */ +class ChaChaPolyCipherState implements CipherState { + + private Poly1305 poly; + private int[] input; + private int[] output; + private byte[] polyKey; + long n; + private boolean haskey; + + /** + * Constructs a new cipher state for the "ChaChaPoly" algorithm. + */ + public ChaChaPolyCipherState() + { + poly = new Poly1305(); + input = new int [16]; + output = new int [16]; + polyKey = new byte [32]; + n = 0; + haskey = false; + } + + @Override + public void destroy() { + poly.destroy(); + Arrays.fill(input, 0); + Arrays.fill(output, 0); + Noise.destroy(polyKey); + } + + @Override + public String getCipherName() { + return "ChaChaPoly"; + } + + @Override + public int getKeyLength() { + return 32; + } + + @Override + public int getMACLength() { + return haskey ? 16 : 0; + } + + @Override + public void initializeKey(byte[] key, int offset) { + ChaChaCore.initKey256(input, key, offset); + n = 0; + haskey = true; + } + + @Override + public boolean hasKey() { + return haskey; + } + + /** + * XOR's the output of ChaCha20 with a byte buffer. + * + * @param input The input byte buffer. + * @param inputOffset The offset of the first input byte. + * @param output The output byte buffer (can be the same as the input). + * @param outputOffset The offset of the first output byte. + * @param length The number of bytes to XOR between 1 and 64. + * @param block The ChaCha20 output block. + */ + private static void xorBlock(byte[] input, int inputOffset, byte[] output, int outputOffset, int length, int[] block) + { + int posn = 0; + int value; + while (length >= 4) { + value = block[posn++]; + output[outputOffset] = (byte)(input[inputOffset] ^ value); + output[outputOffset + 1] = (byte)(input[inputOffset + 1] ^ (value >> 8)); + output[outputOffset + 2] = (byte)(input[inputOffset + 2] ^ (value >> 16)); + output[outputOffset + 3] = (byte)(input[inputOffset + 3] ^ (value >> 24)); + inputOffset += 4; + outputOffset += 4; + length -= 4; + } + if (length == 3) { + value = block[posn]; + output[outputOffset] = (byte)(input[inputOffset] ^ value); + output[outputOffset + 1] = (byte)(input[inputOffset + 1] ^ (value >> 8)); + output[outputOffset + 2] = (byte)(input[inputOffset + 2] ^ (value >> 16)); + } else if (length == 2) { + value = block[posn]; + output[outputOffset] = (byte)(input[inputOffset] ^ value); + output[outputOffset + 1] = (byte)(input[inputOffset + 1] ^ (value >> 8)); + } else if (length == 1) { + value = block[posn]; + output[outputOffset] = (byte)(input[inputOffset] ^ value); + } + } + + /** + * Set up to encrypt or decrypt the next packet. + * + * @param ad The associated data for the packet. + */ + private void setup(byte[] ad) + { + if (n == -1L) + throw new IllegalStateException("Nonce has wrapped around"); + ChaChaCore.initIV(input, n++); + ChaChaCore.hash(output, input); + Arrays.fill(polyKey, (byte)0); + xorBlock(polyKey, 0, polyKey, 0, 32, output); + poly.reset(polyKey, 0); + if (ad != null) { + poly.update(ad, 0, ad.length); + poly.pad(); + } + if (++(input[12]) == 0) + ++(input[13]); + } + + /** + * Puts a 64-bit integer into a buffer in little-endian order. + * + * @param output The output buffer. + * @param offset The offset into the output buffer. + * @param value The 64-bit integer value. + */ + private static void putLittleEndian64(byte[] output, int offset, long value) + { + output[offset] = (byte)value; + output[offset + 1] = (byte)(value >> 8); + output[offset + 2] = (byte)(value >> 16); + output[offset + 3] = (byte)(value >> 24); + output[offset + 4] = (byte)(value >> 32); + output[offset + 5] = (byte)(value >> 40); + output[offset + 6] = (byte)(value >> 48); + output[offset + 7] = (byte)(value >> 56); + } + + /** + * Finishes up the authentication tag for a packet. + * + * @param ad The associated data. + * @param length The length of the plaintext data. + */ + private void finish(byte[] ad, int length) + { + poly.pad(); + putLittleEndian64(polyKey, 0, ad != null ? ad.length : 0); + putLittleEndian64(polyKey, 8, length); + poly.update(polyKey, 0, 16); + poly.finish(polyKey, 0); + } + + /** + * Encrypts or decrypts a buffer of bytes for the active packet. + * + * @param plaintext The plaintext data to be encrypted. + * @param plaintextOffset The offset to the first plaintext byte. + * @param ciphertext The ciphertext data that results from encryption. + * @param ciphertextOffset The offset to the first ciphertext byte. + * @param length The number of bytes to encrypt. + */ + private void encrypt(byte[] plaintext, int plaintextOffset, + byte[] ciphertext, int ciphertextOffset, int length) { + while (length > 0) { + int tempLen = 64; + if (tempLen > length) + tempLen = length; + ChaChaCore.hash(output, input); + xorBlock(plaintext, plaintextOffset, ciphertext, ciphertextOffset, tempLen, output); + if (++(input[12]) == 0) + ++(input[13]); + plaintextOffset += tempLen; + ciphertextOffset += tempLen; + length -= tempLen; + } + } + + @Override + public int encryptWithAd(byte[] ad, byte[] plaintext, int plaintextOffset, + byte[] ciphertext, int ciphertextOffset, int length) throws ShortBufferException { + int space; + if (ciphertextOffset > ciphertext.length) + space = 0; + else + space = ciphertext.length - ciphertextOffset; + if (!haskey) { + // The key is not set yet - return the plaintext as-is. + if (length > space) + throw new ShortBufferException(); + if (plaintext != ciphertext || plaintextOffset != ciphertextOffset) + System.arraycopy(plaintext, plaintextOffset, ciphertext, ciphertextOffset, length); + return length; + } + if (space < 16 || length > (space - 16)) + throw new ShortBufferException(); + setup(ad); + encrypt(plaintext, plaintextOffset, ciphertext, ciphertextOffset, length); + poly.update(ciphertext, ciphertextOffset, length); + finish(ad, length); + System.arraycopy(polyKey, 0, ciphertext, ciphertextOffset + length, 16); + return length + 16; + } + + @Override + public int decryptWithAd(byte[] ad, byte[] ciphertext, + int ciphertextOffset, byte[] plaintext, int plaintextOffset, + int length) throws ShortBufferException, BadPaddingException { + int space; + if (ciphertextOffset > ciphertext.length) + space = 0; + else + space = ciphertext.length - ciphertextOffset; + if (length > space) + throw new ShortBufferException(); + if (plaintextOffset > plaintext.length) + space = 0; + else + space = plaintext.length - plaintextOffset; + if (!haskey) { + // The key is not set yet - return the ciphertext as-is. + if (length > space) + throw new ShortBufferException(); + if (plaintext != ciphertext || plaintextOffset != ciphertextOffset) + System.arraycopy(ciphertext, ciphertextOffset, plaintext, plaintextOffset, length); + return length; + } + if (length < 16) + Noise.throwBadTagException(); + int dataLen = length - 16; + if (dataLen > space) + throw new ShortBufferException(); + setup(ad); + poly.update(ciphertext, ciphertextOffset, dataLen); + finish(ad, dataLen); + int temp = 0; + for (int index = 0; index < 16; ++index) + temp |= (polyKey[index] ^ ciphertext[ciphertextOffset + dataLen + index]); + if ((temp & 0xFF) != 0) + Noise.throwBadTagException(); + encrypt(ciphertext, ciphertextOffset, plaintext, plaintextOffset, dataLen); + return dataLen; + } + + @Override + public CipherState fork(byte[] key, int offset) { + CipherState cipher = new ChaChaPolyCipherState(); + cipher.initializeKey(key, offset); + return cipher; + } + + @Override + public void setNonce(long nonce) { + n = nonce; + } +} diff --git a/router/java/src/com/southernstorm/noise/protocol/CipherState.java b/router/java/src/com/southernstorm/noise/protocol/CipherState.java new file mode 100644 index 000000000..3168139f4 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/CipherState.java @@ -0,0 +1,158 @@ +/* + * 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.protocol; + +import javax.crypto.BadPaddingException; +import javax.crypto.ShortBufferException; + +/** + * Interface to an authenticated cipher for use in the Noise protocol. + * + * CipherState objects are used to encrypt or decrypt data during a + * session. Once the handshake has completed, HandshakeState.split() + * will create two CipherState objects for encrypting packets sent to + * the other party, and decrypting packets received from the other party. + */ +public interface CipherState extends Destroyable { + + /** + * Gets the Noise protocol name for this cipher. + * + * @return The cipher name. + */ + String getCipherName(); + + /** + * Gets the length of the key values for this cipher. + * + * @return The length of the key in bytes; usually 32. + */ + int getKeyLength(); + + /** + * Gets the length of the MAC values for this cipher. + * + * @return The length of MAC values in bytes, or zero if the + * key has not yet been initialized. + */ + int getMACLength(); + + /** + * Initializes the key on this cipher object. + * + * @param key Points to a buffer that contains the key. + * @param offset The offset of the key in the key buffer. + * + * The key buffer must contain at least getKeyLength() bytes + * starting at offset. + * + * @see #hasKey() + */ + void initializeKey(byte[] key, int offset); + + /** + * Determine if this cipher object has been configured with a key. + * + * @return true if this cipher object has a key; false if the + * key has not yet been set with initializeKey(). + * + * @see #initializeKey(byte[], int) + */ + boolean hasKey(); + + /** + * Encrypts a plaintext buffer using the cipher and a block of associated data. + * + * @param ad The associated data, or null if there is none. + * @param plaintext The buffer containing the plaintext to encrypt. + * @param plaintextOffset The offset within the plaintext buffer of the + * first byte or plaintext data. + * @param ciphertext The buffer to place the ciphertext in. This can + * be the same as the plaintext buffer. + * @param ciphertextOffset The first offset within the ciphertext buffer + * to place the ciphertext and the MAC tag. + * @param length The length of the plaintext. + * @return The length of the ciphertext plus the MAC tag, or -1 if the + * ciphertext buffer is not large enough to hold the result. + * + * @throws ShortBufferException The ciphertext buffer does not have + * enough space to hold the ciphertext plus MAC. + * + * @throws IllegalStateException The nonce has wrapped around. + * + * The plaintext and ciphertext buffers can be the same for in-place + * encryption. In that case, plaintextOffset must be identical to + * ciphertextOffset. + * + * There must be enough space in the ciphertext buffer to accomodate + * length + getMACLength() bytes of data starting at ciphertextOffset. + */ + int encryptWithAd(byte[] ad, byte[] plaintext, int plaintextOffset, byte[] ciphertext, int ciphertextOffset, int length) throws ShortBufferException; + + /** + * Decrypts a ciphertext buffer using the cipher and a block of associated data. + * + * @param ad The associated data, or null if there is none. + * @param ciphertext The buffer containing the ciphertext to decrypt. + * @param ciphertextOffset The offset within the ciphertext buffer of + * the first byte of ciphertext data. + * @param plaintext The buffer to place the plaintext in. This can be + * the same as the ciphertext buffer. + * @param plaintextOffset The first offset within the plaintext buffer + * to place the plaintext. + * @param length The length of the incoming ciphertext plus the MAC tag. + * @return The length of the plaintext with the MAC tag stripped off. + * + * @throws ShortBufferException The plaintext buffer does not have + * enough space to store the decrypted data. + * + * @throws BadPaddingException The MAC value failed to verify. + * + * @throws IllegalStateException The nonce has wrapped around. + * + * The plaintext and ciphertext buffers can be the same for in-place + * decryption. In that case, ciphertextOffset must be identical to + * plaintextOffset. + */ + int decryptWithAd(byte[] ad, byte[] ciphertext, int ciphertextOffset, byte[] plaintext, int plaintextOffset, int length) throws ShortBufferException, BadPaddingException; + + /** + * Creates a new instance of this cipher and initializes it with a key. + * + * @param key The buffer containing the key. + * @param offset The offset into the key buffer of the first key byte. + * @return A new CipherState of the same class as this one. + */ + CipherState fork(byte[] key, int offset); + + /** + * Sets the nonce value. + * + * @param nonce The new nonce value, which must be greater than or equal + * to the current value. + * + * This function is intended for testing purposes only. If the nonce + * value goes backwards then security may be compromised. + */ + void setNonce(long nonce); +} diff --git a/router/java/src/com/southernstorm/noise/protocol/CipherStatePair.java b/router/java/src/com/southernstorm/noise/protocol/CipherStatePair.java new file mode 100644 index 000000000..8ec07f29b --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/CipherStatePair.java @@ -0,0 +1,106 @@ +/* + * 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.protocol; + +/** + * Class that contains a pair of CipherState objects. + * + * CipherState pairs typically arise when HandshakeState.split() is called. + */ +public final class CipherStatePair implements Destroyable { + + private CipherState send; + private CipherState recv; + + /** + * Constructs a pair of CipherState objects. + * + * @param sender The CipherState to use to send packets to the remote party. + * @param receiver The CipherState to use to receive packets from the remote party. + */ + public CipherStatePair(CipherState sender, CipherState receiver) + { + send = sender; + recv = receiver; + } + + /** + * Gets the CipherState to use to send packets to the remote party. + * + * @return The sending CipherState. + */ + public CipherState getSender() { + return send; + } + + /** + * Gets the CipherState to use to receive packets from the remote party. + * + * @return The receiving CipherState. + */ + public CipherState getReceiver() { + return recv; + } + + /** + * Destroys the receiving CipherState and retains only the sending CipherState. + * + * This function is intended for use with one-way handshake patterns. + */ + public void senderOnly() + { + if (recv != null) { + recv.destroy(); + recv = null; + } + } + + /** + * Destroys the sending CipherState and retains only the receiving CipherState. + * + * This function is intended for use with one-way handshake patterns. + */ + public void receiverOnly() + { + if (send != null) { + send.destroy(); + send = null; + } + } + + /** + * Swaps the sender and receiver. + */ + public void swap() + { + CipherState temp = send; + send = recv; + recv = temp; + } + + @Override + public void destroy() { + senderOnly(); + receiverOnly(); + } +} diff --git a/router/java/src/com/southernstorm/noise/protocol/Curve25519DHState.java b/router/java/src/com/southernstorm/noise/protocol/Curve25519DHState.java new file mode 100644 index 000000000..771faa2f7 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/Curve25519DHState.java @@ -0,0 +1,156 @@ +/* + * 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.protocol; + +import java.util.Arrays; + +import com.southernstorm.noise.crypto.Curve25519; + +/** + * Implementation of the Curve25519 algorithm for the Noise protocol. + */ +class Curve25519DHState implements DHState { + + private byte[] publicKey; + private byte[] privateKey; + private int mode; + + /** + * Constructs a new Diffie-Hellman object for Curve25519. + */ + public Curve25519DHState() + { + publicKey = new byte [32]; + privateKey = new byte [32]; + mode = 0; + } + + @Override + public void destroy() { + clearKey(); + } + + @Override + public String getDHName() { + return "25519"; + } + + @Override + public int getPublicKeyLength() { + return 32; + } + + @Override + public int getPrivateKeyLength() { + return 32; + } + + @Override + public int getSharedKeyLength() { + return 32; + } + + @Override + public void generateKeyPair() { + Noise.random(privateKey); + Curve25519.eval(publicKey, 0, privateKey, null); + mode = 0x03; + } + + @Override + public void getPublicKey(byte[] key, int offset) { + System.arraycopy(publicKey, 0, key, offset, 32); + } + + @Override + public void setPublicKey(byte[] key, int offset) { + System.arraycopy(key, offset, publicKey, 0, 32); + Arrays.fill(privateKey, (byte)0); + mode = 0x01; + } + + @Override + public void getPrivateKey(byte[] key, int offset) { + System.arraycopy(privateKey, 0, key, offset, 32); + } + + @Override + public void setPrivateKey(byte[] key, int offset) { + System.arraycopy(key, offset, privateKey, 0, 32); + Curve25519.eval(publicKey, 0, privateKey, null); + mode = 0x03; + } + + @Override + public void setToNullPublicKey() { + Arrays.fill(publicKey, (byte)0); + Arrays.fill(privateKey, (byte)0); + mode = 0x01; + } + + @Override + public void clearKey() { + Noise.destroy(publicKey); + Noise.destroy(privateKey); + mode = 0; + } + + @Override + public boolean hasPublicKey() { + return (mode & 0x01) != 0; + } + + @Override + public boolean hasPrivateKey() { + return (mode & 0x02) != 0; + } + + @Override + public boolean isNullPublicKey() { + if ((mode & 0x01) == 0) + return false; + int temp = 0; + for (int index = 0; index < 32; ++index) + temp |= publicKey[index]; + return temp == 0; + } + + @Override + public void calculate(byte[] sharedKey, int offset, DHState publicDH) { + if (!(publicDH instanceof Curve25519DHState)) + throw new IllegalArgumentException("Incompatible DH algorithms"); + Curve25519.eval(sharedKey, offset, privateKey, ((Curve25519DHState)publicDH).publicKey); + } + + @Override + public void copyFrom(DHState other) { + if (!(other instanceof Curve25519DHState)) + throw new IllegalStateException("Mismatched DH key objects"); + if (other == this) + return; + Curve25519DHState dh = (Curve25519DHState)other; + System.arraycopy(dh.privateKey, 0, privateKey, 0, 32); + System.arraycopy(dh.publicKey, 0, publicKey, 0, 32); + mode = dh.mode; + } +} diff --git a/router/java/src/com/southernstorm/noise/protocol/DHState.java b/router/java/src/com/southernstorm/noise/protocol/DHState.java new file mode 100644 index 000000000..6e997c602 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/DHState.java @@ -0,0 +1,156 @@ +/* + * 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.protocol; + +/** + * Interface to a Diffie-Hellman algorithm for the Noise protocol. + */ +public interface DHState extends Destroyable { + + /** + * Gets the Noise protocol name for this Diffie-Hellman algorithm. + * + * @return The algorithm name. + */ + String getDHName(); + + /** + * Gets the length of public keys for this algorithm. + * + * @return The length of public keys in bytes. + */ + int getPublicKeyLength(); + + /** + * Gets the length of private keys for this algorithm. + * + * @return The length of private keys in bytes. + */ + int getPrivateKeyLength(); + + /** + * Gets the length of shared keys for this algorithm. + * + * @return The length of shared keys in bytes. + */ + int getSharedKeyLength(); + + /** + * Generates a new random keypair. + */ + void generateKeyPair(); + + /** + * Gets the public key associated with this object. + * + * @param key The buffer to copy the public key to. + * @param offset The first offset in the key buffer to copy to. + */ + void getPublicKey(byte[] key, int offset); + + /** + * Sets the public key for this object. + * + * @param key The buffer containing the public key. + * @param offset The first offset in the buffer that contains the key. + * + * If this object previously held a key pair, then this function + * will change it into a public key only object. + */ + void setPublicKey(byte[] key, int offset); + + /** + * Gets the private key associated with this object. + * + * @param key The buffer to copy the private key to. + * @param offset The first offset in the key buffer to copy to. + */ + void getPrivateKey(byte[] key, int offset); + + /** + * Sets the private key for this object. + * + * @param key The buffer containing the [rivate key. + * @param offset The first offset in the buffer that contains the key. + * + * If this object previously held only a public key, then + * this function will change it into a key pair. + */ + void setPrivateKey(byte[] key, int offset); + + /** + * Sets this object to the null public key and clears the private key. + */ + void setToNullPublicKey(); + + /** + * Clears the key pair. + */ + void clearKey(); + + /** + * Determine if this object contains a public key. + * + * @return Returns true if this object contains a public key, + * or false if the public key has not yet been set. + */ + boolean hasPublicKey(); + + /** + * Determine if this object contains a private key. + * + * @return Returns true if this object contains a private key, + * or false if the private key has not yet been set. + */ + boolean hasPrivateKey(); + + /** + * Determine if the public key in this object is the special null value. + * + * @return Returns true if the public key is the special null value, + * or false otherwise. + */ + boolean isNullPublicKey(); + + /** + * Performs a Diffie-Hellman calculation with this object as the private key. + * + * @param sharedKey Buffer to put the shared key into. + * @param offset Offset of the first byte for the shared key. + * @param publicDH Object that contains the public key for the calculation. + * + * @throws IllegalArgumentException The publicDH object is not the same + * type as this object, or one of the objects does not contain a valid key. + */ + void calculate(byte[] sharedKey, int offset, DHState publicDH); + + /** + * Copies the key values from another DH object of the same type. + * + * @param other The other DH object to copy from + * + * @throws IllegalStateException The other DH object does not have + * the same type as this object. + */ + void copyFrom(DHState other); +} diff --git a/router/java/src/com/southernstorm/noise/protocol/Destroyable.java b/router/java/src/com/southernstorm/noise/protocol/Destroyable.java new file mode 100644 index 000000000..17785ed22 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/Destroyable.java @@ -0,0 +1,43 @@ +/* + * 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.protocol; + +/** + * Interface for objects that implement destroying. + * + * Applications that use the Noise protocol can inadvertently leave + * sensitive data in the heap if steps are not taken to clean up. + * + * This interface can be implemented by objects that know how to + * securely clean up after themselves. + * + * The Noise.destroy() function can help with destroying byte arrays + * that hold sensitive values. + */ +public interface Destroyable { + + /** + * Destroys all sensitive state in the current object. + */ + void destroy(); +} diff --git a/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java b/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java new file mode 100644 index 000000000..daa67b14d --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/HandshakeState.java @@ -0,0 +1,1260 @@ +/* + * 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.protocol; + +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.ShortBufferException; + +/** + * Interface to a Noise handshake. + */ +public class HandshakeState implements Destroyable { + + private SymmetricState symmetric; + private boolean isInitiator; + private DHState localKeyPair; + private DHState localEphemeral; + private DHState localHybrid; + private DHState remotePublicKey; + private DHState remoteEphemeral; + private DHState remoteHybrid; + private DHState fixedEphemeral; + private DHState fixedHybrid; + private int action; + private int requirements; + private short[] pattern; + private int patternIndex; + private byte[] preSharedKey; + private byte[] prologue; + + /** + * Enumerated value that indicates that the handshake object + * is handling the initiator role. + */ + public static final int INITIATOR = 1; + + /** + * Enumerated value that indicates that the handshake object + * is handling the responder role. + */ + public static final int RESPONDER = 2; + + /** + * No action is required of the application yet because the + * handshake has not started. + */ + public static final int NO_ACTION = 0; + + /** + * The HandshakeState expects the application to write the + * next message payload for the handshake. + */ + public static final int WRITE_MESSAGE = 1; + + /** + * The HandshakeState expects the application to read the + * next message payload from the handshake. + */ + public static final int READ_MESSAGE = 2; + + /** + * The handshake has failed due to some kind of error. + */ + public static final int FAILED = 3; + + /** + * The handshake is over and the application is expected to call + * split() and begin data session communications. + */ + public static final int SPLIT = 4; + + /** + * The handshake is complete and the data session ciphers + * have been split() out successfully. + */ + public static final int COMPLETE = 5; + + /** + * Local static keypair is required for the handshake. + */ + private static final int LOCAL_REQUIRED = 0x01; + + /** + * Remote static keypai is required for the handshake. + */ + private static final int REMOTE_REQUIRED = 0x02; + + /** + * Pre-shared key is required for the handshake. + */ + private static final int PSK_REQUIRED = 0x04; + + /** + * Ephemeral key for fallback pre-message has been provided. + */ + private static final int FALLBACK_PREMSG = 0x08; + + /** + * The local public key is part of the pre-message. + */ + private static final int LOCAL_PREMSG = 0x10; + + /** + * The remote public key is part of the pre-message. + */ + private static final int REMOTE_PREMSG = 0x20; + + /** + * Fallback is possible from this pattern (two-way, ends in "K"). + */ + private static final int FALLBACK_POSSIBLE = 0x40; + + /** + * Creates a new Noise handshake. + * + * @param protocolName The name of the Noise protocol. + * @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. + * + * @throws IllegalArgumentException The protocolName is not + * formatted correctly, or the role is not recognized. + * + * @throws NoSuchAlgorithmException One of the cryptographic algorithms + * that is specified in the protocolName is not supported. + */ + public HandshakeState(String protocolName, int role) throws NoSuchAlgorithmException + { + // Parse the protocol name into its components. + String[] components = protocolName.split("_"); + if (components.length != 5) + throw new IllegalArgumentException("Protocol name must have 5 components"); + String prefix = components[0]; + String patternId = components[1]; + String dh = components[2]; + String hybrid = null; + String cipher = components[3]; + String hash = components[4]; + if (!prefix.equals("Noise") && !prefix.equals("NoisePSK")) + throw new IllegalArgumentException("Prefix must be Noise or NoisePSK"); + pattern = Pattern.lookup(patternId); + if (pattern == null) + throw new IllegalArgumentException("Handshake pattern is not recognized"); + short flags = pattern[0]; + int extraReqs = 0; + if ((flags & Pattern.FLAG_REMOTE_REQUIRED) != 0 && patternId.length() > 1) + extraReqs |= FALLBACK_POSSIBLE; + if (role == RESPONDER) { + // Reverse the pattern flags so that the responder is "local". + flags = Pattern.reverseFlags(flags); + } + int index = dh.indexOf('+'); + if (index != -1) { + // The DH name has two components: regular and hybrid. + hybrid = dh.substring(index + 1); + dh = dh.substring(0, index); + if ((flags & Pattern.FLAG_LOCAL_HYBRID) == 0 || (flags & Pattern.FLAG_REMOTE_HYBRID) == 0) + throw new IllegalArgumentException("Hybrid function specified for non-hybrid pattern"); + } else { + if ((flags & Pattern.FLAG_LOCAL_HYBRID) != 0 || (flags & Pattern.FLAG_REMOTE_HYBRID) != 0) + throw new IllegalArgumentException("Hybrid function not specified for hybrid pattern"); + } + + // Check that the role is correctly specified. + if (role != INITIATOR && role != RESPONDER) + throw new IllegalArgumentException("Role must be initiator or responder"); + + // Initialize this object. This will also create the cipher and hash objects. + symmetric = new SymmetricState(protocolName, cipher, hash); + isInitiator = (role == INITIATOR); + action = NO_ACTION; + requirements = extraReqs | computeRequirements(flags, prefix, role, false); + patternIndex = 1; + + // Create the DH objects that we will need later. + if ((flags & Pattern.FLAG_LOCAL_STATIC) != 0) + localKeyPair = Noise.createDH(dh); + if ((flags & Pattern.FLAG_LOCAL_EPHEMERAL) != 0) + localEphemeral = Noise.createDH(dh); + if ((flags & Pattern.FLAG_LOCAL_HYBRID) != 0) + localHybrid = Noise.createDH(hybrid); + if ((flags & Pattern.FLAG_REMOTE_STATIC) != 0) + remotePublicKey = Noise.createDH(dh); + if ((flags & Pattern.FLAG_REMOTE_EPHEMERAL) != 0) + remoteEphemeral = Noise.createDH(dh); + if ((flags & Pattern.FLAG_REMOTE_HYBRID) != 0) + remoteHybrid = Noise.createDH(hybrid); + + // We cannot use hybrid algorithms like New Hope for ephemeral or static keys, + // as the unbalanced nature of the algorithm only works with "f" and "ff" tokens. + if (localKeyPair instanceof DHStateHybrid) + throw new NoSuchAlgorithmException("Cannot use '" + localKeyPair.getDHName() + "' for static keys"); + if (localEphemeral instanceof DHStateHybrid) + throw new NoSuchAlgorithmException("Cannot use '" + localEphemeral.getDHName() + "' for ephemeral keys"); + if (remotePublicKey instanceof DHStateHybrid) + throw new NoSuchAlgorithmException("Cannot use '" + remotePublicKey.getDHName() + "' for static keys"); + if (remoteEphemeral instanceof DHStateHybrid) + throw new NoSuchAlgorithmException("Cannot use '" + remoteEphemeral.getDHName() + "' for ephemeral keys"); + } + + /** + * Gets the name of the Noise protocol. + * + * @return The protocol name. + */ + public String getProtocolName() + { + return symmetric.getProtocolName(); + } + + /** + * Gets the role for this handshake. + * + * @return The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. + */ + public int getRole() + { + return isInitiator ? INITIATOR : RESPONDER; + } + + /** + * Determine if this handshake needs a pre-shared key value + * and one has not been configured yet. + * + * @return true if a pre-shared key is needed; false if not. + */ + public boolean needsPreSharedKey() + { + if (preSharedKey != null) + return false; + else + return (requirements & PSK_REQUIRED) != 0; + } + + /** + * Determine if this object has already been configured with a + * pre-shared key. + * + * @return true if the pre-shared key has already been configured; + * false if one is not needed or it has not been configured yet. + */ + public boolean hasPreSharedKey() + { + return preSharedKey != null; + } + + /** + * Sets the pre-shared key for this handshake. + * + * @param key Buffer containing the pre-shared key value. + * @param offset Offset into the buffer of the first byte of the key. + * @param length The length of the pre-shared key, which must be 32. + * + * @throws IllegalArgumentException The length is not 32. + * + * @throws UnsupportedOperationException Pre-shared keys are not + * supported for this handshake type. + * + * @throws IllegalStateException The handshake has already started, + * so the pre-shared key can no longer be set. + */ + public void setPreSharedKey(byte[] key, int offset, int length) + { + if (length != 32) { + throw new IllegalArgumentException + ("Pre-shared keys must be 32 bytes in length"); + } + if ((requirements & PSK_REQUIRED) == 0) { + throw new UnsupportedOperationException + ("Pre-shared keys are not supported for this handshake"); + } + if (action != NO_ACTION) { + throw new IllegalStateException + ("Handshake has already started; cannot set pre-shared key"); + } + if (preSharedKey != null) { + Noise.destroy(preSharedKey); + preSharedKey = null; + } + preSharedKey = Noise.copySubArray(key, offset, length); + } + + /** + * Sets the prologue for this handshake. + * + * @param prologue Buffer containing the prologue value. + * @param offset Offset into the buffer of the first byte of the prologue. + * @param length The length of the prologue in bytes. + * + * @throws IllegalStateException The handshake has already started, + * so the prologue can no longer be set. + */ + public void setPrologue(byte[] prologue, int offset, int length) + { + if (action != NO_ACTION) { + throw new IllegalStateException + ("Handshake has already started; cannot set prologue"); + } + if (this.prologue != null) { + Noise.destroy(this.prologue); + this.prologue = null; + } + this.prologue = Noise.copySubArray(prologue, offset, length); + } + + /** + * Gets the keypair object for the local static key. + * + * @return The keypair, or null if a local static key is not required. + */ + public DHState getLocalKeyPair() + { + return localKeyPair; + } + + /** + * Determine if this handshake requires a local static key. + * + * @return true if a local static key is needed; false if not. + * + * If the local static key has already been set, then this function + * will return false. + */ + public boolean needsLocalKeyPair() + { + if (localKeyPair != null) + return !localKeyPair.hasPrivateKey(); + else + return false; + } + + /** + * Determine if this handshake has already been configured + * with a local static key. + * + * @return true if the local static key has been configured; + * false if not. + */ + public boolean hasLocalKeyPair() + { + if (localKeyPair != null) + return localKeyPair.hasPrivateKey(); + else + return false; + } + + /** + * Gets the public key object for the remote static key. + * + * @return The public key, or null if a remote static key + * is not required. + */ + public DHState getRemotePublicKey() + { + return remotePublicKey; + } + + /** + * Determine if this handshake requires a remote static key. + * + * @return true if a remote static key is needed; false if not. + * + * If the remote static key has already been set, then this function + * will return false. + */ + public boolean needsRemotePublicKey() + { + if (remotePublicKey != null) + return !remotePublicKey.hasPublicKey(); + else + return false; + } + + /** + * Determine if this handshake has already been configured + * with a remote static key. + * + * @return true if the remote static key has been configured; + * false if not. + */ + public boolean hasRemotePublicKey() + { + if (remotePublicKey != null) + return remotePublicKey.hasPublicKey(); + else + return false; + } + + /** + * Gets the DHState object containing a fixed local ephemeral + * key value for this handshake. + * + * @return The fixed ephemeral key object, or null if a local + * ephemeral key is not required by this handshake. + * + * This function is intended for testing only. It can be used + * to establish a fixed ephemeral key for test vectors. This + * function should not be used in real applications. + */ + public DHState getFixedEphemeralKey() + { + if (fixedEphemeral != null) + return fixedEphemeral; + if (localEphemeral == null) + return null; + try { + fixedEphemeral = Noise.createDH(localEphemeral.getDHName()); + } catch (NoSuchAlgorithmException e) { + // This shouldn't happen - the local ephemeral key would + // have already been created with the same name! + fixedEphemeral = null; + } + return fixedEphemeral; + } + + /** + * Gets the DHState object containing a fixed local hybrid + * key value for this handshake. + * + * @return The fixed hybrid key object, or null if a local + * hybrid key is not required by this handshake. + * + * This function is intended for testing only. It can be used + * to establish a fixed hybrid key for test vectors. This + * function should not be used in real applications. + */ + public DHState getFixedHybridKey() + { + if (fixedHybrid != null) + return fixedHybrid; + if (localHybrid == null) + return null; + try { + fixedHybrid = Noise.createDH(localHybrid.getDHName()); + } catch (NoSuchAlgorithmException e) { + // This shouldn't happen - the local hybrid key would + // have already been created with the same name! + fixedHybrid = null; + } + return fixedHybrid; + } + + // Empty value for when the prologue is not supplied. + private static final byte[] emptyPrologue = new byte [0]; + + /** + * Starts the handshake running. + * + * This function is called after all of the handshake parameters have been + * provided to the HandshakeState object. This function should be followed + * by calls to writeMessage() or readMessage() to process the handshake + * messages. The getAction() function indicates the action to take next. + * + * @throws IllegalStateException The handshake has already started, or one or + * more of the required parameters has not been supplied. + * + * @throws UnsupportedOperationException An attempt was made to start a + * fallback handshake pattern without first calling fallback() on a + * previous handshake. + * + * @see #getAction() + * @see #writeMessage(byte[], int, byte[], int, int) + * @see #readMessage(byte[], int, int, byte[], int) + * @see #fallback() + */ + public void start() + { + if (action != NO_ACTION) { + throw new IllegalStateException + ("Handshake has already started; cannot start again"); + } + if ((pattern[0] & Pattern.FLAG_REMOTE_EPHEM_REQ) != 0 && + (requirements & FALLBACK_PREMSG) == 0) { + throw new UnsupportedOperationException + ("Cannot start a fallback pattern"); + } + + // Check that we have satisfied all of the pattern requirements. + if ((requirements & LOCAL_REQUIRED) != 0) { + if (localKeyPair == null || !localKeyPair.hasPrivateKey()) + throw new IllegalStateException("Local static key required"); + } + if ((requirements & REMOTE_REQUIRED) != 0) { + if (remotePublicKey == null || !remotePublicKey.hasPublicKey()) + throw new IllegalStateException("Remote static key required"); + } + if ((requirements & PSK_REQUIRED) != 0) { + if (preSharedKey == null) + throw new IllegalStateException("Pre-shared key required"); + } + + // Hash the prologue value. + if (prologue != null) + symmetric.mixHash(prologue, 0, prologue.length); + else + symmetric.mixHash(emptyPrologue, 0, 0); + + // Hash the pre-shared key into the chaining key and handshake hash. + if (preSharedKey != null) + symmetric.mixPreSharedKey(preSharedKey); + + // Mix the pre-supplied public keys into the handshake hash. + if (isInitiator) { + if ((requirements & LOCAL_PREMSG) != 0) + symmetric.mixPublicKey(localKeyPair); + if ((requirements & FALLBACK_PREMSG) != 0) { + symmetric.mixPublicKey(remoteEphemeral); + if (remoteHybrid != null) + symmetric.mixPublicKey(remoteHybrid); + if (preSharedKey != null) + symmetric.mixPublicKeyIntoCK(remoteEphemeral); + } + if ((requirements & REMOTE_PREMSG) != 0) + symmetric.mixPublicKey(remotePublicKey); + } else { + if ((requirements & REMOTE_PREMSG) != 0) + symmetric.mixPublicKey(remotePublicKey); + if ((requirements & FALLBACK_PREMSG) != 0) { + symmetric.mixPublicKey(localEphemeral); + if (localHybrid != null) + symmetric.mixPublicKey(localHybrid); + if (preSharedKey != null) + symmetric.mixPublicKeyIntoCK(localEphemeral); + } + if ((requirements & LOCAL_PREMSG) != 0) + symmetric.mixPublicKey(localKeyPair); + } + + // The handshake has officially started - set the first action. + if (isInitiator) + action = WRITE_MESSAGE; + else + action = READ_MESSAGE; + } + + /** + * Falls back to the "XXfallback" handshake pattern. + * + * This function is intended used to help implement the "Noise Pipes" protocol. + * It resets a HandshakeState object with the original handshake pattern + * (usually "IK"), converting it into an object with the handshake pattern + * "XXfallback". Information from the previous session such as the local + * keypair, the initiator's ephemeral key, the prologue value, and the + * pre-shared key, are passed to the new session. + * + * Once the fallback has been initiated, the application can set + * new values for the handshake parameters if the values from the + * previous session do not apply. For example, the application may + * use a different prologue for the fallback than for the original + * session. + * + * After setting any new parameters, the application calls start() + * again to restart the handshake from where it left off before the fallback. + * + * Note that this function reverses the roles of initiator and responder. + * + * @throws UnsupportedOperationException The current handshake pattern + * is not compatible with "XXfallback". + * + * @throws IllegalStateException The previous protocol has not started + * or it has not reached the fallback position yet. + * + * @throws NoSuchAlgorithmException One of the cryptographic algorithms + * that is specified in the new protocolName is not supported. + * + * @see #start() + */ + public void fallback() throws NoSuchAlgorithmException + { + fallback("XXfallback"); + } + + /** + * Falls back to another handshake pattern. + * + * @param patternName The name of the pattern to fall back to; + * e.g. "XXfallback", "NXfallback", etc. + * + * This function resets a HandshakeState object with the original + * handshake pattern, and converts it into an object with the new handshake + * patternName. Information from the previous session such as the local + * keypair, the initiator's ephemeral key, the prologue value, and the + * pre-shared key, are passed to the new session. + * + * Once the fallback has been initiated, the application can set + * new values for the handshake parameters if the values from the + * previous session do not apply. For example, the application may + * use a different prologue for the fallback than for the original + * session. + * + * After setting any new parameters, the application calls start() + * again to restart the handshake from where it left off before the fallback. + * + * The new pattern may have greater key requirements than the original; + * for example changing from "NK" from "XXfallback" requires that the + * initiator's static public key be set. The application is responsible for + * setting any extra keys before calling start(). + * + * Note that this function reverses the roles of initiator and responder. + * + * @throws UnsupportedOperationException The current handshake pattern + * is not compatible with the patternName, or patternName is not a + * fallback pattern. + * + * @throws IllegalStateException The previous protocol has not started + * or it has not reached the fallback position yet. + * + * @throws NoSuchAlgorithmException One of the cryptographic algorithms + * that is specified in the new protocolName is not supported. + * + * @see #start() + */ + public void fallback(String patternName) throws NoSuchAlgorithmException + { + // The original pattern must end in "K" for fallback to be possible. + if ((requirements & FALLBACK_POSSIBLE) == 0) + throw new UnsupportedOperationException("Previous handshake pattern does not support fallback"); + + // Check that "patternName" supports fallback. + short[] newPattern = Pattern.lookup(patternName); + if (newPattern == null || (newPattern[0] & Pattern.FLAG_REMOTE_EPHEM_REQ) == 0) + throw new UnsupportedOperationException("New pattern is not a fallback pattern"); + + // The initiator should be waiting for a return message from the + // responder, and the responder should have failed on the first + // handshake message from the initiator. We also allow the + // responder to fallback after processing the first message + // successfully; it decides to always fall back anyway. + if (isInitiator) { + if ((action != FAILED && action != READ_MESSAGE) || !localEphemeral.hasPublicKey()) + throw new IllegalStateException("Initiator cannot fall back from this state"); + } else { + if ((action != FAILED && action != WRITE_MESSAGE) || !remoteEphemeral.hasPublicKey()) + throw new IllegalStateException("Responder cannot fall back from this state"); + } + + // Format a new protocol name for the fallback variant + // and recreate the SymmetricState object. + String[] components = symmetric.getProtocolName().split("_"); + components[1] = patternName; + StringBuilder builder = new StringBuilder(); + builder.append(components[0]); + for (int index = 1; index < components.length; ++index) { + builder.append('_'); + builder.append(components[index]); + } + String name = builder.toString(); + SymmetricState newSymmetric = new SymmetricState(name, components[3], components[4]); + symmetric.destroy(); + symmetric = newSymmetric; + + // Convert the HandshakeState to the "XXfallback" pattern. + if (isInitiator) { + if (remoteEphemeral != null) + remoteEphemeral.clearKey(); + if (remoteHybrid != null) + remoteHybrid.clearKey(); + if (remotePublicKey != null) + remotePublicKey.clearKey(); + isInitiator = false; + } else { + if (localEphemeral != null) + localEphemeral.clearKey(); + if (localHybrid != null) + localHybrid.clearKey(); + if ((newPattern[0] & Pattern.FLAG_REMOTE_REQUIRED) == 0 && remotePublicKey != null) + remotePublicKey.clearKey(); + isInitiator = true; + } + action = NO_ACTION; + pattern = newPattern; + patternIndex = 1; + short flags = pattern[0]; + if (!isInitiator) { + // Reverse the pattern flags so that the responder is "local". + flags = Pattern.reverseFlags(flags); + } + requirements = computeRequirements(flags, components[0], isInitiator ? INITIATOR : RESPONDER, true); + } + + /** + * Gets the next action that the application should perform for + * the handshake part of the protocol. + * + * @return One of HandshakeState.NO_ACTION, HandshakeState.WRITE_MESSAGE, + * HandshakeState.READ_MESSAGE, HandshakeState.SPLIT, or + * HandshakeState.FAILED. + */ + public int getAction() + { + return action; + } + + /** + * Mixes the result of a Diffie-Hellman calculation into the chaining key. + * + * @param local Local private key object. + * @param remote Remote public key object. + */ + private void mixDH(DHState local, DHState remote) + { + if (local == null || remote == null) + throw new IllegalStateException("Pattern definition error"); + int len = local.getSharedKeyLength(); + byte[] shared = new byte [len]; + try { + local.calculate(shared, 0, remote); + symmetric.mixKey(shared, 0, len); + } finally { + Noise.destroy(shared); + } + } + + /** + * Writes a message payload during the handshake. + * + * @param message The buffer that will be populated with the + * handshake packet to be written to the transport. + * @param messageOffset First offset within the message buffer + * to be populated. + * @param payload Buffer containing the payload to add to the + * handshake message; can be null if there is no payload. + * @param payloadOffset Offset into the payload buffer of the + * first payload buffer. + * @param payloadLength Length of the payload in bytes. + * + * @return The length of the data written to the message buffer. + * + * @throws IllegalStateException The action is not WRITE_MESSAGE. + * + * @throws IllegalArgumentException The payload is null, but + * payloadOffset or payloadLength is non-zero. + * + * @throws ShortBufferException The message buffer does not have + * enough space for the handshake message. + * + * @see #getAction() + * @see #readMessage(byte[], int, int, byte[], int) + */ + public int writeMessage(byte[] message, int messageOffset, byte[] payload, int payloadOffset, int payloadLength) throws ShortBufferException + { + int messagePosn = messageOffset; + boolean success = false; + + // Validate the parameters and state. + if (action != WRITE_MESSAGE) { + throw new IllegalStateException + ("Handshake state does not allow writing messages"); + } + if (payload == null && (payloadOffset != 0 || payloadLength != 0)) { + throw new IllegalArgumentException("Invalid payload argument"); + } + if (messageOffset > message.length) { + throw new ShortBufferException(); + } + + // Format the message. + try { + // Process tokens until the direction changes or the patten ends. + for (;;) { + if (patternIndex >= pattern.length) { + // The pattern has finished, so the next action is "split". + action = SPLIT; + break; + } + short token = pattern[patternIndex++]; + if (token == Pattern.FLIP_DIR) { + // Change directions, so this message is complete and the + // next action is "read message". + action = READ_MESSAGE; + break; + } + int space = message.length - messagePosn; + int len, macLen; + switch (token) { + case Pattern.E: + { + // Generate a local ephemeral keypair and add the public + // key to the message. If we are running fixed vector tests, + // then the ephemeral key may have already been provided. + if (localEphemeral == null) + throw new IllegalStateException("Pattern definition error"); + if (fixedEphemeral == null) + localEphemeral.generateKeyPair(); + else + localEphemeral.copyFrom(fixedEphemeral); + len = localEphemeral.getPublicKeyLength(); + if (space < len) + throw new ShortBufferException(); + localEphemeral.getPublicKey(message, messagePosn); + symmetric.mixHash(message, messagePosn, len); + + // If the protocol is using pre-shared keys, then also mix + // the local ephemeral key into the chaining key. + if (preSharedKey != null) + symmetric.mixKey(message, messagePosn, len); + messagePosn += len; + } + break; + + case Pattern.S: + { + // Encrypt the local static public key and add it to the message. + if (localKeyPair == null) + throw new IllegalStateException("Pattern definition error"); + len = localKeyPair.getPublicKeyLength(); + macLen = symmetric.getMACLength(); + if (space < (len + macLen)) + throw new ShortBufferException(); + localKeyPair.getPublicKey(message, messagePosn); + messagePosn += symmetric.encryptAndHash(message, messagePosn, message, messagePosn, len); + } + break; + + case Pattern.EE: + { + // DH operation with initiator and responder ephemeral keys. + mixDH(localEphemeral, remoteEphemeral); + } + break; + + case Pattern.ES: + { + // DH operation with initiator ephemeral and responder static keys. + if (isInitiator) + mixDH(localEphemeral, remotePublicKey); + else + mixDH(localKeyPair, remoteEphemeral); + } + break; + + case Pattern.SE: + { + // DH operation with initiator static and responder ephemeral keys. + if (isInitiator) + mixDH(localKeyPair, remoteEphemeral); + else + mixDH(localEphemeral, remotePublicKey); + } + break; + + case Pattern.SS: + { + // DH operation with initiator and responder static keys. + mixDH(localKeyPair, remotePublicKey); + } + break; + + case Pattern.F: + { + // Generate a local hybrid keypair and add the public + // key to the message. If we are running fixed vector tests, + // then a fixed hybrid key may have already been provided. + if (localHybrid == null) + throw new IllegalStateException("Pattern definition error"); + if (localHybrid instanceof DHStateHybrid) { + // The DH object is something like New Hope which needs to + // generate keys relative to the other party's public key. + DHStateHybrid hybrid = (DHStateHybrid)localHybrid; + if (fixedHybrid == null) + hybrid.generateKeyPair(remoteHybrid); + else + hybrid.copyFrom(fixedHybrid, remoteHybrid); + } else { + if (fixedHybrid == null) + localHybrid.generateKeyPair(); + else + localHybrid.copyFrom(fixedHybrid); + } + len = localHybrid.getPublicKeyLength(); + if (space < len) + throw new ShortBufferException(); + macLen = symmetric.getMACLength(); + if (space < (len + macLen)) + throw new ShortBufferException(); + localHybrid.getPublicKey(message, messagePosn); + messagePosn += symmetric.encryptAndHash(message, messagePosn, message, messagePosn, len); + } + break; + + case Pattern.FF: + { + // DH operation with initiator and responder hybrid keys. + mixDH(localHybrid, remoteHybrid); + } + break; + + default: + { + // Unknown token code. Abort. + throw new IllegalStateException("Unknown handshake token " + Integer.toString(token)); + } + } + } + + // Add the payload to the message buffer and encrypt it. + if (payload != null) + messagePosn += symmetric.encryptAndHash(payload, payloadOffset, message, messagePosn, payloadLength); + else + messagePosn += symmetric.encryptAndHash(message, messagePosn, message, messagePosn, 0); + success = true; + } finally { + // If we failed, then clear any sensitive data that may have + // already been written to the message buffer. + if (!success) { + Arrays.fill(message, messageOffset, message.length - messageOffset, (byte)0); + action = FAILED; + } + } + return messagePosn - messageOffset; + } + + /** + * Reads a message payload during the handshake. + * + * @param message Buffer containing the incoming handshake + * that was read from the transport. + * @param messageOffset Offset of the first message byte. + * @param messageLength The length of the incoming message. + * @param payload Buffer that will be populated with the message payload. + * @param payloadOffset Offset of the first byte in the + * payload buffer to be populated with payload data. + * + * @return The length of the payload. + * + * @throws IllegalStateException The action is not READ_MESSAGE. + * + * @throws ShortBufferException The message buffer does not have + * sufficient bytes for a valid message or the payload buffer does + * not have enough space for the decrypted payload. + * + * @throws BadPaddingException A MAC value in the message failed + * to verify. + * + * @see #getAction() + * @see #writeMessage(byte[], int, byte[], int, int) + */ + public int readMessage(byte[] message, int messageOffset, int messageLength, byte[] payload, int payloadOffset) throws ShortBufferException, BadPaddingException + { + boolean success = false; + int messageEnd = messageOffset + messageLength; + + // Validate the parameters. + if (action != READ_MESSAGE) { + throw new IllegalStateException + ("Handshake state does not allow reading messages"); + } + if (messageOffset > message.length || payloadOffset > payload.length) { + throw new ShortBufferException(); + } + if (messageLength > (message.length - messageOffset)) { + throw new ShortBufferException(); + } + + // Process the message. + try { + // Process tokens until the direction changes or the patten ends. + for (;;) { + if (patternIndex >= pattern.length) { + // The pattern has finished, so the next action is "split". + action = SPLIT; + break; + } + short token = pattern[patternIndex++]; + if (token == Pattern.FLIP_DIR) { + // Change directions, so this message is complete and the + // next action is "write message". + action = WRITE_MESSAGE; + break; + } + int space = messageEnd - messageOffset; + int len, macLen; + switch (token) { + case Pattern.E: + { + // Save the remote ephemeral key and hash it. + if (remoteEphemeral == null) + throw new IllegalStateException("Pattern definition error"); + len = remoteEphemeral.getPublicKeyLength(); + if (space < len) + throw new ShortBufferException(); + symmetric.mixHash(message, messageOffset, len); + remoteEphemeral.setPublicKey(message, messageOffset); + if (remoteEphemeral.isNullPublicKey()) { + // The remote ephemeral key is null, which means that it is + // not contributing anything to the security of the session + // and is in fact downgrading the security to "none at all" + // in some of the message patterns. Reject all such keys. + throw new BadPaddingException("Null remote public key"); + } + + // If the protocol is using pre-shared keys, then also mix + // the remote ephemeral key into the chaining key. + if (preSharedKey != null) + symmetric.mixKey(message, messageOffset, len); + messageOffset += len; + } + break; + + case Pattern.S: + { + // Decrypt and read the remote static key. + if (remotePublicKey == null) + throw new IllegalStateException("Pattern definition error"); + len = remotePublicKey.getPublicKeyLength(); + macLen = symmetric.getMACLength(); + if (space < (len + macLen)) + throw new ShortBufferException(); + byte[] temp = new byte [len]; + try { + if (symmetric.decryptAndHash(message, messageOffset, temp, 0, len + macLen) != len) + throw new ShortBufferException(); + remotePublicKey.setPublicKey(temp, 0); + } finally { + Noise.destroy(temp); + } + messageOffset += len + macLen; + } + break; + + case Pattern.EE: + { + // DH operation with initiator and responder ephemeral keys. + mixDH(localEphemeral, remoteEphemeral); + } + break; + + case Pattern.ES: + { + // DH operation with initiator ephemeral and responder static keys. + if (isInitiator) + mixDH(localEphemeral, remotePublicKey); + else + mixDH(localKeyPair, remoteEphemeral); + } + break; + + case Pattern.SE: + { + // DH operation with initiator static and responder ephemeral keys. + if (isInitiator) + mixDH(localKeyPair, remoteEphemeral); + else + mixDH(localEphemeral, remotePublicKey); + } + break; + + case Pattern.SS: + { + // DH operation with initiator and responder static keys. + mixDH(localKeyPair, remotePublicKey); + } + break; + + case Pattern.F: + { + // Decrypt and read the remote hybrid ephemeral key. + if (remoteHybrid == null) + throw new IllegalStateException("Pattern definition error"); + if (remoteHybrid instanceof DHStateHybrid) { + // The DH object is something like New Hope. The public key + // length may need to change based on whether we already have + // generated a local hybrid keypair or not. + ((DHStateHybrid)remoteHybrid).specifyPeer(localHybrid); + } + len = remoteHybrid.getPublicKeyLength(); + macLen = symmetric.getMACLength(); + if (space < (len + macLen)) + throw new ShortBufferException(); + byte[] temp = new byte [len]; + try { + if (symmetric.decryptAndHash(message, messageOffset, temp, 0, len + macLen) != len) + throw new ShortBufferException(); + remoteHybrid.setPublicKey(temp, 0); + } finally { + Noise.destroy(temp); + } + messageOffset += len + macLen; + } + break; + + case Pattern.FF: + { + // DH operation with initiator and responder hybrid keys. + mixDH(localHybrid, remoteHybrid); + } + break; + + default: + { + // Unknown token code. Abort. + throw new IllegalStateException("Unknown handshake token " + Integer.toString(token)); + } + } + } + + // Decrypt the message payload. + int payloadLength = symmetric.decryptAndHash(message, messageOffset, payload, payloadOffset, messageEnd - messageOffset); + success = true; + return payloadLength; + } finally { + // If we failed, then clear any sensitive data that may have + // already been written to the payload buffer. + if (!success) { + Arrays.fill(payload, payloadOffset, payload.length - payloadOffset, (byte)0); + action = FAILED; + } + } + } + + /** + * Splits the transport encryption CipherState objects out of + * this HandshakeState object once the handshake completes. + * + * @return The pair of ciphers for sending and receiving. + * + * @throws IllegalStateException The action is not SPLIT. + */ + public CipherStatePair split() + { + if (action != SPLIT) { + throw new IllegalStateException + ("Handshake has not finished"); + } + CipherStatePair pair = symmetric.split(); + if (!isInitiator) + pair.swap(); + action = COMPLETE; + return pair; + } + + /** + * Splits the transport encryption CipherState objects out of + * this HandshakeObject after mixing in a secondary symmetric key. + * + * @param secondaryKey The buffer containing the secondary key. + * @param offset The offset of the first secondary key byte. + * @param length The length of the secondary key in bytes, which + * must be either 0 or 32. + * @return The pair of ciphers for sending and receiving. + * + * @throws IllegalStateException The action is not SPLIT. + * + * @throws IllegalArgumentException The length is not 0 or 32. + */ + public CipherStatePair split(byte[] secondaryKey, int offset, int length) + { + if (action != SPLIT) { + throw new IllegalStateException + ("Handshake has not finished"); + } + CipherStatePair pair = symmetric.split(secondaryKey, offset, length); + if (!isInitiator) { + // Swap the sender and receiver objects for the responder + // to make it easier on the application to know which is which. + pair.swap(); + } + action = COMPLETE; + return pair; + } + + /** + * Gets the current value of the handshake hash. + * + * @return The handshake hash. This must not be modified by the caller. + * + * @throws IllegalStateException The action is not SPLIT or COMPLETE. + */ + public byte[] getHandshakeHash() + { + if (action != SPLIT && action != COMPLETE) { + throw new IllegalStateException + ("Handshake has not completed"); + } + return symmetric.getHandshakeHash(); + } + + @Override + public void destroy() { + if (symmetric != null) + symmetric.destroy(); + if (localKeyPair != null) + localKeyPair.destroy(); + if (localEphemeral != null) + localEphemeral.destroy(); + if (localHybrid != null) + localHybrid.destroy(); + if (remotePublicKey != null) + remotePublicKey.destroy(); + if (remoteEphemeral != null) + remoteEphemeral.destroy(); + if (remoteHybrid != null) + remoteHybrid.destroy(); + if (fixedEphemeral != null) + fixedEphemeral.destroy(); + if (fixedHybrid != null) + fixedHybrid.destroy(); + if (preSharedKey != null) + Noise.destroy(preSharedKey); + if (prologue != null) + Noise.destroy(prologue); + } + + /** + * Computes the requirements for a handshake. + * + * @param flags The flags from the handshake's pattern. + * @param prefix The prefix from the protocol name; typically + * "Noise" or "NoisePSK". + * @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. + * @param isFallback Set to true if we need the requirements for a + * fallback pattern; false for a regular pattern. + * + * @return The set of requirements for the handshake. + */ + private static int computeRequirements(short flags, String prefix, int role, boolean isFallback) + { + int requirements = 0; + if ((flags & Pattern.FLAG_LOCAL_STATIC) != 0) { + requirements |= LOCAL_REQUIRED; + } + if ((flags & Pattern.FLAG_LOCAL_REQUIRED) != 0) { + requirements |= LOCAL_REQUIRED; + requirements |= LOCAL_PREMSG; + } + if ((flags & Pattern.FLAG_REMOTE_REQUIRED) != 0) { + requirements |= REMOTE_REQUIRED; + requirements |= REMOTE_PREMSG; + } + if ((flags & (Pattern.FLAG_REMOTE_EPHEM_REQ | + Pattern.FLAG_LOCAL_EPHEM_REQ)) != 0) { + if (isFallback) + requirements |= FALLBACK_PREMSG; + } + if (prefix.equals("NoisePSK")) { + requirements |= PSK_REQUIRED; + } + return requirements; + } +} diff --git a/router/java/src/com/southernstorm/noise/protocol/Noise.java b/router/java/src/com/southernstorm/noise/protocol/Noise.java new file mode 100644 index 000000000..34948ad48 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/Noise.java @@ -0,0 +1,237 @@ +/* + * 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.protocol; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; + +import com.southernstorm.noise.crypto.Blake2bMessageDigest; +import com.southernstorm.noise.crypto.Blake2sMessageDigest; +import com.southernstorm.noise.crypto.SHA256MessageDigest; +import com.southernstorm.noise.crypto.SHA512MessageDigest; + +/** + * Utility functions for the Noise protocol library. + */ +public final class Noise { + + /** + * Maximum length for Noise packets. + */ + public static final int MAX_PACKET_LEN = 65535; + + private static SecureRandom random = new SecureRandom(); + + /** + * Generates random data using the system random number generator. + * + * @param data The data buffer to fill with random data. + */ + public static void random(byte[] data) + { + random.nextBytes(data); + } + + private static boolean forceFallbacks = false; + + /** + * Force the use of plain Java fallback crypto implementations. + * + * @param force Set to true for force fallbacks, false to + * try to use the system implementation before falling back. + * + * This function is intended for testing purposes to toggle between + * the system JCA/JCE implementations and the plain Java fallback + * reference implementations. + */ + public static void setForceFallbacks(boolean force) + { + forceFallbacks = force; + } + + /** + * Creates a Diffie-Hellman object from its Noise protocol name. + * + * @param name The name of the DH algorithm; e.g. "25519", "448", etc. + * + * @return The Diffie-Hellman object if the name is recognized. + * + * @throws NoSuchAlgorithmException The name is not recognized as a + * valid Noise protocol name, or there is no cryptography provider + * in the system that implements the algorithm. + */ + public static DHState createDH(String name) throws NoSuchAlgorithmException + { + if (name.equals("25519")) + return new Curve25519DHState(); + if (name.equals("448")) + return new Curve448DHState(); + if (name.equals("NewHope")) + return new NewHopeDHState(); + throw new NoSuchAlgorithmException("Unknown Noise DH algorithm name: " + name); + } + + /** + * Creates a cipher object from its Noise protocol name. + * + * @param name The name of the cipher algorithm; e.g. "AESGCM", "ChaChaPoly", etc. + * + * @return The cipher object if the name is recognized. + * + * @throws NoSuchAlgorithmException The name is not recognized as a + * valid Noise protocol name, or there is no cryptography provider + * in the system that implements the algorithm. + */ + public static CipherState createCipher(String name) throws NoSuchAlgorithmException + { + if (name.equals("AESGCM")) { + if (forceFallbacks) + return new AESGCMFallbackCipherState(); + // "AES/GCM/NoPadding" exists in some recent JDK's but it is flaky + // to use and not easily back-portable to older Android versions. + // We instead emulate AESGCM on top of "AES/CTR/NoPadding". + try { + return new AESGCMOnCtrCipherState(); + } catch (NoSuchAlgorithmException e1) { + // Could not find anything useful in the JCA/JCE so + // use the pure Java fallback implementation instead. + return new AESGCMFallbackCipherState(); + } + } else if (name.equals("ChaChaPoly")) { + return new ChaChaPolyCipherState(); + } + throw new NoSuchAlgorithmException("Unknown Noise cipher algorithm name: " + name); + } + + /** + * Creates a hash object from its Noise protocol name. + * + * @param name The name of the hash algorithm; e.g. "SHA256", "BLAKE2s", etc. + * + * @return The hash object if the name is recognized. + * + * @throws NoSuchAlgorithmException The name is not recognized as a + * valid Noise protocol name, or there is no cryptography provider + * in the system that implements the algorithm. + */ + public static MessageDigest createHash(String name) throws NoSuchAlgorithmException + { + // Look for a JCA/JCE provider first and if that doesn't work, + // use the fallback implementations in this library instead. + // The only algorithm that is required to be implemented by a + // JDK is "SHA-256", although "SHA-512" is fairly common as well. + if (name.equals("SHA256")) { + if (forceFallbacks) + return new SHA256MessageDigest(); + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + return new SHA256MessageDigest(); + } + } else if (name.equals("SHA512")) { + if (forceFallbacks) + return new SHA512MessageDigest(); + try { + return MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException e) { + return new SHA512MessageDigest(); + } + } else if (name.equals("BLAKE2b")) { + // Bouncy Castle registers the BLAKE2b variant we + // want under the name "BLAKE2B-512". + if (forceFallbacks) + return new Blake2bMessageDigest(); + try { + return MessageDigest.getInstance("BLAKE2B-512"); + } catch (NoSuchAlgorithmException e) { + return new Blake2bMessageDigest(); + } + } else if (name.equals("BLAKE2s")) { + // Bouncy Castle doesn't currently (June 2016) have an + // implementation of BLAKE2s, but look for the most + // obvious provider name in case one is added in the future. + if (forceFallbacks) + return new Blake2sMessageDigest(); + try { + return MessageDigest.getInstance("BLAKE2S-256"); + } catch (NoSuchAlgorithmException e) { + return new Blake2sMessageDigest(); + } + } + throw new NoSuchAlgorithmException("Unknown Noise hash algorithm name: " + name); + } + + // The rest of this class consists of internal utility functions + // that are not part of the public API. + + /** + * Destroys the contents of a byte array. + * + * @param array The array whose contents should be destroyed. + */ + static void destroy(byte[] array) + { + Arrays.fill(array, (byte)0); + } + + /** + * Makes a copy of part of an array. + * + * @param data The buffer containing the data to copy. + * @param offset Offset of the first byte to copy. + * @param length The number of bytes to copy. + * + * @return A new array with a copy of the sub-array. + */ + static byte[] copySubArray(byte[] data, int offset, int length) + { + byte[] copy = new byte [length]; + System.arraycopy(data, offset, copy, 0, length); + return copy; + } + + /** + * Throws an instance of AEADBadTagException. + * + * @throws BadPaddingException The AEAD exception. + * + * If the underlying JDK does not have the AEADBadTagException + * class, then this function will instead throw an instance of + * the superclass BadPaddingException. + */ + static void throwBadTagException() throws BadPaddingException + { + try { + Class c = Class.forName("javax.crypto.AEADBadTagException"); + throw (BadPaddingException)(c.newInstance()); + } catch (ClassNotFoundException e) { + } catch (InstantiationException e) { + } catch (IllegalAccessException e) { + } + throw new BadPaddingException(); + } +} diff --git a/router/java/src/com/southernstorm/noise/protocol/Pattern.java b/router/java/src/com/southernstorm/noise/protocol/Pattern.java new file mode 100644 index 000000000..157af6ad7 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/Pattern.java @@ -0,0 +1,835 @@ +/* + * 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.protocol; + +/** + * Information about all supported handshake patterns. + */ +class Pattern { + + private Pattern() {} + + // Token codes. + public static final short S = 1; + public static final short E = 2; + public static final short EE = 3; + public static final short ES = 4; + public static final short SE = 5; + public static final short SS = 6; + public static final short F = 7; + public static final short FF = 8; + public static final short FLIP_DIR = 255; + + // Pattern flag bits. + public static final short FLAG_LOCAL_STATIC = 0x0001; + public static final short FLAG_LOCAL_EPHEMERAL = 0x0002; + public static final short FLAG_LOCAL_REQUIRED = 0x0004; + public static final short FLAG_LOCAL_EPHEM_REQ = 0x0008; + public static final short FLAG_LOCAL_HYBRID = 0x0010; + public static final short FLAG_LOCAL_HYBRID_REQ = 0x0020; + public static final short FLAG_REMOTE_STATIC = 0x0100; + public static final short FLAG_REMOTE_EPHEMERAL = 0x0200; + public static final short FLAG_REMOTE_REQUIRED = 0x0400; + public static final short FLAG_REMOTE_EPHEM_REQ = 0x0800; + public static final short FLAG_REMOTE_HYBRID = 0x1000; + public static final short FLAG_REMOTE_HYBRID_REQ = 0x2000; + + private static final short[] noise_pattern_N = { + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_REQUIRED, + + E, + ES + }; + + private static final short[] noise_pattern_K = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_REQUIRED | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_REQUIRED, + + E, + ES, + SS + }; + + private static final short[] noise_pattern_X = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_REQUIRED, + + E, + ES, + S, + SS + }; + + private static final short[] noise_pattern_NN = { + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_EPHEMERAL, + + E, + FLIP_DIR, + E, + EE + }; + + private static final short[] noise_pattern_NK = { + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_REQUIRED, + + E, + ES, + FLIP_DIR, + E, + EE + }; + + private static final short[] noise_pattern_NX = { + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL, + + E, + FLIP_DIR, + E, + EE, + S, + ES + }; + + private static final short[] noise_pattern_XN = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_EPHEMERAL, + + E, + FLIP_DIR, + E, + EE, + FLIP_DIR, + S, + SE + }; + + private static final short[] noise_pattern_XK = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_REQUIRED, + + E, + ES, + FLIP_DIR, + E, + EE, + FLIP_DIR, + S, + SE + }; + + private static final short[] noise_pattern_XX = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL, + + E, + FLIP_DIR, + E, + EE, + S, + ES, + FLIP_DIR, + S, + SE + }; + + private static final short[] noise_pattern_KN = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_REQUIRED | + FLAG_REMOTE_EPHEMERAL, + + E, + FLIP_DIR, + E, + EE, + SE + }; + + private static final short[] noise_pattern_KK = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_REQUIRED | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_REQUIRED, + + E, + ES, + SS, + FLIP_DIR, + E, + EE, + SE + }; + + private static final short[] noise_pattern_KX = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_REQUIRED | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL, + + E, + FLIP_DIR, + E, + EE, + SE, + S, + ES + }; + + private static final short[] noise_pattern_IN = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_EPHEMERAL, + + E, + S, + FLIP_DIR, + E, + EE, + SE + }; + + private static final short[] noise_pattern_IK = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_REQUIRED, + + E, + ES, + S, + SS, + FLIP_DIR, + E, + EE, + SE + }; + + private static final short[] noise_pattern_IX = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL, + + E, + S, + FLIP_DIR, + E, + EE, + SE, + S, + ES + }; + + private static final short[] noise_pattern_XXfallback = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_EPHEM_REQ, + + E, + EE, + S, + SE, + FLIP_DIR, + S, + ES + }; + + private static final short[] noise_pattern_Xnoidh = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_REQUIRED, + + E, + S, + ES, + SS + }; + + private static final short[] noise_pattern_NXnoidh = { + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL, + + E, + FLIP_DIR, + E, + S, + EE, + ES + }; + + private static final short[] noise_pattern_XXnoidh = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL, + + E, + FLIP_DIR, + E, + S, + EE, + ES, + FLIP_DIR, + S, + SE + }; + + private static final short[] noise_pattern_KXnoidh = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_REQUIRED | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL, + + E, + FLIP_DIR, + E, + S, + EE, + SE, + ES + }; + + private static final short[] noise_pattern_IKnoidh = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_REQUIRED, + + E, + S, + ES, + SS, + FLIP_DIR, + E, + EE, + SE + }; + + private static final short[] noise_pattern_IXnoidh = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL, + + E, + S, + FLIP_DIR, + E, + S, + EE, + SE, + ES + }; + + private static final short[] noise_pattern_NNhfs = { + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + FLIP_DIR, + E, + F, + EE, + FF + }; + + private static final short[] noise_pattern_NKhfs = { + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID | + FLAG_REMOTE_REQUIRED, + + E, + F, + ES, + FLIP_DIR, + E, + F, + EE, + FF + }; + + private static final short[] noise_pattern_NXhfs = { + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + FLIP_DIR, + E, + F, + EE, + FF, + S, + ES + }; + + private static final short[] noise_pattern_XNhfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + FLIP_DIR, + E, + F, + EE, + FF, + FLIP_DIR, + S, + SE + }; + + private static final short[] noise_pattern_XKhfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID | + FLAG_REMOTE_REQUIRED, + + E, + F, + ES, + FLIP_DIR, + E, + F, + EE, + FF, + FLIP_DIR, + S, + SE + }; + + private static final short[] noise_pattern_XXhfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + FLIP_DIR, + E, + F, + EE, + FF, + S, + ES, + FLIP_DIR, + S, + SE + }; + + private static final short[] noise_pattern_KNhfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_REQUIRED | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + FLIP_DIR, + E, + F, + EE, + FF, + SE + }; + + private static final short[] noise_pattern_KKhfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_REQUIRED | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID | + FLAG_REMOTE_REQUIRED, + + E, + F, + ES, + SS, + FLIP_DIR, + E, + F, + EE, + FF, + SE + }; + + private static final short[] noise_pattern_KXhfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_REQUIRED | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + FLIP_DIR, + E, + F, + EE, + FF, + SE, + S, + ES + }; + + private static final short[] noise_pattern_INhfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + S, + FLIP_DIR, + E, + F, + EE, + FF, + SE + }; + + private static final short[] noise_pattern_IKhfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID | + FLAG_REMOTE_REQUIRED, + + E, + F, + ES, + S, + SS, + FLIP_DIR, + E, + F, + EE, + FF, + SE + }; + + private static final short[] noise_pattern_IXhfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + S, + FLIP_DIR, + E, + F, + EE, + FF, + SE, + S, + ES + }; + + private static final short[] noise_pattern_XXfallback_hfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_EPHEM_REQ | + FLAG_REMOTE_HYBRID | + FLAG_REMOTE_HYBRID_REQ, + + E, + F, + EE, + FF, + S, + SE, + FLIP_DIR, + S, + ES + }; + + private static final short[] noise_pattern_NXnoidh_hfs = { + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + FLIP_DIR, + E, + F, + S, + EE, + FF, + ES + }; + + private static final short[] noise_pattern_XXnoidh_hfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + FLIP_DIR, + E, + F, + S, + EE, + FF, + ES, + FLIP_DIR, + S, + SE + }; + + private static final short[] noise_pattern_KXnoidh_hfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_REQUIRED | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + FLIP_DIR, + E, + F, + S, + EE, + FF, + SE, + ES + }; + + private static final short[] noise_pattern_IKnoidh_hfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + S, + ES, + SS, + FLIP_DIR, + E, + F, + EE, + FF, + SE + }; + + private static final short[] noise_pattern_IXnoidh_hfs = { + FLAG_LOCAL_STATIC | + FLAG_LOCAL_EPHEMERAL | + FLAG_LOCAL_HYBRID | + FLAG_REMOTE_STATIC | + FLAG_REMOTE_EPHEMERAL | + FLAG_REMOTE_HYBRID, + + E, + F, + S, + FLIP_DIR, + E, + F, + S, + EE, + FF, + SE, + ES + }; + + /** + * Look up the description information for a pattern. + * + * @param name The name of the pattern. + * @return The pattern description or null. + */ + public static short[] lookup(String name) + { + if (name.equals("N")) + return noise_pattern_N; + else if (name.equals("K")) + return noise_pattern_K; + else if (name.equals("X")) + return noise_pattern_X; + else if (name.equals("NN")) + return noise_pattern_NN; + else if (name.equals("NK")) + return noise_pattern_NK; + else if (name.equals("NX")) + return noise_pattern_NX; + else if (name.equals("XN")) + return noise_pattern_XN; + else if (name.equals("XK")) + return noise_pattern_XK; + else if (name.equals("XX")) + return noise_pattern_XX; + else if (name.equals("KN")) + return noise_pattern_KN; + else if (name.equals("KK")) + return noise_pattern_KK; + else if (name.equals("KX")) + return noise_pattern_KX; + else if (name.equals("IN")) + return noise_pattern_IN; + else if (name.equals("IK")) + return noise_pattern_IK; + else if (name.equals("IX")) + return noise_pattern_IX; + else if (name.equals("XXfallback")) + return noise_pattern_XXfallback; + else if (name.equals("Xnoidh")) + return noise_pattern_Xnoidh; + else if (name.equals("NXnoidh")) + return noise_pattern_NXnoidh; + else if (name.equals("XXnoidh")) + return noise_pattern_XXnoidh; + else if (name.equals("KXnoidh")) + return noise_pattern_KXnoidh; + else if (name.equals("IKnoidh")) + return noise_pattern_IKnoidh; + else if (name.equals("IXnoidh")) + return noise_pattern_IXnoidh; + else if (name.equals("NNhfs")) + return noise_pattern_NNhfs; + else if (name.equals("NKhfs")) + return noise_pattern_NKhfs; + else if (name.equals("NXhfs")) + return noise_pattern_NXhfs; + else if (name.equals("XNhfs")) + return noise_pattern_XNhfs; + else if (name.equals("XKhfs")) + return noise_pattern_XKhfs; + else if (name.equals("XXhfs")) + return noise_pattern_XXhfs; + else if (name.equals("KNhfs")) + return noise_pattern_KNhfs; + else if (name.equals("KKhfs")) + return noise_pattern_KKhfs; + else if (name.equals("KXhfs")) + return noise_pattern_KXhfs; + else if (name.equals("INhfs")) + return noise_pattern_INhfs; + else if (name.equals("IKhfs")) + return noise_pattern_IKhfs; + else if (name.equals("IXhfs")) + return noise_pattern_IXhfs; + else if (name.equals("XXfallback+hfs")) + return noise_pattern_XXfallback_hfs; + else if (name.equals("NXnoidh+hfs")) + return noise_pattern_NXnoidh_hfs; + else if (name.equals("XXnoidh+hfs")) + return noise_pattern_XXnoidh_hfs; + else if (name.equals("KXnoidh+hfs")) + return noise_pattern_KXnoidh_hfs; + else if (name.equals("IKnoidh+hfs")) + return noise_pattern_IKnoidh_hfs; + else if (name.equals("IXnoidh+hfs")) + return noise_pattern_IXnoidh_hfs; + return null; + } + + /** + * Reverses the local and remote flags for a pattern. + * + * @param flags The flags, assuming that the initiator is "local". + * @return The reversed flags, with the responder now being "local". + */ + public static short reverseFlags(short flags) + { + return (short)(((flags >> 8) & 0x00FF) | ((flags << 8) & 0xFF00)); + } +} diff --git a/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java b/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java new file mode 100644 index 000000000..ae54fd3f2 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/SymmetricState.java @@ -0,0 +1,484 @@ +/* + * 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.protocol; + +import java.io.UnsupportedEncodingException; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.ShortBufferException; + +/** + * Symmetric state for helping manage a Noise handshake. + */ +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. + * + * @param protocolName The name of the Noise protocol, which is assumed to be valid. + * @param cipherName The name of the cipher within protocolName. + * @param hashName The name of the hash within protocolName. + * + * @throws NoSuchAlgorithmException The cipher or hash algorithm in the + * protocol name is not supported. + */ + public SymmetricState(String protocolName, String cipherName, String hashName) throws NoSuchAlgorithmException + { + name = protocolName; + cipher = Noise.createCipher(cipherName); + hash = Noise.createHash(hashName); + int hashLength = hash.getDigestLength(); + ck = new byte [hashLength]; + h = new byte [hashLength]; + prev_h = new byte [hashLength]; + + byte[] protocolNameBytes; + try { + protocolNameBytes = protocolName.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // If UTF-8 is not supported, then we are definitely in trouble! + throw new UnsupportedOperationException("UTF-8 encoding is not supported"); + } + + if (protocolNameBytes.length <= hashLength) { + System.arraycopy(protocolNameBytes, 0, h, 0, protocolNameBytes.length); + Arrays.fill(h, protocolNameBytes.length, h.length, (byte)0); + } else { + hashOne(protocolNameBytes, 0, protocolNameBytes.length, h, 0, h.length); + } + + System.arraycopy(h, 0, ck, 0, hashLength); + } + + /** + * Gets the name of the Noise protocol. + * + * @return The protocol name. + */ + public String getProtocolName() + { + return name; + } + + /** + * Gets the length of MAC values in the current state. + * + * @return The length of the MAC value for the underlying cipher + * or zero if the cipher has not yet been initialized with a key. + */ + public int getMACLength() + { + return cipher.getMACLength(); + } + + /** + * Mixes data into the chaining key. + * + * @param data The buffer containing the data to mix in. + * @param offset The offset of the first data byte to mix in. + * @param length The number of bytes to mix in. + */ + public void mixKey(byte[] data, int offset, int length) + { + int keyLength = cipher.getKeyLength(); + byte[] tempKey = new byte [keyLength]; + try { + hkdf(ck, 0, ck.length, data, offset, length, ck, 0, ck.length, tempKey, 0, keyLength); + cipher.initializeKey(tempKey, 0); + } finally { + Noise.destroy(tempKey); + } + } + + /** + * Mixes data into the handshake hash. + * + * @param data The buffer containing the data to mix in. + * @param offset The offset of the first data byte to mix in. + * @param length The number of bytes to mix in. + */ + public void mixHash(byte[] data, int offset, int length) + { + hashTwo(h, 0, h.length, data, offset, length, h, 0, h.length); + } + + /** + * Mixes a pre-shared key into the chaining key and handshake hash. + * + * @param key The pre-shared key value. + */ + public void mixPreSharedKey(byte[] key) + { + byte[] temp = new byte [hash.getDigestLength()]; + try { + hkdf(ck, 0, ck.length, key, 0, key.length, ck, 0, ck.length, temp, 0, temp.length); + mixHash(temp, 0, temp.length); + } finally { + Noise.destroy(temp); + } + } + + /** + * Mixes a pre-supplied public key into the handshake hash. + * + * @param dh The object containing the public key. + */ + public void mixPublicKey(DHState dh) + { + byte[] temp = new byte [dh.getPublicKeyLength()]; + try { + dh.getPublicKey(temp, 0); + mixHash(temp, 0, temp.length); + } finally { + Noise.destroy(temp); + } + } + + /** + * Mixes a pre-supplied public key into the chaining key. + * + * @param dh The object containing the public key. + */ + public void mixPublicKeyIntoCK(DHState dh) + { + byte[] temp = new byte [dh.getPublicKeyLength()]; + try { + dh.getPublicKey(temp, 0); + mixKey(temp, 0, temp.length); + } finally { + Noise.destroy(temp); + } + } + + /** + * Encrypts a block of plaintext and mixes the ciphertext into the handshake hash. + * + * @param plaintext The buffer containing the plaintext to encrypt. + * @param plaintextOffset The offset within the plaintext buffer of the + * first byte or plaintext data. + * @param ciphertext The buffer to place the ciphertext in. This can + * be the same as the plaintext buffer. + * @param ciphertextOffset The first offset within the ciphertext buffer + * to place the ciphertext and the MAC tag. + * @param length The length of the plaintext. + * @return The length of the ciphertext plus the MAC tag. + * + * @throws ShortBufferException There is not enough space in the + * ciphertext buffer for the encrypted data plus MAC value. + * + * The plaintext and ciphertext buffers can be the same for in-place + * encryption. In that case, plaintextOffset must be identical to + * ciphertextOffset. + * + * There must be enough space in the ciphertext buffer to accomodate + * length + getMACLength() bytes of data starting at ciphertextOffset. + */ + public int encryptAndHash(byte[] plaintext, int plaintextOffset, byte[] ciphertext, int ciphertextOffset, int length) throws ShortBufferException + { + int ciphertextLength = cipher.encryptWithAd(h, plaintext, plaintextOffset, ciphertext, ciphertextOffset, length); + mixHash(ciphertext, ciphertextOffset, ciphertextLength); + return ciphertextLength; + } + + /** + * Decrypts a block of ciphertext and mixes it into the handshake hash. + * + * @param ciphertext The buffer containing the ciphertext to decrypt. + * @param ciphertextOffset The offset within the ciphertext buffer of + * the first byte of ciphertext data. + * @param plaintext The buffer to place the plaintext in. This can be + * the same as the ciphertext buffer. + * @param plaintextOffset The first offset within the plaintext buffer + * to place the plaintext. + * @param length The length of the incoming ciphertext plus the MAC tag. + * @return The length of the plaintext with the MAC tag stripped off. + * + * @throws ShortBufferException There is not enough space in the plaintext + * buffer for the decrypted data. + * + * @throws BadPaddingException The MAC value failed to verify. + * + * The plaintext and ciphertext buffers can be the same for in-place + * decryption. In that case, ciphertextOffset must be identical to + * plaintextOffset. + */ + public int decryptAndHash(byte[] ciphertext, int ciphertextOffset, byte[] plaintext, int plaintextOffset, int length) throws ShortBufferException, BadPaddingException + { + System.arraycopy(h, 0, prev_h, 0, h.length); + mixHash(ciphertext, ciphertextOffset, length); + return cipher.decryptWithAd(prev_h, ciphertext, ciphertextOffset, plaintext, plaintextOffset, length); + } + + /** + * Splits the symmetric state into two ciphers for session encryption. + * + * @return The pair of ciphers for sending and receiving. + */ + public CipherStatePair split() + { + return split(new byte[0], 0, 0); + } + + /** + * Splits the symmetric state into two ciphers for session encryption, + * and optionally mixes in a secondary symmetric key. + * + * @param secondaryKey The buffer containing the secondary key. + * @param offset The offset of the first secondary key byte. + * @param length The length of the secondary key in bytes, which + * must be either 0 or 32. + * @return The pair of ciphers for sending and receiving. + * + * @throws IllegalArgumentException The length is not 0 or 32. + */ + public CipherStatePair split(byte[] secondaryKey, int offset, int length) + { + if (length != 0 && length != 32) + throw new IllegalArgumentException("Secondary keys must be 0 or 32 bytes in length"); + int keyLength = cipher.getKeyLength(); + byte[] k1 = new byte [keyLength]; + byte[] k2 = new byte [keyLength]; + try { + hkdf(ck, 0, ck.length, secondaryKey, offset, length, k1, 0, k1.length, k2, 0, k2.length); + CipherState c1 = null; + CipherState c2 = null; + CipherStatePair pair = null; + try { + c1 = cipher.fork(k1, 0); + c2 = cipher.fork(k2, 0); + pair = new CipherStatePair(c1, c2); + } finally { + if (c1 == null || c2 == null || pair == null) { + // Could not create some of the objects. Clean up the others + // to avoid accidental leakage of k1 or k2. + if (c1 != null) + c1.destroy(); + if (c2 != null) + c2.destroy(); + pair = null; + } + } + return pair; + } finally { + Noise.destroy(k1); + Noise.destroy(k2); + } + } + + /** + * Gets the current value of the handshake hash. + * + * @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. + */ + public byte[] getHandshakeHash() + { + return h; + } + + @Override + public void destroy() { + if (cipher != null) { + cipher.destroy(); + cipher = null; + } + if (hash != null) { + // The built-in fallback implementations are destroyable. + // JCA/JCE implementations aren't, so try reset() instead. + if (hash instanceof Destroyable) + ((Destroyable)hash).destroy(); + else + hash.reset(); + hash = null; + } + if (ck != null) { + Noise.destroy(ck); + ck = null; + } + if (h != null) { + Noise.destroy(h); + h = null; + } + if (prev_h != null) { + Noise.destroy(prev_h); + prev_h = null; + } + } + + /** + * Hashes a single data buffer. + * + * @param data The buffer containing the data to hash. + * @param offset Offset into the data buffer of the first byte to hash. + * @param length Length of the data to be hashed. + * @param output The buffer to receive the output hash value. + * @param outputOffset Offset into the output buffer to place the hash value. + * @param outputLength The length of the hash output. + * + * The output buffer can be the same as the input data buffer. + */ + private void hashOne(byte[] data, int offset, int length, byte[] output, int outputOffset, int outputLength) + { + hash.reset(); + hash.update(data, offset, length); + try { + hash.digest(output, outputOffset, outputLength); + } catch (DigestException e) { + Arrays.fill(output, outputOffset, outputLength, (byte)0); + } + } + + /** + * Hashes two data buffers. + * + * @param data1 The buffer containing the first data to hash. + * @param offset1 Offset into the first data buffer of the first byte to hash. + * @param length1 Length of the first data to be hashed. + * @param data2 The buffer containing the second data to hash. + * @param offset2 Offset into the second data buffer of the first byte to hash. + * @param length2 Length of the second data to be hashed. + * @param output The buffer to receive the output hash value. + * @param outputOffset Offset into the output buffer to place the hash value. + * @param outputLength The length of the hash output. + * + * The output buffer can be same as either of the input buffers. + */ + private void hashTwo(byte[] data1, int offset1, int length1, + byte[] data2, int offset2, int length2, + byte[] output, int outputOffset, int outputLength) + { + hash.reset(); + hash.update(data1, offset1, length1); + hash.update(data2, offset2, length2); + try { + hash.digest(output, outputOffset, outputLength); + } catch (DigestException e) { + Arrays.fill(output, outputOffset, outputLength, (byte)0); + } + } + + /** + * Computes a HMAC value using key and data values. + * + * @param key The buffer that contains the key. + * @param keyOffset The offset of the key in the key buffer. + * @param keyLength The length of the key in bytes. + * @param data The buffer that contains the data. + * @param dataOffset The offset of the data in the data buffer. + * @param dataLength The length of the data in bytes. + * @param output The output buffer to place the HMAC value in. + * @param outputOffset Offset into the output buffer for the HMAC value. + * @param outputLength The length of the HMAC output. + */ + private void hmac(byte[] key, int keyOffset, int keyLength, + byte[] data, int dataOffset, int dataLength, + byte[] output, int outputOffset, int outputLength) + { + // In all of the algorithms of interest to us, the block length + // is twice the size of the hash length. + int hashLength = hash.getDigestLength(); + int blockLength = hashLength * 2; + byte[] block = new byte [blockLength]; + int index; + try { + if (keyLength <= blockLength) { + System.arraycopy(key, keyOffset, block, 0, keyLength); + Arrays.fill(block, keyLength, blockLength, (byte)0); + } else { + hash.reset(); + hash.update(key, keyOffset, keyLength); + hash.digest(block, 0, hashLength); + Arrays.fill(block, hashLength, blockLength, (byte)0); + } + for (index = 0; index < blockLength; ++index) + block[index] ^= (byte)0x36; + hash.reset(); + hash.update(block, 0, blockLength); + hash.update(data, dataOffset, dataLength); + hash.digest(output, outputOffset, hashLength); + for (index = 0; index < blockLength; ++index) + block[index] ^= (byte)(0x36 ^ 0x5C); + hash.reset(); + hash.update(block, 0, blockLength); + hash.update(output, outputOffset, hashLength); + hash.digest(output, outputOffset, outputLength); + } catch (DigestException e) { + Arrays.fill(output, outputOffset, outputLength, (byte)0); + } finally { + Noise.destroy(block); + } + } + + /** + * Computes a HKDF value. + * + * @param key The buffer that contains the key. + * @param keyOffset The offset of the key in the key buffer. + * @param keyLength The length of the key in bytes. + * @param data The buffer that contains the data. + * @param dataOffset The offset of the data in the data buffer. + * @param dataLength The length of the data in bytes. + * @param output1 The first output buffer. + * @param output1Offset Offset into the first output buffer. + * @param output1Length Length of the first output which can be + * less than the hash length. + * @param output2 The second output buffer. + * @param output2Offset Offset into the second output buffer. + * @param output2Length Length of the second output which can be + * less than the hash length. + */ + private void hkdf(byte[] key, int keyOffset, int keyLength, + byte[] data, int dataOffset, int dataLength, + byte[] output1, int output1Offset, int output1Length, + byte[] output2, int output2Offset, int output2Length) + { + int hashLength = hash.getDigestLength(); + byte[] tempKey = new byte [hashLength]; + byte[] tempHash = new byte [hashLength + 1]; + try { + hmac(key, keyOffset, keyLength, data, dataOffset, dataLength, tempKey, 0, hashLength); + tempHash[0] = (byte)0x01; + hmac(tempKey, 0, hashLength, tempHash, 0, 1, tempHash, 0, hashLength); + System.arraycopy(tempHash, 0, output1, output1Offset, output1Length); + tempHash[hashLength] = (byte)0x02; + hmac(tempKey, 0, hashLength, tempHash, 0, hashLength + 1, tempHash, 0, hashLength); + System.arraycopy(tempHash, 0, output2, output2Offset, output2Length); + } finally { + Noise.destroy(tempKey); + Noise.destroy(tempHash); + } + } +} diff --git a/router/java/src/com/southernstorm/noise/protocol/package-info.java b/router/java/src/com/southernstorm/noise/protocol/package-info.java new file mode 100644 index 000000000..ef85f00b8 --- /dev/null +++ b/router/java/src/com/southernstorm/noise/protocol/package-info.java @@ -0,0 +1,7 @@ + +/** + * Provides classes for communicating via the Noise protocol. + * + * Reference: http://noiseprotocol.org + */ +package com.southernstorm.noise.protocol;