From 597d9a1ba9c74442c42b064153f95cb8af06c22c Mon Sep 17 00:00:00 2001 From: yet Date: Fri, 15 May 2026 17:43:48 +0400 Subject: [PATCH] refactor(noise/protocol): drop invalid @JvmName on CipherState.macLength MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../protocol/AESGCMFallbackCipherState.java | 265 ---- .../protocol/AESGCMFallbackCipherState.kt | 263 ++++ .../protocol/AESGCMOnCtrCipherState.java | 335 ----- .../protocol/AESGCMOnCtrCipherState.kt | 333 +++++ .../protocol/ChaChaPolyCipherState.java | 290 ---- .../protocol/ChaChaPolyCipherState.kt | 296 ++++ .../southernstorm/protocol/CipherState.kt | 3 - .../protocol/Curve25519DHState.java | 156 -- .../protocol/Curve25519DHState.kt | 131 ++ .../protocol/Curve448DHState.java | 156 -- .../southernstorm/protocol/Curve448DHState.kt | 131 ++ .../noise/southernstorm/protocol/DHState.java | 156 -- .../noise/southernstorm/protocol/DHState.kt | 154 ++ .../{DHStateHybrid.java => DHStateHybrid.kt} | 60 +- .../protocol/HandshakeState.java | 1290 ----------------- .../southernstorm/protocol/HandshakeState.kt | 1196 +++++++++++++++ .../protocol/NewHopeDHState.java | 340 ----- .../southernstorm/protocol/NewHopeDHState.kt | 266 ++++ .../protocol/SymmetricState.java | 532 ------- .../southernstorm/protocol/SymmetricState.kt | 601 ++++++++ .../southernstorm/protocol/package-info.java | 7 - 21 files changed, 3400 insertions(+), 3561 deletions(-) delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMFallbackCipherState.java create mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMFallbackCipherState.kt delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMOnCtrCipherState.java create mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMOnCtrCipherState.kt delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/ChaChaPolyCipherState.java create mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/ChaChaPolyCipherState.kt delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve25519DHState.java create mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve25519DHState.kt delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve448DHState.java create mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve448DHState.kt delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHState.java create mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHState.kt rename app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/{DHStateHybrid.java => DHStateHybrid.kt} (53%) delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.java create mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.kt delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/NewHopeDHState.java create mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/NewHopeDHState.kt delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.java create mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.kt delete mode 100644 app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/package-info.java diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMFallbackCipherState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMFallbackCipherState.java deleted file mode 100644 index 683dcfc4..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMFallbackCipherState.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMFallbackCipherState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMFallbackCipherState.kt new file mode 100644 index 00000000..4820385a --- /dev/null +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMFallbackCipherState.kt @@ -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.. 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 + } +} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMOnCtrCipherState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMOnCtrCipherState.java deleted file mode 100644 index 7795cbdb..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMOnCtrCipherState.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMOnCtrCipherState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMOnCtrCipherState.kt new file mode 100644 index 00000000..9794bb88 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/AESGCMOnCtrCipherState.kt @@ -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 + } +} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/ChaChaPolyCipherState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/ChaChaPolyCipherState.java deleted file mode 100644 index a64182bd..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/ChaChaPolyCipherState.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/ChaChaPolyCipherState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/ChaChaPolyCipherState.kt new file mode 100644 index 00000000..8e988b39 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/ChaChaPolyCipherState.kt @@ -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() + } + } +} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/CipherState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/CipherState.kt index 67efd831..df29a681 100644 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/CipherState.kt +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/CipherState.kt @@ -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 /** diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve25519DHState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve25519DHState.java deleted file mode 100644 index 95b81e5a..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve25519DHState.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve25519DHState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve25519DHState.kt new file mode 100644 index 00000000..19a022eb --- /dev/null +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve25519DHState.kt @@ -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 + } +} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve448DHState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve448DHState.java deleted file mode 100644 index 2ab449f6..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve448DHState.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve448DHState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve448DHState.kt new file mode 100644 index 00000000..160e892e --- /dev/null +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/Curve448DHState.kt @@ -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 + } +} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHState.java deleted file mode 100644 index 2b2477ce..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHState.java +++ /dev/null @@ -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); -} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHState.kt new file mode 100644 index 00000000..a01259f8 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHState.kt @@ -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?) +} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHStateHybrid.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHStateHybrid.kt similarity index 53% rename from app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHStateHybrid.java rename to app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHStateHybrid.kt index 1446684b..9c714ddc 100644 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHStateHybrid.java +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/DHStateHybrid.kt @@ -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?) } diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.java deleted file mode 100644 index 91fec78a..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.java +++ /dev/null @@ -1,1290 +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.security.NoSuchAlgorithmException; -import java.util.Arrays; - -import javax.crypto.BadPaddingException; -import javax.crypto.ShortBufferException; - -/** - * Interface to a Noise handshake. - */ -public class HandshakeState implements Destroyable { - - private static final String TAG = "AndroidHandshake"; - - private SymmetricState symmetric; - private boolean isInitiator; - private DHState localKeyPair; - private DHState localEphemeral; - private DHState localHybrid; - private DHState remotePublicKey; - private DHState remoteEphemeral; - private DHState remoteHybrid; - private DHState fixedEphemeral; - private DHState fixedHybrid; - private int action; - private int requirements; - private short[] pattern; - private int patternIndex; - private byte[] preSharedKey; - private byte[] prologue; - - /** - * Enumerated value that indicates that the handshake object - * is handling the initiator role. - */ - public static final int INITIATOR = 1; - - /** - * Enumerated value that indicates that the handshake object - * is handling the responder role. - */ - public static final int RESPONDER = 2; - - /** - * No action is required of the application yet because the - * handshake has not started. - */ - public static final int NO_ACTION = 0; - - /** - * The HandshakeState expects the application to write the - * next message payload for the handshake. - */ - public static final int WRITE_MESSAGE = 1; - - /** - * The HandshakeState expects the application to read the - * next message payload from the handshake. - */ - public static final int READ_MESSAGE = 2; - - /** - * The handshake has failed due to some kind of error. - */ - public static final int FAILED = 3; - - /** - * The handshake is over and the application is expected to call - * split() and begin data session communications. - */ - public static final int SPLIT = 4; - - /** - * The handshake is complete and the data session ciphers - * have been split() out successfully. - */ - public static final int COMPLETE = 5; - - /** - * Local static keypair is required for the handshake. - */ - private static final int LOCAL_REQUIRED = 0x01; - - /** - * Remote static keypai is required for the handshake. - */ - private static final int REMOTE_REQUIRED = 0x02; - - /** - * Pre-shared key is required for the handshake. - */ - private static final int PSK_REQUIRED = 0x04; - - /** - * Ephemeral key for fallback pre-message has been provided. - */ - private static final int FALLBACK_PREMSG = 0x08; - - /** - * The local public key is part of the pre-message. - */ - private static final int LOCAL_PREMSG = 0x10; - - /** - * The remote public key is part of the pre-message. - */ - private static final int REMOTE_PREMSG = 0x20; - - /** - * Fallback is possible from this pattern (two-way, ends in "K"). - */ - private static final int FALLBACK_POSSIBLE = 0x40; - - /** - * Creates a new Noise handshake. - * - * @param protocolName The name of the Noise protocol. - * @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. - * - * @throws IllegalArgumentException The protocolName is not - * formatted correctly, or the role is not recognized. - * - * @throws NoSuchAlgorithmException One of the cryptographic algorithms - * that is specified in the protocolName is not supported. - */ - public HandshakeState(String protocolName, int role) throws NoSuchAlgorithmException - { - // Parse the protocol name into its components. - String[] components = protocolName.split("_"); - if (components.length != 5) - throw new IllegalArgumentException("Protocol name must have 5 components"); - String prefix = components[0]; - String patternId = components[1]; - String dh = components[2]; - String hybrid = null; - String cipher = components[3]; - String hash = components[4]; - if (!prefix.equals("Noise") && !prefix.equals("NoisePSK")) - throw new IllegalArgumentException("Prefix must be Noise or NoisePSK"); - pattern = Pattern.lookup(patternId); - if (pattern == null) - throw new IllegalArgumentException("Handshake pattern is not recognized"); - short flags = pattern[0]; - int extraReqs = 0; - if ((flags & Pattern.FLAG_REMOTE_REQUIRED) != 0 && patternId.length() > 1) - extraReqs |= FALLBACK_POSSIBLE; - if (role == RESPONDER) { - // Reverse the pattern flags so that the responder is "local". - flags = Pattern.reverseFlags(flags); - } - int index = dh.indexOf('+'); - if (index != -1) { - // The DH name has two components: regular and hybrid. - hybrid = dh.substring(index + 1); - dh = dh.substring(0, index); - if ((flags & Pattern.FLAG_LOCAL_HYBRID) == 0 || (flags & Pattern.FLAG_REMOTE_HYBRID) == 0) - throw new IllegalArgumentException("Hybrid function specified for non-hybrid pattern"); - } else { - if ((flags & Pattern.FLAG_LOCAL_HYBRID) != 0 || (flags & Pattern.FLAG_REMOTE_HYBRID) != 0) - throw new IllegalArgumentException("Hybrid function not specified for hybrid pattern"); - } - - // Check that the role is correctly specified. - if (role != INITIATOR && role != RESPONDER) - throw new IllegalArgumentException("Role must be initiator or responder"); - - // Initialize this object. This will also create the cipher and hash objects. - symmetric = new SymmetricState(protocolName, cipher, hash); - isInitiator = (role == INITIATOR); - action = NO_ACTION; - requirements = extraReqs | computeRequirements(flags, prefix, role, false); - patternIndex = 1; - - // Create the DH objects that we will need later. - if ((flags & Pattern.FLAG_LOCAL_STATIC) != 0) - localKeyPair = Noise.createDH(dh); - if ((flags & Pattern.FLAG_LOCAL_EPHEMERAL) != 0) - localEphemeral = Noise.createDH(dh); - if ((flags & Pattern.FLAG_LOCAL_HYBRID) != 0) - localHybrid = Noise.createDH(hybrid); - if ((flags & Pattern.FLAG_REMOTE_STATIC) != 0) - remotePublicKey = Noise.createDH(dh); - if ((flags & Pattern.FLAG_REMOTE_EPHEMERAL) != 0) - remoteEphemeral = Noise.createDH(dh); - if ((flags & Pattern.FLAG_REMOTE_HYBRID) != 0) - remoteHybrid = Noise.createDH(hybrid); - - // We cannot use hybrid algorithms like New Hope for ephemeral or static keys, - // as the unbalanced nature of the algorithm only works with "f" and "ff" tokens. - if (localKeyPair instanceof DHStateHybrid) - throw new NoSuchAlgorithmException("Cannot use '" + localKeyPair.getDHName() + "' for static keys"); - if (localEphemeral instanceof DHStateHybrid) - throw new NoSuchAlgorithmException("Cannot use '" + localEphemeral.getDHName() + "' for ephemeral keys"); - if (remotePublicKey instanceof DHStateHybrid) - throw new NoSuchAlgorithmException("Cannot use '" + remotePublicKey.getDHName() + "' for static keys"); - if (remoteEphemeral instanceof DHStateHybrid) - throw new NoSuchAlgorithmException("Cannot use '" + remoteEphemeral.getDHName() + "' for ephemeral keys"); - } - - /** - * Gets the name of the Noise protocol. - * - * @return The protocol name. - */ - public String getProtocolName() - { - return symmetric.getProtocolName(); - } - - /** - * Gets the role for this handshake. - * - * @return The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. - */ - public int getRole() - { - return isInitiator ? INITIATOR : RESPONDER; - } - - /** - * Determine if this handshake needs a pre-shared key value - * and one has not been configured yet. - * - * @return true if a pre-shared key is needed; false if not. - */ - public boolean needsPreSharedKey() - { - if (preSharedKey != null) - return false; - else - return (requirements & PSK_REQUIRED) != 0; - } - - /** - * Determine if this object has already been configured with a - * pre-shared key. - * - * @return true if the pre-shared key has already been configured; - * false if one is not needed or it has not been configured yet. - */ - public boolean hasPreSharedKey() - { - return preSharedKey != null; - } - - /** - * Sets the pre-shared key for this handshake. - * - * @param key Buffer containing the pre-shared key value. - * @param offset Offset into the buffer of the first byte of the key. - * @param length The length of the pre-shared key, which must be 32. - * - * @throws IllegalArgumentException The length is not 32. - * - * @throws UnsupportedOperationException Pre-shared keys are not - * supported for this handshake type. - * - * @throws IllegalStateException The handshake has already started, - * so the pre-shared key can no longer be set. - */ - public void setPreSharedKey(byte[] key, int offset, int length) - { - if (length != 32) { - throw new IllegalArgumentException - ("Pre-shared keys must be 32 bytes in length"); - } - if ((requirements & PSK_REQUIRED) == 0) { - throw new UnsupportedOperationException - ("Pre-shared keys are not supported for this handshake"); - } - if (action != NO_ACTION) { - throw new IllegalStateException - ("Handshake has already started; cannot set pre-shared key"); - } - if (preSharedKey != null) { - Noise.destroy(preSharedKey); - preSharedKey = null; - } - preSharedKey = Noise.copySubArray(key, offset, length); - } - - /** - * Sets the prologue for this handshake. - * - * @param prologue Buffer containing the prologue value. - * @param offset Offset into the buffer of the first byte of the prologue. - * @param length The length of the prologue in bytes. - * - * @throws IllegalStateException The handshake has already started, - * so the prologue can no longer be set. - */ - public void setPrologue(byte[] prologue, int offset, int length) - { - if (action != NO_ACTION) { - throw new IllegalStateException - ("Handshake has already started; cannot set prologue"); - } - if (this.prologue != null) { - Noise.destroy(this.prologue); - this.prologue = null; - } - this.prologue = Noise.copySubArray(prologue, offset, length); - } - - /** - * Gets the keypair object for the local static key. - * - * @return The keypair, or null if a local static key is not required. - */ - public DHState getLocalKeyPair() - { - return localKeyPair; - } - - /** - * Determine if this handshake requires a local static key. - * - * @return true if a local static key is needed; false if not. - * - * If the local static key has already been set, then this function - * will return false. - */ - public boolean needsLocalKeyPair() - { - if (localKeyPair != null) - return !localKeyPair.hasPrivateKey(); - else - return false; - } - - /** - * Determine if this handshake has already been configured - * with a local static key. - * - * @return true if the local static key has been configured; - * false if not. - */ - public boolean hasLocalKeyPair() - { - if (localKeyPair != null) - return localKeyPair.hasPrivateKey(); - else - return false; - } - - /** - * Gets the public key object for the remote static key. - * - * @return The public key, or null if a remote static key - * is not required. - */ - public DHState getRemotePublicKey() - { - return remotePublicKey; - } - - /** - * Determine if this handshake requires a remote static key. - * - * @return true if a remote static key is needed; false if not. - * - * If the remote static key has already been set, then this function - * will return false. - */ - public boolean needsRemotePublicKey() - { - if (remotePublicKey != null) - return !remotePublicKey.hasPublicKey(); - else - return false; - } - - /** - * Determine if this handshake has already been configured - * with a remote static key. - * - * @return true if the remote static key has been configured; - * false if not. - */ - public boolean hasRemotePublicKey() - { - if (remotePublicKey != null) - return remotePublicKey.hasPublicKey(); - else - return false; - } - - /** - * Gets the DHState object containing a fixed local ephemeral - * key value for this handshake. - * - * @return The fixed ephemeral key object, or null if a local - * ephemeral key is not required by this handshake. - * - * This function is intended for testing only. It can be used - * to establish a fixed ephemeral key for test vectors. This - * function should not be used in real applications. - */ - public DHState getFixedEphemeralKey() - { - if (fixedEphemeral != null) - return fixedEphemeral; - if (localEphemeral == null) - return null; - try { - fixedEphemeral = Noise.createDH(localEphemeral.getDHName()); - } catch (NoSuchAlgorithmException e) { - // This shouldn't happen - the local ephemeral key would - // have already been created with the same name! - fixedEphemeral = null; - } - return fixedEphemeral; - } - - /** - * Gets the DHState object containing a fixed local hybrid - * key value for this handshake. - * - * @return The fixed hybrid key object, or null if a local - * hybrid key is not required by this handshake. - * - * This function is intended for testing only. It can be used - * to establish a fixed hybrid key for test vectors. This - * function should not be used in real applications. - */ - public DHState getFixedHybridKey() - { - if (fixedHybrid != null) - return fixedHybrid; - if (localHybrid == null) - return null; - try { - fixedHybrid = Noise.createDH(localHybrid.getDHName()); - } catch (NoSuchAlgorithmException e) { - // This shouldn't happen - the local hybrid key would - // have already been created with the same name! - fixedHybrid = null; - } - return fixedHybrid; - } - - // Empty value for when the prologue is not supplied. - private static final byte[] emptyPrologue = new byte [0]; - - /** - * 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(); - } - - /** - * Starts the handshake running. - * - * This function is called after all of the handshake parameters have been - * provided to the HandshakeState object. This function should be followed - * by calls to writeMessage() or readMessage() to process the handshake - * messages. The getAction() function indicates the action to take next. - * - * @throws IllegalStateException The handshake has already started, or one or - * more of the required parameters has not been supplied. - * - * @throws UnsupportedOperationException An attempt was made to start a - * fallback handshake pattern without first calling fallback() on a - * previous handshake. - * - * @see #getAction() - * @see #writeMessage(byte[], int, byte[], int, int) - * @see #readMessage(byte[], int, int, byte[], int) - * @see #fallback() - */ - public void start() - { - if (action != NO_ACTION) { - throw new IllegalStateException - ("Handshake has already started; cannot start again"); - } - if ((pattern[0] & Pattern.FLAG_REMOTE_EPHEM_REQ) != 0 && - (requirements & FALLBACK_PREMSG) == 0) { - throw new UnsupportedOperationException - ("Cannot start a fallback pattern"); - } - - // Check that we have satisfied all of the pattern requirements. - if ((requirements & LOCAL_REQUIRED) != 0) { - if (localKeyPair == null || !localKeyPair.hasPrivateKey()) - throw new IllegalStateException("Local static key required"); - } - if ((requirements & REMOTE_REQUIRED) != 0) { - if (remotePublicKey == null || !remotePublicKey.hasPublicKey()) - throw new IllegalStateException("Remote static key required"); - } - if ((requirements & PSK_REQUIRED) != 0) { - if (preSharedKey == null) - throw new IllegalStateException("Pre-shared key required"); - } - - // Log the symmetric state BEFORE any mixing operations (matching iOS) - Log.d(TAG, "=== ANDROID HANDSHAKE START - INITIAL STATE ==="); - Log.d(TAG, "Protocol: " + symmetric.getProtocolName()); - Log.d(TAG, "Role: " + (isInitiator ? "INITIATOR" : "RESPONDER")); - Log.d(TAG, "Initial symmetric hash: " + bytesToHex(symmetric.getHandshakeHash())); - - // Hash the prologue value. - Log.d(TAG, "Mixing empty prologue"); - if (prologue != null) - symmetric.mixHash(prologue, 0, prologue.length); - else - symmetric.mixHash(emptyPrologue, 0, 0); - Log.d(TAG, "Hash after empty prologue: " + bytesToHex(symmetric.getHandshakeHash())); - - // Hash the pre-shared key into the chaining key and handshake hash. - if (preSharedKey != null) - symmetric.mixPreSharedKey(preSharedKey); - - // Mix the pre-supplied public keys into the handshake hash. - if (isInitiator) { - Log.d(TAG, "XX pattern - no pre-message keys to mix"); - if ((requirements & LOCAL_PREMSG) != 0) - symmetric.mixPublicKey(localKeyPair); - if ((requirements & FALLBACK_PREMSG) != 0) { - symmetric.mixPublicKey(remoteEphemeral); - if (remoteHybrid != null) - symmetric.mixPublicKey(remoteHybrid); - if (preSharedKey != null) - symmetric.mixPublicKeyIntoCK(remoteEphemeral); - } - if ((requirements & REMOTE_PREMSG) != 0) - symmetric.mixPublicKey(remotePublicKey); - } else { - Log.d(TAG, "XX pattern - no pre-message keys to mix"); - if ((requirements & REMOTE_PREMSG) != 0) - symmetric.mixPublicKey(remotePublicKey); - if ((requirements & FALLBACK_PREMSG) != 0) { - symmetric.mixPublicKey(localEphemeral); - if (localHybrid != null) - symmetric.mixPublicKey(localHybrid); - if (preSharedKey != null) - symmetric.mixPublicKeyIntoCK(localEphemeral); - } - if ((requirements & LOCAL_PREMSG) != 0) - symmetric.mixPublicKey(localKeyPair); - } - - // Log final state after all initialization (matching iOS) - Log.d(TAG, "=== ANDROID HANDSHAKE START - FINAL STATE ==="); - Log.d(TAG, "Final symmetric hash after mixPreMessageKeys(): " + bytesToHex(symmetric.getHandshakeHash())); - Log.d(TAG, "==========================================="); - - // The handshake has officially started - set the first action. - if (isInitiator) - action = WRITE_MESSAGE; - else - action = READ_MESSAGE; - } - - /** - * Falls back to the "XXfallback" handshake pattern. - * - * This function is intended used to help implement the "Noise Pipes" protocol. - * It resets a HandshakeState object with the original handshake pattern - * (usually "IK"), converting it into an object with the handshake pattern - * "XXfallback". Information from the previous session such as the local - * keypair, the initiator's ephemeral key, the prologue value, and the - * pre-shared key, are passed to the new session. - * - * Once the fallback has been initiated, the application can set - * new values for the handshake parameters if the values from the - * previous session do not apply. For example, the application may - * use a different prologue for the fallback than for the original - * session. - * - * After setting any new parameters, the application calls start() - * again to restart the handshake from where it left off before the fallback. - * - * Note that this function reverses the roles of initiator and responder. - * - * @throws UnsupportedOperationException The current handshake pattern - * is not compatible with "XXfallback". - * - * @throws IllegalStateException The previous protocol has not started - * or it has not reached the fallback position yet. - * - * @throws NoSuchAlgorithmException One of the cryptographic algorithms - * that is specified in the new protocolName is not supported. - * - * @see #start() - */ - public void fallback() throws NoSuchAlgorithmException - { - fallback("XXfallback"); - } - - /** - * Falls back to another handshake pattern. - * - * @param patternName The name of the pattern to fall back to; - * e.g. "XXfallback", "NXfallback", etc. - * - * This function resets a HandshakeState object with the original - * handshake pattern, and converts it into an object with the new handshake - * patternName. Information from the previous session such as the local - * keypair, the initiator's ephemeral key, the prologue value, and the - * pre-shared key, are passed to the new session. - * - * Once the fallback has been initiated, the application can set - * new values for the handshake parameters if the values from the - * previous session do not apply. For example, the application may - * use a different prologue for the fallback than for the original - * session. - * - * After setting any new parameters, the application calls start() - * again to restart the handshake from where it left off before the fallback. - * - * The new pattern may have greater key requirements than the original; - * for example changing from "NK" from "XXfallback" requires that the - * initiator's static public key be set. The application is responsible for - * setting any extra keys before calling start(). - * - * Note that this function reverses the roles of initiator and responder. - * - * @throws UnsupportedOperationException The current handshake pattern - * is not compatible with the patternName, or patternName is not a - * fallback pattern. - * - * @throws IllegalStateException The previous protocol has not started - * or it has not reached the fallback position yet. - * - * @throws NoSuchAlgorithmException One of the cryptographic algorithms - * that is specified in the new protocolName is not supported. - * - * @see #start() - */ - public void fallback(String patternName) throws NoSuchAlgorithmException - { - // The original pattern must end in "K" for fallback to be possible. - if ((requirements & FALLBACK_POSSIBLE) == 0) - throw new UnsupportedOperationException("Previous handshake pattern does not support fallback"); - - // Check that "patternName" supports fallback. - short[] newPattern = Pattern.lookup(patternName); - if (newPattern == null || (newPattern[0] & Pattern.FLAG_REMOTE_EPHEM_REQ) == 0) - throw new UnsupportedOperationException("New pattern is not a fallback pattern"); - - // The initiator should be waiting for a return message from the - // responder, and the responder should have failed on the first - // handshake message from the initiator. We also allow the - // responder to fallback after processing the first message - // successfully; it decides to always fall back anyway. - if (isInitiator) { - if ((action != FAILED && action != READ_MESSAGE) || !localEphemeral.hasPublicKey()) - throw new IllegalStateException("Initiator cannot fall back from this state"); - } else { - if ((action != FAILED && action != WRITE_MESSAGE) || !remoteEphemeral.hasPublicKey()) - throw new IllegalStateException("Responder cannot fall back from this state"); - } - - // Format a new protocol name for the fallback variant - // and recreate the SymmetricState object. - String[] components = symmetric.getProtocolName().split("_"); - components[1] = patternName; - StringBuilder builder = new StringBuilder(); - builder.append(components[0]); - for (int index = 1; index < components.length; ++index) { - builder.append('_'); - builder.append(components[index]); - } - String name = builder.toString(); - SymmetricState newSymmetric = new SymmetricState(name, components[3], components[4]); - symmetric.destroy(); - symmetric = newSymmetric; - - // Convert the HandshakeState to the "XXfallback" pattern. - if (isInitiator) { - if (remoteEphemeral != null) - remoteEphemeral.clearKey(); - if (remoteHybrid != null) - remoteHybrid.clearKey(); - if (remotePublicKey != null) - remotePublicKey.clearKey(); - isInitiator = false; - } else { - if (localEphemeral != null) - localEphemeral.clearKey(); - if (localHybrid != null) - localHybrid.clearKey(); - if ((newPattern[0] & Pattern.FLAG_REMOTE_REQUIRED) == 0 && remotePublicKey != null) - remotePublicKey.clearKey(); - isInitiator = true; - } - action = NO_ACTION; - pattern = newPattern; - patternIndex = 1; - short flags = pattern[0]; - if (!isInitiator) { - // Reverse the pattern flags so that the responder is "local". - flags = Pattern.reverseFlags(flags); - } - requirements = computeRequirements(flags, components[0], isInitiator ? INITIATOR : RESPONDER, true); - } - - /** - * Gets the next action that the application should perform for - * the handshake part of the protocol. - * - * @return One of HandshakeState.NO_ACTION, HandshakeState.WRITE_MESSAGE, - * HandshakeState.READ_MESSAGE, HandshakeState.SPLIT, or - * HandshakeState.FAILED. - */ - public int getAction() - { - return action; - } - - /** - * Mixes the result of a Diffie-Hellman calculation into the chaining key. - * - * @param local Local private key object. - * @param remote Remote public key object. - */ - private void mixDH(DHState local, DHState remote) - { - if (local == null || remote == null) - throw new IllegalStateException("Pattern definition error"); - int len = local.getSharedKeyLength(); - byte[] shared = new byte [len]; - try { - local.calculate(shared, 0, remote); - symmetric.mixKey(shared, 0, len); - } finally { - Noise.destroy(shared); - } - } - - /** - * Writes a message payload during the handshake. - * - * @param message The buffer that will be populated with the - * handshake packet to be written to the transport. - * @param messageOffset First offset within the message buffer - * to be populated. - * @param payload Buffer containing the payload to add to the - * handshake message; can be null if there is no payload. - * @param payloadOffset Offset into the payload buffer of the - * first payload buffer. - * @param payloadLength Length of the payload in bytes. - * - * @return The length of the data written to the message buffer. - * - * @throws IllegalStateException The action is not WRITE_MESSAGE. - * - * @throws IllegalArgumentException The payload is null, but - * payloadOffset or payloadLength is non-zero. - * - * @throws ShortBufferException The message buffer does not have - * enough space for the handshake message. - * - * @see #getAction() - * @see #readMessage(byte[], int, int, byte[], int) - */ - public int writeMessage(byte[] message, int messageOffset, byte[] payload, int payloadOffset, int payloadLength) throws ShortBufferException - { - int messagePosn = messageOffset; - boolean success = false; - - // Validate the parameters and state. - if (action != WRITE_MESSAGE) { - throw new IllegalStateException - ("Handshake state does not allow writing messages"); - } - if (payload == null && (payloadOffset != 0 || payloadLength != 0)) { - throw new IllegalArgumentException("Invalid payload argument"); - } - if (messageOffset > message.length) { - throw new ShortBufferException(); - } - - // Format the message. - try { - // Process tokens until the direction changes or the patten ends. - for (;;) { - if (patternIndex >= pattern.length) { - // The pattern has finished, so the next action is "split". - action = SPLIT; - break; - } - short token = pattern[patternIndex++]; - if (token == Pattern.FLIP_DIR) { - // Change directions, so this message is complete and the - // next action is "read message". - action = READ_MESSAGE; - break; - } - int space = message.length - messagePosn; - int len, macLen; - switch (token) { - case Pattern.E: - { - // Generate a local ephemeral keypair and add the public - // key to the message. If we are running fixed vector tests, - // then the ephemeral key may have already been provided. - if (localEphemeral == null) - throw new IllegalStateException("Pattern definition error"); - if (fixedEphemeral == null) - localEphemeral.generateKeyPair(); - else - localEphemeral.copyFrom(fixedEphemeral); - len = localEphemeral.getPublicKeyLength(); - if (space < len) - throw new ShortBufferException(); - localEphemeral.getPublicKey(message, messagePosn); - symmetric.mixHash(message, messagePosn, len); - - // If the protocol is using pre-shared keys, then also mix - // the local ephemeral key into the chaining key. - if (preSharedKey != null) - symmetric.mixKey(message, messagePosn, len); - messagePosn += len; - } - break; - - case Pattern.S: - { - // Encrypt the local static public key and add it to the message. - if (localKeyPair == null) - throw new IllegalStateException("Pattern definition error"); - len = localKeyPair.getPublicKeyLength(); - macLen = symmetric.getMACLength(); - if (space < (len + macLen)) - throw new ShortBufferException(); - localKeyPair.getPublicKey(message, messagePosn); - messagePosn += symmetric.encryptAndHash(message, messagePosn, message, messagePosn, len); - } - break; - - case Pattern.EE: - { - // DH operation with initiator and responder ephemeral keys. - mixDH(localEphemeral, remoteEphemeral); - } - break; - - case Pattern.ES: - { - // DH operation with initiator ephemeral and responder static keys. - if (isInitiator) - mixDH(localEphemeral, remotePublicKey); - else - mixDH(localKeyPair, remoteEphemeral); - } - break; - - case Pattern.SE: - { - // DH operation with initiator static and responder ephemeral keys. - if (isInitiator) - mixDH(localKeyPair, remoteEphemeral); - else - mixDH(localEphemeral, remotePublicKey); - } - break; - - case Pattern.SS: - { - // DH operation with initiator and responder static keys. - mixDH(localKeyPair, remotePublicKey); - } - break; - - case Pattern.F: - { - // Generate a local hybrid keypair and add the public - // key to the message. If we are running fixed vector tests, - // then a fixed hybrid key may have already been provided. - if (localHybrid == null) - throw new IllegalStateException("Pattern definition error"); - if (localHybrid instanceof DHStateHybrid) { - // The DH object is something like New Hope which needs to - // generate keys relative to the other party's public key. - DHStateHybrid hybrid = (DHStateHybrid)localHybrid; - if (fixedHybrid == null) - hybrid.generateKeyPair(remoteHybrid); - else - hybrid.copyFrom(fixedHybrid, remoteHybrid); - } else { - if (fixedHybrid == null) - localHybrid.generateKeyPair(); - else - localHybrid.copyFrom(fixedHybrid); - } - len = localHybrid.getPublicKeyLength(); - if (space < len) - throw new ShortBufferException(); - macLen = symmetric.getMACLength(); - if (space < (len + macLen)) - throw new ShortBufferException(); - localHybrid.getPublicKey(message, messagePosn); - messagePosn += symmetric.encryptAndHash(message, messagePosn, message, messagePosn, len); - } - break; - - case Pattern.FF: - { - // DH operation with initiator and responder hybrid keys. - mixDH(localHybrid, remoteHybrid); - } - break; - - default: - { - // Unknown token code. Abort. - throw new IllegalStateException("Unknown handshake token " + Integer.toString(token)); - } - } - } - - // Add the payload to the message buffer and encrypt it. - if (payload != null) - messagePosn += symmetric.encryptAndHash(payload, payloadOffset, message, messagePosn, payloadLength); - else - messagePosn += symmetric.encryptAndHash(message, messagePosn, message, messagePosn, 0); - success = true; - } finally { - // If we failed, then clear any sensitive data that may have - // already been written to the message buffer. - if (!success) { - Arrays.fill(message, messageOffset, message.length - messageOffset, (byte)0); - action = FAILED; - } - } - return messagePosn - messageOffset; - } - - /** - * Reads a message payload during the handshake. - * - * @param message Buffer containing the incoming handshake - * that was read from the transport. - * @param messageOffset Offset of the first message byte. - * @param messageLength The length of the incoming message. - * @param payload Buffer that will be populated with the message payload. - * @param payloadOffset Offset of the first byte in the - * payload buffer to be populated with payload data. - * - * @return The length of the payload. - * - * @throws IllegalStateException The action is not READ_MESSAGE. - * - * @throws ShortBufferException The message buffer does not have - * sufficient bytes for a valid message or the payload buffer does - * not have enough space for the decrypted payload. - * - * @throws BadPaddingException A MAC value in the message failed - * to verify. - * - * @see #getAction() - * @see #writeMessage(byte[], int, byte[], int, int) - */ - public int readMessage(byte[] message, int messageOffset, int messageLength, byte[] payload, int payloadOffset) throws ShortBufferException, BadPaddingException - { - boolean success = false; - int messageEnd = messageOffset + messageLength; - - // Validate the parameters. - if (action != READ_MESSAGE) { - throw new IllegalStateException - ("Handshake state does not allow reading messages"); - } - if (messageOffset > message.length || payloadOffset > payload.length) { - throw new ShortBufferException(); - } - if (messageLength > (message.length - messageOffset)) { - throw new ShortBufferException(); - } - - // Process the message. - try { - // Process tokens until the direction changes or the patten ends. - for (;;) { - if (patternIndex >= pattern.length) { - // The pattern has finished, so the next action is "split". - action = SPLIT; - break; - } - short token = pattern[patternIndex++]; - if (token == Pattern.FLIP_DIR) { - // Change directions, so this message is complete and the - // next action is "write message". - action = WRITE_MESSAGE; - break; - } - int space = messageEnd - messageOffset; - int len, macLen; - switch (token) { - case Pattern.E: - { - // Save the remote ephemeral key and hash it. - if (remoteEphemeral == null) - throw new IllegalStateException("Pattern definition error"); - len = remoteEphemeral.getPublicKeyLength(); - if (space < len) - throw new ShortBufferException(); - symmetric.mixHash(message, messageOffset, len); - remoteEphemeral.setPublicKey(message, messageOffset); - if (remoteEphemeral.isNullPublicKey()) { - // The remote ephemeral key is null, which means that it is - // not contributing anything to the security of the session - // and is in fact downgrading the security to "none at all" - // in some of the message patterns. Reject all such keys. - throw new BadPaddingException("Null remote public key"); - } - - // If the protocol is using pre-shared keys, then also mix - // the remote ephemeral key into the chaining key. - if (preSharedKey != null) - symmetric.mixKey(message, messageOffset, len); - messageOffset += len; - } - break; - - case Pattern.S: - { - // Decrypt and read the remote static key. - if (remotePublicKey == null) - throw new IllegalStateException("Pattern definition error"); - len = remotePublicKey.getPublicKeyLength(); - macLen = symmetric.getMACLength(); - if (space < (len + macLen)) - throw new ShortBufferException(); - byte[] temp = new byte [len]; - try { - if (symmetric.decryptAndHash(message, messageOffset, temp, 0, len + macLen) != len) - throw new ShortBufferException(); - remotePublicKey.setPublicKey(temp, 0); - } finally { - Noise.destroy(temp); - } - messageOffset += len + macLen; - } - break; - - case Pattern.EE: - { - // DH operation with initiator and responder ephemeral keys. - mixDH(localEphemeral, remoteEphemeral); - } - break; - - case Pattern.ES: - { - // DH operation with initiator ephemeral and responder static keys. - if (isInitiator) - mixDH(localEphemeral, remotePublicKey); - else - mixDH(localKeyPair, remoteEphemeral); - } - break; - - case Pattern.SE: - { - // DH operation with initiator static and responder ephemeral keys. - if (isInitiator) - mixDH(localKeyPair, remoteEphemeral); - else - mixDH(localEphemeral, remotePublicKey); - } - break; - - case Pattern.SS: - { - // DH operation with initiator and responder static keys. - mixDH(localKeyPair, remotePublicKey); - } - break; - - case Pattern.F: - { - // Decrypt and read the remote hybrid ephemeral key. - if (remoteHybrid == null) - throw new IllegalStateException("Pattern definition error"); - if (remoteHybrid instanceof DHStateHybrid) { - // The DH object is something like New Hope. The public key - // length may need to change based on whether we already have - // generated a local hybrid keypair or not. - ((DHStateHybrid)remoteHybrid).specifyPeer(localHybrid); - } - len = remoteHybrid.getPublicKeyLength(); - macLen = symmetric.getMACLength(); - if (space < (len + macLen)) - throw new ShortBufferException(); - byte[] temp = new byte [len]; - try { - if (symmetric.decryptAndHash(message, messageOffset, temp, 0, len + macLen) != len) - throw new ShortBufferException(); - remoteHybrid.setPublicKey(temp, 0); - } finally { - Noise.destroy(temp); - } - messageOffset += len + macLen; - } - break; - - case Pattern.FF: - { - // DH operation with initiator and responder hybrid keys. - mixDH(localHybrid, remoteHybrid); - } - break; - - default: - { - // Unknown token code. Abort. - throw new IllegalStateException("Unknown handshake token " + Integer.toString(token)); - } - } - } - - // Decrypt the message payload. - int payloadLength = symmetric.decryptAndHash(message, messageOffset, payload, payloadOffset, messageEnd - messageOffset); - success = true; - return payloadLength; - } finally { - // If we failed, then clear any sensitive data that may have - // already been written to the payload buffer. - if (!success) { - Arrays.fill(payload, payloadOffset, payload.length - payloadOffset, (byte)0); - action = FAILED; - } - } - } - - /** - * Splits the transport encryption CipherState objects out of - * this HandshakeState object once the handshake completes. - * - * @return The pair of ciphers for sending and receiving. - * - * @throws IllegalStateException The action is not SPLIT. - */ - public CipherStatePair split() - { - if (action != SPLIT) { - throw new IllegalStateException - ("Handshake has not finished"); - } - CipherStatePair pair = symmetric.split(); - if (!isInitiator) - pair.swap(); - action = COMPLETE; - return pair; - } - - /** - * Splits the transport encryption CipherState objects out of - * this HandshakeObject after mixing in a secondary symmetric key. - * - * @param secondaryKey The buffer containing the secondary key. - * @param offset The offset of the first secondary key byte. - * @param length The length of the secondary key in bytes, which - * must be either 0 or 32. - * @return The pair of ciphers for sending and receiving. - * - * @throws IllegalStateException The action is not SPLIT. - * - * @throws IllegalArgumentException The length is not 0 or 32. - */ - public CipherStatePair split(byte[] secondaryKey, int offset, int length) - { - if (action != SPLIT) { - throw new IllegalStateException - ("Handshake has not finished"); - } - CipherStatePair pair = symmetric.split(secondaryKey, offset, length); - if (!isInitiator) { - // Swap the sender and receiver objects for the responder - // to make it easier on the application to know which is which. - pair.swap(); - } - action = COMPLETE; - return pair; - } - - /** - * Gets the current value of the handshake hash. - * - * @return The handshake hash. This must not be modified by the caller. - * - * @throws IllegalStateException The action is not SPLIT or COMPLETE. - */ - public byte[] getHandshakeHash() - { - if (action != SPLIT && action != COMPLETE) { - throw new IllegalStateException - ("Handshake has not completed"); - } - return symmetric.getHandshakeHash(); - } - - @Override - public void destroy() { - if (symmetric != null) - symmetric.destroy(); - if (localKeyPair != null) - localKeyPair.destroy(); - if (localEphemeral != null) - localEphemeral.destroy(); - if (localHybrid != null) - localHybrid.destroy(); - if (remotePublicKey != null) - remotePublicKey.destroy(); - if (remoteEphemeral != null) - remoteEphemeral.destroy(); - if (remoteHybrid != null) - remoteHybrid.destroy(); - if (fixedEphemeral != null) - fixedEphemeral.destroy(); - if (fixedHybrid != null) - fixedHybrid.destroy(); - if (preSharedKey != null) - Noise.destroy(preSharedKey); - if (prologue != null) - Noise.destroy(prologue); - } - - /** - * Computes the requirements for a handshake. - * - * @param flags The flags from the handshake's pattern. - * @param prefix The prefix from the protocol name; typically - * "Noise" or "NoisePSK". - * @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. - * @param isFallback Set to true if we need the requirements for a - * fallback pattern; false for a regular pattern. - * - * @return The set of requirements for the handshake. - */ - private static int computeRequirements(short flags, String prefix, int role, boolean isFallback) - { - int requirements = 0; - if ((flags & Pattern.FLAG_LOCAL_STATIC) != 0) { - requirements |= LOCAL_REQUIRED; - } - if ((flags & Pattern.FLAG_LOCAL_REQUIRED) != 0) { - requirements |= LOCAL_REQUIRED; - requirements |= LOCAL_PREMSG; - } - if ((flags & Pattern.FLAG_REMOTE_REQUIRED) != 0) { - requirements |= REMOTE_REQUIRED; - requirements |= REMOTE_PREMSG; - } - if ((flags & (Pattern.FLAG_REMOTE_EPHEM_REQ | - Pattern.FLAG_LOCAL_EPHEM_REQ)) != 0) { - if (isFallback) - requirements |= FALLBACK_PREMSG; - } - if (prefix.equals("NoisePSK")) { - requirements |= PSK_REQUIRED; - } - return requirements; - } -} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.kt new file mode 100644 index 00000000..e2605db1 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/HandshakeState.kt @@ -0,0 +1,1196 @@ +/* + * 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.copySubArray +import com.bitchat.android.noise.southernstorm.protocol.Noise.createDH +import com.bitchat.android.noise.southernstorm.protocol.Noise.destroy +import com.bitchat.android.noise.southernstorm.protocol.Pattern.lookup +import com.bitchat.android.noise.southernstorm.protocol.Pattern.reverseFlags +import java.security.NoSuchAlgorithmException +import java.util.Arrays +import javax.crypto.BadPaddingException +import javax.crypto.ShortBufferException + +/** + * Interface to a Noise handshake. + */ +class HandshakeState(protocolName: String, role: Int) : Destroyable { + private var symmetric: SymmetricState? + private var isInitiator: Boolean + + /** + * Gets the keypair object for the local static key. + * + * @return The keypair, or null if a local static key is not required. + */ + var localKeyPair: DHState? = null + private set + private var localEphemeral: DHState? = null + private var localHybrid: DHState? = null + + /** + * Gets the public key object for the remote static key. + * + * @return The public key, or null if a remote static key + * is not required. + */ + var remotePublicKey: DHState? = null + private set + private var remoteEphemeral: DHState? = null + private var remoteHybrid: DHState? = null + private var fixedEphemeral: DHState? = null + private var fixedHybrid: DHState? = null + + /** + * Gets the next action that the application should perform for + * the handshake part of the protocol. + * + * @return One of HandshakeState.NO_ACTION, HandshakeState.WRITE_MESSAGE, + * HandshakeState.READ_MESSAGE, HandshakeState.SPLIT, or + * HandshakeState.FAILED. + */ + var action: Int + private set + private var requirements: Int + private var pattern: ShortArray? + private var patternIndex: Int + private var preSharedKey: ByteArray? + private var prologue: ByteArray? + + val protocolName: String? + /** + * Gets the name of the Noise protocol. + * + * @return The protocol name. + */ + get() = symmetric!!.getProtocolName() + + val role: Int + /** + * Gets the role for this handshake. + * + * @return The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. + */ + get() = if (isInitiator) INITIATOR else RESPONDER + + /** + * Determine if this handshake needs a pre-shared key value + * and one has not been configured yet. + * + * @return true if a pre-shared key is needed; false if not. + */ + fun needsPreSharedKey(): Boolean { + if (preSharedKey != null) return false + else return (requirements and PSK_REQUIRED) != 0 + } + + /** + * Determine if this object has already been configured with a + * pre-shared key. + * + * @return true if the pre-shared key has already been configured; + * false if one is not needed or it has not been configured yet. + */ + fun hasPreSharedKey(): Boolean { + return preSharedKey != null + } + + /** + * Sets the pre-shared key for this handshake. + * + * @param key Buffer containing the pre-shared key value. + * @param offset Offset into the buffer of the first byte of the key. + * @param length The length of the pre-shared key, which must be 32. + * + * @throws IllegalArgumentException The length is not 32. + * + * @throws UnsupportedOperationException Pre-shared keys are not + * supported for this handshake type. + * + * @throws IllegalStateException The handshake has already started, + * so the pre-shared key can no longer be set. + */ + fun setPreSharedKey(key: ByteArray, offset: Int, length: Int) { + require(length == 32) { "Pre-shared keys must be 32 bytes in length" } + if ((requirements and PSK_REQUIRED) == 0) { + throw UnsupportedOperationException("Pre-shared keys are not supported for this handshake") + } + check(action == NO_ACTION) { "Handshake has already started; cannot set pre-shared key" } + if (preSharedKey != null) { + Noise.destroy(preSharedKey!!) + preSharedKey = null + } + preSharedKey = copySubArray(key, offset, length) + } + + /** + * Sets the prologue for this handshake. + * + * @param prologue Buffer containing the prologue value. + * @param offset Offset into the buffer of the first byte of the prologue. + * @param length The length of the prologue in bytes. + * + * @throws IllegalStateException The handshake has already started, + * so the prologue can no longer be set. + */ + fun setPrologue(prologue: ByteArray, offset: Int, length: Int) { + check(action == NO_ACTION) { "Handshake has already started; cannot set prologue" } + if (this.prologue != null) { + Noise.destroy(this.prologue!!) + this.prologue = null + } + this.prologue = copySubArray(prologue, offset, length) + } + + /** + * Determine if this handshake requires a local static key. + * + * @return true if a local static key is needed; false if not. + * + * If the local static key has already been set, then this function + * will return false. + */ + fun needsLocalKeyPair(): Boolean { + if (localKeyPair != null) return !localKeyPair!!.hasPrivateKey() + else return false + } + + /** + * Determine if this handshake has already been configured + * with a local static key. + * + * @return true if the local static key has been configured; + * false if not. + */ + fun hasLocalKeyPair(): Boolean { + if (localKeyPair != null) return localKeyPair!!.hasPrivateKey() + else return false + } + + /** + * Determine if this handshake requires a remote static key. + * + * @return true if a remote static key is needed; false if not. + * + * If the remote static key has already been set, then this function + * will return false. + */ + fun needsRemotePublicKey(): Boolean { + if (remotePublicKey != null) return !remotePublicKey!!.hasPublicKey() + else return false + } + + /** + * Determine if this handshake has already been configured + * with a remote static key. + * + * @return true if the remote static key has been configured; + * false if not. + */ + fun hasRemotePublicKey(): Boolean { + if (remotePublicKey != null) return remotePublicKey!!.hasPublicKey() + else return false + } + + val fixedEphemeralKey: DHState? + /** + * Gets the DHState object containing a fixed local ephemeral + * key value for this handshake. + * + * @return The fixed ephemeral key object, or null if a local + * ephemeral key is not required by this handshake. + * + * This function is intended for testing only. It can be used + * to establish a fixed ephemeral key for test vectors. This + * function should not be used in real applications. + */ + get() { + if (fixedEphemeral != null) return fixedEphemeral + if (localEphemeral == null) return null + try { + fixedEphemeral = + createDH(localEphemeral!!.getDHName()) + } catch (e: NoSuchAlgorithmException) { + // This shouldn't happen - the local ephemeral key would + // have already been created with the same name! + fixedEphemeral = null + } + return fixedEphemeral + } + + val fixedHybridKey: DHState? + /** + * Gets the DHState object containing a fixed local hybrid + * key value for this handshake. + * + * @return The fixed hybrid key object, or null if a local + * hybrid key is not required by this handshake. + * + * This function is intended for testing only. It can be used + * to establish a fixed hybrid key for test vectors. This + * function should not be used in real applications. + */ + get() { + if (fixedHybrid != null) return fixedHybrid + if (localHybrid == null) return null + try { + fixedHybrid = + createDH(localHybrid!!.getDHName()) + } catch (e: NoSuchAlgorithmException) { + // This shouldn't happen - the local hybrid key would + // have already been created with the same name! + fixedHybrid = null + } + return fixedHybrid + } + + /** + * Creates a new Noise handshake. + * + * @param protocolName The name of the Noise protocol. + * @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. + * + * @throws IllegalArgumentException The protocolName is not + * formatted correctly, or the role is not recognized. + * + * @throws NoSuchAlgorithmException One of the cryptographic algorithms + * that is specified in the protocolName is not supported. + */ + init { + // Parse the protocol name into its components. + val components = + protocolName.split("_".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + require(components.size == 5) { "Protocol name must have 5 components" } + val prefix = components[0] + val patternId = components[1] + var dh = components[2] + var hybrid: String? = null + val cipher: String? = components[3] + val hash: String? = components[4] + require(!(prefix != "Noise" && prefix != "NoisePSK")) { "Prefix must be Noise or NoisePSK" } + pattern = lookup(patternId) + requireNotNull(pattern) { "Handshake pattern is not recognized" } + var flags = pattern!![0] + var extraReqs = 0 + if ((flags.toInt() and Pattern.FLAG_REMOTE_REQUIRED.toInt()) != 0 && patternId.length > 1) extraReqs = + extraReqs or FALLBACK_POSSIBLE + if (role == RESPONDER) { + // Reverse the pattern flags so that the responder is "local". + flags = reverseFlags(flags) + } + val index = dh.indexOf('+') + if (index != -1) { + // The DH name has two components: regular and hybrid. + hybrid = dh.substring(index + 1) + dh = dh.substring(0, index) + require(!((flags.toInt() and Pattern.FLAG_LOCAL_HYBRID.toInt()) == 0 || (flags.toInt() and Pattern.FLAG_REMOTE_HYBRID.toInt()) == 0)) { "Hybrid function specified for non-hybrid pattern" } + } else { + require(!((flags.toInt() and Pattern.FLAG_LOCAL_HYBRID.toInt()) != 0 || (flags.toInt() and Pattern.FLAG_REMOTE_HYBRID.toInt()) != 0)) { "Hybrid function not specified for hybrid pattern" } + } + + // Check that the role is correctly specified. + require(!(role != INITIATOR && role != RESPONDER)) { "Role must be initiator or responder" } + + // Initialize this object. This will also create the cipher and hash objects. + symmetric = SymmetricState(protocolName, cipher, hash) + isInitiator = (role == INITIATOR) + action = NO_ACTION + requirements = extraReqs or computeRequirements(flags, prefix, role, false) + patternIndex = 1 + + + // Create the DH objects that we will need later. + if ((flags.toInt() and Pattern.FLAG_LOCAL_STATIC.toInt()) != 0) localKeyPair = createDH(dh) + if ((flags.toInt() and Pattern.FLAG_LOCAL_EPHEMERAL.toInt()) != 0) localEphemeral = + createDH(dh) + if ((flags.toInt() and Pattern.FLAG_LOCAL_HYBRID.toInt()) != 0) localHybrid = + Noise.createDH(hybrid!!) + if ((flags.toInt() and Pattern.FLAG_REMOTE_STATIC.toInt()) != 0) remotePublicKey = + createDH(dh) + if ((flags.toInt() and Pattern.FLAG_REMOTE_EPHEMERAL.toInt()) != 0) remoteEphemeral = + createDH(dh) + if ((flags.toInt() and Pattern.FLAG_REMOTE_HYBRID.toInt()) != 0) remoteHybrid = + Noise.createDH(hybrid!!) + + + // We cannot use hybrid algorithms like New Hope for ephemeral or static keys, + // as the unbalanced nature of the algorithm only works with "f" and "ff" tokens. + if (localKeyPair is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '" + localKeyPair!!.getDHName() + "' for static keys") + if (localEphemeral is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '" + localEphemeral!!.getDHName() + "' for ephemeral keys") + if (remotePublicKey is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '" + remotePublicKey!!.getDHName() + "' for static keys") + if (remoteEphemeral is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '" + remoteEphemeral!!.getDHName() + "' for ephemeral keys") + } + + /** + * Starts the handshake running. + * + * This function is called after all of the handshake parameters have been + * provided to the HandshakeState object. This function should be followed + * by calls to writeMessage() or readMessage() to process the handshake + * messages. The getAction() function indicates the action to take next. + * + * @throws IllegalStateException The handshake has already started, or one or + * more of the required parameters has not been supplied. + * + * @throws UnsupportedOperationException An attempt was made to start a + * fallback handshake pattern without first calling fallback() on a + * previous handshake. + * + * @see .getAction + * @see .writeMessage + * @see .readMessage + * @see .fallback + */ + fun start() { + check(action == NO_ACTION) { "Handshake has already started; cannot start again" } + if ((pattern!![0].toInt() and Pattern.FLAG_REMOTE_EPHEM_REQ.toInt()) != 0 && + (requirements and FALLBACK_PREMSG) == 0 + ) { + throw UnsupportedOperationException("Cannot start a fallback pattern") + } + + + // Check that we have satisfied all of the pattern requirements. + if ((requirements and LOCAL_REQUIRED) != 0) { + check(!(localKeyPair == null || !localKeyPair!!.hasPrivateKey())) { "Local static key required" } + } + if ((requirements and REMOTE_REQUIRED) != 0) { + check(!(remotePublicKey == null || !remotePublicKey!!.hasPublicKey())) { "Remote static key required" } + } + if ((requirements and PSK_REQUIRED) != 0) { + checkNotNull(preSharedKey) { "Pre-shared key required" } + } + + // Log the symmetric state BEFORE any mixing operations (matching iOS) + Log.d(TAG, "=== ANDROID HANDSHAKE START - INITIAL STATE ===") + Log.d(TAG, "Protocol: " + symmetric!!.getProtocolName()) + Log.d(TAG, "Role: " + (if (isInitiator) "INITIATOR" else "RESPONDER")) + Log.d(TAG, "Initial symmetric hash: " + bytesToHex(symmetric!!.getHandshakeHash())) + + + // Hash the prologue value. + Log.d(TAG, "Mixing empty prologue") + if (prologue != null) symmetric!!.mixHash(prologue, 0, prologue!!.size) + else symmetric!!.mixHash(emptyPrologue, 0, 0) + Log.d(TAG, "Hash after empty prologue: " + bytesToHex(symmetric!!.getHandshakeHash())) + + + // Hash the pre-shared key into the chaining key and handshake hash. + if (preSharedKey != null) symmetric!!.mixPreSharedKey(preSharedKey) + + + // Mix the pre-supplied public keys into the handshake hash. + if (isInitiator) { + Log.d(TAG, "XX pattern - no pre-message keys to mix") + if ((requirements and LOCAL_PREMSG) != 0) symmetric!!.mixPublicKey(localKeyPair) + if ((requirements and FALLBACK_PREMSG) != 0) { + symmetric!!.mixPublicKey(remoteEphemeral) + if (remoteHybrid != null) symmetric!!.mixPublicKey(remoteHybrid) + if (preSharedKey != null) symmetric!!.mixPublicKeyIntoCK(remoteEphemeral) + } + if ((requirements and REMOTE_PREMSG) != 0) symmetric!!.mixPublicKey(remotePublicKey) + } else { + Log.d(TAG, "XX pattern - no pre-message keys to mix") + if ((requirements and REMOTE_PREMSG) != 0) symmetric!!.mixPublicKey(remotePublicKey) + if ((requirements and FALLBACK_PREMSG) != 0) { + symmetric!!.mixPublicKey(localEphemeral) + if (localHybrid != null) symmetric!!.mixPublicKey(localHybrid) + if (preSharedKey != null) symmetric!!.mixPublicKeyIntoCK(localEphemeral) + } + if ((requirements and LOCAL_PREMSG) != 0) symmetric!!.mixPublicKey(localKeyPair) + } + + // Log final state after all initialization (matching iOS) + Log.d(TAG, "=== ANDROID HANDSHAKE START - FINAL STATE ===") + Log.d( + TAG, + "Final symmetric hash after mixPreMessageKeys(): " + bytesToHex(symmetric!!.getHandshakeHash()) + ) + Log.d(TAG, "===========================================") + + // The handshake has officially started - set the first action. + if (isInitiator) action = WRITE_MESSAGE + else action = READ_MESSAGE + } + + /** + * Falls back to another handshake pattern. + * + * @param patternName The name of the pattern to fall back to; + * e.g. "XXfallback", "NXfallback", etc. + * + * This function resets a HandshakeState object with the original + * handshake pattern, and converts it into an object with the new handshake + * patternName. Information from the previous session such as the local + * keypair, the initiator's ephemeral key, the prologue value, and the + * pre-shared key, are passed to the new session. + * + * Once the fallback has been initiated, the application can set + * new values for the handshake parameters if the values from the + * previous session do not apply. For example, the application may + * use a different prologue for the fallback than for the original + * session. + * + * After setting any new parameters, the application calls start() + * again to restart the handshake from where it left off before the fallback. + * + * The new pattern may have greater key requirements than the original; + * for example changing from "NK" from "XXfallback" requires that the + * initiator's static public key be set. The application is responsible for + * setting any extra keys before calling start(). + * + * Note that this function reverses the roles of initiator and responder. + * + * @throws UnsupportedOperationException The current handshake pattern + * is not compatible with the patternName, or patternName is not a + * fallback pattern. + * + * @throws IllegalStateException The previous protocol has not started + * or it has not reached the fallback position yet. + * + * @throws NoSuchAlgorithmException One of the cryptographic algorithms + * that is specified in the new protocolName is not supported. + * + * @see .start + */ + /** + * Falls back to the "XXfallback" handshake pattern. + * + * This function is intended used to help implement the "Noise Pipes" protocol. + * It resets a HandshakeState object with the original handshake pattern + * (usually "IK"), converting it into an object with the handshake pattern + * "XXfallback". Information from the previous session such as the local + * keypair, the initiator's ephemeral key, the prologue value, and the + * pre-shared key, are passed to the new session. + * + * Once the fallback has been initiated, the application can set + * new values for the handshake parameters if the values from the + * previous session do not apply. For example, the application may + * use a different prologue for the fallback than for the original + * session. + * + * After setting any new parameters, the application calls start() + * again to restart the handshake from where it left off before the fallback. + * + * Note that this function reverses the roles of initiator and responder. + * + * @throws UnsupportedOperationException The current handshake pattern + * is not compatible with "XXfallback". + * + * @throws IllegalStateException The previous protocol has not started + * or it has not reached the fallback position yet. + * + * @throws NoSuchAlgorithmException One of the cryptographic algorithms + * that is specified in the new protocolName is not supported. + * + * @see .start + */ + @JvmOverloads + @Throws(NoSuchAlgorithmException::class) + fun fallback(patternName: String = "XXfallback") { + // The original pattern must end in "K" for fallback to be possible. + if ((requirements and FALLBACK_POSSIBLE) == 0) throw UnsupportedOperationException("Previous handshake pattern does not support fallback") + + // Check that "patternName" supports fallback. + val newPattern = lookup(patternName) + if (newPattern == null || (newPattern[0].toInt() and Pattern.FLAG_REMOTE_EPHEM_REQ.toInt()) == 0) throw UnsupportedOperationException( + "New pattern is not a fallback pattern" + ) + + // The initiator should be waiting for a return message from the + // responder, and the responder should have failed on the first + // handshake message from the initiator. We also allow the + // responder to fallback after processing the first message + // successfully; it decides to always fall back anyway. + if (isInitiator) { + check(!((action != FAILED && action != READ_MESSAGE) || !localEphemeral!!.hasPublicKey())) { "Initiator cannot fall back from this state" } + } else { + check(!((action != FAILED && action != WRITE_MESSAGE) || !remoteEphemeral!!.hasPublicKey())) { "Responder cannot fall back from this state" } + } + + + // Format a new protocol name for the fallback variant + // and recreate the SymmetricState object. + val components = + symmetric!!.getProtocolName().split("_".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + components[1] = patternName + val builder = StringBuilder() + builder.append(components[0]) + for (index in 1.. message.size) { + throw ShortBufferException() + } + + + // Format the message. + try { + // Process tokens until the direction changes or the patten ends. + while (true) { + if (patternIndex >= pattern!!.size) { + // The pattern has finished, so the next action is "split". + action = SPLIT + break + } + val token = pattern!![patternIndex++] + if (token == Pattern.FLIP_DIR) { + // Change directions, so this message is complete and the + // next action is "read message". + action = READ_MESSAGE + break + } + val space = message.size - messagePosn + val len: Int + val macLen: Int + when (token) { + Pattern.E -> { + // Generate a local ephemeral keypair and add the public + // key to the message. If we are running fixed vector tests, + // then the ephemeral key may have already been provided. + checkNotNull(localEphemeral) { "Pattern definition error" } + if (fixedEphemeral == null) localEphemeral!!.generateKeyPair() + else localEphemeral!!.copyFrom(fixedEphemeral) + len = localEphemeral!!.getPublicKeyLength() + if (space < len) throw ShortBufferException() + localEphemeral!!.getPublicKey(message, messagePosn) + symmetric!!.mixHash(message, messagePosn, len) + + // If the protocol is using pre-shared keys, then also mix + // the local ephemeral key into the chaining key. + if (preSharedKey != null) symmetric!!.mixKey(message, messagePosn, len) + messagePosn += len + } + + Pattern.S -> { + // Encrypt the local static public key and add it to the message. + checkNotNull(localKeyPair) { "Pattern definition error" } + len = localKeyPair!!.getPublicKeyLength() + macLen = symmetric!!.getMACLength() + if (space < (len + macLen)) throw ShortBufferException() + localKeyPair!!.getPublicKey(message, messagePosn) + messagePosn += symmetric!!.encryptAndHash( + message, + messagePosn, + message, + messagePosn, + len + ) + } + + Pattern.EE -> { + // DH operation with initiator and responder ephemeral keys. + mixDH(localEphemeral!!, remoteEphemeral!!) + } + + Pattern.ES -> { + // DH operation with initiator ephemeral and responder static keys. + if (isInitiator) mixDH(localEphemeral!!, remotePublicKey!!) + else mixDH(localKeyPair!!, remoteEphemeral!!) + } + + Pattern.SE -> { + // DH operation with initiator static and responder ephemeral keys. + if (isInitiator) mixDH(localKeyPair!!, remoteEphemeral!!) + else mixDH(localEphemeral!!, remotePublicKey!!) + } + + Pattern.SS -> { + // DH operation with initiator and responder static keys. + mixDH(localKeyPair!!, remotePublicKey!!) + } + + Pattern.F -> { + // Generate a local hybrid keypair and add the public + // key to the message. If we are running fixed vector tests, + // then a fixed hybrid key may have already been provided. + checkNotNull(localHybrid) { "Pattern definition error" } + if (localHybrid is DHStateHybrid) { + // The DH object is something like New Hope which needs to + // generate keys relative to the other party's public key. + val hybrid = localHybrid as DHStateHybrid + if (fixedHybrid == null) hybrid.generateKeyPair(remoteHybrid) + else hybrid.copyFrom(fixedHybrid, remoteHybrid) + } else { + if (fixedHybrid == null) localHybrid!!.generateKeyPair() + else localHybrid!!.copyFrom(fixedHybrid) + } + len = localHybrid!!.getPublicKeyLength() + if (space < len) throw ShortBufferException() + macLen = symmetric!!.getMACLength() + if (space < (len + macLen)) throw ShortBufferException() + localHybrid!!.getPublicKey(message, messagePosn) + messagePosn += symmetric!!.encryptAndHash( + message, + messagePosn, + message, + messagePosn, + len + ) + } + + Pattern.FF -> { + // DH operation with initiator and responder hybrid keys. + mixDH(localHybrid!!, remoteHybrid!!) + } + + else -> { + // Unknown token code. Abort. + throw IllegalStateException( + "Unknown handshake token " + token.toInt().toString() + ) + } + } + } + + + // Add the payload to the message buffer and encrypt it. + if (payload != null) messagePosn += symmetric!!.encryptAndHash( + payload, + payloadOffset, + message, + messagePosn, + payloadLength + ) + else messagePosn += symmetric!!.encryptAndHash( + message, + messagePosn, + message, + messagePosn, + 0 + ) + success = true + } finally { + // If we failed, then clear any sensitive data that may have + // already been written to the message buffer. + if (!success) { + Arrays.fill(message, messageOffset, message.size - messageOffset, 0.toByte()) + action = FAILED + } + } + return messagePosn - messageOffset + } + + /** + * Reads a message payload during the handshake. + * + * @param message Buffer containing the incoming handshake + * that was read from the transport. + * @param messageOffset Offset of the first message byte. + * @param messageLength The length of the incoming message. + * @param payload Buffer that will be populated with the message payload. + * @param payloadOffset Offset of the first byte in the + * payload buffer to be populated with payload data. + * + * @return The length of the payload. + * + * @throws IllegalStateException The action is not READ_MESSAGE. + * + * @throws ShortBufferException The message buffer does not have + * sufficient bytes for a valid message or the payload buffer does + * not have enough space for the decrypted payload. + * + * @throws BadPaddingException A MAC value in the message failed + * to verify. + * + * @see .getAction + * @see .writeMessage + */ + @Throws(ShortBufferException::class, BadPaddingException::class) + fun readMessage( + message: ByteArray, + messageOffset: Int, + messageLength: Int, + payload: ByteArray, + payloadOffset: Int + ): Int { + var messageOffset = messageOffset + var success = false + val messageEnd = messageOffset + messageLength + + + // Validate the parameters. + check(action == READ_MESSAGE) { "Handshake state does not allow reading messages" } + if (messageOffset > message.size || payloadOffset > payload.size) { + throw ShortBufferException() + } + if (messageLength > (message.size - messageOffset)) { + throw ShortBufferException() + } + + + // Process the message. + try { + // Process tokens until the direction changes or the patten ends. + while (true) { + if (patternIndex >= pattern!!.size) { + // The pattern has finished, so the next action is "split". + action = SPLIT + break + } + val token = pattern!![patternIndex++] + if (token == Pattern.FLIP_DIR) { + // Change directions, so this message is complete and the + // next action is "write message". + action = WRITE_MESSAGE + break + } + val space = messageEnd - messageOffset + val len: Int + val macLen: Int + when (token) { + Pattern.E -> { + // Save the remote ephemeral key and hash it. + checkNotNull(remoteEphemeral) { "Pattern definition error" } + len = remoteEphemeral!!.getPublicKeyLength() + if (space < len) throw ShortBufferException() + symmetric!!.mixHash(message, messageOffset, len) + remoteEphemeral!!.setPublicKey(message, messageOffset) + if (remoteEphemeral!!.isNullPublicKey()) { + // The remote ephemeral key is null, which means that it is + // not contributing anything to the security of the session + // and is in fact downgrading the security to "none at all" + // in some of the message patterns. Reject all such keys. + throw BadPaddingException("Null remote public key") + } + + // If the protocol is using pre-shared keys, then also mix + // the remote ephemeral key into the chaining key. + if (preSharedKey != null) symmetric!!.mixKey(message, messageOffset, len) + messageOffset += len + } + + Pattern.S -> { + // Decrypt and read the remote static key. + checkNotNull(remotePublicKey) { "Pattern definition error" } + len = remotePublicKey!!.getPublicKeyLength() + macLen = symmetric!!.getMACLength() + if (space < (len + macLen)) throw ShortBufferException() + val temp = ByteArray(len) + try { + if (symmetric!!.decryptAndHash( + message, + messageOffset, + temp, + 0, + len + macLen + ) != len + ) throw ShortBufferException() + remotePublicKey!!.setPublicKey(temp, 0) + } finally { + destroy(temp) + } + messageOffset += len + macLen + } + + Pattern.EE -> { + // DH operation with initiator and responder ephemeral keys. + mixDH(localEphemeral!!, remoteEphemeral!!) + } + + Pattern.ES -> { + // DH operation with initiator ephemeral and responder static keys. + if (isInitiator) mixDH(localEphemeral!!, remotePublicKey!!) + else mixDH(localKeyPair!!, remoteEphemeral!!) + } + + Pattern.SE -> { + // DH operation with initiator static and responder ephemeral keys. + if (isInitiator) mixDH(localKeyPair!!, remoteEphemeral!!) + else mixDH(localEphemeral!!, remotePublicKey!!) + } + + Pattern.SS -> { + // DH operation with initiator and responder static keys. + mixDH(localKeyPair!!, remotePublicKey!!) + } + + Pattern.F -> { + // Decrypt and read the remote hybrid ephemeral key. + checkNotNull(remoteHybrid) { "Pattern definition error" } + if (remoteHybrid is DHStateHybrid) { + // The DH object is something like New Hope. The public key + // length may need to change based on whether we already have + // generated a local hybrid keypair or not. + (remoteHybrid as DHStateHybrid).specifyPeer(localHybrid) + } + len = remoteHybrid!!.getPublicKeyLength() + macLen = symmetric!!.getMACLength() + if (space < (len + macLen)) throw ShortBufferException() + val temp = ByteArray(len) + try { + if (symmetric!!.decryptAndHash( + message, + messageOffset, + temp, + 0, + len + macLen + ) != len + ) throw ShortBufferException() + remoteHybrid!!.setPublicKey(temp, 0) + } finally { + destroy(temp) + } + messageOffset += len + macLen + } + + Pattern.FF -> { + // DH operation with initiator and responder hybrid keys. + mixDH(localHybrid!!, remoteHybrid!!) + } + + else -> { + // Unknown token code. Abort. + throw IllegalStateException( + "Unknown handshake token " + token.toInt().toString() + ) + } + } + } + + + // Decrypt the message payload. + val payloadLength = symmetric!!.decryptAndHash( + message, + messageOffset, + payload, + payloadOffset, + messageEnd - messageOffset + ) + success = true + return payloadLength + } finally { + // If we failed, then clear any sensitive data that may have + // already been written to the payload buffer. + if (!success) { + Arrays.fill(payload, payloadOffset, payload.size - payloadOffset, 0.toByte()) + action = FAILED + } + } + } + + /** + * Splits the transport encryption CipherState objects out of + * this HandshakeState object once the handshake completes. + * + * @return The pair of ciphers for sending and receiving. + * + * @throws IllegalStateException The action is not SPLIT. + */ + fun split(): CipherStatePair { + check(action == SPLIT) { "Handshake has not finished" } + val pair = symmetric!!.split() + if (!isInitiator) pair.swap() + action = COMPLETE + return pair + } + + /** + * Splits the transport encryption CipherState objects out of + * this HandshakeObject after mixing in a secondary symmetric key. + * + * @param secondaryKey The buffer containing the secondary key. + * @param offset The offset of the first secondary key byte. + * @param length The length of the secondary key in bytes, which + * must be either 0 or 32. + * @return The pair of ciphers for sending and receiving. + * + * @throws IllegalStateException The action is not SPLIT. + * + * @throws IllegalArgumentException The length is not 0 or 32. + */ + fun split(secondaryKey: ByteArray?, offset: Int, length: Int): CipherStatePair { + check(action == SPLIT) { "Handshake has not finished" } + val pair = symmetric!!.split(secondaryKey, offset, length) + if (!isInitiator) { + // Swap the sender and receiver objects for the responder + // to make it easier on the application to know which is which. + pair.swap() + } + action = COMPLETE + return pair + } + + val handshakeHash: ByteArray? + /** + * Gets the current value of the handshake hash. + * + * @return The handshake hash. This must not be modified by the caller. + * + * @throws IllegalStateException The action is not SPLIT or COMPLETE. + */ + get() { + check(!(action != SPLIT && action != COMPLETE)) { "Handshake has not completed" } + return symmetric!!.getHandshakeHash() + } + + override fun destroy() { + if (symmetric != null) symmetric!!.destroy() + if (localKeyPair != null) localKeyPair!!.destroy() + if (localEphemeral != null) localEphemeral!!.destroy() + if (localHybrid != null) localHybrid!!.destroy() + if (remotePublicKey != null) remotePublicKey!!.destroy() + if (remoteEphemeral != null) remoteEphemeral!!.destroy() + if (remoteHybrid != null) remoteHybrid!!.destroy() + if (fixedEphemeral != null) fixedEphemeral!!.destroy() + if (fixedHybrid != null) fixedHybrid!!.destroy() + if (preSharedKey != null) Noise.destroy(preSharedKey!!) + if (prologue != null) Noise.destroy(prologue!!) + } + + companion object { + private const val TAG = "AndroidHandshake" + + /** + * Enumerated value that indicates that the handshake object + * is handling the initiator role. + */ + const val INITIATOR: Int = 1 + + /** + * Enumerated value that indicates that the handshake object + * is handling the responder role. + */ + const val RESPONDER: Int = 2 + + /** + * No action is required of the application yet because the + * handshake has not started. + */ + const val NO_ACTION: Int = 0 + + /** + * The HandshakeState expects the application to write the + * next message payload for the handshake. + */ + const val WRITE_MESSAGE: Int = 1 + + /** + * The HandshakeState expects the application to read the + * next message payload from the handshake. + */ + const val READ_MESSAGE: Int = 2 + + /** + * The handshake has failed due to some kind of error. + */ + const val FAILED: Int = 3 + + /** + * The handshake is over and the application is expected to call + * split() and begin data session communications. + */ + const val SPLIT: Int = 4 + + /** + * The handshake is complete and the data session ciphers + * have been split() out successfully. + */ + const val COMPLETE: Int = 5 + + /** + * Local static keypair is required for the handshake. + */ + private const val LOCAL_REQUIRED = 0x01 + + /** + * Remote static keypai is required for the handshake. + */ + private const val REMOTE_REQUIRED = 0x02 + + /** + * Pre-shared key is required for the handshake. + */ + private const val PSK_REQUIRED = 0x04 + + /** + * Ephemeral key for fallback pre-message has been provided. + */ + private const val FALLBACK_PREMSG = 0x08 + + /** + * The local public key is part of the pre-message. + */ + private const val LOCAL_PREMSG = 0x10 + + /** + * The remote public key is part of the pre-message. + */ + private const val REMOTE_PREMSG = 0x20 + + /** + * Fallback is possible from this pattern (two-way, ends in "K"). + */ + private const val FALLBACK_POSSIBLE = 0x40 + + // Empty value for when the prologue is not supplied. + private val emptyPrologue = ByteArray(0) + + /** + * 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() + } + + /** + * Computes the requirements for a handshake. + * + * @param flags The flags from the handshake's pattern. + * @param prefix The prefix from the protocol name; typically + * "Noise" or "NoisePSK". + * @param role The role, HandshakeState.INITIATOR or HandshakeState.RESPONDER. + * @param isFallback Set to true if we need the requirements for a + * fallback pattern; false for a regular pattern. + * + * @return The set of requirements for the handshake. + */ + private fun computeRequirements( + flags: Short, + prefix: String, + role: Int, + isFallback: Boolean + ): Int { + var requirements = 0 + if ((flags.toInt() and Pattern.FLAG_LOCAL_STATIC.toInt()) != 0) { + requirements = requirements or LOCAL_REQUIRED + } + if ((flags.toInt() and Pattern.FLAG_LOCAL_REQUIRED.toInt()) != 0) { + requirements = requirements or LOCAL_REQUIRED + requirements = requirements or LOCAL_PREMSG + } + if ((flags.toInt() and Pattern.FLAG_REMOTE_REQUIRED.toInt()) != 0) { + requirements = requirements or REMOTE_REQUIRED + requirements = requirements or REMOTE_PREMSG + } + if ((flags.toInt() and (Pattern.FLAG_REMOTE_EPHEM_REQ.toInt() or + Pattern.FLAG_LOCAL_EPHEM_REQ.toInt())) != 0 + ) { + if (isFallback) requirements = requirements or FALLBACK_PREMSG + } + if (prefix == "NoisePSK") { + requirements = requirements or PSK_REQUIRED + } + return requirements + } + } +} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/NewHopeDHState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/NewHopeDHState.java deleted file mode 100644 index bb3c275f..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/NewHopeDHState.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/NewHopeDHState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/NewHopeDHState.kt new file mode 100644 index 00000000..1225504f --- /dev/null +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/NewHopeDHState.kt @@ -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 + } +} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.java deleted file mode 100644 index d669baa3..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.java +++ /dev/null @@ -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); - } - } -} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.kt b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.kt new file mode 100644 index 00000000..c1328047 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/SymmetricState.kt @@ -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() + } + } +} diff --git a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/package-info.java b/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/package-info.java deleted file mode 100644 index 8027d7ca..00000000 --- a/app/src/main/java/com/bitchat/android/noise/southernstorm/protocol/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ - -/** - * Provides classes for communicating via the Noise protocol. - * - * Reference: http://noiseprotocol.org - */ -package com.bitchat.android.noise.southernstorm.protocol;