mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-11-01 07:51:15 +00:00
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:
parent
fbb1342439
commit
597d9a1ba9
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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?)
|
||||
}
|
||||
@ -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?)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
|
||||
/**
|
||||
* Provides classes for communicating via the Noise protocol.
|
||||
*
|
||||
* Reference: http://noiseprotocol.org
|
||||
*/
|
||||
package com.bitchat.android.noise.southernstorm.protocol;
|
||||
Loading…
x
Reference in New Issue
Block a user