refactor(noise/protocol): polish Kotlin idioms and remove Java legacy

Address IDE/compiler warnings across the protocol/ package after the
Java to Kotlin migration:

- Replace reference-identity ByteArray checks `!=` with `!==` in
  ChaChaPoly/AESGCM*CipherState (in-place encrypt/decrypt guards) to
  silence the "dangerous array comparison" warning while preserving
  the original semantics.
- Drop `java.util.Arrays.fill` and `Arrays`-style helpers in favour of
  ByteArray.fill / copyInto / copyOfRange where appropriate. Cold paths
  in NewHopeDHState, Curve25519DHState, Curve448DHState and parts of
  SymmetricState now use Kotlin extensions; tight crypto inner loops
  (HMAC body, ChaChaPoly) keep System.arraycopy intentionally.
- Use property access (`digestLength`) instead of `getDigestLength()`.
- Fold redundant if/else into `if-expression`, drop redundant
  `val space: Int` + assignment, redundant `Companion.` qualifiers,
  and unused `val cipher` re-assign in fork().
- Rename `prev_h` to `prevH` (style).
- Replace string concatenation with templates in log statements and
  exception messages.
- Convert unused catch parameters `e` to `_` and remove stale local
  `var index: Int` in hmac().
- Strip stale `@param secondaryKey/offset/length` KDoc block above
  SymmetricState.split (the params moved to the merged doc).
- Drop dead `check(!(local == null || remote == null))` from
  HandshakeState.mixDH — Kotlin types are already non-null.

Crypto behaviour is unchanged; output of every method matches the
prior Kotlin/Java implementation byte-for-byte.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
yet 2026-05-15 18:18:23 +04:00
parent 544d569708
commit f1645e73f2
8 changed files with 139 additions and 182 deletions

View File

@ -172,14 +172,13 @@ internal class AESGCMFallbackCipherState : CipherState {
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
val space: Int = 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(
if (plaintext !== ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
plaintext,
plaintextOffset,
ciphertext,
@ -219,7 +218,7 @@ internal class AESGCMFallbackCipherState : CipherState {
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(
if (plaintext !== ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
ciphertext,
ciphertextOffset,
plaintext,
@ -244,8 +243,7 @@ internal class AESGCMFallbackCipherState : CipherState {
}
override fun fork(key: ByteArray, offset: Int): CipherState {
val cipher: CipherState
cipher = AESGCMFallbackCipherState()
val cipher: CipherState = AESGCMFallbackCipherState()
cipher.initializeKey(key, offset)
return cipher
}

View File

@ -45,7 +45,12 @@ import javax.crypto.spec.SecretKeySpec
internal class AESGCMOnCtrCipherState
@Throws(NoSuchAlgorithmException::class)
constructor() : CipherState {
private val cipher: Cipher
private val cipher: Cipher = try {
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)
}
private var keySpec: SecretKeySpec? = null
private var n: Long = 0
private val iv: ByteArray = ByteArray(16)
@ -53,13 +58,6 @@ constructor() : CipherState {
private val ghash: GHASH = GHASH()
init {
cipher = try {
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)
}
// 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.
@ -85,9 +83,9 @@ constructor() : CipherState {
val params = IvParameterSpec(iv)
try {
cipher.init(Cipher.ENCRYPT_MODE, keySpec, params)
} catch (e: InvalidKeyException) {
} catch (_: InvalidKeyException) {
// Shouldn't happen.
} catch (e: InvalidAlgorithmParameterException) {
} catch (_: InvalidAlgorithmParameterException) {
// Shouldn't happen.
}
}
@ -196,14 +194,13 @@ constructor() : CipherState {
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
val space: Int = 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(
if (plaintext !== ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
plaintext,
plaintextOffset,
ciphertext,
@ -259,7 +256,7 @@ constructor() : CipherState {
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(
if (plaintext !== ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
ciphertext,
ciphertextOffset,
plaintext,
@ -281,7 +278,7 @@ constructor() : CipherState {
throw IllegalStateException(e)
}
ghash.update(ciphertext, ciphertextOffset, dataLen)
ghash.pad((if (ad != null) ad.size else 0).toLong(), dataLen.toLong())
ghash.pad((ad?.size ?: 0).toLong(), dataLen.toLong())
ghash.finish(iv, 0, 16)
var temp = 0
for (index in 0..15) temp =

View File

@ -89,7 +89,7 @@ internal class ChaChaPolyCipherState : CipherState {
*/
private fun finish(ad: ByteArray?, length: Int) {
poly.pad()
putLittleEndian64(polyKey, 0, (if (ad != null) ad.size else 0).toLong())
putLittleEndian64(polyKey, 0, (ad?.size ?: 0).toLong())
putLittleEndian64(polyKey, 8, length.toLong())
poly.update(polyKey, 0, 16)
poly.finish(polyKey, 0)
@ -128,14 +128,13 @@ internal class ChaChaPolyCipherState : CipherState {
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
val space: Int = 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(
if (plaintext !== ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
plaintext,
plaintextOffset,
ciphertext,
@ -162,16 +161,15 @@ internal class ChaChaPolyCipherState : CipherState {
plaintextOffset: Int,
length: Int
): Int {
var space: Int
require(!(ciphertextOffset < 0 || ciphertextOffset > ciphertext.size))
space = ciphertext.size - ciphertextOffset
var space: Int = 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(
if (plaintext !== ciphertext || plaintextOffset != ciphertextOffset) System.arraycopy(
ciphertext,
ciphertextOffset,
plaintext,
@ -241,21 +239,25 @@ internal class ChaChaPolyCipherState : CipherState {
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()
when (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()
}
2 -> {
value = block[posn]
output[outputOffset] = (input[inputOffset].toInt() xor value).toByte()
output[outputOffset + 1] =
(input[inputOffset + 1].toInt() xor (value shr 8)).toByte()
}
1 -> {
value = block[posn]
output[outputOffset] = (input[inputOffset].toInt() xor value).toByte()
}
}
}

View File

@ -49,21 +49,21 @@ internal class Curve25519DHState : DHState {
}
override fun getPublicKey(key: ByteArray, offset: Int) {
System.arraycopy(publicKey, 0, key, offset, 32)
publicKey.copyInto(key, offset, 0, 32)
}
override fun setPublicKey(key: ByteArray, offset: Int) {
System.arraycopy(key, offset, publicKey, 0, 32)
key.copyInto(publicKey, 0, offset, offset + 32)
privateKey.fill(0)
mode = 0x01
}
override fun getPrivateKey(key: ByteArray, offset: Int) {
System.arraycopy(privateKey, 0, key, offset, 32)
privateKey.copyInto(key, offset, 0, 32)
}
override fun setPrivateKey(key: ByteArray, offset: Int) {
System.arraycopy(key, offset, privateKey, 0, 32)
key.copyInto(privateKey, 0, offset, offset + 32)
Curve25519.eval(publicKey, 0, privateKey, null)
mode = 0x03
}
@ -99,8 +99,8 @@ internal class Curve25519DHState : DHState {
override fun copyFrom(other: DHState) {
check(other is Curve25519DHState) { "Mismatched DH key objects" }
if (other === this) return
System.arraycopy(other.privateKey, 0, privateKey, 0, 32)
System.arraycopy(other.publicKey, 0, publicKey, 0, 32)
other.privateKey.copyInto(privateKey, 0, 0, 32)
other.publicKey.copyInto(publicKey, 0, 0, 32)
mode = other.mode
}
}

View File

@ -49,21 +49,21 @@ internal class Curve448DHState : DHState {
}
override fun getPublicKey(key: ByteArray, offset: Int) {
System.arraycopy(publicKey, 0, key, offset, 56)
publicKey.copyInto(key, offset, 0, 56)
}
override fun setPublicKey(key: ByteArray, offset: Int) {
System.arraycopy(key, offset, publicKey, 0, 56)
key.copyInto(publicKey, 0, offset, offset + 56)
privateKey.fill(0)
mode = 0x01
}
override fun getPrivateKey(key: ByteArray, offset: Int) {
System.arraycopy(privateKey, 0, key, offset, 56)
privateKey.copyInto(key, offset, 0, 56)
}
override fun setPrivateKey(key: ByteArray, offset: Int) {
System.arraycopy(key, offset, privateKey, 0, 56)
key.copyInto(privateKey, 0, offset, offset + 56)
Curve448.eval(publicKey, 0, privateKey, null)
mode = 0x03
}
@ -99,8 +99,8 @@ internal class Curve448DHState : DHState {
override fun copyFrom(other: DHState) {
check(other is Curve448DHState) { "Mismatched DH key objects" }
if (other === this) return
System.arraycopy(other.privateKey, 0, privateKey, 0, 56)
System.arraycopy(other.publicKey, 0, publicKey, 0, 56)
other.privateKey.copyInto(privateKey, 0, 0, 56)
other.publicKey.copyInto(publicKey, 0, 0, 56)
mode = other.mode
}
}

View File

@ -28,7 +28,6 @@ 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
@ -97,8 +96,8 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
* @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
return if (preSharedKey != null) false
else (requirements and PSK_REQUIRED) != 0
}
/**
@ -168,8 +167,8 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
* will return false.
*/
fun needsLocalKeyPair(): Boolean {
if (localKeyPair != null) return !localKeyPair!!.hasPrivateKey()
else return false
return if (localKeyPair != null) !localKeyPair!!.hasPrivateKey()
else false
}
/**
@ -180,8 +179,8 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
* false if not.
*/
fun hasLocalKeyPair(): Boolean {
if (localKeyPair != null) return localKeyPair!!.hasPrivateKey()
else return false
return if (localKeyPair != null) localKeyPair!!.hasPrivateKey()
else false
}
/**
@ -193,8 +192,8 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
* will return false.
*/
fun needsRemotePublicKey(): Boolean {
if (remotePublicKey != null) return !remotePublicKey!!.hasPublicKey()
else return false
return if (remotePublicKey != null) !remotePublicKey!!.hasPublicKey()
else false
}
/**
@ -205,8 +204,8 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
* false if not.
*/
fun hasRemotePublicKey(): Boolean {
if (remotePublicKey != null) return remotePublicKey!!.hasPublicKey()
else return false
return if (remotePublicKey != null) remotePublicKey!!.hasPublicKey()
else false
}
val fixedEphemeralKey: DHState?
@ -227,7 +226,7 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
try {
fixedEphemeral =
createDH(localEphemeral!!.dhName)
} catch (e: NoSuchAlgorithmException) {
} catch (_: NoSuchAlgorithmException) {
// This shouldn't happen - the local ephemeral key would
// have already been created with the same name!
fixedEphemeral = null
@ -253,7 +252,7 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
try {
fixedHybrid =
createDH(localHybrid!!.dhName)
} catch (e: NoSuchAlgorithmException) {
} catch (_: NoSuchAlgorithmException) {
// This shouldn't happen - the local hybrid key would
// have already been created with the same name!
fixedHybrid = null
@ -320,21 +319,21 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
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!!)
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!!)
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!!.dhName + "' for static keys")
if (localEphemeral is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '" + localEphemeral!!.dhName + "' for ephemeral keys")
if (remotePublicKey is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '" + remotePublicKey!!.dhName + "' for static keys")
if (remoteEphemeral is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '" + remoteEphemeral!!.dhName + "' for ephemeral keys")
if (localKeyPair is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '${localKeyPair!!.dhName}' for static keys")
if (localEphemeral is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '${localEphemeral!!.dhName}' for ephemeral keys")
if (remotePublicKey is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '${remotePublicKey!!.dhName}' for static keys")
if (remoteEphemeral is DHStateHybrid) throw NoSuchAlgorithmException("Cannot use '${remoteEphemeral!!.dhName}' for ephemeral keys")
}
/**
@ -379,9 +378,9 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
// Log the symmetric state BEFORE any mixing operations (matching iOS)
Log.d(TAG, "=== ANDROID HANDSHAKE START - INITIAL STATE ===")
Log.d(TAG, "Protocol: " + symmetric!!.protocolName)
Log.d(TAG, "Role: " + (if (isInitiator) "INITIATOR" else "RESPONDER"))
Log.d(TAG, "Initial symmetric hash: " + bytesToHex(symmetric!!.handshakeHash!!))
Log.d(TAG, "Protocol: ${symmetric!!.protocolName}")
Log.d(TAG, "Role: ${if (isInitiator) "INITIATOR" else "RESPONDER"}")
Log.d(TAG, "Initial symmetric hash: ${bytesToHex(symmetric!!.handshakeHash!!)}")
// Hash the prologue value.
@ -389,7 +388,7 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
val p = prologue
if (p != null) symmetric!!.mixHash(p, 0, p.size)
else symmetric!!.mixHash(emptyPrologue, 0, 0)
Log.d(TAG, "Hash after empty prologue: " + bytesToHex(symmetric!!.handshakeHash!!))
Log.d(TAG, "Hash after empty prologue: ${bytesToHex(symmetric!!.handshakeHash!!)}")
// Hash the pre-shared key into the chaining key and handshake hash.
@ -421,13 +420,12 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
Log.d(TAG, "=== ANDROID HANDSHAKE START - FINAL STATE ===")
Log.d(
TAG,
"Final symmetric hash after mixPreMessageKeys(): " + bytesToHex(symmetric!!.handshakeHash!!)
"Final symmetric hash after mixPreMessageKeys(): ${bytesToHex(symmetric!!.handshakeHash!!)}"
)
Log.d(TAG, "===========================================")
// The handshake has officially started - set the first action.
if (isInitiator) action = WRITE_MESSAGE
else action = READ_MESSAGE
action = if (isInitiator) WRITE_MESSAGE else READ_MESSAGE
}
/**
@ -579,7 +577,6 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
* @param remote Remote public key object.
*/
private fun mixDH(local: DHState, remote: DHState) {
check(!(local == null || remote == null)) { "Pattern definition error" }
val len = local.sharedKeyLength
val shared = ByteArray(len)
try {
@ -750,7 +747,7 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
else -> {
// Unknown token code. Abort.
throw IllegalStateException(
"Unknown handshake token " + token.toInt().toString()
"Unknown handshake token ${token.toInt()}"
)
}
}
@ -777,7 +774,7 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
// 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())
message.fill(0, messageOffset, message.size - messageOffset)
action = FAILED
}
}
@ -817,7 +814,7 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
payload: ByteArray,
payloadOffset: Int
): Int {
var messageOffset = messageOffset
@Suppress("NAME_SHADOWING") var messageOffset = messageOffset
var success = false
val messageEnd = messageOffset + messageLength
@ -955,7 +952,7 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
else -> {
// Unknown token code. Abort.
throw IllegalStateException(
"Unknown handshake token " + token.toInt().toString()
"Unknown handshake token ${token.toInt()}"
)
}
}
@ -976,7 +973,7 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
// 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())
payload.fill(0, payloadOffset, payload.size - payloadOffset)
action = FAILED
}
}
@ -1143,7 +1140,7 @@ class HandshakeState(protocolName: String, role: Int) : Destroyable {
private fun bytesToHex(bytes: ByteArray): String {
val sb = StringBuilder()
for (b in bytes) {
sb.append(String.format("%02x", b))
sb.append("%02x".format(b))
}
return sb.toString()
}

View File

@ -48,7 +48,7 @@ internal class NewHopeDHState : DHStateHybrid {
*/
private inner class NewHopeWithPrivateKey(private val randomData: ByteArray) : NewHopeTor() {
override fun randombytes(buffer: ByteArray) {
System.arraycopy(randomData, 0, buffer, 0, buffer.size)
randomData.copyInto(buffer, 0, 0, buffer.size)
}
}
@ -102,21 +102,19 @@ internal class NewHopeDHState : DHStateHybrid {
override fun getPublicKey(key: ByteArray, offset: Int) {
val pk = publicKey
if (pk != null) System.arraycopy(pk, 0, key, offset, publicKeyLength)
if (pk != null) pk.copyInto(key, offset, 0, publicKeyLength)
else key.fill(0, offset, offset + publicKeyLength)
}
override fun setPublicKey(key: ByteArray, offset: Int) {
publicKey?.let { Noise.destroy(it) }
val len = publicKeyLength
val pk = ByteArray(len)
System.arraycopy(key, offset, pk, 0, len)
publicKey = pk
publicKey = key.copyOfRange(offset, offset + len)
}
override fun getPrivateKey(key: ByteArray, offset: Int) {
val sk = privateKey
if (sk != null) System.arraycopy(sk, 0, key, offset, privateKeyLength)
if (sk != null) sk.copyInto(key, offset, 0, privateKeyLength)
else key.fill(0, offset, offset + privateKeyLength)
}
@ -125,9 +123,7 @@ internal class NewHopeDHState : DHStateHybrid {
// Guess the key type from the length of the test data.
keyType = if (offset == 0 && key.size == 64) KeyType.AlicePrivate else KeyType.BobPrivate
val len = privateKeyLength
val sk = ByteArray(len)
System.arraycopy(key, offset, sk, 0, len)
privateKey = sk
privateKey = key.copyOfRange(offset, offset + len)
}
override fun setToNullPublicKey() {
@ -169,7 +165,7 @@ internal class NewHopeDHState : DHStateHybrid {
}
KeyType.BobCalculated -> {
// The shared key for Bob was already computed when the key was generated.
System.arraycopy(privateKey!!, 0, sharedKey, 0, NewHope.SHAREDBYTES)
privateKey!!.copyInto(sharedKey, 0, 0, NewHope.SHAREDBYTES)
}
else -> throw IllegalStateException("Cannot calculate with this DH object")
}

View File

@ -28,7 +28,6 @@ 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
@ -57,7 +56,7 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
*/
var handshakeHash: ByteArray?
private set
private var prev_h: ByteArray?
private var prevH: ByteArray?
/**
* Constructs a new symmetric state object.
@ -73,50 +72,38 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
this.protocolName = protocolName
cipher = createCipher(cipherName)
hash = createHash(hashName)
val hashLength = hash!!.getDigestLength()
val hashLength = hash!!.digestLength
ck = ByteArray(hashLength)
this.handshakeHash = ByteArray(hashLength)
prev_h = ByteArray(hashLength)
val initialHash = ByteArray(hashLength)
this.handshakeHash = initialHash
prevH = ByteArray(hashLength)
val protocolNameBytes: ByteArray
try {
protocolNameBytes = protocolName.toByteArray(charset("UTF-8"))
} catch (e: UnsupportedEncodingException) {
val protocolNameBytes: ByteArray = try {
protocolName.toByteArray(charset("UTF-8"))
} catch (_: 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()
)
protocolNameBytes.copyInto(initialHash, 0, 0, protocolNameBytes.size)
initialHash.fill(0, protocolNameBytes.size, initialHash.size)
} else {
hashOne(
protocolNameBytes, 0, protocolNameBytes.size,
this.handshakeHash!!, 0, handshakeHash!!.size
initialHash, 0, initialHash.size
)
}
System.arraycopy(this.handshakeHash, 0, ck, 0, hashLength)
initialHash.copyInto(ck!!, 0, 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, "Protocol: $protocolName")
Log.d(TAG, "Initial hash (h): " + bytesToHex(initialHash))
Log.d(TAG, "Initial chaining key (ck): " + bytesToHex(ck!!))
Log.d(TAG, "Hash length: " + initialHash.size)
Log.d(TAG, "=========================================")
}
@ -137,15 +124,11 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
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)
data.copyInto(inputData, 0, offset, offset + 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!!
)
)
Log.d(TAG, "Input data ($length bytes): " + bytesToHex(inputData))
Log.d(TAG, "Current CK: " + bytesToHex(ck!!))
Log.d(TAG, "Current Hash: " + bytesToHex(this.handshakeHash!!))
val keyLength = cipher!!.keyLength
val tempKey = ByteArray(keyLength)
@ -172,12 +155,8 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
// 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, "New CK: " + bytesToHex(ck!!))
Log.d(TAG, "Hash unchanged: " + bytesToHex(this.handshakeHash!!))
Log.d(TAG, "Cipher now has key: " + (cipher!!.macLength > 0))
}
@ -191,28 +170,21 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
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)
data.copyInto(inputData, 0, offset, offset + 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!!
)
)
Log.d(TAG, "Input data ($length bytes): " + bytesToHex(inputData))
Log.d(TAG, "Current Hash: " + bytesToHex(this.handshakeHash!!))
val h = handshakeHash!!
hashTwo(
this.handshakeHash!!, 0, handshakeHash!!.size, data, offset, length,
this.handshakeHash!!, 0, handshakeHash!!.size
h, 0, h.size, data, offset, length,
h, 0, h.size
)
// LOGGING: After mixHash operation (matching iOS)
Log.d(TAG, "*** Android mixHash() AFTER ***")
Log.d(
TAG, "New Hash: " + Companion.bytesToHex(
this.handshakeHash!!
)
)
Log.d(TAG, "New Hash: " + bytesToHex(this.handshakeHash!!))
}
/**
@ -221,7 +193,7 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
* @param key The pre-shared key value.
*/
fun mixPreSharedKey(key: ByteArray) {
val temp = ByteArray(hash!!.getDigestLength())
val temp = ByteArray(hash!!.digestLength)
try {
hkdf(ck!!, 0, ck!!.size, key, 0, key.size, ck!!, 0, ck!!.size, temp, 0, temp.size)
mixHash(temp, 0, temp.size)
@ -333,10 +305,11 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
plaintextOffset: Int,
length: Int
): Int {
System.arraycopy(this.handshakeHash, 0, prev_h, 0, handshakeHash!!.size)
val h = handshakeHash!!
h.copyInto(prevH!!, 0, 0, h.size)
mixHash(ciphertext, ciphertextOffset, length)
return cipher!!.decryptWithAd(
prev_h,
prevH,
ciphertext,
ciphertextOffset,
plaintext,
@ -348,20 +321,15 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
/**
* 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),
@ -388,8 +356,8 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
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()
c1?.destroy()
c2?.destroy()
pair = null
}
}
@ -420,9 +388,9 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
Noise.destroy(this.handshakeHash!!)
this.handshakeHash = null
}
if (prev_h != null) {
Noise.destroy(prev_h!!)
prev_h = null
if (prevH != null) {
Noise.destroy(prevH!!)
prevH = null
}
}
@ -450,8 +418,8 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
hash!!.update(data, offset, length)
try {
hash!!.digest(output, outputOffset, outputLength)
} catch (e: DigestException) {
Arrays.fill(output, outputOffset, outputLength, 0.toByte())
} catch (_: DigestException) {
output.fill(0, outputOffset, outputLength)
}
}
@ -480,8 +448,8 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
hash!!.update(data2, offset2, length2)
try {
hash!!.digest(output, outputOffset, outputLength)
} catch (e: DigestException) {
Arrays.fill(output, outputOffset, outputLength, 0.toByte())
} catch (_: DigestException) {
output.fill(0, outputOffset, outputLength)
}
}
@ -505,19 +473,18 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
) {
// 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 hashLength = hash!!.digestLength
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())
block.fill(0, keyLength, blockLength)
} else {
hash!!.reset()
hash!!.update(key, keyOffset, keyLength)
hash!!.digest(block, 0, hashLength)
Arrays.fill(block, hashLength, blockLength, 0.toByte())
block.fill(0, hashLength, blockLength)
}
for (i in 0 until blockLength) {
block[i] = (block[i].toInt() xor 0x36).toByte()
@ -533,8 +500,8 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
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())
} catch (_: DigestException) {
output.fill(0, outputOffset, outputLength)
} finally {
destroy(block)
}
@ -564,7 +531,7 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
output1: ByteArray, output1Offset: Int, output1Length: Int,
output2: ByteArray, output2Offset: Int, output2Length: Int
) {
val hashLength = hash!!.getDigestLength()
val hashLength = hash!!.digestLength
val tempKey = ByteArray(hashLength)
val tempHash = ByteArray(hashLength + 1)
try {
@ -590,7 +557,7 @@ internal class SymmetricState(protocolName: String, cipherName: String, hashName
private fun bytesToHex(bytes: ByteArray): String {
val sb = StringBuilder()
for (b in bytes) {
sb.append(String.format("%02x", b))
sb.append("%02x".format(b))
}
return sb.toString()
}