WIP: noiseJavaFork #61
@ -24,7 +24,7 @@ android {
|
|||||||
applicationId = "com.icing.dialer"
|
applicationId = "com.icing.dialer"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
minSdk = flutter.minSdkVersion
|
minSdk = 23
|
||||||
targetSdk = flutter.targetSdkVersion
|
targetSdk = flutter.targetSdkVersion
|
||||||
versionCode = flutter.versionCode
|
versionCode = flutter.versionCode
|
||||||
versionName = flutter.versionName
|
versionName = flutter.versionName
|
||||||
@ -42,3 +42,7 @@ android {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation files('libs/noise-java-1.0.jar')
|
||||||
|
}
|
BIN
dialer/android/app/libs/noise-java-1.0.jar
Normal file
BIN
dialer/android/app/libs/noise-java-1.0.jar
Normal file
Binary file not shown.
@ -3,9 +3,15 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||||
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
|
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
|
<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.camera" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.telephony" android:required="true" />
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
the Flutter tool needs it to communicate with the running application
|
the Flutter tool needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
@ -5,8 +5,6 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
<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_CALL_LOG"/>
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
||||||
@ -73,7 +71,7 @@
|
|||||||
android:name="android.telecom.IN_CALL_SERVICE_UI"
|
android:name="android.telecom.IN_CALL_SERVICE_UI"
|
||||||
android:value="true" />
|
android:value="true" />
|
||||||
</service>
|
</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
|
<!-- <service
|
||||||
android:name=".services.CallConnectionService"
|
android:name=".services.CallConnectionService"
|
||||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
package com.icing.dialer
|
|
||||||
|
|
||||||
import java.security.KeyStore
|
|
||||||
|
|
||||||
object KeyDeleterHelper {
|
|
||||||
|
|
||||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the key pair associated with the given alias from the Android Keystore.
|
|
||||||
*
|
|
||||||
* @param alias The alias of the key pair to delete.
|
|
||||||
* @throws Exception if deletion fails.
|
|
||||||
*/
|
|
||||||
fun deleteKeyPair(alias: String) {
|
|
||||||
try {
|
|
||||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
|
||||||
|
|
||||||
if (!keyStore.containsAlias(alias)) {
|
|
||||||
throw Exception("No key found with alias \"$alias\" to delete.")
|
|
||||||
}
|
|
||||||
|
|
||||||
keyStore.deleteEntry(alias)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw Exception("Failed to delete key pair: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
package com.icing.dialer
|
|
||||||
|
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
|
||||||
import android.security.keystore.KeyProperties
|
|
||||||
import java.security.KeyPairGenerator
|
|
||||||
import java.security.KeyStore
|
|
||||||
|
|
||||||
object KeyGeneratorHelper {
|
|
||||||
|
|
||||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates an ECDSA P-256 key pair and stores it in the Android Keystore.
|
|
||||||
*
|
|
||||||
* @param alias Unique identifier for the key pair.
|
|
||||||
* @throws Exception if key generation fails.
|
|
||||||
*/
|
|
||||||
fun generateECKeyPair(alias: String) {
|
|
||||||
try {
|
|
||||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
|
||||||
|
|
||||||
// Check if the key already exists
|
|
||||||
if (keyStore.containsAlias(alias)) {
|
|
||||||
throw Exception("Key with alias \"$alias\" already exists.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyPairGenerator = KeyPairGenerator.getInstance(
|
|
||||||
KeyProperties.KEY_ALGORITHM_EC,
|
|
||||||
ANDROID_KEYSTORE
|
|
||||||
)
|
|
||||||
|
|
||||||
val parameterSpec = KeyGenParameterSpec.Builder(
|
|
||||||
alias,
|
|
||||||
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
|
||||||
)
|
|
||||||
.setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1"))
|
|
||||||
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512)
|
|
||||||
.setUserAuthenticationRequired(false) // Set to true if you require user authentication
|
|
||||||
.build()
|
|
||||||
|
|
||||||
keyPairGenerator.initialize(parameterSpec)
|
|
||||||
keyPairGenerator.generateKeyPair()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw Exception("Failed to generate EC key pair: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
package com.icing.dialer
|
package com.icing.dialer
|
||||||
|
|
||||||
import java.security.PrivateKey
|
import android.os.Build
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
@ -8,15 +8,21 @@ import io.flutter.plugin.common.MethodCall
|
|||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
|
import java.security.PrivateKey
|
||||||
import java.security.Signature
|
import java.security.Signature
|
||||||
|
import java.security.spec.ECGenParameterSpec
|
||||||
|
|
||||||
class KeystoreHelper(private val call: MethodCall, private val result: MethodChannel.Result) {
|
class KeystoreHelper(private val call: MethodCall, private val result: MethodChannel.Result) {
|
||||||
|
|
||||||
private val ANDROID_KEYSTORE = "AndroidKeyStore"
|
private val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||||
|
|
||||||
fun handleMethodCall() {
|
fun handleMethodCall() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
result.error("UNSUPPORTED_API", "ED25519 requires Android 11 (API 30) or higher", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"generateKeyPair" -> generateECKeyPair()
|
"generateKeyPair" -> generateEDKeyPair()
|
||||||
"signData" -> signData()
|
"signData" -> signData()
|
||||||
"getPublicKey" -> getPublicKey()
|
"getPublicKey" -> getPublicKey()
|
||||||
"deleteKeyPair" -> deleteKeyPair()
|
"deleteKeyPair" -> deleteKeyPair()
|
||||||
@ -25,7 +31,7 @@ class KeystoreHelper(private val call: MethodCall, private val result: MethodCha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateECKeyPair() {
|
private fun generateEDKeyPair() {
|
||||||
val alias = call.argument<String>("alias")
|
val alias = call.argument<String>("alias")
|
||||||
if (alias == null) {
|
if (alias == null) {
|
||||||
result.error("INVALID_ARGUMENT", "Alias is required", null)
|
result.error("INVALID_ARGUMENT", "Alias is required", null)
|
||||||
@ -44,16 +50,14 @@ class KeystoreHelper(private val call: MethodCall, private val result: MethodCha
|
|||||||
KeyProperties.KEY_ALGORITHM_EC,
|
KeyProperties.KEY_ALGORITHM_EC,
|
||||||
ANDROID_KEYSTORE
|
ANDROID_KEYSTORE
|
||||||
)
|
)
|
||||||
|
|
||||||
val parameterSpec = KeyGenParameterSpec.Builder(
|
val parameterSpec = KeyGenParameterSpec.Builder(
|
||||||
alias,
|
alias,
|
||||||
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
||||||
)
|
)
|
||||||
.setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1"))
|
.setAlgorithmParameterSpec(ECGenParameterSpec("ed25519"))
|
||||||
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512)
|
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||||
.setUserAuthenticationRequired(false)
|
.setUserAuthenticationRequired(false)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
keyPairGenerator.initialize(parameterSpec)
|
keyPairGenerator.initialize(parameterSpec)
|
||||||
keyPairGenerator.generateKeyPair()
|
keyPairGenerator.generateKeyPair()
|
||||||
|
|
||||||
@ -73,17 +77,14 @@ class KeystoreHelper(private val call: MethodCall, private val result: MethodCha
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||||
|
|
||||||
val privateKey = keyStore.getKey(alias, null) as? PrivateKey ?: run {
|
val privateKey = keyStore.getKey(alias, null) as? PrivateKey ?: run {
|
||||||
result.error("KEY_NOT_FOUND", "Private key not found for alias \"$alias\".", null)
|
result.error("KEY_NOT_FOUND", "Private key not found for alias \"$alias\".", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val signature = Signature.getInstance("Ed25519")
|
||||||
val signature = Signature.getInstance("SHA256withECDSA")
|
|
||||||
signature.initSign(privateKey)
|
signature.initSign(privateKey)
|
||||||
signature.update(data.toByteArray())
|
signature.update(data.toByteArray())
|
||||||
val signedBytes = signature.sign()
|
val signedBytes = signature.sign()
|
||||||
|
|
||||||
val signatureBase64 = Base64.encodeToString(signedBytes, Base64.DEFAULT)
|
val signatureBase64 = Base64.encodeToString(signedBytes, Base64.DEFAULT)
|
||||||
result.success(signatureBase64)
|
result.success(signatureBase64)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -0,0 +1,169 @@
|
|||||||
|
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 NoiseHandlerXK(
|
||||||
|
private val localKeyBase64: String, // ED25519 private (initiator) or private (responder) key (Base64-encoded)
|
||||||
|
private val remotePublicKeyBase64: String? // Remote ED25519 public key (Base64-encoded, required for initiator only)
|
||||||
|
) {
|
||||||
|
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 with the XK pattern.
|
||||||
|
* @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_XK_25519_AESGCM_SHA256"
|
||||||
|
handshakeState = HandshakeState(
|
||||||
|
protocolName,
|
||||||
|
if (isInitiator) HandshakeState.INITIATOR else HandshakeState.RESPONDER
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set local key (private key for both initiator and responder in XK)
|
||||||
|
localKey = Base64.decode(localKeyBase64, Base64.DEFAULT)
|
||||||
|
if (localKey.size != 32) {
|
||||||
|
throw IllegalArgumentException("Invalid local key size: ${localKey.size}")
|
||||||
|
}
|
||||||
|
handshakeState?.localKeyPair?.setPrivateKey(localKey, 0)
|
||||||
|
?: throw IllegalStateException("Local key pair not initialized")
|
||||||
|
|
||||||
|
// Set remote public key (required for initiator only in XK)
|
||||||
|
if (isInitiator) {
|
||||||
|
if (remotePublicKeyBase64 == null) {
|
||||||
|
throw IllegalArgumentException("Remote public key required for initiator")
|
||||||
|
}
|
||||||
|
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 keyLength = handshakeState?.localKeyPair?.getPublicKeyLength() ?: 32
|
||||||
|
val macLength = 16 // AES-GCM MAC length
|
||||||
|
val messageBuffer = ByteArray(keyLength + macLength + 128) // Conservative sizing
|
||||||
|
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 keyLength = handshake.localKeyPair?.getPublicKeyLength() ?: 32
|
||||||
|
val macLength = 16 // AES-GCM MAC length
|
||||||
|
val messageBuffer = ByteArray(keyLength + macLength + 128) // Conservative sizing
|
||||||
|
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(keyLength + macLength + 128)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
package com.icing.dialer
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import com.southernstorm.noise.protocol.CipherStatePair
|
||||||
|
import com.southernstorm.noise.protocol.HandshakeState
|
||||||
|
import com.southernstorm.noise.protocol.Noise
|
||||||
|
import java.util.Arrays
|
||||||
|
import javax.crypto.BadPaddingException
|
||||||
|
import javax.crypto.ShortBufferException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
|
||||||
|
class NoiseHandlerXX(
|
||||||
|
private val localKeyBase64: String // ED25519 private key (Base64-encoded)
|
||||||
|
) {
|
||||||
|
private var handshakeState: HandshakeState? = null
|
||||||
|
private var cipherStatePair: CipherStatePair? = null
|
||||||
|
private var remotePublicKeyBase64: String? = 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 XX handshake.
|
||||||
|
* @param isInitiator True if this is the initiator, false if responder.
|
||||||
|
* @return The initial handshake message.
|
||||||
|
* @throws IllegalArgumentException If local key is invalid.
|
||||||
|
* @throws IllegalStateException If handshake fails to start.
|
||||||
|
*/
|
||||||
|
fun initialize(isInitiator: Boolean): ByteArray {
|
||||||
|
var localKey: ByteArray? = null
|
||||||
|
try {
|
||||||
|
val protocolName = "Noise_XX_25519_AESGCM_SHA256"
|
||||||
|
handshakeState = HandshakeState(
|
||||||
|
protocolName,
|
||||||
|
if (isInitiator) HandshakeState.INITIATOR else HandshakeState.RESPONDER
|
||||||
|
)
|
||||||
|
|
||||||
|
// Set local private key
|
||||||
|
localKey = Base64.decode(localKeyBase64, Base64.DEFAULT)
|
||||||
|
if (localKey.size != 32) {
|
||||||
|
throw IllegalArgumentException("Invalid local key size: ${localKey.size}")
|
||||||
|
}
|
||||||
|
handshakeState?.localKeyPair?.setPrivateKey(localKey, 0)
|
||||||
|
?: throw IllegalStateException("Local key pair not initialized")
|
||||||
|
|
||||||
|
// Start handshake and write initial message
|
||||||
|
handshakeState?.start() ?: throw IllegalStateException("Handshake state not initialized")
|
||||||
|
val keyLength = handshakeState?.localKeyPair?.getPublicKeyLength() ?: 32
|
||||||
|
val macLength = 16 // AES-GCM MAC length
|
||||||
|
val messageBuffer = ByteArray(keyLength + macLength + 128) // Conservative sizing
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a handshake message and returns the next message or null if complete.
|
||||||
|
* @param message The received handshake message.
|
||||||
|
* @return The next handshake message, null if handshake is complete, or the remote public key (Base64-encoded).
|
||||||
|
* @throws IllegalStateException If handshake state is invalid.
|
||||||
|
* @throws BadPaddingException If message decryption fails.
|
||||||
|
*/
|
||||||
|
fun processHandshakeMessage(message: ByteArray): Pair<ByteArray?, String?> {
|
||||||
|
var remotePublicKey: ByteArray? = null
|
||||||
|
try {
|
||||||
|
val handshake = handshakeState ?: throw IllegalStateException("Handshake not initialized")
|
||||||
|
val keyLength = handshake.localKeyPair?.getPublicKeyLength() ?: 32
|
||||||
|
val macLength = 16 // AES-GCM MAC length
|
||||||
|
val messageBuffer = ByteArray(keyLength + macLength + 128) // Conservative sizing
|
||||||
|
val payload = ByteArray(0) // Empty payload
|
||||||
|
val writtenLength: Int = handshake.readMessage(
|
||||||
|
message, 0, message.size, messageBuffer, 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extract remote public key after reading message
|
||||||
|
if (handshake.remotePublicKey.hasPublicKey()) {
|
||||||
|
remotePublicKey = ByteArray(keyLength)
|
||||||
|
handshake.remotePublicKey.getPublicKey(remotePublicKey, 0)
|
||||||
|
remotePublicKeyBase64 = Base64.encodeToString(remotePublicKey, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handshake.getAction() == HandshakeState.SPLIT) {
|
||||||
|
cipherStatePair = handshake.split()
|
||||||
|
return Pair(null, remotePublicKeyBase64) // Handshake complete
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write next message
|
||||||
|
val nextMessage = ByteArray(keyLength + macLength + 128)
|
||||||
|
val nextWrittenLength: Int = handshake.writeMessage(
|
||||||
|
nextMessage, 0, payload, 0, payload.size
|
||||||
|
)
|
||||||
|
return Pair(nextMessage.copyOf(nextWrittenLength), remotePublicKeyBase64)
|
||||||
|
} catch (e: ShortBufferException) {
|
||||||
|
throw IllegalStateException("Buffer too small for handshake message", e)
|
||||||
|
} catch (e: BadPaddingException) {
|
||||||
|
throw IllegalStateException("Invalid handshake message: ${e.message}", e)
|
||||||
|
} finally {
|
||||||
|
wipe(remotePublicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the remote public key received during the handshake.
|
||||||
|
* @return The Base64-encoded remote public key, or null if not yet received.
|
||||||
|
*/
|
||||||
|
fun getRemotePublicKey(): String? {
|
||||||
|
return remotePublicKeyBase64
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up sensitive cryptographic data.
|
||||||
|
*/
|
||||||
|
fun destroy() {
|
||||||
|
handshakeState?.destroy()
|
||||||
|
cipherStatePair?.destroy()
|
||||||
|
handshakeState = null
|
||||||
|
cipherStatePair = null
|
||||||
|
remotePublicKeyBase64 = null
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
package com.icing.dialer
|
|
||||||
|
|
||||||
import java.security.KeyStore
|
|
||||||
import java.security.PublicKey
|
|
||||||
import android.util.Base64
|
|
||||||
|
|
||||||
object PublicKeyHelper {
|
|
||||||
|
|
||||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the public key associated with the given alias.
|
|
||||||
*
|
|
||||||
* @param alias The alias of the key pair.
|
|
||||||
* @return The public key as a Base64-encoded string.
|
|
||||||
* @throws Exception if retrieval fails.
|
|
||||||
*/
|
|
||||||
fun getPublicKey(alias: String): String {
|
|
||||||
try {
|
|
||||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
|
||||||
|
|
||||||
val certificate = keyStore.getCertificate(alias) ?: throw Exception("Certificate not found for alias \"$alias\".")
|
|
||||||
val publicKey: PublicKey = certificate.publicKey
|
|
||||||
|
|
||||||
return Base64.encodeToString(publicKey.encoded, Base64.DEFAULT)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw Exception("Failed to retrieve public key: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
package com.icing.dialer
|
|
||||||
|
|
||||||
import android.security.keystore.KeyProperties
|
|
||||||
import java.security.KeyStore
|
|
||||||
import java.security.Signature
|
|
||||||
import android.util.Base64
|
|
||||||
import java.security.PrivateKey
|
|
||||||
|
|
||||||
object SignerHelper {
|
|
||||||
|
|
||||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs the provided data using the private key associated with the given alias.
|
|
||||||
*
|
|
||||||
* @param alias The alias of the key pair.
|
|
||||||
* @param data The data to sign.
|
|
||||||
* @return The signature as a Base64-encoded string.
|
|
||||||
* @throws Exception if signing fails.
|
|
||||||
*/
|
|
||||||
fun signData(alias: String, data: ByteArray): String {
|
|
||||||
try {
|
|
||||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
|
||||||
|
|
||||||
val privateKey = keyStore.getKey(alias, null) as? PrivateKey?: throw Exception("Private key not found for alias \"$alias\".")
|
|
||||||
|
|
||||||
val signature = Signature.getInstance("SHA256withECDSA")
|
|
||||||
signature.initSign(privateKey)
|
|
||||||
signature.update(data)
|
|
||||||
val signedBytes = signature.sign()
|
|
||||||
|
|
||||||
return Base64.encodeToString(signedBytes, Base64.DEFAULT)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw Exception("Failed to sign data: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,10 +3,15 @@
|
|||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
|
||||||
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
|
<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.camera" android:required="false" />
|
||||||
|
<uses-feature android:name="android.hardware.telephony" android:required="true" />
|
||||||
<!-- The INTERNET permission is required for development. Specifically,
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
the Flutter tool needs it to communicate with the running application
|
the Flutter tool needs it to communicate with the running application
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
@ -10,7 +10,7 @@ class AsymmetricCryptoService {
|
|||||||
final String _aliasPrefix = 'icing_';
|
final String _aliasPrefix = 'icing_';
|
||||||
final Uuid _uuid = Uuid();
|
final Uuid _uuid = Uuid();
|
||||||
|
|
||||||
/// Generates an ECDSA P-256 key pair with a unique alias and stores its metadata.
|
/// Generates an ED25519 key pair with a unique alias and stores its metadata.
|
||||||
Future<String> generateKeyPair({String? label}) async {
|
Future<String> generateKeyPair({String? label}) async {
|
||||||
try {
|
try {
|
||||||
// Generate a unique identifier for the key
|
// Generate a unique identifier for the key
|
||||||
|
Loading…
Reference in New Issue
Block a user