diff --git a/CHANGELOG.md b/CHANGELOG.md index b405069..6b1a98b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [10.1.0] - 2026-02-19 + +* Added Swift Package Manager (SPM) support for iOS and macOS plugin integration. +* Migrated iOS/macOS native source layout to `Package.swift` + `Sources//`. +* Updated Pigeon generation output paths for Darwin host code to match the SPM layout. +* Updated CocoaPods podspecs to remain compatible alongside SPM. +* Updated package and platform version references/documentation. +* **Fix:** Enhanced biometric type detection for android. + ## [10.0.0] - 2026-02-06 * **Breaking:** Added new `BiometricError` enum values; consumers using exhaustive switches must handle the new cases(security update required, not supported, system canceled, prompt error). diff --git a/README.md b/README.md index e4d3262..39bb077 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ To get started with Biometric Signature, follow these steps: ```yaml dependencies: - biometric_signature: ^10.0.0 + biometric_signature: ^10.1.0 ``` | | Android | iOS | macOS | Windows | diff --git a/android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignaturePlugin.kt b/android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignaturePlugin.kt index a6a5f66..10aa4b4 100644 --- a/android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignaturePlugin.kt +++ b/android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignaturePlugin.kt @@ -2,7 +2,7 @@ package com.visionflutter.biometric_signature import android.content.Context import android.content.pm.PackageManager -import android.hardware.fingerprint.FingerprintManager +import android.content.res.Resources import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties @@ -98,12 +98,16 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA override fun biometricAuthAvailable(callback: (Result) -> Unit) { val act = activity if (act == null) { - callback(Result.success(BiometricAvailability( - canAuthenticate = false, - hasEnrolledBiometrics = false, - availableBiometrics = emptyList(), - reason = "NO_ACTIVITY" - ))) + callback( + Result.success( + BiometricAvailability( + canAuthenticate = false, + hasEnrolledBiometrics = false, + availableBiometrics = emptyList(), + reason = "NO_ACTIVITY" + ) + ) + ) return } @@ -115,16 +119,20 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA canAuth != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE && canAuth != BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE - val (types, _) = detectBiometricTypes() + val biometricTypes = detectBiometricTypes() val reason = if (!canAuthenticate) biometricErrorName(canAuth) else null - callback(Result.success(BiometricAvailability( - canAuthenticate = canAuthenticate, - hasEnrolledBiometrics = hasEnrolledBiometrics, - availableBiometrics = types, - reason = reason - ))) + callback( + Result.success( + BiometricAvailability( + canAuthenticate = canAuthenticate, + hasEnrolledBiometrics = hasEnrolledBiometrics, + availableBiometrics = biometricTypes, + reason = reason + ) + ) + ) } override fun createKeys( @@ -135,7 +143,14 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA ) { val act = activity if (act == null) { - callback(Result.success(KeyCreationResult(code = BiometricError.UNKNOWN, error = "Foreground activity required"))) + callback( + Result.success( + KeyCreationResult( + code = BiometricError.UNKNOWN, + error = "Foreground activity required" + ) + ) + ) return } @@ -148,7 +163,7 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA val signatureType = config?.signatureType ?: SignatureType.RSA val enforceBiometric = config?.enforceBiometric ?: false - val mode = when(signatureType) { + val mode = when (signatureType) { SignatureType.RSA -> KeyMode.RSA SignatureType.ECDSA -> if (enableDecryption) KeyMode.HYBRID_EC else KeyMode.EC_SIGN_ONLY } @@ -157,13 +172,47 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA // Logic based on mode when (mode) { - KeyMode.RSA -> createRsaKeys(act, callback, useDeviceCredentials, invalidateOnEnrollment, enableDecryption, enforceBiometric, keyFormat, prompt) - KeyMode.EC_SIGN_ONLY -> createEcSigningKeys(act, callback, useDeviceCredentials, invalidateOnEnrollment, enforceBiometric, keyFormat, prompt) - KeyMode.HYBRID_EC -> createHybridEcKeys(act, callback, useDeviceCredentials, invalidateOnEnrollment, keyFormat, enforceBiometric, prompt) + KeyMode.RSA -> createRsaKeys( + act, + callback, + useDeviceCredentials, + invalidateOnEnrollment, + enableDecryption, + enforceBiometric, + keyFormat, + prompt + ) + + KeyMode.EC_SIGN_ONLY -> createEcSigningKeys( + act, + callback, + useDeviceCredentials, + invalidateOnEnrollment, + enforceBiometric, + keyFormat, + prompt + ) + + KeyMode.HYBRID_EC -> createHybridEcKeys( + act, + callback, + useDeviceCredentials, + invalidateOnEnrollment, + keyFormat, + enforceBiometric, + prompt + ) } } catch (e: Exception) { - callback(Result.success(KeyCreationResult(code = mapToBiometricError(e), error = e.message))) + callback( + Result.success( + KeyCreationResult( + code = mapToBiometricError(e), + error = e.message + ) + ) + ) } } } @@ -260,7 +309,8 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA writeFileAtomic(EC_PUB_FILENAME, publicKeyBytes) // For hybrid, we return the Signing Key as default, and Decryption Key as optional - val decryptingPublicKey = KeyFactory.getInstance("EC").generatePublic(X509EncodedKeySpec(publicKeyBytes)) + val decryptingPublicKey = + KeyFactory.getInstance("EC").generatePublic(X509EncodedKeySpec(publicKeyBytes)) val response = buildKeyResponse( publicKey = signingKeyPair.public, @@ -281,17 +331,32 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA ) { val act = activity if (act == null) { - callback(Result.success(SignatureResult(code = BiometricError.UNKNOWN, error = "Foreground activity required"))) + callback( + Result.success( + SignatureResult( + code = BiometricError.UNKNOWN, + error = "Foreground activity required" + ) + ) + ) return } if (payload.isBlank()) { - callback(Result.success(SignatureResult(code = BiometricError.INVALID_INPUT, error = "Payload is required"))) + callback( + Result.success( + SignatureResult( + code = BiometricError.INVALID_INPUT, + error = "Payload is required" + ) + ) + ) return } pluginScope.launch { try { - val mode = inferKeyModeFromKeystore() ?: throw SecurityException("Signing key not found") + val mode = + inferKeyModeFromKeystore() ?: throw SecurityException("Signing key not found") val allowDeviceCredentials = config?.allowDeviceCredentials ?: false @@ -321,11 +386,19 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA } val publicKey = getSigningPublicKey() - val response = buildSignatureResponse(signatureBytes, publicKey, signatureFormat, keyFormat) + val response = + buildSignatureResponse(signatureBytes, publicKey, signatureFormat, keyFormat) callback(Result.success(response)) } catch (e: Exception) { - callback(Result.success(SignatureResult(code = mapToBiometricError(e), error = e.message))) + callback( + Result.success( + SignatureResult( + code = mapToBiometricError(e), + error = e.message + ) + ) + ) } } } @@ -339,11 +412,25 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA ) { val act = activity if (act == null) { - callback(Result.success(DecryptResult(code = BiometricError.UNKNOWN, error = "Foreground activity required"))) + callback( + Result.success( + DecryptResult( + code = BiometricError.UNKNOWN, + error = "Foreground activity required" + ) + ) + ) return } if (payload.isBlank()) { - callback(Result.success(DecryptResult(code = BiometricError.INVALID_INPUT, error = "Payload is required"))) + callback( + Result.success( + DecryptResult( + code = BiometricError.INVALID_INPUT, + error = "Payload is required" + ) + ) + ) return } @@ -362,15 +449,47 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA val cancel = config?.cancelButtonText ?: "Cancel" val decryptedData = when (mode) { - KeyMode.RSA -> decryptRsa(act, payload, payloadFormat, prompt, subtitle, cancel, allowDeviceCredentials) - KeyMode.HYBRID_EC -> decryptHybridEc(act, payload, payloadFormat, prompt, subtitle, cancel, allowDeviceCredentials) + KeyMode.RSA -> decryptRsa( + act, + payload, + payloadFormat, + prompt, + subtitle, + cancel, + allowDeviceCredentials + ) + + KeyMode.HYBRID_EC -> decryptHybridEc( + act, + payload, + payloadFormat, + prompt, + subtitle, + cancel, + allowDeviceCredentials + ) + else -> throw SecurityException("Unsupported decryption mode") } - callback(Result.success(DecryptResult(decryptedData = decryptedData, code = BiometricError.SUCCESS))) + callback( + Result.success( + DecryptResult( + decryptedData = decryptedData, + code = BiometricError.SUCCESS + ) + ) + ) - } catch(e: Exception) { - callback(Result.success(DecryptResult(code = mapToBiometricError(e), error = e.message))) + } catch (e: Exception) { + callback( + Result.success( + DecryptResult( + code = mapToBiometricError(e), + error = e.message + ) + ) + ) } } } @@ -446,26 +565,31 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA callback(Result.success(true)) } - override fun getKeyInfo(checkValidity: Boolean, keyFormat: KeyFormat, callback: (Result) -> Unit) { + override fun getKeyInfo( + checkValidity: Boolean, + keyFormat: KeyFormat, + callback: (Result) -> Unit + ) { pluginScope.launch(Dispatchers.IO) { try { val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) } - + // Check if signing key exists if (!keyStore.containsAlias(BIOMETRIC_KEY_ALIAS)) { callback(Result.success(KeyInfo(exists = false))) return@launch } - - val entry = keyStore.getEntry(BIOMETRIC_KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry + + val entry = + keyStore.getEntry(BIOMETRIC_KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry if (entry == null) { callback(Result.success(KeyInfo(exists = false))) return@launch } - + val publicKey = entry.certificate.publicKey val mode = inferKeyModeFromKeystore() - + // Check validity if requested val isValid = if (checkValidity) { runCatching { @@ -481,21 +605,23 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA } else { null } - + // Get key metadata val algorithm = publicKey.algorithm - val keySize = (publicKey as? java.security.interfaces.RSAKey)?.modulus?.bitLength()?.toLong() - ?: (publicKey as? java.security.interfaces.ECKey)?.params?.order?.bitLength()?.toLong() - + val keySize = + (publicKey as? java.security.interfaces.RSAKey)?.modulus?.bitLength()?.toLong() + ?: (publicKey as? java.security.interfaces.ECKey)?.params?.order?.bitLength() + ?.toLong() + // Format signing public key val formattedPublicKey = formatOutput(publicKey.encoded, keyFormat) - + // Check for hybrid mode and get decryption key val isHybridMode = mode == KeyMode.HYBRID_EC var decryptingPublicKey: String? = null var decryptingAlgorithm: String? = null var decryptingKeySize: Long? = null - + if (isHybridMode) { val pubBytes = readFileIfExists(EC_PUB_FILENAME) if (pubBytes != null) { @@ -507,18 +633,22 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA decryptingKeySize = 256 } } - - callback(Result.success(KeyInfo( - exists = true, - isValid = isValid, - algorithm = algorithm, - keySize = keySize, - isHybridMode = isHybridMode, - publicKey = formattedPublicKey.value, - decryptingPublicKey = decryptingPublicKey, - decryptingAlgorithm = decryptingAlgorithm, - decryptingKeySize = decryptingKeySize - ))) + + callback( + Result.success( + KeyInfo( + exists = true, + isValid = isValid, + algorithm = algorithm, + keySize = keySize, + isHybridMode = isHybridMode, + publicKey = formattedPublicKey.value, + decryptingPublicKey = decryptingPublicKey, + decryptingAlgorithm = decryptingAlgorithm, + decryptingKeySize = decryptingKeySize + ) + ) + ) } catch (e: Exception) { callback(Result.success(KeyInfo(exists = false))) } @@ -532,11 +662,15 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA ) { val act = activity if (act == null) { - callback(Result.success(SimplePromptResult( - success = false, - error = "Foreground activity required", - code = BiometricError.PROMPT_ERROR - ))) + callback( + Result.success( + SimplePromptResult( + success = false, + error = "Foreground activity required", + code = BiometricError.PROMPT_ERROR + ) + ) + ) return } @@ -551,11 +685,15 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA if (canAuth != BiometricManager.BIOMETRIC_SUCCESS) { val (errorCode, errorMsg) = mapBiometricManagerError(canAuth, biometricStrength) - callback(Result.success(SimplePromptResult( - success = false, - error = errorMsg, - code = errorCode - ))) + callback( + Result.success( + SimplePromptResult( + success = false, + error = errorMsg, + code = errorCode + ) + ) + ) return@launch } @@ -576,45 +714,63 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA val promptInfo = promptInfoBuilder.build() // Show biometric prompt - val result = suspendCancellableCoroutine { cont -> - val callback = object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - if (cont.isActive) cont.resume(result) - } - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { - if (cont.isActive) { - cont.resumeWithException( - SecurityException("$errString", Throwable(errorCode.toString())) - ) + val result = + suspendCancellableCoroutine { cont -> + val callback = object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + if (cont.isActive) cont.resume(result) + } + + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence + ) { + if (cont.isActive) { + cont.resumeWithException( + SecurityException( + "$errString", + Throwable(errorCode.toString()) + ) + ) + } + } + + override fun onAuthenticationFailed() { + // Biometric didn't match, but prompt stays open for retry } } - override fun onAuthenticationFailed() { - // Biometric didn't match, but prompt stays open for retry - } - } - runCatching { - act.setTheme(androidx.appcompat.R.style.Theme_AppCompat_Light_DarkActionBar) - val prompt = BiometricPrompt(act, ContextCompat.getMainExecutor(act), callback) - prompt.authenticate(promptInfo) - }.onFailure { e -> - if (cont.isActive) cont.resumeWithException(e) + runCatching { + act.setTheme(androidx.appcompat.R.style.Theme_AppCompat_Light_DarkActionBar) + val prompt = + BiometricPrompt(act, ContextCompat.getMainExecutor(act), callback) + prompt.authenticate(promptInfo) + }.onFailure { e -> + if (cont.isActive) cont.resumeWithException(e) + } } - } // If we get here, authentication succeeded - callback(Result.success(SimplePromptResult( - success = true, - code = BiometricError.SUCCESS - ))) + callback( + Result.success( + SimplePromptResult( + success = true, + code = BiometricError.SUCCESS + ) + ) + ) } catch (e: Exception) { val errorCode = mapToBiometricError(e) - callback(Result.success(SimplePromptResult( - success = false, - error = e.message, - code = errorCode - ))) + callback( + Result.success( + SimplePromptResult( + success = false, + error = e.message, + code = errorCode + ) + ) + ) } } } @@ -626,18 +782,25 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA return when (canAuthResult) { BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> Pair(BiometricError.NOT_AVAILABLE, "No biometric hardware available") + BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> Pair(BiometricError.NOT_AVAILABLE, "Biometric hardware unavailable") + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { - val strengthName = if (requestedStrength == BiometricStrength.STRONG) "Class 3 (strong)" else "Class 2+ (weak or strong)" + val strengthName = + if (requestedStrength == BiometricStrength.STRONG) "Class 3 (strong)" else "Class 2+ (weak or strong)" Pair(BiometricError.NOT_ENROLLED, "No $strengthName biometrics enrolled.") } + BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> Pair(BiometricError.SECURITY_UPDATE_REQUIRED, "Security update required") + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> Pair(BiometricError.NOT_SUPPORTED, "Biometric authentication not supported") + BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> Pair(BiometricError.UNKNOWN, "Biometric status unknown") + else -> Pair(BiometricError.UNKNOWN, "Unknown biometric error (code: $canAuthResult)") } @@ -776,7 +939,11 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA return entry.certificate.publicKey } - private fun performEciesDecryption(unwrapCipher: Cipher, payload: String, format: PayloadFormat): String { + private fun performEciesDecryption( + unwrapCipher: Cipher, + payload: String, + format: PayloadFormat + ): String { val wrapped = readFileIfExists(EC_WRAPPED_FILENAME) ?: throw IllegalStateException("Encrypted EC key not found") if (wrapped.size < GCM_IV_SIZE + 1) throw IllegalStateException("Malformed wrapped blob") @@ -919,7 +1086,8 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA private fun inferKeyModeFromKeystore(): KeyMode? { val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) } if (!keyStore.containsAlias(BIOMETRIC_KEY_ALIAS)) return null - val entry = keyStore.getEntry(BIOMETRIC_KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry ?: return null + val entry = + keyStore.getEntry(BIOMETRIC_KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry ?: return null val pub = entry.certificate.publicKey return when (pub) { is RSAPublicKey -> KeyMode.RSA @@ -927,6 +1095,7 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA val wrappedExists = File(appContext.filesDir, EC_WRAPPED_FILENAME).exists() if (wrappedExists) KeyMode.HYBRID_EC else KeyMode.EC_SIGN_ONLY } + else -> null } } @@ -955,13 +1124,21 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { if (cont.isActive) cont.resume(result) } + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { if (cont.isActive) { // Map error codes to standard exceptions if needed, or pass raw - cont.resumeWithException(SecurityException("$errString", Throwable(errorCode.toString()))) + cont.resumeWithException( + SecurityException( + "$errString", + Throwable(errorCode.toString()) + ) + ) } } - override fun onAuthenticationFailed() { /* Retry */ } + + override fun onAuthenticationFailed() { /* Retry */ + } } val promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -977,7 +1154,8 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA runCatching { activity.setTheme(androidx.appcompat.R.style.Theme_AppCompat_Light_DarkActionBar) - val prompt = BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), callback) + val prompt = + BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), callback) if (cryptoObject != null) prompt.authenticate(promptInfo, cryptoObject) else prompt.authenticate(promptInfo) }.onFailure { e -> if (cont.isActive) cont.resumeWithException(e) } @@ -999,7 +1177,10 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA } } - private fun configurePerOperationAuth(builder: KeyGenParameterSpec.Builder, useDeviceCredentials: Boolean) { + private fun configurePerOperationAuth( + builder: KeyGenParameterSpec.Builder, + useDeviceCredentials: Boolean + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val authType = if (useDeviceCredentials) { KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL @@ -1012,7 +1193,10 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA } } - private fun configureInvalidation(builder: KeyGenParameterSpec.Builder, invalidateOnEnrollment: Boolean) { + private fun configureInvalidation( + builder: KeyGenParameterSpec.Builder, + invalidateOnEnrollment: Boolean + ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && invalidateOnEnrollment) { builder.setInvalidatedByBiometricEnrollment(true) } @@ -1020,8 +1204,12 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA private fun tryEnableStrongBox(builder: KeyGenParameterSpec.Builder) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && - appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)) { - try { builder.setIsStrongBoxBacked(true) } catch (_: Throwable) {} + appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) + ) { + try { + builder.setIsStrongBoxBacked(true) + } catch (_: Throwable) { + } } } @@ -1041,8 +1229,9 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA if (decryptingKey != null) { decryptingFormatted = formatOutput(decryptingKey.encoded, format) decryptingAlgorithm = decryptingKey.algorithm - decryptingKeySize = ((decryptingKey as? java.security.interfaces.RSAKey)?.modulus?.bitLength() - ?: (decryptingKey as? java.security.interfaces.ECKey)?.params?.order?.bitLength())?.toLong() + decryptingKeySize = + ((decryptingKey as? java.security.interfaces.RSAKey)?.modulus?.bitLength() + ?: (decryptingKey as? java.security.interfaces.ECKey)?.params?.order?.bitLength())?.toLong() } return KeyCreationResult( @@ -1066,8 +1255,12 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA ): SignatureResult { // Format signature explicitly based on SignatureFormat - val sigString = when(format) { - SignatureFormat.BASE64, SignatureFormat.RAW -> Base64.encodeToString(signatureBytes, Base64.NO_WRAP) + val sigString = when (format) { + SignatureFormat.BASE64, SignatureFormat.RAW -> Base64.encodeToString( + signatureBytes, + Base64.NO_WRAP + ) + SignatureFormat.HEX -> bytesToHex(signatureBytes) } @@ -1085,14 +1278,25 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA ) } - private fun formatOutput(bytes: ByteArray, format: KeyFormat, label: String = "PUBLIC KEY"): FormattedOutput = + private fun formatOutput( + bytes: ByteArray, + format: KeyFormat, + label: String = "PUBLIC KEY" + ): FormattedOutput = when (format) { - KeyFormat.BASE64 -> FormattedOutput(Base64.encodeToString(bytes, Base64.NO_WRAP), format) + KeyFormat.BASE64 -> FormattedOutput( + Base64.encodeToString(bytes, Base64.NO_WRAP), + format + ) + KeyFormat.PEM -> FormattedOutput( - "-----BEGIN $label-----\n${Base64.encodeToString(bytes, Base64.NO_WRAP).chunked(64).joinToString("\n")}\n-----END $label-----", + "-----BEGIN $label-----\n${ + Base64.encodeToString(bytes, Base64.NO_WRAP).chunked(64).joinToString("\n") + }\n-----END $label-----", format, label ) + KeyFormat.HEX -> FormattedOutput(bytesToHex(bytes), format) KeyFormat.RAW -> FormattedOutput(Base64.encodeToString(bytes, Base64.NO_WRAP), format) } @@ -1115,62 +1319,133 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA .toByteArray() } - private fun detectBiometricTypes(): Pair, String?> { - var identifiedFingerprint = false + private fun detectBiometricTypes(): List { val pm = appContext.packageManager + val biometricManager = BiometricManager.from(appContext) + val canAuth = biometricManager.canAuthenticate( + BiometricManager.Authenticators.BIOMETRIC_STRONG + ) == BiometricManager.BIOMETRIC_SUCCESS - if (pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { - val fm = appContext.getSystemService(FingerprintManager::class.java) - val enrolled = try { - fm?.hasEnrolledFingerprints() == true - } catch (_: SecurityException) { - true - } - identifiedFingerprint = fm?.isHardwareDetected == true && enrolled - } + if (!canAuth) return emptyList() - val otherString = listOf("face", "iris", ",") - // Use appContext instead of activity!! to avoid NullPointerException - val biometricManager = BiometricManager.from(appContext) + val hasFace = pm.hasSystemFeature(PackageManager.FEATURE_FACE) + val hasFingerprint = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT) + val hasIris = pm.hasSystemFeature(PackageManager.FEATURE_IRIS) + val featureBackedTypes = mutableListOf() + if (hasFace) featureBackedTypes.add(BiometricType.FACE) + if (hasFingerprint) featureBackedTypes.add(BiometricType.FINGERPRINT) + if (hasIris) featureBackedTypes.add(BiometricType.IRIS) - // Use reflection to access getStrings(int authenticators) var buttonLabel: String? = null try { - val getStringsMethod = BiometricManager::class.java.getMethod("getStrings", Int::class.javaPrimitiveType) - val strings = getStringsMethod.invoke(biometricManager, BiometricManager.Authenticators.BIOMETRIC_STRONG) + val getStringsMethod = BiometricManager::class.java.getMethod( + "getStrings", Int::class.javaPrimitiveType + ) + val strings = getStringsMethod.invoke( + biometricManager, BiometricManager.Authenticators.BIOMETRIC_STRONG + ) if (strings != null) { - val getButtonLabelMethod = strings.javaClass.getMethod("getButtonLabel") - buttonLabel = getButtonLabelMethod.invoke(strings) as? String + val getButtonLabel = strings.javaClass.getMethod("getButtonLabel") + val getPromptMessage = try { + strings.javaClass.getMethod("getPromptMessage") + } catch (_: Exception) { + null + } + + buttonLabel = listOfNotNull( + (getButtonLabel.invoke(strings) as? CharSequence)?.toString(), + (getPromptMessage?.invoke(strings) as? CharSequence)?.toString() + ).joinToString(" ") } - } catch (e: Exception) { - // Reflection failed or method not found, ignore + } catch (_: Exception) { } - val otherBiometrics = otherString.filter { - buttonLabel?.contains(it, ignoreCase = true) == true + val systemRes = Resources.getSystem() + + val faceTerms = listOfNotNull( + getFrameworkString(systemRes, "face_icon_content_description"), + getFrameworkString(systemRes, "biometric_face_icon_description"), + getFrameworkString(systemRes, "face_sensor_privacy_title"), + getFrameworkString(systemRes, "face_error_not_recognized"), + getFrameworkString(systemRes, "face_error_lockout"), + getFrameworkString(systemRes, "face_error_lockout_permanent"), + getFrameworkString(systemRes, "face_acquired_too_bright"), + getFrameworkString(systemRes, "face_authenticated"), + getFrameworkString(systemRes, "biometric_dialog_use_face"), + getFrameworkString(systemRes, "face_unlock_recognizing") + ) + + val fingerprintTerms = listOfNotNull( + getFrameworkString(systemRes, "fingerprint_icon_content_description"), + getFrameworkString(systemRes, "biometric_fingerprint_icon_description"), + getFrameworkString(systemRes, "fingerprint_setup_notification_title"), + getFrameworkString(systemRes, "fingerprint_error_not_match"), + getFrameworkString(systemRes, "fingerprint_error_lockout"), + getFrameworkString(systemRes, "fingerprint_error_lockout_permanent"), + getFrameworkString(systemRes, "fingerprint_authenticated"), + getFrameworkString(systemRes, "biometric_dialog_use_fingerprint") + ) + + val irisTerms = listOfNotNull( + getFrameworkString(systemRes, "iris_icon_content_description"), + getFrameworkString(systemRes, "biometric_iris_icon_description"), + getFrameworkString(systemRes, "iris_error_not_recognized"), + getFrameworkString(systemRes, "iris_error_lockout") + ) + +// println("Button label: $buttonLabel") +// println("Face terms: $faceTerms") +// println("Fingerprint terms: $fingerprintTerms") +// println("Iris terms: $irisTerms") + + if (buttonLabel.isNullOrBlank()) return featureBackedTypes + + val labelMatchedTypes = mutableListOf() + if (hasFace && matchesLabel(buttonLabel, faceTerms)) { + labelMatchedTypes.add(BiometricType.FACE) } - val resultString = if (identifiedFingerprint) { - if (otherBiometrics.isEmpty()) "fingerprint" else "biometric" - } else { - if (otherBiometrics.size == 1 && otherBiometrics[0] != ",") otherBiometrics[0] else "biometric" + if (hasFingerprint && matchesLabel(buttonLabel, fingerprintTerms)) { + labelMatchedTypes.add(BiometricType.FINGERPRINT) } - // Map string to List - val types = mutableListOf() - - if (resultString == "fingerprint") { - types.add(BiometricType.FINGERPRINT) - } else if (resultString == "face") { - types.add(BiometricType.FACE) - } else if (resultString == "iris") { - types.add(BiometricType.IRIS) - } else if (resultString == "biometric") { - // Fallback or multiple - types.add(BiometricType.MULTIPLE) + if (hasIris && matchesLabel(buttonLabel, irisTerms)) { + labelMatchedTypes.add(BiometricType.IRIS) } - return Pair(types, null) + return if (labelMatchedTypes.isNotEmpty()) labelMatchedTypes else featureBackedTypes + } + + private fun matchesLabel(buttonLabel: String?, terms: List): Boolean { + if (buttonLabel.isNullOrBlank() || terms.isEmpty()) return false + return terms.any { term -> + // Try full term match first + if (buttonLabel.contains(term, ignoreCase = true)) return@any true + + val hasCJK = term.any { Character.isIdeographic(it.code) } + if (hasCJK) { + // CJK: generate 2-char sliding windows (CJK words are typically 2 chars) + (0 until term.length - 1).any { i -> + val bigram = term.substring(i, i + 2) + bigram.any { Character.isIdeographic(it.code) } && + buttonLabel.contains(bigram) + } + } else { + // Latin/other: split on whitespace + term.split("\\s+".toRegex()) + .filter { it.length >= 3 } + .any { word -> buttonLabel.contains(word, ignoreCase = true) } + } + } + } + + private fun getFrameworkString(res: Resources, name: String): String? { + return try { + val id = res.getIdentifier(name, "string", "android") + if (id != 0) res.getString(id) else null + } catch (_: Exception) { + null + } } private fun mapToBiometricError(e: Throwable): BiometricError { @@ -1197,7 +1472,9 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA // Map simple Cancellation e is CancellationException -> BiometricError.USER_CANCELED - e is IllegalArgumentException && (e.message?.contains("Base64") == true || e.message?.contains("payload") == true) -> BiometricError.INVALID_INPUT + e is IllegalArgumentException && (e.message?.contains("Base64") == true || e.message?.contains( + "payload" + ) == true) -> BiometricError.INVALID_INPUT else -> BiometricError.UNKNOWN } @@ -1206,6 +1483,7 @@ class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityA private fun writeFileAtomic(fileName: String, data: ByteArray) { File(appContext.filesDir, fileName).outputStream().use { it.write(data) } } + private fun readFileIfExists(fileName: String): ByteArray? { val file = File(appContext.filesDir, fileName) return if (!file.exists()) null else file.readBytes() diff --git a/banking_app/ios/Podfile.lock b/banking_app/ios/Podfile.lock index 48730e9..98f7404 100644 --- a/banking_app/ios/Podfile.lock +++ b/banking_app/ios/Podfile.lock @@ -1,28 +1,15 @@ PODS: - - biometric_signature (9.0.1): - - Flutter - Flutter (1.0.0) - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS DEPENDENCIES: - - biometric_signature (from `.symlinks/plugins/biometric_signature/ios`) - Flutter (from `Flutter`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) EXTERNAL SOURCES: - biometric_signature: - :path: ".symlinks/plugins/biometric_signature/ios" Flutter: :path: Flutter - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: - biometric_signature: da57d4ae319902aa96e8193271f7b38ab26db278 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 PODFILE CHECKSUM: 945703bf1a4050fb8e8f15e616b94692e130196f diff --git a/banking_app/ios/Runner.xcodeproj/project.pbxproj b/banking_app/ios/Runner.xcodeproj/project.pbxproj index e331466..3ae40e6 100644 --- a/banking_app/ios/Runner.xcodeproj/project.pbxproj +++ b/banking_app/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -12,6 +12,7 @@ 35FF81AE916B4580A3500461 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC2A67A1B09AD50378D9F7D7 /* Pods_RunnerTests.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -53,6 +54,7 @@ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7488905320430CF8D3F93751 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7690CFADC5ECD78F60F24408 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7B2EE3B8D213F6B6D5E872DF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -72,6 +74,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 9C7D2CAEF20FBB90AC47EB6A /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -120,6 +123,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -197,13 +201,15 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 76B45290C1988DFA1A826088 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -237,6 +243,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -307,27 +316,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 76B45290C1988DFA1A826088 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -729,6 +717,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/banking_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/banking_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d4..c3fedb2 100644 --- a/banking_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/banking_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - FDF882969BAD891C28C55716 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -572,8 +563,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -704,8 +697,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -724,8 +719,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -796,6 +793,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/banking_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/banking_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 6def3ea..eaa2e9d 100644 --- a/banking_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/banking_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - B4C96DDFC80D90E4D92B4004 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; C0130D34B480E6F04AD4C514 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -801,6 +793,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 80e26ac..4d6fe16 100644 --- a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + '../LICENSE' } - s.author = { 'Vision Flutter' => 'chamodananayakkara@gmail.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' + s.homepage = 'https://github.com/chamodanethra/biometric_signature' + s.license = { :type => 'MIT', :file => '../LICENSE' } + s.author = { 'Chamoda Nethra' => 'chamodananayakkara@gmail.com' } + s.source = { :git => 'https://github.com/chamodanethra/biometric_signature.git', :tag => s.version.to_s } + s.source_files = 'biometric_signature/Sources/biometric_signature/**/*.swift' s.dependency 'Flutter' s.platform = :ios, '13.0' @@ -25,5 +25,5 @@ A new Flutter plugin project. # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your # plugin's privacy impact, and then uncomment this line. For more information, # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files - # s.resource_bundles = {'flutter_plugin_privacy' => ['Resources/PrivacyInfo.xcprivacy']} + s.resource_bundles = {'flutter_plugin_privacy' => ['biometric_signature/Sources/biometric_signature/PrivacyInfo.xcprivacy']} end diff --git a/ios/biometric_signature/Package.swift b/ios/biometric_signature/Package.swift new file mode 100644 index 0000000..c63c3b7 --- /dev/null +++ b/ios/biometric_signature/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "biometric_signature", + platforms: [ + .iOS("13.0") + ], + products: [ + .library(name: "biometric-signature", targets: ["biometric_signature"]) + ], + dependencies: [], + targets: [ + .target( + name: "biometric_signature", + dependencies: [], + resources: [ + .process("PrivacyInfo.xcprivacy"), + + // If you have other resources that need to be bundled with your plugin, refer to + // the following instructions to add them: + // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package + ] + ) + ] +) diff --git a/ios/Classes/BiometricSignatureApi.swift b/ios/biometric_signature/Sources/biometric_signature/BiometricSignatureApi.swift similarity index 100% rename from ios/Classes/BiometricSignatureApi.swift rename to ios/biometric_signature/Sources/biometric_signature/BiometricSignatureApi.swift diff --git a/ios/Classes/BiometricSignaturePlugin.swift b/ios/biometric_signature/Sources/biometric_signature/BiometricSignaturePlugin.swift similarity index 100% rename from ios/Classes/BiometricSignaturePlugin.swift rename to ios/biometric_signature/Sources/biometric_signature/BiometricSignaturePlugin.swift diff --git a/ios/Resources/PrivacyInfo.xcprivacy b/ios/biometric_signature/Sources/biometric_signature/PrivacyInfo.xcprivacy similarity index 100% rename from ios/Resources/PrivacyInfo.xcprivacy rename to ios/biometric_signature/Sources/biometric_signature/PrivacyInfo.xcprivacy diff --git a/macos/biometric_signature.podspec b/macos/biometric_signature.podspec index 0ddf9aa..e54de17 100644 --- a/macos/biometric_signature.podspec +++ b/macos/biometric_signature.podspec @@ -4,23 +4,23 @@ # Pod::Spec.new do |s| s.name = 'biometric_signature' - s.version = '10.0.0' - s.summary = 'A new Flutter plugin project.' + s.version = '10.1.0' + s.summary = 'Hardware-backed biometric signatures for Flutter.' s.description = <<-DESC -A new Flutter plugin project. +Create cryptographic signatures using Secure Enclave, StrongBox, and Windows Hello. DESC - s.homepage = 'https://visionflutter.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Vision Flutter' => 'chamodananayakkara@gmail.com' } + s.homepage = 'https://github.com/chamodanethra/biometric_signature' + s.license = { :type => 'MIT', :file => '../LICENSE' } + s.author = { 'Chamoda Nethra' => 'chamodananayakkara@gmail.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' + s.source = { :git => 'https://github.com/chamodanethra/biometric_signature.git', :tag => s.version.to_s } + s.source_files = 'biometric_signature/Sources/biometric_signature/**/*.swift' # If your plugin requires a privacy manifest, for example if it collects user # data, update the PrivacyInfo.xcprivacy file to describe your plugin's # privacy impact, and then uncomment this line. For more information, # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files - # s.resource_bundles = {'biometric_signature_privacy' => ['Resources/PrivacyInfo.xcprivacy']} + s.resource_bundles = {'biometric_signature_privacy' => ['biometric_signature/Sources/biometric_signature/PrivacyInfo.xcprivacy']} s.dependency 'FlutterMacOS' diff --git a/macos/biometric_signature/Package.swift b/macos/biometric_signature/Package.swift new file mode 100644 index 0000000..7382737 --- /dev/null +++ b/macos/biometric_signature/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "biometric_signature", + platforms: [ + .macOS("10.15") + ], + products: [ + .library(name: "biometric-signature", targets: ["biometric_signature"]) + ], + dependencies: [], + targets: [ + .target( + name: "biometric_signature", + dependencies: [], + resources: [ + .process("PrivacyInfo.xcprivacy"), + + // If you have other resources that need to be bundled with your plugin, refer to + // the following instructions to add them: + // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package + ] + ) + ] +) diff --git a/macos/Classes/BiometricSignatureApi.swift b/macos/biometric_signature/Sources/biometric_signature/BiometricSignatureApi.swift similarity index 100% rename from macos/Classes/BiometricSignatureApi.swift rename to macos/biometric_signature/Sources/biometric_signature/BiometricSignatureApi.swift diff --git a/macos/Classes/BiometricSignaturePlugin.swift b/macos/biometric_signature/Sources/biometric_signature/BiometricSignaturePlugin.swift similarity index 100% rename from macos/Classes/BiometricSignaturePlugin.swift rename to macos/biometric_signature/Sources/biometric_signature/BiometricSignaturePlugin.swift diff --git a/macos/Resources/PrivacyInfo.xcprivacy b/macos/biometric_signature/Sources/biometric_signature/PrivacyInfo.xcprivacy similarity index 100% rename from macos/Resources/PrivacyInfo.xcprivacy rename to macos/biometric_signature/Sources/biometric_signature/PrivacyInfo.xcprivacy diff --git a/passwordless_login/ios/Podfile.lock b/passwordless_login/ios/Podfile.lock index bc58c68..64cf43c 100644 --- a/passwordless_login/ios/Podfile.lock +++ b/passwordless_login/ios/Podfile.lock @@ -1,28 +1,15 @@ PODS: - - biometric_signature (9.0.1): - - Flutter - Flutter (1.0.0) - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS DEPENDENCIES: - - biometric_signature (from `.symlinks/plugins/biometric_signature/ios`) - Flutter (from `Flutter`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) EXTERNAL SOURCES: - biometric_signature: - :path: ".symlinks/plugins/biometric_signature/ios" Flutter: :path: Flutter - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: - biometric_signature: da57d4ae319902aa96e8193271f7b38ab26db278 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/passwordless_login/ios/Runner.xcodeproj/project.pbxproj b/passwordless_login/ios/Runner.xcodeproj/project.pbxproj index 4ea7e3e..7fc0299 100644 --- a/passwordless_login/ios/Runner.xcodeproj/project.pbxproj +++ b/passwordless_login/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -12,6 +12,7 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -51,6 +52,7 @@ 574F6899E0981C30CE62AAB7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7A1A0EB80F7ECABEA3377817 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 835CC05218E68D47A805FA6C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; @@ -80,6 +82,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, AB1A9ADA1EEB45C2C7183796 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -107,6 +110,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -197,13 +201,15 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - B5E2643A7B888D27EB4C7719 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -237,6 +243,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -344,27 +353,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - B5E2643A7B888D27EB4C7719 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -729,6 +717,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/passwordless_login/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/passwordless_login/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e3773d4..c3fedb2 100644 --- a/passwordless_login/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/passwordless_login/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + + + + + + + + + + +