feat: noise java lib | WIP NoiseHandler IK handshake
Some checks failed
/ mirror (push) Failing after 4s
/ build-stealth (push) Successful in 10m8s
/ build (push) Successful in 10m12s

This commit is contained in:
Florian Griffon 2025-06-14 11:29:22 +03:00
parent 9ef3ad5b56
commit 0badc8862c
7 changed files with 188 additions and 10 deletions

View File

@ -24,7 +24,7 @@ android {
applicationId = "com.icing.dialer"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
@ -42,3 +42,7 @@ android {
flutter {
source = "../.."
}
dependencies {
implementation files('libs/noise-java-1.0.jar')
}

Binary file not shown.

View File

@ -3,9 +3,15 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.telephony" android:required="true" />
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.

View File

@ -5,8 +5,6 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
@ -73,7 +71,7 @@
android:name="android.telecom.IN_CALL_SERVICE_UI"
android:value="true" />
</service>
<!-- Custom ConnextionService, will be needed at some point when we implement our own protocol -->
<!-- Custom ConnectionService, will be needed when we implement our own protocol -->
<!-- <service
android:name=".services.CallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"

View File

@ -0,0 +1,165 @@
package com.icing.dialer
import android.util.Base64
import com.southernstorm.noise.protocol.CipherState
import com.southernstorm.noise.protocol.CipherStatePair
import com.southernstorm.noise.protocol.HandshakeState
import com.southernstorm.noise.protocol.Noise
import javax.crypto.BadPaddingException
import javax.crypto.ShortBufferException
import java.security.NoSuchAlgorithmException
import java.util.Arrays
class NoiseHandler(
private val localKeyBase64: String, // ED25519 private (initiator) or public (responder) key (Base64-encoded)
private val remotePublicKeyBase64: String // Remote ED25519 public key (Base64-encoded)
) {
private var handshakeState: HandshakeState? = null
private var cipherStatePair: CipherStatePair? = null
/**
* Wipes sensitive data by filling the byte array with zeros.
*/
private fun wipe(data: ByteArray?) {
data?.let { Arrays.fill(it, 0.toByte()) }
}
/**
* Initializes the Noise handshake.
* @param isInitiator True if this is the initiator, false if responder.
* @return The initial handshake message.
* @throws IllegalArgumentException If keys are invalid.
* @throws IllegalStateException If handshake fails to start.
*/
fun initialize(isInitiator: Boolean): ByteArray {
var localKey: ByteArray? = null
var remotePublicKey: ByteArray? = null
try {
val protocolName = "Noise_IK_25519_AESGCM_SHA256"
handshakeState = HandshakeState(
protocolName,
if (isInitiator) HandshakeState.INITIATOR else HandshakeState.RESPONDER
)
// Set local key (private for initiator, public for responder)
localKey = Base64.decode(localKeyBase64, Base64.DEFAULT)
if (localKey.size != 32) {
throw IllegalArgumentException("Invalid local key size: ${localKey.size}")
}
if (isInitiator) {
handshakeState?.localKeyPair?.setPrivateKey(localKey, 0)
?: throw IllegalStateException("Local key pair not initialized")
} else {
handshakeState?.localKeyPair?.setPublicKey(localKey, 0)
?: throw IllegalStateException("Local key pair not initialized")
}
// Set remote public key
remotePublicKey = Base64.decode(remotePublicKeyBase64, Base64.DEFAULT)
if (remotePublicKey.size != 32) {
throw IllegalArgumentException("Invalid remote public key size: ${remotePublicKey.size}")
}
handshakeState?.remotePublicKey?.setPublicKey(remotePublicKey, 0)
?: throw IllegalStateException("Remote public key not initialized")
// Start handshake and write initial message
handshakeState?.start() ?: throw IllegalStateException("Handshake state not initialized")
val messageBuffer = ByteArray(256) // Sufficient for IK initial message
val payload = ByteArray(0) // Empty payload
val writtenLength: Int = handshakeState?.writeMessage(
messageBuffer, 0, payload, 0, payload.size
) ?: throw IllegalStateException("Failed to write handshake message")
return messageBuffer.copyOf(writtenLength)
} catch (e: NoSuchAlgorithmException) {
throw IllegalStateException("Unsupported algorithm: ${e.message}", e)
} catch (e: ShortBufferException) {
throw IllegalStateException("Buffer too small for handshake message", e)
} finally {
wipe(localKey)
wipe(remotePublicKey)
}
}
/**
* Processes a handshake message and returns the next message or null if complete.
* @param message The received handshake message.
* @return The next handshake message or null if handshake is complete.
* @throws IllegalStateException If handshake state is invalid.
* @throws BadPaddingException If message decryption fails.
*/
fun processHandshakeMessage(message: ByteArray): ByteArray? {
try {
val handshake = handshakeState ?: throw IllegalStateException("Handshake not initialized")
val messageBuffer = ByteArray(256) // Sufficient for IK payload + MAC
val writtenLength: Int = handshake.readMessage(
message, 0, message.size, messageBuffer, 0
)
if (handshake.getAction() == HandshakeState.SPLIT) {
cipherStatePair = handshake.split()
return null // Handshake complete
}
// Write next message
val payload = ByteArray(0) // Empty payload
val nextMessage = ByteArray(256)
val nextWrittenLength: Int = handshake.writeMessage(
nextMessage, 0, payload, 0, payload.size
)
return nextMessage.copyOf(nextWrittenLength)
} catch (e: ShortBufferException) {
throw IllegalStateException("Buffer too small for handshake message", e)
} catch (e: BadPaddingException) {
throw IllegalStateException("Invalid handshake message: ${e.message}", e)
}
}
/**
* Encrypts data using the sender's cipher state.
* @param data The data to encrypt.
* @return The encrypted data.
* @throws IllegalStateException If handshake is not completed.
*/
fun encryptData(data: ByteArray): ByteArray {
val cipherState = cipherStatePair?.getSender()
?: throw IllegalStateException("Handshake not completed")
try {
val outputBuffer = ByteArray(data.size + cipherState.getMACLength()) // Account for AES-GCM MAC
val length: Int = cipherState.encryptWithAd(null, data, 0, outputBuffer, 0, data.size)
return outputBuffer.copyOf(length)
} catch (e: ShortBufferException) {
throw IllegalStateException("Buffer too small for encryption: ${e.message}", e)
}
}
/**
* Decrypts data using the receiver's cipher state.
* @param data The encrypted data.
* @return The decrypted data.
* @throws IllegalStateException If handshake is not completed.
* @throws BadPaddingException If decryption fails.
*/
fun decryptData(data: ByteArray): ByteArray {
val cipherState = cipherStatePair?.getReceiver()
?: throw IllegalStateException("Handshake not completed")
try {
val outputBuffer = ByteArray(data.size)
val length: Int = cipherState.decryptWithAd(null, data, 0, outputBuffer, 0, data.size)
return outputBuffer.copyOf(length)
} catch (e: ShortBufferException) {
throw IllegalStateException("Buffer too small for decryption: ${e.message}", e)
} catch (e: BadPaddingException) {
throw IllegalStateException("Invalid ciphertext: ${e.message}", e)
}
}
/**
* Cleans up sensitive cryptographic data.
*/
fun destroy() {
handshakeState?.destroy()
cipherStatePair?.destroy()
handshakeState = null
cipherStatePair = null
}
}

View File

@ -127,7 +127,7 @@ class MyInCallService : InCallService() {
}
call.registerCallback(callCallback)
if (callAudioState != null) {
val audioState = callAudioState
val audioState = callAudioState
channel?.invokeMethod("audioStateChanged", mapOf(
"route" to audioState.route,
"muted" to audioState.isMuted,

View File

@ -3,10 +3,15 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.telephony" android:required="true" />
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.