refactor(noise/protocol): drop invalid @JvmName on CipherState.macLength

- Remove `@get:JvmName("getMACLength")` — Kotlin rejects @JvmName on
  interface getters and all remaining callers in this module are Kotlin.
- macLength is now exposed idiomatically as a Kotlin property.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
yet 2026-05-15 17:43:48 +04:00
parent fbb1342439
commit 597d9a1ba9
21 changed files with 3400 additions and 3561 deletions

View File

@ -1,265 +0,0 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.ShortBufferException;
import com.bitchat.android.noise.southernstorm.crypto.GHASH;
import com.bitchat.android.noise.southernstorm.crypto.RijndaelAES;
/**
* Fallback implementation of "AESGCM" on platforms where
* the JCA/JCE does not have a suitable GCM or CTR provider.
*/
class AESGCMFallbackCipherState implements CipherState {
private RijndaelAES aes;
private long n;
private byte[] iv;
private byte[] enciv;
private byte[] hashKey;
private GHASH ghash;
private boolean haskey;
/**
* Constructs a new cipher state for the "AESGCM" algorithm.
*/
public AESGCMFallbackCipherState()
{
aes = new RijndaelAES();
n = 0;
iv = new byte [16];
enciv = new byte [16];
hashKey = new byte [16];
ghash = new GHASH();
haskey = false;
}
@Override
public void destroy() {
aes.destroy();
ghash.destroy();
Noise.destroy(hashKey);
Noise.destroy(iv);
Noise.destroy(enciv);
}
@Override
public String getCipherName() {
return "AESGCM";
}
@Override
public int getKeyLength() {
return 32;
}
@Override
public int getMACLength() {
return haskey ? 16 : 0;
}
@Override
public void initializeKey(byte[] key, int offset) {
// Set up the AES key.
aes.setupEnc(key, offset, 256);
haskey = true;
// Generate the hashing key by encrypting a block of zeroes.
Arrays.fill(hashKey, (byte)0);
aes.encrypt(hashKey, 0, hashKey, 0);
ghash.reset(hashKey, 0);
// Reset the nonce.
n = 0;
}
@Override
public boolean hasKey() {
return haskey;
}
/**
* Set up to encrypt or decrypt the next packet.
*
* @param ad The associated data for the packet.
*/
private void setup(byte[] ad)
{
// Check for nonce wrap-around.
if (n == -1L)
throw new IllegalStateException("Nonce has wrapped around");
// Format the counter/IV block.
iv[0] = 0;
iv[1] = 0;
iv[2] = 0;
iv[3] = 0;
iv[4] = (byte)(n >> 56);
iv[5] = (byte)(n >> 48);
iv[6] = (byte)(n >> 40);
iv[7] = (byte)(n >> 32);
iv[8] = (byte)(n >> 24);
iv[9] = (byte)(n >> 16);
iv[10] = (byte)(n >> 8);
iv[11] = (byte)n;
iv[12] = 0;
iv[13] = 0;
iv[14] = 0;
iv[15] = 1;
++n;
// Encrypt a block of zeroes to generate the hash key to XOR
// the GHASH tag with at the end of the encrypt/decrypt operation.
Arrays.fill(hashKey, (byte)0);
aes.encrypt(iv, 0, hashKey, 0);
// Initialize the GHASH with the associated data value.
ghash.reset();
if (ad != null) {
ghash.update(ad, 0, ad.length);
ghash.pad();
}
}
/**
* Encrypts a block in CTR mode.
*
* @param plaintext The plaintext to encrypt.
* @param plaintextOffset Offset of the first plaintext byte.
* @param ciphertext The resulting ciphertext.
* @param ciphertextOffset Offset of the first ciphertext byte.
* @param length The number of bytes to encrypt.
*
* This function can also be used to decrypt.
*/
private void encryptCTR(byte[] plaintext, int plaintextOffset, byte[] ciphertext, int ciphertextOffset, int length)
{
while (length > 0) {
// Increment the IV and encrypt it to get the next keystream block.
if (++(iv[15]) == 0)
if (++(iv[14]) == 0)
if (++(iv[13]) == 0)
++(iv[12]);
aes.encrypt(iv, 0, enciv, 0);
// XOR the keystream block with the plaintext to create the ciphertext.
int temp = length;
if (temp > 16)
temp = 16;
for (int index = 0; index < temp; ++index)
ciphertext[ciphertextOffset + index] = (byte)(plaintext[plaintextOffset + index] ^ enciv[index]);
// Advance to the next block.
plaintextOffset += temp;
ciphertextOffset += temp;
length -= temp;
}
}
@Override
public int encryptWithAd(byte[] ad, byte[] plaintext, int plaintextOffset,
byte[] ciphertext, int ciphertextOffset, int length)
throws ShortBufferException {
int space;
if (ciphertextOffset < 0 || ciphertextOffset > ciphertext.length)
throw new IllegalArgumentException();
if (length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.length || length > plaintext.length || (plaintext.length - plaintextOffset) < length)
throw new IllegalArgumentException();
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);
encryptCTR(plaintext, plaintextOffset, ciphertext, ciphertextOffset, length);
ghash.update(ciphertext, ciphertextOffset, length);
ghash.pad(ad != null ? ad.length : 0, length);
ghash.finish(ciphertext, ciphertextOffset + length, 16);
for (int index = 0; index < 16; ++index)
ciphertext[ciphertextOffset + length + index] ^= hashKey[index];
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 < 0 || ciphertextOffset > ciphertext.length)
throw new IllegalArgumentException();
else
space = ciphertext.length - ciphertextOffset;
if (length > space)
throw new ShortBufferException();
if (length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.length || length > ciphertext.length || (ciphertext.length - ciphertextOffset) < length)
throw new IllegalArgumentException();
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);
ghash.update(ciphertext, ciphertextOffset, dataLen);
ghash.pad(ad != null ? ad.length : 0, dataLen);
ghash.finish(enciv, 0, 16);
int temp = 0;
for (int index = 0; index < 16; ++index)
temp |= (hashKey[index] ^ enciv[index] ^ ciphertext[ciphertextOffset + dataLen + index]);
if ((temp & 0xFF) != 0)
Noise.throwBadTagException();
encryptCTR(ciphertext, ciphertextOffset, plaintext, plaintextOffset, dataLen);
return dataLen;
}
@Override
public CipherState fork(byte[] key, int offset) {
CipherState cipher;
cipher = new AESGCMFallbackCipherState();
cipher.initializeKey(key, offset);
return cipher;
}
@Override
public void setNonce(long nonce) {
n = nonce;
}
}

View File

@ -0,0 +1,263 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol
import com.bitchat.android.noise.southernstorm.crypto.GHASH
import com.bitchat.android.noise.southernstorm.crypto.RijndaelAES
import com.bitchat.android.noise.southernstorm.protocol.Noise.destroy
import com.bitchat.android.noise.southernstorm.protocol.Noise.throwBadTagException
import java.util.Arrays
import javax.crypto.BadPaddingException
import javax.crypto.ShortBufferException
/**
* Fallback implementation of "AESGCM" on platforms where
* the JCA/JCE does not have a suitable GCM or CTR provider.
*/
internal class AESGCMFallbackCipherState : CipherState {
private val aes: RijndaelAES
private var n: Long
private val iv: ByteArray
private val enciv: ByteArray
private val hashKey: ByteArray
private val ghash: GHASH
private var haskey: Boolean
/**
* Constructs a new cipher state for the "AESGCM" algorithm.
*/
init {
aes = RijndaelAES()
n = 0
iv = ByteArray(16)
enciv = ByteArray(16)
hashKey = ByteArray(16)
ghash = GHASH()
haskey = false
}
override fun destroy() {
aes.destroy()
ghash.destroy()
destroy(hashKey)
destroy(iv)
destroy(enciv)
}
val cipherName: String
get() = "AESGCM"
val keyLength: Int
get() = 32
val mACLength: Int
get() = if (haskey) 16 else 0
override fun initializeKey(key: ByteArray, offset: Int) {
// Set up the AES key.
aes.setupEnc(key, offset, 256)
haskey = true
// Generate the hashing key by encrypting a block of zeroes.
Arrays.fill(hashKey, 0.toByte())
aes.encrypt(hashKey, 0, hashKey, 0)
ghash.reset(hashKey, 0)
// Reset the nonce.
n = 0
}
override fun hasKey(): Boolean {
return haskey
}
/**
* Set up to encrypt or decrypt the next packet.
*
* @param ad The associated data for the packet.
*/
private fun setup(ad: ByteArray?) {
// Check for nonce wrap-around.
check(n != -1L) { "Nonce has wrapped around" }
// Format the counter/IV block.
iv[0] = 0
iv[1] = 0
iv[2] = 0
iv[3] = 0
iv[4] = (n shr 56).toByte()
iv[5] = (n shr 48).toByte()
iv[6] = (n shr 40).toByte()
iv[7] = (n shr 32).toByte()
iv[8] = (n shr 24).toByte()
iv[9] = (n shr 16).toByte()
iv[10] = (n shr 8).toByte()
iv[11] = n.toByte()
iv[12] = 0
iv[13] = 0
iv[14] = 0
iv[15] = 1
++n
// Encrypt a block of zeroes to generate the hash key to XOR
// the GHASH tag with at the end of the encrypt/decrypt operation.
Arrays.fill(hashKey, 0.toByte())
aes.encrypt(iv, 0, hashKey, 0)
// Initialize the GHASH with the associated data value.
ghash.reset()
if (ad != null) {
ghash.update(ad, 0, ad.size)
ghash.pad()
}
}
/**
* Encrypts a block in CTR mode.
*
* @param plaintext The plaintext to encrypt.
* @param plaintextOffset Offset of the first plaintext byte.
* @param ciphertext The resulting ciphertext.
* @param ciphertextOffset Offset of the first ciphertext byte.
* @param length The number of bytes to encrypt.
*
* This function can also be used to decrypt.
*/
private fun encryptCTR(
plaintext: ByteArray,
plaintextOffset: Int,
ciphertext: ByteArray,
ciphertextOffset: Int,
length: Int
) {
var plaintextOffset = plaintextOffset
var ciphertextOffset = ciphertextOffset
var length = length
while (length > 0) {
// Increment the IV and encrypt it to get the next keystream block.
if ((++(iv[15])).toInt() == 0) if ((++(iv[14])).toInt() == 0) if ((++(iv[13])).toInt() == 0) ++(iv[12])
aes.encrypt(iv, 0, enciv, 0)
// XOR the keystream block with the plaintext to create the ciphertext.
var temp = length
if (temp > 16) temp = 16
for (index in 0..<temp) ciphertext[ciphertextOffset + index] =
(plaintext[plaintextOffset + index].toInt() xor enciv[index].toInt()).toByte()
// Advance to the next block.
plaintextOffset += temp
ciphertextOffset += temp
length -= temp
}
}
@Throws(ShortBufferException::class)
override fun encryptWithAd(
ad: ByteArray?, plaintext: ByteArray, plaintextOffset: Int,
ciphertext: ByteArray, ciphertextOffset: Int, length: Int
): Int {
val space: Int
require(!(ciphertextOffset < 0 || ciphertextOffset > ciphertext.size))
require(!(length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.size || length > plaintext.size || (plaintext.size - plaintextOffset) < length))
space = ciphertext.size - ciphertextOffset
if (!haskey) {
// The key is not set yet - return the plaintext as-is.
if (length > space) throw ShortBufferException()
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
plaintext,
plaintextOffset,
ciphertext,
ciphertextOffset,
length
)
return length
}
if (space < 16 || length > (space - 16)) throw ShortBufferException()
setup(ad)
encryptCTR(plaintext, plaintextOffset, ciphertext, ciphertextOffset, length)
ghash.update(ciphertext, ciphertextOffset, length)
ghash.pad((if (ad != null) ad.size else 0).toLong(), length.toLong())
ghash.finish(ciphertext, ciphertextOffset + length, 16)
for (index in 0..15) ciphertext[ciphertextOffset + length + index] =
ciphertext[ciphertextOffset + length + index].toInt() xor hashKey[index].toInt()
return length + 16
}
@Throws(ShortBufferException::class, BadPaddingException::class)
override fun decryptWithAd(
ad: ByteArray?,
ciphertext: ByteArray,
ciphertextOffset: Int,
plaintext: ByteArray,
plaintextOffset: Int,
length: Int
): Int {
var space: Int
require(!(ciphertextOffset < 0 || ciphertextOffset > ciphertext.size))
space = ciphertext.size - ciphertextOffset
if (length > space) throw ShortBufferException()
require(!(length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.size || length > ciphertext.size || (ciphertext.size - ciphertextOffset) < length))
space = plaintext.size - plaintextOffset
if (!haskey) {
// The key is not set yet - return the ciphertext as-is.
if (length > space) throw ShortBufferException()
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
ciphertext,
ciphertextOffset,
plaintext,
plaintextOffset,
length
)
return length
}
if (length < 16) throwBadTagException()
val dataLen = length - 16
if (dataLen > space) throw ShortBufferException()
setup(ad)
ghash.update(ciphertext, ciphertextOffset, dataLen)
ghash.pad((if (ad != null) ad.size else 0).toLong(), dataLen.toLong())
ghash.finish(enciv, 0, 16)
var temp = 0
for (index in 0..15) temp =
temp or (hashKey[index].toInt() xor enciv[index].toInt() xor ciphertext[ciphertextOffset + dataLen + index].toInt())
if ((temp and 0xFF) != 0) throwBadTagException()
encryptCTR(ciphertext, ciphertextOffset, plaintext, plaintextOffset, dataLen)
return dataLen
}
override fun fork(key: ByteArray, offset: Int): CipherState {
val cipher: CipherState
cipher = AESGCMFallbackCipherState()
cipher.initializeKey(key, offset)
return cipher
}
override fun setNonce(nonce: Long) {
n = nonce
}
}

