HandshakeState API

This commit is contained in:
Rhys Weatherley 2016-06-25 15:57:05 +10:00
parent 1764404d1d
commit b29ba50927
9 changed files with 1521 additions and 35 deletions

View File

@ -26,6 +26,7 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
@ -130,13 +131,22 @@ class AESGCMCipherState implements CipherState {
@Override
public int encryptWithAd(byte[] ad, byte[] plaintext, int plaintextOffset,
byte[] ciphertext, int ciphertextOffset, int length) {
byte[] ciphertext, int ciphertextOffset, int length) throws ShortBufferException {
int space;
if (ciphertextOffset > ciphertext.length)
space = 0;
else
space = ciphertext.length - ciphertextOffset;
if (keySpec == null) {
// 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();
try {
cipher.init(Cipher.ENCRYPT_MODE, keySpec, createGCMParams());
} catch (InvalidKeyException e) {
@ -152,8 +162,6 @@ class AESGCMCipherState implements CipherState {
int result = cipher.update(plaintext, plaintextOffset, length, ciphertext, ciphertextOffset);
result += cipher.doFinal(ciphertext, ciphertextOffset + result);
return result;
} catch (ShortBufferException e) {
return -1;
} catch (IllegalBlockSizeException e) {
return -1;
} catch (BadPaddingException e) {
@ -164,7 +172,14 @@ class AESGCMCipherState implements CipherState {
@Override
public int decryptWithAd(byte[] ad, byte[] ciphertext,
int ciphertextOffset, byte[] plaintext, int plaintextOffset,
int length) {
int length) throws ShortBufferException, AEADBadTagException {
int space;
if (plaintextOffset > plaintext.length)
space = 0;
else
space = plaintext.length - plaintextOffset;
if (length > space)
throw new ShortBufferException();
if (keySpec == null) {
// The key is not set yet - return the ciphertext as-is.
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset)
@ -175,10 +190,10 @@ class AESGCMCipherState implements CipherState {
cipher.init(Cipher.DECRYPT_MODE, keySpec, createGCMParams());
} catch (InvalidKeyException e) {
// Shouldn't happen.
return -1;
throw new AEADBadTagException();
} catch (InvalidAlgorithmParameterException e) {
// Shouldn't happen.
return -1;
throw new AEADBadTagException();
}
if (ad != null)
cipher.updateAAD(ad);
@ -186,12 +201,12 @@ class AESGCMCipherState implements CipherState {
int result = cipher.update(ciphertext, ciphertextOffset, length, plaintext, plaintextOffset);
result += cipher.doFinal(plaintext, plaintextOffset + result);
return result;
} catch (ShortBufferException e) {
return -1;
} catch (IllegalBlockSizeException e) {
return -1;
throw new AEADBadTagException();
} catch (AEADBadTagException e) {
throw e;
} catch (BadPaddingException e) {
return -1;
throw new AEADBadTagException();
}
}

View File

@ -24,6 +24,9 @@ package com.southernstorm.noise.protocol;
import java.util.Arrays;
import javax.crypto.AEADBadTagException;
import javax.crypto.ShortBufferException;
import com.southernstorm.noise.crypto.ChaChaCore;
import com.southernstorm.noise.crypto.Poly1305;
@ -207,13 +210,22 @@ class ChaChaPolyCipherState implements CipherState {
@Override
public int encryptWithAd(byte[] ad, byte[] plaintext, int plaintextOffset,
byte[] ciphertext, int ciphertextOffset, int length) {
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);
finish(ad, length);
@ -224,7 +236,14 @@ class ChaChaPolyCipherState implements CipherState {
@Override
public int decryptWithAd(byte[] ad, byte[] ciphertext,
int ciphertextOffset, byte[] plaintext, int plaintextOffset,
int length) {
int length) throws ShortBufferException, AEADBadTagException {
int space;
if (plaintextOffset > plaintext.length)
space = 0;
else
space = plaintext.length - plaintextOffset;
if (length > space)
throw new ShortBufferException();
if (!haskey) {
// The key is not set yet - return the ciphertext as-is.
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset)
@ -232,7 +251,7 @@ class ChaChaPolyCipherState implements CipherState {
return length;
}
if (length < 16)
return -1;
throw new AEADBadTagException();
int dataLen = length = 16;
setup(ad);
poly.update(ciphertext, ciphertextOffset, dataLen);
@ -241,7 +260,7 @@ class ChaChaPolyCipherState implements CipherState {
for (int index = 0; index < 16; ++index)
temp |= (polyKey[index] ^ ciphertext[ciphertextOffset + dataLen + index]);
if (temp != 0)
return -1;
throw new AEADBadTagException();
encrypt(ciphertext, ciphertextOffset, plaintext, plaintextOffset, dataLen);
return dataLen;
}

View File

@ -22,6 +22,9 @@
package com.southernstorm.noise.protocol;
import javax.crypto.AEADBadTagException;
import javax.crypto.ShortBufferException;
/**
* Interface to an authenticated cipher for use in the Noise protocol.
*
@ -92,6 +95,9 @@ public interface CipherState extends Destroyable {
* @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.
*
* The plaintext and ciphertext buffers can be the same for in-place
* encryption. In that case, plaintextOffset must be identical to
* ciphertextOffset.
@ -99,7 +105,7 @@ public interface CipherState extends Destroyable {
* 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);
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.
@ -113,15 +119,18 @@ public interface CipherState extends Destroyable {
* @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,
* or -1 if the tag did not verify, or -1 if the plaintext buffer is
* not big enough to hold the result.
* @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 AEADBadTagException 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.
*/
int decryptWithAd(byte[] ad, byte[] ciphertext, int ciphertextOffset, byte[] plaintext, int plaintextOffset, int length);
int decryptWithAd(byte[] ad, byte[] ciphertext, int ciphertextOffset, byte[] plaintext, int plaintextOffset, int length) throws ShortBufferException, AEADBadTagException;
/**
* Creates a new instance of this cipher and initializes it with a key.

View File

@ -141,4 +141,16 @@ class Curve25519DHState implements DHState {
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;
}
}

View File

@ -143,4 +143,14 @@ public interface DHState extends Destroyable {
* 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);
}

File diff suppressed because it is too large Load Diff

View File

@ -36,15 +36,10 @@ import com.southernstorm.noise.crypto.Blake2sMessageDigest;
public final class Noise {
/**
* Destroys the contents of a byte array.
*
* @param array The array whose contents should be destroyed.
* Maximum length for Noise packets.
*/
public static void destroy(byte[] array)
{
Arrays.fill(array, (byte)0);
}
public static final int MAX_PACKET_LEN = 65535;
private static SecureRandom random = new SecureRandom();
/**
@ -135,4 +130,33 @@ public final class Noise {
}
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;
}
}

View File

@ -0,0 +1,329 @@
/*
* 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 byte S = 1;
public static final byte E = 2;
public static final byte DHEE = 3;
public static final byte DHES = 4;
public static final byte DHSE = 5;
public static final byte DHSS = 6;
public static final byte FLIP_DIR = 7;
// Pattern flag bits.
public static final byte FLAG_LOCAL_STATIC = 0x01;
public static final byte FLAG_LOCAL_EPHEMERAL = 0x02;
public static final byte FLAG_LOCAL_REQUIRED = 0x04;
public static final byte FLAG_LOCAL_EPHEM_REQ = 0x08;
public static final byte FLAG_REMOTE_STATIC = 0x10;
public static final byte FLAG_REMOTE_EPHEMERAL = 0x20;
public static final byte FLAG_REMOTE_REQUIRED = 0x40;
public static final byte FLAG_REMOTE_EPHEM_REQ = (byte)0x80;
private static final byte[] noise_pattern_N = {
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_REQUIRED,
E,
DHES
};
private static final byte[] noise_pattern_K = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_LOCAL_REQUIRED |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_REQUIRED,
E,
DHES,
DHSS
};
private static final byte[] noise_pattern_X = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_REQUIRED,
E,
DHES,
S,
DHSS
};
private static final byte[] noise_pattern_NN = {
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_EPHEMERAL,
E,
FLIP_DIR,
E,
DHEE
};
private static final byte[] noise_pattern_NK = {
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_EPHEMERAL |
FLAG_REMOTE_REQUIRED,
E,
DHES,
FLIP_DIR,
E,
DHEE
};
private static final byte[] noise_pattern_NX = {
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_EPHEMERAL,
E,
FLIP_DIR,
E,
DHEE,
S,
DHSE
};
private static final byte[] noise_pattern_XN = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_EPHEMERAL,
E,
FLIP_DIR,
E,
DHEE,
FLIP_DIR,
S,
DHSE
};
private static final byte[] noise_pattern_XK = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_EPHEMERAL |
FLAG_REMOTE_REQUIRED,
E,
DHES,
FLIP_DIR,
E,
DHEE,
FLIP_DIR,
S,
DHSE
};
private static final byte[] noise_pattern_XX = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_EPHEMERAL,
E,
FLIP_DIR,
E,
DHEE,
S,
DHSE,
FLIP_DIR,
S,
DHSE
};
private static final byte[] noise_pattern_KN = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_LOCAL_REQUIRED |
FLAG_REMOTE_EPHEMERAL,
E,
FLIP_DIR,
E,
DHEE,
DHES
};
private static final byte[] noise_pattern_KK = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_LOCAL_REQUIRED |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_EPHEMERAL |
FLAG_REMOTE_REQUIRED,
E,
DHES,
DHSS,
FLIP_DIR,
E,
DHEE,
DHES
};
private static final byte[] noise_pattern_KX = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_LOCAL_REQUIRED |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_EPHEMERAL,
E,
FLIP_DIR,
E,
DHEE,
DHES,
S,
DHSE
};
private static final byte[] noise_pattern_IN = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_EPHEMERAL,
E,
S,
FLIP_DIR,
E,
DHEE,
DHES
};
static final byte[] noise_pattern_IK = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_EPHEMERAL |
FLAG_REMOTE_REQUIRED,
E,
DHES,
S,
DHSS,
FLIP_DIR,
E,
DHEE,
DHES
};
private static final byte[] noise_pattern_IX = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_EPHEMERAL,
E,
S,
FLIP_DIR,
E,
DHEE,
DHES,
S,
DHSE
};
static final byte[] noise_pattern_XXfallback = {
FLAG_LOCAL_STATIC |
FLAG_LOCAL_EPHEMERAL |
FLAG_REMOTE_STATIC |
FLAG_REMOTE_EPHEMERAL |
FLAG_REMOTE_EPHEM_REQ,
E,
DHEE,
S,
DHSE,
FLIP_DIR,
S,
DHSE
};
/**
* Look up the description information for a pattern.
*
* @param name The name of the pattern.
* @return The pattern description or null.
*/
public static byte[] 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;
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 byte reverseFlags(byte flags)
{
return (byte)(((flags >> 4) & 0x0F) | ((flags << 4) & 0xF0));
}
}

View File

@ -28,6 +28,9 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.AEADBadTagException;
import javax.crypto.ShortBufferException;
/**
* Symmetric state for helping manage a Noise handshake.
*/
@ -43,16 +46,17 @@ public class SymmetricState implements Destroyable {
* 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) throws NoSuchAlgorithmException
public SymmetricState(String protocolName, String cipherName, String hashName) throws NoSuchAlgorithmException
{
String[] components = protocolName.split("_");
name = protocolName;
cipher = Noise.createCipher(components[3]);
hash = Noise.createHash(components[4]);
cipher = Noise.createCipher(cipherName);
hash = Noise.createHash(hashName);
int keyLength = cipher.getKeyLength();
ck = new byte [keyLength];
int hashLength = hash.getDigestLength();
@ -128,6 +132,38 @@ public class SymmetricState implements Destroyable {
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);
}
}
/**
* Encrypts a block of plaintext and mixes the ciphertext into the handshake hash.
*
@ -141,6 +177,9 @@ public class SymmetricState implements Destroyable {
* @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.
@ -148,7 +187,7 @@ public class SymmetricState implements Destroyable {
* 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)
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);
@ -166,14 +205,18 @@ public class SymmetricState implements Destroyable {
* @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,
* or -1 if the tag did not verify.
* @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 AEADBadTagException 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)
public int decryptAndHash(byte[] ciphertext, int ciphertextOffset, byte[] plaintext, int plaintextOffset, int length) throws ShortBufferException, AEADBadTagException
{
mixHash(ciphertext, ciphertextOffset, length);
return cipher.decryptWithAd(h, ciphertext, ciphertextOffset, plaintext, plaintextOffset, length);