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;