View File

@ -1,335 +0,0 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import com.bitchat.android.noise.southernstorm.crypto.GHASH;
/**
* Emulates the "AESGCM" cipher for Noise using the "AES/CTR/NoPadding"
* transformation from JCA/JCE.
*
* This class is used on platforms that don't have "AES/GCM/NoPadding",
* but which do have the older "AES/CTR/NoPadding".
*/
class AESGCMOnCtrCipherState implements CipherState {
private Cipher cipher;
private SecretKeySpec keySpec;
private long n;
private byte[] iv;
private byte[] hashKey;
private GHASH ghash;
/**
* Constructs a new cipher state for the "AESGCM" algorithm.
*
* @throws NoSuchAlgorithmException The system does not have a
* provider for this algorithm.
*/
public AESGCMOnCtrCipherState() throws NoSuchAlgorithmException
{
try {
cipher = Cipher.getInstance("AES/CTR/NoPadding");
} catch (NoSuchPaddingException e) {
// AES/CTR is available, but not the unpadded version? Huh?
throw new NoSuchAlgorithmException("AES/CTR/NoPadding not available", e);
}
keySpec = null;
n = 0;
iv = new byte [16];
hashKey = new byte [16];
ghash = new GHASH();
// Try to set a 256-bit key on the cipher. Some JCE's are
// configured to disallow 256-bit AES if an extra policy
// file has not been installed.
try {
SecretKeySpec spec = new SecretKeySpec(new byte [32], "AES");
IvParameterSpec params = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, spec, params);
} catch (InvalidKeyException e) {
throw new NoSuchAlgorithmException("AES/CTR/NoPadding does not support 256-bit keys", e);
} catch (InvalidAlgorithmParameterException e) {
throw new NoSuchAlgorithmException("AES/CTR/NoPadding does not support 256-bit keys", e);
}
}
@Override
public void destroy() {
// There doesn't seem to be a standard API to clean out a Cipher.
// So we instead set the key and IV to all-zeroes to hopefully
// destroy the sensitive data in the cipher instance.
ghash.destroy();
Noise.destroy(hashKey);
Noise.destroy(iv);
keySpec = new SecretKeySpec(new byte [32], "AES");
IvParameterSpec params = new IvParameterSpec(iv);
try {
cipher.init(Cipher.ENCRYPT_MODE, keySpec, params);
} catch (InvalidKeyException e) {
// Shouldn't happen.
} catch (InvalidAlgorithmParameterException e) {
// Shouldn't happen.
}
}
@Override
public String getCipherName() {
return "AESGCM";
}
@Override
public int getKeyLength() {
return 32;
}
@Override
public int getMACLength() {
return keySpec != null ? 16 : 0;
}
@Override
public void initializeKey(byte[] key, int offset) {
// Set the encryption key.
keySpec = new SecretKeySpec(key, offset, 32, "AES");
// Generate the hashing key by encrypting a block of zeroes.
Arrays.fill(iv, (byte)0);
Arrays.fill(hashKey, (byte)0);
try {
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv));
} catch (InvalidKeyException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
} catch (InvalidAlgorithmParameterException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
try {
int result = cipher.update(hashKey, 0, 16, hashKey, 0);
cipher.doFinal(hashKey, result);
} catch (ShortBufferException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
} catch (IllegalBlockSizeException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
} catch (BadPaddingException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
ghash.reset(hashKey, 0);
// Reset the nonce.
n = 0;
}
@Override
public boolean hasKey() {
return keySpec != null;
}
/**
* Set up to encrypt or decrypt the next packet.
*
* @param ad The associated data for the packet.
*/
private void setup(byte[] ad) throws InvalidKeyException, InvalidAlgorithmParameterException
{
// Check for nonce wrap-around.
if (n == -1L)
throw new IllegalStateException("Nonce has wrapped around");
// Format the counter/IV block for AES/CTR/NoPadding.
iv[0] = 0;
iv[1] = 0;
iv[2] = 0;
iv[3] = 0;
iv[4] = (byte)(n >> 56);
iv[5] = (byte)(n >> 48);
iv[6] = (byte)(n >> 40);
iv[7] = (byte)(n >> 32);
iv[8] = (byte)(n >> 24);
iv[9] = (byte)(n >> 16);
iv[10] = (byte)(n >> 8);
iv[11] = (byte)n;
iv[12] = 0;
iv[13] = 0;
iv[14] = 0;
iv[15] = 1;
++n;
// Initialize the CTR mode cipher with the key and IV.
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv));
// Encrypt a block of zeroes to generate the hash key to XOR
// the GHASH tag with at the end of the encrypt/decrypt operation.
Arrays.fill(hashKey, (byte)0);
try {
cipher.update(hashKey, 0, 16, hashKey, 0);
} catch (ShortBufferException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
// Initialize the GHASH with the associated data value.
ghash.reset();
if (ad != null) {
ghash.update(ad, 0, ad.length);
ghash.pad();
}
}
@Override
public int encryptWithAd(byte[] ad, byte[] plaintext, int plaintextOffset,
byte[] ciphertext, int ciphertextOffset, int length)
throws ShortBufferException {
int space;
if (ciphertextOffset < 0 || ciphertextOffset > ciphertext.length)
throw new IllegalArgumentException();
if (length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.length || length > plaintext.length || (plaintext.length - plaintextOffset) < length)
throw new IllegalArgumentException();
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 {
setup(ad);
int result = cipher.update(plaintext, plaintextOffset, length, ciphertext, ciphertextOffset);
cipher.doFinal(ciphertext, ciphertextOffset + result);
} catch (InvalidKeyException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
} catch (InvalidAlgorithmParameterException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
} catch (IllegalBlockSizeException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
} catch (BadPaddingException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
ghash.update(ciphertext, ciphertextOffset, length);
ghash.pad(ad != null ? ad.length : 0, length);
ghash.finish(ciphertext, ciphertextOffset + length, 16);
for (int index = 0; index < 16; ++index)
ciphertext[ciphertextOffset + length + index] ^= hashKey[index];
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 < 0 || ciphertextOffset > ciphertext.length)
throw new IllegalArgumentException();
else
space = ciphertext.length - ciphertextOffset;
if (length > space)
throw new ShortBufferException();
if (length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.length || length > ciphertext.length || (ciphertext.length - ciphertextOffset) < length)
throw new IllegalArgumentException();
space = plaintext.length - plaintextOffset;
if (keySpec == null) {
// 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();
try {
setup(ad);
} catch (InvalidKeyException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
} catch (InvalidAlgorithmParameterException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
ghash.update(ciphertext, ciphertextOffset, dataLen);
ghash.pad(ad != null ? ad.length : 0, dataLen);
ghash.finish(iv, 0, 16);
int temp = 0;
for (int index = 0; index < 16; ++index)
temp |= (hashKey[index] ^ iv[index] ^ ciphertext[ciphertextOffset + dataLen + index]);
if ((temp & 0xFF) != 0)
Noise.throwBadTagException();
try {
int result = cipher.update(ciphertext, ciphertextOffset, dataLen, plaintext, plaintextOffset);
cipher.doFinal(plaintext, plaintextOffset + result);
} catch (IllegalBlockSizeException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
} catch (BadPaddingException e) {
// Shouldn't happen.
throw new IllegalStateException(e);
}
return dataLen;
}
@Override
public CipherState fork(byte[] key, int offset) {
CipherState cipher;
try {
cipher = new AESGCMOnCtrCipherState();
} catch (NoSuchAlgorithmException e) {
// Shouldn't happen.
return null;
}
cipher.initializeKey(key, offset);
return cipher;
}
@Override
public void setNonce(long nonce) {
n = nonce;
}
}

View File

@ -0,0 +1,333 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol
import com.bitchat.android.noise.southernstorm.crypto.GHASH
import com.bitchat.android.noise.southernstorm.protocol.Noise.destroy
import com.bitchat.android.noise.southernstorm.protocol.Noise.throwBadTagException
import java.security.InvalidAlgorithmParameterException
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.util.Arrays
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.IllegalBlockSizeException
import javax.crypto.NoSuchPaddingException
import javax.crypto.ShortBufferException
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
* Emulates the "AESGCM" cipher for Noise using the "AES/CTR/NoPadding"
* transformation from JCA/JCE.
*
* This class is used on platforms that don't have "AES/GCM/NoPadding",
* but which do have the older "AES/CTR/NoPadding".
*/
internal class AESGCMOnCtrCipherState : CipherState {
private val cipher: Cipher? = null
private var keySpec: SecretKeySpec?
private var n: Long
private val iv: ByteArray
private val hashKey: ByteArray
private val ghash: GHASH
/**
* Constructs a new cipher state for the "AESGCM" algorithm.
*
* @throws NoSuchAlgorithmException The system does not have a
* provider for this algorithm.
*/
init {
try {
cipher = Cipher.getInstance("AES/CTR/NoPadding")
} catch (e: NoSuchPaddingException) {
// AES/CTR is available, but not the unpadded version? Huh?
throw NoSuchAlgorithmException("AES/CTR/NoPadding not available", e)
}
keySpec = null
n = 0
iv = ByteArray(16)
hashKey = ByteArray(16)
ghash = GHASH()
// Try to set a 256-bit key on the cipher. Some JCE's are
// configured to disallow 256-bit AES if an extra policy
// file has not been installed.
try {
val spec = SecretKeySpec(ByteArray(32), "AES")
val params = IvParameterSpec(iv)
cipher.init(Cipher.ENCRYPT_MODE, spec, params)
} catch (e: InvalidKeyException) {
throw NoSuchAlgorithmException("AES/CTR/NoPadding does not support 256-bit keys", e)
} catch (e: InvalidAlgorithmParameterException) {
throw NoSuchAlgorithmException("AES/CTR/NoPadding does not support 256-bit keys", e)
}
}
override fun destroy() {
// There doesn't seem to be a standard API to clean out a Cipher.
// So we instead set the key and IV to all-zeroes to hopefully
// destroy the sensitive data in the cipher instance.
ghash.destroy()
destroy(hashKey)
destroy(iv)
keySpec = SecretKeySpec(ByteArray(32), "AES")
val params = IvParameterSpec(iv)
try {
cipher!!.init(Cipher.ENCRYPT_MODE, keySpec, params)
} catch (e: InvalidKeyException) {
// Shouldn't happen.
} catch (e: InvalidAlgorithmParameterException) {
// Shouldn't happen.
}
}
val cipherName: String
get() = "AESGCM"
val keyLength: Int
get() = 32
val mACLength: Int
get() = if (keySpec != null) 16 else 0
override fun initializeKey(key: ByteArray?, offset: Int) {
// Set the encryption key.
keySpec = SecretKeySpec(key, offset, 32, "AES")
// Generate the hashing key by encrypting a block of zeroes.
Arrays.fill(iv, 0.toByte())
Arrays.fill(hashKey, 0.toByte())
try {
cipher!!.init(Cipher.ENCRYPT_MODE, keySpec, IvParameterSpec(iv))
} catch (e: InvalidKeyException) {
// Shouldn't happen.
throw IllegalStateException(e)
} catch (e: InvalidAlgorithmParameterException) {
// Shouldn't happen.
throw IllegalStateException(e)
}
try {
val result = cipher.update(hashKey, 0, 16, hashKey, 0)
cipher.doFinal(hashKey, result)
} catch (e: ShortBufferException) {
// Shouldn't happen.
throw IllegalStateException(e)
} catch (e: IllegalBlockSizeException) {
// Shouldn't happen.
throw IllegalStateException(e)
} catch (e: BadPaddingException) {
// Shouldn't happen.
throw IllegalStateException(e)
}
ghash.reset(hashKey, 0)
// Reset the nonce.
n = 0
}
override fun hasKey(): Boolean {
return keySpec != null
}
/**
* Set up to encrypt or decrypt the next packet.
*
* @param ad The associated data for the packet.
*/
@Throws(InvalidKeyException::class, InvalidAlgorithmParameterException::class)
private fun setup(ad: ByteArray?) {
// Check for nonce wrap-around.
check(n != -1L) { "Nonce has wrapped around" }
// Format the counter/IV block for AES/CTR/NoPadding.
iv[0] = 0
iv[1] = 0
iv[2] = 0
iv[3] = 0
iv[4] = (n shr 56).toByte()
iv[5] = (n shr 48).toByte()
iv[6] = (n shr 40).toByte()
iv[7] = (n shr 32).toByte()
iv[8] = (n shr 24).toByte()
iv[9] = (n shr 16).toByte()
iv[10] = (n shr 8).toByte()
iv[11] = n.toByte()
iv[12] = 0
iv[13] = 0
iv[14] = 0
iv[15] = 1
++n
// Initialize the CTR mode cipher with the key and IV.
cipher!!.init(Cipher.ENCRYPT_MODE, keySpec, IvParameterSpec(iv))
// Encrypt a block of zeroes to generate the hash key to XOR
// the GHASH tag with at the end of the encrypt/decrypt operation.
Arrays.fill(hashKey, 0.toByte())
try {
cipher.update(hashKey, 0, 16, hashKey, 0)
} catch (e: ShortBufferException) {
// Shouldn't happen.
throw IllegalStateException(e)
}
// Initialize the GHASH with the associated data value.
ghash.reset()
if (ad != null) {
ghash.update(ad, 0, ad.size)
ghash.pad()
}
}
@Throws(ShortBufferException::class)
override fun encryptWithAd(
ad: ByteArray?, plaintext: ByteArray, plaintextOffset: Int,
ciphertext: ByteArray, ciphertextOffset: Int, length: Int
): Int {
val space: Int
require(!(ciphertextOffset < 0 || ciphertextOffset > ciphertext.size))
require(!(length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.size || length > plaintext.size || (plaintext.size - plaintextOffset) < length))
space = ciphertext.size - ciphertextOffset
if (keySpec == null) {
// The key is not set yet - return the plaintext as-is.
if (length > space) throw ShortBufferException()
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
plaintext,
plaintextOffset,
ciphertext,
ciphertextOffset,
length
)
return length
}
if (space < 16 || length > (space - 16)) throw ShortBufferException()
try {
setup(ad)
val result =
cipher!!.update(plaintext, plaintextOffset, length, ciphertext, ciphertextOffset)
cipher.doFinal(ciphertext, ciphertextOffset + result)
} catch (e: InvalidKeyException) {
// Shouldn't happen.
throw IllegalStateException(e)
} catch (e: InvalidAlgorithmParameterException) {
// Shouldn't happen.
throw IllegalStateException(e)
} catch (e: IllegalBlockSizeException) {
// Shouldn't happen.
throw IllegalStateException(e)
} catch (e: BadPaddingException) {
// Shouldn't happen.
throw IllegalStateException(e)
}
ghash.update(ciphertext, ciphertextOffset, length)
ghash.pad((if (ad != null) ad.size else 0).toLong(), length.toLong())
ghash.finish(ciphertext, ciphertextOffset + length, 16)
for (index in 0..15) ciphertext[ciphertextOffset + length + index] =
ciphertext[ciphertextOffset + length + index].toInt() xor hashKey[index].toInt()
return length + 16
}
@Throws(ShortBufferException::class, BadPaddingException::class)
override fun decryptWithAd(
ad: ByteArray?,
ciphertext: ByteArray,
ciphertextOffset: Int,
plaintext: ByteArray,
plaintextOffset: Int,
length: Int
): Int {
var space: Int
require(!(ciphertextOffset < 0 || ciphertextOffset > ciphertext.size))
space = ciphertext.size - ciphertextOffset
if (length > space) throw ShortBufferException()
require(!(length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.size || length > ciphertext.size || (ciphertext.size - ciphertextOffset) < length))
space = plaintext.size - plaintextOffset
if (keySpec == null) {
// The key is not set yet - return the ciphertext as-is.
if (length > space) throw ShortBufferException()
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
ciphertext,
ciphertextOffset,
plaintext,
plaintextOffset,
length
)
return length
}
if (length < 16) throwBadTagException()
val dataLen = length - 16
if (dataLen > space) throw ShortBufferException()
try {
setup(ad)
} catch (e: InvalidKeyException) {
// Shouldn't happen.
throw IllegalStateException(e)
} catch (e: InvalidAlgorithmParameterException) {
// Shouldn't happen.
throw IllegalStateException(e)
}
ghash.update(ciphertext, ciphertextOffset, dataLen)
ghash.pad((if (ad != null) ad.size else 0).toLong(), dataLen.toLong())
ghash.finish(iv, 0, 16)
var temp = 0
for (index in 0..15) temp =
temp or (hashKey[index].toInt() xor iv[index].toInt() xor ciphertext[ciphertextOffset + dataLen + index].toInt())
if ((temp and 0xFF) != 0) throwBadTagException()
try {
val result =
cipher!!.update(ciphertext, ciphertextOffset, dataLen, plaintext, plaintextOffset)
cipher.doFinal(plaintext, plaintextOffset + result)
} catch (e: IllegalBlockSizeException) {
// Shouldn't happen.
throw IllegalStateException(e)
} catch (e: BadPaddingException) {
// Shouldn't happen.
throw IllegalStateException(e)
}
return dataLen
}
override fun fork(key: ByteArray, offset: Int): CipherState {
val cipher: CipherState
try {
cipher = AESGCMOnCtrCipherState()
} catch (e: NoSuchAlgorithmException) {
// Shouldn't happen.
return null
}
cipher.initializeKey(key, offset)
return cipher
}
override fun setNonce(nonce: Long) {
n = nonce
}
}

