WIP: noiseJavaFork #61

Draft
florian wants to merge 3 commits from noiseJavaFork into dev
14 changed files with 377 additions and 164 deletions

View File

@ -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')
}

Binary file not shown.

View File

@ -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.

View File

@ -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"

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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.

View File

@ -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