View File

@ -1,290 +0,0 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.ShortBufferException;
import com.bitchat.android.noise.southernstorm.crypto.ChaChaCore;
import com.bitchat.android.noise.southernstorm.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.INSTANCE.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.INSTANCE.initIV(input, n++);
ChaChaCore.INSTANCE.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.INSTANCE.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 < 0 || ciphertextOffset > ciphertext.length)
throw new IllegalArgumentException();
if (length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.length || length > plaintext.length || (plaintext.length - plaintextOffset) < length)
throw new IllegalArgumentException();
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 < 0 || ciphertextOffset > ciphertext.length)
throw new IllegalArgumentException();
else
space = ciphertext.length - ciphertextOffset;
if (length > space)
throw new ShortBufferException();
if (length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.length || length > ciphertext.length || (ciphertext.length - ciphertextOffset) < length)
throw new IllegalArgumentException();
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;
}
}

View File

@ -0,0 +1,296 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol
import com.bitchat.android.noise.southernstorm.crypto.ChaChaCore.hash
import com.bitchat.android.noise.southernstorm.crypto.ChaChaCore.initIV
import com.bitchat.android.noise.southernstorm.crypto.ChaChaCore.initKey256
import com.bitchat.android.noise.southernstorm.crypto.Poly1305
import com.bitchat.android.noise.southernstorm.protocol.Noise.destroy
import com.bitchat.android.noise.southernstorm.protocol.Noise.throwBadTagException
import java.util.Arrays
import javax.crypto.BadPaddingException
import javax.crypto.ShortBufferException
/**
* Implements the ChaChaPoly cipher for Noise.
*/
internal class ChaChaPolyCipherState : CipherState {
private val poly: Poly1305
private val input: IntArray
private val output: IntArray
private val polyKey: ByteArray
var n: Long = 0
private var haskey = false
/**
* Constructs a new cipher state for the "ChaChaPoly" algorithm.
*/
init {
poly = Poly1305()
input = IntArray(16)
output = IntArray(16)
polyKey = ByteArray(32)
}
override fun destroy() {
poly.destroy()
Arrays.fill(input, 0)
Arrays.fill(output, 0)
destroy(polyKey)
}
val cipherName: String
get() = "ChaChaPoly"
val keyLength: Int
get() = 32
val mACLength: Int
get() = if (haskey) 16 else 0
override fun initializeKey(key: ByteArray, offset: Int) {
initKey256(input, key, offset)
n = 0
haskey = true
}
override fun hasKey(): Boolean {
return haskey
}
/**
* Set up to encrypt or decrypt the next packet.
*
* @param ad The associated data for the packet.
*/
private fun setup(ad: ByteArray?) {
check(n != -1L) { "Nonce has wrapped around" }
initIV(input, n++)
hash(output, input)
Arrays.fill(polyKey, 0.toByte())
xorBlock(polyKey, 0, polyKey, 0, 32, output)
poly.reset(polyKey, 0)
if (ad != null) {
poly.update(ad, 0, ad.size)
poly.pad()
}
if (++(input[12]) == 0) ++(input[13])
}
/**
* Finishes up the authentication tag for a packet.
*
* @param ad The associated data.
* @param length The length of the plaintext data.
*/
private fun finish(ad: ByteArray?, length: Int) {
poly.pad()
putLittleEndian64(polyKey, 0, (if (ad != null) ad.size else 0).toLong())
putLittleEndian64(polyKey, 8, length.toLong())
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 fun encrypt(
plaintext: ByteArray, plaintextOffset: Int,
ciphertext: ByteArray, ciphertextOffset: Int, length: Int
) {
var plaintextOffset = plaintextOffset
var ciphertextOffset = ciphertextOffset
var length = length
while (length > 0) {
var tempLen = 64
if (tempLen > length) tempLen = length
hash(output, input)
xorBlock(plaintext, plaintextOffset, ciphertext, ciphertextOffset, tempLen, output)
if (++(input[12]) == 0) ++(input[13])
plaintextOffset += tempLen
ciphertextOffset += tempLen
length -= tempLen
}
}
@Throws(ShortBufferException::class)
override fun encryptWithAd(
ad: ByteArray?, plaintext: ByteArray, plaintextOffset: Int,
ciphertext: ByteArray, ciphertextOffset: Int, length: Int
): Int {
val space: Int
require(!(ciphertextOffset < 0 || ciphertextOffset > ciphertext.size))
require(!(length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.size || length > plaintext.size || (plaintext.size - plaintextOffset) < length))
space = ciphertext.size - ciphertextOffset
if (!haskey) {
// The key is not set yet - return the plaintext as-is.
if (length > space) throw ShortBufferException()
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
plaintext,
plaintextOffset,
ciphertext,
ciphertextOffset,
length
)
return length
}
if (space < 16 || length > (space - 16)) throw 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
}
@Throws(ShortBufferException::class, BadPaddingException::class)
override fun decryptWithAd(
ad: ByteArray?,
ciphertext: ByteArray,
ciphertextOffset: Int,
plaintext: ByteArray,
plaintextOffset: Int,
length: Int
): Int {
var space: Int
require(!(ciphertextOffset < 0 || ciphertextOffset > ciphertext.size))
space = ciphertext.size - ciphertextOffset
if (length > space) throw ShortBufferException()
require(!(length < 0 || plaintextOffset < 0 || plaintextOffset > plaintext.size || length > ciphertext.size || (ciphertext.size - ciphertextOffset) < length))
space = plaintext.size - plaintextOffset
if (!haskey) {
// The key is not set yet - return the ciphertext as-is.
if (length > space) throw ShortBufferException()
if (plaintext != ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
ciphertext,
ciphertextOffset,
plaintext,
plaintextOffset,
length
)
return length
}
if (length < 16) throwBadTagException()
val dataLen = length - 16
if (dataLen > space) throw ShortBufferException()
setup(ad)
poly.update(ciphertext, ciphertextOffset, dataLen)
finish(ad, dataLen)
var temp = 0
for (index in 0..15) temp =
temp or (polyKey[index].toInt() xor ciphertext[ciphertextOffset + dataLen + index].toInt())
if ((temp and 0xFF) != 0) throwBadTagException()
encrypt(ciphertext, ciphertextOffset, plaintext, plaintextOffset, dataLen)
return dataLen
}
override fun fork(key: ByteArray, offset: Int): CipherState {
val cipher: CipherState = ChaChaPolyCipherState()
cipher.initializeKey(key, offset)
return cipher
}
override fun setNonce(nonce: Long) {
n = nonce
}
companion object {
/**
* 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 fun xorBlock(
input: ByteArray,
inputOffset: Int,
output: ByteArray,
outputOffset: Int,
length: Int,
block: IntArray
) {
var inputOffset = inputOffset
var outputOffset = outputOffset
var length = length
var posn = 0
var value: Int
while (length >= 4) {
value = block[posn++]
output[outputOffset] = (input[inputOffset].toInt() xor value).toByte()
output[outputOffset + 1] =
(input[inputOffset + 1].toInt() xor (value shr 8)).toByte()
output[outputOffset + 2] =
(input[inputOffset + 2].toInt() xor (value shr 16)).toByte()
output[outputOffset + 3] =
(input[inputOffset + 3].toInt() xor (value shr 24)).toByte()
inputOffset += 4
outputOffset += 4
length -= 4
}
if (length == 3) {
value = block[posn]
output[outputOffset] = (input[inputOffset].toInt() xor value).toByte()
output[outputOffset + 1] =
(input[inputOffset + 1].toInt() xor (value shr 8)).toByte()
output[outputOffset + 2] =
(input[inputOffset + 2].toInt() xor (value shr 16)).toByte()
} else if (length == 2) {
value = block[posn]
output[outputOffset] = (input[inputOffset].toInt() xor value).toByte()
output[outputOffset + 1] =
(input[inputOffset + 1].toInt() xor (value shr 8)).toByte()
} else if (length == 1) {
value = block[posn]
output[outputOffset] = (input[inputOffset].toInt() xor value).toByte()
}
}
/**
* 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 fun putLittleEndian64(output: ByteArray, offset: Int, value: Long) {
output[offset] = value.toByte()
output[offset + 1] = (value shr 8).toByte()
output[offset + 2] = (value shr 16).toByte()
output[offset + 3] = (value shr 24).toByte()
output[offset + 4] = (value shr 32).toByte()
output[offset + 5] = (value shr 40).toByte()
output[offset + 6] = (value shr 48).toByte()
output[offset + 7] = (value shr 56).toByte()
}
}
}

View File

@ -53,9 +53,6 @@ interface CipherState : Destroyable {
* @return The length of MAC values in bytes, or zero if the
* key has not yet been initialized.
*/
// JvmName preserved so existing Java impls/callers (getMACLength) keep
// resolving — Kotlin would otherwise expose this as getMacLength.
@get:JvmName("getMACLength")
val macLength: Int
/**

View File

@ -1,156 +0,0 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol;
import java.util.Arrays;
import com.bitchat.android.noise.southernstorm.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;
}
}

View File

@ -0,0 +1,131 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol
import com.bitchat.android.noise.southernstorm.crypto.Curve25519
import com.bitchat.android.noise.southernstorm.protocol.Noise.destroy
import com.bitchat.android.noise.southernstorm.protocol.Noise.random
import java.util.Arrays
/**
* Implementation of the Curve25519 algorithm for the Noise protocol.
*/
internal class Curve25519DHState : DHState {
private val publicKey: ByteArray
private val privateKey: ByteArray
private var mode = 0
/**
* Constructs a new Diffie-Hellman object for Curve25519.
*/
init {
publicKey = ByteArray(32)
privateKey = ByteArray(32)
}
override fun destroy() {
clearKey()
}
override fun getDHName(): String {
return "25519"
}
override fun getPublicKeyLength(): Int {
return 32
}
override fun getPrivateKeyLength(): Int {
return 32
}
override fun getSharedKeyLength(): Int {
return 32
}
override fun generateKeyPair() {
random(privateKey)
Curve25519.eval(publicKey, 0, privateKey, null)
mode = 0x03
}
override fun getPublicKey(key: ByteArray, offset: Int) {
System.arraycopy(publicKey, 0, key, offset, 32)
}
override fun setPublicKey(key: ByteArray, offset: Int) {
System.arraycopy(key, offset, publicKey, 0, 32)
Arrays.fill(privateKey, 0.toByte())
mode = 0x01
}
override fun getPrivateKey(key: ByteArray, offset: Int) {
System.arraycopy(privateKey, 0, key, offset, 32)
}
override fun setPrivateKey(key: ByteArray, offset: Int) {
System.arraycopy(key, offset, privateKey, 0, 32)
Curve25519.eval(publicKey, 0, privateKey, null)
mode = 0x03
}
override fun setToNullPublicKey() {
Arrays.fill(publicKey, 0.toByte())
Arrays.fill(privateKey, 0.toByte())
mode = 0x01
}
override fun clearKey() {
destroy(publicKey)
destroy(privateKey)
mode = 0
}
override fun hasPublicKey(): Boolean {
return (mode and 0x01) != 0
}
override fun hasPrivateKey(): Boolean {
return (mode and 0x02) != 0
}
override fun isNullPublicKey(): Boolean {
if ((mode and 0x01) == 0) return false
var temp = 0
for (index in 0..31) temp = temp or publicKey[index].toInt()
return temp == 0
}
override fun calculate(sharedKey: ByteArray, offset: Int, publicDH: DHState?) {
require(publicDH is Curve25519DHState) { "Incompatible DH algorithms" }
Curve25519.eval(sharedKey, offset, privateKey, publicDH.publicKey)
}
override fun copyFrom(other: DHState?) {
check(other is Curve25519DHState) { "Mismatched DH key objects" }
if (other === this) return
val dh = other
System.arraycopy(dh.privateKey, 0, privateKey, 0, 32)
System.arraycopy(dh.publicKey, 0, publicKey, 0, 32)
mode = dh.mode
}
}

View File

@ -1,156 +0,0 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol;
import java.util.Arrays;
import com.bitchat.android.noise.southernstorm.crypto.Curve448;
/**
* Implementation of the Curve448 algorithm for the Noise protocol.
*/
class Curve448DHState implements DHState {
private byte[] publicKey;
private byte[] privateKey;
private int mode;
/**
* Constructs a new Diffie-Hellman object for Curve448.
*/
public Curve448DHState()
{
publicKey = new byte [56];
privateKey = new byte [56];
mode = 0;
}
@Override
public void destroy() {
clearKey();
}
@Override
public String getDHName() {
return "448";
}
@Override
public int getPublicKeyLength() {
return 56;
}
@Override
public int getPrivateKeyLength() {
return 56;
}
@Override
public int getSharedKeyLength() {
return 56;
}
@Override
public void generateKeyPair() {
Noise.random(privateKey);
Curve448.eval(publicKey, 0, privateKey, null);
mode = 0x03;
}
@Override
public void getPublicKey(byte[] key, int offset) {
System.arraycopy(publicKey, 0, key, offset, 56);
}
@Override
public void setPublicKey(byte[] key, int offset) {
System.arraycopy(key, offset, publicKey, 0, 56);
Arrays.fill(privateKey, (byte)0);
mode = 0x01;
}
@Override
public void getPrivateKey(byte[] key, int offset) {
System.arraycopy(privateKey, 0, key, offset, 56);
}
@Override
public void setPrivateKey(byte[] key, int offset) {
System.arraycopy(key, offset, privateKey, 0, 56);
Curve448.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 < 56; ++index)
temp |= publicKey[index];
return temp == 0;
}
@Override
public void calculate(byte[] sharedKey, int offset, DHState publicDH) {
if (!(publicDH instanceof Curve448DHState))
throw new IllegalArgumentException("Incompatible DH algorithms");
Curve448.eval(sharedKey, offset, privateKey, ((Curve448DHState)publicDH).publicKey);
}
@Override
public void copyFrom(DHState other) {
if (!(other instanceof Curve448DHState))
throw new IllegalStateException("Mismatched DH key objects");
if (other == this)
return;
Curve448DHState dh = (Curve448DHState)other;
System.arraycopy(dh.privateKey, 0, privateKey, 0, 56);
System.arraycopy(dh.publicKey, 0, publicKey, 0, 56);
mode = dh.mode;
}
}

View File

@ -0,0 +1,131 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol
import com.bitchat.android.noise.southernstorm.crypto.Curve448
import com.bitchat.android.noise.southernstorm.protocol.Noise.destroy
import com.bitchat.android.noise.southernstorm.protocol.Noise.random
import java.util.Arrays
/**
* Implementation of the Curve448 algorithm for the Noise protocol.
*/
internal class Curve448DHState : DHState {
private val publicKey: ByteArray
private val privateKey: ByteArray
private var mode = 0
/**
* Constructs a new Diffie-Hellman object for Curve448.
*/
init {
publicKey = ByteArray(56)
privateKey = ByteArray(56)
}
override fun destroy() {
clearKey()
}
override fun getDHName(): String {
return "448"
}
override fun getPublicKeyLength(): Int {
return 56
}
override fun getPrivateKeyLength(): Int {
return 56
}
override fun getSharedKeyLength(): Int {
return 56
}
override fun generateKeyPair() {
random(privateKey)
Curve448.eval(publicKey, 0, privateKey, null)
mode = 0x03
}
override fun getPublicKey(key: ByteArray, offset: Int) {
System.arraycopy(publicKey, 0, key, offset, 56)
}
override fun setPublicKey(key: ByteArray, offset: Int) {
System.arraycopy(key, offset, publicKey, 0, 56)
Arrays.fill(privateKey, 0.toByte())
mode = 0x01
}
override fun getPrivateKey(key: ByteArray, offset: Int) {
System.arraycopy(privateKey, 0, key, offset, 56)
}
override fun setPrivateKey(key: ByteArray, offset: Int) {
System.arraycopy(key, offset, privateKey, 0, 56)
Curve448.eval(publicKey, 0, privateKey, null)
mode = 0x03
}
override fun setToNullPublicKey() {
Arrays.fill(publicKey, 0.toByte())
Arrays.fill(privateKey, 0.toByte())
mode = 0x01
}
override fun clearKey() {
destroy(publicKey)
destroy(privateKey)
mode = 0
}
override fun hasPublicKey(): Boolean {
return (mode and 0x01) != 0
}
override fun hasPrivateKey(): Boolean {
return (mode and 0x02) != 0
}
override fun isNullPublicKey(): Boolean {
if ((mode and 0x01) == 0) return false
var temp = 0
for (index in 0..55) temp = temp or publicKey[index].toInt()
return temp == 0
}
override fun calculate(sharedKey: ByteArray, offset: Int, publicDH: DHState?) {
require(publicDH is Curve448DHState) { "Incompatible DH algorithms" }
Curve448.eval(sharedKey, offset, privateKey, publicDH.publicKey)
}
override fun copyFrom(other: DHState?) {
check(other is Curve448DHState) { "Mismatched DH key objects" }
if (other === this) return
val dh = other
System.arraycopy(dh.privateKey, 0, privateKey, 0, 56)
System.arraycopy(dh.publicKey, 0, publicKey, 0, 56)
mode = dh.mode
}
}

View File

@ -1,156 +0,0 @@
/*
* 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.bitchat.android.noise.southernstorm.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);
}

View File

@ -0,0 +1,154 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol
/**
* Interface to a Diffie-Hellman algorithm for the Noise protocol.
*/
interface DHState : Destroyable {
/**
* Gets the Noise protocol name for this Diffie-Hellman algorithm.
*
* @return The algorithm name.
*/
val dHName: String?
/**
* Gets the length of public keys for this algorithm.
*
* @return The length of public keys in bytes.
*/
val publicKeyLength: Int
/**
* Gets the length of private keys for this algorithm.
*
* @return The length of private keys in bytes.
*/
val privateKeyLength: Int
/**
* Gets the length of shared keys for this algorithm.
*
* @return The length of shared keys in bytes.
*/
val sharedKeyLength: Int
/**
* Generates a new random keypair.
*/
fun 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.
*/
fun getPublicKey(key: ByteArray?, offset: Int)
/**
* 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.
*/
fun setPublicKey(key: ByteArray?, offset: Int)
/**
* 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.
*/
fun getPrivateKey(key: ByteArray?, offset: Int)
/**
* 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.
*/
fun setPrivateKey(key: ByteArray?, offset: Int)
/**
* Sets this object to the null public key and clears the private key.
*/
fun setToNullPublicKey()
/**
* Clears the key pair.
*/
fun 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.
*/
fun hasPublicKey(): Boolean
/**
* 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.
*/
fun hasPrivateKey(): Boolean
/**
* 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.
*/
val isNullPublicKey: Boolean
/**
* 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.
*/
fun calculate(sharedKey: ByteArray?, offset: Int, publicDH: DHState?)
/**
* 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.
*/
fun copyFrom(other: DHState?)
}

View File

@ -19,42 +19,40 @@
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.bitchat.android.noise.southernstorm.protocol;
package com.bitchat.android.noise.southernstorm.protocol
/**
* Additional API for DH objects that need special handling for
* hybrid operations.
*/
public interface DHStateHybrid extends DHState {
interface DHStateHybrid : DHState {
/**
* Generates a new random keypair relative to the parameters
* in another object.
*
* @param remote The remote party in this communication to obtain parameters.
*
* @throws IllegalStateException The other or remote DH object does not have
* the same type as this object.
*/
fun generateKeyPair(remote: DHState?)
/**
* Generates a new random keypair relative to the parameters
* in another object.
*
* @param remote The remote party in this communication to obtain parameters.
*
* @throws IllegalStateException The other or remote DH object does not have
* the same type as this object.
*/
void generateKeyPair(DHState remote);
/**
* Copies the key values from another DH object of the same type.
*
* @param other The other DH object to copy from
* @param remote The remote party in this communication to obtain parameters.
*
* @throws IllegalStateException The other or remote DH object does not have
* the same type as this object.
*/
fun copyFrom(other: DHState?, remote: DHState?)
/**
* Copies the key values from another DH object of the same type.
*
* @param other The other DH object to copy from
* @param remote The remote party in this communication to obtain parameters.
*
* @throws IllegalStateException The other or remote DH object does not have
* the same type as this object.
*/
void copyFrom(DHState other, DHState remote);
/**
* Specifies the local peer object prior to setting a public key
* on a remote object.
*
* @param local The local peer object.
*/
void specifyPeer(DHState local);
/**
* Specifies the local peer object prior to setting a public key
* on a remote object.
*
* @param local The local peer object.
*/
fun specifyPeer(local: DHState?)
}

View File

@ -1,340 +0,0 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol;
import java.util.Arrays;
import com.bitchat.android.noise.southernstorm.crypto.NewHope;
import com.bitchat.android.noise.southernstorm.crypto.NewHopeTor;
/**
* Implementation of the New Hope post-quantum algorithm for the Noise protocol.
*/
final class NewHopeDHState implements DHStateHybrid {
enum KeyType
{
None,
AlicePrivate,
AlicePublic,
BobPrivate,
BobPublic,
BobCalculated;
}
private NewHopeTor nh;
private byte[] publicKey;
private byte[] privateKey;
private KeyType keyType;
/**
* Special version of NewHopeTor that allows explicit random data
* to be specified for test vectors.
*/
private class NewHopeWithPrivateKey extends NewHopeTor {
byte[] randomData;
public NewHopeWithPrivateKey(byte[] randomData)
{
this.randomData = randomData;
}
@Override
protected void randombytes(byte[] buffer)
{
System.arraycopy(randomData, 0, buffer, 0, buffer.length);
}
}
/**
* Constructs a new key exchange object for New Hope.
*/
public NewHopeDHState() {
nh = null;
publicKey = null;
privateKey = null;
keyType = KeyType.None;
}
private boolean isAlice() {
return keyType == KeyType.AlicePrivate || keyType == KeyType.AlicePublic;
}
@Override
public void destroy() {
clearKey();
}
@Override
public String getDHName() {
return "NewHope";
}
@Override
public int getPublicKeyLength() {
if (isAlice())
return NewHope.SENDABYTES;
else
return NewHope.SENDBBYTES;
}
@Override
public int getPrivateKeyLength() {
// New Hope doesn't have private keys in the same sense as
// Curve25519 and Curve448. Instead return the number of
// random bytes that we need to generate each key type.
if (isAlice())
return 64;
else
return 32;
}
@Override
public int getSharedKeyLength() {
return NewHope.SHAREDBYTES;
}
@Override
public void generateKeyPair() {
clearKey();
keyType = KeyType.AlicePrivate;
nh = new NewHopeTor();
publicKey = new byte [NewHope.SENDABYTES];
nh.keygen(publicKey, 0);
}
@Override
public void generateKeyPair(DHState remote) {
if (remote == null) {
// No remote public key, so always generate in Alice mode.
generateKeyPair();
return;
} else if (!(remote instanceof NewHopeDHState)) {
throw new IllegalStateException("Mismatched DH objects");
}
NewHopeDHState r = (NewHopeDHState)remote;
if (r.isAlice() && r.publicKey != null) {
// We have a remote public key for Alice, so generate in Bob mode.
clearKey();
keyType = KeyType.BobCalculated;
nh = new NewHopeTor();
publicKey = new byte [NewHope.SENDBBYTES];
privateKey = new byte [NewHope.SHAREDBYTES];
nh.sharedb(privateKey, 0, publicKey, 0, r.publicKey, 0);
} else {
generateKeyPair();
}
}
@Override
public void getPublicKey(byte[] key, int offset) {
if (publicKey != null)
System.arraycopy(publicKey, 0, key, offset, getPublicKeyLength());
else
Arrays.fill(key, 0, getPublicKeyLength(), (byte)0);
}
@Override
public void setPublicKey(byte[] key, int offset) {
if (publicKey != null)
Noise.destroy(publicKey);
publicKey = new byte [getPublicKeyLength()];
System.arraycopy(key, 0, publicKey, 0, publicKey.length);
}
@Override
public void getPrivateKey(byte[] key, int offset) {
if (privateKey != null)
System.arraycopy(privateKey, 0, key, offset, getPrivateKeyLength());
else
Arrays.fill(key, 0, getPrivateKeyLength(), (byte)0);
}
@Override
public void setPrivateKey(byte[] key, int offset) {
clearKey();
// Guess the key type from the length of the test data.
if (offset == 0 && key.length == 64)
keyType = KeyType.AlicePrivate;
else
keyType = KeyType.BobPrivate;
privateKey = new byte [getPrivateKeyLength()];
System.arraycopy(key, 0, privateKey, 0, privateKey.length);
}
@Override
public void setToNullPublicKey() {
// Null public keys are not supported by New Hope.
// Destroy the current values but otherwise ignore.
clearKey();
}
@Override
public void clearKey() {
if (nh != null) {
nh.destroy();
nh = null;
}
if (publicKey != null) {
Noise.destroy(publicKey);
publicKey = null;
}
if (privateKey != null) {
Noise.destroy(privateKey);
privateKey = null;
}
keyType = KeyType.None;
}
@Override
public boolean hasPublicKey() {
return publicKey != null;
}
@Override
public boolean hasPrivateKey() {
return privateKey != null;
}
@Override
public boolean isNullPublicKey() {
return false;
}
@Override
public void calculate(byte[] sharedKey, int offset, DHState publicDH) {
if (!(publicDH instanceof NewHopeDHState))
throw new IllegalArgumentException("Incompatible DH algorithms");
NewHopeDHState other = (NewHopeDHState)publicDH;
if (keyType == KeyType.AlicePrivate) {
// Compute the shared key for Alice.
nh.shareda(sharedKey, 0, other.publicKey, 0);
} else if (keyType == KeyType.BobCalculated) {
// The shared key for Bob was already computed when the key was generated.
System.arraycopy(privateKey, 0, sharedKey, 0, NewHope.SHAREDBYTES);
} else {
throw new IllegalStateException("Cannot calculate with this DH object");
}
}
@Override
public void copyFrom(DHState other) {
if (!(other instanceof NewHopeDHState))
throw new IllegalStateException("Mismatched DH key objects");
if (other == this)
return;
NewHopeDHState dh = (NewHopeDHState)other;
clearKey();
switch (dh.keyType) {
case None:
break;
case AlicePrivate:
if (dh.privateKey != null) {
keyType = KeyType.AlicePrivate;
privateKey = new byte [dh.privateKey.length];
System.arraycopy(dh.privateKey, 0, privateKey, 0, privateKey.length);
} else {
throw new IllegalStateException("Cannot copy generated key for Alice");
}
break;
case BobPrivate:
case BobCalculated:
throw new IllegalStateException("Cannot copy private key for Bob without public key for Alice");
case AlicePublic:
case BobPublic:
keyType = dh.keyType;
publicKey = new byte [dh.publicKey.length];
System.arraycopy(dh.publicKey, 0, publicKey, 0, publicKey.length);
break;
}
}
@Override
public void copyFrom(DHState other, DHState remote) {
if (remote == null) {
copyFrom(other);
return;
}
if (!(other instanceof NewHopeDHState) || !(remote instanceof NewHopeDHState))
throw new IllegalStateException("Mismatched DH key objects");
if (other == this)
return;
NewHopeDHState dh = (NewHopeDHState)other;
NewHopeDHState remotedh = (NewHopeDHState)remote;
clearKey();
switch (dh.keyType) {
case None:
break;
case AlicePrivate:
if (dh.privateKey != null) {
// Generate Alice's public and private key now.
keyType = KeyType.AlicePrivate;
nh = new NewHopeWithPrivateKey(dh.privateKey);
publicKey = new byte [NewHope.SENDABYTES];
nh.keygen(publicKey, 0);
} else {
throw new IllegalStateException("Cannot copy generated key for Alice");
}
break;
case BobPrivate:
if (dh.privateKey != null && remotedh.keyType == KeyType.AlicePublic) {
// Now we know the public key for Alice, we can calculate Bob's public and shared keys.
keyType = KeyType.BobCalculated;
nh = new NewHopeWithPrivateKey(dh.privateKey);
publicKey = new byte [NewHope.SENDBBYTES];
privateKey = new byte [NewHope.SHAREDBYTES];
nh.sharedb(privateKey, 0, publicKey, 0, remotedh.publicKey, 0);
} else {
throw new IllegalStateException("Cannot copy private key for Bob without public key for Alice");
}
break;
case BobCalculated:
throw new IllegalStateException("Cannot copy generated key for Bob");
case AlicePublic:
case BobPublic:
keyType = dh.keyType;
publicKey = new byte [dh.publicKey.length];
System.arraycopy(dh.publicKey, 0, publicKey, 0, publicKey.length);
break;
}
}
@Override
public void specifyPeer(DHState local) {
if (!(local instanceof NewHopeDHState))
return;
clearKey();
if (((NewHopeDHState)local).keyType == KeyType.AlicePrivate)
keyType = KeyType.BobPublic;
else
keyType = KeyType.AlicePublic;
}
}

View File

@ -0,0 +1,266 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol
import com.bitchat.android.noise.southernstorm.crypto.NewHope
import com.bitchat.android.noise.southernstorm.crypto.NewHopeTor
import java.util.Arrays
/**
* Implementation of the New Hope post-quantum algorithm for the Noise protocol.
*/
internal class NewHopeDHState : DHStateHybrid {
internal enum class KeyType {
None,
AlicePrivate,
AlicePublic,
BobPrivate,
BobPublic,
BobCalculated
}
private var nh: NewHopeTor? = null
private var publicKey: ByteArray? = null
private var privateKey: ByteArray? = null
private var keyType: KeyType
/**
* Special version of NewHopeTor that allows explicit random data
* to be specified for test vectors.
*/
private inner class NewHopeWithPrivateKey(var randomData: ByteArray) : NewHopeTor() {
override fun randombytes(buffer: ByteArray) {
System.arraycopy(randomData, 0, buffer, 0, buffer.size)
}
}
/**
* Constructs a new key exchange object for New Hope.
*/
init {
keyType = KeyType.None
}
private val isAlice: Boolean
get() = keyType == KeyType.AlicePrivate || keyType == KeyType.AlicePublic
override fun destroy() {
clearKey()
}
override fun getDHName(): String {
return "NewHope"
}
override fun getPublicKeyLength(): Int {
if (this.isAlice) return NewHope.SENDABYTES
else return NewHope.SENDBBYTES
}
override fun getPrivateKeyLength(): Int {
// New Hope doesn't have private keys in the same sense as
// Curve25519 and Curve448. Instead return the number of
// random bytes that we need to generate each key type.
if (this.isAlice) return 64
else return 32
}
override fun getSharedKeyLength(): Int {
return NewHope.SHAREDBYTES
}
override fun generateKeyPair() {
clearKey()
keyType = KeyType.AlicePrivate
nh = NewHopeTor()
publicKey = ByteArray(NewHope.SENDABYTES)
nh!!.keygen(publicKey!!, 0)
}
override fun generateKeyPair(remote: DHState?) {
if (remote == null) {
// No remote public key, so always generate in Alice mode.
generateKeyPair()
return
} else check(remote is NewHopeDHState) { "Mismatched DH objects" }
val r = remote
if (r.isAlice && r.publicKey != null) {
// We have a remote public key for Alice, so generate in Bob mode.
clearKey()
keyType = KeyType.BobCalculated
nh = NewHopeTor()
publicKey = ByteArray(NewHope.SENDBBYTES)
privateKey = ByteArray(NewHope.SHAREDBYTES)
nh!!.sharedb(privateKey!!, 0, publicKey!!, 0, r.publicKey!!, 0)
} else {
generateKeyPair()
}
}
override fun getPublicKey(key: ByteArray, offset: Int) {
if (publicKey != null) System.arraycopy(publicKey, 0, key, offset, getPublicKeyLength())
else Arrays.fill(key, 0, getPublicKeyLength(), 0.toByte())
}
override fun setPublicKey(key: ByteArray, offset: Int) {
if (publicKey != null) Noise.destroy(publicKey!!)
publicKey = ByteArray(getPublicKeyLength())
System.arraycopy(key, 0, publicKey, 0, publicKey!!.size)
}
override fun getPrivateKey(key: ByteArray, offset: Int) {
if (privateKey != null) System.arraycopy(privateKey, 0, key, offset, getPrivateKeyLength())
else Arrays.fill(key, 0, getPrivateKeyLength(), 0.toByte())
}
override fun setPrivateKey(key: ByteArray, offset: Int) {
clearKey()
// Guess the key type from the length of the test data.
if (offset == 0 && key.size == 64) keyType = KeyType.AlicePrivate
else keyType = KeyType.BobPrivate
privateKey = ByteArray(getPrivateKeyLength())
System.arraycopy(key, 0, privateKey, 0, privateKey!!.size)
}
override fun setToNullPublicKey() {
// Null public keys are not supported by New Hope.
// Destroy the current values but otherwise ignore.
clearKey()
}
override fun clearKey() {
if (nh != null) {
nh!!.destroy()
nh = null
}
if (publicKey != null) {
Noise.destroy(publicKey!!)
publicKey = null
}
if (privateKey != null) {
Noise.destroy(privateKey!!)
privateKey = null
}
keyType = KeyType.None
}
override fun hasPublicKey(): Boolean {
return publicKey != null
}
override fun hasPrivateKey(): Boolean {
return privateKey != null
}
override fun isNullPublicKey(): Boolean {
return false
}
override fun calculate(sharedKey: ByteArray, offset: Int, publicDH: DHState?) {
require(publicDH is NewHopeDHState) { "Incompatible DH algorithms" }
val other = publicDH
if (keyType == KeyType.AlicePrivate) {
// Compute the shared key for Alice.
nh!!.shareda(sharedKey, 0, other.publicKey!!, 0)
} else if (keyType == KeyType.BobCalculated) {
// The shared key for Bob was already computed when the key was generated.
System.arraycopy(privateKey, 0, sharedKey, 0, NewHope.SHAREDBYTES)
} else {
throw IllegalStateException("Cannot calculate with this DH object")
}
}
override fun copyFrom(other: DHState?) {
check(other is NewHopeDHState) { "Mismatched DH key objects" }
if (other === this) return
val dh = other
clearKey()
when (dh.keyType) {
KeyType.None -> {}
KeyType.AlicePrivate -> if (dh.privateKey != null) {
keyType = KeyType.AlicePrivate
privateKey = ByteArray(dh.privateKey!!.size)
System.arraycopy(dh.privateKey, 0, privateKey, 0, privateKey!!.size)
} else {
throw IllegalStateException("Cannot copy generated key for Alice")
}
KeyType.BobPrivate, KeyType.BobCalculated -> throw IllegalStateException("Cannot copy private key for Bob without public key for Alice")
KeyType.AlicePublic, KeyType.BobPublic -> {
keyType = dh.keyType
publicKey = ByteArray(dh.publicKey!!.size)
System.arraycopy(dh.publicKey, 0, publicKey, 0, publicKey!!.size)
}
}
}
override fun copyFrom(other: DHState?, remote: DHState?) {
if (remote == null) {
copyFrom(other)
return
}
check(!(other !is NewHopeDHState || remote !is NewHopeDHState)) { "Mismatched DH key objects" }
if (other === this) return
val dh = other
val remotedh = remote
clearKey()
when (dh.keyType) {
KeyType.None -> {}
KeyType.AlicePrivate -> if (dh.privateKey != null) {
// Generate Alice's public and private key now.
keyType = KeyType.AlicePrivate
nh = NewHopeWithPrivateKey(dh.privateKey!!)
publicKey = ByteArray(NewHope.SENDABYTES)
nh!!.keygen(publicKey!!, 0)
} else {
throw IllegalStateException("Cannot copy generated key for Alice")
}
KeyType.BobPrivate -> if (dh.privateKey != null && remotedh.keyType == KeyType.AlicePublic) {
// Now we know the public key for Alice, we can calculate Bob's public and shared keys.
keyType = KeyType.BobCalculated
nh = NewHopeWithPrivateKey(dh.privateKey!!)
publicKey = ByteArray(NewHope.SENDBBYTES)
privateKey = ByteArray(NewHope.SHAREDBYTES)
nh!!.sharedb(privateKey!!, 0, publicKey!!, 0, remotedh.publicKey!!, 0)
} else {
throw IllegalStateException("Cannot copy private key for Bob without public key for Alice")
}
KeyType.BobCalculated -> throw IllegalStateException("Cannot copy generated key for Bob")
KeyType.AlicePublic, KeyType.BobPublic -> {
keyType = dh.keyType
publicKey = ByteArray(dh.publicKey!!.size)
System.arraycopy(dh.publicKey, 0, publicKey, 0, publicKey!!.size)
}
}
}
override fun specifyPeer(local: DHState?) {
if (local !is NewHopeDHState) return
clearKey()
if (local.keyType == KeyType.AlicePrivate) keyType = KeyType.BobPublic
else keyType = KeyType.AlicePublic
}
}

View File

@ -1,532 +0,0 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol;
import android.util.Log;
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 static final String TAG = "AndroidSymmetric";
private String name;
private CipherState cipher;
private MessageDigest hash;
private byte[] ck;
private byte[] h;
private byte[] prev_h;
/**
* Converts a byte array to hex string for logging (matching iOS hex format)
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
/**
* 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);
// LOGGING: Initial symmetric state after protocol name initialization (matching iOS)
Log.d(TAG, "=== ANDROID SYMMETRIC STATE INITIALIZED ===");
Log.d(TAG, "Protocol: " + protocolName);
Log.d(TAG, "Initial hash (h): " + bytesToHex(h));
Log.d(TAG, "Initial chaining key (ck): " + bytesToHex(ck));
Log.d(TAG, "Hash length: " + h.length);
Log.d(TAG, "=========================================");
}
/**
* 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.mACLength;
}
/**
* 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)
{
// LOGGING: Before mixKey operation (matching iOS)
byte[] inputData = new byte[length];
System.arraycopy(data, offset, inputData, 0, length);
Log.d(TAG, "*** Android mixKey() BEFORE ***");
Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData));
Log.d(TAG, "Current CK: " + bytesToHex(ck));
Log.d(TAG, "Current Hash: " + bytesToHex(h));
int keyLength = cipher.keyLength;
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);
}
// LOGGING: After mixKey operation (matching iOS)
Log.d(TAG, "*** Android mixKey() AFTER ***");
Log.d(TAG, "New CK: " + bytesToHex(ck));
Log.d(TAG, "Hash unchanged: " + bytesToHex(h));
Log.d(TAG, "Cipher now has key: " + (cipher.mACLength > 0));
}
/**
* 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)
{
// LOGGING: Before mixHash operation (matching iOS)
byte[] inputData = new byte[length];
System.arraycopy(data, offset, inputData, 0, length);
Log.d(TAG, "*** Android mixHash() BEFORE ***");
Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData));
Log.d(TAG, "Current Hash: " + bytesToHex(h));
hashTwo(h, 0, h.length, data, offset, length, h, 0, h.length);
// LOGGING: After mixHash operation (matching iOS)
Log.d(TAG, "*** Android mixHash() AFTER ***");
Log.d(TAG, "New Hash: " + bytesToHex(h));
}
/**
* 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.keyLength;
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);
}
}
}

View File

@ -0,0 +1,601 @@
/*
* 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.bitchat.android.noise.southernstorm.protocol
import android.util.Log
import com.bitchat.android.noise.southernstorm.protocol.Noise.createCipher
import com.bitchat.android.noise.southernstorm.protocol.Noise.createHash
import com.bitchat.android.noise.southernstorm.protocol.Noise.destroy
import java.io.UnsupportedEncodingException
import java.security.DigestException
import java.security.MessageDigest
import java.util.Arrays
import javax.crypto.BadPaddingException
import javax.crypto.ShortBufferException
/**
* Symmetric state for helping manage a Noise handshake.
*/
internal class SymmetricState(protocolName: String, cipherName: String, hashName: String) :
Destroyable {
/**
* Gets the name of the Noise protocol.
*
* @return The protocol name.
*/
val protocolName: String?
private var cipher: CipherState?
private var hash: MessageDigest?
private var ck: ByteArray?
/**
* 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.
*/
var handshakeHash: ByteArray?
private set
private var prev_h: ByteArray?
/**
* 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.
*/
init {
this.protocolName = protocolName
cipher = createCipher(cipherName)
hash = createHash(hashName)
val hashLength = hash!!.getDigestLength()
ck = ByteArray(hashLength)
this.handshakeHash = ByteArray(hashLength)
prev_h = ByteArray(hashLength)
val protocolNameBytes: ByteArray
try {
protocolNameBytes = protocolName.toByteArray(charset("UTF-8"))
} catch (e: UnsupportedEncodingException) {
// If UTF-8 is not supported, then we are definitely in trouble!
throw UnsupportedOperationException("UTF-8 encoding is not supported")
}
if (protocolNameBytes.size <= hashLength) {
System.arraycopy(
protocolNameBytes, 0,
this.handshakeHash, 0, protocolNameBytes.size
)
Arrays.fill(
this.handshakeHash,
protocolNameBytes.size,
handshakeHash!!.size,
0.toByte()
)
} else {
hashOne(
protocolNameBytes, 0, protocolNameBytes.size,
this.handshakeHash!!, 0, handshakeHash!!.size
)
}
System.arraycopy(this.handshakeHash, 0, ck, 0, hashLength)
// LOGGING: Initial symmetric state after protocol name initialization (matching iOS)
Log.d(TAG, "=== ANDROID SYMMETRIC STATE INITIALIZED ===")
Log.d(TAG, "Protocol: " + protocolName)
Log.d(
TAG, "Initial hash (h): " + Companion.bytesToHex(
this.handshakeHash!!
)
)
Log.d(TAG, "Initial chaining key (ck): " + Companion.bytesToHex(ck!!))
Log.d(TAG, "Hash length: " + handshakeHash!!.size)
Log.d(TAG, "=========================================")
}
val mACLength: Int
/**
* 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.
*/
get() = cipher.mACLength
/**
* 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.
*/
fun mixKey(data: ByteArray, offset: Int, length: Int) {
// LOGGING: Before mixKey operation (matching iOS)
val inputData = ByteArray(length)
System.arraycopy(data, offset, inputData, 0, length)
Log.d(TAG, "*** Android mixKey() BEFORE ***")
Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData))
Log.d(TAG, "Current CK: " + Companion.bytesToHex(ck!!))
Log.d(
TAG, "Current Hash: " + Companion.bytesToHex(
this.handshakeHash!!
)
)
val keyLength = cipher!!.keyLength
val tempKey = ByteArray(keyLength)
try {
hkdf(
ck!!,
0,
ck!!.size,
data,
offset,
length,
ck!!,
0,
ck!!.size,
tempKey,
0,
keyLength
)
cipher!!.initializeKey(tempKey, 0)
} finally {
destroy(tempKey)
}
// LOGGING: After mixKey operation (matching iOS)
Log.d(TAG, "*** Android mixKey() AFTER ***")
Log.d(TAG, "New CK: " + Companion.bytesToHex(ck!!))
Log.d(
TAG, "Hash unchanged: " + Companion.bytesToHex(
this.handshakeHash!!
)
)
Log.d(TAG, "Cipher now has key: " + (cipher.mACLength > 0))
}
/**
* 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.
*/
fun mixHash(data: ByteArray, offset: Int, length: Int) {
// LOGGING: Before mixHash operation (matching iOS)
val inputData = ByteArray(length)
System.arraycopy(data, offset, inputData, 0, length)
Log.d(TAG, "*** Android mixHash() BEFORE ***")
Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData))
Log.d(
TAG, "Current Hash: " + Companion.bytesToHex(
this.handshakeHash!!
)
)
hashTwo(
this.handshakeHash!!, 0, handshakeHash!!.size, data, offset, length,
this.handshakeHash!!, 0, handshakeHash!!.size
)
// LOGGING: After mixHash operation (matching iOS)
Log.d(TAG, "*** Android mixHash() AFTER ***")
Log.d(
TAG, "New Hash: " + Companion.bytesToHex(
this.handshakeHash!!
)
)
}
/**
* Mixes a pre-shared key into the chaining key and handshake hash.
*
* @param key The pre-shared key value.
*/
fun mixPreSharedKey(key: ByteArray) {
val temp = ByteArray(hash!!.getDigestLength())
try {
hkdf(ck!!, 0, ck!!.size, key, 0, key.size, ck!!, 0, ck!!.size, temp, 0, temp.size)
mixHash(temp, 0, temp.size)
} finally {
destroy(temp)
}
}
/**
* Mixes a pre-supplied public key into the handshake hash.
*
* @param dh The object containing the public key.
*/
fun mixPublicKey(dh: DHState) {
val temp = ByteArray(dh.getPublicKeyLength())
try {
dh.getPublicKey(temp, 0)
mixHash(temp, 0, temp.size)
} finally {
destroy(temp)
}
}
/**
* Mixes a pre-supplied public key into the chaining key.
*
* @param dh The object containing the public key.
*/
fun mixPublicKeyIntoCK(dh: DHState) {
val temp = ByteArray(dh.getPublicKeyLength())
try {
dh.getPublicKey(temp, 0)
mixKey(temp, 0, temp.size)
} finally {
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.
*/
@Throws(ShortBufferException::class)
fun encryptAndHash(
plaintext: ByteArray,
plaintextOffset: Int,
ciphertext: ByteArray,
ciphertextOffset: Int,
length: Int
): Int {
val ciphertextLength = cipher!!.encryptWithAd(
this.handshakeHash,
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.
*/
@Throws(ShortBufferException::class, BadPaddingException::class)
fun decryptAndHash(
ciphertext: ByteArray,
ciphertextOffset: Int,
plaintext: ByteArray,
plaintextOffset: Int,
length: Int
): Int {
System.arraycopy(this.handshakeHash, 0, prev_h, 0, handshakeHash!!.size)
mixHash(ciphertext, ciphertextOffset, length)
return cipher!!.decryptWithAd(
prev_h,
ciphertext,
ciphertextOffset,
plaintext,
plaintextOffset,
length
)
}
/**
* 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.
*/
/**
* Splits the symmetric state into two ciphers for session encryption.
*
* @return The pair of ciphers for sending and receiving.
*/
@JvmOverloads
fun split(
secondaryKey: ByteArray = ByteArray(0),
offset: Int = 0,
length: Int = 0
): CipherStatePair {
require(!(length != 0 && length != 32)) { "Secondary keys must be 0 or 32 bytes in length" }
val keyLength = cipher!!.keyLength
val k1 = ByteArray(keyLength)
val k2 = ByteArray(keyLength)
try {
hkdf(ck!!, 0, ck!!.size, secondaryKey, offset, length, k1, 0, k1.size, k2, 0, k2.size)
var c1: CipherState? = null
var c2: CipherState? = null
var pair: CipherStatePair? = null
try {
c1 = cipher!!.fork(k1, 0)
c2 = cipher!!.fork(k2, 0)
pair = 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 {
destroy(k1)
destroy(k2)
}
}
override fun 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 is Destroyable) (hash as Destroyable).destroy()
else hash!!.reset()
hash = null
}
if (ck != null) {
Noise.destroy(ck!!)
ck = null
}
if (this.handshakeHash != null) {
Noise.destroy(this.handshakeHash!!)
this.handshakeHash = 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 fun hashOne(
data: ByteArray,
offset: Int,
length: Int,
output: ByteArray,
outputOffset: Int,
outputLength: Int
) {
hash!!.reset()
hash!!.update(data, offset, length)
try {
hash!!.digest(output, outputOffset, outputLength)
} catch (e: DigestException) {
Arrays.fill(output, outputOffset, outputLength, 0.toByte())
}
}
/**
* 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 fun hashTwo(
data1: ByteArray, offset1: Int, length1: Int,
data2: ByteArray, offset2: Int, length2: Int,
output: ByteArray, outputOffset: Int, outputLength: Int
) {
hash!!.reset()
hash!!.update(data1, offset1, length1)
hash!!.update(data2, offset2, length2)
try {
hash!!.digest(output, outputOffset, outputLength)
} catch (e: DigestException) {
Arrays.fill(output, outputOffset, outputLength, 0.toByte())
}
}
/**
* 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 fun hmac(
key: ByteArray, keyOffset: Int, keyLength: Int,
data: ByteArray, dataOffset: Int, dataLength: Int,
output: ByteArray, outputOffset: Int, outputLength: Int
) {
// In all of the algorithms of interest to us, the block length
// is twice the size of the hash length.
val hashLength = hash!!.getDigestLength()
val blockLength = hashLength * 2
val block = ByteArray(blockLength)
var index: Int
try {
if (keyLength <= blockLength) {
System.arraycopy(key, keyOffset, block, 0, keyLength)
Arrays.fill(block, keyLength, blockLength, 0.toByte())
} else {
hash!!.reset()
hash!!.update(key, keyOffset, keyLength)
hash!!.digest(block, 0, hashLength)
Arrays.fill(block, hashLength, blockLength, 0.toByte())
}
index = 0
while (index < blockLength) {
block[index] = block[index].toInt() xor 0x36.toByte().toInt()
++index
}
hash!!.reset()
hash!!.update(block, 0, blockLength)
hash!!.update(data, dataOffset, dataLength)
hash!!.digest(output, outputOffset, hashLength)
index = 0
while (index < blockLength) {
block[index] = block[index].toInt() xor (0x36 xor 0x5C).toByte().toInt()
++index
}
hash!!.reset()
hash!!.update(block, 0, blockLength)
hash!!.update(output, outputOffset, hashLength)
hash!!.digest(output, outputOffset, outputLength)
} catch (e: DigestException) {
Arrays.fill(output, outputOffset, outputLength, 0.toByte())
} finally {
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 fun hkdf(
key: ByteArray, keyOffset: Int, keyLength: Int,
data: ByteArray, dataOffset: Int, dataLength: Int,
output1: ByteArray, output1Offset: Int, output1Length: Int,
output2: ByteArray, output2Offset: Int, output2Length: Int
) {
val hashLength = hash!!.getDigestLength()
val tempKey = ByteArray(hashLength)
val tempHash = ByteArray(hashLength + 1)
try {
hmac(key, keyOffset, keyLength, data, dataOffset, dataLength, tempKey, 0, hashLength)
tempHash[0] = 0x01.toByte()
hmac(tempKey, 0, hashLength, tempHash, 0, 1, tempHash, 0, hashLength)
System.arraycopy(tempHash, 0, output1, output1Offset, output1Length)
tempHash[hashLength] = 0x02.toByte()
hmac(tempKey, 0, hashLength, tempHash, 0, hashLength + 1, tempHash, 0, hashLength)
System.arraycopy(tempHash, 0, output2, output2Offset, output2Length)
} finally {
destroy(tempKey)
destroy(tempHash)
}
}
companion object {
private const val TAG = "AndroidSymmetric"
/**
* Converts a byte array to hex string for logging (matching iOS hex format)
*/
private fun bytesToHex(bytes: ByteArray): String {
val sb = StringBuilder()
for (b in bytes) {
sb.append(String.format("%02x", b))
}
return sb.toString()
}
}
}

View File

@ -1,7 +0,0 @@
/**
* Provides classes for communicating via the Noise protocol.
*
* Reference: http://noiseprotocol.org
*/
package com.bitchat.android.noise.southernstorm.protocol;