diff --git a/CHANGELOG.md b/CHANGELOG.md index 9823167..e6faf2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +## [9.0.0] - 2025-12-18 + +* **Breaking**: Method signature changes: + - `createKeys()` now takes `config`, `keyFormat`, `promptMessage` parameters + - `createSignature()` now takes `payload`, `config`, `signatureFormat`, `keyFormat`, `promptMessage` parameters + - `decrypt()` now takes `payload`, `payloadFormat`, `config`, `promptMessage` parameters + +* Moved cross-platform parameters into unified config objects: + - `signatureType`, `enforceBiometric`, `setInvalidatedByBiometricEnrollment`, `useDeviceCredentials` now in `CreateKeysConfig` + - Each field is documented with which platform(s) it applies to + +### Architecture - Type-safe Communication with Pigeon +* **Breaking**: Migrated entire platform communication layer to [Pigeon](https://pub.dev/packages/pigeon). +* **Breaking**: Replaced raw string/map returns with structured strongly-typed objects: + - `KeyCreationResult`: Contains `publicKey`, `error`, and `code`. + - `SignatureResult`: Contains `signature`, `publicKey`, `error`, and `code`. + - `DecryptResult`: Contains `decryptedData`, `error`, and `code`. + - `BiometricAvailability`: detailed availability status including enrolled biometric types and error reasons. +* **Breaking**: Standardized `BiometricError` enum across all platforms. + +### API Improvements +* **Breaking**: `biometricAuthAvailable()` now returns a `BiometricAvailability` object instead of a raw string. +* Removed legacy `signature_options.dart`, `decryption_options.dart` and old config classes. +* Enhanced error handling with specific error codes (e.g., `userCanceled`, `notEnrolled`, `lockedOut`) instead of generic strings. +* **New `getKeyInfo()` method**: Retrieve detailed information about existing biometric keys without creating a signature. + - Returns `KeyInfo` object with: `exists`, `isValid`, `algorithm`, `keySize`, `isHybridMode`, `publicKey`, `decryptingPublicKey`. + - Accepts `checkValidity` parameter to verify key hasn't been invalidated by biometric changes. + - Accepts `keyFormat` parameter to specify output format (base64, pem, hex). +* **New `KeyInfo` class**: Exported via Pigeon for type-safe key metadata. +* `biometricKeyExists()` is now a convenience wrapper around `getKeyInfo()`. + +### Improved +* Cleaner, simpler API with fewer method parameters +* Better documentation of platform-specific options +* Updated all example projects to use new API + ## [8.5.0] - 2025-12-09 ### Added - macOS Platform Support diff --git a/EXAMPLES.md b/EXAMPLES.md index 5ee9daa..5df5cd0 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -168,53 +168,112 @@ All examples demonstrate: ### Initialize Biometric Service ```dart -import 'package:biometric_signature/key_material.dart'; +import 'package:biometric_signature/biometric_signature.dart'; final biometric = BiometricSignature(); // Check availability -final available = await biometric.biometricAuthAvailable(); +final availability = await biometric.biometricAuthAvailable(); +if (availability.canAuthenticate ?? false) { + print('Biometrics available: ${availability.availableBiometrics}'); +} -// Create keys +// Create keys (RSA by default) final keyResult = await biometric.createKeys( keyFormat: KeyFormat.pem, - androidConfig: AndroidConfig( - useDeviceCredentials: false, - signatureType: AndroidSignatureType.RSA, - ), - iosConfig: IosConfig( - useDeviceCredentials: false, - signatureType: IOSSignatureType.RSA, + promptMessage: 'Authenticate to create keys', + config: CreateKeysConfig( + signatureType: SignatureType.rsa, + useDeviceCredentials: true, + setInvalidatedByBiometricEnrollment: true, + enforceBiometric: true, + enableDecryption: false, // Android only ), ); -final pemPublicKey = keyResult?.publicKey.asString(); +if (keyResult.code == BiometricError.success) { + print('Public Key: ${keyResult.publicKey}'); +} +``` + +### Get Key Info +```dart +// Check key existence with metadata +final info = await biometric.getKeyInfo( + checkValidity: true, + keyFormat: KeyFormat.pem, +); + +if (info.exists && (info.isValid ?? true)) { + print('Algorithm: ${info.algorithm}, Size: ${info.keySize} bits'); + print('Hybrid Mode: ${info.isHybridMode}'); + print('Public Key: ${info.publicKey}'); +} ``` ### Sign Data ```dart -final signatureResult = await biometric.createSignature( - SignatureOptions( - payload: 'data_to_sign', - promptMessage: 'Authenticate to continue', - keyFormat: KeyFormat.raw, +final result = await biometric.createSignature( + payload: 'data_to_sign', + promptMessage: 'Authenticate to sign', + signatureFormat: SignatureFormat.base64, + keyFormat: KeyFormat.pem, + config: CreateSignatureConfig( + allowDeviceCredentials: true, ), ); -final signatureBase64 = signatureResult?.signature.toBase64(); +if (result.code == BiometricError.success) { + print('Signature: ${result.signature}'); +} +``` + +### Decrypt Data +```dart +final decryptResult = await biometric.decrypt( + payload: encryptedBase64, + payloadFormat: PayloadFormat.base64, + promptMessage: 'Authenticate to decrypt', + config: DecryptConfig( + allowDeviceCredentials: false, + ), +); + +if (decryptResult.code == BiometricError.success) { + print('Decrypted: ${decryptResult.decryptedData}'); +} ``` ### Error Handling ```dart -try { - final signatureResult = await biometric.createSignature(options); - final signatureHex = signatureResult?.signature.toHex(); -} on PlatformException catch (e) { - if (e.code == 'AUTH_FAILED') { - // Handle authentication failure - } else if (e.code == 'CANCELLED') { - // Handle user cancellation - } +final result = await biometric.createSignature( + payload: 'data_to_sign', + promptMessage: 'Authenticate', +); + +switch (result.code) { + case BiometricError.success: + print('Signed: ${result.signature}'); + break; + case BiometricError.userCanceled: + print('User cancelled authentication'); + break; + case BiometricError.keyInvalidated: + print('Key invalidated - re-enrollment required'); + break; + case BiometricError.lockedOut: + print('Too many attempts - locked out'); + break; + default: + print('Error: ${result.code} - ${result.error}'); +} +``` + +### Delete Keys +```dart +final deleted = await biometric.deleteKeys(); +if (deleted) { + print('All biometric keys removed'); } ``` @@ -232,7 +291,7 @@ Found an issue or want to improve an example? Contributions are welcome! 1. Fork the repository 2. Create your feature branch -3. Test your changes on both platforms +3. Test your changes on all platforms 4. Submit a pull request ## 📄 License diff --git a/README.md b/README.md index cb4688b..c71be04 100644 --- a/README.md +++ b/README.md @@ -9,51 +9,61 @@ Even if an attacker bypasses or hooks biometric APIs, your backend will still re ## Features -- **Cryptographic Proof Of Identity:** Hardware-backed RSA or ECDSA signatures that your backend can independently verify. +- **Cryptographic Proof Of Identity:** Hardware-backed RSA (Android) or ECDSA (all platforms) signatures that your backend can independently verify. - **Decryption Support:** - - **RSA**: RSA/ECB/PKCS1Padding (Android + iOS) - - **EC**: ECIES (X9.63 → SHA-256 → AES-GCM) -- **Hardware Security:** Uses Secure Enclave (iOS) and Keystore/StrongBox (Android). + - **RSA**: RSA/ECB/PKCS1Padding (Android native, iOS/macOS via wrapped software key) + - **EC**: ECIES (`eciesEncryptionStandardX963SHA256AESGCM`) +- **Hardware Security:** Uses Secure Enclave (iOS/macOS) and Keystore/StrongBox (Android). - **Hybrid Architectures:** -- **Android Hybrid EC:** - - Hardware EC signing + software ECIES decryption. - The software EC private key is AES-wrapped using a StrongBox/Keystore AES-256 master key that requires biometric authentication for every unwrap. -- **iOS Hybrid RSA:** - - Hardware EC signing + software RSA decryption key. - The RSA key is encrypted using ECIES with Secure Enclave EC public key material. + - **Android Hybrid EC:** Hardware EC signing + software ECIES decryption. Software EC private key is AES-wrapped using a Keystore/StrongBox AES-256 master key that requires biometric authentication for every unwrap. + - **iOS/macOS Hybrid RSA:** Software RSA key for **both signing and decryption**, wrapped using ECIES with Secure Enclave EC public key. Hardware EC is only used for wrapping/unwrapping. - **Key Invalidation:** Keys can be bound to biometric enrollment state (fingerprint/Face ID changes). - **Device Credentials:** Optional PIN/Pattern/Password fallback on Android. + ## Security Architecture ### Key Modes -The plugin supports three secure operational modes: +The plugin supports different operational modes depending on the platform: + +#### Android -1. **RSA Mode**: - - RSA-2048 signing (always hardware-backed) - - Optional RSA decryption +Android supports three key modes: + +1. **RSA Mode** (`SignatureType.rsa`): + - Hardware-backed RSA-2048 signing (Keystore/StrongBox) + - Optional RSA decryption (PKCS#1 padding) - Private key never leaves secure hardware -2. **EC Signing-Only**: - - Hardware-backed P-256 key + +2. **EC Signing-Only** (`SignatureType.ecdsa`, `enableDecryption: false`): + - Hardware-backed P-256 key in Keystore/StrongBox - ECDSA signing only - No decryption support -3. **Hybrid EC Mode**: Combines hardware signing with software decryption keys: - - **Android**: - - Hardware EC key for signing - - Software EC key for ECIES decryption - - Software EC private key encrypted using: - - AES-256 GCM master key stored in Keystore/StrongBox - - Per-operation biometric authentication required - - Wrapped EC private key blob stored in app-private files (MODE_PRIVATE) - - Public EC key also stored in app-private files - - **iOS**: - - Hardware EC key for signing - - Software RSA key for PKCS#1 decryption - - RSA private key is wrapped using ECIES with Secure Enclave EC public key - - Wrapped RSA key stored in Keychain as `kSecClassGenericPassword` + +3. **Hybrid EC Mode** (`SignatureType.ecdsa`, `enableDecryption: true`): + - Hardware EC key for signing + - Software EC key for ECIES decryption + - Software EC private key encrypted using AES-256 GCM master key (Keystore/StrongBox) + - Per-operation biometric authentication required for decryption + +#### iOS / macOS + +Apple platforms support two key modes (Secure Enclave only supports EC keys natively): + +1. **EC Mode** (`SignatureType.ecdsa`): + - Hardware-backed P-256 key in Secure Enclave + - ECDSA signing + - Native ECIES decryption (`eciesEncryptionStandardX963SHA256AESGCM`) + - Single key for both operations + +2. **RSA Mode** (`SignatureType.rsa`) - Hybrid Architecture: + - Software RSA-2048 key for **both signing and decryption** + - RSA private key wrapped using ECIES with Secure Enclave EC public key + - Hardware EC key is **only** used for wrapping/unwrapping the RSA key + - Wrapped RSA key stored in Keychain as `kSecClassGenericPassword` + - Per-operation biometric authentication required to unwrap RSA key + ### Workflow Overview @@ -154,12 +164,12 @@ To get started with Biometric Signature, follow these steps: ```yaml dependencies: - biometric_signature: ^8.5.0 + biometric_signature: ^9.0.0 ``` -| | Android | iOS | macOS | -|-------------|---------|-------|----------| -| **Support** | SDK 24+ | 13.0+ | 10.15+ | +| | Android | iOS | macOS | Windows | +|-------------|---------|-------|--------|--------| +| **Support** | SDK 24+ | 13.0+ | 10.15+ | 10+ | ### iOS Integration @@ -180,9 +190,14 @@ to your Info.plist file. #### Activity Changes -This plugin requires the use of a FragmentActivity as opposed to Activity. This can be easily done -by switching to use FlutterFragmentActivity as opposed to FlutterActivity in your manifest or your -own Activity class if you are extending the base class. +This plugin requires the use of a `FragmentActivity` instead of `Activity`. Update your `MainActivity.kt` to extend `FlutterFragmentActivity`: + +```kotlin +import io.flutter.embedding.android.FlutterFragmentActivity + +class MainActivity : FlutterFragmentActivity() { +} +``` #### Permissions @@ -223,19 +238,27 @@ Replace `com.yourdomain.yourapp` with your actual bundle identifier. platform :osx, '10.15' ``` +### Windows Integration + +This plugin uses **Windows Hello** for biometric authentication on Windows 10 and later. + +**Platform Limitations:** +- Windows only supports **RSA keys** (ECDSA is ignored) +- Windows Hello **always authenticates** during key creation (`enforceBiometric` is effectively always `true`) +- `setInvalidatedByBiometricEnrollment` and `useDeviceCredentials` are ignored +- **Decryption is not supported** on Windows + +No additional configuration is required. The plugin will automatically use Windows Hello when available. -2. Import the package in your Dart code: +### Common Setup + +1. Import the package in your Dart code: ```dart import 'package:biometric_signature/biometric_signature.dart'; -import 'package:biometric_signature/android_config.dart'; -import 'package:biometric_signature/ios_config.dart'; -import 'package:biometric_signature/macos_config.dart'; -import 'package:biometric_signature/signature_options.dart'; -import 'package:biometric_signature/decryption_options.dart'; ``` -3. Initialize the Biometric Signature instance: +2. Initialize the Biometric Signature instance: ```dart @@ -254,287 +277,200 @@ When a user enrolls in biometrics, a key pair is generated. The private key is s This class provides methods to manage and utilize biometric authentication for secure server interactions. It supports both Android and iOS platforms. -### `createKeys({ androidConfig, iosConfig, macosConfig, keyFormat, enforceBiometric, promptMessage })` +### `createKeys({ config, keyFormat, promptMessage })` -Generates a new key pair (RSA 2048 or EC) for biometric authentication. The private key is securely stored on the device, while the `KeyCreationResult` returned from this call contains a `FormattedValue` with the public key in the requested representation. StrongBox support is available for compatible Android devices and Secure Enclave support is available for iOS. -Hybrid modes generate both hardware and software keys, encrypting software keys via secure hardware. +Generates a new key pair (RSA 2048 or EC) for biometric authentication. The private key is securely stored on the device. - **Parameters**: - -`androidConfig`: An `AndroidConfig` object containing following properties: - - `useDeviceCredentials`: A `bool` to indicate whether Device Credentials' fallback support is needed for the compatible Android devices. - - `signatureType`: An enum value of `AndroidSignatureType`. - - `setInvalidatedByBiometricEnrollment` *(optional)*: A `bool` to indicate whether the key should be invalidated when a new biometric is enrolled. Defaults to `true`. When set to `true`, adding a new fingerprint, face, or iris will invalidate the existing key, requiring re-enrollment. This enhances security by ensuring keys are tied to the specific biometric set at creation time. - - `enableDecryption` *(optional)*: A `bool` to indicate whether the generated key should support decryption (RSA only). Defaults to `false`. - - `iosConfig`: An `IosConfig` object containing following properties: - - `useDeviceCredentials`: A `bool` to indicate whether Device Credentials' fallback support is needed. - - `signatureType`: An enum value of `IOSSignatureType`. - - `biometryCurrentSet` *(optional)*: A `bool` to constrain key usage to the current biometric enrollment. Defaults to `true`. When set to `true`, the key is bound to the current set of enrolled biometrics. If biometrics are changed (e.g., a new fingerprint is added or removed), the key becomes invalid, requiring re-enrollment. - - `macosConfig`: A `MacosConfig` object containing following properties: - - `useDeviceCredentials`: A `bool` to indicate whether Device Credentials' fallback support is needed. - - `signatureType`: An enum value of `MacosSignatureType`. - - `biometryCurrentSet` *(optional)*: A `bool` to constrain key usage to the current biometric enrollment. Defaults to `true`. When set to `true`, the key is bound to the current set of enrolled biometrics (Touch ID). If biometrics are changed, the key becomes invalid, requiring re-enrollment. - - `keyFormat` *(optional)*: A `KeyFormat` value describing how the public key should be returned. Defaults to `KeyFormat.base64` for backward compatibility. - - `enforceBiometric` *(optional)*: A `bool` to require biometric authentication before generating the key-pair. Defaults to `false`. When set to `true`, the user will be prompted for biometric authentication (fingerprint, face, or iris) before the key-pair is generated. This ensures that the person holding the device is verified before keys are created, adding an extra layer of security for sensitive use cases. - - `promptMessage` *(optional)*: A `String` to customize the authentication prompt message when `enforceBiometric` is `true`. Defaults to `"Authenticate to create keys"`. This allows you to provide context-specific instructions to the user during key generation. - -- **Returns**: `Future`. Access the formatted public key through `result.publicKey`, e.g.: + - `config`: `CreateKeysConfig` with platform options (see below) + - `keyFormat`: Output format (`KeyFormat.base64`, `pem`, `hex`) + - `promptMessage`: Custom authentication prompt message + +- **Returns**: `Future`. + - `publicKey`: The formatted public key string (Base64 or PEM). + - `code`: `BiometricError` code (e.g., `success`, `userCanceled`). + - `error`: Descriptive error message. + +#### CreateKeysConfig Options + +| Option | Platforms | Description | +|--------|-----------|-------------| +| `signatureType` | Android/iOS/macOS | `SignatureType.rsa` or `SignatureType.ecdsa` | +| `enforceBiometric` | Android/iOS/macOS | Require biometric during key creation | +| `setInvalidatedByBiometricEnrollment` | Android/iOS/macOS | Invalidate key on biometric changes | +| `useDeviceCredentials` | Android/iOS/macOS | Allow PIN/passcode fallback | +| `enableDecryption` | Android | Enable decryption capability | +| `promptSubtitle` | Android | Subtitle for biometric prompt | +| `promptDescription` | Android | Description for biometric prompt | +| `cancelButtonText` | Android | Cancel button text | ```dart -final keyResult = await biometricSignature.createKeys(keyFormat: KeyFormat.pem); -final pem = keyResult?.publicKey.asString(); -final derBytes = keyResult?.publicKey.toBytes(); -``` +final result = await biometricSignature.createKeys( + keyFormat: KeyFormat.pem, + promptMessage: 'Authenticate to create keys', + config: CreateKeysConfig( + signatureType: SignatureType.rsa, + enforceBiometric: true, + setInvalidatedByBiometricEnrollment: true, + useDeviceCredentials: false, + enableDecryption: true, // Android only + ), +); -- **Error Codes**: - - `AUTH_FAILED`: Error generating keys. +if (result.code == BiometricError.success) { + print('Public Key: ${result.publicKey}'); +} +``` -### `createSignature(SignatureOptions options)` +### `createSignature({ payload, config, signatureFormat, keyFormat, promptMessage })` -Prompts the user for biometric authentication and generates a cryptographic signature (RSA PKCS#1v1.5 SHA-256 or ECDSA P-256) using the securely stored private key. The new response is a `SignatureResult` that carries both the signature and public key in the requested output format. -Hybrid modes always sign using the hardware EC key. +Prompts the user for biometric authentication and generates a cryptographic signature. - **Parameters**: - - `options`: A `SignatureOptions` instance that specifies: - - `payload` (required): The UTF-8 payload to sign. - - `promptMessage` (optional): Message displayed in the biometric prompt. Default to `Authenticate`. - - `androidOptions` (optional): An `AndroidSignatureOptions` object offering: - - `cancelButtonText`: Overrides the cancel button label. Defaults to `Cancel`. - - `allowDeviceCredentials`: Enables device-credential fallback on compatible Android devices. - - `subtitle`: Optional secondary text displayed under the prompt title on Android. - - `iosOptions` (optional): An `IosSignatureOptions` object offering: - - `shouldMigrate`: Triggers migration from pre-5.x Keychain storage to Secure Enclave. - - `keyFormat` *(optional)*: Preferred output format (`KeyFormat.base64` by default). This is a new parameter. - -- **Returns**: `Future`. Use the `FormattedValue` helpers to obtain the representation you need: + - `payload`: The data to sign + - `config`: `CreateSignatureConfig` with platform options + - `signatureFormat`: Output format for signature + - `keyFormat`: Output format for public key + - `promptMessage`: Custom authentication prompt + +#### CreateSignatureConfig Options + +| Option | Platforms | Description | +|--------|-----------|-------------| +| `allowDeviceCredentials` | Android | Allow PIN/pattern fallback | +| `promptSubtitle` | Android | Subtitle for biometric prompt | +| `promptDescription` | Android | Description for biometric prompt | +| `cancelButtonText` | Android | Cancel button text | +| `shouldMigrate` | iOS | Migrate from legacy keychain storage | + +- **Returns**: `Future`. + - `signature`: The signed payload. + - `publicKey`: The public key. + - `code`: `BiometricError` code. ```dart -final signatureResult = await biometricSignature.createSignature( - SignatureOptions( - payload: 'Payload to sign', - keyFormat: KeyFormat.raw, - promptMessage: 'Authenticate to Sign', - androidOptions: const AndroidSignatureOptions(allowDeviceCredentials: false), - iosOptions: const IosSignatureOptions(shouldMigrate: false), - ), +final result = await biometricSignature.createSignature( + payload: 'Data to sign', + promptMessage: 'Please authenticate', + signatureFormat: SignatureFormat.base64, + keyFormat: KeyFormat.base64, + config: CreateSignatureConfig( + allowDeviceCredentials: false, + ), ); - -final Uint8List rawSignature = signatureResult!.signature.toBytes(); -final String base64Signature = signatureResult.signature.toBase64(); ``` -- **Error Codes**: - - `INVALID_PAYLOAD`: Payload is required and must be valid UTF-8. - - `AUTH_FAILED`: Error generating the signature. +### `decrypt({ payload, payloadFormat, config, promptMessage })` -#### Supported output formats +Decrypts the given payload using the private key and biometrics. -`KeyFormat` lets you decide how both the public key and signature are returned: +- **Parameters**: + - `payload`: The encrypted data + - `payloadFormat`: Format of encrypted data (`PayloadFormat.base64`, `hex`) + - `config`: `DecryptConfig` with platform options + - `promptMessage`: Custom authentication prompt -- `KeyFormat.base64` — URL/transport friendly string (default). -- `KeyFormat.pem` — PEM block with headers, using SubjectPublicKeyInfo on both platforms. -- `KeyFormat.raw` — `Uint8List` (DER bytes for public keys, raw signature bytes). -- `KeyFormat.hex` — Lowercase hexadecimal string. +#### DecryptConfig Options -Each `FormattedValue` exposes helpers such as `toBase64()`, `toBytes()`, `toHex()` and `asString()` so you can easily convert between representations. +| Option | Platforms | Description | +|--------|-----------|-------------| +| `allowDeviceCredentials` | Android | Allow PIN/pattern fallback | +| `promptSubtitle` | Android | Subtitle for biometric prompt | +| `promptDescription` | Android | Description for biometric prompt | +| `cancelButtonText` | Android | Cancel button text | +| `shouldMigrate` | iOS | Migrate from legacy keychain storage | -### `decrypt(DecryptionOptions options)` +> **Note**: Decryption is not supported on Windows. -Decrypts the given payload using the private key and biometrics. Supports both **RSA** (PKCS#1) and **EC** (ECIES with P-256 → ECDH → X9.63 KDF (SHA-256) → AES-128-GCM) decryption. - -- **Parameters**: - - `options`: A `DecryptionOptions` instance that specifies: - - `payload` (required): The Base64 encoded encrypted payload to decrypt. - - `promptMessage` (optional): Message displayed in the biometric prompt. Defaults to `Authenticate`. - - `androidOptions` (optional): An `AndroidDecryptionOptions` object offering: - - `cancelButtonText`: Overrides the cancel button label. Defaults to `Cancel`. - - `allowDeviceCredentials`: Enables device-credential fallback on compatible Android devices. - - `subtitle`: Optional secondary text displayed under the prompt title on Android. - - `iosOptions` (optional): An `IosDecryptionOptions` object offering: - - `shouldMigrate`: Triggers migration from pre-5.x Keychain storage to Secure Enclave. - -- **Returns**: `Future`. The `DecryptResult` contains the `decryptedData` string. - -- **Supported Algorithms**: - - **RSA**: Uses RSA/ECB/PKCS1Padding (Android & iOS Hybrid mode) - - **EC**: Uses ECIES (Elliptic Curve Integrated Encryption Scheme) with: - - Curve: P-256 (secp256r1) - - Key Agreement: ECDH - - KDF: ANSI X9.63 with SHA-256 - - Encryption: AES-128-GCM (12-byte IV, 128-bit auth tag) - -- **Native Architecture Summary**: - - **Android**: - - Hardware EC/RSA keys for signing (Keystore/StrongBox) - - Software EC key for ECIES decryption - - Wrapped EC private key stored in app-private files - - Unwrapped at runtime using biometric-protected AES-256 master key - - Manual ECIES implementation: ECDH → X9.63 KDF → AES-GCM - - All sensitive material zeroized immediately after use - - **iOS**: - - Secure Enclave EC key for signing - - Native ECIES using `SecKeyAlgorithm.eciesEncryptionStandardX963SHA256AESGCM` - - Hybrid RSA mode: software RSA key wrapped via ECIES, stored in Keychain +- **Returns**: `Future`. + - `decryptedData`: The plaintext string. + - `code`: `BiometricError` code. ```dart -// RSA Decryption Example -final decryptResult = await biometricSignature.decrypt( - DecryptionOptions( - payload: 'Base64 Encrypted RSA Payload', - promptMessage: 'Authenticate to Decrypt', - androidOptions: const AndroidDecryptionOptions(allowDeviceCredentials: false), - iosOptions: const IosDecryptionOptions(shouldMigrate: false), - ), -); - -// EC Decryption Example (ECIES) -final ecDecryptResult = await biometricSignature.decrypt( - DecryptionOptions( - payload: 'Base64 Encrypted ECIES Payload', - promptMessage: 'Authenticate to Decrypt', - androidOptions: const AndroidDecryptionOptions(allowDeviceCredentials: false), - iosOptions: const IosDecryptionOptions(shouldMigrate: false), - ), +final result = await biometricSignature.decrypt( + payload: encryptedBase64, + payloadFormat: PayloadFormat.base64, + promptMessage: 'Authenticate to decrypt', + config: DecryptConfig( + allowDeviceCredentials: false, + ), ); - -final decryptedString = decryptResult?.decryptedData; ``` -- **Error Codes**: - - `INVALID_PAYLOAD`: Payload is required or invalid format. - - `AUTH_FAILED`: Error decrypting the payload or authentication failed. - ### `deleteKeys()` -Deletes all key material (hardware + hybrid). -Hybrid wrapped keys stored in Keystore are also removed. - -- **Returns**: `bool` - `true` if the key(s) was successfully deleted. - -- **Error Codes**: - -- `AUTH_FAILED`: Error deleting the biometric key - -### `biometricAuthAvailable()` - -Checks if biometric authentication is available on the device. On Android, it specifically checks for Biometric Strong Authenticators, which provide a higher level of security. +Deletes all biometric key material (signing and decryption keys) from the device's secure storage. -- **Returns**: `String` - The type of biometric authentication available (`fingerprint`, `face`, `iris`, `TouchID`, `FaceID`, or `biometric`) or a string indicating the error if no biometrics are available. +- **Returns**: `Future`. + - `true`: Keys were successfully deleted, or no keys existed (idempotent). + - `false`: Deletion failed due to a system error. -- **Possible negative returns in Android**: +> **Note**: This operation is idempotent—calling `deleteKeys()` when no keys exist will still return `true`. This allows safe "logout" or "reset" flows without checking key existence first. -- `none, BIOMETRIC_ERROR_NO_HARDWARE`: No biometric hardware available. - -- `none, BIOMETRIC_ERROR_HW_UNAVAILABLE`: Biometric hardware currently unavailable. - -- `none, BIOMETRIC_ERROR_NONE_ENROLLED`: No biometric credentials enrolled. +```dart +final deleted = await biometricSignature.deleteKeys(); +if (deleted) { + print('All biometric keys removed'); +} +``` -- `none, BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED`: Security update required. -- `none, BIOMETRIC_ERROR_UNSUPPORTED`: Biometric authentication is unsupported. +### `biometricAuthAvailable()` -- `none, BIOMETRIC_STATUS_UNKNOWN`: Unknown status. +Checks if biometric authentication is available on the device and returns a structured response. -- `none, NO_BIOMETRICS`: No biometrics. +- **Returns**: `Future`. + - `canAuthenticate`: `bool` indicating if auth is possible. + - `hasEnrolledBiometrics`: `bool` indicating if user has enrolled biometrics. + - `availableBiometrics`: `List` (e.g., `fingerprint`, `face`). + - `reason`: String explanation if unavailable. -### `biometricKeyExists(checkValidity: bool)` +```dart +final availability = await biometricSignature.biometricAuthAvailable(); +if (availability.canAuthenticate) { + print('Biometrics available: ${availability.availableBiometrics}'); +} else { + print('Not available: ${availability.reason}'); +} +``` -Checks if the biometric key pair exists on the device. Optionally, it can also verify the validity of the key by attempting to initialize a signature with it. Since the key requires that user authentication takes place for every use of the key, it is also irreversibly invalidated once a new biometric is enrolled or once no more biometrics are enrolled (when `setInvalidatedByBiometricEnrollment` is `true` on Android or `biometryCurrentSet` is `true` on iOS). +### `getKeyInfo({ checkValidity, keyFormat })` -- **Parameters**: - - `checkValidity`: A bool indicating whether to check the validity of the key by initializing a signature. Default is `false`. -- **Returns**: `bool` - `true` if the key pair exists (and is valid if `checkValidity` is `true`), `false` otherwise. -- **Error Codes**: - - `AUTH_FAILED`: Error checking if the biometric key exists. +Retrieves detailed information about existing biometric keys without prompting for authentication. -## Example +- **Parameters**: + - `checkValidity`: Whether to verify the key hasn't been invalidated by biometric changes. Default is `false`. + - `keyFormat`: Output format for public keys (`KeyFormat.base64`, `pem`, `hex`). Default is `base64`. +- **Returns**: `Future`. + - `exists`: Whether any biometric key exists. + - `isValid`: Key validity status (only populated when `checkValidity: true`). + - `algorithm`: `"RSA"` or `"EC"`. + - `keySize`: Key size in bits (e.g., 2048, 256). + - `isHybridMode`: Whether using hybrid signing/decryption keys. + - `publicKey`: The signing public key. + - `decryptingPublicKey`: Decryption key (hybrid mode only). ```dart -import 'package:biometric_signature/biometric_signature.dart'; -import 'package:flutter/material.dart'; - -void main() => runApp(const MyApp()); - -class MyApp extends StatelessWidget { - const MyApp({super.key}); +final info = await biometricSignature.getKeyInfo( + checkValidity: true, + keyFormat: KeyFormat.pem, +); - @override - Widget build(BuildContext context) { - return const MaterialApp(home: Scaffold(body: BiometricDemo())); - } +if (info.exists && (info.isValid ?? true)) { + print('Algorithm: ${info.algorithm}, Size: ${info.keySize}'); + print('Hybrid Mode: ${info.isHybridMode}'); } +``` -class BiometricDemo extends StatefulWidget { - const BiometricDemo({super.key}); +### `biometricKeyExists({ checkValidity })` - @override - State createState() => _BiometricDemoState(); -} +Convenience method that wraps `getKeyInfo()` and returns a simple boolean. -class _BiometricDemoState extends State { - final _biometricSignature = BiometricSignature(); - KeyCreationResult? keyResult; - SignatureResult? signatureResult; - - Future _generateKeys() async { - keyResult = await _biometricSignature.createKeys( - keyFormat: KeyFormat.pem, - androidConfig: AndroidConfig( - useDeviceCredentials: false, - signatureType: AndroidSignatureType.RSA, - setInvalidatedByBiometricEnrollment: true, // Key invalidated when new biometric is enrolled - enableDecryption: true, // Enable decryption support - ), - iosConfig: IosConfig( - useDeviceCredentials: false, - signatureType: IOSSignatureType.RSA, - biometryCurrentSet: true, // Key constrained to current biometric enrollment - ), - enforceBiometric: true, // Require biometric authentication before generating keys - ); - debugPrint('Public key (${keyResult.publicKey.format.wireValue}):\n${keyResult?.publicKey.asString()}'); - setState(() {}); - } - - Future _sign() async { - signatureResult = await _biometricSignature.createSignature( - SignatureOptions( - payload: 'Payload to sign', - keyFormat: KeyFormat.base64, - promptMessage: 'Authenticate to Sign', - androidOptions: const AndroidSignatureOptions( - subtitle: 'Approve the login to continue', - allowDeviceCredentials: false, - ), - iosOptions: const IosSignatureOptions(shouldMigrate: false), - ), - ); - debugPrint('Signature (${signatureResult.signature.format.wireValue}): ${signatureResult?.signature}'); - setState(() {}); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ElevatedButton( - onPressed: _generateKeys, - child: const Text('Create keys (PEM)'), - ), - const SizedBox(height: 12), - ElevatedButton( - onPressed: _sign, - child: const Text('Sign payload (RAW)'), - ), - if (signatureResult != null) ...[ - const SizedBox(height: 16), - Text('Signature HEX:\n${signatureResult!.signature.toHex()}'), - ], - ], - ), - ); - } -} +- **Parameters**: + - `checkValidity`: Whether to check key validity. Default is `false`. +- **Returns**: `Future` - `true` if key exists and is valid. + +```dart +final exists = await biometricSignature.biometricKeyExists(checkValidity: true); ``` + diff --git a/android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignatureApi.kt b/android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignatureApi.kt new file mode 100644 index 0000000..6a9c9d1 --- /dev/null +++ b/android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignatureApi.kt @@ -0,0 +1,939 @@ +// Autogenerated from Pigeon (v26.1.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package com.visionflutter.biometric_signature + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMethodCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer +private object BiometricSignatureApiPigeonUtils { + + fun wrapResult(result: Any?): List { + return listOf(result) + } + + fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf( + exception.code, + exception.message, + exception.details + ) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) + ) + } + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a is ByteArray && b is ByteArray) { + return a.contentEquals(b) + } + if (a is IntArray && b is IntArray) { + return a.contentEquals(b) + } + if (a is LongArray && b is LongArray) { + return a.contentEquals(b) + } + if (a is DoubleArray && b is DoubleArray) { + return a.contentEquals(b) + } + if (a is Array<*> && b is Array<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is List<*> && b is List<*>) { + return a.size == b.size && + a.indices.all{ deepEquals(a[it], b[it]) } + } + if (a is Map<*, *> && b is Map<*, *>) { + return a.size == b.size && a.all { + (b as Map).contains(it.key) && + deepEquals(it.value, b[it.key]) + } + } + return a == b + } + +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError ( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +/** Types of biometric authentication supported by the device. */ +enum class BiometricType(val raw: Int) { + /** Face recognition (Face ID on iOS, face unlock on Android). */ + FACE(0), + /** Fingerprint recognition (Touch ID on iOS/macOS, fingerprint on Android). */ + FINGERPRINT(1), + /** Iris scanner (Android only, rare on consumer devices). */ + IRIS(2), + /** Multiple biometric types are available on the device. */ + MULTIPLE(3), + /** No biometric hardware available or biometrics are disabled. */ + UNAVAILABLE(4); + + companion object { + fun ofRaw(raw: Int): BiometricType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Standardized error codes for the plugin. */ +enum class BiometricError(val raw: Int) { + /** The operation was successful. */ + SUCCESS(0), + /** The user canceled the operation. */ + USER_CANCELED(1), + /** Biometric authentication is not available on this device. */ + NOT_AVAILABLE(2), + /** No biometrics are enrolled. */ + NOT_ENROLLED(3), + /** The user is temporarily locked out due to too many failed attempts. */ + LOCKED_OUT(4), + /** The user is permanently locked out until they log in with a strong method. */ + LOCKED_OUT_PERMANENT(5), + /** The requested key was not found. */ + KEY_NOT_FOUND(6), + /** The key has been invalidated (e.g. by new biometric enrollment). */ + KEY_INVALIDATED(7), + /** An unknown error occurred. */ + UNKNOWN(8), + /** The input payload was invalid (e.g. not valid Base64). */ + INVALID_INPUT(9); + + companion object { + fun ofRaw(raw: Int): BiometricError? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** The cryptographic algorithm to use for key generation. */ +enum class SignatureType(val raw: Int) { + /** RSA-2048 (Android: native, iOS/macOS: hybrid mode with Secure Enclave EC). */ + RSA(0), + /** ECDSA P-256 (hardware-backed on all platforms). */ + ECDSA(1); + + companion object { + fun ofRaw(raw: Int): SignatureType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Output format for public keys. */ +enum class KeyFormat(val raw: Int) { + /** Base64-encoded DER (SubjectPublicKeyInfo). */ + BASE64(0), + /** PEM format with BEGIN/END PUBLIC KEY headers. */ + PEM(1), + /** Hexadecimal-encoded DER. */ + HEX(2), + /** Raw DER bytes (returned via `publicKeyBytes`). */ + RAW(3); + + companion object { + fun ofRaw(raw: Int): KeyFormat? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Output format for cryptographic signatures. */ +enum class SignatureFormat(val raw: Int) { + /** Base64-encoded signature bytes. */ + BASE64(0), + /** Hexadecimal-encoded signature bytes. */ + HEX(1), + /** Raw signature bytes (returned via `signatureBytes`). */ + RAW(2); + + companion object { + fun ofRaw(raw: Int): SignatureFormat? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Input format for encrypted payloads to decrypt. */ +enum class PayloadFormat(val raw: Int) { + /** Base64-encoded ciphertext. */ + BASE64(0), + /** Hexadecimal-encoded ciphertext. */ + HEX(1), + /** Raw UTF-8 string (not recommended for binary data). */ + RAW(2); + + companion object { + fun ofRaw(raw: Int): PayloadFormat? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class BiometricAvailability ( + val canAuthenticate: Boolean? = null, + val hasEnrolledBiometrics: Boolean? = null, + val availableBiometrics: List? = null, + val reason: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): BiometricAvailability { + val canAuthenticate = pigeonVar_list[0] as Boolean? + val hasEnrolledBiometrics = pigeonVar_list[1] as Boolean? + val availableBiometrics = pigeonVar_list[2] as List? + val reason = pigeonVar_list[3] as String? + return BiometricAvailability(canAuthenticate, hasEnrolledBiometrics, availableBiometrics, reason) + } + } + fun toList(): List { + return listOf( + canAuthenticate, + hasEnrolledBiometrics, + availableBiometrics, + reason, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is BiometricAvailability) { + return false + } + if (this === other) { + return true + } + return BiometricSignatureApiPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class KeyCreationResult ( + val publicKey: String? = null, + val publicKeyBytes: ByteArray? = null, + val error: String? = null, + val code: BiometricError? = null, + val algorithm: String? = null, + val keySize: Long? = null, + val decryptingPublicKey: String? = null, + val decryptingAlgorithm: String? = null, + val decryptingKeySize: Long? = null, + val isHybridMode: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): KeyCreationResult { + val publicKey = pigeonVar_list[0] as String? + val publicKeyBytes = pigeonVar_list[1] as ByteArray? + val error = pigeonVar_list[2] as String? + val code = pigeonVar_list[3] as BiometricError? + val algorithm = pigeonVar_list[4] as String? + val keySize = pigeonVar_list[5] as Long? + val decryptingPublicKey = pigeonVar_list[6] as String? + val decryptingAlgorithm = pigeonVar_list[7] as String? + val decryptingKeySize = pigeonVar_list[8] as Long? + val isHybridMode = pigeonVar_list[9] as Boolean? + return KeyCreationResult(publicKey, publicKeyBytes, error, code, algorithm, keySize, decryptingPublicKey, decryptingAlgorithm, decryptingKeySize, isHybridMode) + } + } + fun toList(): List { + return listOf( + publicKey, + publicKeyBytes, + error, + code, + algorithm, + keySize, + decryptingPublicKey, + decryptingAlgorithm, + decryptingKeySize, + isHybridMode, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is KeyCreationResult) { + return false + } + if (this === other) { + return true + } + return BiometricSignatureApiPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SignatureResult ( + val signature: String? = null, + val signatureBytes: ByteArray? = null, + val publicKey: String? = null, + val error: String? = null, + val code: BiometricError? = null, + val algorithm: String? = null, + val keySize: Long? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): SignatureResult { + val signature = pigeonVar_list[0] as String? + val signatureBytes = pigeonVar_list[1] as ByteArray? + val publicKey = pigeonVar_list[2] as String? + val error = pigeonVar_list[3] as String? + val code = pigeonVar_list[4] as BiometricError? + val algorithm = pigeonVar_list[5] as String? + val keySize = pigeonVar_list[6] as Long? + return SignatureResult(signature, signatureBytes, publicKey, error, code, algorithm, keySize) + } + } + fun toList(): List { + return listOf( + signature, + signatureBytes, + publicKey, + error, + code, + algorithm, + keySize, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is SignatureResult) { + return false + } + if (this === other) { + return true + } + return BiometricSignatureApiPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class DecryptResult ( + val decryptedData: String? = null, + val error: String? = null, + val code: BiometricError? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): DecryptResult { + val decryptedData = pigeonVar_list[0] as String? + val error = pigeonVar_list[1] as String? + val code = pigeonVar_list[2] as BiometricError? + return DecryptResult(decryptedData, error, code) + } + } + fun toList(): List { + return listOf( + decryptedData, + error, + code, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is DecryptResult) { + return false + } + if (this === other) { + return true + } + return BiometricSignatureApiPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Detailed information about existing biometric keys. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class KeyInfo ( + /** Whether any biometric key exists on the device. */ + val exists: Boolean? = null, + /** + * Whether the key is still valid (not invalidated by biometric changes). + * Only populated when `checkValidity: true` is passed. + */ + val isValid: Boolean? = null, + /** The algorithm of the signing key (e.g., "RSA", "EC"). */ + val algorithm: String? = null, + /** The key size in bits (e.g., 2048 for RSA, 256 for EC). */ + val keySize: Long? = null, + /** Whether the key is in hybrid mode (separate signing and decryption keys). */ + val isHybridMode: Boolean? = null, + /** Signing key public key (formatted according to the requested format). */ + val publicKey: String? = null, + /** Decryption key public key for hybrid mode. */ + val decryptingPublicKey: String? = null, + /** Algorithm of the decryption key (hybrid mode only). */ + val decryptingAlgorithm: String? = null, + /** Key size of the decryption key in bits (hybrid mode only). */ + val decryptingKeySize: Long? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): KeyInfo { + val exists = pigeonVar_list[0] as Boolean? + val isValid = pigeonVar_list[1] as Boolean? + val algorithm = pigeonVar_list[2] as String? + val keySize = pigeonVar_list[3] as Long? + val isHybridMode = pigeonVar_list[4] as Boolean? + val publicKey = pigeonVar_list[5] as String? + val decryptingPublicKey = pigeonVar_list[6] as String? + val decryptingAlgorithm = pigeonVar_list[7] as String? + val decryptingKeySize = pigeonVar_list[8] as Long? + return KeyInfo(exists, isValid, algorithm, keySize, isHybridMode, publicKey, decryptingPublicKey, decryptingAlgorithm, decryptingKeySize) + } + } + fun toList(): List { + return listOf( + exists, + isValid, + algorithm, + keySize, + isHybridMode, + publicKey, + decryptingPublicKey, + decryptingAlgorithm, + decryptingKeySize, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is KeyInfo) { + return false + } + if (this === other) { + return true + } + return BiometricSignatureApiPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Configuration for key creation (all platforms). + * + * Fields are documented with which platform(s) they apply to. + * Windows ignores most fields as it only supports RSA with mandatory + * Windows Hello authentication. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class CreateKeysConfig ( + /** + * [Android/iOS/macOS] The cryptographic algorithm to use. + * Windows only supports RSA and ignores this field. + */ + val signatureType: SignatureType? = null, + /** + * [Android/iOS/macOS] Whether to require biometric authentication + * during key creation. Windows always authenticates via Windows Hello. + */ + val enforceBiometric: Boolean? = null, + /** + * [Android/iOS/macOS] Whether to invalidate the key when new biometrics + * are enrolled. Not supported on Windows. + * + * **Security Note**: When `true`, keys become invalid if fingerprints/faces + * are added or removed, preventing unauthorized access if an attacker + * enrolls their own biometrics on a compromised device. + */ + val setInvalidatedByBiometricEnrollment: Boolean? = null, + /** + * [Android/iOS/macOS] Whether to allow device credentials (PIN/pattern/passcode) + * as fallback for biometric authentication. Not supported on Windows. + */ + val useDeviceCredentials: Boolean? = null, + /** + * [Android] Whether to enable decryption capability for the key. + * On iOS/macOS, decryption is always available with EC keys. + */ + val enableDecryption: Boolean? = null, + /** [Android] Subtitle text for the biometric prompt. */ + val promptSubtitle: String? = null, + /** [Android] Description text for the biometric prompt. */ + val promptDescription: String? = null, + /** [Android] Text for the cancel button in the biometric prompt. */ + val cancelButtonText: String? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): CreateKeysConfig { + val signatureType = pigeonVar_list[0] as SignatureType? + val enforceBiometric = pigeonVar_list[1] as Boolean? + val setInvalidatedByBiometricEnrollment = pigeonVar_list[2] as Boolean? + val useDeviceCredentials = pigeonVar_list[3] as Boolean? + val enableDecryption = pigeonVar_list[4] as Boolean? + val promptSubtitle = pigeonVar_list[5] as String? + val promptDescription = pigeonVar_list[6] as String? + val cancelButtonText = pigeonVar_list[7] as String? + return CreateKeysConfig(signatureType, enforceBiometric, setInvalidatedByBiometricEnrollment, useDeviceCredentials, enableDecryption, promptSubtitle, promptDescription, cancelButtonText) + } + } + fun toList(): List { + return listOf( + signatureType, + enforceBiometric, + setInvalidatedByBiometricEnrollment, + useDeviceCredentials, + enableDecryption, + promptSubtitle, + promptDescription, + cancelButtonText, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is CreateKeysConfig) { + return false + } + if (this === other) { + return true + } + return BiometricSignatureApiPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Configuration for signature creation (all platforms). + * + * Fields are documented with which platform(s) they apply to. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class CreateSignatureConfig ( + /** [Android] Subtitle text for the biometric prompt. */ + val promptSubtitle: String? = null, + /** [Android] Description text for the biometric prompt. */ + val promptDescription: String? = null, + /** [Android] Text for the cancel button in the biometric prompt. */ + val cancelButtonText: String? = null, + /** [Android] Whether to allow device credentials (PIN/pattern) as fallback. */ + val allowDeviceCredentials: Boolean? = null, + /** [iOS] Whether to migrate from legacy keychain storage. */ + val shouldMigrate: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): CreateSignatureConfig { + val promptSubtitle = pigeonVar_list[0] as String? + val promptDescription = pigeonVar_list[1] as String? + val cancelButtonText = pigeonVar_list[2] as String? + val allowDeviceCredentials = pigeonVar_list[3] as Boolean? + val shouldMigrate = pigeonVar_list[4] as Boolean? + return CreateSignatureConfig(promptSubtitle, promptDescription, cancelButtonText, allowDeviceCredentials, shouldMigrate) + } + } + fun toList(): List { + return listOf( + promptSubtitle, + promptDescription, + cancelButtonText, + allowDeviceCredentials, + shouldMigrate, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is CreateSignatureConfig) { + return false + } + if (this === other) { + return true + } + return BiometricSignatureApiPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} + +/** + * Configuration for decryption (all platforms). + * + * Fields are documented with which platform(s) they apply to. + * Note: Decryption is not supported on Windows. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class DecryptConfig ( + /** [Android] Subtitle text for the biometric prompt. */ + val promptSubtitle: String? = null, + /** [Android] Description text for the biometric prompt. */ + val promptDescription: String? = null, + /** [Android] Text for the cancel button in the biometric prompt. */ + val cancelButtonText: String? = null, + /** [Android] Whether to allow device credentials (PIN/pattern) as fallback. */ + val allowDeviceCredentials: Boolean? = null, + /** [iOS] Whether to migrate from legacy keychain storage. */ + val shouldMigrate: Boolean? = null +) + { + companion object { + fun fromList(pigeonVar_list: List): DecryptConfig { + val promptSubtitle = pigeonVar_list[0] as String? + val promptDescription = pigeonVar_list[1] as String? + val cancelButtonText = pigeonVar_list[2] as String? + val allowDeviceCredentials = pigeonVar_list[3] as Boolean? + val shouldMigrate = pigeonVar_list[4] as Boolean? + return DecryptConfig(promptSubtitle, promptDescription, cancelButtonText, allowDeviceCredentials, shouldMigrate) + } + } + fun toList(): List { + return listOf( + promptSubtitle, + promptDescription, + cancelButtonText, + allowDeviceCredentials, + shouldMigrate, + ) + } + override fun equals(other: Any?): Boolean { + if (other !is DecryptConfig) { + return false + } + if (this === other) { + return true + } + return BiometricSignatureApiPigeonUtils.deepEquals(toList(), other.toList()) } + + override fun hashCode(): Int = toList().hashCode() +} +private open class BiometricSignatureApiPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + BiometricType.ofRaw(it.toInt()) + } + } + 130.toByte() -> { + return (readValue(buffer) as Long?)?.let { + BiometricError.ofRaw(it.toInt()) + } + } + 131.toByte() -> { + return (readValue(buffer) as Long?)?.let { + SignatureType.ofRaw(it.toInt()) + } + } + 132.toByte() -> { + return (readValue(buffer) as Long?)?.let { + KeyFormat.ofRaw(it.toInt()) + } + } + 133.toByte() -> { + return (readValue(buffer) as Long?)?.let { + SignatureFormat.ofRaw(it.toInt()) + } + } + 134.toByte() -> { + return (readValue(buffer) as Long?)?.let { + PayloadFormat.ofRaw(it.toInt()) + } + } + 135.toByte() -> { + return (readValue(buffer) as? List)?.let { + BiometricAvailability.fromList(it) + } + } + 136.toByte() -> { + return (readValue(buffer) as? List)?.let { + KeyCreationResult.fromList(it) + } + } + 137.toByte() -> { + return (readValue(buffer) as? List)?.let { + SignatureResult.fromList(it) + } + } + 138.toByte() -> { + return (readValue(buffer) as? List)?.let { + DecryptResult.fromList(it) + } + } + 139.toByte() -> { + return (readValue(buffer) as? List)?.let { + KeyInfo.fromList(it) + } + } + 140.toByte() -> { + return (readValue(buffer) as? List)?.let { + CreateKeysConfig.fromList(it) + } + } + 141.toByte() -> { + return (readValue(buffer) as? List)?.let { + CreateSignatureConfig.fromList(it) + } + } + 142.toByte() -> { + return (readValue(buffer) as? List)?.let { + DecryptConfig.fromList(it) + } + } + else -> super.readValueOfType(type, buffer) + } + } + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is BiometricType -> { + stream.write(129) + writeValue(stream, value.raw.toLong()) + } + is BiometricError -> { + stream.write(130) + writeValue(stream, value.raw.toLong()) + } + is SignatureType -> { + stream.write(131) + writeValue(stream, value.raw.toLong()) + } + is KeyFormat -> { + stream.write(132) + writeValue(stream, value.raw.toLong()) + } + is SignatureFormat -> { + stream.write(133) + writeValue(stream, value.raw.toLong()) + } + is PayloadFormat -> { + stream.write(134) + writeValue(stream, value.raw.toLong()) + } + is BiometricAvailability -> { + stream.write(135) + writeValue(stream, value.toList()) + } + is KeyCreationResult -> { + stream.write(136) + writeValue(stream, value.toList()) + } + is SignatureResult -> { + stream.write(137) + writeValue(stream, value.toList()) + } + is DecryptResult -> { + stream.write(138) + writeValue(stream, value.toList()) + } + is KeyInfo -> { + stream.write(139) + writeValue(stream, value.toList()) + } + is CreateKeysConfig -> { + stream.write(140) + writeValue(stream, value.toList()) + } + is CreateSignatureConfig -> { + stream.write(141) + writeValue(stream, value.toList()) + } + is DecryptConfig -> { + stream.write(142) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface BiometricSignatureApi { + /** Checks if biometric authentication is available. */ + fun biometricAuthAvailable(callback: (Result) -> Unit) + /** + * Creates a new key pair. + * + * [config] contains platform-specific options. See [CreateKeysConfig]. + * [keyFormat] specifies the output format for the public key. + * [promptMessage] is the message shown to the user during authentication. + */ + fun createKeys(config: CreateKeysConfig?, keyFormat: KeyFormat, promptMessage: String?, callback: (Result) -> Unit) + /** + * Creates a signature. + * + * [payload] is the data to sign. + * [config] contains platform-specific options. See [CreateSignatureConfig]. + * [signatureFormat] specifies the output format for the signature. + * [keyFormat] specifies the output format for the public key. + * [promptMessage] is the message shown to the user during authentication. + */ + fun createSignature(payload: String, config: CreateSignatureConfig?, signatureFormat: SignatureFormat, keyFormat: KeyFormat, promptMessage: String?, callback: (Result) -> Unit) + /** + * Decrypts data. + * + * Note: Not supported on Windows. + * [payload] is the encrypted data. + * [payloadFormat] specifies the format of the encrypted data. + * [config] contains platform-specific options. See [DecryptConfig]. + * [promptMessage] is the message shown to the user during authentication. + */ + fun decrypt(payload: String, payloadFormat: PayloadFormat, config: DecryptConfig?, promptMessage: String?, callback: (Result) -> Unit) + /** Deletes keys. */ + fun deleteKeys(callback: (Result) -> Unit) + /** + * Gets detailed information about existing biometric keys. + * + * Returns key metadata including algorithm, size, validity, and public keys. + */ + fun getKeyInfo(checkValidity: Boolean, keyFormat: KeyFormat, callback: (Result) -> Unit) + + companion object { + /** The codec used by BiometricSignatureApi. */ + val codec: MessageCodec by lazy { + BiometricSignatureApiPigeonCodec() + } + /** Sets up an instance of `BiometricSignatureApi` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: BiometricSignatureApi?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.biometricAuthAvailable$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.biometricAuthAvailable{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(BiometricSignatureApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(BiometricSignatureApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createKeys$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val configArg = args[0] as CreateKeysConfig? + val keyFormatArg = args[1] as KeyFormat + val promptMessageArg = args[2] as String? + api.createKeys(configArg, keyFormatArg, promptMessageArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(BiometricSignatureApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(BiometricSignatureApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createSignature$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val payloadArg = args[0] as String + val configArg = args[1] as CreateSignatureConfig? + val signatureFormatArg = args[2] as SignatureFormat + val keyFormatArg = args[3] as KeyFormat + val promptMessageArg = args[4] as String? + api.createSignature(payloadArg, configArg, signatureFormatArg, keyFormatArg, promptMessageArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(BiometricSignatureApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(BiometricSignatureApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.decrypt$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val payloadArg = args[0] as String + val payloadFormatArg = args[1] as PayloadFormat + val configArg = args[2] as DecryptConfig? + val promptMessageArg = args[3] as String? + api.decrypt(payloadArg, payloadFormatArg, configArg, promptMessageArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(BiometricSignatureApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(BiometricSignatureApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.deleteKeys$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.deleteKeys{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(BiometricSignatureApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(BiometricSignatureApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.getKeyInfo$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val checkValidityArg = args[0] as Boolean + val keyFormatArg = args[1] as KeyFormat + api.getKeyInfo(checkValidityArg, keyFormatArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(BiometricSignatureApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(BiometricSignatureApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} 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 078e65b..5f31467 100644 --- a/android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignaturePlugin.kt +++ b/android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignaturePlugin.kt @@ -6,7 +6,6 @@ import android.hardware.fingerprint.FingerprintManager import android.os.Build import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties -import android.security.keystore.StrongBoxUnavailableException import android.util.Base64 import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt @@ -16,11 +15,6 @@ import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.StandardMethodCodec import kotlinx.coroutines.* import java.io.File import java.security.* @@ -30,7 +24,6 @@ import java.security.spec.ECGenParameterSpec import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.RSAKeyGenParameterSpec import java.security.spec.X509EncodedKeySpec -import java.text.SimpleDateFormat import java.util.* import javax.crypto.Cipher import javax.crypto.KeyAgreement @@ -43,103 +36,51 @@ import kotlin.coroutines.resumeWithException /** * BiometricSignaturePlugin - Flutter plugin for biometric-protected cryptographic operations. - * - * Storage Architecture (mirrors iOS Keychain approach): - * - Wrapped software EC private key is stored in the app's private files directory as a single binary file: - * [IV (12 bytes)] || [ciphertext] - * - The associated public key (DER) is stored in a separate file - * - * Security Model: - * - Files are private to the app (MODE_PRIVATE) and not world-readable - * - The AES master key that encrypts the private key is Keystore-backed and requires biometric auth - * - This mirrors iOS where encrypted RSA key is stored as kSecClassGenericPassword - * - * Security notes: - * - Raw private key bytes are zeroed immediately after use - * - Sensitive derived keys/bytes are zeroized where possible */ -class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware { - - // ==================== Constants ==================== +class BiometricSignaturePlugin : FlutterPlugin, BiometricSignatureApi, ActivityAware { private companion object { - const val CHANNEL_NAME = "biometric_signature" const val KEYSTORE_PROVIDER = "AndroidKeyStore" - - // Key aliases in Keystore - const val BIOMETRIC_KEY_ALIAS = "biometric_key" // RSA or EC signing key - const val MASTER_KEY_ALIAS = "biometric_master_key" // AES wrapper for hybrid mode - - // File storage (mirrors iOS kSecClassGenericPassword storage) - private const val EC_WRAPPED_FILENAME = - "biometric_ec_wrapped.bin" // contains iv||ciphertext + const val BIOMETRIC_KEY_ALIAS = "biometric_key" + const val MASTER_KEY_ALIAS = "biometric_master_key" + private const val EC_WRAPPED_FILENAME = "biometric_ec_wrapped.bin" private const val EC_PUB_FILENAME = "biometric_ec_pub.der" - // ECIES constants - const val EC_PUBKEY_SIZE = 65 // Uncompressed P-256: 0x04 || X(32) || Y(32) + const val EC_PUBKEY_SIZE = 65 const val GCM_TAG_BITS = 128 const val GCM_TAG_BYTES = 16 - const val AES_KEY_SIZE = 16 // AES-128 for ECIES + const val AES_KEY_SIZE = 16 const val GCM_IV_SIZE = 12 } - private object Errors { - const val NO_ACTIVITY = "NO_ACTIVITY" - const val AUTH_FAILED = "AUTH_FAILED" - const val INVALID_PAYLOAD = "INVALID_PAYLOAD" - const val KEY_NOT_FOUND = "KEY_NOT_FOUND" - const val DECRYPTION_NOT_ENABLED = "DECRYPTION_NOT_ENABLED" - } - private enum class KeyMode { RSA, EC_SIGN_ONLY, HYBRID_EC } - private enum class KeyFormat { - BASE64, PEM, RAW, HEX; - - companion object { - fun from(value: String?): KeyFormat = runCatching { - valueOf(value?.uppercase(Locale.US) ?: "BASE64") - }.getOrDefault(BASE64) - } - } - private data class FormattedOutput( - val value: Any, + val value: String, val format: KeyFormat, val pemLabel: String? = null ) - // ==================== Plugin State ==================== - private lateinit var channel: MethodChannel private lateinit var appContext: Context private var activity: FlutterFragmentActivity? = null private val pluginJob = SupervisorJob() private val pluginScope = CoroutineScope(Dispatchers.Main.immediate + pluginJob) - // ==================== FlutterPlugin Lifecycle ==================== override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { appContext = binding.applicationContext - val taskQueue = binding.binaryMessenger.makeBackgroundTaskQueue() - channel = MethodChannel( - binding.binaryMessenger, - CHANNEL_NAME, - StandardMethodCodec.INSTANCE, - taskQueue - ) - channel.setMethodCallHandler(this) + BiometricSignatureApi.setUp(binding.binaryMessenger, this) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) + BiometricSignatureApi.setUp(binding.binaryMessenger, null) pluginJob.cancel() } - // ==================== ActivityAware ==================== override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity as? FlutterFragmentActivity } @@ -152,94 +93,85 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) = onAttachedToActivity(binding) - // ==================== Method Channel Handler ==================== - override fun onMethodCall(call: MethodCall, result: Result) { - val act = activity ?: return result.error( - Errors.NO_ACTIVITY, - "Foreground activity required", - null - ) - - pluginScope.launch { - try { - when (call.method) { - "createKeys" -> createKeys(call, result, act) - "createSignature" -> createSignature(call, result, act) - "decrypt" -> decrypt(call, result, act) - "deleteKeys" -> deleteKeys(result) - "biometricAuthAvailable" -> result.success(getBiometricAvailability()) - "biometricKeyExists" -> result.success(checkKeyExists(call.arguments as? Boolean == true)) - else -> result.notImplemented() - } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - // Generic failures -> AUTH_FAILED - result.error(Errors.AUTH_FAILED, e.message, null) - } + // ==================== BiometricSignatureApi Implementation ==================== + + 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" + ))) + return } + + val manager = BiometricManager.from(act) + val canAuth = manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + + val canAuthenticate = canAuth == BiometricManager.BIOMETRIC_SUCCESS + val hasEnrolledBiometrics = canAuth != BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED && + canAuth != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE && + canAuth != BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE + + val (types, _) = detectBiometricTypes() + + val reason = if (!canAuthenticate) biometricErrorName(canAuth) else null + + callback(Result.success(BiometricAvailability( + canAuthenticate = canAuthenticate, + hasEnrolledBiometrics = hasEnrolledBiometrics, + availableBiometrics = types, + reason = reason + ))) } - // ==================== Create Keys ==================== - private suspend fun createKeys( - call: MethodCall, - result: Result, - activity: FlutterFragmentActivity + override fun createKeys( + config: CreateKeysConfig?, + keyFormat: KeyFormat, + promptMessage: String?, + callback: (Result) -> Unit ) { - val args = call.arguments>() ?: emptyMap() - val useEc = args.boolean("useEc") - val enableDecryption = args.boolean("enableDecryption") - val useDeviceCredentials = args.boolean("useDeviceCredentials") - val invalidateOnEnrollment = args.boolean("setInvalidatedByBiometricEnrollment") - val enforceBiometric = args.boolean("enforceBiometric") - val keyFormat = KeyFormat.from(args["keyFormat"] as? String) - val promptMessage = args["promptMessage"] as? String ?: "Authenticate to create keys" - - // Determine mode - val mode = when { - !useEc -> KeyMode.RSA - useEc && !enableDecryption -> KeyMode.EC_SIGN_ONLY - else -> KeyMode.HYBRID_EC + val act = activity + if (act == null) { + callback(Result.success(KeyCreationResult(code = BiometricError.UNKNOWN, error = "Foreground activity required"))) + return } - when (mode) { - KeyMode.RSA -> createRsaKeys( - activity, - result, - useDeviceCredentials, - invalidateOnEnrollment, - enableDecryption, - enforceBiometric, - keyFormat, - promptMessage - ) + pluginScope.launch { + try { + // Extract config values with defaults + val useDeviceCredentials = config?.useDeviceCredentials ?: false + val enableDecryption = config?.enableDecryption ?: false + val invalidateOnEnrollment = config?.setInvalidatedByBiometricEnrollment ?: true + val signatureType = config?.signatureType ?: SignatureType.RSA + val enforceBiometric = config?.enforceBiometric ?: false + + val mode = when(signatureType) { + SignatureType.RSA -> KeyMode.RSA + SignatureType.ECDSA -> if (enableDecryption) KeyMode.HYBRID_EC else KeyMode.EC_SIGN_ONLY + } - KeyMode.EC_SIGN_ONLY -> createEcSigningKeys( - activity, - result, - useDeviceCredentials, - invalidateOnEnrollment, - enforceBiometric, - keyFormat, - promptMessage - ) + val prompt = promptMessage ?: "Authenticate to create keys" - KeyMode.HYBRID_EC -> createHybridEcKeys( - activity, - result, - useDeviceCredentials, - invalidateOnEnrollment, - keyFormat, - enforceBiometric, - promptMessage - ) + // 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) + } + + } catch (e: Exception) { + callback(Result.success(KeyCreationResult(code = mapToBiometricError(e), error = e.message))) + } } } - // ---------- RSA Mode ---------- + // Helper to consolidate Key creation logic return private suspend fun createRsaKeys( activity: FlutterFragmentActivity, - result: Result, + callback: (Result) -> Unit, useDeviceCredentials: Boolean, invalidateOnEnrollment: Boolean, enableDecryption: Boolean, @@ -249,14 +181,7 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware ) { if (enforceBiometric) { checkBiometricAvailability(activity, useDeviceCredentials) - authenticate( - activity, - promptMessage, - null, - "Cancel", - useDeviceCredentials, - null - ) + authenticate(activity, promptMessage, null, "Cancel", useDeviceCredentials, null) } val keyPair = withContext(Dispatchers.IO) { @@ -264,44 +189,13 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware generateRsaKeyInKeyStore(useDeviceCredentials, invalidateOnEnrollment, enableDecryption) } - val response = buildKeyResponse(keyPair.public, keyFormat, "RSA") - result.success(response) - } - - private fun generateRsaKeyInKeyStore( - useDeviceCredentials: Boolean, - invalidateOnEnrollment: Boolean, - enableDecryption: Boolean - ): KeyPair { - val purposes = if (enableDecryption) { - KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_DECRYPT - } else { - KeyProperties.PURPOSE_SIGN - } - - val builder = KeyGenParameterSpec.Builder(BIOMETRIC_KEY_ALIAS, purposes) - .setDigests(KeyProperties.DIGEST_SHA256) - .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) - .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) - .setUserAuthenticationRequired(true) - - if (enableDecryption) { - builder.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) - } - - configurePerOperationAuth(builder, useDeviceCredentials) - configureInvalidation(builder, invalidateOnEnrollment) - tryEnableStrongBox(builder) - - val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEYSTORE_PROVIDER) - kpg.initialize(builder.build()) - return kpg.generateKeyPair() + val response = buildKeyResponse(keyPair.public, keyFormat) + callback(Result.success(response)) } - // ---------- EC Signing Only Mode ---------- private suspend fun createEcSigningKeys( activity: FlutterFragmentActivity, - result: Result, + callback: (Result) -> Unit, useDeviceCredentials: Boolean, invalidateOnEnrollment: Boolean, enforceBiometric: Boolean, @@ -310,14 +204,7 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware ) { if (enforceBiometric) { checkBiometricAvailability(activity, useDeviceCredentials) - authenticate( - activity, - promptMessage, - null, - "Cancel", - useDeviceCredentials, - null - ) + authenticate(activity, promptMessage, null, "Cancel", useDeviceCredentials, null) } val keyPair = withContext(Dispatchers.IO) { @@ -325,70 +212,33 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware generateEcKeyInKeyStore(useDeviceCredentials, invalidateOnEnrollment) } - val response = buildKeyResponse(keyPair.public, keyFormat, "EC") - result.success(response) + val response = buildKeyResponse(keyPair.public, keyFormat) + callback(Result.success(response)) } - private fun generateEcKeyInKeyStore( - useDeviceCredentials: Boolean, - invalidateOnEnrollment: Boolean - ): KeyPair { - val builder = KeyGenParameterSpec.Builder(BIOMETRIC_KEY_ALIAS, KeyProperties.PURPOSE_SIGN) - .setDigests(KeyProperties.DIGEST_SHA256) - .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) - .setUserAuthenticationRequired(true) - - configurePerOperationAuth(builder, useDeviceCredentials) - configureInvalidation(builder, invalidateOnEnrollment) - tryEnableStrongBox(builder) - - val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEYSTORE_PROVIDER) - kpg.initialize(builder.build()) - return kpg.generateKeyPair() - } - - // ---------- Hybrid EC Mode (Android) ---------- - // Generates hardware EC signing key (Keystore), AES master key (Keystore), - // then creates a software EC keypair and encrypts the private key with the - // master key. The wrapped blob is stored to app-private file. - private suspend fun createHybridEcKeys( activity: FlutterFragmentActivity, - result: Result, + callback: (Result) -> Unit, useDeviceCredentials: Boolean, invalidateOnEnrollment: Boolean, keyFormat: KeyFormat, enforceBiometric: Boolean, promptMessage: String ) { - // Step 0: If requested, force biometric before key creation if (enforceBiometric) { checkBiometricAvailability(activity, useDeviceCredentials) - // Authenticate with a no-crypto prompt for enforcement only - authenticate( - activity, - promptMessage, - null, - "Cancel", - useDeviceCredentials, - null - ) + authenticate(activity, promptMessage, null, "Cancel", useDeviceCredentials, null) } - // 1. Generate signing EC key and master AES key val signingKeyPair = withContext(Dispatchers.IO) { deleteAllKeys() - // Generate EC signing key (hardware) val ecKeyPair = generateEcKeyInKeyStore(useDeviceCredentials, invalidateOnEnrollment) - // Generate master AES key (hardware-backed secret) for wrapping generateMasterKey(useDeviceCredentials, invalidateOnEnrollment) ecKeyPair } - // 2. Prepare an ENCRYPT cipher from master key — this operation requires biometric auth later val cipherForWrap = withContext(Dispatchers.IO) { getCipherForEncryption() } - // 3. Ask user to authenticate to allow wrapping of the software private key checkBiometricAvailability(activity, useDeviceCredentials) val authResult = authenticate( activity, @@ -402,64 +252,327 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware val authenticatedCipher = authResult.cryptoObject?.cipher ?: throw SecurityException("Authentication failed - no cipher returned") - // 4. Generate software EC keypair and seal (encrypt) private key, store to files val (wrappedBlob, publicKeyBytes) = withContext(Dispatchers.IO) { generateAndSealDecryptionEcKeyLocal(authenticatedCipher) } - // Persist wrapped blob and public key to files (app-private storage). - // The wrappedBlob = IV || ciphertext writeFileAtomic(EC_WRAPPED_FILENAME, wrappedBlob) writeFileAtomic(EC_PUB_FILENAME, publicKeyBytes) - // 5. Return response: - // For compatibility with previous responses, we return "publicKey" = the encryption public key (DER), - // and include separate "signingPublicKey" in response payload. - val signingPubFormatted = formatOutput(signingKeyPair.public.encoded, keyFormat) - val decryptionPubFormatted = formatOutput(publicKeyBytes, keyFormat) - - val response = hashMapOf( - "publicKey" to decryptionPubFormatted.value, - "publicKeyFormat" to decryptionPubFormatted.format.name, - "algorithm" to "EC", - "keySize" to 256, - "keyFormat" to keyFormat.name, - "signingPublicKey" to signingPubFormatted.value, - "signingPublicKeyFormat" to signingPubFormatted.format.name, - "signingAlgorithm" to "EC", - "signingKeySize" to 256, - "hybridMode" to true + // For hybrid, we return the Signing Key as default, and Decryption Key as optional + val decryptingPublicKey = KeyFactory.getInstance("EC").generatePublic(X509EncodedKeySpec(publicKeyBytes)) + + val response = buildKeyResponse( + publicKey = signingKeyPair.public, + format = keyFormat, + decryptingKey = decryptingPublicKey ) - decryptionPubFormatted.pemLabel?.let { response["publicKeyPemLabel"] = it } - signingPubFormatted.pemLabel?.let { response["signingPublicKeyPemLabel"] = it } - result.success(response) + callback(Result.success(response)) } - /** - * Attempt to enable StrongBox (best-effort). If unavailable, leave builder as-is (TEE). - */ - private fun tryEnableStrongBox(builder: KeyGenParameterSpec.Builder) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && - appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE) - ) { + override fun createSignature( + payload: String, + config: CreateSignatureConfig?, + signatureFormat: SignatureFormat, + keyFormat: KeyFormat, + promptMessage: String?, + callback: (Result) -> Unit + ) { + val act = activity + if (act == null) { + 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"))) + return + } + + pluginScope.launch { try { - builder.setIsStrongBoxBacked(true) - } catch (_: StrongBoxUnavailableException) { - // Fallback silently (TEE) - } catch (_: Throwable) { - // Some devices may throw other runtime errors — ignore and fallback + val mode = inferKeyModeFromKeystore() ?: throw SecurityException("Signing key not found") + + val allowDeviceCredentials = config?.allowDeviceCredentials ?: false + + val (signature, cryptoObject) = withContext(Dispatchers.IO) { + prepareSignature(mode) + } + + checkBiometricAvailability(act, allowDeviceCredentials) + + val authResult = authenticate( + act, + promptMessage ?: "Authenticate", + config?.promptSubtitle, + config?.cancelButtonText ?: "Cancel", + allowDeviceCredentials, + cryptoObject + ) + + val signatureBytes = withContext(Dispatchers.IO) { + val sig = authResult.cryptoObject?.signature ?: signature + try { + sig.update(payload.toByteArray(Charsets.UTF_8)) + sig.sign() + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid payload", e) + } + } + + val publicKey = getSigningPublicKey() + val response = buildSignatureResponse(signatureBytes, publicKey, signatureFormat, keyFormat) + callback(Result.success(response)) + + } catch (e: Exception) { + callback(Result.success(SignatureResult(code = mapToBiometricError(e), error = e.message))) } } } - /** - * Generates an AES master key (256-bit) in AndroidKeyStore. - * The key is created for per-operation (user-auth) usage. - * - * Note: We intentionally do NOT enable StrongBox for the master key to avoid - * compatibility issues on some devices where StrongBox AES keys have limitations. - */ + override fun decrypt( + payload: String, + payloadFormat: PayloadFormat, + config: DecryptConfig?, + promptMessage: String?, + callback: (Result) -> Unit + ) { + val act = activity + if (act == null) { + 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"))) + return + } + + pluginScope.launch { + try { + val mode = inferKeyModeFromKeystore() + ?: throw SecurityException("Keys not found") + + if (mode == KeyMode.EC_SIGN_ONLY) { + throw SecurityException("Decryption not enabled for EC signing-only mode") + } + + val allowDeviceCredentials = config?.allowDeviceCredentials ?: false + val prompt = promptMessage ?: "Authenticate" + val subtitle = config?.promptSubtitle + 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) + else -> throw SecurityException("Unsupported decryption mode") + } + + callback(Result.success(DecryptResult(decryptedData = decryptedData, code = BiometricError.SUCCESS))) + + } catch(e: Exception) { + callback(Result.success(DecryptResult(code = mapToBiometricError(e), error = e.message))) + } + } + } + + private suspend fun decryptRsa( + activity: FlutterFragmentActivity, + payload: String, + payloadFormat: PayloadFormat, + prompt: String, + subtitle: String?, + cancel: String, + allowDeviceCredentials: Boolean + ): String { + val cipher = withContext(Dispatchers.IO) { + val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) } + val entry = keyStore.getEntry(BIOMETRIC_KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry + ?: throw IllegalStateException("RSA key not found") + Cipher.getInstance("RSA/ECB/PKCS1Padding").apply { + init(Cipher.DECRYPT_MODE, entry.privateKey) + } + } + + checkBiometricAvailability(activity, allowDeviceCredentials) + + val authResult = authenticate( + activity, prompt, subtitle, cancel, allowDeviceCredentials, + BiometricPrompt.CryptoObject(cipher) + ) + + val decrypted = withContext(Dispatchers.IO) { + val authenticatedCipher = authResult.cryptoObject?.cipher + ?: throw SecurityException("Authentication failed - no cipher returned") + try { + val encryptedBytes = parsePayload(payload, payloadFormat) + authenticatedCipher.doFinal(encryptedBytes) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid Base64 payload", e) + } + } + + return String(decrypted, Charsets.UTF_8) + } + + private suspend fun decryptHybridEc( + activity: FlutterFragmentActivity, + payload: String, + payloadFormat: PayloadFormat, + prompt: String, + subtitle: String?, + cancel: String, + allowDeviceCredentials: Boolean + ): String { + val cipher = withContext(Dispatchers.IO) { + getCipherForDecryption() + } ?: throw SecurityException("Decryption keys not found") + + checkBiometricAvailability(activity, allowDeviceCredentials) + + val authResult = authenticate( + activity, prompt, subtitle, cancel, allowDeviceCredentials, + BiometricPrompt.CryptoObject(cipher) + ) + + return withContext(Dispatchers.IO) { + val authenticatedCipher = authResult.cryptoObject?.cipher + ?: throw SecurityException("Authentication failed - no cipher returned") + performEciesDecryption(authenticatedCipher, payload, payloadFormat) + } + } + + override fun deleteKeys(callback: (Result) -> Unit) { + deleteAllKeys() + callback(Result.success(true)) + } + + 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 + 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 { + // Try to initialize signature to verify key is not invalidated + val algorithm = when (mode) { + KeyMode.RSA -> "SHA256withRSA" + else -> "SHA256withECDSA" + } + val signature = java.security.Signature.getInstance(algorithm) + signature.initSign(entry.privateKey) + true + }.getOrDefault(false) + } 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() + + // 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) { + val decryptKey = KeyFactory.getInstance("EC").generatePublic( + java.security.spec.X509EncodedKeySpec(pubBytes) + ) + decryptingPublicKey = formatOutput(decryptKey.encoded, keyFormat).value + decryptingAlgorithm = "EC" + 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 + ))) + } catch (e: Exception) { + callback(Result.success(KeyInfo(exists = false))) + } + } + } + + private fun generateRsaKeyInKeyStore( + useDeviceCredentials: Boolean, + invalidateOnEnrollment: Boolean, + enableDecryption: Boolean + ): KeyPair { + val purposes = if (enableDecryption) { + KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_DECRYPT + } else { + KeyProperties.PURPOSE_SIGN + } + + val builder = KeyGenParameterSpec.Builder(BIOMETRIC_KEY_ALIAS, purposes) + .setDigests(KeyProperties.DIGEST_SHA256) + .setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1) + .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) + .setUserAuthenticationRequired(true) + + if (enableDecryption) { + builder.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) + } + + configurePerOperationAuth(builder, useDeviceCredentials) + configureInvalidation(builder, invalidateOnEnrollment) + tryEnableStrongBox(builder) + + val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEYSTORE_PROVIDER) + kpg.initialize(builder.build()) + return kpg.generateKeyPair() + } + + private fun generateEcKeyInKeyStore( + useDeviceCredentials: Boolean, + invalidateOnEnrollment: Boolean + ): KeyPair { + val builder = KeyGenParameterSpec.Builder(BIOMETRIC_KEY_ALIAS, KeyProperties.PURPOSE_SIGN) + .setDigests(KeyProperties.DIGEST_SHA256) + .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) + .setUserAuthenticationRequired(true) + + configurePerOperationAuth(builder, useDeviceCredentials) + configureInvalidation(builder, invalidateOnEnrollment) + tryEnableStrongBox(builder) + + val kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEYSTORE_PROVIDER) + kpg.initialize(builder.build()) + return kpg.generateKeyPair() + } + private fun generateMasterKey(useDeviceCredentials: Boolean, invalidateOnEnrollment: Boolean) { val builder = KeyGenParameterSpec.Builder( MASTER_KEY_ALIAS, @@ -472,17 +585,12 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware configurePerOperationAuth(builder, useDeviceCredentials) configureInvalidation(builder, invalidateOnEnrollment) - // Note: StrongBox intentionally not enabled for master key (compatibility) val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER) keyGen.init(builder.build()) keyGen.generateKey() } - /** - * Returns an AES/GCM Cipher instance initialised for ENCRYPT_MODE with the - * master key stored in AndroidKeyStore under MASTER_KEY_ALIAS. - */ private fun getCipherForEncryption(): Cipher { val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) } val masterKey = keyStore.getKey(MASTER_KEY_ALIAS, null) as? SecretKey @@ -492,12 +600,6 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware return cipher } - /** - * Returns an AES/GCM Cipher instance initialised for DECRYPT_MODE using the - * master key and IV read from the wrapped file (first 12 bytes). - * - * Returns null if there is no wrapped file (i.e. decryption blob missing). - */ private fun getCipherForDecryption(): Cipher? { val wrapped = readFileIfExists(EC_WRAPPED_FILENAME) ?: return null if (wrapped.size < GCM_IV_SIZE + 1) return null @@ -510,16 +612,7 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware return cipher } - /** - * Generate software EC P-256 keypair, encrypt (seal) the private key using the - * provided (already authenticated) cipher, and return (wrappedBlob, publicKeyBytes). - * - * wrappedBlob layout: [IV (12 bytes)] || [ciphertext] - * - * Important: private key raw bytes are zeroed immediately after use. - */ private fun generateAndSealDecryptionEcKeyLocal(cipher: Cipher): Pair { - // Generate software EC keypair (P-256) val kpg = KeyPairGenerator.getInstance("EC") kpg.initialize(ECGenParameterSpec("secp256r1"), SecureRandom()) val keyPair = kpg.generateKeyPair() @@ -530,65 +623,15 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware try { val encrypted = cipher.doFinal(privateKeyBytes) val iv = cipher.iv ?: throw IllegalStateException("Cipher IV missing") - // Build wrapped = iv || encrypted val wrapped = ByteArray(iv.size + encrypted.size) System.arraycopy(iv, 0, wrapped, 0, iv.size) System.arraycopy(encrypted, 0, wrapped, iv.size, encrypted.size) return Pair(wrapped, publicKeyBytes) } finally { - // Zero raw private key bytes ASAP privateKeyBytes.fill(0) } } - // ==================== Create Signature ==================== - private suspend fun createSignature( - call: MethodCall, - result: Result, - activity: FlutterFragmentActivity - ) { - val args = call.arguments>() ?: emptyMap() - val payload = args["payload"] as? String - - if (payload.isNullOrBlank()) { - return result.error(Errors.INVALID_PAYLOAD, "Payload is required", null) - } - - val mode = inferKeyModeFromKeystore() ?: return result.error( - Errors.KEY_NOT_FOUND, - "Signing key not found", - null - ) - val allowDeviceCredentials = args.boolean("allowDeviceCredentials") - val keyFormat = KeyFormat.from(args["keyFormat"] as? String) - - // All modes use the KeyStore key for signing - val (signature, cryptoObject) = withContext(Dispatchers.IO) { - prepareSignature(mode) - } - - checkBiometricAvailability(activity, allowDeviceCredentials) - - val authResult = authenticate( - activity, - args["promptMessage"] as? String ?: "Authenticate", - args["subtitle"] as? String, - args["cancelButtonText"] as? String ?: "Cancel", - allowDeviceCredentials, - cryptoObject - ) - - val signatureBytes = withContext(Dispatchers.IO) { - val sig = authResult.cryptoObject?.signature ?: signature - sig.update(payload.toByteArray(Charsets.UTF_8)) - sig.sign() - } - - val publicKey = getSigningPublicKey() - val response = buildSignatureResponse(signatureBytes, publicKey, keyFormat, mode) - result.success(response) - } - private fun prepareSignature(mode: KeyMode): Pair { val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) } val entry = keyStore.getEntry(BIOMETRIC_KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry @@ -604,7 +647,6 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware signature.initSign(entry.privateKey) Pair(signature, BiometricPrompt.CryptoObject(signature)) } catch (e: Exception) { - // Fallback to non-crypto prompt: signature object returned but cryptoObject null Pair(signature, null) } } @@ -616,140 +658,25 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware return entry.certificate.publicKey } - // ==================== Decrypt ==================== - private suspend fun decrypt( - call: MethodCall, - result: Result, - activity: FlutterFragmentActivity - ) { - val args = call.arguments>() ?: emptyMap() - val payload = args["payload"] as? String - - if (payload.isNullOrBlank()) { - return result.error(Errors.INVALID_PAYLOAD, "Payload is required", null) - } - - val mode = inferKeyModeFromKeystore() - ?: return result.error(Errors.KEY_NOT_FOUND, "Keys not found", null) - - if (mode == KeyMode.EC_SIGN_ONLY) { - return result.error( - Errors.DECRYPTION_NOT_ENABLED, - "Decryption not enabled for EC signing-only mode", - null - ) - } - - val allowDeviceCredentials = args.boolean("allowDeviceCredentials") - when (mode) { - KeyMode.RSA -> decryptRsa(activity, result, payload, args, allowDeviceCredentials) - KeyMode.HYBRID_EC -> decryptHybridEc( - activity, - result, - payload, - args, - allowDeviceCredentials - ) - - else -> result.error(Errors.DECRYPTION_NOT_ENABLED, "Unsupported decryption mode", null) - } - } - - private suspend fun decryptRsa( - activity: FlutterFragmentActivity, - result: Result, - payload: String, - args: Map, - allowDeviceCredentials: Boolean - ) { - val cipher = withContext(Dispatchers.IO) { - val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) } - val entry = keyStore.getEntry(BIOMETRIC_KEY_ALIAS, null) as? KeyStore.PrivateKeyEntry - ?: throw IllegalStateException("RSA key not found") - - Cipher.getInstance("RSA/ECB/PKCS1Padding").apply { - init(Cipher.DECRYPT_MODE, entry.privateKey) - } - } - - checkBiometricAvailability(activity, allowDeviceCredentials) - - val authResult = authenticate( - activity, - args["promptMessage"] as? String ?: "Authenticate", - args["subtitle"] as? String, - args["cancelButtonText"] as? String ?: "Cancel", - allowDeviceCredentials, - BiometricPrompt.CryptoObject(cipher) - ) - - val decrypted = withContext(Dispatchers.IO) { - val authenticatedCipher = authResult.cryptoObject?.cipher - ?: throw SecurityException("Authentication failed - no cipher returned") - val encryptedBytes = Base64.decode(payload, Base64.NO_WRAP) - authenticatedCipher.doFinal(encryptedBytes) - } - - result.success(mapOf("decryptedData" to String(decrypted, Charsets.UTF_8))) - } - - private suspend fun decryptHybridEc( - activity: FlutterFragmentActivity, - result: Result, - payload: String, - args: Map, - allowDeviceCredentials: Boolean - ) { - // Prepare cipher to unwrap EC key from wrapped file - val cipher = withContext(Dispatchers.IO) { - getCipherForDecryption() - } ?: return result.error(Errors.KEY_NOT_FOUND, "Decryption keys not found", null) - - checkBiometricAvailability(activity, allowDeviceCredentials) - - val authResult = authenticate( - activity, - args["promptMessage"] as? String ?: "Authenticate", - args["subtitle"] as? String, - args["cancelButtonText"] as? String ?: "Cancel", - allowDeviceCredentials, - BiometricPrompt.CryptoObject(cipher) - ) - - val decrypted = withContext(Dispatchers.IO) { - val authenticatedCipher = authResult.cryptoObject?.cipher - ?: throw SecurityException("Authentication failed - no cipher returned") - performEciesDecryption(authenticatedCipher, payload) - } - - result.success(mapOf("decryptedData" to decrypted)) - } - - /** - * ECIES decryption implementation that un-wraps the encrypted EC private key using the authenticated - * AES master key (cipher). Sensitive material is zeroized ASAP. - * - * Payload format: [ephemeral_public_key || ciphertext || auth_tag] (all base64-encoded input) - */ - private fun performEciesDecryption(unwrapCipher: Cipher, payloadBase64: String): String { - // 1. Read wrapped blob file + 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") - // Split wrapped: iv || encryptedPrivateKey val encryptedKey = wrapped.copyOfRange(GCM_IV_SIZE, wrapped.size) var privateKeyBytes: ByteArray? = null try { - // decrypt using the provided authenticated cipher privateKeyBytes = unwrapCipher.doFinal(encryptedKey) val privateKey: PrivateKey = KeyFactory.getInstance("EC") .generatePrivate(PKCS8EncodedKeySpec(privateKeyBytes)) - // 2. Parse ECIES payload - val data = Base64.decode(payloadBase64, Base64.NO_WRAP) + val data = try { + parsePayload(payload, format) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid payload", e) + } require(data.size >= EC_PUBKEY_SIZE + GCM_TAG_BYTES) { "Invalid ECIES payload: too short (${data.size} bytes)" } @@ -761,18 +688,15 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware val ciphertextWithTag = data.copyOfRange(EC_PUBKEY_SIZE, data.size) - // 3. Reconstruct ephemeral public key val ephemeralPubKey = KeyFactory.getInstance("EC") .generatePublic(X509EncodedKeySpec(createX509ForRawEcPub(ephemeralKeyBytes))) - // 4. ECDH val sharedSecret: ByteArray = KeyAgreement.getInstance("ECDH").run { init(privateKey) doPhase(ephemeralPubKey, true) generateSecret() } - // 5. KDF -> AES key + IV val derived: ByteArray = try { kdfX963(sharedSecret, AES_KEY_SIZE + GCM_IV_SIZE) } finally { @@ -783,7 +707,6 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware val gcmIv = derived.copyOfRange(AES_KEY_SIZE, AES_KEY_SIZE + GCM_IV_SIZE) derived.fill(0) - // 6. AES-GCM decrypt try { val aesKey = SecretKeySpec(aesKeyBytes, "AES") val decrypted = Cipher.getInstance("AES/GCM/NoPadding").run { @@ -796,14 +719,11 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware gcmIv.fill(0) } } finally { - // Zero raw private key bytes privateKeyBytes?.fill(0) - encryptedKey.fill(0) } } private fun createX509ForRawEcPub(raw: ByteArray): ByteArray { - // X.509 header for P-256 followed by raw uncompressed point val header = byteArrayOf( 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A.toByte(), 0x86.toByte(), 0x48.toByte(), 0xCE.toByte(), 0x3D.toByte(), 0x02.toByte(), 0x01.toByte(), @@ -843,72 +763,20 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware return result } - // ==================== Delete Keys ==================== - private suspend fun deleteKeys(result: Result) { - withContext(Dispatchers.IO) { deleteAllKeys() } - result.success(true) - } - private fun deleteAllKeys() { - // Delete Keystore entries val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) } runCatching { keyStore.deleteEntry(BIOMETRIC_KEY_ALIAS) } runCatching { keyStore.deleteEntry(MASTER_KEY_ALIAS) } - // Delete files with secure overwrite listOf(EC_WRAPPED_FILENAME, EC_PUB_FILENAME).forEach { fileName -> val file = File(appContext.filesDir, fileName) if (file.exists()) { - // Overwrite with zeros before deletion - runCatching { - file.writeBytes(ByteArray(file.length().toInt())) - } + runCatching { file.writeBytes(ByteArray(file.length().toInt())) } file.delete() } } } - // ==================== Biometric Availability ==================== - private fun getBiometricAvailability(): String { - val act = activity ?: return "none, NO_ACTIVITY" - val manager = BiometricManager.from(act) - val canAuth = manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) - return if (canAuth == BiometricManager.BIOMETRIC_SUCCESS) { - detectBiometricType() - } else { - "none, ${biometricErrorName(canAuth)}" - } - } - - private fun detectBiometricType(): String { - var identifiedFingerprint = false - val pm = appContext.packageManager - - 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 - } - - val otherString = listOf("face", "iris", ",") - val otherBiometrics = otherString.filter { - BiometricManager.from(activity!!) - .getStrings(BiometricManager.Authenticators.BIOMETRIC_STRONG)?.buttonLabel - .toString().contains(it, ignoreCase = true) - } - - return if (identifiedFingerprint) { - if (otherBiometrics.isEmpty()) "fingerprint" else "biometric" - } else { - if (otherBiometrics.size == 1 && otherBiometrics[0] != ",") otherBiometrics[0] else "biometric" - } - } - - private fun biometricErrorName(code: Int) = when (code) { BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> "BIOMETRIC_ERROR_NO_HARDWARE" BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> "BIOMETRIC_ERROR_HW_UNAVAILABLE" @@ -919,8 +787,7 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware else -> "UNKNOWN_ERROR" } - // ==================== Key Exists ==================== - private fun checkKeyExists(checkValidity: Boolean): Boolean { + private fun checkKeyExistsInternal(checkValidity: Boolean): Boolean { val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) } if (!keyStore.containsAlias(BIOMETRIC_KEY_ALIAS)) return false if (!checkValidity) return true @@ -931,17 +798,11 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware }.getOrDefault(false) } - /** - * Infer key mode from Keystore and presence of wrapped blob file. - */ 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 is ECPublicKey -> { @@ -952,7 +813,6 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware } } - // ==================== Authentication ==================== private suspend fun checkBiometricAvailability( activity: FragmentActivity, allowDeviceCredentials: Boolean @@ -960,7 +820,7 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware val authenticators = getAuthenticators(allowDeviceCredentials) val canAuth = BiometricManager.from(activity).canAuthenticate(authenticators) if (canAuth != BiometricManager.BIOMETRIC_SUCCESS) { - throw SecurityException("Biometric not available (code: $canAuth)") + throw SecurityException("Biometric not available (code: ${biometricErrorName(canAuth)})") } } @@ -972,20 +832,18 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware allowDeviceCredentials: Boolean, cryptoObject: BiometricPrompt.CryptoObject? ): BiometricPrompt.AuthenticationResult = suspendCancellableCoroutine { cont -> - val authenticators = getAuthenticators(allowDeviceCredentials) - 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 (code: $errorCode)")) - } - - override fun onAuthenticationFailed() { /* User can retry */ + if (cont.isActive) { + // Map error codes to standard exceptions if needed, or pass raw + cont.resumeWithException(SecurityException("$errString", Throwable(errorCode.toString()))) + } } + override fun onAuthenticationFailed() { /* Retry */ } } val promptInfo = BiometricPrompt.PromptInfo.Builder() @@ -1001,16 +859,10 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware runCatching { activity.setTheme(androidx.appcompat.R.style.Theme_AppCompat_Light_DarkActionBar) - 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) - } + 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) } } private fun getAuthenticators(allowDeviceCredentials: Boolean): Int { @@ -1021,11 +873,7 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware } } - // ==================== Key Generation Helpers ==================== - 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 @@ -1038,111 +886,192 @@ class BiometricSignaturePlugin : FlutterPlugin, MethodCallHandler, ActivityAware } } - 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) } } - // ==================== Response Builders ==================== + 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) {} + } + } private fun buildKeyResponse( publicKey: PublicKey, format: KeyFormat, - algorithm: String - ): Map { + decryptingKey: PublicKey? = null + ): KeyCreationResult { val formatted = formatOutput(publicKey.encoded, format) - val keySize = when (publicKey) { - is RSAPublicKey -> publicKey.modulus.bitLength() - is ECPublicKey -> publicKey.params.order.bitLength() - else -> 0 + val keySize = (publicKey as? java.security.interfaces.RSAKey)?.modulus?.bitLength() + ?: (publicKey as? java.security.interfaces.ECKey)?.params?.order?.bitLength() + + var decryptingFormatted: FormattedOutput? = null + var decryptingAlgorithm: String? = null + var decryptingKeySize: Long? = null + + 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() } - return hashMapOf( - "publicKey" to formatted.value, - "publicKeyFormat" to formatted.format.name, - "algorithm" to algorithm, - "keySize" to keySize, - "keyFormat" to format.name - ).apply { - formatted.pemLabel?.let { put("publicKeyPemLabel", it) } - } + return KeyCreationResult( + publicKey = formatted.value, + publicKeyBytes = publicKey.encoded, + code = BiometricError.SUCCESS, + algorithm = publicKey.algorithm, + keySize = keySize?.toLong(), + decryptingPublicKey = decryptingFormatted?.value, + decryptingAlgorithm = decryptingAlgorithm, + decryptingKeySize = decryptingKeySize, + isHybridMode = decryptingKey != null + ) } private fun buildSignatureResponse( signatureBytes: ByteArray, publicKey: PublicKey, - format: KeyFormat, - mode: KeyMode - ): Map { - val sigFormatted = formatOutput(signatureBytes, format, "SIGNATURE") - val pubFormatted = formatOutput(publicKey.encoded, format) - - val algorithm = if (mode == KeyMode.RSA) "RSA" else "EC" - val keySize = when (publicKey) { - is RSAPublicKey -> publicKey.modulus.bitLength() - is ECPublicKey -> publicKey.params.order.bitLength() - else -> 0 + format: SignatureFormat, + keyFormat: KeyFormat + ): SignatureResult { + + // Format signature explicitly based on SignatureFormat + val sigString = when(format) { + SignatureFormat.BASE64, SignatureFormat.RAW -> Base64.encodeToString(signatureBytes, Base64.NO_WRAP) + SignatureFormat.HEX -> bytesToHex(signatureBytes) } - return hashMapOf( - "signature" to sigFormatted.value, - "signatureFormat" to sigFormatted.format.name, - "publicKey" to pubFormatted.value, - "publicKeyFormat" to pubFormatted.format.name, - "algorithm" to algorithm, - "keySize" to keySize, - "timestamp" to isoTimestamp() - ).apply { - sigFormatted.pemLabel?.let { put("signaturePemLabel", it) } - pubFormatted.pemLabel?.let { put("publicKeyPemLabel", it) } - } + val pubFormatted = formatOutput(publicKey.encoded, keyFormat) + val keySize = (publicKey as? java.security.interfaces.RSAKey)?.modulus?.bitLength() + ?: (publicKey as? java.security.interfaces.ECKey)?.params?.order?.bitLength() + + return SignatureResult( + signature = sigString, + signatureBytes = signatureBytes, + publicKey = pubFormatted.value, + code = BiometricError.SUCCESS, + algorithm = publicKey.algorithm, + keySize = keySize?.toLong() + ) } - // ==================== Formatting ==================== - 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(bytes.toBase64(), format) - KeyFormat.HEX -> FormattedOutput(bytes.joinToString("") { "%02x".format(it) }, format) - KeyFormat.RAW -> FormattedOutput(bytes, format) + KeyFormat.BASE64 -> FormattedOutput(Base64.encodeToString(bytes, Base64.NO_WRAP), format) KeyFormat.PEM -> FormattedOutput( - "-----BEGIN $label-----\n${ - bytes.toBase64().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) } - private fun isoTimestamp(): String = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US) - .apply { timeZone = TimeZone.getTimeZone("UTC") } - .format(Date()) + private fun parsePayload(payload: String, format: PayloadFormat): ByteArray { + return when (format) { + PayloadFormat.BASE64, PayloadFormat.RAW -> Base64.decode(payload, Base64.NO_WRAP) + PayloadFormat.HEX -> hexToBytes(payload) + } + } - // ==================== I/O Helpers ==================== + private fun bytesToHex(bytes: ByteArray): String { + return bytes.joinToString("") { "%02x".format(it) } + } - private fun writeFileAtomic(fileName: String, data: ByteArray) { - File(appContext.filesDir, fileName).outputStream().use { it.write(data) } + private fun hexToBytes(hex: String): ByteArray { + val cleanHex = if (hex.length % 2 != 0) "0$hex" else hex + return cleanHex.chunked(2) + .map { it.toInt(16).toByte() } + .toByteArray() } - private fun readFileIfExists(fileName: String): ByteArray? { - val file = File(appContext.filesDir, fileName) - return if (!file.exists()) null else file.readBytes() + private fun detectBiometricTypes(): Pair, String?> { + var identifiedFingerprint = false + val pm = appContext.packageManager + + 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 + } + + val otherString = listOf("face", "iris", ",") + val biometricManager = BiometricManager.from(activity!!) + + // 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) + if (strings != null) { + val getButtonLabelMethod = strings.javaClass.getMethod("getButtonLabel") + buttonLabel = getButtonLabelMethod.invoke(strings) as? String + } + } catch (e: Exception) { + // Reflection failed or method not found, ignore + } + + val otherBiometrics = otherString.filter { + buttonLabel?.contains(it, ignoreCase = true) == true + } + + val resultString = if (identifiedFingerprint) { + if (otherBiometrics.isEmpty()) "fingerprint" else "biometric" + } else { + if (otherBiometrics.size == 1 && otherBiometrics[0] != ",") otherBiometrics[0] else "biometric" + } + + // 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) + } + + return Pair(types, null) } - // ==================== Extensions ==================== + private fun mapToBiometricError(e: Throwable): BiometricError { + // Map exceptions to BiometricError + val msg = e.message ?: "" + return when { + msg.contains("BIOMETRIC_ERROR_NONE_ENROLLED") -> BiometricError.NOT_ENROLLED + msg.contains("BIOMETRIC_ERROR_NO_HARDWARE") -> BiometricError.NOT_AVAILABLE + msg.contains("BIOMETRIC_ERROR_HW_UNAVAILABLE") -> BiometricError.NOT_AVAILABLE + // User canceled + // e.cause might contain code + e.cause?.message == "10" -> BiometricError.USER_CANCELED // 10 is USER_CANCELED usually + e.cause?.message == "13" -> BiometricError.USER_CANCELED // Negative button + + // Map simple Cancellation + e is CancellationException -> BiometricError.USER_CANCELED - private fun ByteArray.toBase64(): String = Base64.encodeToString(this, Base64.NO_WRAP) + e is IllegalArgumentException && (e.message?.contains("Base64") == true || e.message?.contains("payload") == true) -> BiometricError.INVALID_INPUT + + else -> BiometricError.UNKNOWN + } + } - private fun Map.boolean(key: String): Boolean = when (val v = this[key]) { - is Boolean -> v - is String -> v.equals("true", ignoreCase = true) - else -> false + 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/.metadata b/banking_app/.metadata index fca9f99..2e80cfe 100644 --- a/banking_app/.metadata +++ b/banking_app/.metadata @@ -15,7 +15,7 @@ migration: - platform: root create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - - platform: macos + - platform: windows create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 diff --git a/banking_app/ios/Podfile.lock b/banking_app/ios/Podfile.lock index 631d3ba..9136fca 100644 --- a/banking_app/ios/Podfile.lock +++ b/banking_app/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - biometric_signature (8.5.0): + - biometric_signature (9.0.0): - Flutter - Flutter (1.0.0) - shared_preferences_foundation (0.0.1): @@ -20,7 +20,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: - biometric_signature: 8e36f1308ce31fd274982403728b2337913efba4 + biometric_signature: e3da6b76957b6a9cd71e62c1664be9174d4a2d80 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 diff --git a/banking_app/ios/Runner.xcodeproj/project.pbxproj b/banking_app/ios/Runner.xcodeproj/project.pbxproj index 3320f05..e331466 100644 --- a/banking_app/ios/Runner.xcodeproj/project.pbxproj +++ b/banking_app/ios/Runner.xcodeproj/project.pbxproj @@ -97,7 +97,6 @@ 7690CFADC5ECD78F60F24408 /* Pods-RunnerTests.release.xcconfig */, 7B2EE3B8D213F6B6D5E872DF /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -316,10 +315,14 @@ 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"; @@ -471,7 +474,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Y8K9F7TMXG; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -654,7 +657,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Y8K9F7TMXG; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -677,7 +680,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Y8K9F7TMXG; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/banking_app/lib/services/biometric_service.dart b/banking_app/lib/services/biometric_service.dart index 87b61a0..315fd47 100644 --- a/banking_app/lib/services/biometric_service.dart +++ b/banking_app/lib/services/biometric_service.dart @@ -1,4 +1,7 @@ -import 'package:biometric_signature/biometric_signature.dart'; +import 'package:biometric_signature/biometric_signature.dart' + hide BiometricAvailability; +import 'package:biometric_signature/biometric_signature.dart' as plugin + show BiometricAvailability; import 'package:flutter/services.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -11,14 +14,21 @@ class BiometricService { try { final result = await _biometric.biometricAuthAvailable(); print('result: $result'); - if (result == null || result.contains('none,')) { + if (!(result.canAuthenticate ?? false)) { return BiometricAvailability( isAvailable: false, biometricType: 'none', - errorMessage: result, + errorMessage: result.reason, ); } - return BiometricAvailability(isAvailable: true, biometricType: result); + final biometricTypes = (result.availableBiometrics ?? []) + .map((b) => b?.name ?? '') + .where((s) => s.isNotEmpty) + .join(','); + return BiometricAvailability( + isAvailable: true, + biometricType: biometricTypes.isNotEmpty ? biometricTypes : 'biometric', + ); } catch (e) { return BiometricAvailability( isAvailable: false, @@ -32,24 +42,22 @@ class BiometricService { Future initializeKeys() async { try { final keyResult = await _biometric.createKeys( - androidConfig: AndroidConfig( - useDeviceCredentials: false, - signatureType: AndroidSignatureType.RSA, - ), - iosConfig: IosConfig( + keyFormat: KeyFormat.base64, + config: CreateKeysConfig( useDeviceCredentials: false, - signatureType: IOSSignatureType.RSA, + signatureType: SignatureType.rsa, + enforceBiometric: true, ), ); - if (keyResult != null) { - final publicKey = keyResult.publicKey.toBase64(); + final publicKey = keyResult.publicKey; + if (publicKey != null) { // Store public key for future reference final prefs = await SharedPreferences.getInstance(); await prefs.setString(_publicKeyKey, publicKey); return publicKey; } - throw Exception('Failed to generate keys'); + throw Exception('Failed to generate keys: ${keyResult.error}'); } on PlatformException catch (e) { throw Exception('Biometric key creation failed: ${e.message}'); } @@ -59,7 +67,7 @@ class BiometricService { Future hasKeys() async { try { final exists = await _biometric.biometricKeyExists(checkValidity: true); - return exists ?? false; + return exists; } catch (e) { return false; } @@ -75,21 +83,22 @@ class BiometricService { Future signData(String payload, String promptMessage) async { try { final signatureResult = await _biometric.createSignature( - SignatureOptions( - payload: payload, - promptMessage: promptMessage, - androidOptions: const AndroidSignatureOptions( - cancelButtonText: 'Cancel', - allowDeviceCredentials: false, - ), - iosOptions: const IosSignatureOptions(shouldMigrate: false), + payload: payload, + promptMessage: promptMessage, + signatureFormat: SignatureFormat.base64, + keyFormat: KeyFormat.base64, + config: CreateSignatureConfig( + cancelButtonText: 'Cancel', + allowDeviceCredentials: false, + shouldMigrate: false, ), ); - if (signatureResult != null) { - return signatureResult.signature.toBase64(); + final signature = signatureResult.signature; + if (signature != null) { + return signature; } - throw Exception('Failed to create signature'); + throw Exception('Failed to create signature: ${signatureResult.error}'); } on PlatformException catch (e) { if (e.code == 'AUTH_FAILED') { throw Exception('Authentication failed: ${e.message}'); @@ -108,7 +117,7 @@ class BiometricService { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_publicKeyKey); } - return result ?? false; + return result; } catch (e) { return false; } diff --git a/banking_app/macos/Podfile.lock b/banking_app/macos/Podfile.lock index ee05a00..1add8a4 100644 --- a/banking_app/macos/Podfile.lock +++ b/banking_app/macos/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - biometric_signature (8.5.0): + - biometric_signature (9.0.0): - FlutterMacOS - FlutterMacOS (1.0.0) - shared_preferences_foundation (0.0.1): @@ -20,7 +20,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin SPEC CHECKSUMS: - biometric_signature: 1effe0546fb383befaac2b96a418f97571b92555 + biometric_signature: fcc23fe926798544af948f78621af1e85b35d372 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 diff --git a/banking_app/pubspec.lock b/banking_app/pubspec.lock index 8371468..56a5189 100644 --- a/banking_app/pubspec.lock +++ b/banking_app/pubspec.lock @@ -15,7 +15,7 @@ packages: path: ".." relative: true source: path - version: "8.5.0" + version: "9.0.0" boolean_selector: dependency: transitive description: diff --git a/banking_app/windows/.gitignore b/banking_app/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/banking_app/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/banking_app/windows/CMakeLists.txt b/banking_app/windows/CMakeLists.txt new file mode 100644 index 0000000..2fa3613 --- /dev/null +++ b/banking_app/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(banking_app_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "banking_app_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/banking_app/windows/flutter/CMakeLists.txt b/banking_app/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/banking_app/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/banking_app/windows/flutter/generated_plugin_registrant.cc b/banking_app/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..c777059 --- /dev/null +++ b/banking_app/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + BiometricSignaturePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BiometricSignaturePlugin")); +} diff --git a/banking_app/windows/flutter/generated_plugin_registrant.h b/banking_app/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/banking_app/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/banking_app/windows/flutter/generated_plugins.cmake b/banking_app/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..5c7dd30 --- /dev/null +++ b/banking_app/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + biometric_signature +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/banking_app/windows/runner/CMakeLists.txt b/banking_app/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/banking_app/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/banking_app/windows/runner/Runner.rc b/banking_app/windows/runner/Runner.rc new file mode 100644 index 0000000..7a00ccd --- /dev/null +++ b/banking_app/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "banking_app_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "banking_app_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "banking_app_example.exe" "\0" + VALUE "ProductName", "banking_app_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/banking_app/windows/runner/flutter_window.cpp b/banking_app/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/banking_app/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/banking_app/windows/runner/flutter_window.h b/banking_app/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/banking_app/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/banking_app/windows/runner/main.cpp b/banking_app/windows/runner/main.cpp new file mode 100644 index 0000000..d7b24ff --- /dev/null +++ b/banking_app/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"banking_app_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/banking_app/windows/runner/resource.h b/banking_app/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/banking_app/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/banking_app/windows/runner/resources/app_icon.ico b/banking_app/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/banking_app/windows/runner/resources/app_icon.ico differ diff --git a/banking_app/windows/runner/runner.exe.manifest b/banking_app/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/banking_app/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/banking_app/windows/runner/utils.cpp b/banking_app/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/banking_app/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/banking_app/windows/runner/utils.h b/banking_app/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/banking_app/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/banking_app/windows/runner/win32_window.cpp b/banking_app/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/banking_app/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/banking_app/windows/runner/win32_window.h b/banking_app/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/banking_app/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/document_signer/.metadata b/document_signer/.metadata index fca9f99..2e80cfe 100644 --- a/document_signer/.metadata +++ b/document_signer/.metadata @@ -15,7 +15,7 @@ migration: - platform: root create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 - - platform: macos + - platform: windows create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 diff --git a/document_signer/ios/Podfile.lock b/document_signer/ios/Podfile.lock index 278dad9..aba2530 100644 --- a/document_signer/ios/Podfile.lock +++ b/document_signer/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - biometric_signature (8.5.0): + - biometric_signature (9.0.0): - Flutter - Flutter (1.0.0) - shared_preferences_foundation (0.0.1): @@ -20,7 +20,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: - biometric_signature: 8e36f1308ce31fd274982403728b2337913efba4 + biometric_signature: e3da6b76957b6a9cd71e62c1664be9174d4a2d80 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 diff --git a/document_signer/ios/Runner.xcodeproj/project.pbxproj b/document_signer/ios/Runner.xcodeproj/project.pbxproj index a6cc54e..15f136b 100644 --- a/document_signer/ios/Runner.xcodeproj/project.pbxproj +++ b/document_signer/ios/Runner.xcodeproj/project.pbxproj @@ -114,7 +114,6 @@ 199AA0489C4E5E92FB232D4F /* Pods-RunnerTests.release.xcconfig */, 776B7659A300063AD7B94864 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -353,10 +352,14 @@ 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"; @@ -471,7 +474,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Y8K9F7TMXG; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -654,7 +657,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Y8K9F7TMXG; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -677,7 +680,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Y8K9F7TMXG; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/document_signer/lib/screens/home_screen.dart b/document_signer/lib/screens/home_screen.dart index 352c921..ca95a43 100644 --- a/document_signer/lib/screens/home_screen.dart +++ b/document_signer/lib/screens/home_screen.dart @@ -215,16 +215,15 @@ class _HomeScreenState extends State { } void _openDocument(Document doc) async { - final result = await Navigator.push( + await Navigator.push( context, MaterialPageRoute( builder: (context) => DocumentDetailScreen(document: doc), ), ); - if (result == true) { - await _loadDocuments(); - } + // Refresh list on return to update signed status + await _loadDocuments(); } void _createDocument() async { diff --git a/document_signer/lib/services/signature_service.dart b/document_signer/lib/services/signature_service.dart index 18ab940..60bf66c 100644 --- a/document_signer/lib/services/signature_service.dart +++ b/document_signer/lib/services/signature_service.dart @@ -16,23 +16,21 @@ class SignatureService { Future initializeKeys() async { try { final keyResult = await _biometric.createKeys( - androidConfig: AndroidConfig( + keyFormat: KeyFormat.base64, + config: CreateKeysConfig( useDeviceCredentials: false, - signatureType: AndroidSignatureType.RSA, - ), - iosConfig: IosConfig( - useDeviceCredentials: false, - signatureType: IOSSignatureType.RSA, + signatureType: SignatureType.rsa, + enforceBiometric: true, ), ); - if (keyResult != null) { - final publicKey = keyResult.publicKey.toBase64(); + final publicKey = keyResult.publicKey; + if (publicKey != null) { final prefs = await SharedPreferences.getInstance(); await prefs.setString(_publicKeyKey, publicKey); return publicKey; } - throw Exception('Failed to generate keys'); + throw Exception('Failed to generate keys: ${keyResult.error}'); } catch (e) { throw Exception('Key initialization failed: $e'); } @@ -42,7 +40,7 @@ class SignatureService { Future hasKeys() async { try { final exists = await _biometric.biometricKeyExists(checkValidity: true); - return exists ?? false; + return exists; } catch (e) { return false; } @@ -76,9 +74,9 @@ class SignatureService { /// Sign a document Future signDocument(Document document) async { try { - // Get biometric type - final biometricType = await _biometric.biometricAuthAvailable(); - if (biometricType == null || biometricType.contains('none,')) { + // Get biometric availability + final availability = await _biometric.biometricAuthAvailable(); + if (!(availability.canAuthenticate ?? false)) { throw Exception('Biometric authentication not available'); } @@ -99,23 +97,22 @@ class SignatureService { // Sign with biometric final signatureResult = await _biometric.createSignature( - SignatureOptions( - payload: payload, - promptMessage: 'Sign "${document.title}"', - androidOptions: const AndroidSignatureOptions( - cancelButtonText: 'Cancel', - allowDeviceCredentials: false, - ), - iosOptions: const IosSignatureOptions(shouldMigrate: false), + payload: payload, + promptMessage: 'Sign "${document.title}"', + signatureFormat: SignatureFormat.base64, + keyFormat: KeyFormat.base64, + config: CreateSignatureConfig( + cancelButtonText: 'Cancel', + allowDeviceCredentials: false, + shouldMigrate: false, ), ); - if (signatureResult == null) { - throw Exception('Failed to create signature'); + final signatureValue = signatureResult.signature; + if (signatureValue == null) { + throw Exception('Failed to create signature: ${signatureResult.error}'); } - final signatureValue = signatureResult.signature.toBase64(); - // Get signer info final publicKey = await getPublicKey(); final signerName = await getSignerName(); @@ -124,12 +121,18 @@ class SignatureService { throw Exception('Public key not found'); } + // Format biometric type string + final biometricType = (availability.availableBiometrics ?? []) + .map((b) => b?.name ?? '') + .where((s) => s.isNotEmpty) + .join(','); + return SignatureInfo( signatureValue: signatureValue, timestamp: DateTime.now(), signerPublicKey: publicKey, documentHash: documentHash, - biometricType: biometricType, + biometricType: biometricType.isNotEmpty ? biometricType : 'biometric', signerName: signerName, ); } on PlatformException catch (e) { @@ -169,7 +172,7 @@ class SignatureService { Future isBiometricAvailable() async { try { final result = await _biometric.biometricAuthAvailable(); - return result != null && !result.contains('none,'); + return result.canAuthenticate ?? false; } catch (e) { return false; } @@ -183,7 +186,7 @@ class SignatureService { final prefs = await SharedPreferences.getInstance(); await prefs.remove(_publicKeyKey); } - return result ?? false; + return result; } catch (e) { return false; } diff --git a/document_signer/macos/Podfile.lock b/document_signer/macos/Podfile.lock index ee05a00..1add8a4 100644 --- a/document_signer/macos/Podfile.lock +++ b/document_signer/macos/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - biometric_signature (8.5.0): + - biometric_signature (9.0.0): - FlutterMacOS - FlutterMacOS (1.0.0) - shared_preferences_foundation (0.0.1): @@ -20,7 +20,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin SPEC CHECKSUMS: - biometric_signature: 1effe0546fb383befaac2b96a418f97571b92555 + biometric_signature: fcc23fe926798544af948f78621af1e85b35d372 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 diff --git a/document_signer/macos/Runner.xcodeproj/project.pbxproj b/document_signer/macos/Runner.xcodeproj/project.pbxproj index 5530512..fd860fc 100644 --- a/document_signer/macos/Runner.xcodeproj/project.pbxproj +++ b/document_signer/macos/Runner.xcodeproj/project.pbxproj @@ -330,10 +330,14 @@ 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"; diff --git a/document_signer/pubspec.lock b/document_signer/pubspec.lock index 302e5da..384eeb9 100644 --- a/document_signer/pubspec.lock +++ b/document_signer/pubspec.lock @@ -15,7 +15,7 @@ packages: path: ".." relative: true source: path - version: "8.5.0" + version: "9.0.0" boolean_selector: dependency: transitive description: diff --git a/document_signer/windows/.gitignore b/document_signer/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/document_signer/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/document_signer/windows/CMakeLists.txt b/document_signer/windows/CMakeLists.txt new file mode 100644 index 0000000..bc5c032 --- /dev/null +++ b/document_signer/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(document_signer_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "document_signer_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/document_signer/windows/flutter/CMakeLists.txt b/document_signer/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/document_signer/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/document_signer/windows/flutter/generated_plugin_registrant.cc b/document_signer/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..c777059 --- /dev/null +++ b/document_signer/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + BiometricSignaturePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BiometricSignaturePlugin")); +} diff --git a/document_signer/windows/flutter/generated_plugin_registrant.h b/document_signer/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/document_signer/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/document_signer/windows/flutter/generated_plugins.cmake b/document_signer/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..5c7dd30 --- /dev/null +++ b/document_signer/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + biometric_signature +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/document_signer/windows/runner/CMakeLists.txt b/document_signer/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/document_signer/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/document_signer/windows/runner/Runner.rc b/document_signer/windows/runner/Runner.rc new file mode 100644 index 0000000..292391f --- /dev/null +++ b/document_signer/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "document_signer_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "document_signer_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "document_signer_example.exe" "\0" + VALUE "ProductName", "document_signer_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/document_signer/windows/runner/flutter_window.cpp b/document_signer/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/document_signer/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/document_signer/windows/runner/flutter_window.h b/document_signer/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/document_signer/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/document_signer/windows/runner/main.cpp b/document_signer/windows/runner/main.cpp new file mode 100644 index 0000000..c8f9e92 --- /dev/null +++ b/document_signer/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"document_signer_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/document_signer/windows/runner/resource.h b/document_signer/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/document_signer/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/document_signer/windows/runner/resources/app_icon.ico b/document_signer/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/document_signer/windows/runner/resources/app_icon.ico differ diff --git a/document_signer/windows/runner/runner.exe.manifest b/document_signer/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/document_signer/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/document_signer/windows/runner/utils.cpp b/document_signer/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/document_signer/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/document_signer/windows/runner/utils.h b/document_signer/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/document_signer/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/document_signer/windows/runner/win32_window.cpp b/document_signer/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/document_signer/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/document_signer/windows/runner/win32_window.h b/document_signer/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/document_signer/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..2e80cfe --- /dev/null +++ b/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: windows + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 653cc90..56c8e56 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - biometric_signature (8.5.0): + - biometric_signature (9.0.0): - Flutter - Flutter (1.0.0) - integration_test (0.0.1): @@ -19,7 +19,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/integration_test/ios" SPEC CHECKSUMS: - biometric_signature: 8e36f1308ce31fd274982403728b2337913efba4 + biometric_signature: e3da6b76957b6a9cd71e62c1664be9174d4a2d80 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index c283747..b1cc763 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -258,10 +258,14 @@ 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"; @@ -428,8 +432,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Y8K9F7TMXG; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -609,8 +614,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Y8K9F7TMXG; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -633,8 +639,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = Y8K9F7TMXG; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; diff --git a/example/lib/main.dart b/example/lib/main.dart index fbefd22..d9c9dd2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,11 +1,11 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'dart:typed_data'; import 'package:biometric_signature/biometric_signature.dart'; import 'package:encrypt/encrypt.dart' as enc; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:pointycastle/asn1/asn1_parser.dart'; import 'package:pointycastle/asn1/primitives/asn1_bit_string.dart'; import 'package:pointycastle/asn1/primitives/asn1_sequence.dart'; @@ -23,7 +23,7 @@ class MyApp extends StatelessWidget { return MaterialApp( theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue), home: Scaffold( - appBar: AppBar(title: const Text('Biometric Signature Test')), + appBar: AppBar(title: const Text('Biometric Signature v9.0.0')), body: const ExampleAppBody(), ), ); @@ -40,212 +40,131 @@ class ExampleAppBody extends StatefulWidget { class _ExampleAppBodyState extends State { final _biometricSignature = BiometricSignature(); - // Mode selection + // Settings bool useEc = false; bool enableDecryption = false; - - // State - KeyCreationResult? keyMaterial; + KeyFormat _publicKeyFormat = KeyFormat.pem; + KeyFormat _signatureKeyFormat = KeyFormat.base64; + SignatureFormat _signatureFormat = SignatureFormat.base64; + KeyInfo? _keyInfo; + bool _checkKeyValidity = false; + + // Results + KeyCreationResult? keyResult; SignatureResult? signatureResult; - String? decryptResult; + DecryptResult? decryptResult; String? payload; String? errorMessage; bool isLoading = false; + BiometricAvailability? availability; - /// Returns current mode description - String get currentMode { - if (!useEc) return 'RSA'; - if (!enableDecryption) return 'EC (Sign Only)'; - return 'Hybrid EC (Sign + ECIES)'; + @override + void initState() { + super.initState(); + _checkAvailability(); } - /// Check if hybrid mode - /// Option 1: Use KeyCreationResult.isHybridMode if your class supports it - /// Option 2: Infer from configuration (Android + EC + decryption enabled) - bool get isHybridMode { - if (keyMaterial == null) return false; - // If KeyCreationResult has isHybridMode field, use it: - // return keyMaterial!.isHybridMode; - // Otherwise, infer from configuration: - return Platform.isAndroid && useEc && enableDecryption; + Future _checkAvailability() async { + final result = await _biometricSignature.biometricAuthAvailable(); + setState(() { + availability = result; + }); } - /// Whether we're running on an Apple platform (iOS or macOS) - bool get isApplePlatform => Platform.isIOS || Platform.isMacOS; - Future _createKeys() async { - // Hide keyboard and clear errors first. FocusScope.of(context).unfocus(); - setState(() { - errorMessage = null; - }); + setState(() => errorMessage = null); try { - // Do not set isLoading before prompting biometrics — avoid overlay-related flicker. final result = await _biometricSignature.createKeys( - androidConfig: AndroidConfig( + keyFormat: _publicKeyFormat, + promptMessage: 'Authenticate to create keys', + config: CreateKeysConfig( useDeviceCredentials: false, - signatureType: useEc - ? AndroidSignatureType.ECDSA - : AndroidSignatureType.RSA, + signatureType: useEc ? SignatureType.ecdsa : SignatureType.rsa, setInvalidatedByBiometricEnrollment: true, + enforceBiometric: true, enableDecryption: enableDecryption, ), - iosConfig: IosConfig( - useDeviceCredentials: false, - signatureType: useEc ? IOSSignatureType.ECDSA : IOSSignatureType.RSA, - biometryCurrentSet: true, - ), - macosConfig: MacosConfig( - useDeviceCredentials: false, - signatureType: useEc - ? MacosSignatureType.ECDSA - : MacosSignatureType.RSA, - biometryCurrentSet: true, - ), - enforceBiometric: true, ); - // Optionally show overlay while doing post-key-creation processing. - setState(() { - isLoading = true; - }); - - setState(() => keyMaterial = result); - - if (result != null) { - debugPrint('✅ Keys created ($currentMode)'); - debugPrint(' Algorithm: ${result.algorithm}'); - debugPrint(' Key Size: ${result.keySize}'); - if (Platform.isAndroid && useEc && enableDecryption) { - debugPrint(' Hybrid Mode: EC signing + ECIES encryption'); - } + if (result.code == BiometricError.success) { + setState(() => keyResult = result); + } else { + setState( + () => errorMessage = 'Error: ${result.code} - ${result.error}', + ); } } catch (e) { setState(() => errorMessage = e.toString()); - debugPrint('❌ Error creating keys: $e'); - } finally { - setState(() => isLoading = false); } } - void _onModeChanged() { - // Clear keys when mode changes - _biometricSignature.deleteKeys().then((success) { - if (success == true) { - setState(() { - keyMaterial = null; - signatureResult = null; - decryptResult = null; - errorMessage = null; - }); - } - }); - } - - void _payloadChanged(String value) { - if (value == payload) return; - setState(() { - payload = value; - signatureResult = null; - decryptResult = null; - errorMessage = null; - }); - } - Future _createSignature() async { if (payload == null || payload!.isEmpty) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Please enter a payload'))); + _showSnack('Enter payload first'); return; } - - // Hide keyboard — avoids layout changes while FaceID prompt animates. FocusScope.of(context).unfocus(); - - // We don't set isLoading = true before the biometric prompt to avoid - // drawing a semi-opaque overlay while iOS presents FaceID (causes flicker). setState(() { errorMessage = null; + signatureResult = null; }); try { - // Call createSignature directly — the plugin will show native biometric UI. final result = await _biometricSignature.createSignature( - SignatureOptions( - payload: payload!, - promptMessage: 'Sign Payload', - androidOptions: const AndroidSignatureOptions( - allowDeviceCredentials: false, - subtitle: 'Approve to sign data', - ), - iosOptions: const IosSignatureOptions(shouldMigrate: false), - ), + payload: payload!, + signatureFormat: _signatureFormat, + keyFormat: _signatureKeyFormat, + promptMessage: 'Sign Data', + config: CreateSignatureConfig(allowDeviceCredentials: false), ); - // Only show the app loading overlay for any post-auth processing. - setState(() { - isLoading = true; - }); - - // Apply result and update state (this will repaint after prompt dismisses). - setState(() { - signatureResult = result; - }); - - if (result != null) { - debugPrint('✅ Signature created (${result.algorithm})'); + if (result.code == BiometricError.success) { + setState(() => signatureResult = result); + } else { + setState( + () => errorMessage = 'Error: ${result.code} - ${result.error}', + ); } } catch (e) { setState(() => errorMessage = e.toString()); - debugPrint('❌ Error signing: $e'); - } finally { - // Ensure overlay is removed after all work - setState(() => isLoading = false); } } Future _decrypt() async { - if (payload == null || payload!.isEmpty) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Please enter a payload'))); + if (Platform.isWindows) { + setState(() { + errorMessage = + 'Decryption is not supported on Windows. ' + 'Windows Hello is designed for authentication and signing only.'; + }); return; } - if (keyMaterial == null) { - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('Please create keys first'))); + if (payload == null || payload!.isEmpty) { + _showSnack('Enter payload first'); return; } - - // Hide keyboard before presenting the biometric prompt. FocusScope.of(context).unfocus(); - - // Clear previous error and do not show the global overlay while FaceID animates. setState(() { errorMessage = null; + decryptResult = null; }); try { - // 1) Do any non-auth expensive work first if needed (none here). - // 2) Call encrypt helper to produce encryptedBase64 (no overlay). + // 1) Encrypt payload first (Roundtrip verification) final encryptedBase64 = await _encryptPayload(payload!); - debugPrint('📦 Encrypted: ${encryptedBase64.substring(0, 40)}...'); + debugPrint( + '📦 Encrypted: ${encryptedBase64.substring(0, min(40, encryptedBase64.length))}...', + ); - // 3) Present biometric prompt via plugin (native UI). Avoid overlay while prompt is visible. + // 2) Present biometric prompt via plugin (native UI). final result = await _biometricSignature.decrypt( - DecryptionOptions( - payload: encryptedBase64, - promptMessage: 'Decrypt Payload', - androidOptions: const AndroidDecryptionOptions( - allowDeviceCredentials: false, - subtitle: 'Approve to decrypt data', - ), - iosOptions: const IosDecryptionOptions(shouldMigrate: false), - ), + payload: encryptedBase64, + payloadFormat: PayloadFormat.base64, + promptMessage: 'Decrypt Payload', + config: DecryptConfig(allowDeviceCredentials: false), ); // Only show overlay if we need to do extra processing after auth. @@ -253,8 +172,15 @@ class _ExampleAppBodyState extends State { isLoading = true; }); - setState(() => decryptResult = result?.decryptedData); - debugPrint('✅ Decrypted: ${result?.decryptedData}'); + setState(() => decryptResult = result); + if (result.decryptedData != null) { + debugPrint('✅ Decrypted: ${result.decryptedData}'); + } else { + debugPrint( + '❌ Decryption Failed: Code=${result.code}, Error=${result.error}', + ); + setState(() => errorMessage = 'Decryption Failed: ${result.code}'); + } } catch (e, stack) { setState(() => errorMessage = e.toString()); debugPrint('❌ Error: $e\n$stack'); @@ -263,29 +189,41 @@ class _ExampleAppBodyState extends State { } } + Future _checkKeyExists() async { + try { + final info = await _biometricSignature.getKeyInfo( + checkValidity: _checkKeyValidity, + keyFormat: _publicKeyFormat, + ); + setState(() => _keyInfo = info); + _showSnack( + 'Key exists: ${info.exists}${info.isValid != null ? ', valid: ${info.isValid}' : ''}', + ); + } catch (e) { + setState(() => errorMessage = e.toString()); + } + } + /// Encrypts payload based on current key type Future _encryptPayload(String plaintext) async { - final algorithm = keyMaterial!.algorithm; - - if (algorithm == 'RSA') { + // useEc is the source of truth for what we requested. + if (!useEc) { return _encryptRsa(plaintext); } else { // EC - use ECIES - if (isApplePlatform) { - // iOS and macOS use native ECIES via method channel - return _encryptEciesIos(plaintext); - } else { - // Android uses Dart-based ECIES - return _encryptEciesDart(plaintext); - } + return _encryptEcies(plaintext); } } /// RSA encryption String _encryptRsa(String plaintext) { - final publicKeyPem = keyMaterial!.publicKey.pemLabel != null - ? keyMaterial!.publicKey.asString()! - : '-----BEGIN PUBLIC KEY-----\n${keyMaterial!.publicKey.toBase64()}\n-----END PUBLIC KEY-----'; + // All platforms now return SPKI (Standard X.509) + // In Hybrid mode, RSA key is in decryptingPublicKey + final publicKeyStr = + keyResult!.decryptingPublicKey ?? keyResult!.publicKey!; + final publicKeyPem = publicKeyStr.contains('BEGIN PUBLIC KEY') + ? publicKeyStr + : '-----BEGIN PUBLIC KEY-----\n$publicKeyStr\n-----END PUBLIC KEY-----'; final parser = enc.RSAKeyParser(); final rsaPublicKey = parser.parse(publicKeyPem) as RSAPublicKey; @@ -293,20 +231,13 @@ class _ExampleAppBodyState extends State { return encrypter.encrypt(plaintext).base64; } - /// ECIES encryption using iOS native - Future _encryptEciesIos(String plaintext) async { - const platform = MethodChannel('biometric_signature'); - final result = await platform.invokeMethod('testEncrypt', { - 'payload': plaintext, - }); - return result['encryptedPayload'] as String; - } - - /// ECIES encryption using Dart (PointyCastle) - String _encryptEciesDart(String plaintext) { - // Parse recipient's public key - final pem = keyMaterial!.publicKey.toPem(); - final ecPublicKey = _parseEcPublicKeyFromPem(pem); + /// ECIES encryption + String _encryptEcies(String plaintext) { + // Parse recipient's public key (handling both PEM and raw Base64 if needed) + final publicKeyStr = + keyResult!.decryptingPublicKey ?? keyResult!.publicKey!; + // Note: _parseEcPublicKeyFromPem handles stripping headers + final ecPublicKey = _parseEcPublicKeyFromPem(publicKeyStr); // Generate ephemeral keypair final ephemeralKeyPair = _generateEphemeralKeyPair(ecPublicKey.parameters!); @@ -317,43 +248,83 @@ class _ExampleAppBodyState extends State { final agreement = ECDHBasicAgreement()..init(ephemeralPrivate); final sharedSecret = agreement.calculateAgreement(ecPublicKey); - // X9.63 KDF -> AES key (16) + IV (12) - final derived = _kdfX963(sharedSecret, 28, Uint8List(0)); - final aesKey = derived.sublist(0, 16); - final gcmIv = derived.sublist(16, 28); + // Output: [EphemeralPubKey (Uncompressed 65)] || [Ciphertext + Tag] + final isApple = Platform.isIOS || Platform.isMacOS; + final ephemeralPubBytes = ephemeralPublic.Q!.getEncoded( + false, + ); // Uncompressed required + + // ECIES Parameters + // Hypothesis: Apple Standard Mode uses Static Zero IV and binds EphemKey in SharedInfo. + final sharedInfo = isApple ? ephemeralPubBytes : Uint8List(0); + + Uint8List gcmIv; + Uint8List aesKey; + final Uint8List aad; + + if (isApple) { + // iOS Standard Mode Hypothesis + // 1. IV is Static Zeros (16 bytes). + // 2. KDF derives ONLY Key (16 bytes). + final keySize = 16; + aesKey = _kdfX963(sharedSecret, keySize, sharedInfo); + gcmIv = Uint8List(16); // Zero IV + } else { + // Android Standard Mode (Derived IV) + final keySize = 16; + final ivSize = 12; + final derived = _kdfX963(sharedSecret, keySize + ivSize, sharedInfo); + aesKey = derived.sublist(0, keySize); + gcmIv = derived.sublist(keySize, keySize + ivSize); + } + + aad = Uint8List(0); - // AES-128-GCM encryption + // AES-GCM encryption final cipher = GCMBlockCipher(AESEngine()); - cipher.init( - true, - AEADParameters(KeyParameter(aesKey), 128, gcmIv, Uint8List(0)), - ); + cipher.init(true, AEADParameters(KeyParameter(aesKey), 128, gcmIv, aad)); final ciphertext = cipher.process( Uint8List.fromList(utf8.encode(plaintext)), ); - // Output: [EphemeralPubKey(65)] || [Ciphertext + Tag] - final ephemeralPubBytes = ephemeralPublic.Q!.getEncoded(false); + // Construct Payload: [EphemKey] [Ciphertext] + // Note: Android uses same payload structure + final payloadParts = [ephemeralPubBytes, ciphertext]; + return base64Encode( - Uint8List.fromList([...ephemeralPubBytes, ...ciphertext]), + Uint8List.fromList(payloadParts.expand((x) => x).toList()), ); } // ==================== ECIES Helpers ==================== ECPublicKey _parseEcPublicKeyFromPem(String pem) { + // Strip headers if present final rows = pem .split('\n') .where((l) => !l.startsWith('-----') && l.trim().isNotEmpty) .join(''); final bytes = base64Decode(rows); + final params = ECDomainParameters('secp256r1'); + Uint8List pubBytes; - final parser = ASN1Parser(bytes); - final topLevel = parser.nextObject() as ASN1Sequence; - final bitString = topLevel.elements![1] as ASN1BitString; - final pubBytes = bitString.stringValues!; + try { + final parser = ASN1Parser(bytes); + final topLevel = parser.nextObject(); + + if (topLevel is ASN1Sequence) { + // SPKI format (Android) + final bitString = topLevel.elements![1] as ASN1BitString; + pubBytes = Uint8List.fromList(bitString.stringValues!); + } else { + // iOS returns raw bytes (often parses as OctetString due to 0x04 tag) + pubBytes = bytes; + } + } catch (_) { + // Fallback to raw bytes just in case + pubBytes = bytes; + } - final params = ECDomainParameters('secp256r1'); final q = params.curve.decodePoint(pubBytes)!; return ECPublicKey(q, params); } @@ -423,270 +394,382 @@ class _ExampleAppBodyState extends State { } Future _deleteKeys() async { - final success = await _biometricSignature.deleteKeys(); - debugPrint('🗑️ Delete keys: $success'); - if (success == true) { - setState(() { - keyMaterial = null; - signatureResult = null; - decryptResult = null; - errorMessage = null; - }); + try { + final success = await _biometricSignature.deleteKeys(); + if (success) { + setState(() { + keyResult = null; + signatureResult = null; + decryptResult = null; + errorMessage = null; + }); + _showSnack('Keys deleted'); + } else { + setState(() => errorMessage = 'Failed to delete keys'); + } + } catch (e) { + setState(() => errorMessage = e.toString()); } } + void _showSnack(String msg) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); + } + @override Widget build(BuildContext context) { - // On Apple platforms (iOS/macOS), decryption is always available - // On Android, it requires explicit enableDecryption flag - final canDecrypt = enableDecryption || isApplePlatform; - - return SafeArea( - child: Stack( + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Mode Selection Card - Card( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Mode: $currentMode', - style: Theme.of(context).textTheme.titleMedium, + // Availability Info + if (availability != null) + Card( + child: ListTile( + leading: Icon( + (availability!.canAuthenticate ?? false) + ? Icons.check_circle + : Icons.warning, + color: (availability!.canAuthenticate ?? false) + ? Colors.green + : Colors.orange, + ), + title: Text( + (availability!.canAuthenticate ?? false) + ? 'Biometrics Available' + : 'Biometrics Unavailable', + ), + subtitle: Text(availability!.availableBiometrics.toString()), + ), + ), + + const SizedBox(height: 10), + + // Config + Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Row( + children: [ + // Hide EC toggle on Windows - Windows Hello only supports RSA + if (!Platform.isWindows) ...[ + const Text('Use EC'), + Switch( + value: useEc, + onChanged: (v) => setState(() => useEc = v), ), - const SizedBox(height: 8), - Row( - children: [ - FilterChip( - label: const Text('EC'), - selected: useEc, - onSelected: (v) { - setState(() => useEc = v); - _onModeChanged(); - }, - ), - const SizedBox(width: 8), - FilterChip( - label: const Text('Decryption'), - selected: enableDecryption, - onSelected: (v) { - setState(() => enableDecryption = v); - _onModeChanged(); - }, - ), - ], + ], + if (Platform.isAndroid) ...[ + const SizedBox(width: 20), + const Text('Decrypt Support'), + Switch( + value: enableDecryption, + onChanged: (v) => + setState(() => enableDecryption = v), ), ], - ), + ], ), - ), - - const SizedBox(height: 12), - - // Key Status Card - Card( - color: keyMaterial != null - ? Colors.green.shade50 - : Colors.grey.shade100, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - keyMaterial != null - ? Icons.check_circle - : Icons.cancel, - color: keyMaterial != null - ? Colors.green - : Colors.grey, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - keyMaterial != null - ? 'Keys: ${keyMaterial!.algorithm} (${keyMaterial!.keySize} bits)' - : 'No Keys', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Pub Key: '), + DropdownButton( + value: _publicKeyFormat, + onChanged: (v) { + if (v != null) setState(() => _publicKeyFormat = v); + }, + items: KeyFormat.values + .map( + (f) => DropdownMenuItem( + value: f, + child: Text(f.name), ), - ), - ], - ), - if (keyMaterial != null && isHybridMode) ...[ - const SizedBox(height: 4), - Text( - '🔀 Hybrid: EC signing + EC encryption (ECIES)', - style: TextStyle( - fontSize: 12, - color: Colors.blue.shade700, - ), - ), - ], - ], - ), + ) + .toList(), + ), + ], ), - ), - - const SizedBox(height: 12), + ElevatedButton( + onPressed: _createKeys, + child: const Text('Create Keys'), + ), + ], + ), + ), + ), - // Action Buttons - Row( + if (keyResult != null) + Card( + color: Colors.green.shade50, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: FilledButton.icon( - onPressed: isLoading ? null : _createKeys, - icon: const Icon(Icons.key), - label: const Text('Create Keys'), + const Text( + 'Public Key Created:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + Text( + keyResult!.publicKey ?? '', + style: const TextStyle( + fontSize: 10, + fontFamily: 'monospace', ), ), - const SizedBox(width: 8), - OutlinedButton.icon( - onPressed: !isLoading ? _deleteKeys : null, - icon: const Icon(Icons.delete), - label: const Text('Delete'), + if (keyResult!.publicKeyBytes != null) + Text( + 'Bytes: ${keyResult!.publicKeyBytes!.length} (Hex: ${keyResult!.publicKeyBytes!.map((e) => e.toRadixString(16).padLeft(2, '0')).join()})', + style: const TextStyle(fontSize: 8, color: Colors.grey), + ), + if (keyResult!.decryptingPublicKey != null) ...[ + const SizedBox(height: 8), + const Text( + 'Decrypting Key (Hybrid):', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text( + keyResult!.decryptingPublicKey!, + style: const TextStyle( + fontSize: 10, + fontFamily: 'monospace', + ), + ), + if (keyResult!.decryptingAlgorithm != null) + Text( + 'Alg: ${keyResult!.decryptingAlgorithm}, Size: ${keyResult!.decryptingKeySize}', + style: const TextStyle(fontSize: 10), + ), + ], + const SizedBox(height: 8), + TextButton.icon( + icon: const Icon(Icons.delete, size: 16), + label: const Text('Delete Keys'), + onPressed: _deleteKeys, ), ], ), + ), + ), - const SizedBox(height: 20), - - // Payload Input - TextField( - decoration: const InputDecoration( - labelText: 'Payload', - hintText: 'Enter text to sign/encrypt', - border: OutlineInputBorder(), + const SizedBox(height: 10), + Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Key Info (getKeyInfo)', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), - onChanged: _payloadChanged, - ), - - const SizedBox(height: 12), - - // Sign & Decrypt Buttons - Row( - children: [ - Expanded( - child: FilledButton.tonalIcon( - onPressed: !isLoading ? _createSignature : null, - icon: const Icon(Icons.draw), - label: const Text('Sign'), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Check Validity'), + Switch( + value: _checkKeyValidity, + onChanged: (v) => setState(() => _checkKeyValidity = v), ), + ], + ), + OutlinedButton.icon( + onPressed: _checkKeyExists, + icon: const Icon(Icons.vpn_key), + label: const Text('Get Key Info'), + ), + if (_keyInfo != null) ...[ + const Divider(), + _buildKeyInfoRow( + 'Exists', + (_keyInfo!.exists ?? false) ? 'Yes ✓' : 'No', ), - const SizedBox(width: 8), - Expanded( - child: FilledButton.icon( - onPressed: - keyMaterial != null && canDecrypt && !isLoading - ? _decrypt - : null, - icon: const Icon(Icons.lock_open), - label: const Text('Decrypt'), - style: FilledButton.styleFrom( - backgroundColor: Colors.teal, + if (_keyInfo!.isValid != null) + _buildKeyInfoRow( + 'Valid', + _keyInfo!.isValid! ? 'Yes ✓' : 'No ✗', + ), + if (_keyInfo!.algorithm != null) + _buildKeyInfoRow('Algorithm', _keyInfo!.algorithm!), + if (_keyInfo!.keySize != null) + _buildKeyInfoRow('Key Size', '${_keyInfo!.keySize} bits'), + if (_keyInfo!.isHybridMode != null) + _buildKeyInfoRow( + 'Hybrid Mode', + _keyInfo!.isHybridMode! ? 'Yes' : 'No', + ), + if (_keyInfo!.publicKey != null) ...[ + const SizedBox(height: 8), + const Text( + 'Public Key:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, ), ), - ), + const SizedBox(height: 4), + SelectableText( + _keyInfo!.publicKey!, + style: const TextStyle( + fontSize: 9, + fontFamily: 'monospace', + ), + ), + ], + if (_keyInfo!.decryptingPublicKey != null) ...[ + const SizedBox(height: 8), + const Text( + 'Decrypting Key:', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + Text( + '${_keyInfo!.decryptingAlgorithm} / ${_keyInfo!.decryptingKeySize} bits', + style: const TextStyle( + fontSize: 10, + color: Colors.grey, + ), + ), + SelectableText( + _keyInfo!.decryptingPublicKey!, + style: const TextStyle( + fontSize: 9, + fontFamily: 'monospace', + ), + ), + ], ], - ), + ], + ), + ), + ), - const SizedBox(height: 16), - - // Results - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (errorMessage != null) - _buildResultCard( - icon: Icons.error, - color: Colors.red, - title: 'Error', - content: errorMessage!, - ), - - if (signatureResult != null) - _buildResultCard( - icon: Icons.verified, - color: Colors.blue, - title: 'Signature (${signatureResult!.algorithm})', - content: signatureResult!.signature.toBase64(), - isMonospace: true, - ), - - if (decryptResult != null) - _buildResultCard( - icon: Icons.check_circle, - color: Colors.green.shade900, - title: 'Decrypted', - content: decryptResult!, - ), - ], - ), - ), - ), - ], + const SizedBox(height: 20), + + TextField( + decoration: const InputDecoration( + labelText: 'Payload (Text or Base64)', ), + onChanged: (v) => payload = v, + ), + + const SizedBox(height: 10), + Row( + children: [ + const Text('Sig Format: '), + DropdownButton( + value: _signatureFormat, + onChanged: (v) { + if (v != null) setState(() => _signatureFormat = v); + }, + items: SignatureFormat.values + .map((f) => DropdownMenuItem(value: f, child: Text(f.name))) + .toList(), + ), + const SizedBox(width: 10), + const Text('Key Format: '), + DropdownButton( + value: _signatureKeyFormat, + onChanged: (v) { + if (v != null) setState(() => _signatureKeyFormat = v); + }, + items: KeyFormat.values + .map((f) => DropdownMenuItem(value: f, child: Text(f.name))) + .toList(), + ), + ], + ), + const SizedBox(height: 10), + Row( + children: [ + Expanded( + child: FilledButton( + onPressed: _createSignature, + child: const Text('Sign'), + ), + ), + const SizedBox(width: 10), + Expanded( + child: FilledButton.tonal( + onPressed: _decrypt, + child: const Text('Decrypt'), + ), + ), + ], ), - // Loading overlay - if (isLoading) - Container( - color: Colors.black26, - child: const Center(child: CircularProgressIndicator()), + if (errorMessage != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + errorMessage!, + style: const TextStyle(color: Colors.red), + ), + ), + + if (signatureResult != null) ...[ + _buildResult( + 'Signature', + signatureResult!.signature, + bytes: signatureResult!.signatureBytes, ), + if (signatureResult!.publicKey != null) + _buildResult('Signer Public Key', signatureResult!.publicKey), + ], + + if (decryptResult != null) + _buildResult('Decrypted', decryptResult!.decryptedData), ], ), ); } - Widget _buildResultCard({ - required IconData icon, - required Color color, - required String title, - required String content, - bool isMonospace = false, - }) { + Widget _buildResult(String title, String? data, {Uint8List? bytes}) { return Card( - color: color.withOpacity(0.1), - margin: const EdgeInsets.only(bottom: 12), + margin: const EdgeInsets.only(top: 10), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Icon(icon, color: color, size: 20), - const SizedBox(width: 8), - Text( - title, - style: TextStyle(fontWeight: FontWeight.bold, color: color), - ), - ], - ), - const SizedBox(height: 8), + Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 4), SelectableText( - content, - style: TextStyle( - fontSize: 12, - fontFamily: isMonospace ? 'monospace' : null, - color: color.withOpacity(0.8), - ), + data ?? 'null', + style: const TextStyle(fontFamily: 'monospace'), ), + if (bytes != null) + Text( + 'Bytes: ${bytes.length} (Hex: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join()})', + style: const TextStyle(fontSize: 8, color: Colors.grey), + ), ], ), ), ); } + + Widget _buildKeyInfoRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(fontSize: 13, color: Colors.grey)), + Text( + value, + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + ), + ], + ), + ); + } } diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index c5195f1..606983c 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - biometric_signature (8.5.0): + - biometric_signature (9.0.0): - FlutterMacOS - FlutterMacOS (1.0.0) @@ -14,7 +14,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral SPEC CHECKSUMS: - biometric_signature: 299607c931c3b796e2eb3e0d5d5c3d16d95c364f + biometric_signature: fcc23fe926798544af948f78621af1e85b35d372 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 diff --git a/example/pubspec.lock b/example/pubspec.lock index 488e0e3..0efca6e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -31,7 +31,7 @@ packages: path: ".." relative: true source: path - version: "8.5.0" + version: "9.0.0" boolean_selector: dependency: transitive description: diff --git a/example/windows/.gitignore b/example/windows/.gitignore new file mode 100644 index 0000000..ec4098a --- /dev/null +++ b/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/example/windows/CMakeLists.txt b/example/windows/CMakeLists.txt new file mode 100644 index 0000000..b15a87c --- /dev/null +++ b/example/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(biometric_signature_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "biometric_signature_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..efb62eb --- /dev/null +++ b/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..c777059 --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + BiometricSignaturePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BiometricSignaturePlugin")); +} diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..5c7dd30 --- /dev/null +++ b/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + biometric_signature +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/windows/runner/CMakeLists.txt b/example/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..2041a04 --- /dev/null +++ b/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/example/windows/runner/Runner.rc b/example/windows/runner/Runner.rc new file mode 100644 index 0000000..e517954 --- /dev/null +++ b/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.visionflutter" "\0" + VALUE "FileDescription", "biometric_signature_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "biometric_signature_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.visionflutter. All rights reserved." "\0" + VALUE "OriginalFilename", "biometric_signature_example.exe" "\0" + VALUE "ProductName", "biometric_signature_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/example/windows/runner/flutter_window.cpp b/example/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..c819cb0 --- /dev/null +++ b/example/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/example/windows/runner/flutter_window.h b/example/windows/runner/flutter_window.h new file mode 100644 index 0000000..28c2383 --- /dev/null +++ b/example/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/example/windows/runner/main.cpp b/example/windows/runner/main.cpp new file mode 100644 index 0000000..1170f9b --- /dev/null +++ b/example/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"biometric_signature_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/example/windows/runner/resource.h b/example/windows/runner/resource.h new file mode 100644 index 0000000..ddc7f3e --- /dev/null +++ b/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/example/windows/runner/resources/app_icon.ico b/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/example/windows/runner/resources/app_icon.ico differ diff --git a/example/windows/runner/runner.exe.manifest b/example/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..4b962bb --- /dev/null +++ b/example/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/example/windows/runner/utils.cpp b/example/windows/runner/utils.cpp new file mode 100644 index 0000000..259d85b --- /dev/null +++ b/example/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/example/windows/runner/utils.h b/example/windows/runner/utils.h new file mode 100644 index 0000000..3f0e05c --- /dev/null +++ b/example/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/example/windows/runner/win32_window.cpp b/example/windows/runner/win32_window.cpp new file mode 100644 index 0000000..b5ba2a0 --- /dev/null +++ b/example/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/example/windows/runner/win32_window.h b/example/windows/runner/win32_window.h new file mode 100644 index 0000000..49b847f --- /dev/null +++ b/example/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/ios/Classes/BiometricSignatureApi.swift b/ios/Classes/BiometricSignatureApi.swift new file mode 100644 index 0000000..5f2dbe8 --- /dev/null +++ b/ios/Classes/BiometricSignatureApi.swift @@ -0,0 +1,934 @@ +// Autogenerated from Pigeon (v26.1.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +func deepEqualsBiometricSignatureApi(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsBiometricSignatureApi(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsBiometricSignatureApi(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashBiometricSignatureApi(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashBiometricSignatureApi(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashBiometricSignatureApi(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +/// Types of biometric authentication supported by the device. +enum BiometricType: Int { + /// Face recognition (Face ID on iOS, face unlock on Android). + case face = 0 + /// Fingerprint recognition (Touch ID on iOS/macOS, fingerprint on Android). + case fingerprint = 1 + /// Iris scanner (Android only, rare on consumer devices). + case iris = 2 + /// Multiple biometric types are available on the device. + case multiple = 3 + /// No biometric hardware available or biometrics are disabled. + case unavailable = 4 +} + +/// Standardized error codes for the plugin. +enum BiometricError: Int { + /// The operation was successful. + case success = 0 + /// The user canceled the operation. + case userCanceled = 1 + /// Biometric authentication is not available on this device. + case notAvailable = 2 + /// No biometrics are enrolled. + case notEnrolled = 3 + /// The user is temporarily locked out due to too many failed attempts. + case lockedOut = 4 + /// The user is permanently locked out until they log in with a strong method. + case lockedOutPermanent = 5 + /// The requested key was not found. + case keyNotFound = 6 + /// The key has been invalidated (e.g. by new biometric enrollment). + case keyInvalidated = 7 + /// An unknown error occurred. + case unknown = 8 + /// The input payload was invalid (e.g. not valid Base64). + case invalidInput = 9 +} + +/// The cryptographic algorithm to use for key generation. +enum SignatureType: Int { + /// RSA-2048 (Android: native, iOS/macOS: hybrid mode with Secure Enclave EC). + case rsa = 0 + /// ECDSA P-256 (hardware-backed on all platforms). + case ecdsa = 1 +} + +/// Output format for public keys. +enum KeyFormat: Int { + /// Base64-encoded DER (SubjectPublicKeyInfo). + case base64 = 0 + /// PEM format with BEGIN/END PUBLIC KEY headers. + case pem = 1 + /// Hexadecimal-encoded DER. + case hex = 2 + /// Raw DER bytes (returned via `publicKeyBytes`). + case raw = 3 +} + +/// Output format for cryptographic signatures. +enum SignatureFormat: Int { + /// Base64-encoded signature bytes. + case base64 = 0 + /// Hexadecimal-encoded signature bytes. + case hex = 1 + /// Raw signature bytes (returned via `signatureBytes`). + case raw = 2 +} + +/// Input format for encrypted payloads to decrypt. +enum PayloadFormat: Int { + /// Base64-encoded ciphertext. + case base64 = 0 + /// Hexadecimal-encoded ciphertext. + case hex = 1 + /// Raw UTF-8 string (not recommended for binary data). + case raw = 2 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct BiometricAvailability: Hashable { + var canAuthenticate: Bool? = nil + var hasEnrolledBiometrics: Bool? = nil + var availableBiometrics: [BiometricType?]? = nil + var reason: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> BiometricAvailability? { + let canAuthenticate: Bool? = nilOrValue(pigeonVar_list[0]) + let hasEnrolledBiometrics: Bool? = nilOrValue(pigeonVar_list[1]) + let availableBiometrics: [BiometricType?]? = nilOrValue(pigeonVar_list[2]) + let reason: String? = nilOrValue(pigeonVar_list[3]) + + return BiometricAvailability( + canAuthenticate: canAuthenticate, + hasEnrolledBiometrics: hasEnrolledBiometrics, + availableBiometrics: availableBiometrics, + reason: reason + ) + } + func toList() -> [Any?] { + return [ + canAuthenticate, + hasEnrolledBiometrics, + availableBiometrics, + reason, + ] + } + static func == (lhs: BiometricAvailability, rhs: BiometricAvailability) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct KeyCreationResult: Hashable { + var publicKey: String? = nil + var publicKeyBytes: FlutterStandardTypedData? = nil + var error: String? = nil + var code: BiometricError? = nil + var algorithm: String? = nil + var keySize: Int64? = nil + var decryptingPublicKey: String? = nil + var decryptingAlgorithm: String? = nil + var decryptingKeySize: Int64? = nil + var isHybridMode: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> KeyCreationResult? { + let publicKey: String? = nilOrValue(pigeonVar_list[0]) + let publicKeyBytes: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[1]) + let error: String? = nilOrValue(pigeonVar_list[2]) + let code: BiometricError? = nilOrValue(pigeonVar_list[3]) + let algorithm: String? = nilOrValue(pigeonVar_list[4]) + let keySize: Int64? = nilOrValue(pigeonVar_list[5]) + let decryptingPublicKey: String? = nilOrValue(pigeonVar_list[6]) + let decryptingAlgorithm: String? = nilOrValue(pigeonVar_list[7]) + let decryptingKeySize: Int64? = nilOrValue(pigeonVar_list[8]) + let isHybridMode: Bool? = nilOrValue(pigeonVar_list[9]) + + return KeyCreationResult( + publicKey: publicKey, + publicKeyBytes: publicKeyBytes, + error: error, + code: code, + algorithm: algorithm, + keySize: keySize, + decryptingPublicKey: decryptingPublicKey, + decryptingAlgorithm: decryptingAlgorithm, + decryptingKeySize: decryptingKeySize, + isHybridMode: isHybridMode + ) + } + func toList() -> [Any?] { + return [ + publicKey, + publicKeyBytes, + error, + code, + algorithm, + keySize, + decryptingPublicKey, + decryptingAlgorithm, + decryptingKeySize, + isHybridMode, + ] + } + static func == (lhs: KeyCreationResult, rhs: KeyCreationResult) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct SignatureResult: Hashable { + var signature: String? = nil + var signatureBytes: FlutterStandardTypedData? = nil + var publicKey: String? = nil + var error: String? = nil + var code: BiometricError? = nil + var algorithm: String? = nil + var keySize: Int64? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> SignatureResult? { + let signature: String? = nilOrValue(pigeonVar_list[0]) + let signatureBytes: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[1]) + let publicKey: String? = nilOrValue(pigeonVar_list[2]) + let error: String? = nilOrValue(pigeonVar_list[3]) + let code: BiometricError? = nilOrValue(pigeonVar_list[4]) + let algorithm: String? = nilOrValue(pigeonVar_list[5]) + let keySize: Int64? = nilOrValue(pigeonVar_list[6]) + + return SignatureResult( + signature: signature, + signatureBytes: signatureBytes, + publicKey: publicKey, + error: error, + code: code, + algorithm: algorithm, + keySize: keySize + ) + } + func toList() -> [Any?] { + return [ + signature, + signatureBytes, + publicKey, + error, + code, + algorithm, + keySize, + ] + } + static func == (lhs: SignatureResult, rhs: SignatureResult) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct DecryptResult: Hashable { + var decryptedData: String? = nil + var error: String? = nil + var code: BiometricError? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> DecryptResult? { + let decryptedData: String? = nilOrValue(pigeonVar_list[0]) + let error: String? = nilOrValue(pigeonVar_list[1]) + let code: BiometricError? = nilOrValue(pigeonVar_list[2]) + + return DecryptResult( + decryptedData: decryptedData, + error: error, + code: code + ) + } + func toList() -> [Any?] { + return [ + decryptedData, + error, + code, + ] + } + static func == (lhs: DecryptResult, rhs: DecryptResult) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Detailed information about existing biometric keys. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct KeyInfo: Hashable { + /// Whether any biometric key exists on the device. + var exists: Bool? = nil + /// Whether the key is still valid (not invalidated by biometric changes). + /// Only populated when `checkValidity: true` is passed. + var isValid: Bool? = nil + /// The algorithm of the signing key (e.g., "RSA", "EC"). + var algorithm: String? = nil + /// The key size in bits (e.g., 2048 for RSA, 256 for EC). + var keySize: Int64? = nil + /// Whether the key is in hybrid mode (separate signing and decryption keys). + var isHybridMode: Bool? = nil + /// Signing key public key (formatted according to the requested format). + var publicKey: String? = nil + /// Decryption key public key for hybrid mode. + var decryptingPublicKey: String? = nil + /// Algorithm of the decryption key (hybrid mode only). + var decryptingAlgorithm: String? = nil + /// Key size of the decryption key in bits (hybrid mode only). + var decryptingKeySize: Int64? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> KeyInfo? { + let exists: Bool? = nilOrValue(pigeonVar_list[0]) + let isValid: Bool? = nilOrValue(pigeonVar_list[1]) + let algorithm: String? = nilOrValue(pigeonVar_list[2]) + let keySize: Int64? = nilOrValue(pigeonVar_list[3]) + let isHybridMode: Bool? = nilOrValue(pigeonVar_list[4]) + let publicKey: String? = nilOrValue(pigeonVar_list[5]) + let decryptingPublicKey: String? = nilOrValue(pigeonVar_list[6]) + let decryptingAlgorithm: String? = nilOrValue(pigeonVar_list[7]) + let decryptingKeySize: Int64? = nilOrValue(pigeonVar_list[8]) + + return KeyInfo( + exists: exists, + isValid: isValid, + algorithm: algorithm, + keySize: keySize, + isHybridMode: isHybridMode, + publicKey: publicKey, + decryptingPublicKey: decryptingPublicKey, + decryptingAlgorithm: decryptingAlgorithm, + decryptingKeySize: decryptingKeySize + ) + } + func toList() -> [Any?] { + return [ + exists, + isValid, + algorithm, + keySize, + isHybridMode, + publicKey, + decryptingPublicKey, + decryptingAlgorithm, + decryptingKeySize, + ] + } + static func == (lhs: KeyInfo, rhs: KeyInfo) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Configuration for key creation (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// Windows ignores most fields as it only supports RSA with mandatory +/// Windows Hello authentication. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct CreateKeysConfig: Hashable { + /// [Android/iOS/macOS] The cryptographic algorithm to use. + /// Windows only supports RSA and ignores this field. + var signatureType: SignatureType? = nil + /// [Android/iOS/macOS] Whether to require biometric authentication + /// during key creation. Windows always authenticates via Windows Hello. + var enforceBiometric: Bool? = nil + /// [Android/iOS/macOS] Whether to invalidate the key when new biometrics + /// are enrolled. Not supported on Windows. + /// + /// **Security Note**: When `true`, keys become invalid if fingerprints/faces + /// are added or removed, preventing unauthorized access if an attacker + /// enrolls their own biometrics on a compromised device. + var setInvalidatedByBiometricEnrollment: Bool? = nil + /// [Android/iOS/macOS] Whether to allow device credentials (PIN/pattern/passcode) + /// as fallback for biometric authentication. Not supported on Windows. + var useDeviceCredentials: Bool? = nil + /// [Android] Whether to enable decryption capability for the key. + /// On iOS/macOS, decryption is always available with EC keys. + var enableDecryption: Bool? = nil + /// [Android] Subtitle text for the biometric prompt. + var promptSubtitle: String? = nil + /// [Android] Description text for the biometric prompt. + var promptDescription: String? = nil + /// [Android] Text for the cancel button in the biometric prompt. + var cancelButtonText: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> CreateKeysConfig? { + let signatureType: SignatureType? = nilOrValue(pigeonVar_list[0]) + let enforceBiometric: Bool? = nilOrValue(pigeonVar_list[1]) + let setInvalidatedByBiometricEnrollment: Bool? = nilOrValue(pigeonVar_list[2]) + let useDeviceCredentials: Bool? = nilOrValue(pigeonVar_list[3]) + let enableDecryption: Bool? = nilOrValue(pigeonVar_list[4]) + let promptSubtitle: String? = nilOrValue(pigeonVar_list[5]) + let promptDescription: String? = nilOrValue(pigeonVar_list[6]) + let cancelButtonText: String? = nilOrValue(pigeonVar_list[7]) + + return CreateKeysConfig( + signatureType: signatureType, + enforceBiometric: enforceBiometric, + setInvalidatedByBiometricEnrollment: setInvalidatedByBiometricEnrollment, + useDeviceCredentials: useDeviceCredentials, + enableDecryption: enableDecryption, + promptSubtitle: promptSubtitle, + promptDescription: promptDescription, + cancelButtonText: cancelButtonText + ) + } + func toList() -> [Any?] { + return [ + signatureType, + enforceBiometric, + setInvalidatedByBiometricEnrollment, + useDeviceCredentials, + enableDecryption, + promptSubtitle, + promptDescription, + cancelButtonText, + ] + } + static func == (lhs: CreateKeysConfig, rhs: CreateKeysConfig) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Configuration for signature creation (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct CreateSignatureConfig: Hashable { + /// [Android] Subtitle text for the biometric prompt. + var promptSubtitle: String? = nil + /// [Android] Description text for the biometric prompt. + var promptDescription: String? = nil + /// [Android] Text for the cancel button in the biometric prompt. + var cancelButtonText: String? = nil + /// [Android] Whether to allow device credentials (PIN/pattern) as fallback. + var allowDeviceCredentials: Bool? = nil + /// [iOS] Whether to migrate from legacy keychain storage. + var shouldMigrate: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> CreateSignatureConfig? { + let promptSubtitle: String? = nilOrValue(pigeonVar_list[0]) + let promptDescription: String? = nilOrValue(pigeonVar_list[1]) + let cancelButtonText: String? = nilOrValue(pigeonVar_list[2]) + let allowDeviceCredentials: Bool? = nilOrValue(pigeonVar_list[3]) + let shouldMigrate: Bool? = nilOrValue(pigeonVar_list[4]) + + return CreateSignatureConfig( + promptSubtitle: promptSubtitle, + promptDescription: promptDescription, + cancelButtonText: cancelButtonText, + allowDeviceCredentials: allowDeviceCredentials, + shouldMigrate: shouldMigrate + ) + } + func toList() -> [Any?] { + return [ + promptSubtitle, + promptDescription, + cancelButtonText, + allowDeviceCredentials, + shouldMigrate, + ] + } + static func == (lhs: CreateSignatureConfig, rhs: CreateSignatureConfig) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Configuration for decryption (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// Note: Decryption is not supported on Windows. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct DecryptConfig: Hashable { + /// [Android] Subtitle text for the biometric prompt. + var promptSubtitle: String? = nil + /// [Android] Description text for the biometric prompt. + var promptDescription: String? = nil + /// [Android] Text for the cancel button in the biometric prompt. + var cancelButtonText: String? = nil + /// [Android] Whether to allow device credentials (PIN/pattern) as fallback. + var allowDeviceCredentials: Bool? = nil + /// [iOS] Whether to migrate from legacy keychain storage. + var shouldMigrate: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> DecryptConfig? { + let promptSubtitle: String? = nilOrValue(pigeonVar_list[0]) + let promptDescription: String? = nilOrValue(pigeonVar_list[1]) + let cancelButtonText: String? = nilOrValue(pigeonVar_list[2]) + let allowDeviceCredentials: Bool? = nilOrValue(pigeonVar_list[3]) + let shouldMigrate: Bool? = nilOrValue(pigeonVar_list[4]) + + return DecryptConfig( + promptSubtitle: promptSubtitle, + promptDescription: promptDescription, + cancelButtonText: cancelButtonText, + allowDeviceCredentials: allowDeviceCredentials, + shouldMigrate: shouldMigrate + ) + } + func toList() -> [Any?] { + return [ + promptSubtitle, + promptDescription, + cancelButtonText, + allowDeviceCredentials, + shouldMigrate, + ] + } + static func == (lhs: DecryptConfig, rhs: DecryptConfig) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +private class BiometricSignatureApiPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return BiometricType(rawValue: enumResultAsInt) + } + return nil + case 130: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return BiometricError(rawValue: enumResultAsInt) + } + return nil + case 131: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return SignatureType(rawValue: enumResultAsInt) + } + return nil + case 132: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return KeyFormat(rawValue: enumResultAsInt) + } + return nil + case 133: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return SignatureFormat(rawValue: enumResultAsInt) + } + return nil + case 134: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PayloadFormat(rawValue: enumResultAsInt) + } + return nil + case 135: + return BiometricAvailability.fromList(self.readValue() as! [Any?]) + case 136: + return KeyCreationResult.fromList(self.readValue() as! [Any?]) + case 137: + return SignatureResult.fromList(self.readValue() as! [Any?]) + case 138: + return DecryptResult.fromList(self.readValue() as! [Any?]) + case 139: + return KeyInfo.fromList(self.readValue() as! [Any?]) + case 140: + return CreateKeysConfig.fromList(self.readValue() as! [Any?]) + case 141: + return CreateSignatureConfig.fromList(self.readValue() as! [Any?]) + case 142: + return DecryptConfig.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class BiometricSignatureApiPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? BiometricType { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? BiometricError { + super.writeByte(130) + super.writeValue(value.rawValue) + } else if let value = value as? SignatureType { + super.writeByte(131) + super.writeValue(value.rawValue) + } else if let value = value as? KeyFormat { + super.writeByte(132) + super.writeValue(value.rawValue) + } else if let value = value as? SignatureFormat { + super.writeByte(133) + super.writeValue(value.rawValue) + } else if let value = value as? PayloadFormat { + super.writeByte(134) + super.writeValue(value.rawValue) + } else if let value = value as? BiometricAvailability { + super.writeByte(135) + super.writeValue(value.toList()) + } else if let value = value as? KeyCreationResult { + super.writeByte(136) + super.writeValue(value.toList()) + } else if let value = value as? SignatureResult { + super.writeByte(137) + super.writeValue(value.toList()) + } else if let value = value as? DecryptResult { + super.writeByte(138) + super.writeValue(value.toList()) + } else if let value = value as? KeyInfo { + super.writeByte(139) + super.writeValue(value.toList()) + } else if let value = value as? CreateKeysConfig { + super.writeByte(140) + super.writeValue(value.toList()) + } else if let value = value as? CreateSignatureConfig { + super.writeByte(141) + super.writeValue(value.toList()) + } else if let value = value as? DecryptConfig { + super.writeByte(142) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class BiometricSignatureApiPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return BiometricSignatureApiPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return BiometricSignatureApiPigeonCodecWriter(data: data) + } +} + +class BiometricSignatureApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = BiometricSignatureApiPigeonCodec(readerWriter: BiometricSignatureApiPigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol BiometricSignatureApi { + /// Checks if biometric authentication is available. + func biometricAuthAvailable(completion: @escaping (Result) -> Void) + /// Creates a new key pair. + /// + /// [config] contains platform-specific options. See [CreateKeysConfig]. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + func createKeys(config: CreateKeysConfig?, keyFormat: KeyFormat, promptMessage: String?, completion: @escaping (Result) -> Void) + /// Creates a signature. + /// + /// [payload] is the data to sign. + /// [config] contains platform-specific options. See [CreateSignatureConfig]. + /// [signatureFormat] specifies the output format for the signature. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + func createSignature(payload: String, config: CreateSignatureConfig?, signatureFormat: SignatureFormat, keyFormat: KeyFormat, promptMessage: String?, completion: @escaping (Result) -> Void) + /// Decrypts data. + /// + /// Note: Not supported on Windows. + /// [payload] is the encrypted data. + /// [payloadFormat] specifies the format of the encrypted data. + /// [config] contains platform-specific options. See [DecryptConfig]. + /// [promptMessage] is the message shown to the user during authentication. + func decrypt(payload: String, payloadFormat: PayloadFormat, config: DecryptConfig?, promptMessage: String?, completion: @escaping (Result) -> Void) + /// Deletes keys. + func deleteKeys(completion: @escaping (Result) -> Void) + /// Gets detailed information about existing biometric keys. + /// + /// Returns key metadata including algorithm, size, validity, and public keys. + func getKeyInfo(checkValidity: Bool, keyFormat: KeyFormat, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class BiometricSignatureApiSetup { + static var codec: FlutterStandardMessageCodec { BiometricSignatureApiPigeonCodec.shared } + /// Sets up an instance of `BiometricSignatureApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BiometricSignatureApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + /// Checks if biometric authentication is available. + let biometricAuthAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.biometricAuthAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + biometricAuthAvailableChannel.setMessageHandler { _, reply in + api.biometricAuthAvailable { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + biometricAuthAvailableChannel.setMessageHandler(nil) + } + /// Creates a new key pair. + /// + /// [config] contains platform-specific options. See [CreateKeysConfig]. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + let createKeysChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createKeys\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + createKeysChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let configArg: CreateKeysConfig? = nilOrValue(args[0]) + let keyFormatArg = args[1] as! KeyFormat + let promptMessageArg: String? = nilOrValue(args[2]) + api.createKeys(config: configArg, keyFormat: keyFormatArg, promptMessage: promptMessageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + createKeysChannel.setMessageHandler(nil) + } + /// Creates a signature. + /// + /// [payload] is the data to sign. + /// [config] contains platform-specific options. See [CreateSignatureConfig]. + /// [signatureFormat] specifies the output format for the signature. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + let createSignatureChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createSignature\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + createSignatureChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let payloadArg = args[0] as! String + let configArg: CreateSignatureConfig? = nilOrValue(args[1]) + let signatureFormatArg = args[2] as! SignatureFormat + let keyFormatArg = args[3] as! KeyFormat + let promptMessageArg: String? = nilOrValue(args[4]) + api.createSignature(payload: payloadArg, config: configArg, signatureFormat: signatureFormatArg, keyFormat: keyFormatArg, promptMessage: promptMessageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + createSignatureChannel.setMessageHandler(nil) + } + /// Decrypts data. + /// + /// Note: Not supported on Windows. + /// [payload] is the encrypted data. + /// [payloadFormat] specifies the format of the encrypted data. + /// [config] contains platform-specific options. See [DecryptConfig]. + /// [promptMessage] is the message shown to the user during authentication. + let decryptChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.decrypt\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + decryptChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let payloadArg = args[0] as! String + let payloadFormatArg = args[1] as! PayloadFormat + let configArg: DecryptConfig? = nilOrValue(args[2]) + let promptMessageArg: String? = nilOrValue(args[3]) + api.decrypt(payload: payloadArg, payloadFormat: payloadFormatArg, config: configArg, promptMessage: promptMessageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + decryptChannel.setMessageHandler(nil) + } + /// Deletes keys. + let deleteKeysChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.deleteKeys\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + deleteKeysChannel.setMessageHandler { _, reply in + api.deleteKeys { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + deleteKeysChannel.setMessageHandler(nil) + } + /// Gets detailed information about existing biometric keys. + /// + /// Returns key metadata including algorithm, size, validity, and public keys. + let getKeyInfoChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.getKeyInfo\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getKeyInfoChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let checkValidityArg = args[0] as! Bool + let keyFormatArg = args[1] as! KeyFormat + api.getKeyInfo(checkValidity: checkValidityArg, keyFormat: keyFormatArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getKeyInfoChannel.setMessageHandler(nil) + } + } +} diff --git a/ios/Classes/BiometricSignaturePlugin.swift b/ios/Classes/BiometricSignaturePlugin.swift index 21313fe..e291c9f 100644 --- a/ios/Classes/BiometricSignaturePlugin.swift +++ b/ios/Classes/BiometricSignaturePlugin.swift @@ -4,47 +4,14 @@ import LocalAuthentication import Security private enum Constants { - static let authFailed = "AUTH_FAILED" - static let invalidPayload = "INVALID_PAYLOAD" - static let invalidArguments = "INVALID_ARGUMENTS" static let biometricKeyAlias = "biometric_key" static let ecKeyAlias = "com.visionflutter.eckey".data(using: .utf8)! static let invalidationSettingKey = "com.visionflutter.biometric_signature.invalidation_setting" } -private enum KeyFormat: String { - case base64 = "BASE64" - case pem = "PEM" - case raw = "RAW" - case hex = "HEX" - - static func from(_ raw: Any?) -> KeyFormat { - guard let string = raw as? String, - let format = KeyFormat(rawValue: string.uppercased()) else { - return .base64 - } - return format - } - - var channelValue: String { rawValue } -} - -private struct FormattedOutput { - let value: Any - let format: KeyFormat - let pemLabel: String? -} - -private let iso8601Formatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return formatter -}() - // MARK: - Domain State (biometry change detection) private enum DomainState { static let service = "com.visionflutter.biometric_signature.domain_state" - private static func account() -> String { "biometric_domain_state_v1" } static func saveCurrent() { @@ -96,7 +63,7 @@ private enum DomainState { let ctx = LAContext() var laErr: NSError? guard ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &laErr), - let current = ctx.evaluatedPolicyDomainState else { + let current = ctx.evaluatedPolicyDomainState else { // If we can't evaluate and we *had* a baseline, be conservative. return loadSaved() != nil } @@ -153,1143 +120,751 @@ private enum InvalidationSetting { } } -public class BiometricSignaturePlugin: NSObject, FlutterPlugin { +public class BiometricSignaturePlugin: NSObject, FlutterPlugin, BiometricSignatureApi { + public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "biometric_signature", binaryMessenger: registrar.messenger()) let instance = BiometricSignaturePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) + BiometricSignatureApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance) } - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "createKeys": - if let arguments = call.arguments as? [String: Any] { - let useDeviceCredentials = arguments["useDeviceCredentials"] as? Bool ?? false - let useEc = arguments["useEc"] as? Bool ?? false - let keyFormat = KeyFormat.from(arguments["keyFormat"]) - let biometryCurrentSet = arguments["biometryCurrentSet"] as! Bool - let enforceBiometric = arguments["enforceBiometric"] as? Bool ?? false - let promptMessage = arguments["promptMessage"] as? String ?? "Authenticate to create keys" - createKeys( - useDeviceCredentials: useDeviceCredentials, - useEc: useEc, - keyFormat: keyFormat, - biometryCurrentSet: biometryCurrentSet, - enforceBiometric: enforceBiometric, - promptMessage: promptMessage, - result: result - ) - } else { - result(FlutterError(code: Constants.invalidArguments, message: "Invalid arguments", details: nil)) - } - case "createSignature": - createSignature(options: call.arguments as? [String: Any], result: result) - case "decrypt": - decrypt(options: call.arguments as? [String: Any], result: result) - case "testEncrypt": - testEncrypt(options: call.arguments as? [String: Any], result: result) - case "deleteKeys": - deleteKeys(result: result) - case "biometricAuthAvailable": - biometricAuthAvailable(result: result) - case "biometricKeyExists": - guard let checkValidity = call.arguments as? Bool else { return } - biometricKeyExists(checkValidity: checkValidity, result: result) - default: - result(FlutterMethodNotImplemented) - } - } - // MARK: - Public API + // MARK: - BiometricSignatureApi Implementation - private func biometricAuthAvailable(result: @escaping FlutterResult) { + func biometricAuthAvailable(completion: @escaping (Result) -> Void) { let context = LAContext() var error: NSError? - let canEvaluatePolicy = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) - - if canEvaluatePolicy { - let biometricType = getBiometricType(context) - dispatchMainAsync { result(biometricType) } - } else { - let errorMessage = error?.localizedDescription ?? "" - dispatchMainAsync { result("none, \(errorMessage)") } - } - } - - private func biometricKeyExists(checkValidity: Bool, result: @escaping FlutterResult) { - let exists = self.doesBiometricKeyExist(checkValidity: checkValidity) - dispatchMainAsync { result(exists) } - } - - private func deleteKeys(result: @escaping FlutterResult) { - // Delete EC key pair - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - ] - let ecStatus = SecItemDelete(ecKeyQuery as CFDictionary) - - // Delete encrypted RSA private key from Keychain - let encryptedKeyTag = getBiometricKeyTag() - let encryptedKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag - ] - let rsaStatus = SecItemDelete(encryptedKeyQuery as CFDictionary) - - // Delete saved domain-state baseline - let dsOK = DomainState.deleteSaved() - - // Delete invalidation setting - let isOK = InvalidationSetting.delete() - - let success = (ecStatus == errSecSuccess || ecStatus == errSecItemNotFound) - && (rsaStatus == errSecSuccess || rsaStatus == errSecItemNotFound) - && dsOK && isOK - dispatchMainAsync { - if success { - result(true) - } else { - result(FlutterError(code: Constants.authFailed, message: "Error deleting the biometric key", details: nil)) - } + let canEvaluate = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + + var availableBiometrics: [BiometricType?] = [] + if canEvaluate { + // Basic detection based on biometryType + if #available(iOS 11.0, *) { + switch context.biometryType { + case .faceID: availableBiometrics.append(.face) + case .touchID: availableBiometrics.append(.fingerprint) + default: break + } + } } + + let hasEnrolled = error?.code != LAError.biometryNotEnrolled.rawValue + + completion(.success(BiometricAvailability( + canAuthenticate: canEvaluate, + hasEnrolledBiometrics: hasEnrolled, + availableBiometrics: availableBiometrics, + reason: error?.localizedDescription + ))) } - private func deleteExistingKeys() { - // Delete EC key pair - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - ] - SecItemDelete(ecKeyQuery as CFDictionary) - - // Delete encrypted RSA private key from Keychain - let encryptedKeyTag = getBiometricKeyTag() - let encryptedKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag - ] - SecItemDelete(encryptedKeyQuery as CFDictionary) - - // Also delete the baseline and invalidation setting to keep invariant: no baseline/setting without keys - _ = DomainState.deleteSaved() - _ = InvalidationSetting.delete() - } - - private func createKeys( - useDeviceCredentials: Bool, - useEc: Bool, - keyFormat: KeyFormat, - biometryCurrentSet: Bool, - enforceBiometric: Bool, - promptMessage: String, - result: @escaping FlutterResult + func createKeys( + config: CreateKeysConfig?, + keyFormat: KeyFormat, + promptMessage: String?, + completion: @escaping (Result) -> Void ) { - // Delete existing keys (and baseline) + // Extract config values with defaults + let useDeviceCredentials = config?.useDeviceCredentials ?? false + let biometryCurrentSet = config?.setInvalidatedByBiometricEnrollment ?? false + let signatureType = config?.signatureType ?? .rsa + let enforceBiometric = config?.enforceBiometric ?? false + let prompt = promptMessage ?? "Authenticate to create keys" + + // Always delete existing keys first deleteExistingKeys() - // Define the actual generation logic as a closure we can call later - let generateKeysBlock = { - // Generate EC key pair in Secure Enclave - let ecAccessControl = SecAccessControlCreateWithFlags( - kCFAllocatorDefault, - kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, - [.privateKeyUsage, useDeviceCredentials ? .userPresence : biometryCurrentSet ? .biometryCurrentSet : .biometryAny], - nil - ) - - guard let ecAccessControl = ecAccessControl else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, - message: "Failed to create access control for EC key", - details: nil)) - } - return - } - - let ecTag = Constants.ecKeyAlias - let ecKeyAttributes: [String: Any] = [ - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecAttrKeySizeInBits as String: 256, - kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, - kSecAttrAccessControl as String: ecAccessControl, - kSecPrivateKeyAttrs as String: [ - kSecAttrIsPermanent as String: true, - kSecAttrApplicationTag as String: ecTag - ] - ] - - var error: Unmanaged? - guard let ecPrivateKey = SecKeyCreateRandomKey(ecKeyAttributes as CFDictionary, &error) else { - self.dispatchMainAsync { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - result(FlutterError(code: Constants.authFailed, message: "Error generating EC key: \(msg)", details: nil)) - } - return - } - - guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error getting EC public key", details: nil)) - } - return - } - - // Persist domain-state baseline right after successful EC creation (only if biometry-invalidation is enabled) - if biometryCurrentSet { - DomainState.saveCurrent() - } - InvalidationSetting.save(biometryCurrentSet) - - if useEc { - // EC-only: return EC public key - guard let response = self.buildKeyResponse(publicKey: ecPublicKey, format: keyFormat, algorithm: "EC") else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to encode EC public key", details: nil)) - } - return - } - self.dispatchMainAsync { result(response) } - return - } - - // --- Hybrid path: generate RSA and wrap its private key with ECIES(X9.63/SHA-256/AES-GCM) - let rsaKeyAttributes: [String: Any] = [ - kSecAttrKeyType as String: kSecAttrKeyTypeRSA, - kSecAttrKeySizeInBits as String: 2048, - kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent as String: false] - ] - - guard let rsaPrivate = SecKeyCreateRandomKey(rsaKeyAttributes as CFDictionary, &error), - let rsaPublicKey = SecKeyCopyPublicKey(rsaPrivate) else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error generating RSA key pair", details: nil)) - } - return - } - - // Extract RSA private key data - guard let rsaPrivateKeyData = SecKeyCopyExternalRepresentation(rsaPrivate, &error) as Data? else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error extracting RSA private key data", details: nil)) - } - return - } - - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPublicKey, .encrypt, algorithm) else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC encryption algorithm not supported", details: nil)) - } - return - } - - guard let encryptedRSAKeyData = SecKeyCreateEncryptedData(ecPublicKey, algorithm, rsaPrivateKeyData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error encrypting RSA private key: \(msg)", details: nil)) - } - return - } - - // Store encrypted RSA private key data in Keychain - let encryptedKeyTag = self.getBiometricKeyTag() - let encryptedKeyAttributes: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag, - kSecValueData as String: encryptedRSAKeyData, - kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly - ] - SecItemDelete(encryptedKeyAttributes as CFDictionary) // Delete existing item - let status = SecItemAdd(encryptedKeyAttributes as CFDictionary, nil) - guard status == errSecSuccess else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error storing encrypted RSA private key in Keychain", details: nil)) - } - return - } - - guard let response = self.buildKeyResponse(publicKey: rsaPublicKey, format: keyFormat, algorithm: "RSA") else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to encode RSA public key", details: nil)) - } - return + let generateBlock = { + self.performKeyGeneration( + useDeviceCredentials: useDeviceCredentials, + biometryCurrentSet: biometryCurrentSet, + signatureType: signatureType, + keyFormat: keyFormat + ) { result in + completion(result) } - self.dispatchMainAsync { result(response) } } if enforceBiometric { let context = LAContext() context.localizedFallbackTitle = "" - context.localizedReason = promptMessage - - context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: promptMessage) { success, error in + context.localizedReason = prompt + + context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: prompt) { success, _ in if success { - // Continue to generation on background thread (already on bg thread from evaluatePolicy) - generateKeysBlock() + generateBlock() } else { - let errorMessage = error?.localizedDescription ?? "Authentication failed" - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: errorMessage, details: nil)) - } + completion(.success(KeyCreationResult(publicKey: nil, error: "Authentication failed", code: .userCanceled))) } } } else { - // Run immediately on a background queue to avoid blocking UI - DispatchQueue.global(qos: .userInitiated).async { - generateKeysBlock() + DispatchQueue.global(qos: .userInitiated).async { + generateBlock() } } } - private func createSignature(options: [String: Any]?, result: @escaping FlutterResult) { - let promptMessage = (options?["promptMessage"] as? String) ?? "Authenticate" - let keyFormat = KeyFormat.from(options?["keyFormat"]) - guard let payload = options?["payload"] as? String, - let dataToSign = payload.data(using: .utf8) else { - dispatchMainAsync { - result(FlutterError(code: Constants.invalidPayload, message: "Payload is required and must be valid UTF-8", details: nil)) - } - return + func createSignature( + payload: String, + config: CreateSignatureConfig?, + signatureFormat: SignatureFormat, + keyFormat: KeyFormat, + promptMessage: String?, + completion: @escaping (Result) -> Void + ) { + guard let dataToSign = payload.data(using: .utf8) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Invalid payload", code: .invalidInput))) + return } - - // Check if we should use EC-only mode by checking if RSA key exists - let encryptedKeyTag = getBiometricKeyTag() - let encryptedKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag, - kSecReturnData as String: true, - kSecMatchLimit as String: kSecMatchLimitOne - ] - var item: CFTypeRef? - let status = SecItemCopyMatching(encryptedKeyQuery as CFDictionary, &item) - if status != errSecSuccess { - let shouldMigrate = parseBool(options?["shouldMigrate"]) ?? false - if shouldMigrate { - self.migrateToSecureEnclave(options: options, keyFormat: keyFormat, result: result) { opts, res in - self.createSignature(options: opts, result: res) + + let prompt = promptMessage ?? "Authenticate" + let shouldMigrate = config?.shouldMigrate ?? false + + if hasRsaKey() { + performRsaSigning(dataToSign: dataToSign, prompt: prompt, signatureFormat: signatureFormat, keyFormat: keyFormat, completion: completion) + } else if shouldMigrate { + migrateToSecureEnclave(prompt: prompt) { result in + switch result { + case .success: + self.performRsaSigning(dataToSign: dataToSign, prompt: prompt, signatureFormat: signatureFormat, keyFormat: keyFormat, completion: completion) + case .failure(let error): + // If migration fails, returning error. + let msg = (error as? PigeonError)?.message ?? (error as NSError).localizedDescription + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Migration Error: \(msg)", code: .unknown))) } - } else { - // No RSA: EC-only signing - createECSignature( - dataToSign: dataToSign, - promptMessage: promptMessage, - keyFormat: keyFormat, - result: result - ) - } - return + } + } else { + // Fallback to EC signing + performEcSigning(dataToSign: dataToSign, prompt: prompt, signatureFormat: signatureFormat, keyFormat: keyFormat, completion: completion) } + } - // 1. Retrieve encrypted RSA private key from Keychain - guard let encryptedRSAKeyData = item as? Data else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to retrieve encrypted RSA key data", details: nil)) - } + private func migrateToSecureEnclave(prompt: String, completion: @escaping (Result) -> Void) { + // Generate EC key pair in Secure Enclave + let ecAccessControl = SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, + [.privateKeyUsage, .biometryAny], // Defaulting to biometryAny for migration + nil + ) + + guard let ecAccessControl = ecAccessControl else { + completion(.failure(PigeonError(code: "authFailed", message: "Failed to create access control for EC key", details: nil))) return } - // 2. Retrieve EC private key from Secure Enclave let ecTag = Constants.ecKeyAlias - - let context = LAContext() - context.localizedFallbackTitle = "" - context.localizedReason = promptMessage - - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, + let ecKeyAttributes: [String: Any] = [ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, - kSecUseAuthenticationContext as String: context, - kSecUseOperationPrompt as String: promptMessage + kSecAttrKeySizeInBits as String: 256, + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecAttrAccessControl as String: ecAccessControl, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: ecTag + ] ] - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC private key not found", details: nil)) - } + var error: Unmanaged? + guard let ecPrivateKey = SecKeyCreateRandomKey(ecKeyAttributes as CFDictionary, &error) else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" + completion(.failure(PigeonError(code: "authFailed", message: "Error generating EC key: \(msg)", details: nil))) return } - let ecPrivateKey = ecPrivateKeyRef as! SecKey - // 3. Decrypt RSA private key data using the EC private key - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPrivateKey, .decrypt, algorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC decryption algorithm not supported", details: nil)) - } + guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { + completion(.failure(PigeonError(code: "authFailed", message: "Error getting EC public key", details: nil))) return } - var error: Unmanaged? - guard var rsaPrivateKeyData = SecKeyCreateDecryptedData(ecPrivateKey, algorithm, encryptedRSAKeyData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error decrypting RSA private key: \(msg)", details: nil)) - } - return - } + // Save baseline after EC key creation (migration assumes biometry-any, so no baseline needed) + // But save the invalidation setting + InvalidationSetting.save(false) - // 4. Reconstruct RSA private key from data - let rsaKeyAttributes: [String: Any] = [ + let unencryptedKeyTag = Constants.biometricKeyAlias + let unencryptedKeyTagData = unencryptedKeyTag.data(using: .utf8)! + // Note: The legacy key was stored as kSecClassKey. The new wrapped key is kSecClassGenericPassword. + let unencryptedKeyQuery: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: unencryptedKeyTagData, kSecAttrKeyType as String: kSecAttrKeyTypeRSA, - kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, - kSecAttrKeySizeInBits as String: 2048 + kSecReturnData as String: true ] - guard let rsaPrivateKey = SecKeyCreateWithData(rsaPrivateKeyData as CFData, rsaKeyAttributes as CFDictionary, &error) else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error reconstructing RSA private key: \(msg)", details: nil)) - } - return - } - // 5. Sign data with RSA private key - let signAlgorithm = SecKeyAlgorithm.rsaSignatureMessagePKCS1v15SHA256 - guard SecKeyIsAlgorithmSupported(rsaPrivateKey, .sign, signAlgorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "RSA signing algorithm not supported", details: nil)) - } + var rsaItem: CFTypeRef? + let status = SecItemCopyMatching(unencryptedKeyQuery as CFDictionary, &rsaItem) + guard status == errSecSuccess else { + completion(.failure(PigeonError(code: "authFailed", message: "RSA private key not found in Keychain", details: nil))) return } - - guard let signature = SecKeyCreateSignature(rsaPrivateKey, signAlgorithm, dataToSign as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error signing data: \(msg)", details: nil)) - } + guard let rsaPrivateKeyData = rsaItem as? Data else { + completion(.failure(PigeonError(code: "authFailed", message: "Failed to retrieve RSA private key data", details: nil))) return } - // 6. Zero the decrypted RSA private key bytes in memory - rsaPrivateKeyData.resetBytes(in: 0..) -> Void + ) { + let prompt = promptMessage ?? "Authenticate" + let shouldMigrate = config?.shouldMigrate ?? false - // No RSA key found - check if should migrate - let shouldMigrate = parseBool(options?["shouldMigrate"]) ?? false - if shouldMigrate { - self.migrateToSecureEnclave(options: options, keyFormat: .base64, result: result) { opts, res in - self.decrypt(options: opts, result: res) - } - return + if hasRsaKey() { + performRsaDecryption(payload: payload, payloadFormat: payloadFormat, prompt: prompt, completion: completion) + } else if shouldMigrate { + migrateToSecureEnclave(prompt: prompt) { result in + switch result { + case .success: + self.performRsaDecryption(payload: payload, payloadFormat: payloadFormat, prompt: prompt, completion: completion) + case .failure(let error): + let msg = (error as? PigeonError)?.message ?? (error as NSError).localizedDescription + completion(.success(DecryptResult(decryptedData: nil, error: "Migration Error: \(msg)", code: .unknown))) + } + } + } else { + performEcDecryption(payload: payload, payloadFormat: payloadFormat, prompt: prompt, completion: completion) } - - // Try EC-only decryption - decryptWithECKey( - encryptedData: encryptedData, - promptMessage: promptMessage, - result: result - ) } - - // MARK: - Decryption Helper Functions - - /// Decrypt using hybrid RSA approach (RSA key wrapped by EC key in Secure Enclave) - private func decryptWithHybridRSA( - encryptedRSAKeyData: Data, - encryptedPayload: Data, - promptMessage: String, - result: @escaping FlutterResult - ) { - // 1. Retrieve EC private key from Secure Enclave - let ecTag = Constants.ecKeyAlias - let context = LAContext() - context.localizedFallbackTitle = "" - context.localizedReason = promptMessage + func deleteKeys(completion: @escaping (Result) -> Void) { + deleteExistingKeys() + completion(.success(true)) + } + + func getKeyInfo(checkValidity: Bool, keyFormat: KeyFormat, completion: @escaping (Result) -> Void) { + // Check EC key existence + let ecTag = Constants.ecKeyAlias let ecKeyQuery: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrApplicationTag as String: ecTag, kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecReturnRef as String: true, - kSecUseAuthenticationContext as String: context, - kSecUseOperationPrompt as String: promptMessage ] + var ecItem: CFTypeRef? + let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecItem) + let ecKeyExists = (ecStatus == errSecSuccess) + let ecKey = ecItem as! SecKey? - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC private key not found", details: nil)) - } - return - } - let ecPrivateKey = ecPrivateKeyRef as! SecKey + // Check if encrypted RSA key exists (hybrid mode) + let encryptedKeyTag = Constants.biometricKeyAlias + let encryptedKeyQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: encryptedKeyTag, + kSecAttrAccount as String: encryptedKeyTag, + kSecReturnData as String: true, + ] + var rsaItem: CFTypeRef? + let rsaStatus = SecItemCopyMatching(encryptedKeyQuery as CFDictionary, &rsaItem) + let rsaKeyExists = (rsaStatus == errSecSuccess) - // 2. Decrypt RSA private key data using the EC private key (ECIES) - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPrivateKey, .decrypt, algorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC decryption algorithm not supported", details: nil)) - } + // No keys exist + guard ecKeyExists else { + completion(.success(KeyInfo(exists: false))) return } - var error: Unmanaged? - guard var rsaPrivateKeyData = SecKeyCreateDecryptedData(ecPrivateKey, algorithm, encryptedRSAKeyData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error decrypting RSA private key: \(msg)", details: nil)) + // Determine validity + var isValid: Bool? = nil + if checkValidity { + let shouldInvalidateOnEnrollment = InvalidationSetting.load() ?? true + if shouldInvalidateOnEnrollment { + isValid = !DomainState.biometryChangedOrUnknown() + } else { + isValid = true } - return } - // 3. Reconstruct RSA private key from data - let rsaKeyAttributes: [String: Any] = [ - kSecAttrKeyType as String: kSecAttrKeyTypeRSA, - kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, - kSecAttrKeySizeInBits as String: 2048 - ] - guard let rsaPrivateKey = SecKeyCreateWithData(rsaPrivateKeyData as CFData, rsaKeyAttributes as CFDictionary, &error) else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error reconstructing RSA private key: \(msg)", details: nil)) - } - return - } - - // 4. Decrypt payload with RSA private key - let decryptAlgorithm = SecKeyAlgorithm.rsaEncryptionPKCS1 - guard SecKeyIsAlgorithmSupported(rsaPrivateKey, .decrypt, decryptAlgorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "RSA decryption algorithm not supported", details: nil)) - } - return - } - - guard let decryptedData = SecKeyCreateDecryptedData(rsaPrivateKey, decryptAlgorithm, encryptedPayload as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error decrypting payload: \(msg)", details: nil)) + // For EC-only mode + if ecKeyExists && !rsaKeyExists { + guard let ecPublicKey = ecKey.flatMap({ SecKeyCopyPublicKey($0) }) else { + completion(.success(KeyInfo(exists: true, isValid: isValid, algorithm: "EC", keySize: 256, isHybridMode: false))) + return } - return - } + + let publicKeyStr = formatKey(ecPublicKey, format: keyFormat) + + completion(.success(KeyInfo( + exists: true, + isValid: isValid, + algorithm: "EC", + keySize: 256, + isHybridMode: false, + publicKey: publicKeyStr, + decryptingPublicKey: nil, + decryptingAlgorithm: nil, + decryptingKeySize: nil + ))) + return + } + + // Hybrid RSA mode: Software RSA for BOTH signing and decryption + // RSA key is wrapped with EC; we cannot retrieve RSA public key without auth + // Note: publicKey is nil because RSA key requires biometric auth to unwrap + completion(.success(KeyInfo( + exists: true, + isValid: isValid, + algorithm: "RSA", + keySize: 2048, + isHybridMode: true, + publicKey: nil, // RSA public key requires authentication to unwrap + decryptingPublicKey: nil, + decryptingAlgorithm: nil, + decryptingKeySize: nil + ))) + } - // 5. Zero the decrypted RSA private key bytes in memory - rsaPrivateKeyData.resetBytes(in: 0..) -> Void + ) { + // Access Control + let flags: SecAccessControlCreateFlags = [.privateKeyUsage, useDeviceCredentials ? .userPresence : (biometryCurrentSet ? .biometryCurrentSet : .biometryAny)] + guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, flags, nil) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "Failed to create access control", code: .unknown))) return } - - dispatchMainAsync { result(["decryptedData": decryptedString]) } - } - - /// Decrypt using EC-only key (direct ECIES decryption) - private func decryptWithECKey( - encryptedData: Data, - promptMessage: String, - result: @escaping FlutterResult - ) { - // 1. Retrieve EC private key from Secure Enclave + + // Create EC Key let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, + let ecAttributes: [String: Any] = [ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, - kSecUseOperationPrompt as String: promptMessage + kSecAttrKeySizeInBits as String: 256, + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecAttrAccessControl as String: accessControl, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: ecTag + ] ] - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC private key not found. Please create keys first.", details: nil)) - } - return + var error: Unmanaged? + guard let ecPrivateKey = SecKeyCreateRandomKey(ecAttributes as CFDictionary, &error) else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "EC Key Gen Error: \(msg)", code: .unknown))) + return } - let ecPrivateKey = ecPrivateKeyRef as! SecKey - - // 2. Decrypt payload directly using ECIES - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPrivateKey, .decrypt, algorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "ECIES decryption algorithm not supported", details: nil)) - } - return + + // Save metadata + if biometryCurrentSet { DomainState.saveCurrent() } + InvalidationSetting.save(biometryCurrentSet) + + guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "EC Pub Key Error", code: .unknown))) + return + } + + if signatureType == .ecdsa { + let keyStr = formatKey(ecPublicKey, format: keyFormat) + let data = SecKeyCopyExternalRepresentation(ecPublicKey, &error) as Data? + let typedData = data != nil ? FlutterStandardTypedData(bytes: data!) : nil + completion(.success(KeyCreationResult( + publicKey: keyStr, + publicKeyBytes: typedData, + error: nil, + code: .success, + algorithm: "EC", + keySize: 256, + decryptingPublicKey: nil, + decryptingAlgorithm: nil, + decryptingKeySize: nil, + isHybridMode: false + ))) + return } - - var error: Unmanaged? - guard let decryptedData = SecKeyCreateDecryptedData(ecPrivateKey, algorithm, encryptedData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error decrypting payload with EC key: \(msg)", details: nil)) - } - return + + // Check encryption support for Hybrid + guard SecKeyIsAlgorithmSupported(ecPublicKey, .encrypt, .eciesEncryptionStandardX963SHA256AESGCM) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "ECIES not supported", code: .unknown))) + return } - guard let decryptedString = String(data: decryptedData, encoding: .utf8) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Decrypted data is not valid UTF-8", details: nil)) - } - return + // Generate RSA Key + let rsaAttributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeRSA, + kSecAttrKeySizeInBits as String: 2048, + kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent as String: false] + ] + guard let rsaPrivateKey = SecKeyCreateRandomKey(rsaAttributes as CFDictionary, &error) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "RSA Gen Error", code: .unknown))) + return } - - dispatchMainAsync { result(["decryptedData": decryptedString]) } - } - - /// Helper method for testing EC encryption using native iOS ECIES - /// This is only for testing/example purposes - private func testEncrypt(options: [String: Any]?, result: @escaping FlutterResult) { - guard let payload = options?["payload"] as? String else { - dispatchMainAsync { - result(FlutterError(code: Constants.invalidPayload, message: "Payload is required", details: nil)) - } - return + + // Wrap RSA Private Key + guard let rsaPrivateData = SecKeyCopyExternalRepresentation(rsaPrivateKey, &error) as Data?, + let encryptedRsa = SecKeyCreateEncryptedData(ecPublicKey, .eciesEncryptionStandardX963SHA256AESGCM, rsaPrivateData as CFData, &error) as Data? else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "RSA Wrapping Error", code: .unknown))) + return } - // Retrieve EC public key - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true + // Save Wrapped Key + let tag = Constants.biometricKeyAlias + let saveQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: tag, + kSecAttrAccount as String: tag, + kSecValueData as String: encryptedRsa, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] + SecItemAdd(saveQuery as CFDictionary, nil) - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC key not found", details: nil)) - } - return - } - let ecPrivateKey = ecPrivateKeyRef as! SecKey - guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC public key not found", details: nil)) - } - return + guard let rsaPublicKey = SecKeyCopyPublicKey(rsaPrivateKey) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "RSA Pub Key Error", code: .unknown))) + return } - // Encrypt using native ECIES - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPublicKey, .encrypt, algorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "ECIES encryption algorithm not supported", details: nil)) - } - return - } + let rsaData = SecKeyCopyExternalRepresentation(rsaPublicKey, &error) as Data? + let rsaTypedData = rsaData != nil ? FlutterStandardTypedData(bytes: rsaData!) : nil - guard let payloadData = payload.data(using: .utf8) else { - dispatchMainAsync { - result(FlutterError(code: Constants.invalidPayload, message: "Payload must be valid UTF-8", details: nil)) - } - return + let rsaKeyStr = formatKey(rsaPublicKey, format: keyFormat) + + completion(.success(KeyCreationResult( + publicKey: rsaKeyStr, + publicKeyBytes: rsaTypedData, + error: nil, + code: .success, + algorithm: "RSA", + keySize: 2048 + ))) + } + + private func performRsaSigning(dataToSign: Data, prompt: String, signatureFormat: SignatureFormat, keyFormat: KeyFormat, completion: @escaping (Result) -> Void) { + guard let rsaPrivateKey = unwrapRsaKey(prompt: prompt) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Failed to access/unwrap RSA key", code: .unknown))) + return } var error: Unmanaged? - guard let encryptedData = SecKeyCreateEncryptedData(ecPublicKey, algorithm, payloadData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error encrypting: \(msg)", details: nil)) - } - return + guard let signature = SecKeyCreateSignature(rsaPrivateKey, .rsaSignatureMessagePKCS1v15SHA256, dataToSign as CFData, &error) as Data? else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Signing Error: \(msg)", code: .unknown))) + return } - let base64Encrypted = encryptedData.base64EncodedString() - dispatchMainAsync { result(["encryptedPayload": base64Encrypted]) } - } - - private func createECSignature( - dataToSign: Data, - promptMessage: String, - keyFormat: KeyFormat, - result: @escaping FlutterResult - ) { - // Retrieve EC private key from Secure Enclave - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, - kSecUseOperationPrompt as String: promptMessage - ] - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC private key not found", details: nil)) - } - return + guard let pub = SecKeyCopyPublicKey(rsaPrivateKey) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Pub Key Error", code: .unknown))) + return } - let ecPrivateKey = ecPrivateKeyRef as! SecKey - - guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC public key not found", details: nil)) - } - return - } - - // Sign data with EC private key - let signAlgorithm = SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256 - guard SecKeyIsAlgorithmSupported(ecPrivateKey, .sign, signAlgorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC signing algorithm not supported", details: nil)) - } - return + + completion(.success(SignatureResult( + signature: formatSignature(signature, format: signatureFormat), + signatureBytes: FlutterStandardTypedData(bytes: signature), + publicKey: formatKey(pub, format: keyFormat), + error: nil, + code: .success, + algorithm: "RSA", + keySize: 2048 + ))) + } + + private func performEcSigning(dataToSign: Data, prompt: String, signatureFormat: SignatureFormat, keyFormat: KeyFormat, completion: @escaping (Result) -> Void) { + guard let ecKey = getEcPrivateKey(prompt: prompt) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "EC Key not found or auth failed", code: .unknown))) + return } - + var error: Unmanaged? - guard let signature = SecKeyCreateSignature(ecPrivateKey, signAlgorithm, dataToSign as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error signing data with EC key: \(msg)", details: nil)) - } - return + guard let signature = SecKeyCreateSignature(ecKey, .ecdsaSignatureMessageX962SHA256, dataToSign as CFData, &error) as Data? else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Signing Error: \(msg)", code: .unknown))) + return } - guard let response = buildSignatureResponse( - publicKey: ecPublicKey, - signature: signature, - algorithm: "EC", - format: keyFormat - ) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to format EC signature", details: nil)) - } - return + guard let pub = SecKeyCopyPublicKey(ecKey) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Pub Key Error", code: .unknown))) + return } - - dispatchMainAsync { result(response) } + + completion(.success(SignatureResult( + signature: formatSignature(signature, format: signatureFormat), + signatureBytes: FlutterStandardTypedData(bytes: signature), + publicKey: formatKey(pub, format: keyFormat), + error: nil, + code: .success, + algorithm: "EC", + keySize: 256 + ))) } - - private func migrateToSecureEnclave( - options: [String: Any]?, - keyFormat: KeyFormat, - result: @escaping FlutterResult, - onSuccess: @escaping ([String: Any]?, @escaping FlutterResult) -> Void - ) { - // Generate EC key pair in Secure Enclave - let ecAccessControl = SecAccessControlCreateWithFlags( - kCFAllocatorDefault, - kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, - [.privateKeyUsage, .biometryAny], - nil - ) - - guard let ecAccessControl = ecAccessControl else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to create access control for EC key", details: nil)) - } - return + + private func performRsaDecryption(payload: String, payloadFormat: PayloadFormat, prompt: String, completion: @escaping (Result) -> Void) { + guard let rsaPrivateKey = unwrapRsaKey(prompt: prompt) else { + completion(.success(DecryptResult(decryptedData: nil, error: "Failed to access/unwrap RSA key", code: .unknown))) + return } - - let ecTag = Constants.ecKeyAlias - let ecKeyAttributes: [String: Any] = [ - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecAttrKeySizeInBits as String: 256, - kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, - kSecAttrAccessControl as String: ecAccessControl, - kSecPrivateKeyAttrs as String: [ - kSecAttrIsPermanent as String: true, - kSecAttrApplicationTag as String: ecTag - ] - ] - + var error: Unmanaged? - guard let ecPrivateKey = SecKeyCreateRandomKey(ecKeyAttributes as CFDictionary, &error) else { - dispatchMainAsync { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - result(FlutterError(code: Constants.authFailed, message: "Error generating EC key: \(msg)", details: nil)) - } - return + guard let encryptedData = parsePayload(payload, format: payloadFormat) else { + completion(.success(DecryptResult(decryptedData: nil, error: "Invalid payload", code: .invalidInput))) + return } - - guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error getting EC public key", details: nil)) - } - return + + guard let decrypted = SecKeyCreateDecryptedData(rsaPrivateKey, .rsaEncryptionPKCS1, encryptedData as CFData, &error) as Data?, + let str = String(data: decrypted, encoding: .utf8) else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(DecryptResult(decryptedData: nil, error: "Decryption Error: \(msg)", code: .unknown))) + return } - - // Save baseline after EC key creation (migration assumes biometry-any, so no baseline needed) - // But save the invalidation setting - InvalidationSetting.save(false) - - let unencryptedKeyTag = getBiometricKeyTag() - let unencryptedKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: unencryptedKeyTag, - kSecAttrKeyType as String: kSecAttrKeyTypeRSA, - kSecReturnData as String: true - ] - - var rsaItem: CFTypeRef? - let status = SecItemCopyMatching(unencryptedKeyQuery as CFDictionary, &rsaItem) - guard status == errSecSuccess else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "RSA private key not found in Keychain", details: nil)) - } - return + + completion(.success(DecryptResult(decryptedData: str, error: nil, code: .success))) + } + + private func performEcDecryption(payload: String, payloadFormat: PayloadFormat, prompt: String, completion: @escaping (Result) -> Void) { + guard let ecKey = getEcPrivateKey(prompt: prompt) else { + completion(.success(DecryptResult(decryptedData: nil, error: "EC Key not found or auth failed", code: .unknown))) + return } - guard var rsaPrivateKeyData = rsaItem as? Data else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to retrieve RSA private key data", details: nil)) - } - return + + guard let encryptedData = parsePayload(payload, format: payloadFormat) else { + completion(.success(DecryptResult(decryptedData: nil, error: "Invalid payload", code: .invalidInput))) + return } - - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPublicKey, .encrypt, algorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC encryption algorithm not supported", details: nil)) - } - return + + var error: Unmanaged? + guard let decrypted = SecKeyCreateDecryptedData(ecKey, .eciesEncryptionStandardX963SHA256AESGCM, encryptedData as CFData, &error) as Data?, + let str = String(data: decrypted, encoding: .utf8) else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(DecryptResult(decryptedData: nil, error: "Decryption Error: \(msg)", code: .unknown))) + return } + completion(.success(DecryptResult(decryptedData: str, error: nil, code: .success))) + } - guard let encryptedRSAKeyData = SecKeyCreateEncryptedData(ecPublicKey, algorithm, rsaPrivateKeyData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error encrypting RSA private key: \(msg)", details: nil)) - } - return - } + // MARK: - Helpers - let encryptedKeyAttributes: [String: Any] = [ + private func deleteExistingKeys() { + let ecQuery: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: Constants.ecKeyAlias, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom + ] + SecItemDelete(ecQuery as CFDictionary) + + let rsaTag = Constants.biometricKeyAlias + let rsaQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: unencryptedKeyTag, - kSecAttrAccount as String: unencryptedKeyTag, - kSecValueData as String: encryptedRSAKeyData, - kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly + kSecAttrService as String: rsaTag, + kSecAttrAccount as String: rsaTag ] - - SecItemDelete(encryptedKeyAttributes as CFDictionary) // Delete any existing item - let storeStatus = SecItemAdd(encryptedKeyAttributes as CFDictionary, nil) - if storeStatus != errSecSuccess { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error storing encrypted RSA private key in Keychain", details: nil)) - } - return - } - - SecItemDelete(unencryptedKeyQuery as CFDictionary) - rsaPrivateKeyData.resetBytes(in: 0.. [String: Any]? { - guard let formatted = formatPublicKey(publicKey, format: format) else { return nil } - var response: [String: Any] = [ - "publicKey": formatted.value, - "publicKeyFormat": formatted.format.channelValue, - "algorithm": algorithm, - "keySize": keySizeInBits(publicKey), - "keyFormat": format.channelValue + + private func hasRsaKey() -> Bool { + let tag = Constants.biometricKeyAlias + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: tag, + kSecAttrAccount as String: tag, + kSecReturnData as String: false, + kSecMatchLimit as String: kSecMatchLimitOne ] - if let label = formatted.pemLabel { - response["publicKeyPemLabel"] = label - } - return response + var item: CFTypeRef? + return SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess } - - private func buildSignatureResponse(publicKey: SecKey, signature: Data, algorithm: String, format: KeyFormat) -> [String: Any]? { - guard let formattedKey = formatPublicKey(publicKey, format: format) else { return nil } - let formattedSignature = formatSignature(signature, format: format) - var response: [String: Any] = [ - "publicKey": formattedKey.value, - "publicKeyFormat": formattedKey.format.channelValue, - "signature": formattedSignature.value, - "signatureFormat": formattedSignature.format.channelValue, - "algorithm": algorithm, - "keySize": keySizeInBits(publicKey), - "timestamp": isoTimestamp(), - "keyFormat": format.channelValue + + private func getEcPrivateKey(prompt: String) -> SecKey? { + let tag = Constants.ecKeyAlias + let context = LAContext() + context.localizedReason = prompt + + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecReturnRef as String: true, + kSecUseAuthenticationContext as String: context ] - if let keyLabel = formattedKey.pemLabel { - response["publicKeyPemLabel"] = keyLabel - } - if let signatureLabel = formattedSignature.pemLabel { - response["signaturePemLabel"] = signatureLabel + + var item: CFTypeRef? + if SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess { + return (item as! SecKey) } - return response - } - - private func formatPublicKey(_ key: SecKey, format: KeyFormat) -> FormattedOutput? { - guard let data = subjectPublicKeyInfo(for: key) else { return nil } - return formatData(data, format: format, pemLabel: "PUBLIC KEY") + return nil } - - private func formatSignature(_ data: Data, format: KeyFormat) -> FormattedOutput { - return formatData(data, format: format, pemLabel: "SIGNATURE") + + private func unwrapRsaKey(prompt: String) -> SecKey? { + // 1. Get Wrapped Data + let tag = Constants.biometricKeyAlias + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: tag, + kSecAttrAccount as String: tag, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + var item: CFTypeRef? + guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, + let wrappedData = item as? Data else { return nil } + + // 2. Get EC Key (Auth logic handled by Secure Enclave) + guard let ecKey = getEcPrivateKey(prompt: prompt) else { return nil } + + // 3. Unwrap + var error: Unmanaged? + guard let rsaData = SecKeyCreateDecryptedData(ecKey, .eciesEncryptionStandardX963SHA256AESGCM, wrappedData as CFData, &error) as Data? else { + return nil + } + + // 4. Restore Key + let attrs: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeRSA, + kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, + kSecAttrKeySizeInBits as String: 2048 + ] + return SecKeyCreateWithData(rsaData as CFData, attrs as CFDictionary, nil) } - private func formatData(_ data: Data, format: KeyFormat, pemLabel: String) -> FormattedOutput { + private func formatKey(_ key: SecKey, format: KeyFormat) -> String { + guard let data = subjectPublicKeyInfo(for: key) else { return "" } + switch format { - case .base64: - return FormattedOutput(value: data.base64EncodedString(), format: .base64, pemLabel: nil) - case .hex: - return FormattedOutput(value: hexString(from: data), format: .hex, pemLabel: nil) - case .raw: - return FormattedOutput(value: FlutterStandardTypedData(bytes: data), format: .raw, pemLabel: nil) + case .base64, .raw: + return data.base64EncodedString() case .pem: - let body = chunkedBase64(data.base64EncodedString()) - let pem = "-----BEGIN \(pemLabel)-----\n\(body)\n-----END \(pemLabel)-----" - return FormattedOutput(value: pem, format: .pem, pemLabel: pemLabel) + let base64 = data.base64EncodedString(options: [.lineLength64Characters, .endLineWithLineFeed]) + return "-----BEGIN PUBLIC KEY-----\n\(base64)\n-----END PUBLIC KEY-----" + case .hex: + return data.map { String(format: "%02x", $0) }.joined() } } - + private func subjectPublicKeyInfo(for key: SecKey) -> Data? { var error: Unmanaged? - guard let publicKeyData = SecKeyCopyExternalRepresentation(key, &error) as Data? else { - return nil - } - let attributes = SecKeyCopyAttributes(key) as? [String: Any] - let keyType = attributes?[kSecAttrKeyType as String] as? String - let isEc = keyType == (kSecAttrKeyTypeECSECPrimeRandom as String) || publicKeyData.count == 65 - return BiometricSignaturePlugin.addHeader(publicKeyData: publicKeyData, isEc: isEc) - } - - private func keySizeInBits(_ key: SecKey) -> Int { + guard let rawData = SecKeyCopyExternalRepresentation(key, &error) as Data? else { return nil } + guard let attributes = SecKeyCopyAttributes(key) as? [String: Any], - let bits = attributes[kSecAttrKeySizeInBits as String] as? Int else { - return 0 - } - return bits - } + let keyType = attributes[kSecAttrKeyType as String] as? String else { return rawData } - private func isoTimestamp() -> String { - return iso8601Formatter.string(from: Date()) - } - - private func chunkedBase64(_ string: String, chunkSize: Int = 64) -> String { - guard !string.isEmpty else { return string } - var chunks: [String] = [] - var index = string.startIndex - while index < string.endIndex { - let end = string.index(index, offsetBy: chunkSize, limitedBy: string.endIndex) ?? string.endIndex - chunks.append(String(string[index.. String { - return data.map { String(format: "%02x", $0) }.joined() + + return rawData } - - private func parseBool(_ value: Any?) -> Bool? { - if let boolValue = value as? Bool { - return boolValue - } - if let numberValue = value as? NSNumber { - return numberValue.boolValue - } - if let stringValue = value as? String { - return Bool(stringValue) + + private func encodeASN1Content(tag: UInt8, content: Data) -> Data { + var data = Data() + data.append(tag) + let length = content.count + + if length < 128 { + data.append(UInt8(length)) + } else if length < 256 { + data.append(0x81) + data.append(UInt8(length)) + } else if length < 65536 { + data.append(0x82) + data.append(UInt8(length >> 8)) + data.append(UInt8(length & 0xFF)) + } else { + data.append(0x83) + data.append(UInt8(length >> 16)) + data.append(UInt8((length >> 8) & 0xFF)) + data.append(UInt8(length & 0xFF)) } - return nil - } - - private func dispatchMainAsync(_ block: @escaping () -> Void) { - DispatchQueue.main.async(execute: block) - } - - private func getBiometricType(_ context: LAContext?) -> String { - return context?.biometryType == .faceID ? "FaceID" : - context?.biometryType == .touchID ? "TouchID" : "none, NO_BIOMETRICS" + + data.append(content) + return data } - - private func doesBiometricKeyExist(checkValidity: Bool = false) -> Bool { - // Check EC key existence - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, - ] - var ecItem: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecItem) - let ecKeyExists = (ecStatus == errSecSuccess) - - // Check if encrypted RSA key exists - let encryptedKeyTag = getBiometricKeyTag() - let encryptedKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag, - kSecReturnData as String: true, - ] - var rsaItem: CFTypeRef? - let rsaStatus = SecItemCopyMatching(encryptedKeyQuery as CFDictionary, &rsaItem) - let rsaKeyExists = (rsaStatus == errSecSuccess) - - // For EC-only mode, only EC key needs to exist - if ecKeyExists && !rsaKeyExists { - guard checkValidity else { return true } - - // Check if invalidation was enabled for this key - let shouldInvalidateOnEnrollment = InvalidationSetting.load() ?? true - - // Only check domain state if invalidation is enabled - if shouldInvalidateOnEnrollment { - return !DomainState.biometryChangedOrUnknown() - } - - // If invalidation is disabled (biometryAny), key remains valid - return true - } - - // Hybrid: both must exist - guard ecKeyExists, rsaKeyExists else { return false } - guard checkValidity else { return true } - - // Check if invalidation was enabled for this key - let shouldInvalidateOnEnrollment = InvalidationSetting.load() ?? true - - // Only check domain state if invalidation is enabled - if shouldInvalidateOnEnrollment { - return !DomainState.biometryChangedOrUnknown() + + private func formatSignature(_ data: Data, format: SignatureFormat) -> String { + switch format { + case .base64, .raw: + return data.base64EncodedString() + case .hex: + return data.map { String(format: "%02x", $0) }.joined() } - - // If invalidation is disabled (biometryAny), key remains valid - return true - } - - private func getBiometricKeyTag() -> Data { - let BIOMETRIC_KEY_ALIAS = Constants.biometricKeyAlias - return BIOMETRIC_KEY_ALIAS.data(using: .utf8)! - } - - private static let encodedRSAEncryptionOID: [UInt8] = [ - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00 - ] - - private static let encodedECEncryptionOID: [UInt8] = [ - 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, - 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07 - ] - - private static func addHeader(publicKeyData: Data?, isEc: Bool = false) -> Data? { - guard let publicKeyData = publicKeyData else { return nil } - return isEc ? addECHeader(publicKeyData: publicKeyData) : addRSAHeader(publicKeyData: publicKeyData) - } - - private static func addRSAHeader(publicKeyData: Data?) -> Data? { - guard let publicKeyData = publicKeyData else { return nil } - var builder = [UInt8](repeating: 0, count: 15) - var encKey = Data() - let bitLen: UInt = (publicKeyData.count + 1 < 128) ? 1 : UInt(((publicKeyData.count + 1) / 256) + 2) - builder[0] = 0x30 - let i = encodedRSAEncryptionOID.count + 2 + Int(bitLen) + publicKeyData.count - var j = encodedLength(&builder[1], i) - encKey.append(&builder, count: Int(j + 1)) - encKey.append(encodedRSAEncryptionOID, count: encodedRSAEncryptionOID.count) - builder[0] = 0x03 - j = encodedLength(&builder[1], publicKeyData.count + 1) - builder[j + 1] = 0x00 - encKey.append(&builder, count: Int(j + 2)) - encKey.append(publicKeyData) - return encKey - } - - private static func addECHeader(publicKeyData: Data?) -> Data? { - guard let publicKeyData = publicKeyData else { return nil } - var builder = [UInt8](repeating: 0, count: 15) - var encKey = Data() - let bitLen: UInt = (publicKeyData.count + 1 < 128) ? 1 : UInt(((publicKeyData.count + 1) / 256) + 2) - builder[0] = 0x30 - let i = encodedECEncryptionOID.count + 2 + Int(bitLen) + publicKeyData.count - var j = encodedLength(&builder[1], i) - encKey.append(&builder, count: Int(j + 1)) - encKey.append(encodedECEncryptionOID, count: encodedECEncryptionOID.count) - builder[0] = 0x03 - j = encodedLength(&builder[1], publicKeyData.count + 1) - builder[j + 1] = 0x00 - encKey.append(&builder, count: Int(j + 2)) - encKey.append(publicKeyData) - return encKey } - - private static func encodedLength(_ buf: UnsafeMutablePointer?, _ length: size_t) -> size_t { - var length = length - if length < 128 { - buf?[0] = UInt8(length) - return 1 - } - let i: size_t = (length / 256) + 1 - buf?[0] = UInt8(i + 0x80) - for j in 0..>= 8 + + private func parsePayload(_ payload: String, format: PayloadFormat) -> Data? { + switch format { + case .base64: + return Data(base64Encoded: payload, options: .ignoreUnknownCharacters) + case .hex: + return parseHex(payload) + case .raw: + return Data(base64Encoded: payload, options: .ignoreUnknownCharacters) // Raw assumes base64 input string for transport } - return i + 1 + } + + private func parseHex(_ hex: String) -> Data? { + var data = Data() + var hexStr = hex + if hexStr.count % 2 != 0 { hexStr = "0" + hexStr } + for i in stride(from: 0, to: hexStr.count, by: 2) { + let start = hexStr.index(hexStr.startIndex, offsetBy: i) + let end = hexStr.index(start, offsetBy: 2) + guard let byte = UInt8(hexStr[start.. this == AndroidSignatureType.ECDSA; -} diff --git a/lib/biometric_signature.dart b/lib/biometric_signature.dart index e045334..77b46a1 100644 --- a/lib/biometric_signature.dart +++ b/lib/biometric_signature.dart @@ -1,162 +1,128 @@ -import 'android_config.dart'; import 'biometric_signature_platform_interface.dart'; -import 'decryption_options.dart'; -import 'ios_config.dart'; -import 'key_material.dart'; -import 'macos_config.dart'; -import 'signature_options.dart'; -export 'android_config.dart'; -export 'decryption_options.dart'; -export 'ios_config.dart'; -export 'key_material.dart'; -export 'macos_config.dart'; -export 'signature_options.dart'; +export 'biometric_signature_platform_interface.dart' + show + CreateKeysConfig, + CreateSignatureConfig, + DecryptConfig, + SignatureType, + KeyFormat, + SignatureFormat, + PayloadFormat, + BiometricError, + BiometricType, + BiometricAvailability, + KeyCreationResult, + SignatureResult, + DecryptResult, + KeyInfo; /// High-level API for interacting with the Biometric Signature plugin. -/// -/// This class provides a uniform Flutter interface over platform-specific -/// hardware-backed signing and secure decryption workflows. When supported, -/// keys are always created inside secure hardware: -/// -/// - **Android**: Android Keystore / StrongBox -/// - **iOS**: Secure Enclave -/// - **macOS**: Secure Enclave -/// -/// Hybrid modes are used automatically when hardware keys cannot perform the -/// required decryption operation (for example, ECIES on Android or RSA PKCS#1 -/// decryption on iOS/macOS). class BiometricSignature { - /// Creates a new biometric-protected key pair used for signing - /// (and optionally decryption, depending on the platform and configuration). + /// Creates a new biometric-protected key pair. /// - /// ## Key Modes + /// [config] contains platform-specific options. See [CreateKeysConfig] for + /// available options and which platforms they apply to. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown during biometric authentication. /// - /// The plugin automatically selects the appropriate mode: - /// - /// - **RSA Mode** - /// Hardware RSA-2048 key pair supporting SHA256withRSA signatures. - /// Optional RSA/PKCS#1 decryption when enabled. Private key never leaves - /// secure hardware. - /// - /// - **EC Signing-Only** - /// Hardware-backed P-256 EC key pair supporting ECDSA signatures only. - /// Decryption is not supported in this mode. - /// - /// - **Hybrid EC Mode** - /// Used when EC signing is required but the platform cannot perform ECIES - /// decryption inside hardware. - /// - /// - **Android** - /// Hardware EC signing key + software EC key for ECIES decryption. - /// The software EC private key is encrypted using a biometric-protected - /// AES-256 master key (stored in Keystore/StrongBox). The wrapped key - /// itself is stored in app-private files with MODE_PRIVATE permissions. - /// - /// - **iOS/macOS** - /// Hardware EC signing key + software RSA private key for PKCS#1 - /// decryption. The software RSA key is encrypted using ECIES with - /// Secure Enclave EC public key material and stored in Keychain. - /// - /// The returned [KeyCreationResult] includes the public key in the requested - /// output format. - Future createKeys({ - AndroidConfig? androidConfig, - IosConfig? iosConfig, - MacosConfig? macosConfig, + /// Returns a [KeyCreationResult] containing the public key or error details. + Future createKeys({ + CreateKeysConfig? config, KeyFormat keyFormat = KeyFormat.base64, - bool enforceBiometric = false, String? promptMessage, }) async { - final response = await BiometricSignaturePlatform.instance.createKeys( - androidConfig ?? - AndroidConfig( - useDeviceCredentials: false, - setInvalidatedByBiometricEnrollment: true, - ), - iosConfig ?? - IosConfig(useDeviceCredentials: false, biometryCurrentSet: true), - macosConfig ?? - MacosConfig(useDeviceCredentials: false, biometryCurrentSet: true), - keyFormat: keyFormat, - enforceBiometric: enforceBiometric, - promptMessage: promptMessage, + return BiometricSignaturePlatform.instance.createKeys( + config, + keyFormat, + promptMessage, ); - - return response == null ? null : KeyCreationResult.fromChannel(response); } /// Creates a digital signature using biometric authentication. /// - /// ## Algorithms - /// - /// - **RSA**: SHA256withRSA (PKCS#1 v1.5) - /// - **EC**: - /// - Android: SHA256withECDSA - /// - iOS: ecdsaSignatureMessageX962SHA256 (ANSI X9.62 format) - /// - /// Hybrid EC mode always uses the hardware EC signing key. - /// - /// The returned [SignatureResult] contains both the signature and the - /// corresponding public key in the requested format. - Future createSignature(SignatureOptions options) async { - final response = await BiometricSignaturePlatform.instance.createSignature( - options, + /// [payload] is the data to sign. + /// [config] contains platform-specific options. See [CreateSignatureConfig]. + /// [signatureFormat] specifies the output format for the signature. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown during biometric authentication. + /// + /// Returns a [SignatureResult] containing the signature or error details. + Future createSignature({ + required String payload, + CreateSignatureConfig? config, + SignatureFormat signatureFormat = SignatureFormat.base64, + KeyFormat keyFormat = KeyFormat.base64, + String? promptMessage, + }) async { + return BiometricSignaturePlatform.instance.createSignature( + payload, + config, + signatureFormat, + keyFormat, + promptMessage, ); - return response == null ? null : SignatureResult.fromChannel(response); } - /// Decrypts a Base64-encoded payload using biometric authentication. + /// Decrypts data using biometric authentication. /// - /// ## Supported Algorithms + /// Note: Not supported on Windows. /// - /// - **RSA** - /// RSA/ECB/PKCS1Padding (Android and iOS hybrid mode). + /// [payload] is the encrypted data. + /// [payloadFormat] specifies the format of the encrypted data. + /// [config] contains platform-specific options. See [DecryptConfig]. + /// [promptMessage] is the message shown during biometric authentication. /// - /// - **ECIES** - /// P-256 ECIES using ECDH → X9.63 KDF (SHA-256) → AES-128-GCM. - /// - /// - **Android** - /// Manual ECIES implementation (ECDH → X9.63 KDF → AES-GCM). - /// The wrapped software EC private key is read from app-private file storage - /// and unwrapped using a biometric-protected AES-256 master key from Keystore/StrongBox. - /// All sensitive key material is zeroized immediately after use. - /// - /// - **iOS** - /// Native ECIES using `SecKeyAlgorithm.eciesEncryptionStandardX963SHA256AESGCM`. - /// EC-only mode uses direct ECIES decryption; hybrid mode unwraps the RSA key - /// from Keychain before performing RSA decryption. - Future decrypt(DecryptionOptions options) async { - final response = await BiometricSignaturePlatform.instance.decrypt(options); - return response == null ? null : DecryptResult.fromChannel(response); + /// Returns a [DecryptResult] containing the decrypted data or error details. + Future decrypt({ + required String payload, + required PayloadFormat payloadFormat, + DecryptConfig? config, + String? promptMessage, + }) async { + return BiometricSignaturePlatform.instance.decrypt( + payload, + payloadFormat, + config, + promptMessage, + ); } /// Deletes all active biometric key material. /// - /// - Hardware keys (RSA or EC): removed from Keystore / Secure Enclave. - /// - Hybrid mode keys: wrapped software keys are also cleared. - Future deleteKeys() async { - final response = await BiometricSignaturePlatform.instance.deleteKeys(); - return response; + /// Returns `true` if keys were deleted or no keys existed. + Future deleteKeys() async { + return BiometricSignaturePlatform.instance.deleteKeys(); } /// Determines whether biometric authentication is available on the device. /// - /// Returns: - /// - `"fingerprint"`, `"face"`, `"iris"`, `"TouchID"`, `"FaceID"`, etc. - /// - On Android, `"none, "` when unavailable. - Future biometricAuthAvailable() async { + /// Returns a [BiometricAvailability] with details about available biometrics. + Future biometricAuthAvailable() async { return BiometricSignaturePlatform.instance.biometricAuthAvailable(); } - /// Checks whether a hardware-backed signing key currently exists. + /// Gets detailed information about existing biometric keys. + /// + /// [checkValidity] whether to verify key hasn't been invalidated. + /// [keyFormat] output format for the public key. /// - /// If [checkValidity] is `true`, the plugin attempts to initialize a - /// signature operation. This may fail if the biometric enrollment has - /// changed and the key has been invalidated. - Future biometricKeyExists({bool checkValidity = false}) async { - return BiometricSignaturePlatform.instance.biometricKeyExists( + /// Returns a [KeyInfo] with key metadata. + Future getKeyInfo({ + bool checkValidity = false, + KeyFormat keyFormat = KeyFormat.base64, + }) async { + return BiometricSignaturePlatform.instance.getKeyInfo( checkValidity, + keyFormat, ); } + + /// Checks whether a hardware-backed signing key currently exists. + /// + /// This is a convenience wrapper around [getKeyInfo]. + Future biometricKeyExists({bool checkValidity = false}) async { + final info = await getKeyInfo(checkValidity: checkValidity); + return (info.exists ?? false) && (info.isValid ?? true); + } } diff --git a/lib/biometric_signature_method_channel.dart b/lib/biometric_signature_method_channel.dart deleted file mode 100644 index cb7aa97..0000000 --- a/lib/biometric_signature_method_channel.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'dart:io'; - -import 'package:biometric_signature/android_config.dart'; -import 'package:biometric_signature/ios_config.dart'; -import 'package:biometric_signature/macos_config.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'biometric_signature_platform_interface.dart'; -import 'decryption_options.dart'; -import 'key_material.dart'; -import 'signature_options.dart'; - -/// An implementation of [BiometricSignaturePlatform] that uses method channels. -class MethodChannelBiometricSignature extends BiometricSignaturePlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - final methodChannel = const MethodChannel('biometric_signature'); - - @override - Future?> createKeys( - AndroidConfig androidConfig, - IosConfig iosConfig, - MacosConfig macosConfig, { - required KeyFormat keyFormat, - bool enforceBiometric = false, - String? promptMessage, - }) async { - try { - if (Platform.isAndroid) { - final response = await methodChannel - .invokeMethod('createKeys', { - 'useDeviceCredentials': androidConfig.useDeviceCredentials, - 'useEc': androidConfig.signatureType.isEc, - 'keyFormat': keyFormat.wireValue, - 'setInvalidatedByBiometricEnrollment': - androidConfig.setInvalidatedByBiometricEnrollment, - 'enableDecryption': androidConfig.enableDecryption, - 'enforceBiometric': enforceBiometric, - if (promptMessage != null) 'promptMessage': promptMessage, - }); - return _normalizeMapResponse(response); - } else if (Platform.isMacOS) { - final response = await methodChannel - .invokeMethod('createKeys', { - 'useDeviceCredentials': macosConfig.useDeviceCredentials, - 'useEc': macosConfig.signatureType.isEc, - 'keyFormat': keyFormat.wireValue, - 'biometryCurrentSet': macosConfig.biometryCurrentSet, - 'enforceBiometric': enforceBiometric, - if (promptMessage != null) 'promptMessage': promptMessage, - }); - return _normalizeMapResponse(response); - } else { - final response = await methodChannel - .invokeMethod('createKeys', { - 'useDeviceCredentials': iosConfig.useDeviceCredentials, - 'useEc': iosConfig.signatureType.isEc, - 'keyFormat': keyFormat.wireValue, - 'biometryCurrentSet': iosConfig.biometryCurrentSet, - 'enforceBiometric': enforceBiometric, - if (promptMessage != null) 'promptMessage': promptMessage, - }); - return _normalizeMapResponse(response); - } - } on PlatformException { - rethrow; - } - } - - @override - Future deleteKeys() async { - try { - return methodChannel.invokeMethod('deleteKeys'); - } on PlatformException catch (e) { - debugPrint(e.message); - return false; - } - } - - @override - Future?> createSignature( - SignatureOptions options, - ) async { - try { - final response = await methodChannel.invokeMethod( - 'createSignature', - options.toMethodChannelMap(), - ); - return _normalizeMapResponse(response); - } on PlatformException { - rethrow; - } - } - - @override - Future?> decrypt(DecryptionOptions options) async { - try { - final response = await methodChannel.invokeMethod( - 'decrypt', - options.toMethodChannelMap(), - ); - return _normalizeMapResponse(response); - } on PlatformException { - rethrow; - } - } - - @override - Future biometricAuthAvailable() async { - try { - final response = await methodChannel.invokeMethod( - 'biometricAuthAvailable', - ); - return response; - } on PlatformException { - rethrow; - } - } - - @override - Future biometricKeyExists(bool checkValidity) async { - try { - return methodChannel.invokeMethod( - 'biometricKeyExists', - checkValidity, - ); - } on PlatformException catch (e) { - debugPrint(e.message); - return false; - } - } - - Map? _normalizeMapResponse(dynamic response) { - if (response == null) { - return null; - } - if (response is Map) { - return Map.from(response); - } - throw StateError('Unsupported response type ${response.runtimeType}'); - } -} diff --git a/lib/biometric_signature_platform_interface.dart b/lib/biometric_signature_platform_interface.dart index 2241c36..4a0dd24 100644 --- a/lib/biometric_signature_platform_interface.dart +++ b/lib/biometric_signature_platform_interface.dart @@ -1,12 +1,8 @@ -import 'package:biometric_signature/android_config.dart'; -import 'package:biometric_signature/ios_config.dart'; -import 'package:biometric_signature/macos_config.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'biometric_signature_method_channel.dart'; -import 'decryption_options.dart'; -import 'key_material.dart'; -import 'signature_options.dart'; +import 'biometric_signature_platform_interface.pigeon.dart'; + +export 'biometric_signature_platform_interface.pigeon.dart'; /// Platform interface that defines the methods exposed to plugin /// implementations. @@ -16,12 +12,9 @@ abstract class BiometricSignaturePlatform extends PlatformInterface { static final Object _token = Object(); - static BiometricSignaturePlatform _instance = - MethodChannelBiometricSignature(); + static BiometricSignaturePlatform _instance = _PigeonBiometricSignature(); /// The default instance of [BiometricSignaturePlatform] to use. - /// - /// Defaults to [MethodChannelBiometricSignature]. static BiometricSignaturePlatform get instance => _instance; /// Platform-specific implementations should set this with their own @@ -32,50 +25,105 @@ abstract class BiometricSignaturePlatform extends PlatformInterface { _instance = instance; } - /// Creates a key pair using the supplied platform-specific configuration. - Future?> createKeys( - AndroidConfig androidConfig, - IosConfig iosConfig, - MacosConfig macosConfig, { - required KeyFormat keyFormat, - bool enforceBiometric = false, - String? promptMessage, - }) { + /// Checks if biometric authentication is available. + Future biometricAuthAvailable() { throw UnimplementedError( - 'createKeys(AndroidConfig androidConfig, IosConfig iosConfig, MacosConfig macosConfig, {required KeyFormat keyFormat, bool enforceBiometric, String? promptMessage}) has not been implemented.', + 'biometricAuthAvailable() has not been implemented.', ); } - /// Deletes the stored biometric key if present. - Future deleteKeys() { + /// Creates a new key pair. + Future createKeys( + CreateKeysConfig? config, + KeyFormat keyFormat, + String? promptMessage, + ) { + throw UnimplementedError('createKeys() has not been implemented.'); + } + + /// Creates a signature. + Future createSignature( + String payload, + CreateSignatureConfig? config, + SignatureFormat signatureFormat, + KeyFormat keyFormat, + String? promptMessage, + ) { + throw UnimplementedError('createSignature() has not been implemented.'); + } + + /// Decrypts data. + Future decrypt( + String payload, + PayloadFormat payloadFormat, + DecryptConfig? config, + String? promptMessage, + ) { + throw UnimplementedError('decrypt() has not been implemented.'); + } + + /// Deletes keys. + Future deleteKeys() { throw UnimplementedError('deleteKeys() has not been implemented.'); } - /// Returns information about the biometric availability on the device. - Future biometricAuthAvailable() { - throw UnimplementedError( - 'biometricAuthAvailable() has not been implemented.', - ); + /// Gets detailed information about existing biometric keys. + Future getKeyInfo(bool checkValidity, KeyFormat keyFormat) { + throw UnimplementedError('getKeyInfo() has not been implemented.'); } +} - /// Creates a signature for the given payload using biometrics. - Future?> createSignature(SignatureOptions options) { - throw UnimplementedError( - 'createSignature(SignatureOptions options) has not been implemented.', - ); +class _PigeonBiometricSignature extends BiometricSignaturePlatform { + final BiometricSignatureApi _api = BiometricSignatureApi(); + + @override + Future biometricAuthAvailable() { + return _api.biometricAuthAvailable(); } - /// Decrypts the given payload using the private key and biometrics. - Future?> decrypt(DecryptionOptions options) { - throw UnimplementedError( - 'decrypt(DecryptionOptions options) has not been implemented.', - ); + @override + Future createKeys( + CreateKeysConfig? config, + KeyFormat keyFormat, + String? promptMessage, + ) { + return _api.createKeys(config, keyFormat, promptMessage); } - /// Checks whether the biometric key exists, optionally validating the key. - Future biometricKeyExists(bool checkValidity) { - throw UnimplementedError( - 'biometricKeyExists(bool checkValidity) has not been implemented.', + @override + Future createSignature( + String payload, + CreateSignatureConfig? config, + SignatureFormat signatureFormat, + KeyFormat keyFormat, + String? promptMessage, + ) { + return _api.createSignature( + payload, + config, + signatureFormat, + keyFormat, + promptMessage, ); } + + @override + Future decrypt( + String payload, + PayloadFormat payloadFormat, + DecryptConfig? config, + String? promptMessage, + ) { + return _api.decrypt(payload, payloadFormat, config, promptMessage); + } + + @override + Future deleteKeys() { + return _api.deleteKeys(); + } + + @override + Future getKeyInfo(bool checkValidity, KeyFormat keyFormat) { + return _api.getKeyInfo(checkValidity, keyFormat); + } } diff --git a/lib/biometric_signature_platform_interface.pigeon.dart b/lib/biometric_signature_platform_interface.pigeon.dart new file mode 100644 index 0000000..12f9487 --- /dev/null +++ b/lib/biometric_signature_platform_interface.pigeon.dart @@ -0,0 +1,1049 @@ +// Autogenerated from Pigeon (v26.1.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +bool _deepEquals(Object? a, Object? b) { + if (a is List && b is List) { + return a.length == b.length && + a.indexed.every( + ((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]), + ); + } + if (a is Map && b is Map) { + return a.length == b.length && + a.entries.every( + (MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key]), + ); + } + return a == b; +} + +/// Types of biometric authentication supported by the device. +enum BiometricType { + /// Face recognition (Face ID on iOS, face unlock on Android). + face, + + /// Fingerprint recognition (Touch ID on iOS/macOS, fingerprint on Android). + fingerprint, + + /// Iris scanner (Android only, rare on consumer devices). + iris, + + /// Multiple biometric types are available on the device. + multiple, + + /// No biometric hardware available or biometrics are disabled. + unavailable, +} + +/// Standardized error codes for the plugin. +enum BiometricError { + /// The operation was successful. + success, + + /// The user canceled the operation. + userCanceled, + + /// Biometric authentication is not available on this device. + notAvailable, + + /// No biometrics are enrolled. + notEnrolled, + + /// The user is temporarily locked out due to too many failed attempts. + lockedOut, + + /// The user is permanently locked out until they log in with a strong method. + lockedOutPermanent, + + /// The requested key was not found. + keyNotFound, + + /// The key has been invalidated (e.g. by new biometric enrollment). + keyInvalidated, + + /// An unknown error occurred. + unknown, + + /// The input payload was invalid (e.g. not valid Base64). + invalidInput, +} + +/// The cryptographic algorithm to use for key generation. +enum SignatureType { + /// RSA-2048 (Android: native, iOS/macOS: hybrid mode with Secure Enclave EC). + rsa, + + /// ECDSA P-256 (hardware-backed on all platforms). + ecdsa, +} + +/// Output format for public keys. +enum KeyFormat { + /// Base64-encoded DER (SubjectPublicKeyInfo). + base64, + + /// PEM format with BEGIN/END PUBLIC KEY headers. + pem, + + /// Hexadecimal-encoded DER. + hex, + + /// Raw DER bytes (returned via `publicKeyBytes`). + raw, +} + +/// Output format for cryptographic signatures. +enum SignatureFormat { + /// Base64-encoded signature bytes. + base64, + + /// Hexadecimal-encoded signature bytes. + hex, + + /// Raw signature bytes (returned via `signatureBytes`). + raw, +} + +/// Input format for encrypted payloads to decrypt. +enum PayloadFormat { + /// Base64-encoded ciphertext. + base64, + + /// Hexadecimal-encoded ciphertext. + hex, + + /// Raw UTF-8 string (not recommended for binary data). + raw, +} + +class BiometricAvailability { + BiometricAvailability({ + this.canAuthenticate, + this.hasEnrolledBiometrics, + this.availableBiometrics, + this.reason, + }); + + bool? canAuthenticate; + + bool? hasEnrolledBiometrics; + + List? availableBiometrics; + + String? reason; + + List _toList() { + return [ + canAuthenticate, + hasEnrolledBiometrics, + availableBiometrics, + reason, + ]; + } + + Object encode() { + return _toList(); + } + + static BiometricAvailability decode(Object result) { + result as List; + return BiometricAvailability( + canAuthenticate: result[0] as bool?, + hasEnrolledBiometrics: result[1] as bool?, + availableBiometrics: (result[2] as List?) + ?.cast(), + reason: result[3] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! BiometricAvailability || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class KeyCreationResult { + KeyCreationResult({ + this.publicKey, + this.publicKeyBytes, + this.error, + this.code, + this.algorithm, + this.keySize, + this.decryptingPublicKey, + this.decryptingAlgorithm, + this.decryptingKeySize, + this.isHybridMode, + }); + + String? publicKey; + + Uint8List? publicKeyBytes; + + String? error; + + BiometricError? code; + + String? algorithm; + + int? keySize; + + String? decryptingPublicKey; + + String? decryptingAlgorithm; + + int? decryptingKeySize; + + bool? isHybridMode; + + List _toList() { + return [ + publicKey, + publicKeyBytes, + error, + code, + algorithm, + keySize, + decryptingPublicKey, + decryptingAlgorithm, + decryptingKeySize, + isHybridMode, + ]; + } + + Object encode() { + return _toList(); + } + + static KeyCreationResult decode(Object result) { + result as List; + return KeyCreationResult( + publicKey: result[0] as String?, + publicKeyBytes: result[1] as Uint8List?, + error: result[2] as String?, + code: result[3] as BiometricError?, + algorithm: result[4] as String?, + keySize: result[5] as int?, + decryptingPublicKey: result[6] as String?, + decryptingAlgorithm: result[7] as String?, + decryptingKeySize: result[8] as int?, + isHybridMode: result[9] as bool?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! KeyCreationResult || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class SignatureResult { + SignatureResult({ + this.signature, + this.signatureBytes, + this.publicKey, + this.error, + this.code, + this.algorithm, + this.keySize, + }); + + String? signature; + + Uint8List? signatureBytes; + + String? publicKey; + + String? error; + + BiometricError? code; + + String? algorithm; + + int? keySize; + + List _toList() { + return [ + signature, + signatureBytes, + publicKey, + error, + code, + algorithm, + keySize, + ]; + } + + Object encode() { + return _toList(); + } + + static SignatureResult decode(Object result) { + result as List; + return SignatureResult( + signature: result[0] as String?, + signatureBytes: result[1] as Uint8List?, + publicKey: result[2] as String?, + error: result[3] as String?, + code: result[4] as BiometricError?, + algorithm: result[5] as String?, + keySize: result[6] as int?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! SignatureResult || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class DecryptResult { + DecryptResult({this.decryptedData, this.error, this.code}); + + String? decryptedData; + + String? error; + + BiometricError? code; + + List _toList() { + return [decryptedData, error, code]; + } + + Object encode() { + return _toList(); + } + + static DecryptResult decode(Object result) { + result as List; + return DecryptResult( + decryptedData: result[0] as String?, + error: result[1] as String?, + code: result[2] as BiometricError?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! DecryptResult || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +/// Detailed information about existing biometric keys. +class KeyInfo { + KeyInfo({ + this.exists, + this.isValid, + this.algorithm, + this.keySize, + this.isHybridMode, + this.publicKey, + this.decryptingPublicKey, + this.decryptingAlgorithm, + this.decryptingKeySize, + }); + + /// Whether any biometric key exists on the device. + bool? exists; + + /// Whether the key is still valid (not invalidated by biometric changes). + /// Only populated when `checkValidity: true` is passed. + bool? isValid; + + /// The algorithm of the signing key (e.g., "RSA", "EC"). + String? algorithm; + + /// The key size in bits (e.g., 2048 for RSA, 256 for EC). + int? keySize; + + /// Whether the key is in hybrid mode (separate signing and decryption keys). + bool? isHybridMode; + + /// Signing key public key (formatted according to the requested format). + String? publicKey; + + /// Decryption key public key for hybrid mode. + String? decryptingPublicKey; + + /// Algorithm of the decryption key (hybrid mode only). + String? decryptingAlgorithm; + + /// Key size of the decryption key in bits (hybrid mode only). + int? decryptingKeySize; + + List _toList() { + return [ + exists, + isValid, + algorithm, + keySize, + isHybridMode, + publicKey, + decryptingPublicKey, + decryptingAlgorithm, + decryptingKeySize, + ]; + } + + Object encode() { + return _toList(); + } + + static KeyInfo decode(Object result) { + result as List; + return KeyInfo( + exists: result[0] as bool?, + isValid: result[1] as bool?, + algorithm: result[2] as String?, + keySize: result[3] as int?, + isHybridMode: result[4] as bool?, + publicKey: result[5] as String?, + decryptingPublicKey: result[6] as String?, + decryptingAlgorithm: result[7] as String?, + decryptingKeySize: result[8] as int?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! KeyInfo || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +/// Configuration for key creation (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// Windows ignores most fields as it only supports RSA with mandatory +/// Windows Hello authentication. +class CreateKeysConfig { + CreateKeysConfig({ + this.signatureType, + this.enforceBiometric, + this.setInvalidatedByBiometricEnrollment, + this.useDeviceCredentials, + this.enableDecryption, + this.promptSubtitle, + this.promptDescription, + this.cancelButtonText, + }); + + /// [Android/iOS/macOS] The cryptographic algorithm to use. + /// Windows only supports RSA and ignores this field. + SignatureType? signatureType; + + /// [Android/iOS/macOS] Whether to require biometric authentication + /// during key creation. Windows always authenticates via Windows Hello. + bool? enforceBiometric; + + /// [Android/iOS/macOS] Whether to invalidate the key when new biometrics + /// are enrolled. Not supported on Windows. + /// + /// **Security Note**: When `true`, keys become invalid if fingerprints/faces + /// are added or removed, preventing unauthorized access if an attacker + /// enrolls their own biometrics on a compromised device. + bool? setInvalidatedByBiometricEnrollment; + + /// [Android/iOS/macOS] Whether to allow device credentials (PIN/pattern/passcode) + /// as fallback for biometric authentication. Not supported on Windows. + bool? useDeviceCredentials; + + /// [Android] Whether to enable decryption capability for the key. + /// On iOS/macOS, decryption is always available with EC keys. + bool? enableDecryption; + + /// [Android] Subtitle text for the biometric prompt. + String? promptSubtitle; + + /// [Android] Description text for the biometric prompt. + String? promptDescription; + + /// [Android] Text for the cancel button in the biometric prompt. + String? cancelButtonText; + + List _toList() { + return [ + signatureType, + enforceBiometric, + setInvalidatedByBiometricEnrollment, + useDeviceCredentials, + enableDecryption, + promptSubtitle, + promptDescription, + cancelButtonText, + ]; + } + + Object encode() { + return _toList(); + } + + static CreateKeysConfig decode(Object result) { + result as List; + return CreateKeysConfig( + signatureType: result[0] as SignatureType?, + enforceBiometric: result[1] as bool?, + setInvalidatedByBiometricEnrollment: result[2] as bool?, + useDeviceCredentials: result[3] as bool?, + enableDecryption: result[4] as bool?, + promptSubtitle: result[5] as String?, + promptDescription: result[6] as String?, + cancelButtonText: result[7] as String?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! CreateKeysConfig || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +/// Configuration for signature creation (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +class CreateSignatureConfig { + CreateSignatureConfig({ + this.promptSubtitle, + this.promptDescription, + this.cancelButtonText, + this.allowDeviceCredentials, + this.shouldMigrate, + }); + + /// [Android] Subtitle text for the biometric prompt. + String? promptSubtitle; + + /// [Android] Description text for the biometric prompt. + String? promptDescription; + + /// [Android] Text for the cancel button in the biometric prompt. + String? cancelButtonText; + + /// [Android] Whether to allow device credentials (PIN/pattern) as fallback. + bool? allowDeviceCredentials; + + /// [iOS] Whether to migrate from legacy keychain storage. + bool? shouldMigrate; + + List _toList() { + return [ + promptSubtitle, + promptDescription, + cancelButtonText, + allowDeviceCredentials, + shouldMigrate, + ]; + } + + Object encode() { + return _toList(); + } + + static CreateSignatureConfig decode(Object result) { + result as List; + return CreateSignatureConfig( + promptSubtitle: result[0] as String?, + promptDescription: result[1] as String?, + cancelButtonText: result[2] as String?, + allowDeviceCredentials: result[3] as bool?, + shouldMigrate: result[4] as bool?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! CreateSignatureConfig || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +/// Configuration for decryption (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// Note: Decryption is not supported on Windows. +class DecryptConfig { + DecryptConfig({ + this.promptSubtitle, + this.promptDescription, + this.cancelButtonText, + this.allowDeviceCredentials, + this.shouldMigrate, + }); + + /// [Android] Subtitle text for the biometric prompt. + String? promptSubtitle; + + /// [Android] Description text for the biometric prompt. + String? promptDescription; + + /// [Android] Text for the cancel button in the biometric prompt. + String? cancelButtonText; + + /// [Android] Whether to allow device credentials (PIN/pattern) as fallback. + bool? allowDeviceCredentials; + + /// [iOS] Whether to migrate from legacy keychain storage. + bool? shouldMigrate; + + List _toList() { + return [ + promptSubtitle, + promptDescription, + cancelButtonText, + allowDeviceCredentials, + shouldMigrate, + ]; + } + + Object encode() { + return _toList(); + } + + static DecryptConfig decode(Object result) { + result as List; + return DecryptConfig( + promptSubtitle: result[0] as String?, + promptDescription: result[1] as String?, + cancelButtonText: result[2] as String?, + allowDeviceCredentials: result[3] as bool?, + shouldMigrate: result[4] as bool?, + ); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (other is! DecryptConfig || other.runtimeType != runtimeType) { + return false; + } + if (identical(this, other)) { + return true; + } + return _deepEquals(encode(), other.encode()); + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hashAll(_toList()); +} + +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is BiometricType) { + buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is BiometricError) { + buffer.putUint8(130); + writeValue(buffer, value.index); + } else if (value is SignatureType) { + buffer.putUint8(131); + writeValue(buffer, value.index); + } else if (value is KeyFormat) { + buffer.putUint8(132); + writeValue(buffer, value.index); + } else if (value is SignatureFormat) { + buffer.putUint8(133); + writeValue(buffer, value.index); + } else if (value is PayloadFormat) { + buffer.putUint8(134); + writeValue(buffer, value.index); + } else if (value is BiometricAvailability) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is KeyCreationResult) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is SignatureResult) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is DecryptResult) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); + } else if (value is KeyInfo) { + buffer.putUint8(139); + writeValue(buffer, value.encode()); + } else if (value is CreateKeysConfig) { + buffer.putUint8(140); + writeValue(buffer, value.encode()); + } else if (value is CreateSignatureConfig) { + buffer.putUint8(141); + writeValue(buffer, value.encode()); + } else if (value is DecryptConfig) { + buffer.putUint8(142); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + final value = readValue(buffer) as int?; + return value == null ? null : BiometricType.values[value]; + case 130: + final value = readValue(buffer) as int?; + return value == null ? null : BiometricError.values[value]; + case 131: + final value = readValue(buffer) as int?; + return value == null ? null : SignatureType.values[value]; + case 132: + final value = readValue(buffer) as int?; + return value == null ? null : KeyFormat.values[value]; + case 133: + final value = readValue(buffer) as int?; + return value == null ? null : SignatureFormat.values[value]; + case 134: + final value = readValue(buffer) as int?; + return value == null ? null : PayloadFormat.values[value]; + case 135: + return BiometricAvailability.decode(readValue(buffer)!); + case 136: + return KeyCreationResult.decode(readValue(buffer)!); + case 137: + return SignatureResult.decode(readValue(buffer)!); + case 138: + return DecryptResult.decode(readValue(buffer)!); + case 139: + return KeyInfo.decode(readValue(buffer)!); + case 140: + return CreateKeysConfig.decode(readValue(buffer)!); + case 141: + return CreateSignatureConfig.decode(readValue(buffer)!); + case 142: + return DecryptConfig.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class BiometricSignatureApi { + /// Constructor for [BiometricSignatureApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + BiometricSignatureApi({ + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty + ? '.$messageChannelSuffix' + : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Checks if biometric authentication is available. + Future biometricAuthAvailable() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.biometricAuthAvailable$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as BiometricAvailability?)!; + } + } + + /// Creates a new key pair. + /// + /// [config] contains platform-specific options. See [CreateKeysConfig]. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + Future createKeys( + CreateKeysConfig? config, + KeyFormat keyFormat, + String? promptMessage, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createKeys$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [config, keyFormat, promptMessage], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as KeyCreationResult?)!; + } + } + + /// Creates a signature. + /// + /// [payload] is the data to sign. + /// [config] contains platform-specific options. See [CreateSignatureConfig]. + /// [signatureFormat] specifies the output format for the signature. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + Future createSignature( + String payload, + CreateSignatureConfig? config, + SignatureFormat signatureFormat, + KeyFormat keyFormat, + String? promptMessage, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createSignature$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [payload, config, signatureFormat, keyFormat, promptMessage], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as SignatureResult?)!; + } + } + + /// Decrypts data. + /// + /// Note: Not supported on Windows. + /// [payload] is the encrypted data. + /// [payloadFormat] specifies the format of the encrypted data. + /// [config] contains platform-specific options. See [DecryptConfig]. + /// [promptMessage] is the message shown to the user during authentication. + Future decrypt( + String payload, + PayloadFormat payloadFormat, + DecryptConfig? config, + String? promptMessage, + ) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.decrypt$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [payload, payloadFormat, config, promptMessage], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as DecryptResult?)!; + } + } + + /// Deletes keys. + Future deleteKeys() async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.deleteKeys$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as bool?)!; + } + } + + /// Gets detailed information about existing biometric keys. + /// + /// Returns key metadata including algorithm, size, validity, and public keys. + Future getKeyInfo(bool checkValidity, KeyFormat keyFormat) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.getKeyInfo$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send( + [checkValidity, keyFormat], + ); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else if (pigeonVar_replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (pigeonVar_replyList[0] as KeyInfo?)!; + } + } +} diff --git a/lib/decryption_options.dart b/lib/decryption_options.dart deleted file mode 100644 index 24bc7ba..0000000 --- a/lib/decryption_options.dart +++ /dev/null @@ -1,159 +0,0 @@ -/// Configuration for controlling how a decryption request is executed. -/// -/// Decryption is performed using the key material currently stored on the -/// device. The plugin automatically selects the appropriate algorithm based -/// on the active key mode (RSA, EC signing-only, or Hybrid EC). -/// -/// ## Supported Algorithms -/// -/// **RSA Decryption** -/// - Uses RSA/ECB/PKCS1Padding on Android, iOS, and macOS. -/// -/// **EC Decryption (ECIES)** -/// - Uses ECIES with ANSI X9.63 KDF (SHA-256) and AES-128-GCM. -/// - Android: -/// - ECIES is implemented manually (ECDH → X9.63 KDF → AES-GCM). -/// - The software EC private key is stored in app-private files (encrypted), -/// and unwrapped at runtime using a biometric-protected AES-256 master key -/// stored inside Keystore/StrongBox. -/// - iOS/macOS: -/// - ECIES is performed natively using -/// `SecKeyAlgorithm.eciesEncryptionStandardX963SHA256AESGCM`. -/// -/// ## Payload Format -/// -/// The encrypted payload must be Base64-encoded: -/// -/// - **RSA:** -/// A standard PKCS#1 RSA block. -/// -/// - **EC (ECIES):** -/// `ephemeralPubKey(65 bytes) || ciphertext || gcmTag(16 bytes)` -/// -/// The plugin validates and parses this structure automatically. -class DecryptionOptions { - /// Creates a new [DecryptionOptions] instance. - const DecryptionOptions({ - required this.payload, - this.promptMessage, - this.androidOptions, - this.iosOptions, - this.macosOptions, - }); - - /// Base64-encoded encrypted payload. - /// - /// - **RSA:** PKCS#1-encoded block. - /// - **EC:** Concatenated ECIES blob consisting of: - /// - Uncompressed ephemeral public key (65 bytes: `0x04 || X || Y`) - /// - AES-GCM ciphertext - /// - 16-byte GCM authentication tag - final String payload; - - /// Optional custom message shown in the biometric authentication prompt. - final String? promptMessage; - - /// Android-specific biometric and prompt configuration. - final AndroidDecryptionOptions? androidOptions; - - /// iOS-specific configuration, including optional migration of legacy keys. - final IosDecryptionOptions? iosOptions; - - /// macOS-specific configuration. - final MacosDecryptionOptions? macosOptions; - - /// Converts this object to a map suitable for method-channel transport. - Map toMethodChannelMap() { - final map = { - 'payload': payload, - if (promptMessage != null) 'promptMessage': promptMessage, - }; - - if (androidOptions != null) { - map.addAll(androidOptions!.toMethodChannelMap()); - } - - if (iosOptions != null) { - map.addAll(iosOptions!.toMethodChannelMap()); - } - - if (macosOptions != null) { - map.addAll(macosOptions!.toMethodChannelMap()); - } - - return map; - } -} - -/// Android-specific decryption parameters. -class AndroidDecryptionOptions { - /// Creates a new [AndroidDecryptionOptions] instance. - const AndroidDecryptionOptions({ - this.cancelButtonText, - this.allowDeviceCredentials, - this.subtitle, - }); - - /// Text displayed on the cancel button in the biometric prompt. - final String? cancelButtonText; - - /// Whether device credentials (PIN / Pattern / Password) may satisfy the - /// biometric prompt on Android 11+. - final bool? allowDeviceCredentials; - - /// Optional subtitle displayed beneath the prompt title. - final String? subtitle; - - /// Whether any Android-specific parameters were provided. - bool get hasValues => - cancelButtonText != null || - allowDeviceCredentials != null || - subtitle != null; - - /// Converts Android-specific options to a method-channel-compatible map. - Map toMethodChannelMap() { - return { - if (cancelButtonText != null) 'cancelButtonText': cancelButtonText, - if (allowDeviceCredentials != null) - 'allowDeviceCredentials': allowDeviceCredentials, - if (subtitle != null) 'subtitle': subtitle, - }; - } -} - -/// iOS-specific decryption parameters. -class IosDecryptionOptions { - /// Creates a new [IosDecryptionOptions] instance. - const IosDecryptionOptions({this.shouldMigrate}); - - /// Whether legacy (pre-5.x) Keychain keys should be migrated into the - /// Secure Enclave during the decryption request. - /// - /// Migration is only required when supporting older installations; new - /// deployments can leave this disabled for optimal performance. - final bool? shouldMigrate; - - /// Whether any iOS-specific parameters were provided. - bool get hasValues => shouldMigrate != null; - - /// Converts iOS-specific options to a method-channel-compatible map. - Map toMethodChannelMap() { - return {if (shouldMigrate != null) 'shouldMigrate': shouldMigrate}; - } -} - -/// macOS-specific decryption parameters. -/// -/// Currently empty as macOS does not require migration or other special options. -class MacosDecryptionOptions { - /// Creates a new [MacosDecryptionOptions] instance. - const MacosDecryptionOptions(); - - /// Whether any macOS-specific parameters were provided. - bool get hasValues => false; - - /// Converts macOS-specific options to a method-channel-compatible map. - Map toMethodChannelMap() { - return {}; - } -} diff --git a/lib/ios_config.dart b/lib/ios_config.dart deleted file mode 100644 index 19c249e..0000000 --- a/lib/ios_config.dart +++ /dev/null @@ -1,29 +0,0 @@ -// ignore_for_file: constant_identifier_names -/// Supported signature algorithms on iOS. -enum IOSSignatureType { RSA, ECDSA } - -/// Convenience helpers for [IOSSignatureType]. -extension IOSSignatureTypeExtension on IOSSignatureType { - /// Returns `true` when the ECDSA algorithm is selected. - bool get isEc => this == IOSSignatureType.ECDSA; -} - -/// iOS-specific configuration for enrolling or using biometric keys. -class IosConfig { - /// Whether device credentials (passcode) can unlock the key instead of - /// biometrics. - bool useDeviceCredentials; - - /// Key algorithm to use when creating a signature. - IOSSignatureType signatureType; - - /// Whether to constraint Key usage for current biometric enrollment. - bool biometryCurrentSet; - - /// Creates a new iOS configuration. - IosConfig({ - required this.useDeviceCredentials, - this.signatureType = IOSSignatureType.RSA, - this.biometryCurrentSet = true, - }); -} diff --git a/lib/key_material.dart b/lib/key_material.dart deleted file mode 100644 index da6b673..0000000 --- a/lib/key_material.dart +++ /dev/null @@ -1,284 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -/// Supported serialization formats for keys and signatures. -enum KeyFormat { base64, pem, raw, hex } - -extension KeyFormatWire on KeyFormat { - /// Uppercase representation used on the platform channel. - String get wireValue { - switch (this) { - case KeyFormat.base64: - return 'BASE64'; - case KeyFormat.pem: - return 'PEM'; - case KeyFormat.raw: - return 'RAW'; - case KeyFormat.hex: - return 'HEX'; - } - } - - /// Parses a [KeyFormat] from its wire representation. - static KeyFormat? fromWire(String? value) { - switch (value) { - case 'BASE64': - return KeyFormat.base64; - case 'PEM': - return KeyFormat.pem; - case 'RAW': - return KeyFormat.raw; - case 'HEX': - return KeyFormat.hex; - default: - return null; - } - } -} - -/// Holds a value alongside the format it is encoded with. -class FormattedValue { - const FormattedValue({ - required this.format, - required this.value, - this.pemLabel, - }); - - /// The declared format of [value]. - final KeyFormat format; - - /// The PEM label to use when emitting PEM (e.g. `PUBLIC KEY`). - final String? pemLabel; - - /// Either a [String] or [Uint8List] holding the formatted bytes. - final Object value; - - /// Returns the underlying bytes, decoding when necessary. - Uint8List toBytes() { - if (value is Uint8List) { - return value as Uint8List; - } - final stringValue = value as String; - switch (format) { - case KeyFormat.base64: - return Uint8List.fromList(base64Decode(stringValue)); - case KeyFormat.pem: - return Uint8List.fromList(base64Decode(_stripPem(stringValue))); - case KeyFormat.hex: - return Uint8List.fromList(_decodeHex(stringValue)); - case KeyFormat.raw: - throw StateError('RAW values must be provided as byte data.'); - } - } - - /// Returns the contents encoded as base64. - String toBase64() { - if (value is String && format == KeyFormat.base64) { - return value as String; - } - return base64Encode(toBytes()); - } - - /// Returns the contents encoded as hexadecimal. - String toHex({bool uppercase = false}) { - String source; - if (value is String && format == KeyFormat.hex) { - source = value as String; - } else { - final bytes = toBytes(); - final buffer = StringBuffer(); - for (final b in bytes) { - buffer.write(b.toRadixString(16).padLeft(2, '0')); - } - source = buffer.toString(); - } - return uppercase ? source.toUpperCase() : source.toLowerCase(); - } - - /// Returns the contents encoded as a PEM block. - /// - /// Throws when [format] is [KeyFormat.raw] and [value] is not UTF-8 data. - String toPem({String? overrideLabel}) { - final label = overrideLabel ?? pemLabel ?? 'PUBLIC KEY'; - if (value is String && format == KeyFormat.pem) { - return _ensurePemLabel(value as String, label); - } - final normalized = base64Encode(toBytes()); - final chunked = _chunkBase64(normalized); - return '-----BEGIN $label-----\n$chunked\n-----END $label-----'; - } - - /// Returns the formatted value as a string if already string-backed. - String? asString() => value is String ? value as String : null; - - static String _chunkBase64(String input, [int chunkSize = 64]) { - final buffer = StringBuffer(); - for (var i = 0; i < input.length; i += chunkSize) { - final end = (i + chunkSize < input.length) ? i + chunkSize : input.length; - buffer.writeln(input.substring(i, end)); - } - final result = buffer.toString().trimRight(); - return result; - } - - static String _stripPem(String pem) { - return pem - .replaceAll(RegExp(r'-----BEGIN [^-]+-----'), '') - .replaceAll(RegExp(r'-----END [^-]+-----'), '') - .replaceAll(RegExp(r'\s'), ''); - } - - static String _ensurePemLabel(String pem, String label) { - final stripped = _stripPem(pem); - final chunked = _chunkBase64(stripped); - return '-----BEGIN $label-----\n$chunked\n-----END $label-----'; - } - - static List _decodeHex(String input) { - final sanitized = input.replaceAll(RegExp(r'\s'), ''); - if (sanitized.length % 2 != 0) { - throw const FormatException('Hex payload must have even length'); - } - final bytes = []; - for (var i = 0; i < sanitized.length; i += 2) { - bytes.add(int.parse(sanitized.substring(i, i + 2), radix: 16)); - } - return bytes; - } -} - -/// Capture structured response for `createKeys`. -class KeyCreationResult { - KeyCreationResult({ - required this.publicKey, - required this.algorithm, - required this.keySize, - this.signingPublicKey, - this.signingAlgorithm, - this.signingKeySize, - this.isHybridMode = false, - }); - - /// The primary public key (for encryption in hybrid mode, or general use otherwise) - final FormattedValue publicKey; - - /// The algorithm of the primary key ("RSA" or "EC") - final String algorithm; - - /// The size of the primary key in bits - final int keySize; - - // ========== Hybrid Mode Fields (Android EC + Decryption) ========== - - /// The signing public key (only present in hybrid mode) - final FormattedValue? signingPublicKey; - - /// The signing algorithm (only present in hybrid mode, typically "EC") - final String? signingAlgorithm; - - /// The signing key size in bits (only present in hybrid mode, typically 256) - final int? signingKeySize; - - /// Whether this result is from hybrid mode - /// In hybrid mode: - /// - `publicKey` is the encryption key (for ECIES) - /// - `signingPublicKey` is the signing key (for ECDSA) - final bool isHybridMode; - - factory KeyCreationResult.fromChannel(Map raw) { - final isHybrid = raw['hybridMode'] == true; - - FormattedValue? signingPubKey; - if (isHybrid && raw['signingPublicKey'] != null) { - signingPubKey = FormattedValue( - format: - KeyFormatWire.fromWire(raw['signingPublicKeyFormat'] as String?) ?? - KeyFormat.base64, - value: raw['signingPublicKey']!, - pemLabel: raw['signingPublicKeyPemLabel'] as String?, - ); - } - - return KeyCreationResult( - publicKey: FormattedValue( - format: - KeyFormatWire.fromWire(raw['publicKeyFormat'] as String?) ?? - KeyFormat.base64, - value: raw['publicKey']!, - pemLabel: raw['publicKeyPemLabel'] as String?, - ), - algorithm: (raw['algorithm'] as String?) ?? 'RSA', - keySize: (raw['keySize'] as num?)?.toInt() ?? 2048, - signingPublicKey: signingPubKey, - signingAlgorithm: raw['signingAlgorithm'] as String?, - signingKeySize: (raw['signingKeySize'] as num?)?.toInt(), - isHybridMode: isHybrid, - ); - } - - @override - String toString() { - if (isHybridMode) { - return 'KeyCreationResult(hybrid: encryption=$algorithm/$keySize, signing=$signingAlgorithm/$signingKeySize)'; - } - return 'KeyCreationResult($algorithm/$keySize)'; - } -} - -/// Capture structured response for `createSignature`. -class SignatureResult { - SignatureResult({ - required this.publicKey, - required this.signature, - required this.algorithm, - required this.keySize, - this.timestamp, - }); - - final FormattedValue publicKey; - final FormattedValue signature; - final String algorithm; - final int keySize; - final DateTime? timestamp; - - factory SignatureResult.fromChannel(Map raw) { - final timestampString = raw['timestamp'] as String?; - return SignatureResult( - publicKey: FormattedValue( - format: - KeyFormatWire.fromWire(raw['publicKeyFormat'] as String?) ?? - KeyFormat.base64, - value: raw['publicKey']!, - pemLabel: raw['publicKeyPemLabel'] as String?, - ), - signature: FormattedValue( - format: - KeyFormatWire.fromWire(raw['signatureFormat'] as String?) ?? - KeyFormat.base64, - value: raw['signature']!, - pemLabel: raw['signaturePemLabel'] as String?, - ), - algorithm: (raw['algorithm'] as String?) ?? 'RSA', - keySize: (raw['keySize'] as num?)?.toInt() ?? 2048, - timestamp: timestampString != null - ? DateTime.parse(timestampString) - : null, - ); - } -} - -/// Capture structured response for `decrypt`. -/// -/// Contains the decrypted data as a UTF-8 string. Works with both RSA and EC -/// (ECIES) encrypted payloads. The decryption algorithm is automatically -/// selected based on the key type stored on the device. -class DecryptResult { - DecryptResult({required this.decryptedData}); - - /// The decrypted plaintext string (UTF-8 encoded). - final String decryptedData; - - factory DecryptResult.fromChannel(Map raw) { - return DecryptResult(decryptedData: raw['decryptedData'] as String); - } -} diff --git a/lib/macos_config.dart b/lib/macos_config.dart deleted file mode 100644 index ff6923d..0000000 --- a/lib/macos_config.dart +++ /dev/null @@ -1,29 +0,0 @@ -// ignore_for_file: constant_identifier_names -/// Supported signature algorithms on macOS. -enum MacosSignatureType { RSA, ECDSA } - -/// Convenience helpers for [MacosSignatureType]. -extension MacosSignatureTypeExtension on MacosSignatureType { - /// Returns `true` when the ECDSA algorithm is selected. - bool get isEc => this == MacosSignatureType.ECDSA; -} - -/// macOS-specific configuration for enrolling or using biometric keys. -class MacosConfig { - /// Whether device credentials (passcode) can unlock the key instead of - /// biometrics. - bool useDeviceCredentials; - - /// Key algorithm to use when creating a signature. - MacosSignatureType signatureType; - - /// Whether to constraint Key usage for current biometric enrollment. - bool biometryCurrentSet; - - /// Creates a new macOS configuration. - MacosConfig({ - required this.useDeviceCredentials, - this.signatureType = MacosSignatureType.RSA, - this.biometryCurrentSet = true, - }); -} diff --git a/lib/signature_options.dart b/lib/signature_options.dart deleted file mode 100644 index dacdb41..0000000 --- a/lib/signature_options.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'key_material.dart'; - -/// Options that control how a signature request behaves on each platform. -class SignatureOptions { - /// Creates a new [SignatureOptions] instance. - const SignatureOptions({ - required this.payload, - this.promptMessage, - this.androidOptions, - this.iosOptions, - this.macosOptions, - this.keyFormat = KeyFormat.base64, - }); - - /// Payload string that will be signed and returned in the response. - final String payload; - - /// Optional message shown in the biometric authentication prompt. - final String? promptMessage; - - /// Android-specific biometric prompt and configuration overrides. - final AndroidSignatureOptions? androidOptions; - - /// iOS-specific signature options. - final IosSignatureOptions? iosOptions; - - /// macOS-specific signature options. - final MacosSignatureOptions? macosOptions; - - /// Preferred output format for both public key and signature. - final KeyFormat keyFormat; - - /// Converts this object to a method-channel map. - Map toMethodChannelMap() { - final Map map = { - 'payload': payload, - if (promptMessage != null) 'promptMessage': promptMessage, - 'keyFormat': keyFormat.wireValue, - }; - - if (androidOptions != null) { - map.addAll(androidOptions!.toMethodChannelMap()); - } - - if (iosOptions != null) { - map.addAll(iosOptions!.toMethodChannelMap()); - } - - if (macosOptions != null) { - map.addAll(macosOptions!.toMethodChannelMap()); - } - - return map; - } -} - -/// Android-specific overrides for a signature request. -class AndroidSignatureOptions { - /// Creates a new [AndroidSignatureOptions] instance. - const AndroidSignatureOptions({ - this.cancelButtonText, - this.allowDeviceCredentials, - this.subtitle, - }); - - /// Text displayed on the cancel button in the biometric prompt. - final String? cancelButtonText; - - /// Whether device credentials (PIN/Pattern/Password) may satisfy the biometric prompt. - final bool? allowDeviceCredentials; - - /// Optional subtitle displayed beneath the prompt title. - final String? subtitle; - - /// Whether any Android-specific values have been provided. - bool get hasValues => - cancelButtonText != null || - allowDeviceCredentials != null || - subtitle != null; - - /// Converts Android-specific options to a method-channel friendly map. - Map toMethodChannelMap() { - return { - if (cancelButtonText != null) 'cancelButtonText': cancelButtonText, - if (allowDeviceCredentials != null) - 'allowDeviceCredentials': allowDeviceCredentials, - if (subtitle != null) 'subtitle': subtitle, - }; - } -} - -/// iOS-specific overrides for a signature request. -class IosSignatureOptions { - /// Creates a new [IosSignatureOptions] instance. - const IosSignatureOptions({this.shouldMigrate}); - - /// Whether legacy Keychain keys (pre-5.x) should be migrated into Secure Enclave. - final bool? shouldMigrate; - - /// Whether any iOS-specific values have been provided. - bool get hasValues => shouldMigrate != null; - - /// Converts iOS-specific options to a method-channel friendly map. - Map toMethodChannelMap() { - return {if (shouldMigrate != null) 'shouldMigrate': shouldMigrate}; - } -} - -/// macOS-specific overrides for a signature request. -/// -/// Currently empty as macOS does not require migration or other special options. -class MacosSignatureOptions { - /// Creates a new [MacosSignatureOptions] instance. - const MacosSignatureOptions(); - - /// Whether any macOS-specific values have been provided. - bool get hasValues => false; - - /// Converts macOS-specific options to a method-channel friendly map. - Map toMethodChannelMap() { - return {}; - } -} diff --git a/macos/Classes/BiometricSignatureApi.swift b/macos/Classes/BiometricSignatureApi.swift new file mode 100644 index 0000000..5f2dbe8 --- /dev/null +++ b/macos/Classes/BiometricSignatureApi.swift @@ -0,0 +1,934 @@ +// Autogenerated from Pigeon (v26.1.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Sendable? + + init(code: String, message: String?, details: Sendable?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +func deepEqualsBiometricSignatureApi(_ lhs: Any?, _ rhs: Any?) -> Bool { + let cleanLhs = nilOrValue(lhs) as Any? + let cleanRhs = nilOrValue(rhs) as Any? + switch (cleanLhs, cleanRhs) { + case (nil, nil): + return true + + case (nil, _), (_, nil): + return false + + case is (Void, Void): + return true + + case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): + return cleanLhsHashable == cleanRhsHashable + + case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): + guard cleanLhsArray.count == cleanRhsArray.count else { return false } + for (index, element) in cleanLhsArray.enumerated() { + if !deepEqualsBiometricSignatureApi(element, cleanRhsArray[index]) { + return false + } + } + return true + + case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } + for (key, cleanLhsValue) in cleanLhsDictionary { + guard cleanRhsDictionary.index(forKey: key) != nil else { return false } + if !deepEqualsBiometricSignatureApi(cleanLhsValue, cleanRhsDictionary[key]!) { + return false + } + } + return true + + default: + // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. + return false + } +} + +func deepHashBiometricSignatureApi(value: Any?, hasher: inout Hasher) { + if let valueList = value as? [AnyHashable] { + for item in valueList { deepHashBiometricSignatureApi(value: item, hasher: &hasher) } + return + } + + if let valueDict = value as? [AnyHashable: AnyHashable] { + for key in valueDict.keys { + hasher.combine(key) + deepHashBiometricSignatureApi(value: valueDict[key]!, hasher: &hasher) + } + return + } + + if let hashableValue = value as? AnyHashable { + hasher.combine(hashableValue.hashValue) + } + + return hasher.combine(String(describing: value)) +} + + + +/// Types of biometric authentication supported by the device. +enum BiometricType: Int { + /// Face recognition (Face ID on iOS, face unlock on Android). + case face = 0 + /// Fingerprint recognition (Touch ID on iOS/macOS, fingerprint on Android). + case fingerprint = 1 + /// Iris scanner (Android only, rare on consumer devices). + case iris = 2 + /// Multiple biometric types are available on the device. + case multiple = 3 + /// No biometric hardware available or biometrics are disabled. + case unavailable = 4 +} + +/// Standardized error codes for the plugin. +enum BiometricError: Int { + /// The operation was successful. + case success = 0 + /// The user canceled the operation. + case userCanceled = 1 + /// Biometric authentication is not available on this device. + case notAvailable = 2 + /// No biometrics are enrolled. + case notEnrolled = 3 + /// The user is temporarily locked out due to too many failed attempts. + case lockedOut = 4 + /// The user is permanently locked out until they log in with a strong method. + case lockedOutPermanent = 5 + /// The requested key was not found. + case keyNotFound = 6 + /// The key has been invalidated (e.g. by new biometric enrollment). + case keyInvalidated = 7 + /// An unknown error occurred. + case unknown = 8 + /// The input payload was invalid (e.g. not valid Base64). + case invalidInput = 9 +} + +/// The cryptographic algorithm to use for key generation. +enum SignatureType: Int { + /// RSA-2048 (Android: native, iOS/macOS: hybrid mode with Secure Enclave EC). + case rsa = 0 + /// ECDSA P-256 (hardware-backed on all platforms). + case ecdsa = 1 +} + +/// Output format for public keys. +enum KeyFormat: Int { + /// Base64-encoded DER (SubjectPublicKeyInfo). + case base64 = 0 + /// PEM format with BEGIN/END PUBLIC KEY headers. + case pem = 1 + /// Hexadecimal-encoded DER. + case hex = 2 + /// Raw DER bytes (returned via `publicKeyBytes`). + case raw = 3 +} + +/// Output format for cryptographic signatures. +enum SignatureFormat: Int { + /// Base64-encoded signature bytes. + case base64 = 0 + /// Hexadecimal-encoded signature bytes. + case hex = 1 + /// Raw signature bytes (returned via `signatureBytes`). + case raw = 2 +} + +/// Input format for encrypted payloads to decrypt. +enum PayloadFormat: Int { + /// Base64-encoded ciphertext. + case base64 = 0 + /// Hexadecimal-encoded ciphertext. + case hex = 1 + /// Raw UTF-8 string (not recommended for binary data). + case raw = 2 +} + +/// Generated class from Pigeon that represents data sent in messages. +struct BiometricAvailability: Hashable { + var canAuthenticate: Bool? = nil + var hasEnrolledBiometrics: Bool? = nil + var availableBiometrics: [BiometricType?]? = nil + var reason: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> BiometricAvailability? { + let canAuthenticate: Bool? = nilOrValue(pigeonVar_list[0]) + let hasEnrolledBiometrics: Bool? = nilOrValue(pigeonVar_list[1]) + let availableBiometrics: [BiometricType?]? = nilOrValue(pigeonVar_list[2]) + let reason: String? = nilOrValue(pigeonVar_list[3]) + + return BiometricAvailability( + canAuthenticate: canAuthenticate, + hasEnrolledBiometrics: hasEnrolledBiometrics, + availableBiometrics: availableBiometrics, + reason: reason + ) + } + func toList() -> [Any?] { + return [ + canAuthenticate, + hasEnrolledBiometrics, + availableBiometrics, + reason, + ] + } + static func == (lhs: BiometricAvailability, rhs: BiometricAvailability) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct KeyCreationResult: Hashable { + var publicKey: String? = nil + var publicKeyBytes: FlutterStandardTypedData? = nil + var error: String? = nil + var code: BiometricError? = nil + var algorithm: String? = nil + var keySize: Int64? = nil + var decryptingPublicKey: String? = nil + var decryptingAlgorithm: String? = nil + var decryptingKeySize: Int64? = nil + var isHybridMode: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> KeyCreationResult? { + let publicKey: String? = nilOrValue(pigeonVar_list[0]) + let publicKeyBytes: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[1]) + let error: String? = nilOrValue(pigeonVar_list[2]) + let code: BiometricError? = nilOrValue(pigeonVar_list[3]) + let algorithm: String? = nilOrValue(pigeonVar_list[4]) + let keySize: Int64? = nilOrValue(pigeonVar_list[5]) + let decryptingPublicKey: String? = nilOrValue(pigeonVar_list[6]) + let decryptingAlgorithm: String? = nilOrValue(pigeonVar_list[7]) + let decryptingKeySize: Int64? = nilOrValue(pigeonVar_list[8]) + let isHybridMode: Bool? = nilOrValue(pigeonVar_list[9]) + + return KeyCreationResult( + publicKey: publicKey, + publicKeyBytes: publicKeyBytes, + error: error, + code: code, + algorithm: algorithm, + keySize: keySize, + decryptingPublicKey: decryptingPublicKey, + decryptingAlgorithm: decryptingAlgorithm, + decryptingKeySize: decryptingKeySize, + isHybridMode: isHybridMode + ) + } + func toList() -> [Any?] { + return [ + publicKey, + publicKeyBytes, + error, + code, + algorithm, + keySize, + decryptingPublicKey, + decryptingAlgorithm, + decryptingKeySize, + isHybridMode, + ] + } + static func == (lhs: KeyCreationResult, rhs: KeyCreationResult) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct SignatureResult: Hashable { + var signature: String? = nil + var signatureBytes: FlutterStandardTypedData? = nil + var publicKey: String? = nil + var error: String? = nil + var code: BiometricError? = nil + var algorithm: String? = nil + var keySize: Int64? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> SignatureResult? { + let signature: String? = nilOrValue(pigeonVar_list[0]) + let signatureBytes: FlutterStandardTypedData? = nilOrValue(pigeonVar_list[1]) + let publicKey: String? = nilOrValue(pigeonVar_list[2]) + let error: String? = nilOrValue(pigeonVar_list[3]) + let code: BiometricError? = nilOrValue(pigeonVar_list[4]) + let algorithm: String? = nilOrValue(pigeonVar_list[5]) + let keySize: Int64? = nilOrValue(pigeonVar_list[6]) + + return SignatureResult( + signature: signature, + signatureBytes: signatureBytes, + publicKey: publicKey, + error: error, + code: code, + algorithm: algorithm, + keySize: keySize + ) + } + func toList() -> [Any?] { + return [ + signature, + signatureBytes, + publicKey, + error, + code, + algorithm, + keySize, + ] + } + static func == (lhs: SignatureResult, rhs: SignatureResult) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Generated class from Pigeon that represents data sent in messages. +struct DecryptResult: Hashable { + var decryptedData: String? = nil + var error: String? = nil + var code: BiometricError? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> DecryptResult? { + let decryptedData: String? = nilOrValue(pigeonVar_list[0]) + let error: String? = nilOrValue(pigeonVar_list[1]) + let code: BiometricError? = nilOrValue(pigeonVar_list[2]) + + return DecryptResult( + decryptedData: decryptedData, + error: error, + code: code + ) + } + func toList() -> [Any?] { + return [ + decryptedData, + error, + code, + ] + } + static func == (lhs: DecryptResult, rhs: DecryptResult) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Detailed information about existing biometric keys. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct KeyInfo: Hashable { + /// Whether any biometric key exists on the device. + var exists: Bool? = nil + /// Whether the key is still valid (not invalidated by biometric changes). + /// Only populated when `checkValidity: true` is passed. + var isValid: Bool? = nil + /// The algorithm of the signing key (e.g., "RSA", "EC"). + var algorithm: String? = nil + /// The key size in bits (e.g., 2048 for RSA, 256 for EC). + var keySize: Int64? = nil + /// Whether the key is in hybrid mode (separate signing and decryption keys). + var isHybridMode: Bool? = nil + /// Signing key public key (formatted according to the requested format). + var publicKey: String? = nil + /// Decryption key public key for hybrid mode. + var decryptingPublicKey: String? = nil + /// Algorithm of the decryption key (hybrid mode only). + var decryptingAlgorithm: String? = nil + /// Key size of the decryption key in bits (hybrid mode only). + var decryptingKeySize: Int64? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> KeyInfo? { + let exists: Bool? = nilOrValue(pigeonVar_list[0]) + let isValid: Bool? = nilOrValue(pigeonVar_list[1]) + let algorithm: String? = nilOrValue(pigeonVar_list[2]) + let keySize: Int64? = nilOrValue(pigeonVar_list[3]) + let isHybridMode: Bool? = nilOrValue(pigeonVar_list[4]) + let publicKey: String? = nilOrValue(pigeonVar_list[5]) + let decryptingPublicKey: String? = nilOrValue(pigeonVar_list[6]) + let decryptingAlgorithm: String? = nilOrValue(pigeonVar_list[7]) + let decryptingKeySize: Int64? = nilOrValue(pigeonVar_list[8]) + + return KeyInfo( + exists: exists, + isValid: isValid, + algorithm: algorithm, + keySize: keySize, + isHybridMode: isHybridMode, + publicKey: publicKey, + decryptingPublicKey: decryptingPublicKey, + decryptingAlgorithm: decryptingAlgorithm, + decryptingKeySize: decryptingKeySize + ) + } + func toList() -> [Any?] { + return [ + exists, + isValid, + algorithm, + keySize, + isHybridMode, + publicKey, + decryptingPublicKey, + decryptingAlgorithm, + decryptingKeySize, + ] + } + static func == (lhs: KeyInfo, rhs: KeyInfo) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Configuration for key creation (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// Windows ignores most fields as it only supports RSA with mandatory +/// Windows Hello authentication. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct CreateKeysConfig: Hashable { + /// [Android/iOS/macOS] The cryptographic algorithm to use. + /// Windows only supports RSA and ignores this field. + var signatureType: SignatureType? = nil + /// [Android/iOS/macOS] Whether to require biometric authentication + /// during key creation. Windows always authenticates via Windows Hello. + var enforceBiometric: Bool? = nil + /// [Android/iOS/macOS] Whether to invalidate the key when new biometrics + /// are enrolled. Not supported on Windows. + /// + /// **Security Note**: When `true`, keys become invalid if fingerprints/faces + /// are added or removed, preventing unauthorized access if an attacker + /// enrolls their own biometrics on a compromised device. + var setInvalidatedByBiometricEnrollment: Bool? = nil + /// [Android/iOS/macOS] Whether to allow device credentials (PIN/pattern/passcode) + /// as fallback for biometric authentication. Not supported on Windows. + var useDeviceCredentials: Bool? = nil + /// [Android] Whether to enable decryption capability for the key. + /// On iOS/macOS, decryption is always available with EC keys. + var enableDecryption: Bool? = nil + /// [Android] Subtitle text for the biometric prompt. + var promptSubtitle: String? = nil + /// [Android] Description text for the biometric prompt. + var promptDescription: String? = nil + /// [Android] Text for the cancel button in the biometric prompt. + var cancelButtonText: String? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> CreateKeysConfig? { + let signatureType: SignatureType? = nilOrValue(pigeonVar_list[0]) + let enforceBiometric: Bool? = nilOrValue(pigeonVar_list[1]) + let setInvalidatedByBiometricEnrollment: Bool? = nilOrValue(pigeonVar_list[2]) + let useDeviceCredentials: Bool? = nilOrValue(pigeonVar_list[3]) + let enableDecryption: Bool? = nilOrValue(pigeonVar_list[4]) + let promptSubtitle: String? = nilOrValue(pigeonVar_list[5]) + let promptDescription: String? = nilOrValue(pigeonVar_list[6]) + let cancelButtonText: String? = nilOrValue(pigeonVar_list[7]) + + return CreateKeysConfig( + signatureType: signatureType, + enforceBiometric: enforceBiometric, + setInvalidatedByBiometricEnrollment: setInvalidatedByBiometricEnrollment, + useDeviceCredentials: useDeviceCredentials, + enableDecryption: enableDecryption, + promptSubtitle: promptSubtitle, + promptDescription: promptDescription, + cancelButtonText: cancelButtonText + ) + } + func toList() -> [Any?] { + return [ + signatureType, + enforceBiometric, + setInvalidatedByBiometricEnrollment, + useDeviceCredentials, + enableDecryption, + promptSubtitle, + promptDescription, + cancelButtonText, + ] + } + static func == (lhs: CreateKeysConfig, rhs: CreateKeysConfig) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Configuration for signature creation (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct CreateSignatureConfig: Hashable { + /// [Android] Subtitle text for the biometric prompt. + var promptSubtitle: String? = nil + /// [Android] Description text for the biometric prompt. + var promptDescription: String? = nil + /// [Android] Text for the cancel button in the biometric prompt. + var cancelButtonText: String? = nil + /// [Android] Whether to allow device credentials (PIN/pattern) as fallback. + var allowDeviceCredentials: Bool? = nil + /// [iOS] Whether to migrate from legacy keychain storage. + var shouldMigrate: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> CreateSignatureConfig? { + let promptSubtitle: String? = nilOrValue(pigeonVar_list[0]) + let promptDescription: String? = nilOrValue(pigeonVar_list[1]) + let cancelButtonText: String? = nilOrValue(pigeonVar_list[2]) + let allowDeviceCredentials: Bool? = nilOrValue(pigeonVar_list[3]) + let shouldMigrate: Bool? = nilOrValue(pigeonVar_list[4]) + + return CreateSignatureConfig( + promptSubtitle: promptSubtitle, + promptDescription: promptDescription, + cancelButtonText: cancelButtonText, + allowDeviceCredentials: allowDeviceCredentials, + shouldMigrate: shouldMigrate + ) + } + func toList() -> [Any?] { + return [ + promptSubtitle, + promptDescription, + cancelButtonText, + allowDeviceCredentials, + shouldMigrate, + ] + } + static func == (lhs: CreateSignatureConfig, rhs: CreateSignatureConfig) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +/// Configuration for decryption (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// Note: Decryption is not supported on Windows. +/// +/// Generated class from Pigeon that represents data sent in messages. +struct DecryptConfig: Hashable { + /// [Android] Subtitle text for the biometric prompt. + var promptSubtitle: String? = nil + /// [Android] Description text for the biometric prompt. + var promptDescription: String? = nil + /// [Android] Text for the cancel button in the biometric prompt. + var cancelButtonText: String? = nil + /// [Android] Whether to allow device credentials (PIN/pattern) as fallback. + var allowDeviceCredentials: Bool? = nil + /// [iOS] Whether to migrate from legacy keychain storage. + var shouldMigrate: Bool? = nil + + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ pigeonVar_list: [Any?]) -> DecryptConfig? { + let promptSubtitle: String? = nilOrValue(pigeonVar_list[0]) + let promptDescription: String? = nilOrValue(pigeonVar_list[1]) + let cancelButtonText: String? = nilOrValue(pigeonVar_list[2]) + let allowDeviceCredentials: Bool? = nilOrValue(pigeonVar_list[3]) + let shouldMigrate: Bool? = nilOrValue(pigeonVar_list[4]) + + return DecryptConfig( + promptSubtitle: promptSubtitle, + promptDescription: promptDescription, + cancelButtonText: cancelButtonText, + allowDeviceCredentials: allowDeviceCredentials, + shouldMigrate: shouldMigrate + ) + } + func toList() -> [Any?] { + return [ + promptSubtitle, + promptDescription, + cancelButtonText, + allowDeviceCredentials, + shouldMigrate, + ] + } + static func == (lhs: DecryptConfig, rhs: DecryptConfig) -> Bool { + return deepEqualsBiometricSignatureApi(lhs.toList(), rhs.toList()) } + func hash(into hasher: inout Hasher) { + deepHashBiometricSignatureApi(value: toList(), hasher: &hasher) + } +} + +private class BiometricSignatureApiPigeonCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 129: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return BiometricType(rawValue: enumResultAsInt) + } + return nil + case 130: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return BiometricError(rawValue: enumResultAsInt) + } + return nil + case 131: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return SignatureType(rawValue: enumResultAsInt) + } + return nil + case 132: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return KeyFormat(rawValue: enumResultAsInt) + } + return nil + case 133: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return SignatureFormat(rawValue: enumResultAsInt) + } + return nil + case 134: + let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) + if let enumResultAsInt = enumResultAsInt { + return PayloadFormat(rawValue: enumResultAsInt) + } + return nil + case 135: + return BiometricAvailability.fromList(self.readValue() as! [Any?]) + case 136: + return KeyCreationResult.fromList(self.readValue() as! [Any?]) + case 137: + return SignatureResult.fromList(self.readValue() as! [Any?]) + case 138: + return DecryptResult.fromList(self.readValue() as! [Any?]) + case 139: + return KeyInfo.fromList(self.readValue() as! [Any?]) + case 140: + return CreateKeysConfig.fromList(self.readValue() as! [Any?]) + case 141: + return CreateSignatureConfig.fromList(self.readValue() as! [Any?]) + case 142: + return DecryptConfig.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class BiometricSignatureApiPigeonCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? BiometricType { + super.writeByte(129) + super.writeValue(value.rawValue) + } else if let value = value as? BiometricError { + super.writeByte(130) + super.writeValue(value.rawValue) + } else if let value = value as? SignatureType { + super.writeByte(131) + super.writeValue(value.rawValue) + } else if let value = value as? KeyFormat { + super.writeByte(132) + super.writeValue(value.rawValue) + } else if let value = value as? SignatureFormat { + super.writeByte(133) + super.writeValue(value.rawValue) + } else if let value = value as? PayloadFormat { + super.writeByte(134) + super.writeValue(value.rawValue) + } else if let value = value as? BiometricAvailability { + super.writeByte(135) + super.writeValue(value.toList()) + } else if let value = value as? KeyCreationResult { + super.writeByte(136) + super.writeValue(value.toList()) + } else if let value = value as? SignatureResult { + super.writeByte(137) + super.writeValue(value.toList()) + } else if let value = value as? DecryptResult { + super.writeByte(138) + super.writeValue(value.toList()) + } else if let value = value as? KeyInfo { + super.writeByte(139) + super.writeValue(value.toList()) + } else if let value = value as? CreateKeysConfig { + super.writeByte(140) + super.writeValue(value.toList()) + } else if let value = value as? CreateSignatureConfig { + super.writeByte(141) + super.writeValue(value.toList()) + } else if let value = value as? DecryptConfig { + super.writeByte(142) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class BiometricSignatureApiPigeonCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return BiometricSignatureApiPigeonCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return BiometricSignatureApiPigeonCodecWriter(data: data) + } +} + +class BiometricSignatureApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { + static let shared = BiometricSignatureApiPigeonCodec(readerWriter: BiometricSignatureApiPigeonCodecReaderWriter()) +} + + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol BiometricSignatureApi { + /// Checks if biometric authentication is available. + func biometricAuthAvailable(completion: @escaping (Result) -> Void) + /// Creates a new key pair. + /// + /// [config] contains platform-specific options. See [CreateKeysConfig]. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + func createKeys(config: CreateKeysConfig?, keyFormat: KeyFormat, promptMessage: String?, completion: @escaping (Result) -> Void) + /// Creates a signature. + /// + /// [payload] is the data to sign. + /// [config] contains platform-specific options. See [CreateSignatureConfig]. + /// [signatureFormat] specifies the output format for the signature. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + func createSignature(payload: String, config: CreateSignatureConfig?, signatureFormat: SignatureFormat, keyFormat: KeyFormat, promptMessage: String?, completion: @escaping (Result) -> Void) + /// Decrypts data. + /// + /// Note: Not supported on Windows. + /// [payload] is the encrypted data. + /// [payloadFormat] specifies the format of the encrypted data. + /// [config] contains platform-specific options. See [DecryptConfig]. + /// [promptMessage] is the message shown to the user during authentication. + func decrypt(payload: String, payloadFormat: PayloadFormat, config: DecryptConfig?, promptMessage: String?, completion: @escaping (Result) -> Void) + /// Deletes keys. + func deleteKeys(completion: @escaping (Result) -> Void) + /// Gets detailed information about existing biometric keys. + /// + /// Returns key metadata including algorithm, size, validity, and public keys. + func getKeyInfo(checkValidity: Bool, keyFormat: KeyFormat, completion: @escaping (Result) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class BiometricSignatureApiSetup { + static var codec: FlutterStandardMessageCodec { BiometricSignatureApiPigeonCodec.shared } + /// Sets up an instance of `BiometricSignatureApi` to handle messages through the `binaryMessenger`. + static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BiometricSignatureApi?, messageChannelSuffix: String = "") { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + /// Checks if biometric authentication is available. + let biometricAuthAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.biometricAuthAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + biometricAuthAvailableChannel.setMessageHandler { _, reply in + api.biometricAuthAvailable { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + biometricAuthAvailableChannel.setMessageHandler(nil) + } + /// Creates a new key pair. + /// + /// [config] contains platform-specific options. See [CreateKeysConfig]. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + let createKeysChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createKeys\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + createKeysChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let configArg: CreateKeysConfig? = nilOrValue(args[0]) + let keyFormatArg = args[1] as! KeyFormat + let promptMessageArg: String? = nilOrValue(args[2]) + api.createKeys(config: configArg, keyFormat: keyFormatArg, promptMessage: promptMessageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + createKeysChannel.setMessageHandler(nil) + } + /// Creates a signature. + /// + /// [payload] is the data to sign. + /// [config] contains platform-specific options. See [CreateSignatureConfig]. + /// [signatureFormat] specifies the output format for the signature. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + let createSignatureChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createSignature\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + createSignatureChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let payloadArg = args[0] as! String + let configArg: CreateSignatureConfig? = nilOrValue(args[1]) + let signatureFormatArg = args[2] as! SignatureFormat + let keyFormatArg = args[3] as! KeyFormat + let promptMessageArg: String? = nilOrValue(args[4]) + api.createSignature(payload: payloadArg, config: configArg, signatureFormat: signatureFormatArg, keyFormat: keyFormatArg, promptMessage: promptMessageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + createSignatureChannel.setMessageHandler(nil) + } + /// Decrypts data. + /// + /// Note: Not supported on Windows. + /// [payload] is the encrypted data. + /// [payloadFormat] specifies the format of the encrypted data. + /// [config] contains platform-specific options. See [DecryptConfig]. + /// [promptMessage] is the message shown to the user during authentication. + let decryptChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.decrypt\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + decryptChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let payloadArg = args[0] as! String + let payloadFormatArg = args[1] as! PayloadFormat + let configArg: DecryptConfig? = nilOrValue(args[2]) + let promptMessageArg: String? = nilOrValue(args[3]) + api.decrypt(payload: payloadArg, payloadFormat: payloadFormatArg, config: configArg, promptMessage: promptMessageArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + decryptChannel.setMessageHandler(nil) + } + /// Deletes keys. + let deleteKeysChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.deleteKeys\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + deleteKeysChannel.setMessageHandler { _, reply in + api.deleteKeys { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + deleteKeysChannel.setMessageHandler(nil) + } + /// Gets detailed information about existing biometric keys. + /// + /// Returns key metadata including algorithm, size, validity, and public keys. + let getKeyInfoChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.getKeyInfo\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + getKeyInfoChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let checkValidityArg = args[0] as! Bool + let keyFormatArg = args[1] as! KeyFormat + api.getKeyInfo(checkValidity: checkValidityArg, keyFormat: keyFormatArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getKeyInfoChannel.setMessageHandler(nil) + } + } +} diff --git a/macos/Classes/BiometricSignaturePlugin.swift b/macos/Classes/BiometricSignaturePlugin.swift index 25613a6..6e274b9 100644 --- a/macos/Classes/BiometricSignaturePlugin.swift +++ b/macos/Classes/BiometricSignaturePlugin.swift @@ -1,89 +1,49 @@ -import Cocoa import FlutterMacOS +import Cocoa import LocalAuthentication import Security private enum Constants { - static let authFailed = "AUTH_FAILED" - static let invalidPayload = "INVALID_PAYLOAD" - static let invalidArguments = "INVALID_ARGUMENTS" - // App-specific prefix to isolate keychain items per app on macOS private static var appPrefix: String { Bundle.main.bundleIdentifier ?? "com.visionflutter.biometric_signature" } - + // Bundle-specific keychain identifiers to prevent cross-app conflicts static var biometricKeyAlias: String { "\(appPrefix).biometric_key" } - + static var ecKeyAlias: Data { "\(appPrefix).eckey".data(using: .utf8)! } - + static var invalidationSettingKey: String { "\(appPrefix).invalidation_setting" } } -private enum KeyFormat: String { - case base64 = "BASE64" - case pem = "PEM" - case raw = "RAW" - case hex = "HEX" - - static func from(_ raw: Any?) -> KeyFormat { - guard let string = raw as? String, - let format = KeyFormat(rawValue: string.uppercased()) else { - return .base64 - } - return format - } - - var channelValue: String { rawValue } -} - -private struct FormattedOutput { - let value: Any - let format: KeyFormat - let pemLabel: String? -} - -private let iso8601Formatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return formatter -}() - // MARK: - Domain State (biometry change detection) private enum DomainState { - // App-specific service identifier - private static var service: String { - let bundleId = Bundle.main.bundleIdentifier ?? "com.visionflutter.biometric_signature" - return "\(bundleId).domain_state" - } - + static let service = "com.visionflutter.biometric_signature.domain_state" private static func account() -> String { "biometric_domain_state_v1" } static func saveCurrent() { let ctx = LAContext() var err: NSError? guard ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &err), - let state = ctx.evaluatedPolicyDomainState else { return } + let state = ctx.evaluatedPolicyDomainState else { return } - autoreleasepool { - let base: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: account() - ] - let attrs: [String: Any] = [kSecValueData as String: state] - let status = SecItemUpdate(base as CFDictionary, attrs as CFDictionary) - if status == errSecItemNotFound { - var add = base; add[kSecValueData as String] = state - _ = SecItemAdd(add as CFDictionary, nil) - } + let base: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account() + ] + let attrs: [String: Any] = [kSecValueData as String: state] + let status = SecItemUpdate(base as CFDictionary, attrs as CFDictionary) + if status == errSecItemNotFound { + var add = base; add[kSecValueData as String] = state + _ = SecItemAdd(add as CFDictionary, nil) } } @@ -117,7 +77,7 @@ private enum DomainState { let ctx = LAContext() var laErr: NSError? guard ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &laErr), - let current = ctx.evaluatedPolicyDomainState else { + let current = ctx.evaluatedPolicyDomainState else { // If we can't evaluate and we *had* a baseline, be conservative. return loadSaved() != nil } @@ -132,19 +92,17 @@ private enum DomainState { private enum InvalidationSetting { static func save(_ invalidateOnEnrollment: Bool) { let data = invalidateOnEnrollment ? Data([1]) : Data([0]) - autoreleasepool { - let base: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: Constants.invalidationSettingKey, - kSecAttrAccount as String: Constants.invalidationSettingKey - ] - let attrs: [String: Any] = [kSecValueData as String: data] - let status = SecItemUpdate(base as CFDictionary, attrs as CFDictionary) - if status == errSecItemNotFound { - var add = base - add[kSecValueData as String] = data - _ = SecItemAdd(add as CFDictionary, nil) - } + let base: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: Constants.invalidationSettingKey, + kSecAttrAccount as String: Constants.invalidationSettingKey + ] + let attrs: [String: Any] = [kSecValueData as String: data] + let status = SecItemUpdate(base as CFDictionary, attrs as CFDictionary) + if status == errSecItemNotFound { + var add = base + add[kSecValueData as String] = data + _ = SecItemAdd(add as CFDictionary, nil) } } @@ -176,1018 +134,632 @@ private enum InvalidationSetting { } } -public class BiometricSignaturePlugin: NSObject, FlutterPlugin { +public class BiometricSignaturePlugin: NSObject, FlutterPlugin, BiometricSignatureApi { + public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "biometric_signature", binaryMessenger: registrar.messenger) let instance = BiometricSignaturePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) + BiometricSignatureApiSetup.setUp(binaryMessenger: registrar.messenger, api: instance) } - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "createKeys": - if let arguments = call.arguments as? [String: Any] { - let useDeviceCredentials = arguments["useDeviceCredentials"] as? Bool ?? false - let useEc = arguments["useEc"] as? Bool ?? false - let keyFormat = KeyFormat.from(arguments["keyFormat"]) - let biometryCurrentSet = arguments["biometryCurrentSet"] as! Bool - let enforceBiometric = arguments["enforceBiometric"] as? Bool ?? false - let promptMessage = arguments["promptMessage"] as? String ?? "Authenticate to create keys" - createKeys( - useDeviceCredentials: useDeviceCredentials, - useEc: useEc, - keyFormat: keyFormat, - biometryCurrentSet: biometryCurrentSet, - enforceBiometric: enforceBiometric, - promptMessage: promptMessage, - result: result - ) - } else { - result(FlutterError(code: Constants.invalidArguments, message: "Invalid arguments", details: nil)) - } - case "createSignature": - createSignature(options: call.arguments as? [String: Any], result: result) - case "decrypt": - decrypt(options: call.arguments as? [String: Any], result: result) - case "testEncrypt": - testEncrypt(options: call.arguments as? [String: Any], result: result) - case "deleteKeys": - deleteKeys(result: result) - case "biometricAuthAvailable": - biometricAuthAvailable(result: result) - case "biometricKeyExists": - guard let checkValidity = call.arguments as? Bool else { return } - biometricKeyExists(checkValidity: checkValidity, result: result) - default: - result(FlutterMethodNotImplemented) - } - } - // MARK: - Public API + // MARK: - BiometricSignatureApi Implementation - private func biometricAuthAvailable(result: @escaping FlutterResult) { + func biometricAuthAvailable(completion: @escaping (Result) -> Void) { let context = LAContext() var error: NSError? - let canEvaluatePolicy = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) - - if canEvaluatePolicy { - let biometricType = getBiometricType(context) - dispatchMainAsync { result(biometricType) } - } else { - let errorMessage = error?.localizedDescription ?? "" - dispatchMainAsync { result("none, \(errorMessage)") } - } - } - - private func biometricKeyExists(checkValidity: Bool, result: @escaping FlutterResult) { - let exists = self.doesBiometricKeyExist(checkValidity: checkValidity) - dispatchMainAsync { result(exists) } - } - - private func deleteKeys(result: @escaping FlutterResult) { - autoreleasepool { - // Delete EC key pair - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - ] - let ecStatus = SecItemDelete(ecKeyQuery as CFDictionary) - - // Delete encrypted RSA private key from Keychain - let encryptedKeyTag = Constants.biometricKeyAlias - let encryptedKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag - ] - let rsaStatus = SecItemDelete(encryptedKeyQuery as CFDictionary) - - // Delete saved domain-state baseline - let dsOK = DomainState.deleteSaved() - - // Delete invalidation setting - let isOK = InvalidationSetting.delete() - - let success = (ecStatus == errSecSuccess || ecStatus == errSecItemNotFound) - && (rsaStatus == errSecSuccess || rsaStatus == errSecItemNotFound) - && dsOK && isOK - dispatchMainAsync { - if success { - result(true) - } else { - result(FlutterError(code: Constants.authFailed, message: "Error deleting the biometric key", details: nil)) - } - } - } - } - - private func deleteExistingKeys() { - // --- Delete EC key pair --- - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: CFDictionary = [ - kSecClass: kSecClassKey, - kSecAttrApplicationTag: ecTag, - kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom - ] as CFDictionary - SecItemDelete(ecKeyQuery) - - // --- Delete encrypted RSA private key --- - let rsaTag = getBiometricKeyTag() - let rsaQuery: CFDictionary = [ - kSecClass: kSecClassGenericPassword, - kSecAttrService: rsaTag, - kSecAttrAccount: rsaTag - ] as CFDictionary - SecItemDelete(rsaQuery) - - // --- Domain state + invalidation flag --- - _ = DomainState.deleteSaved() - _ = InvalidationSetting.delete() - } - - private func createKeys( - useDeviceCredentials: Bool, - useEc: Bool, - keyFormat: KeyFormat, - biometryCurrentSet: Bool, - enforceBiometric: Bool, - promptMessage: String, - result: @escaping FlutterResult + let canEvaluate = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + + var availableBiometrics: [BiometricType?] = [] + if canEvaluate { + // Basic detection based on biometryType + if #available(iOS 11.0, *) { + switch context.biometryType { + case .faceID: availableBiometrics.append(.face) + case .touchID: availableBiometrics.append(.fingerprint) + default: break + } + } + } + + let hasEnrolled = error?.code != LAError.biometryNotEnrolled.rawValue + + completion(.success(BiometricAvailability( + canAuthenticate: canEvaluate, + hasEnrolledBiometrics: hasEnrolled, + availableBiometrics: availableBiometrics, + reason: error?.localizedDescription + ))) + } + + func createKeys( + config: CreateKeysConfig?, + keyFormat: KeyFormat, + promptMessage: String?, + completion: @escaping (Result) -> Void ) { - // Delete existing keys (and baseline) + // Extract config values with defaults + let useDeviceCredentials = config?.useDeviceCredentials ?? false + let biometryCurrentSet = config?.setInvalidatedByBiometricEnrollment ?? false + let signatureType = config?.signatureType ?? .rsa + let enforceBiometric = config?.enforceBiometric ?? false + let prompt = promptMessage ?? "Authenticate to create keys" + + // Always delete existing keys first deleteExistingKeys() - // Define the actual generation logic as a closure we can call later - let generateKeysBlock = { - autoreleasepool { - // Generate EC key pair in Secure Enclave - let flags: SecAccessControlCreateFlags = [.privateKeyUsage, useDeviceCredentials ? .userPresence : (biometryCurrentSet ? .biometryCurrentSet : .biometryAny)] - guard let ecAccessControl = SecAccessControlCreateWithFlags( - kCFAllocatorDefault, - kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, - flags, - nil - ) else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, - message: "Failed to create access control for EC key", - details: nil)) - } - return - } - - let ecTag = Constants.ecKeyAlias - let ecKeyAttributes: [String: Any] = [ - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecAttrKeySizeInBits as String: 256, - kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, - kSecAttrAccessControl as String: ecAccessControl, - kSecPrivateKeyAttrs as String: [ - kSecAttrIsPermanent as String: true, - kSecAttrApplicationTag as String: ecTag - ] - ] - - var error: Unmanaged? - guard let ecPrivateKey = SecKeyCreateRandomKey(ecKeyAttributes as CFDictionary, &error) else { - self.dispatchMainAsync { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - result(FlutterError(code: Constants.authFailed, message: "Error generating EC key: \(msg)", details: nil)) - } - return - } - - guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error getting EC public key", details: nil)) - } - return - } - - // Persist domain-state baseline right after successful EC creation (only if biometry-invalidation is enabled) - if biometryCurrentSet { - DomainState.saveCurrent() - } - InvalidationSetting.save(biometryCurrentSet) - - if useEc { - // EC-only: return EC public key - guard let response = self.buildKeyResponse(publicKey: ecPublicKey, format: keyFormat, algorithm: "EC") else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to encode EC public key", details: nil)) - } - return - } - self.dispatchMainAsync { result(response) } - return - } - - // --- Hybrid path: generate RSA and wrap its private key with ECIES(X9.63/SHA-256/AES-GCM) - let rsaKeyAttributes: [String: Any] = [ - kSecAttrKeyType as String: kSecAttrKeyTypeRSA, - kSecAttrKeySizeInBits as String: 2048, - kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent as String: false] - ] - - guard let rsaPrivate = SecKeyCreateRandomKey(rsaKeyAttributes as CFDictionary, &error), - let rsaPublicKey = SecKeyCopyPublicKey(rsaPrivate) else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error generating RSA key pair", details: nil)) - } - return - } - - // Extract RSA private key data - guard let rsaPrivateKeyData = SecKeyCopyExternalRepresentation(rsaPrivate, &error) as Data? else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error extracting RSA private key data", details: nil)) - } - return - } - - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPublicKey, .encrypt, algorithm) else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC encryption algorithm not supported", details: nil)) - } - return - } - - guard let encryptedRSAKeyData = SecKeyCreateEncryptedData(ecPublicKey, algorithm, rsaPrivateKeyData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error encrypting RSA private key: \(msg)", details: nil)) - } - return - } - - // Store encrypted RSA private key data in Keychain - let encryptedKeyTag = self.getBiometricKeyTag() - let encryptedKeyAttributes: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag, - kSecValueData as String: encryptedRSAKeyData, - kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly - ] - SecItemDelete(encryptedKeyAttributes as CFDictionary) // Delete existing item - let status = SecItemAdd(encryptedKeyAttributes as CFDictionary, nil) - guard status == errSecSuccess else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error storing encrypted RSA private key in Keychain", details: nil)) - } - return - } - - guard let response = self.buildKeyResponse(publicKey: rsaPublicKey, format: keyFormat, algorithm: "RSA") else { - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to encode RSA public key", details: nil)) - } - return - } - self.dispatchMainAsync { result(response) } - } // autoreleasepool + let generateBlock = { + self.performKeyGeneration( + useDeviceCredentials: useDeviceCredentials, + biometryCurrentSet: biometryCurrentSet, + signatureType: signatureType, + keyFormat: keyFormat + ) { result in + completion(result) + } } if enforceBiometric { let context = LAContext() context.localizedFallbackTitle = "" - context.localizedReason = promptMessage - - context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: promptMessage) { success, error in + context.localizedReason = prompt + + context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: prompt) { success, _ in if success { - // Continue to generation on background thread (already on bg thread from evaluatePolicy) - generateKeysBlock() + generateBlock() } else { - let errorMessage = error?.localizedDescription ?? "Authentication failed" - self.dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: errorMessage, details: nil)) - } + completion(.success(KeyCreationResult(publicKey: nil, error: "Authentication failed", code: .userCanceled))) } } } else { - // Run immediately on a background queue to avoid blocking UI - DispatchQueue.global(qos: .userInitiated).async { - generateKeysBlock() + DispatchQueue.global(qos: .userInitiated).async { + generateBlock() } } } - private func createSignature(options: [String: Any]?, result: @escaping FlutterResult) { - let promptMessage = (options?["promptMessage"] as? String) ?? "Authenticate" - let keyFormat = KeyFormat.from(options?["keyFormat"]) - guard let payload = options?["payload"] as? String, - let dataToSign = payload.data(using: .utf8) else { - dispatchMainAsync { - result(FlutterError(code: Constants.invalidPayload, message: "Payload is required and must be valid UTF-8", details: nil)) - } - return + func createSignature( + payload: String, + config: CreateSignatureConfig?, + signatureFormat: SignatureFormat, + keyFormat: KeyFormat, + promptMessage: String?, + completion: @escaping (Result) -> Void + ) { + guard let dataToSign = payload.data(using: .utf8) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Invalid payload", code: .invalidInput))) + return } + + let prompt = promptMessage ?? "Authenticate" - // Check if we should use EC-only mode by checking if RSA key exists - let encryptedKeyTag = getBiometricKeyTag() - let encryptedKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag, - kSecReturnData as String: true, - kSecMatchLimit as String: kSecMatchLimitOne - ] - var item: CFTypeRef? - let status = SecItemCopyMatching(encryptedKeyQuery as CFDictionary, &item) - if status != errSecSuccess { - // No RSA: EC-only signing - createECSignature( - dataToSign: dataToSign, - promptMessage: promptMessage, - keyFormat: keyFormat, - result: result - ) - return + if hasRsaKey() { + performRsaSigning(dataToSign: dataToSign, prompt: prompt, signatureFormat: signatureFormat, keyFormat: keyFormat, completion: completion) + } else { + performEcSigning(dataToSign: dataToSign, prompt: prompt, signatureFormat: signatureFormat, keyFormat: keyFormat, completion: completion) } + } - // 1. Retrieve encrypted RSA private key from Keychain - guard let encryptedRSAKeyData = item as? Data else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to retrieve encrypted RSA key data", details: nil)) - } - return + func decrypt( + payload: String, + payloadFormat: PayloadFormat, + config: DecryptConfig?, + promptMessage: String?, + completion: @escaping (Result) -> Void + ) { + let prompt = promptMessage ?? "Authenticate" + + if hasRsaKey() { + performRsaDecryption(payload: payload, payloadFormat: payloadFormat, prompt: prompt, completion: completion) + } else { + performEcDecryption(payload: payload, payloadFormat: payloadFormat, prompt: prompt, completion: completion) } + } - // 2. Retrieve EC private key from Secure Enclave - let ecTag = Constants.ecKeyAlias - - let context = LAContext() - context.localizedFallbackTitle = "" - context.localizedReason = promptMessage + func deleteKeys(completion: @escaping (Result) -> Void) { + deleteExistingKeys() + completion(.success(true)) + } + func getKeyInfo(checkValidity: Bool, keyFormat: KeyFormat, completion: @escaping (Result) -> Void) { + // Check EC key existence + let ecTag = Constants.ecKeyAlias let ecKeyQuery: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrApplicationTag as String: ecTag, kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecReturnRef as String: true, - kSecUseAuthenticationContext as String: context, - kSecUseOperationPrompt as String: promptMessage ] + var ecItem: CFTypeRef? + let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecItem) + let ecKeyExists = (ecStatus == errSecSuccess) + let ecKey = ecItem as! SecKey? - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC private key not found", details: nil)) - } - return - } - let ecPrivateKey = ecPrivateKeyRef as! SecKey - - // 3. Decrypt RSA private key data using the EC private key - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPrivateKey, .decrypt, algorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC decryption algorithm not supported", details: nil)) - } - return - } - - var error: Unmanaged? - guard var rsaPrivateKeyData = SecKeyCreateDecryptedData(ecPrivateKey, algorithm, encryptedRSAKeyData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error decrypting RSA private key: \(msg)", details: nil)) - } - return - } - - // 4. Reconstruct RSA private key from data - let rsaKeyAttributes: [String: Any] = [ - kSecAttrKeyType as String: kSecAttrKeyTypeRSA, - kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, - kSecAttrKeySizeInBits as String: 2048 + // Check if encrypted RSA key exists (hybrid mode) + let encryptedKeyTag = Constants.biometricKeyAlias + let encryptedKeyQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: encryptedKeyTag, + kSecAttrAccount as String: encryptedKeyTag, + kSecReturnData as String: true, ] - guard let rsaPrivateKey = SecKeyCreateWithData(rsaPrivateKeyData as CFData, rsaKeyAttributes as CFDictionary, &error) else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error reconstructing RSA private key: \(msg)", details: nil)) - } - return - } + var rsaItem: CFTypeRef? + let rsaStatus = SecItemCopyMatching(encryptedKeyQuery as CFDictionary, &rsaItem) + let rsaKeyExists = (rsaStatus == errSecSuccess) - // 5. Sign data with RSA private key - let signAlgorithm = SecKeyAlgorithm.rsaSignatureMessagePKCS1v15SHA256 - guard SecKeyIsAlgorithmSupported(rsaPrivateKey, .sign, signAlgorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "RSA signing algorithm not supported", details: nil)) - } + // No keys exist + guard ecKeyExists else { + completion(.success(KeyInfo(exists: false))) return } - guard let signature = SecKeyCreateSignature(rsaPrivateKey, signAlgorithm, dataToSign as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error signing data: \(msg)", details: nil)) + // Determine validity + var isValid: Bool? = nil + if checkValidity { + let shouldInvalidateOnEnrollment = InvalidationSetting.load() ?? true + if shouldInvalidateOnEnrollment { + isValid = !DomainState.biometryChangedOrUnknown() + } else { + isValid = true } - return } - // 6. Zero the decrypted RSA private key bytes in memory - rsaPrivateKeyData.resetBytes(in: 0..) -> Void + ) { + // Access Control + let flags: SecAccessControlCreateFlags = [.privateKeyUsage, useDeviceCredentials ? .userPresence : (biometryCurrentSet ? .biometryCurrentSet : .biometryAny)] + guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, flags, nil) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "Failed to create access control", code: .unknown))) return } - - // 1. Retrieve encrypted RSA private key from Keychain - let encryptedKeyTag = getBiometricKeyTag() - let encryptedKeyQuery: [String: Any] = [ + + // Create EC Key + let ecTag = Constants.ecKeyAlias + let ecAttributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecAttrKeySizeInBits as String: 256, + kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, + kSecAttrAccessControl as String: accessControl, + kSecPrivateKeyAttrs as String: [ + kSecAttrIsPermanent as String: true, + kSecAttrApplicationTag as String: ecTag + ] + ] + + var error: Unmanaged? + guard let ecPrivateKey = SecKeyCreateRandomKey(ecAttributes as CFDictionary, &error) else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "EC Key Gen Error: \(msg)", code: .unknown))) + return + } + + // Save metadata + if biometryCurrentSet { DomainState.saveCurrent() } + InvalidationSetting.save(biometryCurrentSet) + + guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "EC Pub Key Error", code: .unknown))) + return + } + + if signatureType == .ecdsa { + let keyStr = formatKey(ecPublicKey, format: keyFormat) + let data = SecKeyCopyExternalRepresentation(ecPublicKey, &error) as Data? + let typedData = data != nil ? FlutterStandardTypedData(bytes: data!) : nil + completion(.success(KeyCreationResult( + publicKey: keyStr, + publicKeyBytes: typedData, + error: nil, + code: .success, + algorithm: "EC", + keySize: 256, + decryptingPublicKey: nil, + decryptingAlgorithm: nil, + decryptingKeySize: nil, + isHybridMode: false + ))) + return + } + + // Check encryption support for Hybrid + guard SecKeyIsAlgorithmSupported(ecPublicKey, .encrypt, .eciesEncryptionStandardX963SHA256AESGCM) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "ECIES not supported", code: .unknown))) + return + } + + // Generate RSA Key + let rsaAttributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeRSA, + kSecAttrKeySizeInBits as String: 2048, + kSecPrivateKeyAttrs as String: [kSecAttrIsPermanent as String: false] + ] + guard let rsaPrivateKey = SecKeyCreateRandomKey(rsaAttributes as CFDictionary, &error) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "RSA Gen Error", code: .unknown))) + return + } + + // Wrap RSA Private Key + guard let rsaPrivateData = SecKeyCopyExternalRepresentation(rsaPrivateKey, &error) as Data?, + let encryptedRsa = SecKeyCreateEncryptedData(ecPublicKey, .eciesEncryptionStandardX963SHA256AESGCM, rsaPrivateData as CFData, &error) as Data? else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "RSA Wrapping Error", code: .unknown))) + return + } + + // Save Wrapped Key + let tag = Constants.biometricKeyAlias + let saveQuery: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag, - kSecReturnData as String: true, - kSecMatchLimit as String: kSecMatchLimitOne + kSecAttrService as String: tag, + kSecAttrAccount as String: tag, + kSecValueData as String: encryptedRsa, + kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] - var item: CFTypeRef? - let status = SecItemCopyMatching(encryptedKeyQuery as CFDictionary, &item) - - // Check if we have a hybrid RSA key or EC-only key - if status == errSecSuccess, let encryptedRSAKeyData = item as? Data { - // Hybrid RSA decryption path (existing implementation) - decryptWithHybridRSA( - encryptedRSAKeyData: encryptedRSAKeyData, - encryptedPayload: encryptedData, - promptMessage: promptMessage, - result: result - ) - return + SecItemAdd(saveQuery as CFDictionary, nil) + + guard let rsaPublicKey = SecKeyCopyPublicKey(rsaPrivateKey) else { + completion(.success(KeyCreationResult(publicKey: nil, publicKeyBytes: nil, error: "RSA Pub Key Error", code: .unknown))) + return } - // Try EC-only decryption - decryptWithECKey( - encryptedData: encryptedData, - promptMessage: promptMessage, - result: result - ) - } - - // MARK: - Decryption Helper Functions - - private func decryptWithHybridRSA( - encryptedRSAKeyData: Data, - encryptedPayload: Data, - promptMessage: String, - result: @escaping FlutterResult - ) { - autoreleasepool { - // 1. Retrieve EC private key from Secure Enclave - let ecTag = Constants.ecKeyAlias - let context = LAContext() - context.localizedFallbackTitle = "" - context.localizedReason = promptMessage - - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, - kSecUseAuthenticationContext as String: context, - kSecUseOperationPrompt as String: promptMessage - ] - - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC private key not found", details: nil)) - } - return - } - let ecPrivateKey = ecPrivateKeyRef as! SecKey + let rsaData = SecKeyCopyExternalRepresentation(rsaPublicKey, &error) as Data? + let rsaTypedData = rsaData != nil ? FlutterStandardTypedData(bytes: rsaData!) : nil - // 2. Decrypt RSA private key data using the EC private key (ECIES) - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPrivateKey, .decrypt, algorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC decryption algorithm not supported", details: nil)) - } - return - } - - var error: Unmanaged? - guard var rsaPrivateKeyData = SecKeyCreateDecryptedData(ecPrivateKey, algorithm, encryptedRSAKeyData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error decrypting RSA private key: \(msg)", details: nil)) - } - return - } - - // 3. Reconstruct RSA private key from data - let rsaKeyAttributes: [String: Any] = [ - kSecAttrKeyType as String: kSecAttrKeyTypeRSA, - kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, - kSecAttrKeySizeInBits as String: 2048 - ] - guard let rsaPrivateKey = SecKeyCreateWithData(rsaPrivateKeyData as CFData, rsaKeyAttributes as CFDictionary, &error) else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error reconstructing RSA private key: \(msg)", details: nil)) - } - return - } + let rsaKeyStr = formatKey(rsaPublicKey, format: keyFormat) - // 4. Decrypt payload with RSA private key - let decryptAlgorithm = SecKeyAlgorithm.rsaEncryptionPKCS1 - guard SecKeyIsAlgorithmSupported(rsaPrivateKey, .decrypt, decryptAlgorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "RSA decryption algorithm not supported", details: nil)) - } - return - } - - guard let decryptedData = SecKeyCreateDecryptedData(rsaPrivateKey, decryptAlgorithm, encryptedPayload as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error decrypting payload: \(msg)", details: nil)) - } - return - } - - // 5. Zero the decrypted RSA private key bytes in memory - rsaPrivateKeyData.resetBytes(in: 0..) -> Void) { + guard let rsaPrivateKey = unwrapRsaKey(prompt: prompt) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Failed to access/unwrap RSA key", code: .unknown))) + return } + + var error: Unmanaged? + guard let signature = SecKeyCreateSignature(rsaPrivateKey, .rsaSignatureMessagePKCS1v15SHA256, dataToSign as CFData, &error) as Data? else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Signing Error: \(msg)", code: .unknown))) + return + } + + guard let pub = SecKeyCopyPublicKey(rsaPrivateKey) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Pub Key Error", code: .unknown))) + return + } + + completion(.success(SignatureResult( + signature: formatSignature(signature, format: signatureFormat), + signatureBytes: FlutterStandardTypedData(bytes: signature), + publicKey: formatKey(pub, format: keyFormat), + error: nil, + code: .success, + algorithm: "RSA", + keySize: 2048 + ))) } - - private func decryptWithECKey( - encryptedData: Data, - promptMessage: String, - result: @escaping FlutterResult - ) { - autoreleasepool { - // 1. Retrieve EC private key from Secure Enclave - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, - kSecUseOperationPrompt as String: promptMessage - ] - - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC private key not found. Please create keys first.", details: nil)) - } - return - } - let ecPrivateKey = ecPrivateKeyRef as! SecKey - - // 2. Decrypt payload directly using ECIES - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPrivateKey, .decrypt, algorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "ECIES decryption algorithm not supported", details: nil)) - } - return - } - - var error: Unmanaged? - guard let decryptedData = SecKeyCreateDecryptedData(ecPrivateKey, algorithm, encryptedData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error decrypting payload with EC key: \(msg)", details: nil)) - } - return - } - - guard let decryptedString = String(data: decryptedData, encoding: .utf8) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Decrypted data is not valid UTF-8", details: nil)) - } - return - } - - dispatchMainAsync { result(["decryptedData": decryptedString]) } + + private func performEcSigning(dataToSign: Data, prompt: String, signatureFormat: SignatureFormat, keyFormat: KeyFormat, completion: @escaping (Result) -> Void) { + guard let ecKey = getEcPrivateKey(prompt: prompt) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "EC Key not found or auth failed", code: .unknown))) + return } + + var error: Unmanaged? + guard let signature = SecKeyCreateSignature(ecKey, .ecdsaSignatureMessageX962SHA256, dataToSign as CFData, &error) as Data? else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Signing Error: \(msg)", code: .unknown))) + return + } + guard let pub = SecKeyCopyPublicKey(ecKey) else { + completion(.success(SignatureResult(signature: nil, signatureBytes: nil, publicKey: nil, error: "Pub Key Error", code: .unknown))) + return + } + + completion(.success(SignatureResult( + signature: formatSignature(signature, format: signatureFormat), + signatureBytes: FlutterStandardTypedData(bytes: signature), + publicKey: formatKey(pub, format: keyFormat), + error: nil, + code: .success, + algorithm: "EC", + keySize: 256 + ))) } - - private func testEncrypt(options: [String: Any]?, result: @escaping FlutterResult) { - guard let payload = options?["payload"] as? String else { - dispatchMainAsync { - result(FlutterError(code: Constants.invalidPayload, message: "Payload is required", details: nil)) - } - return + + private func performRsaDecryption(payload: String, payloadFormat: PayloadFormat, prompt: String, completion: @escaping (Result) -> Void) { + guard let rsaPrivateKey = unwrapRsaKey(prompt: prompt) else { + completion(.success(DecryptResult(decryptedData: nil, error: "Failed to access/unwrap RSA key", code: .unknown))) + return } - - autoreleasepool { - // Retrieve EC public key - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true - ] - - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC key not found", details: nil)) - } - return - } - let ecPrivateKey = ecPrivateKeyRef as! SecKey - guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC public key not found", details: nil)) - } - return - } - - // Encrypt using native ECIES - let algorithm: SecKeyAlgorithm = .eciesEncryptionStandardX963SHA256AESGCM - guard SecKeyIsAlgorithmSupported(ecPublicKey, .encrypt, algorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "ECIES encryption algorithm not supported", details: nil)) - } - return - } - - guard let payloadData = payload.data(using: .utf8) else { - dispatchMainAsync { - result(FlutterError(code: Constants.invalidPayload, message: "Payload must be valid UTF-8", details: nil)) - } - return - } - - var error: Unmanaged? - guard let encryptedData = SecKeyCreateEncryptedData(ecPublicKey, algorithm, payloadData as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error encrypting: \(msg)", details: nil)) - } - return - } - - let base64Encrypted = encryptedData.base64EncodedString() - dispatchMainAsync { result(["encryptedPayload": base64Encrypted]) } + + var error: Unmanaged? + guard let encryptedData = parsePayload(payload, format: payloadFormat) else { + completion(.success(DecryptResult(decryptedData: nil, error: "Invalid payload", code: .invalidInput))) + return + } + + guard let decrypted = SecKeyCreateDecryptedData(rsaPrivateKey, .rsaEncryptionPKCS1, encryptedData as CFData, &error) as Data?, + let str = String(data: decrypted, encoding: .utf8) else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(DecryptResult(decryptedData: nil, error: "Decryption Error: \(msg)", code: .unknown))) + return } + + completion(.success(DecryptResult(decryptedData: str, error: nil, code: .success))) } - - private func createECSignature( - dataToSign: Data, - promptMessage: String, - keyFormat: KeyFormat, - result: @escaping FlutterResult - ) { - autoreleasepool { - // Retrieve EC private key from Secure Enclave - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, - kSecUseOperationPrompt as String: promptMessage - ] - var ecPrivateKeyRef: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecPrivateKeyRef) - guard ecStatus == errSecSuccess, let ecPrivateKeyRef = ecPrivateKeyRef else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC private key not found", details: nil)) - } - return - } - let ecPrivateKey = ecPrivateKeyRef as! SecKey - - guard let ecPublicKey = SecKeyCopyPublicKey(ecPrivateKey) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC public key not found", details: nil)) - } - return - } - - // Sign data with EC private key - let signAlgorithm = SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256 - guard SecKeyIsAlgorithmSupported(ecPrivateKey, .sign, signAlgorithm) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "EC signing algorithm not supported", details: nil)) - } - return - } - - var error: Unmanaged? - guard let signature = SecKeyCreateSignature(ecPrivateKey, signAlgorithm, dataToSign as CFData, &error) as Data? else { - let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown error" - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Error signing data with EC key: \(msg)", details: nil)) - } - return - } - guard let response = buildSignatureResponse( - publicKey: ecPublicKey, - signature: signature, - algorithm: "EC", - format: keyFormat - ) else { - dispatchMainAsync { - result(FlutterError(code: Constants.authFailed, message: "Failed to format EC signature", details: nil)) - } - return - } - - dispatchMainAsync { result(response) } + + private func performEcDecryption(payload: String, payloadFormat: PayloadFormat, prompt: String, completion: @escaping (Result) -> Void) { + guard let ecKey = getEcPrivateKey(prompt: prompt) else { + completion(.success(DecryptResult(decryptedData: nil, error: "EC Key not found or auth failed", code: .unknown))) + return } + + guard let encryptedData = parsePayload(payload, format: payloadFormat) else { + completion(.success(DecryptResult(decryptedData: nil, error: "Invalid payload", code: .invalidInput))) + return + } + + var error: Unmanaged? + guard let decrypted = SecKeyCreateDecryptedData(ecKey, .eciesEncryptionStandardX963SHA256AESGCM, encryptedData as CFData, &error) as Data?, + let str = String(data: decrypted, encoding: .utf8) else { + let msg = error?.takeRetainedValue().localizedDescription ?? "Unknown" + completion(.success(DecryptResult(decryptedData: nil, error: "Decryption Error: \(msg)", code: .unknown))) + return + } + completion(.success(DecryptResult(decryptedData: str, error: nil, code: .success))) } - // MARK: - Helpers for building responses / formatting + // MARK: - Helpers - private func buildKeyResponse(publicKey: SecKey, format: KeyFormat, algorithm: String) -> [String: Any]? { - guard let formatted = formatPublicKey(publicKey, format: format) else { return nil } - var response: [String: Any] = [ - "publicKey": formatted.value, - "publicKeyFormat": formatted.format.channelValue, - "algorithm": algorithm, - "keySize": keySizeInBits(publicKey), - "keyFormat": format.channelValue + private func deleteExistingKeys() { + let ecQuery: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: Constants.ecKeyAlias, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom ] - if let label = formatted.pemLabel { - response["publicKeyPemLabel"] = label - } - return response + SecItemDelete(ecQuery as CFDictionary) + + let rsaTag = Constants.biometricKeyAlias + let rsaQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: rsaTag, + kSecAttrAccount as String: rsaTag + ] + SecItemDelete(rsaQuery as CFDictionary) + + _ = DomainState.deleteSaved() + _ = InvalidationSetting.delete() } - - private func buildSignatureResponse(publicKey: SecKey, signature: Data, algorithm: String, format: KeyFormat) -> [String: Any]? { - guard let formattedKey = formatPublicKey(publicKey, format: format) else { return nil } - let formattedSignature = formatSignature(signature, format: format) - var response: [String: Any] = [ - "publicKey": formattedKey.value, - "publicKeyFormat": formattedKey.format.channelValue, - "signature": formattedSignature.value, - "signatureFormat": formattedSignature.format.channelValue, - "algorithm": algorithm, - "keySize": keySizeInBits(publicKey), - "timestamp": isoTimestamp(), - "keyFormat": format.channelValue + + private func hasRsaKey() -> Bool { + let tag = Constants.biometricKeyAlias + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: tag, + kSecAttrAccount as String: tag, + kSecReturnData as String: false, + kSecMatchLimit as String: kSecMatchLimitOne ] - if let keyLabel = formattedKey.pemLabel { - response["publicKeyPemLabel"] = keyLabel - } - if let signatureLabel = formattedSignature.pemLabel { - response["signaturePemLabel"] = signatureLabel - } - return response + var item: CFTypeRef? + return SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess } - - private func formatPublicKey(_ key: SecKey, format: KeyFormat) -> FormattedOutput? { - guard let data = subjectPublicKeyInfo(for: key) else { return nil } - return formatData(data, format: format, pemLabel: "PUBLIC KEY") + + private func getEcPrivateKey(prompt: String) -> SecKey? { + let tag = Constants.ecKeyAlias + let context = LAContext() + context.localizedReason = prompt + + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecReturnRef as String: true, + kSecUseAuthenticationContext as String: context + ] + + var item: CFTypeRef? + if SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess { + return (item as! SecKey) + } + return nil } - - private func formatSignature(_ data: Data, format: KeyFormat) -> FormattedOutput { - return formatData(data, format: format, pemLabel: "SIGNATURE") + + private func unwrapRsaKey(prompt: String) -> SecKey? { + // 1. Get Wrapped Data + let tag = Constants.biometricKeyAlias + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: tag, + kSecAttrAccount as String: tag, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + var item: CFTypeRef? + guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, + let wrappedData = item as? Data else { return nil } + + // 2. Get EC Key (Auth logic handled by Secure Enclave) + guard let ecKey = getEcPrivateKey(prompt: prompt) else { return nil } + + // 3. Unwrap + var error: Unmanaged? + guard let rsaData = SecKeyCreateDecryptedData(ecKey, .eciesEncryptionStandardX963SHA256AESGCM, wrappedData as CFData, &error) as Data? else { + return nil + } + + // 4. Restore Key + let attrs: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeRSA, + kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, + kSecAttrKeySizeInBits as String: 2048 + ] + return SecKeyCreateWithData(rsaData as CFData, attrs as CFDictionary, nil) } - private func formatData(_ data: Data, format: KeyFormat, pemLabel: String) -> FormattedOutput { + private func formatKey(_ key: SecKey, format: KeyFormat) -> String { + guard let data = subjectPublicKeyInfo(for: key) else { return "" } + switch format { - case .base64: - return FormattedOutput(value: data.base64EncodedString(), format: .base64, pemLabel: nil) - case .hex: - return FormattedOutput(value: hexString(from: data), format: .hex, pemLabel: nil) - case .raw: - return FormattedOutput(value: FlutterStandardTypedData(bytes: data), format: .raw, pemLabel: nil) + case .base64, .raw: + return data.base64EncodedString() case .pem: - let body = chunkedBase64(data.base64EncodedString()) - let pem = "-----BEGIN \(pemLabel)-----\n\(body)\n-----END \(pemLabel)-----" - return FormattedOutput(value: pem, format: .pem, pemLabel: pemLabel) + let base64 = data.base64EncodedString(options: [.lineLength64Characters, .endLineWithLineFeed]) + return "-----BEGIN PUBLIC KEY-----\n\(base64)\n-----END PUBLIC KEY-----" + case .hex: + return data.map { String(format: "%02x", $0) }.joined() } } - + private func subjectPublicKeyInfo(for key: SecKey) -> Data? { var error: Unmanaged? - guard let publicKeyData = SecKeyCopyExternalRepresentation(key, &error) as Data? else { - return nil - } - let attributes = SecKeyCopyAttributes(key) as? [String: Any] - let keyType = attributes?[kSecAttrKeyType as String] as? String - let isEc = keyType == (kSecAttrKeyTypeECSECPrimeRandom as String) || publicKeyData.count == 65 - return BiometricSignaturePlugin.addHeader(publicKeyData: publicKeyData, isEc: isEc) - } - - private func keySizeInBits(_ key: SecKey) -> Int { + guard let rawData = SecKeyCopyExternalRepresentation(key, &error) as Data? else { return nil } + guard let attributes = SecKeyCopyAttributes(key) as? [String: Any], - let bits = attributes[kSecAttrKeySizeInBits as String] as? Int else { - return 0 - } - return bits - } + let keyType = attributes[kSecAttrKeyType as String] as? String else { return rawData } - private func isoTimestamp() -> String { - return iso8601Formatter.string(from: Date()) + if keyType == (kSecAttrKeyTypeRSA as String) { + // AlgorithmIdentifier: rsaEncryption, NULL + let algorithmHeader: [UInt8] = [ + 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00 + ] + + var bitString = Data() + bitString.append(0x00) // unused bits + bitString.append(rawData) + let bitStringEncoded = encodeASN1Content(tag: 0x03, content: bitString) + + var sequenceContent = Data(algorithmHeader) + sequenceContent.append(bitStringEncoded) + + return encodeASN1Content(tag: 0x30, content: sequenceContent) + + } else if keyType == (kSecAttrKeyTypeECSECPrimeRandom as String) { + // AlgorithmIdentifier: id-ecPublicKey, prime256v1 + let algorithmHeader: [UInt8] = [ + 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07 + ] + + var bitString = Data() + bitString.append(0x00) // unused bits + bitString.append(rawData) + let bitStringEncoded = encodeASN1Content(tag: 0x03, content: bitString) + + var sequenceContent = Data(algorithmHeader) + sequenceContent.append(bitStringEncoded) + return encodeASN1Content(tag: 0x30, content: sequenceContent) + } + + return rawData } - - private func chunkedBase64(_ string: String, chunkSize: Int = 64) -> String { - guard !string.isEmpty else { return string } - var chunks: [String] = [] - var index = string.startIndex - while index < string.endIndex { - let end = string.index(index, offsetBy: chunkSize, limitedBy: string.endIndex) ?? string.endIndex - chunks.append(String(string[index.. Data { + var data = Data() + data.append(tag) + let length = content.count + + if length < 128 { + data.append(UInt8(length)) + } else if length < 256 { + data.append(0x81) + data.append(UInt8(length)) + } else if length < 65536 { + data.append(0x82) + data.append(UInt8(length >> 8)) + data.append(UInt8(length & 0xFF)) + } else { + data.append(0x83) + data.append(UInt8(length >> 16)) + data.append(UInt8((length >> 8) & 0xFF)) + data.append(UInt8(length & 0xFF)) } - return chunks.joined(separator: "\n") - } - - private func hexString(from data: Data) -> String { - return data.map { String(format: "%02x", $0) }.joined() + + data.append(content) + return data } - private func parseBool(_ value: Any?) -> Bool? { - if let boolValue = value as? Bool { - return boolValue - } - if let numberValue = value as? NSNumber { - return numberValue.boolValue - } - if let stringValue = value as? String { - return Bool(stringValue) + private func formatSignature(_ data: Data, format: SignatureFormat) -> String { + switch format { + case .base64, .raw: + return data.base64EncodedString() + case .hex: + return data.map { String(format: "%02x", $0) }.joined() } - return nil - } - - private func dispatchMainAsync(_ block: @escaping () -> Void) { - DispatchQueue.main.async(execute: block) - } - - private func getBiometricType(_ context: LAContext?) -> String { - return context?.biometryType == .faceID ? "FaceID" : - context?.biometryType == .touchID ? "TouchID" : "none, NO_BIOMETRICS" } - private func doesBiometricKeyExist(checkValidity: Bool = false) -> Bool { - autoreleasepool { - // Check EC key existence - let ecTag = Constants.ecKeyAlias - let ecKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: ecTag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, - ] - var ecItem: CFTypeRef? - let ecStatus = SecItemCopyMatching(ecKeyQuery as CFDictionary, &ecItem) - let ecKeyExists = (ecStatus == errSecSuccess) - - // Check if encrypted RSA key exists - let encryptedKeyTag = getBiometricKeyTag() - let encryptedKeyQuery: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: encryptedKeyTag, - kSecAttrAccount as String: encryptedKeyTag, - kSecReturnData as String: true, - ] - var rsaItem: CFTypeRef? - let rsaStatus = SecItemCopyMatching(encryptedKeyQuery as CFDictionary, &rsaItem) - let rsaKeyExists = (rsaStatus == errSecSuccess) - - // For EC-only mode, only EC key needs to exist - if ecKeyExists && !rsaKeyExists { - guard checkValidity else { return true } - - // Check if invalidation was enabled for this key - let shouldInvalidateOnEnrollment = InvalidationSetting.load() ?? true - - // Only check domain state if invalidation is enabled - if shouldInvalidateOnEnrollment { - return !DomainState.biometryChangedOrUnknown() - } - - // If invalidation is disabled (biometryAny), key remains valid - return true - } - - // Hybrid: both must exist - guard ecKeyExists, rsaKeyExists else { return false } - guard checkValidity else { return true } - - // Check if invalidation was enabled for this key - let shouldInvalidateOnEnrollment = InvalidationSetting.load() ?? true - - // Only check domain state if invalidation is enabled - if shouldInvalidateOnEnrollment { - return !DomainState.biometryChangedOrUnknown() - } - - // If invalidation is disabled (biometryAny), key remains valid - return true + private func parsePayload(_ payload: String, format: PayloadFormat) -> Data? { + switch format { + case .base64: + return Data(base64Encoded: payload, options: .ignoreUnknownCharacters) + case .hex: + return parseHex(payload) + case .raw: + return Data(base64Encoded: payload, options: .ignoreUnknownCharacters) // Raw assumes base64 input string for transport } } - private func getBiometricKeyTag() -> String { - return Constants.biometricKeyAlias - } - - private static let encodedRSAEncryptionOID: [UInt8] = [ - 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00 - ] - - private static let encodedECEncryptionOID: [UInt8] = [ - 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, - 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07 - ] - - private static func addHeader(publicKeyData: Data?, isEc: Bool = false) -> Data? { - guard let publicKeyData = publicKeyData else { return nil } - return isEc ? addECHeader(publicKeyData: publicKeyData) : addRSAHeader(publicKeyData: publicKeyData) - } - - private static func addRSAHeader(publicKeyData: Data?) -> Data? { - guard let publicKeyData = publicKeyData else { return nil } - var builder = [UInt8](repeating: 0, count: 15) - var encKey = Data() - let bitLen: UInt = (publicKeyData.count + 1 < 128) ? 1 : UInt(((publicKeyData.count + 1) / 256) + 2) - builder[0] = 0x30 - let i = encodedRSAEncryptionOID.count + 2 + Int(bitLen) + publicKeyData.count - var j = encodedLength(&builder[1], i) - encKey.append(&builder, count: Int(j + 1)) - encKey.append(encodedRSAEncryptionOID, count: encodedRSAEncryptionOID.count) - builder[0] = 0x03 - j = encodedLength(&builder[1], publicKeyData.count + 1) - builder[j + 1] = 0x00 - encKey.append(&builder, count: Int(j + 2)) - encKey.append(publicKeyData) - return encKey - } - - private static func addECHeader(publicKeyData: Data?) -> Data? { - guard let publicKeyData = publicKeyData else { return nil } - var builder = [UInt8](repeating: 0, count: 15) - var encKey = Data() - let bitLen: UInt = (publicKeyData.count + 1 < 128) ? 1 : UInt(((publicKeyData.count + 1) / 256) + 2) - builder[0] = 0x30 - let i = encodedECEncryptionOID.count + 2 + Int(bitLen) + publicKeyData.count - var j = encodedLength(&builder[1], i) - encKey.append(&builder, count: Int(j + 1)) - encKey.append(encodedECEncryptionOID, count: encodedECEncryptionOID.count) - builder[0] = 0x03 - j = encodedLength(&builder[1], publicKeyData.count + 1) - builder[j + 1] = 0x00 - encKey.append(&builder, count: Int(j + 2)) - encKey.append(publicKeyData) - return encKey - } - - private static func encodedLength(_ buf: UnsafeMutablePointer?, _ length: size_t) -> size_t { - var length = length - if length < 128 { - buf?[0] = UInt8(length) - return 1 - } - let i: size_t = (length / 256) + 1 - buf?[0] = UInt8(i + 0x80) - for j in 0..>= 8 - } - return i + 1 + private func parseHex(_ hex: String) -> Data? { + var data = Data() + var hexStr = hex + if hexStr.count % 2 != 0 { hexStr = "0" + hexStr } + for i in stride(from: 0, to: hexStr.count, by: 2) { + let start = hexStr.index(hexStr.startIndex, offsetBy: i) + let end = hexStr.index(start, offsetBy: 2) + guard let byte = UInt8(hexStr[start..:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/passwordless_login/windows/flutter/CMakeLists.txt b/passwordless_login/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/passwordless_login/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/passwordless_login/windows/flutter/generated_plugin_registrant.cc b/passwordless_login/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..c777059 --- /dev/null +++ b/passwordless_login/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + BiometricSignaturePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BiometricSignaturePlugin")); +} diff --git a/passwordless_login/windows/flutter/generated_plugin_registrant.h b/passwordless_login/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/passwordless_login/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/passwordless_login/windows/flutter/generated_plugins.cmake b/passwordless_login/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..5c7dd30 --- /dev/null +++ b/passwordless_login/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + biometric_signature +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/passwordless_login/windows/runner/CMakeLists.txt b/passwordless_login/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/passwordless_login/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/passwordless_login/windows/runner/Runner.rc b/passwordless_login/windows/runner/Runner.rc new file mode 100644 index 0000000..fb0c453 --- /dev/null +++ b/passwordless_login/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "passwordless_login_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "passwordless_login_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "passwordless_login_example.exe" "\0" + VALUE "ProductName", "passwordless_login_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/passwordless_login/windows/runner/flutter_window.cpp b/passwordless_login/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/passwordless_login/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/passwordless_login/windows/runner/flutter_window.h b/passwordless_login/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/passwordless_login/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/passwordless_login/windows/runner/main.cpp b/passwordless_login/windows/runner/main.cpp new file mode 100644 index 0000000..39ad221 --- /dev/null +++ b/passwordless_login/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"passwordless_login_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/passwordless_login/windows/runner/resource.h b/passwordless_login/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/passwordless_login/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/passwordless_login/windows/runner/resources/app_icon.ico b/passwordless_login/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/passwordless_login/windows/runner/resources/app_icon.ico differ diff --git a/passwordless_login/windows/runner/runner.exe.manifest b/passwordless_login/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/passwordless_login/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/passwordless_login/windows/runner/utils.cpp b/passwordless_login/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/passwordless_login/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/passwordless_login/windows/runner/utils.h b/passwordless_login/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/passwordless_login/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/passwordless_login/windows/runner/win32_window.cpp b/passwordless_login/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/passwordless_login/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/passwordless_login/windows/runner/win32_window.h b/passwordless_login/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/passwordless_login/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/pigeons/messages.dart b/pigeons/messages.dart new file mode 100644 index 0000000..9ad8ae1 --- /dev/null +++ b/pigeons/messages.dart @@ -0,0 +1,336 @@ +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/biometric_signature_platform_interface.pigeon.dart', + dartOptions: DartOptions(), + kotlinOut: + 'android/src/main/kotlin/com/visionflutter/biometric_signature/BiometricSignatureApi.kt', + kotlinOptions: KotlinOptions( + package: 'com.visionflutter.biometric_signature', + ), + swiftOut: 'ios/Classes/BiometricSignatureApi.swift', + swiftOptions: SwiftOptions(), + cppSourceOut: 'windows/messages.g.cpp', + cppHeaderOut: 'windows/messages.g.h', + cppOptions: CppOptions(namespace: 'biometric_signature'), + ), +) +/// Types of biometric authentication supported by the device. +enum BiometricType { + /// Face recognition (Face ID on iOS, face unlock on Android). + face, + + /// Fingerprint recognition (Touch ID on iOS/macOS, fingerprint on Android). + fingerprint, + + /// Iris scanner (Android only, rare on consumer devices). + iris, + + /// Multiple biometric types are available on the device. + multiple, + + /// No biometric hardware available or biometrics are disabled. + unavailable, +} + +/// Standardized error codes for the plugin. +enum BiometricError { + /// The operation was successful. + success, + + /// The user canceled the operation. + userCanceled, + + /// Biometric authentication is not available on this device. + notAvailable, + + /// No biometrics are enrolled. + notEnrolled, + + /// The user is temporarily locked out due to too many failed attempts. + lockedOut, + + /// The user is permanently locked out until they log in with a strong method. + lockedOutPermanent, + + /// The requested key was not found. + keyNotFound, + + /// The key has been invalidated (e.g. by new biometric enrollment). + keyInvalidated, + + /// An unknown error occurred. + unknown, + + /// The input payload was invalid (e.g. not valid Base64). + invalidInput, +} + +class BiometricAvailability { + bool? canAuthenticate; + bool? hasEnrolledBiometrics; + List? availableBiometrics; + String? reason; +} + +class KeyCreationResult { + String? publicKey; + Uint8List? publicKeyBytes; + String? error; + BiometricError? code; + String? algorithm; + int? keySize; + String? decryptingPublicKey; + String? decryptingAlgorithm; + int? decryptingKeySize; + bool? isHybridMode; +} + +class SignatureResult { + String? signature; + Uint8List? signatureBytes; + String? publicKey; + String? error; + BiometricError? code; + String? algorithm; + int? keySize; +} + +class DecryptResult { + String? decryptedData; + String? error; + BiometricError? code; +} + +/// Detailed information about existing biometric keys. +class KeyInfo { + /// Whether any biometric key exists on the device. + bool? exists; + + /// Whether the key is still valid (not invalidated by biometric changes). + /// Only populated when `checkValidity: true` is passed. + bool? isValid; + + /// The algorithm of the signing key (e.g., "RSA", "EC"). + String? algorithm; + + /// The key size in bits (e.g., 2048 for RSA, 256 for EC). + int? keySize; + + /// Whether the key is in hybrid mode (separate signing and decryption keys). + bool? isHybridMode; + + /// Signing key public key (formatted according to the requested format). + String? publicKey; + + /// Decryption key public key for hybrid mode. + String? decryptingPublicKey; + + /// Algorithm of the decryption key (hybrid mode only). + String? decryptingAlgorithm; + + /// Key size of the decryption key in bits (hybrid mode only). + int? decryptingKeySize; +} + +/// The cryptographic algorithm to use for key generation. +enum SignatureType { + /// RSA-2048 (Android: native, iOS/macOS: hybrid mode with Secure Enclave EC). + rsa, + + /// ECDSA P-256 (hardware-backed on all platforms). + ecdsa, +} + +/// Configuration for key creation (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// Windows ignores most fields as it only supports RSA with mandatory +/// Windows Hello authentication. +class CreateKeysConfig { + // === Cross-platform options (availability varies by platform) === + + /// [Android/iOS/macOS] The cryptographic algorithm to use. + /// Windows only supports RSA and ignores this field. + SignatureType? signatureType; + + /// [Android/iOS/macOS] Whether to require biometric authentication + /// during key creation. Windows always authenticates via Windows Hello. + bool? enforceBiometric; + + /// [Android/iOS/macOS] Whether to invalidate the key when new biometrics + /// are enrolled. Not supported on Windows. + /// + /// **Security Note**: When `true`, keys become invalid if fingerprints/faces + /// are added or removed, preventing unauthorized access if an attacker + /// enrolls their own biometrics on a compromised device. + bool? setInvalidatedByBiometricEnrollment; + + /// [Android/iOS/macOS] Whether to allow device credentials (PIN/pattern/passcode) + /// as fallback for biometric authentication. Not supported on Windows. + bool? useDeviceCredentials; + + /// [Android] Whether to enable decryption capability for the key. + /// On iOS/macOS, decryption is always available with EC keys. + bool? enableDecryption; + + // === Android prompt customization === + + /// [Android] Subtitle text for the biometric prompt. + String? promptSubtitle; + + /// [Android] Description text for the biometric prompt. + String? promptDescription; + + /// [Android] Text for the cancel button in the biometric prompt. + String? cancelButtonText; +} + +/// Configuration for signature creation (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +class CreateSignatureConfig { + // === Android prompt customization === + + /// [Android] Subtitle text for the biometric prompt. + String? promptSubtitle; + + /// [Android] Description text for the biometric prompt. + String? promptDescription; + + /// [Android] Text for the cancel button in the biometric prompt. + String? cancelButtonText; + + /// [Android] Whether to allow device credentials (PIN/pattern) as fallback. + bool? allowDeviceCredentials; + + // === iOS options === + + /// [iOS] Whether to migrate from legacy keychain storage. + bool? shouldMigrate; +} + +/// Configuration for decryption (all platforms). +/// +/// Fields are documented with which platform(s) they apply to. +/// Note: Decryption is not supported on Windows. +class DecryptConfig { + // === Android prompt customization === + + /// [Android] Subtitle text for the biometric prompt. + String? promptSubtitle; + + /// [Android] Description text for the biometric prompt. + String? promptDescription; + + /// [Android] Text for the cancel button in the biometric prompt. + String? cancelButtonText; + + /// [Android] Whether to allow device credentials (PIN/pattern) as fallback. + bool? allowDeviceCredentials; + + // === iOS options === + + /// [iOS] Whether to migrate from legacy keychain storage. + bool? shouldMigrate; +} + +/// Output format for public keys. +enum KeyFormat { + /// Base64-encoded DER (SubjectPublicKeyInfo). + base64, + + /// PEM format with BEGIN/END PUBLIC KEY headers. + pem, + + /// Hexadecimal-encoded DER. + hex, + + /// Raw DER bytes (returned via `publicKeyBytes`). + raw, +} + +/// Output format for cryptographic signatures. +enum SignatureFormat { + /// Base64-encoded signature bytes. + base64, + + /// Hexadecimal-encoded signature bytes. + hex, + + /// Raw signature bytes (returned via `signatureBytes`). + raw, +} + +/// Input format for encrypted payloads to decrypt. +enum PayloadFormat { + /// Base64-encoded ciphertext. + base64, + + /// Hexadecimal-encoded ciphertext. + hex, + + /// Raw UTF-8 string (not recommended for binary data). + raw, +} + +@HostApi() +abstract class BiometricSignatureApi { + /// Checks if biometric authentication is available. + @async + BiometricAvailability biometricAuthAvailable(); + + /// Creates a new key pair. + /// + /// [config] contains platform-specific options. See [CreateKeysConfig]. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + @async + KeyCreationResult createKeys( + CreateKeysConfig? config, + KeyFormat keyFormat, + String? promptMessage, + ); + + /// Creates a signature. + /// + /// [payload] is the data to sign. + /// [config] contains platform-specific options. See [CreateSignatureConfig]. + /// [signatureFormat] specifies the output format for the signature. + /// [keyFormat] specifies the output format for the public key. + /// [promptMessage] is the message shown to the user during authentication. + @async + SignatureResult createSignature( + String payload, + CreateSignatureConfig? config, + SignatureFormat signatureFormat, + KeyFormat keyFormat, + String? promptMessage, + ); + + /// Decrypts data. + /// + /// Note: Not supported on Windows. + /// [payload] is the encrypted data. + /// [payloadFormat] specifies the format of the encrypted data. + /// [config] contains platform-specific options. See [DecryptConfig]. + /// [promptMessage] is the message shown to the user during authentication. + @async + DecryptResult decrypt( + String payload, + PayloadFormat payloadFormat, + DecryptConfig? config, + String? promptMessage, + ); + + /// Deletes keys. + @async + bool deleteKeys(); + + /// Gets detailed information about existing biometric keys. + /// + /// Returns key metadata including algorithm, size, validity, and public keys. + @async + KeyInfo getKeyInfo(bool checkValidity, KeyFormat keyFormat); +} diff --git a/pubspec.yaml b/pubspec.yaml index 477362c..9b15d5d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ topics: [biometrics, secure-enclave, strongbox, rsa, ecdsa] screenshots: - description: "Domain Logo" path: assets/logo.png -version: 8.5.0 +version: 9.0.0 homepage: https://github.com/chamodanethra/biometric_signature environment: @@ -20,6 +20,7 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 + pigeon: ^26.1.4 flutter: plugin: @@ -31,3 +32,5 @@ flutter: pluginClass: BiometricSignaturePlugin macos: pluginClass: BiometricSignaturePlugin + windows: + pluginClass: BiometricSignaturePlugin diff --git a/scripts/generate_pigeon.sh b/scripts/generate_pigeon.sh new file mode 100755 index 0000000..1d64c8b --- /dev/null +++ b/scripts/generate_pigeon.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Script to generate Pigeon code and copy Swift file to macOS +# Usage: ./scripts/generate_pigeon.sh + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +echo "🐦 Running Pigeon code generator..." +fvm use 3.35.7 && fvm flutter pub run pigeon --input pigeons/messages.dart + +echo "📋 Copying generated Swift file to macOS..." +cp "$PROJECT_ROOT/ios/Classes/BiometricSignatureApi.swift" "$PROJECT_ROOT/macos/Classes/BiometricSignatureApi.swift" + +echo "✅ Done! Generated code for:" +echo " - Dart: lib/biometric_signature_platform_interface.pigeon.dart" +echo " - Android: android/src/main/kotlin/.../BiometricSignatureApi.kt" +echo " - iOS: ios/Classes/BiometricSignatureApi.swift" +echo " - macOS: macos/Classes/BiometricSignatureApi.swift (copied from iOS)" diff --git a/test/biometric_signature_test.dart b/test/biometric_signature_test.dart index 79f65b7..b35f51c 100644 --- a/test/biometric_signature_test.dart +++ b/test/biometric_signature_test.dart @@ -6,10 +6,16 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; class MockBiometricSignaturePlatform with MockPlatformInterfaceMixin implements BiometricSignaturePlatform { - String? _authAvailableResult = 'fingerprint'; + BiometricAvailability _authAvailableResult = BiometricAvailability( + canAuthenticate: true, + hasEnrolledBiometrics: true, + availableBiometrics: [BiometricType.fingerprint], + reason: null, + ); bool _shouldThrowError = false; + SignatureType _signatureType = SignatureType.rsa; - void setAuthAvailableResult(String? result) { + void setAuthAvailableResult(BiometricAvailability result) { _authAvailableResult = result; } @@ -17,56 +23,80 @@ class MockBiometricSignaturePlatform _shouldThrowError = value; } + void setSignatureType(SignatureType type) { + _signatureType = type; + } + @override - Future biometricAuthAvailable() async { + Future biometricAuthAvailable() async { if (_shouldThrowError) throw Exception('Auth check failed'); - return Future.value(_authAvailableResult); + return _authAvailableResult; } @override - Future biometricKeyExists(bool checkValidity) => Future.value(true); + Future getKeyInfo(bool checkValidity, KeyFormat keyFormat) async { + return KeyInfo( + exists: true, + isValid: true, + algorithm: 'RSA', + keySize: 2048, + isHybridMode: false, + publicKey: 'test_public_key', + ); + } @override - Future?> createKeys( - AndroidConfig androidConfig, - IosConfig iosConfig, - MacosConfig macosConfig, { - required KeyFormat keyFormat, - bool enforceBiometric = false, + Future createKeys( + CreateKeysConfig? config, + KeyFormat keyFormat, String? promptMessage, - }) { + ) async { if (_shouldThrowError) throw Exception('Key creation failed'); - final isEc = androidConfig.signatureType == AndroidSignatureType.ECDSA; - return Future.value({ - 'publicKey': 'test_public_key', - 'publicKeyFormat': keyFormat.wireValue, - 'algorithm': isEc ? 'EC' : 'RSA', - 'keySize': isEc ? 256 : 2048, - }); + final isEc = + (config?.signatureType ?? _signatureType) == SignatureType.ecdsa; + return KeyCreationResult( + publicKey: 'test_public_key', + code: BiometricError.success, + algorithm: isEc ? 'EC' : 'RSA', + keySize: isEc ? 256 : 2048, + ); } @override - Future?> createSignature(SignatureOptions options) { + Future createSignature( + String payload, + CreateSignatureConfig? config, + SignatureFormat signatureFormat, + KeyFormat keyFormat, + String? promptMessage, + ) async { if (_shouldThrowError) throw Exception('Signing failed'); - return Future.value({ - 'signature': 'test_signature', - 'signatureFormat': options.keyFormat.wireValue, - 'publicKey': 'test_public_key', - 'publicKeyFormat': options.keyFormat.wireValue, - 'algorithm': 'RSA', // Simplified for mock - 'keySize': 2048, - }); + return SignatureResult( + signature: 'test_signature', + publicKey: 'test_public_key', + code: BiometricError.success, + algorithm: 'RSA', + keySize: 2048, + ); } @override - Future deleteKeys() => Future.value(true); + Future deleteKeys() => Future.value(true); @override - Future?> decrypt(DecryptionOptions options) { + Future decrypt( + String payload, + PayloadFormat payloadFormat, + DecryptConfig? config, + String? promptMessage, + ) async { if (_shouldThrowError) throw Exception('Decryption failed'); - return Future.value({'decryptedData': 'decrypted_${options.payload}'}); + return DecryptResult( + decryptedData: 'decrypted_$payload', + code: BiometricError.success, + ); } } @@ -74,17 +104,41 @@ void main() { final BiometricSignaturePlatform initialPlatform = BiometricSignaturePlatform.instance; - test('$BiometricSignaturePlatform is the default instance', () { + test('\$BiometricSignaturePlatform is the default instance', () { expect(initialPlatform, isInstanceOf()); }); - test('biometricAuthAvailable', () async { - BiometricSignature biometricSignature = BiometricSignature(); - MockBiometricSignaturePlatform fakePlatform = - MockBiometricSignaturePlatform(); - BiometricSignaturePlatform.instance = fakePlatform; + group('biometricAuthAvailable', () { + test('returns availability info', () async { + BiometricSignature biometricSignature = BiometricSignature(); + MockBiometricSignaturePlatform fakePlatform = + MockBiometricSignaturePlatform(); + BiometricSignaturePlatform.instance = fakePlatform; + + final result = await biometricSignature.biometricAuthAvailable(); + expect(result.canAuthenticate, true); + expect(result.hasEnrolledBiometrics, true); + expect(result.availableBiometrics, contains(BiometricType.fingerprint)); + }); - expect(await biometricSignature.biometricAuthAvailable(), 'fingerprint'); + test('handles unavailable biometrics', () async { + BiometricSignature biometricSignature = BiometricSignature(); + MockBiometricSignaturePlatform fakePlatform = + MockBiometricSignaturePlatform(); + fakePlatform.setAuthAvailableResult( + BiometricAvailability( + canAuthenticate: false, + hasEnrolledBiometrics: false, + availableBiometrics: [BiometricType.unavailable], + reason: 'No biometric hardware', + ), + ); + BiometricSignaturePlatform.instance = fakePlatform; + + final result = await biometricSignature.biometricAuthAvailable(); + expect(result.canAuthenticate, false); + expect(result.reason, 'No biometric hardware'); + }); }); group('createKeys', () { @@ -95,9 +149,10 @@ void main() { BiometricSignaturePlatform.instance = fakePlatform; final result = await biometricSignature.createKeys(); - expect(result?.publicKey.asString(), 'test_public_key'); - expect(result?.algorithm, 'RSA'); - expect(result?.keySize, 2048); + expect(result.publicKey, 'test_public_key'); + expect(result.algorithm, 'RSA'); + expect(result.keySize, 2048); + expect(result.code, BiometricError.success); }); test('EC keys', () async { @@ -107,13 +162,27 @@ void main() { BiometricSignaturePlatform.instance = fakePlatform; final result = await biometricSignature.createKeys( - androidConfig: AndroidConfig( - useDeviceCredentials: false, - signatureType: AndroidSignatureType.ECDSA, + config: CreateKeysConfig(signatureType: SignatureType.ecdsa), + ); + expect(result.algorithm, 'EC'); + expect(result.keySize, 256); + }); + + test('with config options', () async { + BiometricSignature biometricSignature = BiometricSignature(); + MockBiometricSignaturePlatform fakePlatform = + MockBiometricSignaturePlatform(); + BiometricSignaturePlatform.instance = fakePlatform; + + final result = await biometricSignature.createKeys( + config: CreateKeysConfig( + enableDecryption: true, + promptSubtitle: 'Test subtitle', + enforceBiometric: true, + setInvalidatedByBiometricEnrollment: true, ), ); - expect(result?.algorithm, 'EC'); - expect(result?.keySize, 256); + expect(result.code, BiometricError.success); }); test('Error handling', () async { @@ -135,13 +204,25 @@ void main() { BiometricSignaturePlatform.instance = fakePlatform; final result = await biometricSignature.createSignature( - SignatureOptions( - payload: 'test', - androidOptions: AndroidSignatureOptions(), - ), + payload: 'test_data', ); - expect(result?.signature.asString(), 'test_signature'); - expect(result?.publicKey.asString(), 'test_public_key'); + expect(result.signature, 'test_signature'); + expect(result.publicKey, 'test_public_key'); + expect(result.code, BiometricError.success); + }); + + test('with custom prompt message', () async { + BiometricSignature biometricSignature = BiometricSignature(); + MockBiometricSignaturePlatform fakePlatform = + MockBiometricSignaturePlatform(); + BiometricSignaturePlatform.instance = fakePlatform; + + final result = await biometricSignature.createSignature( + payload: 'test_data', + promptMessage: 'Please authenticate', + config: CreateSignatureConfig(allowDeviceCredentials: false), + ); + expect(result.code, BiometricError.success); }); test('Error handling', () async { @@ -152,9 +233,7 @@ void main() { BiometricSignaturePlatform.instance = fakePlatform; expect( - () => biometricSignature.createSignature( - SignatureOptions(payload: 'test'), - ), + () => biometricSignature.createSignature(payload: 'test'), throwsException, ); }); @@ -186,9 +265,25 @@ void main() { BiometricSignaturePlatform.instance = fakePlatform; final result = await biometricSignature.decrypt( - DecryptionOptions(payload: 'encrypted_payload'), + payload: 'encrypted_payload', + payloadFormat: PayloadFormat.base64, + ); + expect(result.decryptedData, 'decrypted_encrypted_payload'); + expect(result.code, BiometricError.success); + }); + + test('with config options', () async { + BiometricSignature biometricSignature = BiometricSignature(); + MockBiometricSignaturePlatform fakePlatform = + MockBiometricSignaturePlatform(); + BiometricSignaturePlatform.instance = fakePlatform; + + final result = await biometricSignature.decrypt( + payload: 'encrypted_payload', + payloadFormat: PayloadFormat.base64, + config: DecryptConfig(allowDeviceCredentials: false), ); - expect(result?.decryptedData, 'decrypted_encrypted_payload'); + expect(result.code, BiometricError.success); }); test('Error handling', () async { @@ -200,7 +295,8 @@ void main() { expect( () => biometricSignature.decrypt( - DecryptionOptions(payload: 'encrypted_payload'), + payload: 'encrypted_payload', + payloadFormat: PayloadFormat.base64, ), throwsException, ); diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..c25342d --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,69 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. +set(PROJECT_NAME "biometric_signature") +project(${PROJECT_NAME} LANGUAGES CXX) + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# This value is used when generating builds using this plugin, so it must +# not be changed +set(PLUGIN_NAME "biometric_signature_plugin") + +# Any new source files that you add to the plugin should be added here. +list(APPEND PLUGIN_SOURCES + "biometric_signature_plugin.cpp" + "biometric_signature_plugin.h" + "messages.g.cpp" + "messages.g.h" +) + +# Define the plugin library target. Its name must not be changed (see comment +# on PLUGIN_NAME above). +add_library(${PLUGIN_NAME} SHARED + "include/biometric_signature/biometric_signature_plugin_c_api.h" + "include/biometric_signature/biometric_signature_plugin.h" + "biometric_signature_plugin_c_api.cpp" + ${PLUGIN_SOURCES} +) + +# Don't use apply_standard_settings because it disables exceptions which WinRT requires. +# Instead, apply settings manually with exceptions enabled. +target_compile_features(${PLUGIN_NAME} PUBLIC cxx_std_17) +target_compile_options(${PLUGIN_NAME} PRIVATE /W4 /WX /wd"4100") +target_compile_options(${PLUGIN_NAME} PRIVATE /EHsc) +target_compile_options(${PLUGIN_NAME} PRIVATE /await) +target_compile_definitions(${PLUGIN_NAME} PRIVATE "$<$:_DEBUG>") + +# Symbols are hidden by default to reduce the chance of accidental conflicts +# between plugins. This should not be removed; any symbols that should be +# exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) + +# Source include directories and library dependencies. Add any plugin-specific +# dependencies here. +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_include_directories(${PLUGIN_NAME} PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}") + +# Link against Flutter and Windows libraries +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) +target_link_libraries(${PLUGIN_NAME} PRIVATE windowsapp) + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(biometric_signature_bundled_libraries + "" + PARENT_SCOPE +) + diff --git a/windows/biometric_signature_plugin.cpp b/windows/biometric_signature_plugin.cpp new file mode 100644 index 0000000..1c95363 --- /dev/null +++ b/windows/biometric_signature_plugin.cpp @@ -0,0 +1,415 @@ +#include "biometric_signature_plugin.h" + +#include +#include +#include + +// C++/WinRT Windows Hello APIs +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +// Base64 encoding utility +#include +#pragma comment(lib, "crypt32.lib") + +namespace biometric_signature { + +namespace { + +// Key identifier for Windows Hello credential +const std::wstring kKeyName = L"BiometricSignatureKey"; + +// Base64 encode bytes +std::string Base64Encode(const std::vector& data) { + if (data.empty()) return ""; + DWORD size = 0; + CryptBinaryToStringA(data.data(), static_cast(data.size()), + CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &size); + std::string result(size, 0); + CryptBinaryToStringA(data.data(), static_cast(data.size()), + CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, &result[0], &size); + if (!result.empty() && result.back() == '\0') { + result.pop_back(); + } + return result; +} + +// Hex encode bytes +std::string HexEncode(const std::vector& data) { + if (data.empty()) return ""; + std::ostringstream oss; + for (uint8_t byte : data) { + oss << std::hex << std::setfill('0') << std::setw(2) << static_cast(byte); + } + return oss.str(); +} + +// Format public key according to the requested format +std::string FormatPublicKey(const std::vector& key_bytes, KeyFormat key_format) { + std::string base64_key = Base64Encode(key_bytes); + + switch (key_format) { + case KeyFormat::kBase64: + case KeyFormat::kRaw: + return base64_key; + case KeyFormat::kPem: { + std::string pem = "-----BEGIN PUBLIC KEY-----\n"; + for (size_t i = 0; i < base64_key.length(); i += 64) { + pem += base64_key.substr(i, 64) + "\n"; + } + pem += "-----END PUBLIC KEY-----"; + return pem; + } + case KeyFormat::kHex: + return HexEncode(key_bytes); + default: + return base64_key; + } +} + +// Format signature according to the requested format +std::string FormatSignature(const std::vector& sig_bytes, SignatureFormat sig_format) { + switch (sig_format) { + case SignatureFormat::kBase64: + case SignatureFormat::kRaw: + return Base64Encode(sig_bytes); + case SignatureFormat::kHex: + return HexEncode(sig_bytes); + default: + return Base64Encode(sig_bytes); + } +} + +// Convert IBuffer to vector +std::vector IBufferToVector( + const winrt::Windows::Storage::Streams::IBuffer& buffer) { + auto reader = winrt::Windows::Storage::Streams::DataReader::FromBuffer(buffer); + std::vector data(buffer.Length()); + reader.ReadBytes(data); + return data; +} + +// Convert vector to IBuffer +winrt::Windows::Storage::Streams::IBuffer VectorToIBuffer( + const std::vector& data) { + auto writer = winrt::Windows::Storage::Streams::DataWriter(); + writer.WriteBytes(data); + return writer.DetachBuffer(); +} + +} // namespace + +// Static window handle to bring window to foreground before Windows Hello dialogs +static HWND g_window_handle = nullptr; + +// Helper function to bring the Flutter window to the foreground +static void BringWindowToForeground() { + if (g_window_handle != nullptr) { + SetForegroundWindow(g_window_handle); + SetFocus(g_window_handle); + } +} + +// static +void BiometricSignaturePlugin::RegisterWithRegistrar( + flutter::PluginRegistrarWindows *registrar) { + + // Get the Flutter window handle for bringing it to foreground before Windows Hello dialogs + flutter::FlutterView* view = registrar->GetView(); + if (view != nullptr) { + g_window_handle = view->GetNativeWindow(); + } + + auto plugin = std::make_unique(); + + // Set up the Pigeon API + BiometricSignatureApi::SetUp(registrar->messenger(), plugin.get()); + + registrar->AddPlugin(std::move(plugin)); +} + +BiometricSignaturePlugin::BiometricSignaturePlugin() {} + +BiometricSignaturePlugin::~BiometricSignaturePlugin() {} + +void BiometricSignaturePlugin::BiometricAuthAvailable( + std::function reply)> result) { + + auto async_op = winrt::Windows::Security::Credentials::KeyCredentialManager:: + IsSupportedAsync(); + + async_op.Completed([result](auto const& op, auto status) { + BiometricAvailability response; + + if (status == winrt::Windows::Foundation::AsyncStatus::Completed) { + bool is_supported = op.GetResults(); + + response.set_can_authenticate(is_supported); + response.set_has_enrolled_biometrics(is_supported); + + flutter::EncodableList biometrics; + if (is_supported) { + biometrics.push_back(flutter::CustomEncodableValue(BiometricType::kFingerprint)); + } + response.set_available_biometrics(biometrics); + + if (!is_supported) { + response.set_reason("Windows Hello is not configured on this device"); + } + } else { + response.set_can_authenticate(false); + response.set_has_enrolled_biometrics(false); + response.set_available_biometrics(flutter::EncodableList()); + response.set_reason("Failed to check Windows Hello availability"); + } + + result(response); + }); +} + +void BiometricSignaturePlugin::CreateKeys( + const CreateKeysConfig* config, + const KeyFormat& key_format, + const std::string* prompt_message, + std::function reply)> result) { + + // Bring Flutter window to foreground so Windows Hello dialog appears properly + BringWindowToForeground(); + + auto async_op = winrt::Windows::Security::Credentials::KeyCredentialManager:: + RequestCreateAsync(kKeyName, + winrt::Windows::Security::Credentials::KeyCredentialCreationOption:: + ReplaceExisting); + + async_op.Completed([result, key_format](auto const& op, auto status) { + KeyCreationResult response; + + if (status == winrt::Windows::Foundation::AsyncStatus::Completed) { + auto create_result = op.GetResults(); + + if (create_result.Status() == + winrt::Windows::Security::Credentials::KeyCredentialStatus::Success) { + + auto credential = create_result.Credential(); + auto public_key_buffer = credential.RetrievePublicKey(); + auto public_key_bytes = IBufferToVector(public_key_buffer); + auto public_key_formatted = FormatPublicKey(public_key_bytes, key_format); + + response.set_public_key(public_key_formatted); + response.set_public_key_bytes(public_key_bytes); + response.set_algorithm("RSA"); + response.set_key_size(static_cast(2048)); + response.set_code(BiometricError::kSuccess); + response.set_is_hybrid_mode(false); + } else { + std::string error_msg = "Failed to create key"; + BiometricError error_code = BiometricError::kUnknown; + + switch (create_result.Status()) { + case winrt::Windows::Security::Credentials::KeyCredentialStatus::UserCanceled: + error_msg = "User canceled the operation"; + error_code = BiometricError::kUserCanceled; + break; + case winrt::Windows::Security::Credentials::KeyCredentialStatus::NotFound: + error_msg = "Windows Hello not found"; + error_code = BiometricError::kNotAvailable; + break; + case winrt::Windows::Security::Credentials::KeyCredentialStatus::SecurityDeviceLocked: + error_msg = "Security device is locked"; + error_code = BiometricError::kLockedOut; + break; + default: + break; + } + + response.set_error(error_msg); + response.set_code(error_code); + } + } else { + response.set_error("Operation failed or was canceled"); + response.set_code(BiometricError::kUnknown); + } + + result(response); + }); +} + +void BiometricSignaturePlugin::CreateSignature( + const std::string& payload, + const CreateSignatureConfig* config, + const SignatureFormat& signature_format, + const KeyFormat& key_format, + const std::string* prompt_message, + std::function reply)> result) { + + if (payload.empty()) { + SignatureResult response; + response.set_error("Payload is required"); + response.set_code(BiometricError::kInvalidInput); + result(response); + return; + } + + // Bring Flutter window to foreground so Windows Hello dialog appears properly + BringWindowToForeground(); + + std::string payload_copy = payload; + + auto async_op = winrt::Windows::Security::Credentials::KeyCredentialManager:: + OpenAsync(kKeyName); + + async_op.Completed([result, payload_copy, key_format, signature_format](auto const& op, auto status) { + SignatureResult response; + + if (status == winrt::Windows::Foundation::AsyncStatus::Completed) { + auto open_result = op.GetResults(); + + if (open_result.Status() == + winrt::Windows::Security::Credentials::KeyCredentialStatus::Success) { + + auto credential = open_result.Credential(); + std::vector payload_bytes(payload_copy.begin(), payload_copy.end()); + auto data_buffer = VectorToIBuffer(payload_bytes); + + auto sign_op = credential.RequestSignAsync(data_buffer); + sign_op.Completed([result, credential, key_format, signature_format](auto const& sign_async, auto sign_status) { + SignatureResult resp; + + if (sign_status == winrt::Windows::Foundation::AsyncStatus::Completed) { + auto sign_result = sign_async.GetResults(); + + if (sign_result.Status() == + winrt::Windows::Security::Credentials::KeyCredentialStatus::Success) { + + auto signature_buffer = sign_result.Result(); + auto signature_bytes = IBufferToVector(signature_buffer); + auto signature_formatted = FormatSignature(signature_bytes, signature_format); + + auto public_key_buffer = credential.RetrievePublicKey(); + auto public_key_bytes = IBufferToVector(public_key_buffer); + auto public_key_formatted = FormatPublicKey(public_key_bytes, key_format); + + resp.set_signature(signature_formatted); + resp.set_signature_bytes(signature_bytes); + resp.set_public_key(public_key_formatted); + resp.set_algorithm("RSA"); + resp.set_key_size(static_cast(2048)); + resp.set_code(BiometricError::kSuccess); + } else { + std::string error_msg = "Signing failed"; + BiometricError error_code = BiometricError::kUnknown; + + switch (sign_result.Status()) { + case winrt::Windows::Security::Credentials::KeyCredentialStatus::UserCanceled: + error_msg = "User canceled the operation"; + error_code = BiometricError::kUserCanceled; + break; + case winrt::Windows::Security::Credentials::KeyCredentialStatus::SecurityDeviceLocked: + error_msg = "Security device is locked"; + error_code = BiometricError::kLockedOut; + break; + default: + break; + } + + resp.set_error(error_msg); + resp.set_code(error_code); + } + } else { + resp.set_error("Signing operation failed"); + resp.set_code(BiometricError::kUnknown); + } + + result(resp); + }); + return; // Don't call result here, the nested callback will + } else { + response.set_error("Key not found. Please create keys first."); + response.set_code(BiometricError::kKeyNotFound); + } + } else { + response.set_error("Failed to open key"); + response.set_code(BiometricError::kUnknown); + } + + result(response); + }); +} + +void BiometricSignaturePlugin::DeleteKeys( + std::function reply)> result) { + + auto async_op = winrt::Windows::Security::Credentials::KeyCredentialManager:: + DeleteAsync(kKeyName); + + async_op.Completed([result](auto const& op, auto status) { + result(true); // Return true even if key didn't exist + }); +} + +void BiometricSignaturePlugin::GetKeyInfo( + bool check_validity, + const KeyFormat& key_format, + std::function reply)> result) { + + auto async_op = winrt::Windows::Security::Credentials::KeyCredentialManager:: + OpenAsync(kKeyName); + + async_op.Completed([result, key_format, check_validity](auto const& op, auto status) { + KeyInfo response; + + if (status == winrt::Windows::Foundation::AsyncStatus::Completed) { + auto open_result = op.GetResults(); + + if (open_result.Status() == + winrt::Windows::Security::Credentials::KeyCredentialStatus::Success) { + + auto credential = open_result.Credential(); + auto public_key_buffer = credential.RetrievePublicKey(); + auto public_key_bytes = IBufferToVector(public_key_buffer); + auto public_key_formatted = FormatPublicKey(public_key_bytes, key_format); + + response.set_exists(true); + if (check_validity) { + response.set_is_valid(true); + } + response.set_algorithm("RSA"); + response.set_key_size(static_cast(2048)); + response.set_is_hybrid_mode(false); + response.set_public_key(public_key_formatted); + } else { + response.set_exists(false); + } + } else { + response.set_exists(false); + } + + result(response); + }); +} + +void BiometricSignaturePlugin::Decrypt( + const std::string& payload, + const PayloadFormat& payload_format, + const DecryptConfig* config, + const std::string* prompt_message, + std::function reply)> result) { + + DecryptResult response; + response.set_error("Decryption is not supported on Windows. " + "Windows Hello is designed for authentication and signing only."); + response.set_code(BiometricError::kNotAvailable); + result(response); +} + +} // namespace biometric_signature diff --git a/windows/biometric_signature_plugin.h b/windows/biometric_signature_plugin.h new file mode 100644 index 0000000..ae9a23b --- /dev/null +++ b/windows/biometric_signature_plugin.h @@ -0,0 +1,61 @@ +#ifndef FLUTTER_PLUGIN_BIOMETRIC_SIGNATURE_PLUGIN_H_ +#define FLUTTER_PLUGIN_BIOMETRIC_SIGNATURE_PLUGIN_H_ + +#include +#include "messages.g.h" + +#include +#include + +namespace biometric_signature { + +class BiometricSignaturePlugin : public flutter::Plugin, + public BiometricSignatureApi { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + + BiometricSignaturePlugin(); + + virtual ~BiometricSignaturePlugin(); + + // Disallow copy and assign. + BiometricSignaturePlugin(const BiometricSignaturePlugin&) = delete; + BiometricSignaturePlugin& operator=(const BiometricSignaturePlugin&) = delete; + + // BiometricSignatureApi implementation + void BiometricAuthAvailable( + std::function reply)> result) override; + + void CreateKeys( + const CreateKeysConfig* config, + const KeyFormat& key_format, + const std::string* prompt_message, + std::function reply)> result) override; + + void CreateSignature( + const std::string& payload, + const CreateSignatureConfig* config, + const SignatureFormat& signature_format, + const KeyFormat& key_format, + const std::string* prompt_message, + std::function reply)> result) override; + + void Decrypt( + const std::string& payload, + const PayloadFormat& payload_format, + const DecryptConfig* config, + const std::string* prompt_message, + std::function reply)> result) override; + + void DeleteKeys( + std::function reply)> result) override; + + void GetKeyInfo( + bool check_validity, + const KeyFormat& key_format, + std::function reply)> result) override; +}; + +} // namespace biometric_signature + +#endif // FLUTTER_PLUGIN_BIOMETRIC_SIGNATURE_PLUGIN_H_ diff --git a/windows/biometric_signature_plugin_c_api.cpp b/windows/biometric_signature_plugin_c_api.cpp new file mode 100644 index 0000000..8da57ed --- /dev/null +++ b/windows/biometric_signature_plugin_c_api.cpp @@ -0,0 +1,20 @@ +#include "include/biometric_signature/biometric_signature_plugin_c_api.h" +#include "include/biometric_signature/biometric_signature_plugin.h" + +#include + +#include "biometric_signature_plugin.h" + +void BiometricSignaturePluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + biometric_signature::BiometricSignaturePlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} + +void BiometricSignaturePluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + biometric_signature::BiometricSignaturePlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/windows/include/biometric_signature/biometric_signature_plugin.h b/windows/include/biometric_signature/biometric_signature_plugin.h new file mode 100644 index 0000000..e0c86aa --- /dev/null +++ b/windows/include/biometric_signature/biometric_signature_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_BIOMETRIC_SIGNATURE_PLUGIN_H_PUBLIC_ +#define FLUTTER_PLUGIN_BIOMETRIC_SIGNATURE_PLUGIN_H_PUBLIC_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void BiometricSignaturePluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_BIOMETRIC_SIGNATURE_PLUGIN_H_PUBLIC_ diff --git a/windows/include/biometric_signature/biometric_signature_plugin_c_api.h b/windows/include/biometric_signature/biometric_signature_plugin_c_api.h new file mode 100644 index 0000000..673f4dd --- /dev/null +++ b/windows/include/biometric_signature/biometric_signature_plugin_c_api.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_BIOMETRIC_SIGNATURE_PLUGIN_C_API_H_ +#define FLUTTER_PLUGIN_BIOMETRIC_SIGNATURE_PLUGIN_C_API_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void BiometricSignaturePluginCApiRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_BIOMETRIC_SIGNATURE_PLUGIN_C_API_H_ diff --git a/windows/messages.g.cpp b/windows/messages.g.cpp new file mode 100644 index 0000000..c8629c2 --- /dev/null +++ b/windows/messages.g.cpp @@ -0,0 +1,1562 @@ +// Autogenerated from Pigeon (v26.1.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.g.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace biometric_signature { +using flutter::BasicMessageChannel; +using flutter::CustomEncodableValue; +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +FlutterError CreateConnectionError(const std::string channel_name) { + return FlutterError( + "channel-error", + "Unable to establish connection on channel: '" + channel_name + "'.", + EncodableValue("")); +} + +// BiometricAvailability + +BiometricAvailability::BiometricAvailability() {} + +BiometricAvailability::BiometricAvailability( + const bool* can_authenticate, + const bool* has_enrolled_biometrics, + const EncodableList* available_biometrics, + const std::string* reason) + : can_authenticate_(can_authenticate ? std::optional(*can_authenticate) : std::nullopt), + has_enrolled_biometrics_(has_enrolled_biometrics ? std::optional(*has_enrolled_biometrics) : std::nullopt), + available_biometrics_(available_biometrics ? std::optional(*available_biometrics) : std::nullopt), + reason_(reason ? std::optional(*reason) : std::nullopt) {} + +const bool* BiometricAvailability::can_authenticate() const { + return can_authenticate_ ? &(*can_authenticate_) : nullptr; +} + +void BiometricAvailability::set_can_authenticate(const bool* value_arg) { + can_authenticate_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void BiometricAvailability::set_can_authenticate(bool value_arg) { + can_authenticate_ = value_arg; +} + + +const bool* BiometricAvailability::has_enrolled_biometrics() const { + return has_enrolled_biometrics_ ? &(*has_enrolled_biometrics_) : nullptr; +} + +void BiometricAvailability::set_has_enrolled_biometrics(const bool* value_arg) { + has_enrolled_biometrics_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void BiometricAvailability::set_has_enrolled_biometrics(bool value_arg) { + has_enrolled_biometrics_ = value_arg; +} + + +const EncodableList* BiometricAvailability::available_biometrics() const { + return available_biometrics_ ? &(*available_biometrics_) : nullptr; +} + +void BiometricAvailability::set_available_biometrics(const EncodableList* value_arg) { + available_biometrics_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void BiometricAvailability::set_available_biometrics(const EncodableList& value_arg) { + available_biometrics_ = value_arg; +} + + +const std::string* BiometricAvailability::reason() const { + return reason_ ? &(*reason_) : nullptr; +} + +void BiometricAvailability::set_reason(const std::string_view* value_arg) { + reason_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void BiometricAvailability::set_reason(std::string_view value_arg) { + reason_ = value_arg; +} + + +EncodableList BiometricAvailability::ToEncodableList() const { + EncodableList list; + list.reserve(4); + list.push_back(can_authenticate_ ? EncodableValue(*can_authenticate_) : EncodableValue()); + list.push_back(has_enrolled_biometrics_ ? EncodableValue(*has_enrolled_biometrics_) : EncodableValue()); + list.push_back(available_biometrics_ ? EncodableValue(*available_biometrics_) : EncodableValue()); + list.push_back(reason_ ? EncodableValue(*reason_) : EncodableValue()); + return list; +} + +BiometricAvailability BiometricAvailability::FromEncodableList(const EncodableList& list) { + BiometricAvailability decoded; + auto& encodable_can_authenticate = list[0]; + if (!encodable_can_authenticate.IsNull()) { + decoded.set_can_authenticate(std::get(encodable_can_authenticate)); + } + auto& encodable_has_enrolled_biometrics = list[1]; + if (!encodable_has_enrolled_biometrics.IsNull()) { + decoded.set_has_enrolled_biometrics(std::get(encodable_has_enrolled_biometrics)); + } + auto& encodable_available_biometrics = list[2]; + if (!encodable_available_biometrics.IsNull()) { + decoded.set_available_biometrics(std::get(encodable_available_biometrics)); + } + auto& encodable_reason = list[3]; + if (!encodable_reason.IsNull()) { + decoded.set_reason(std::get(encodable_reason)); + } + return decoded; +} + +// KeyCreationResult + +KeyCreationResult::KeyCreationResult() {} + +KeyCreationResult::KeyCreationResult( + const std::string* public_key, + const std::vector* public_key_bytes, + const std::string* error, + const BiometricError* code, + const std::string* algorithm, + const int64_t* key_size, + const std::string* decrypting_public_key, + const std::string* decrypting_algorithm, + const int64_t* decrypting_key_size, + const bool* is_hybrid_mode) + : public_key_(public_key ? std::optional(*public_key) : std::nullopt), + public_key_bytes_(public_key_bytes ? std::optional>(*public_key_bytes) : std::nullopt), + error_(error ? std::optional(*error) : std::nullopt), + code_(code ? std::optional(*code) : std::nullopt), + algorithm_(algorithm ? std::optional(*algorithm) : std::nullopt), + key_size_(key_size ? std::optional(*key_size) : std::nullopt), + decrypting_public_key_(decrypting_public_key ? std::optional(*decrypting_public_key) : std::nullopt), + decrypting_algorithm_(decrypting_algorithm ? std::optional(*decrypting_algorithm) : std::nullopt), + decrypting_key_size_(decrypting_key_size ? std::optional(*decrypting_key_size) : std::nullopt), + is_hybrid_mode_(is_hybrid_mode ? std::optional(*is_hybrid_mode) : std::nullopt) {} + +const std::string* KeyCreationResult::public_key() const { + return public_key_ ? &(*public_key_) : nullptr; +} + +void KeyCreationResult::set_public_key(const std::string_view* value_arg) { + public_key_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_public_key(std::string_view value_arg) { + public_key_ = value_arg; +} + + +const std::vector* KeyCreationResult::public_key_bytes() const { + return public_key_bytes_ ? &(*public_key_bytes_) : nullptr; +} + +void KeyCreationResult::set_public_key_bytes(const std::vector* value_arg) { + public_key_bytes_ = value_arg ? std::optional>(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_public_key_bytes(const std::vector& value_arg) { + public_key_bytes_ = value_arg; +} + + +const std::string* KeyCreationResult::error() const { + return error_ ? &(*error_) : nullptr; +} + +void KeyCreationResult::set_error(const std::string_view* value_arg) { + error_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_error(std::string_view value_arg) { + error_ = value_arg; +} + + +const BiometricError* KeyCreationResult::code() const { + return code_ ? &(*code_) : nullptr; +} + +void KeyCreationResult::set_code(const BiometricError* value_arg) { + code_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_code(const BiometricError& value_arg) { + code_ = value_arg; +} + + +const std::string* KeyCreationResult::algorithm() const { + return algorithm_ ? &(*algorithm_) : nullptr; +} + +void KeyCreationResult::set_algorithm(const std::string_view* value_arg) { + algorithm_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_algorithm(std::string_view value_arg) { + algorithm_ = value_arg; +} + + +const int64_t* KeyCreationResult::key_size() const { + return key_size_ ? &(*key_size_) : nullptr; +} + +void KeyCreationResult::set_key_size(const int64_t* value_arg) { + key_size_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_key_size(int64_t value_arg) { + key_size_ = value_arg; +} + + +const std::string* KeyCreationResult::decrypting_public_key() const { + return decrypting_public_key_ ? &(*decrypting_public_key_) : nullptr; +} + +void KeyCreationResult::set_decrypting_public_key(const std::string_view* value_arg) { + decrypting_public_key_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_decrypting_public_key(std::string_view value_arg) { + decrypting_public_key_ = value_arg; +} + + +const std::string* KeyCreationResult::decrypting_algorithm() const { + return decrypting_algorithm_ ? &(*decrypting_algorithm_) : nullptr; +} + +void KeyCreationResult::set_decrypting_algorithm(const std::string_view* value_arg) { + decrypting_algorithm_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_decrypting_algorithm(std::string_view value_arg) { + decrypting_algorithm_ = value_arg; +} + + +const int64_t* KeyCreationResult::decrypting_key_size() const { + return decrypting_key_size_ ? &(*decrypting_key_size_) : nullptr; +} + +void KeyCreationResult::set_decrypting_key_size(const int64_t* value_arg) { + decrypting_key_size_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_decrypting_key_size(int64_t value_arg) { + decrypting_key_size_ = value_arg; +} + + +const bool* KeyCreationResult::is_hybrid_mode() const { + return is_hybrid_mode_ ? &(*is_hybrid_mode_) : nullptr; +} + +void KeyCreationResult::set_is_hybrid_mode(const bool* value_arg) { + is_hybrid_mode_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyCreationResult::set_is_hybrid_mode(bool value_arg) { + is_hybrid_mode_ = value_arg; +} + + +EncodableList KeyCreationResult::ToEncodableList() const { + EncodableList list; + list.reserve(10); + list.push_back(public_key_ ? EncodableValue(*public_key_) : EncodableValue()); + list.push_back(public_key_bytes_ ? EncodableValue(*public_key_bytes_) : EncodableValue()); + list.push_back(error_ ? EncodableValue(*error_) : EncodableValue()); + list.push_back(code_ ? CustomEncodableValue(*code_) : EncodableValue()); + list.push_back(algorithm_ ? EncodableValue(*algorithm_) : EncodableValue()); + list.push_back(key_size_ ? EncodableValue(*key_size_) : EncodableValue()); + list.push_back(decrypting_public_key_ ? EncodableValue(*decrypting_public_key_) : EncodableValue()); + list.push_back(decrypting_algorithm_ ? EncodableValue(*decrypting_algorithm_) : EncodableValue()); + list.push_back(decrypting_key_size_ ? EncodableValue(*decrypting_key_size_) : EncodableValue()); + list.push_back(is_hybrid_mode_ ? EncodableValue(*is_hybrid_mode_) : EncodableValue()); + return list; +} + +KeyCreationResult KeyCreationResult::FromEncodableList(const EncodableList& list) { + KeyCreationResult decoded; + auto& encodable_public_key = list[0]; + if (!encodable_public_key.IsNull()) { + decoded.set_public_key(std::get(encodable_public_key)); + } + auto& encodable_public_key_bytes = list[1]; + if (!encodable_public_key_bytes.IsNull()) { + decoded.set_public_key_bytes(std::get>(encodable_public_key_bytes)); + } + auto& encodable_error = list[2]; + if (!encodable_error.IsNull()) { + decoded.set_error(std::get(encodable_error)); + } + auto& encodable_code = list[3]; + if (!encodable_code.IsNull()) { + decoded.set_code(std::any_cast(std::get(encodable_code))); + } + auto& encodable_algorithm = list[4]; + if (!encodable_algorithm.IsNull()) { + decoded.set_algorithm(std::get(encodable_algorithm)); + } + auto& encodable_key_size = list[5]; + if (!encodable_key_size.IsNull()) { + decoded.set_key_size(std::get(encodable_key_size)); + } + auto& encodable_decrypting_public_key = list[6]; + if (!encodable_decrypting_public_key.IsNull()) { + decoded.set_decrypting_public_key(std::get(encodable_decrypting_public_key)); + } + auto& encodable_decrypting_algorithm = list[7]; + if (!encodable_decrypting_algorithm.IsNull()) { + decoded.set_decrypting_algorithm(std::get(encodable_decrypting_algorithm)); + } + auto& encodable_decrypting_key_size = list[8]; + if (!encodable_decrypting_key_size.IsNull()) { + decoded.set_decrypting_key_size(std::get(encodable_decrypting_key_size)); + } + auto& encodable_is_hybrid_mode = list[9]; + if (!encodable_is_hybrid_mode.IsNull()) { + decoded.set_is_hybrid_mode(std::get(encodable_is_hybrid_mode)); + } + return decoded; +} + +// SignatureResult + +SignatureResult::SignatureResult() {} + +SignatureResult::SignatureResult( + const std::string* signature, + const std::vector* signature_bytes, + const std::string* public_key, + const std::string* error, + const BiometricError* code, + const std::string* algorithm, + const int64_t* key_size) + : signature_(signature ? std::optional(*signature) : std::nullopt), + signature_bytes_(signature_bytes ? std::optional>(*signature_bytes) : std::nullopt), + public_key_(public_key ? std::optional(*public_key) : std::nullopt), + error_(error ? std::optional(*error) : std::nullopt), + code_(code ? std::optional(*code) : std::nullopt), + algorithm_(algorithm ? std::optional(*algorithm) : std::nullopt), + key_size_(key_size ? std::optional(*key_size) : std::nullopt) {} + +const std::string* SignatureResult::signature() const { + return signature_ ? &(*signature_) : nullptr; +} + +void SignatureResult::set_signature(const std::string_view* value_arg) { + signature_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void SignatureResult::set_signature(std::string_view value_arg) { + signature_ = value_arg; +} + + +const std::vector* SignatureResult::signature_bytes() const { + return signature_bytes_ ? &(*signature_bytes_) : nullptr; +} + +void SignatureResult::set_signature_bytes(const std::vector* value_arg) { + signature_bytes_ = value_arg ? std::optional>(*value_arg) : std::nullopt; +} + +void SignatureResult::set_signature_bytes(const std::vector& value_arg) { + signature_bytes_ = value_arg; +} + + +const std::string* SignatureResult::public_key() const { + return public_key_ ? &(*public_key_) : nullptr; +} + +void SignatureResult::set_public_key(const std::string_view* value_arg) { + public_key_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void SignatureResult::set_public_key(std::string_view value_arg) { + public_key_ = value_arg; +} + + +const std::string* SignatureResult::error() const { + return error_ ? &(*error_) : nullptr; +} + +void SignatureResult::set_error(const std::string_view* value_arg) { + error_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void SignatureResult::set_error(std::string_view value_arg) { + error_ = value_arg; +} + + +const BiometricError* SignatureResult::code() const { + return code_ ? &(*code_) : nullptr; +} + +void SignatureResult::set_code(const BiometricError* value_arg) { + code_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void SignatureResult::set_code(const BiometricError& value_arg) { + code_ = value_arg; +} + + +const std::string* SignatureResult::algorithm() const { + return algorithm_ ? &(*algorithm_) : nullptr; +} + +void SignatureResult::set_algorithm(const std::string_view* value_arg) { + algorithm_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void SignatureResult::set_algorithm(std::string_view value_arg) { + algorithm_ = value_arg; +} + + +const int64_t* SignatureResult::key_size() const { + return key_size_ ? &(*key_size_) : nullptr; +} + +void SignatureResult::set_key_size(const int64_t* value_arg) { + key_size_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void SignatureResult::set_key_size(int64_t value_arg) { + key_size_ = value_arg; +} + + +EncodableList SignatureResult::ToEncodableList() const { + EncodableList list; + list.reserve(7); + list.push_back(signature_ ? EncodableValue(*signature_) : EncodableValue()); + list.push_back(signature_bytes_ ? EncodableValue(*signature_bytes_) : EncodableValue()); + list.push_back(public_key_ ? EncodableValue(*public_key_) : EncodableValue()); + list.push_back(error_ ? EncodableValue(*error_) : EncodableValue()); + list.push_back(code_ ? CustomEncodableValue(*code_) : EncodableValue()); + list.push_back(algorithm_ ? EncodableValue(*algorithm_) : EncodableValue()); + list.push_back(key_size_ ? EncodableValue(*key_size_) : EncodableValue()); + return list; +} + +SignatureResult SignatureResult::FromEncodableList(const EncodableList& list) { + SignatureResult decoded; + auto& encodable_signature = list[0]; + if (!encodable_signature.IsNull()) { + decoded.set_signature(std::get(encodable_signature)); + } + auto& encodable_signature_bytes = list[1]; + if (!encodable_signature_bytes.IsNull()) { + decoded.set_signature_bytes(std::get>(encodable_signature_bytes)); + } + auto& encodable_public_key = list[2]; + if (!encodable_public_key.IsNull()) { + decoded.set_public_key(std::get(encodable_public_key)); + } + auto& encodable_error = list[3]; + if (!encodable_error.IsNull()) { + decoded.set_error(std::get(encodable_error)); + } + auto& encodable_code = list[4]; + if (!encodable_code.IsNull()) { + decoded.set_code(std::any_cast(std::get(encodable_code))); + } + auto& encodable_algorithm = list[5]; + if (!encodable_algorithm.IsNull()) { + decoded.set_algorithm(std::get(encodable_algorithm)); + } + auto& encodable_key_size = list[6]; + if (!encodable_key_size.IsNull()) { + decoded.set_key_size(std::get(encodable_key_size)); + } + return decoded; +} + +// DecryptResult + +DecryptResult::DecryptResult() {} + +DecryptResult::DecryptResult( + const std::string* decrypted_data, + const std::string* error, + const BiometricError* code) + : decrypted_data_(decrypted_data ? std::optional(*decrypted_data) : std::nullopt), + error_(error ? std::optional(*error) : std::nullopt), + code_(code ? std::optional(*code) : std::nullopt) {} + +const std::string* DecryptResult::decrypted_data() const { + return decrypted_data_ ? &(*decrypted_data_) : nullptr; +} + +void DecryptResult::set_decrypted_data(const std::string_view* value_arg) { + decrypted_data_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DecryptResult::set_decrypted_data(std::string_view value_arg) { + decrypted_data_ = value_arg; +} + + +const std::string* DecryptResult::error() const { + return error_ ? &(*error_) : nullptr; +} + +void DecryptResult::set_error(const std::string_view* value_arg) { + error_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DecryptResult::set_error(std::string_view value_arg) { + error_ = value_arg; +} + + +const BiometricError* DecryptResult::code() const { + return code_ ? &(*code_) : nullptr; +} + +void DecryptResult::set_code(const BiometricError* value_arg) { + code_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DecryptResult::set_code(const BiometricError& value_arg) { + code_ = value_arg; +} + + +EncodableList DecryptResult::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(decrypted_data_ ? EncodableValue(*decrypted_data_) : EncodableValue()); + list.push_back(error_ ? EncodableValue(*error_) : EncodableValue()); + list.push_back(code_ ? CustomEncodableValue(*code_) : EncodableValue()); + return list; +} + +DecryptResult DecryptResult::FromEncodableList(const EncodableList& list) { + DecryptResult decoded; + auto& encodable_decrypted_data = list[0]; + if (!encodable_decrypted_data.IsNull()) { + decoded.set_decrypted_data(std::get(encodable_decrypted_data)); + } + auto& encodable_error = list[1]; + if (!encodable_error.IsNull()) { + decoded.set_error(std::get(encodable_error)); + } + auto& encodable_code = list[2]; + if (!encodable_code.IsNull()) { + decoded.set_code(std::any_cast(std::get(encodable_code))); + } + return decoded; +} + +// KeyInfo + +KeyInfo::KeyInfo() {} + +KeyInfo::KeyInfo( + const bool* exists, + const bool* is_valid, + const std::string* algorithm, + const int64_t* key_size, + const bool* is_hybrid_mode, + const std::string* public_key, + const std::string* decrypting_public_key, + const std::string* decrypting_algorithm, + const int64_t* decrypting_key_size) + : exists_(exists ? std::optional(*exists) : std::nullopt), + is_valid_(is_valid ? std::optional(*is_valid) : std::nullopt), + algorithm_(algorithm ? std::optional(*algorithm) : std::nullopt), + key_size_(key_size ? std::optional(*key_size) : std::nullopt), + is_hybrid_mode_(is_hybrid_mode ? std::optional(*is_hybrid_mode) : std::nullopt), + public_key_(public_key ? std::optional(*public_key) : std::nullopt), + decrypting_public_key_(decrypting_public_key ? std::optional(*decrypting_public_key) : std::nullopt), + decrypting_algorithm_(decrypting_algorithm ? std::optional(*decrypting_algorithm) : std::nullopt), + decrypting_key_size_(decrypting_key_size ? std::optional(*decrypting_key_size) : std::nullopt) {} + +const bool* KeyInfo::exists() const { + return exists_ ? &(*exists_) : nullptr; +} + +void KeyInfo::set_exists(const bool* value_arg) { + exists_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyInfo::set_exists(bool value_arg) { + exists_ = value_arg; +} + + +const bool* KeyInfo::is_valid() const { + return is_valid_ ? &(*is_valid_) : nullptr; +} + +void KeyInfo::set_is_valid(const bool* value_arg) { + is_valid_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyInfo::set_is_valid(bool value_arg) { + is_valid_ = value_arg; +} + + +const std::string* KeyInfo::algorithm() const { + return algorithm_ ? &(*algorithm_) : nullptr; +} + +void KeyInfo::set_algorithm(const std::string_view* value_arg) { + algorithm_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyInfo::set_algorithm(std::string_view value_arg) { + algorithm_ = value_arg; +} + + +const int64_t* KeyInfo::key_size() const { + return key_size_ ? &(*key_size_) : nullptr; +} + +void KeyInfo::set_key_size(const int64_t* value_arg) { + key_size_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyInfo::set_key_size(int64_t value_arg) { + key_size_ = value_arg; +} + + +const bool* KeyInfo::is_hybrid_mode() const { + return is_hybrid_mode_ ? &(*is_hybrid_mode_) : nullptr; +} + +void KeyInfo::set_is_hybrid_mode(const bool* value_arg) { + is_hybrid_mode_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyInfo::set_is_hybrid_mode(bool value_arg) { + is_hybrid_mode_ = value_arg; +} + + +const std::string* KeyInfo::public_key() const { + return public_key_ ? &(*public_key_) : nullptr; +} + +void KeyInfo::set_public_key(const std::string_view* value_arg) { + public_key_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyInfo::set_public_key(std::string_view value_arg) { + public_key_ = value_arg; +} + + +const std::string* KeyInfo::decrypting_public_key() const { + return decrypting_public_key_ ? &(*decrypting_public_key_) : nullptr; +} + +void KeyInfo::set_decrypting_public_key(const std::string_view* value_arg) { + decrypting_public_key_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyInfo::set_decrypting_public_key(std::string_view value_arg) { + decrypting_public_key_ = value_arg; +} + + +const std::string* KeyInfo::decrypting_algorithm() const { + return decrypting_algorithm_ ? &(*decrypting_algorithm_) : nullptr; +} + +void KeyInfo::set_decrypting_algorithm(const std::string_view* value_arg) { + decrypting_algorithm_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyInfo::set_decrypting_algorithm(std::string_view value_arg) { + decrypting_algorithm_ = value_arg; +} + + +const int64_t* KeyInfo::decrypting_key_size() const { + return decrypting_key_size_ ? &(*decrypting_key_size_) : nullptr; +} + +void KeyInfo::set_decrypting_key_size(const int64_t* value_arg) { + decrypting_key_size_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void KeyInfo::set_decrypting_key_size(int64_t value_arg) { + decrypting_key_size_ = value_arg; +} + + +EncodableList KeyInfo::ToEncodableList() const { + EncodableList list; + list.reserve(9); + list.push_back(exists_ ? EncodableValue(*exists_) : EncodableValue()); + list.push_back(is_valid_ ? EncodableValue(*is_valid_) : EncodableValue()); + list.push_back(algorithm_ ? EncodableValue(*algorithm_) : EncodableValue()); + list.push_back(key_size_ ? EncodableValue(*key_size_) : EncodableValue()); + list.push_back(is_hybrid_mode_ ? EncodableValue(*is_hybrid_mode_) : EncodableValue()); + list.push_back(public_key_ ? EncodableValue(*public_key_) : EncodableValue()); + list.push_back(decrypting_public_key_ ? EncodableValue(*decrypting_public_key_) : EncodableValue()); + list.push_back(decrypting_algorithm_ ? EncodableValue(*decrypting_algorithm_) : EncodableValue()); + list.push_back(decrypting_key_size_ ? EncodableValue(*decrypting_key_size_) : EncodableValue()); + return list; +} + +KeyInfo KeyInfo::FromEncodableList(const EncodableList& list) { + KeyInfo decoded; + auto& encodable_exists = list[0]; + if (!encodable_exists.IsNull()) { + decoded.set_exists(std::get(encodable_exists)); + } + auto& encodable_is_valid = list[1]; + if (!encodable_is_valid.IsNull()) { + decoded.set_is_valid(std::get(encodable_is_valid)); + } + auto& encodable_algorithm = list[2]; + if (!encodable_algorithm.IsNull()) { + decoded.set_algorithm(std::get(encodable_algorithm)); + } + auto& encodable_key_size = list[3]; + if (!encodable_key_size.IsNull()) { + decoded.set_key_size(std::get(encodable_key_size)); + } + auto& encodable_is_hybrid_mode = list[4]; + if (!encodable_is_hybrid_mode.IsNull()) { + decoded.set_is_hybrid_mode(std::get(encodable_is_hybrid_mode)); + } + auto& encodable_public_key = list[5]; + if (!encodable_public_key.IsNull()) { + decoded.set_public_key(std::get(encodable_public_key)); + } + auto& encodable_decrypting_public_key = list[6]; + if (!encodable_decrypting_public_key.IsNull()) { + decoded.set_decrypting_public_key(std::get(encodable_decrypting_public_key)); + } + auto& encodable_decrypting_algorithm = list[7]; + if (!encodable_decrypting_algorithm.IsNull()) { + decoded.set_decrypting_algorithm(std::get(encodable_decrypting_algorithm)); + } + auto& encodable_decrypting_key_size = list[8]; + if (!encodable_decrypting_key_size.IsNull()) { + decoded.set_decrypting_key_size(std::get(encodable_decrypting_key_size)); + } + return decoded; +} + +// CreateKeysConfig + +CreateKeysConfig::CreateKeysConfig() {} + +CreateKeysConfig::CreateKeysConfig( + const SignatureType* signature_type, + const bool* enforce_biometric, + const bool* set_invalidated_by_biometric_enrollment, + const bool* use_device_credentials, + const bool* enable_decryption, + const std::string* prompt_subtitle, + const std::string* prompt_description, + const std::string* cancel_button_text) + : signature_type_(signature_type ? std::optional(*signature_type) : std::nullopt), + enforce_biometric_(enforce_biometric ? std::optional(*enforce_biometric) : std::nullopt), + set_invalidated_by_biometric_enrollment_(set_invalidated_by_biometric_enrollment ? std::optional(*set_invalidated_by_biometric_enrollment) : std::nullopt), + use_device_credentials_(use_device_credentials ? std::optional(*use_device_credentials) : std::nullopt), + enable_decryption_(enable_decryption ? std::optional(*enable_decryption) : std::nullopt), + prompt_subtitle_(prompt_subtitle ? std::optional(*prompt_subtitle) : std::nullopt), + prompt_description_(prompt_description ? std::optional(*prompt_description) : std::nullopt), + cancel_button_text_(cancel_button_text ? std::optional(*cancel_button_text) : std::nullopt) {} + +const SignatureType* CreateKeysConfig::signature_type() const { + return signature_type_ ? &(*signature_type_) : nullptr; +} + +void CreateKeysConfig::set_signature_type(const SignatureType* value_arg) { + signature_type_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateKeysConfig::set_signature_type(const SignatureType& value_arg) { + signature_type_ = value_arg; +} + + +const bool* CreateKeysConfig::enforce_biometric() const { + return enforce_biometric_ ? &(*enforce_biometric_) : nullptr; +} + +void CreateKeysConfig::set_enforce_biometric(const bool* value_arg) { + enforce_biometric_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateKeysConfig::set_enforce_biometric(bool value_arg) { + enforce_biometric_ = value_arg; +} + + +const bool* CreateKeysConfig::set_invalidated_by_biometric_enrollment() const { + return set_invalidated_by_biometric_enrollment_ ? &(*set_invalidated_by_biometric_enrollment_) : nullptr; +} + +void CreateKeysConfig::set_set_invalidated_by_biometric_enrollment(const bool* value_arg) { + set_invalidated_by_biometric_enrollment_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateKeysConfig::set_set_invalidated_by_biometric_enrollment(bool value_arg) { + set_invalidated_by_biometric_enrollment_ = value_arg; +} + + +const bool* CreateKeysConfig::use_device_credentials() const { + return use_device_credentials_ ? &(*use_device_credentials_) : nullptr; +} + +void CreateKeysConfig::set_use_device_credentials(const bool* value_arg) { + use_device_credentials_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateKeysConfig::set_use_device_credentials(bool value_arg) { + use_device_credentials_ = value_arg; +} + + +const bool* CreateKeysConfig::enable_decryption() const { + return enable_decryption_ ? &(*enable_decryption_) : nullptr; +} + +void CreateKeysConfig::set_enable_decryption(const bool* value_arg) { + enable_decryption_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateKeysConfig::set_enable_decryption(bool value_arg) { + enable_decryption_ = value_arg; +} + + +const std::string* CreateKeysConfig::prompt_subtitle() const { + return prompt_subtitle_ ? &(*prompt_subtitle_) : nullptr; +} + +void CreateKeysConfig::set_prompt_subtitle(const std::string_view* value_arg) { + prompt_subtitle_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateKeysConfig::set_prompt_subtitle(std::string_view value_arg) { + prompt_subtitle_ = value_arg; +} + + +const std::string* CreateKeysConfig::prompt_description() const { + return prompt_description_ ? &(*prompt_description_) : nullptr; +} + +void CreateKeysConfig::set_prompt_description(const std::string_view* value_arg) { + prompt_description_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateKeysConfig::set_prompt_description(std::string_view value_arg) { + prompt_description_ = value_arg; +} + + +const std::string* CreateKeysConfig::cancel_button_text() const { + return cancel_button_text_ ? &(*cancel_button_text_) : nullptr; +} + +void CreateKeysConfig::set_cancel_button_text(const std::string_view* value_arg) { + cancel_button_text_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateKeysConfig::set_cancel_button_text(std::string_view value_arg) { + cancel_button_text_ = value_arg; +} + + +EncodableList CreateKeysConfig::ToEncodableList() const { + EncodableList list; + list.reserve(8); + list.push_back(signature_type_ ? CustomEncodableValue(*signature_type_) : EncodableValue()); + list.push_back(enforce_biometric_ ? EncodableValue(*enforce_biometric_) : EncodableValue()); + list.push_back(set_invalidated_by_biometric_enrollment_ ? EncodableValue(*set_invalidated_by_biometric_enrollment_) : EncodableValue()); + list.push_back(use_device_credentials_ ? EncodableValue(*use_device_credentials_) : EncodableValue()); + list.push_back(enable_decryption_ ? EncodableValue(*enable_decryption_) : EncodableValue()); + list.push_back(prompt_subtitle_ ? EncodableValue(*prompt_subtitle_) : EncodableValue()); + list.push_back(prompt_description_ ? EncodableValue(*prompt_description_) : EncodableValue()); + list.push_back(cancel_button_text_ ? EncodableValue(*cancel_button_text_) : EncodableValue()); + return list; +} + +CreateKeysConfig CreateKeysConfig::FromEncodableList(const EncodableList& list) { + CreateKeysConfig decoded; + auto& encodable_signature_type = list[0]; + if (!encodable_signature_type.IsNull()) { + decoded.set_signature_type(std::any_cast(std::get(encodable_signature_type))); + } + auto& encodable_enforce_biometric = list[1]; + if (!encodable_enforce_biometric.IsNull()) { + decoded.set_enforce_biometric(std::get(encodable_enforce_biometric)); + } + auto& encodable_set_invalidated_by_biometric_enrollment = list[2]; + if (!encodable_set_invalidated_by_biometric_enrollment.IsNull()) { + decoded.set_set_invalidated_by_biometric_enrollment(std::get(encodable_set_invalidated_by_biometric_enrollment)); + } + auto& encodable_use_device_credentials = list[3]; + if (!encodable_use_device_credentials.IsNull()) { + decoded.set_use_device_credentials(std::get(encodable_use_device_credentials)); + } + auto& encodable_enable_decryption = list[4]; + if (!encodable_enable_decryption.IsNull()) { + decoded.set_enable_decryption(std::get(encodable_enable_decryption)); + } + auto& encodable_prompt_subtitle = list[5]; + if (!encodable_prompt_subtitle.IsNull()) { + decoded.set_prompt_subtitle(std::get(encodable_prompt_subtitle)); + } + auto& encodable_prompt_description = list[6]; + if (!encodable_prompt_description.IsNull()) { + decoded.set_prompt_description(std::get(encodable_prompt_description)); + } + auto& encodable_cancel_button_text = list[7]; + if (!encodable_cancel_button_text.IsNull()) { + decoded.set_cancel_button_text(std::get(encodable_cancel_button_text)); + } + return decoded; +} + +// CreateSignatureConfig + +CreateSignatureConfig::CreateSignatureConfig() {} + +CreateSignatureConfig::CreateSignatureConfig( + const std::string* prompt_subtitle, + const std::string* prompt_description, + const std::string* cancel_button_text, + const bool* allow_device_credentials, + const bool* should_migrate) + : prompt_subtitle_(prompt_subtitle ? std::optional(*prompt_subtitle) : std::nullopt), + prompt_description_(prompt_description ? std::optional(*prompt_description) : std::nullopt), + cancel_button_text_(cancel_button_text ? std::optional(*cancel_button_text) : std::nullopt), + allow_device_credentials_(allow_device_credentials ? std::optional(*allow_device_credentials) : std::nullopt), + should_migrate_(should_migrate ? std::optional(*should_migrate) : std::nullopt) {} + +const std::string* CreateSignatureConfig::prompt_subtitle() const { + return prompt_subtitle_ ? &(*prompt_subtitle_) : nullptr; +} + +void CreateSignatureConfig::set_prompt_subtitle(const std::string_view* value_arg) { + prompt_subtitle_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateSignatureConfig::set_prompt_subtitle(std::string_view value_arg) { + prompt_subtitle_ = value_arg; +} + + +const std::string* CreateSignatureConfig::prompt_description() const { + return prompt_description_ ? &(*prompt_description_) : nullptr; +} + +void CreateSignatureConfig::set_prompt_description(const std::string_view* value_arg) { + prompt_description_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateSignatureConfig::set_prompt_description(std::string_view value_arg) { + prompt_description_ = value_arg; +} + + +const std::string* CreateSignatureConfig::cancel_button_text() const { + return cancel_button_text_ ? &(*cancel_button_text_) : nullptr; +} + +void CreateSignatureConfig::set_cancel_button_text(const std::string_view* value_arg) { + cancel_button_text_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateSignatureConfig::set_cancel_button_text(std::string_view value_arg) { + cancel_button_text_ = value_arg; +} + + +const bool* CreateSignatureConfig::allow_device_credentials() const { + return allow_device_credentials_ ? &(*allow_device_credentials_) : nullptr; +} + +void CreateSignatureConfig::set_allow_device_credentials(const bool* value_arg) { + allow_device_credentials_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateSignatureConfig::set_allow_device_credentials(bool value_arg) { + allow_device_credentials_ = value_arg; +} + + +const bool* CreateSignatureConfig::should_migrate() const { + return should_migrate_ ? &(*should_migrate_) : nullptr; +} + +void CreateSignatureConfig::set_should_migrate(const bool* value_arg) { + should_migrate_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void CreateSignatureConfig::set_should_migrate(bool value_arg) { + should_migrate_ = value_arg; +} + + +EncodableList CreateSignatureConfig::ToEncodableList() const { + EncodableList list; + list.reserve(5); + list.push_back(prompt_subtitle_ ? EncodableValue(*prompt_subtitle_) : EncodableValue()); + list.push_back(prompt_description_ ? EncodableValue(*prompt_description_) : EncodableValue()); + list.push_back(cancel_button_text_ ? EncodableValue(*cancel_button_text_) : EncodableValue()); + list.push_back(allow_device_credentials_ ? EncodableValue(*allow_device_credentials_) : EncodableValue()); + list.push_back(should_migrate_ ? EncodableValue(*should_migrate_) : EncodableValue()); + return list; +} + +CreateSignatureConfig CreateSignatureConfig::FromEncodableList(const EncodableList& list) { + CreateSignatureConfig decoded; + auto& encodable_prompt_subtitle = list[0]; + if (!encodable_prompt_subtitle.IsNull()) { + decoded.set_prompt_subtitle(std::get(encodable_prompt_subtitle)); + } + auto& encodable_prompt_description = list[1]; + if (!encodable_prompt_description.IsNull()) { + decoded.set_prompt_description(std::get(encodable_prompt_description)); + } + auto& encodable_cancel_button_text = list[2]; + if (!encodable_cancel_button_text.IsNull()) { + decoded.set_cancel_button_text(std::get(encodable_cancel_button_text)); + } + auto& encodable_allow_device_credentials = list[3]; + if (!encodable_allow_device_credentials.IsNull()) { + decoded.set_allow_device_credentials(std::get(encodable_allow_device_credentials)); + } + auto& encodable_should_migrate = list[4]; + if (!encodable_should_migrate.IsNull()) { + decoded.set_should_migrate(std::get(encodable_should_migrate)); + } + return decoded; +} + +// DecryptConfig + +DecryptConfig::DecryptConfig() {} + +DecryptConfig::DecryptConfig( + const std::string* prompt_subtitle, + const std::string* prompt_description, + const std::string* cancel_button_text, + const bool* allow_device_credentials, + const bool* should_migrate) + : prompt_subtitle_(prompt_subtitle ? std::optional(*prompt_subtitle) : std::nullopt), + prompt_description_(prompt_description ? std::optional(*prompt_description) : std::nullopt), + cancel_button_text_(cancel_button_text ? std::optional(*cancel_button_text) : std::nullopt), + allow_device_credentials_(allow_device_credentials ? std::optional(*allow_device_credentials) : std::nullopt), + should_migrate_(should_migrate ? std::optional(*should_migrate) : std::nullopt) {} + +const std::string* DecryptConfig::prompt_subtitle() const { + return prompt_subtitle_ ? &(*prompt_subtitle_) : nullptr; +} + +void DecryptConfig::set_prompt_subtitle(const std::string_view* value_arg) { + prompt_subtitle_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DecryptConfig::set_prompt_subtitle(std::string_view value_arg) { + prompt_subtitle_ = value_arg; +} + + +const std::string* DecryptConfig::prompt_description() const { + return prompt_description_ ? &(*prompt_description_) : nullptr; +} + +void DecryptConfig::set_prompt_description(const std::string_view* value_arg) { + prompt_description_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DecryptConfig::set_prompt_description(std::string_view value_arg) { + prompt_description_ = value_arg; +} + + +const std::string* DecryptConfig::cancel_button_text() const { + return cancel_button_text_ ? &(*cancel_button_text_) : nullptr; +} + +void DecryptConfig::set_cancel_button_text(const std::string_view* value_arg) { + cancel_button_text_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DecryptConfig::set_cancel_button_text(std::string_view value_arg) { + cancel_button_text_ = value_arg; +} + + +const bool* DecryptConfig::allow_device_credentials() const { + return allow_device_credentials_ ? &(*allow_device_credentials_) : nullptr; +} + +void DecryptConfig::set_allow_device_credentials(const bool* value_arg) { + allow_device_credentials_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DecryptConfig::set_allow_device_credentials(bool value_arg) { + allow_device_credentials_ = value_arg; +} + + +const bool* DecryptConfig::should_migrate() const { + return should_migrate_ ? &(*should_migrate_) : nullptr; +} + +void DecryptConfig::set_should_migrate(const bool* value_arg) { + should_migrate_ = value_arg ? std::optional(*value_arg) : std::nullopt; +} + +void DecryptConfig::set_should_migrate(bool value_arg) { + should_migrate_ = value_arg; +} + + +EncodableList DecryptConfig::ToEncodableList() const { + EncodableList list; + list.reserve(5); + list.push_back(prompt_subtitle_ ? EncodableValue(*prompt_subtitle_) : EncodableValue()); + list.push_back(prompt_description_ ? EncodableValue(*prompt_description_) : EncodableValue()); + list.push_back(cancel_button_text_ ? EncodableValue(*cancel_button_text_) : EncodableValue()); + list.push_back(allow_device_credentials_ ? EncodableValue(*allow_device_credentials_) : EncodableValue()); + list.push_back(should_migrate_ ? EncodableValue(*should_migrate_) : EncodableValue()); + return list; +} + +DecryptConfig DecryptConfig::FromEncodableList(const EncodableList& list) { + DecryptConfig decoded; + auto& encodable_prompt_subtitle = list[0]; + if (!encodable_prompt_subtitle.IsNull()) { + decoded.set_prompt_subtitle(std::get(encodable_prompt_subtitle)); + } + auto& encodable_prompt_description = list[1]; + if (!encodable_prompt_description.IsNull()) { + decoded.set_prompt_description(std::get(encodable_prompt_description)); + } + auto& encodable_cancel_button_text = list[2]; + if (!encodable_cancel_button_text.IsNull()) { + decoded.set_cancel_button_text(std::get(encodable_cancel_button_text)); + } + auto& encodable_allow_device_credentials = list[3]; + if (!encodable_allow_device_credentials.IsNull()) { + decoded.set_allow_device_credentials(std::get(encodable_allow_device_credentials)); + } + auto& encodable_should_migrate = list[4]; + if (!encodable_should_migrate.IsNull()) { + decoded.set_should_migrate(std::get(encodable_should_migrate)); + } + return decoded; +} + + +PigeonInternalCodecSerializer::PigeonInternalCodecSerializer() {} + +EncodableValue PigeonInternalCodecSerializer::ReadValueOfType( + uint8_t type, + flutter::ByteStreamReader* stream) const { + switch (type) { + case 129: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 130: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 131: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 132: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 133: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 134: { + const auto& encodable_enum_arg = ReadValue(stream); + const int64_t enum_arg_value = encodable_enum_arg.IsNull() ? 0 : encodable_enum_arg.LongValue(); + return encodable_enum_arg.IsNull() ? EncodableValue() : CustomEncodableValue(static_cast(enum_arg_value)); + } + case 135: { + return CustomEncodableValue(BiometricAvailability::FromEncodableList(std::get(ReadValue(stream)))); + } + case 136: { + return CustomEncodableValue(KeyCreationResult::FromEncodableList(std::get(ReadValue(stream)))); + } + case 137: { + return CustomEncodableValue(SignatureResult::FromEncodableList(std::get(ReadValue(stream)))); + } + case 138: { + return CustomEncodableValue(DecryptResult::FromEncodableList(std::get(ReadValue(stream)))); + } + case 139: { + return CustomEncodableValue(KeyInfo::FromEncodableList(std::get(ReadValue(stream)))); + } + case 140: { + return CustomEncodableValue(CreateKeysConfig::FromEncodableList(std::get(ReadValue(stream)))); + } + case 141: { + return CustomEncodableValue(CreateSignatureConfig::FromEncodableList(std::get(ReadValue(stream)))); + } + case 142: { + return CustomEncodableValue(DecryptConfig::FromEncodableList(std::get(ReadValue(stream)))); + } + default: + return flutter::StandardCodecSerializer::ReadValueOfType(type, stream); + } +} + +void PigeonInternalCodecSerializer::WriteValue( + const EncodableValue& value, + flutter::ByteStreamWriter* stream) const { + if (const CustomEncodableValue* custom_value = std::get_if(&value)) { + if (custom_value->type() == typeid(BiometricType)) { + stream->WriteByte(129); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(BiometricError)) { + stream->WriteByte(130); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(SignatureType)) { + stream->WriteByte(131); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(KeyFormat)) { + stream->WriteByte(132); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(SignatureFormat)) { + stream->WriteByte(133); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(PayloadFormat)) { + stream->WriteByte(134); + WriteValue(EncodableValue(static_cast(std::any_cast(*custom_value))), stream); + return; + } + if (custom_value->type() == typeid(BiometricAvailability)) { + stream->WriteByte(135); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(KeyCreationResult)) { + stream->WriteByte(136); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(SignatureResult)) { + stream->WriteByte(137); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(DecryptResult)) { + stream->WriteByte(138); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(KeyInfo)) { + stream->WriteByte(139); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(CreateKeysConfig)) { + stream->WriteByte(140); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(CreateSignatureConfig)) { + stream->WriteByte(141); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + if (custom_value->type() == typeid(DecryptConfig)) { + stream->WriteByte(142); + WriteValue(EncodableValue(std::any_cast(*custom_value).ToEncodableList()), stream); + return; + } + } + flutter::StandardCodecSerializer::WriteValue(value, stream); +} + +/// The codec used by BiometricSignatureApi. +const flutter::StandardMessageCodec& BiometricSignatureApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance(&PigeonInternalCodecSerializer::GetInstance()); +} + +// Sets up an instance of `BiometricSignatureApi` to handle messages through the `binary_messenger`. +void BiometricSignatureApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + BiometricSignatureApi* api) { + BiometricSignatureApi::SetUp(binary_messenger, api, ""); +} + +void BiometricSignatureApi::SetUp( + flutter::BinaryMessenger* binary_messenger, + BiometricSignatureApi* api, + const std::string& message_channel_suffix) { + const std::string prepended_suffix = message_channel_suffix.length() > 0 ? std::string(".") + message_channel_suffix : ""; + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.biometricAuthAvailable" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->BiometricAuthAvailable([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createKeys" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_config_arg = args.at(0); + const auto* config_arg = encodable_config_arg.IsNull() ? nullptr : &(std::any_cast(std::get(encodable_config_arg))); + const auto& encodable_key_format_arg = args.at(1); + if (encodable_key_format_arg.IsNull()) { + reply(WrapError("key_format_arg unexpectedly null.")); + return; + } + const auto& key_format_arg = std::any_cast(std::get(encodable_key_format_arg)); + const auto& encodable_prompt_message_arg = args.at(2); + const auto* prompt_message_arg = std::get_if(&encodable_prompt_message_arg); + api->CreateKeys(config_arg, key_format_arg, prompt_message_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.createSignature" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_payload_arg = args.at(0); + if (encodable_payload_arg.IsNull()) { + reply(WrapError("payload_arg unexpectedly null.")); + return; + } + const auto& payload_arg = std::get(encodable_payload_arg); + const auto& encodable_config_arg = args.at(1); + const auto* config_arg = encodable_config_arg.IsNull() ? nullptr : &(std::any_cast(std::get(encodable_config_arg))); + const auto& encodable_signature_format_arg = args.at(2); + if (encodable_signature_format_arg.IsNull()) { + reply(WrapError("signature_format_arg unexpectedly null.")); + return; + } + const auto& signature_format_arg = std::any_cast(std::get(encodable_signature_format_arg)); + const auto& encodable_key_format_arg = args.at(3); + if (encodable_key_format_arg.IsNull()) { + reply(WrapError("key_format_arg unexpectedly null.")); + return; + } + const auto& key_format_arg = std::any_cast(std::get(encodable_key_format_arg)); + const auto& encodable_prompt_message_arg = args.at(4); + const auto* prompt_message_arg = std::get_if(&encodable_prompt_message_arg); + api->CreateSignature(payload_arg, config_arg, signature_format_arg, key_format_arg, prompt_message_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.decrypt" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_payload_arg = args.at(0); + if (encodable_payload_arg.IsNull()) { + reply(WrapError("payload_arg unexpectedly null.")); + return; + } + const auto& payload_arg = std::get(encodable_payload_arg); + const auto& encodable_payload_format_arg = args.at(1); + if (encodable_payload_format_arg.IsNull()) { + reply(WrapError("payload_format_arg unexpectedly null.")); + return; + } + const auto& payload_format_arg = std::any_cast(std::get(encodable_payload_format_arg)); + const auto& encodable_config_arg = args.at(2); + const auto* config_arg = encodable_config_arg.IsNull() ? nullptr : &(std::any_cast(std::get(encodable_config_arg))); + const auto& encodable_prompt_message_arg = args.at(3); + const auto* prompt_message_arg = std::get_if(&encodable_prompt_message_arg); + api->Decrypt(payload_arg, payload_format_arg, config_arg, prompt_message_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.deleteKeys" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + api->DeleteKeys([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } + { + BasicMessageChannel<> channel(binary_messenger, "dev.flutter.pigeon.biometric_signature.BiometricSignatureApi.getKeyInfo" + prepended_suffix, &GetCodec()); + if (api != nullptr) { + channel.SetMessageHandler([api](const EncodableValue& message, const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_check_validity_arg = args.at(0); + if (encodable_check_validity_arg.IsNull()) { + reply(WrapError("check_validity_arg unexpectedly null.")); + return; + } + const auto& check_validity_arg = std::get(encodable_check_validity_arg); + const auto& encodable_key_format_arg = args.at(1); + if (encodable_key_format_arg.IsNull()) { + reply(WrapError("key_format_arg unexpectedly null.")); + return; + } + const auto& key_format_arg = std::any_cast(std::get(encodable_key_format_arg)); + api->GetKeyInfo(check_validity_arg, key_format_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back(CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel.SetMessageHandler(nullptr); + } + } +} + +EncodableValue BiometricSignatureApi::WrapError(std::string_view error_message) { + return EncodableValue(EncodableList{ + EncodableValue(std::string(error_message)), + EncodableValue("Error"), + EncodableValue() + }); +} + +EncodableValue BiometricSignatureApi::WrapError(const FlutterError& error) { + return EncodableValue(EncodableList{ + EncodableValue(error.code()), + EncodableValue(error.message()), + error.details() + }); +} + +} // namespace biometric_signature diff --git a/windows/messages.g.h b/windows/messages.g.h new file mode 100644 index 0000000..94300af --- /dev/null +++ b/windows/messages.g.h @@ -0,0 +1,722 @@ +// Autogenerated from Pigeon (v26.1.4), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_MESSAGES_G_H_ +#define PIGEON_MESSAGES_G_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace biometric_signature { + + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) + : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template class ErrorOr { + public: + ErrorOr(const T& rhs) : v_(rhs) {} + ErrorOr(const T&& rhs) : v_(std::move(rhs)) {} + ErrorOr(const FlutterError& rhs) : v_(rhs) {} + ErrorOr(const FlutterError&& rhs) : v_(std::move(rhs)) {} + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class BiometricSignatureApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + + +// Types of biometric authentication supported by the device. +enum class BiometricType { + // Face recognition (Face ID on iOS, face unlock on Android). + kFace = 0, + // Fingerprint recognition (Touch ID on iOS/macOS, fingerprint on Android). + kFingerprint = 1, + // Iris scanner (Android only, rare on consumer devices). + kIris = 2, + // Multiple biometric types are available on the device. + kMultiple = 3, + // No biometric hardware available or biometrics are disabled. + kUnavailable = 4 +}; + +// Standardized error codes for the plugin. +enum class BiometricError { + // The operation was successful. + kSuccess = 0, + // The user canceled the operation. + kUserCanceled = 1, + // Biometric authentication is not available on this device. + kNotAvailable = 2, + // No biometrics are enrolled. + kNotEnrolled = 3, + // The user is temporarily locked out due to too many failed attempts. + kLockedOut = 4, + // The user is permanently locked out until they log in with a strong method. + kLockedOutPermanent = 5, + // The requested key was not found. + kKeyNotFound = 6, + // The key has been invalidated (e.g. by new biometric enrollment). + kKeyInvalidated = 7, + // An unknown error occurred. + kUnknown = 8, + // The input payload was invalid (e.g. not valid Base64). + kInvalidInput = 9 +}; + +// The cryptographic algorithm to use for key generation. +enum class SignatureType { + // RSA-2048 (Android: native, iOS/macOS: hybrid mode with Secure Enclave EC). + kRsa = 0, + // ECDSA P-256 (hardware-backed on all platforms). + kEcdsa = 1 +}; + +// Output format for public keys. +enum class KeyFormat { + // Base64-encoded DER (SubjectPublicKeyInfo). + kBase64 = 0, + // PEM format with BEGIN/END PUBLIC KEY headers. + kPem = 1, + // Hexadecimal-encoded DER. + kHex = 2, + // Raw DER bytes (returned via `publicKeyBytes`). + kRaw = 3 +}; + +// Output format for cryptographic signatures. +enum class SignatureFormat { + // Base64-encoded signature bytes. + kBase64 = 0, + // Hexadecimal-encoded signature bytes. + kHex = 1, + // Raw signature bytes (returned via `signatureBytes`). + kRaw = 2 +}; + +// Input format for encrypted payloads to decrypt. +enum class PayloadFormat { + // Base64-encoded ciphertext. + kBase64 = 0, + // Hexadecimal-encoded ciphertext. + kHex = 1, + // Raw UTF-8 string (not recommended for binary data). + kRaw = 2 +}; + + +// Generated class from Pigeon that represents data sent in messages. +class BiometricAvailability { + public: + // Constructs an object setting all non-nullable fields. + BiometricAvailability(); + + // Constructs an object setting all fields. + explicit BiometricAvailability( + const bool* can_authenticate, + const bool* has_enrolled_biometrics, + const flutter::EncodableList* available_biometrics, + const std::string* reason); + + const bool* can_authenticate() const; + void set_can_authenticate(const bool* value_arg); + void set_can_authenticate(bool value_arg); + + const bool* has_enrolled_biometrics() const; + void set_has_enrolled_biometrics(const bool* value_arg); + void set_has_enrolled_biometrics(bool value_arg); + + const flutter::EncodableList* available_biometrics() const; + void set_available_biometrics(const flutter::EncodableList* value_arg); + void set_available_biometrics(const flutter::EncodableList& value_arg); + + const std::string* reason() const; + void set_reason(const std::string_view* value_arg); + void set_reason(std::string_view value_arg); + + private: + static BiometricAvailability FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class BiometricSignatureApi; + friend class PigeonInternalCodecSerializer; + std::optional can_authenticate_; + std::optional has_enrolled_biometrics_; + std::optional available_biometrics_; + std::optional reason_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class KeyCreationResult { + public: + // Constructs an object setting all non-nullable fields. + KeyCreationResult(); + + // Constructs an object setting all fields. + explicit KeyCreationResult( + const std::string* public_key, + const std::vector* public_key_bytes, + const std::string* error, + const BiometricError* code, + const std::string* algorithm, + const int64_t* key_size, + const std::string* decrypting_public_key, + const std::string* decrypting_algorithm, + const int64_t* decrypting_key_size, + const bool* is_hybrid_mode); + + const std::string* public_key() const; + void set_public_key(const std::string_view* value_arg); + void set_public_key(std::string_view value_arg); + + const std::vector* public_key_bytes() const; + void set_public_key_bytes(const std::vector* value_arg); + void set_public_key_bytes(const std::vector& value_arg); + + const std::string* error() const; + void set_error(const std::string_view* value_arg); + void set_error(std::string_view value_arg); + + const BiometricError* code() const; + void set_code(const BiometricError* value_arg); + void set_code(const BiometricError& value_arg); + + const std::string* algorithm() const; + void set_algorithm(const std::string_view* value_arg); + void set_algorithm(std::string_view value_arg); + + const int64_t* key_size() const; + void set_key_size(const int64_t* value_arg); + void set_key_size(int64_t value_arg); + + const std::string* decrypting_public_key() const; + void set_decrypting_public_key(const std::string_view* value_arg); + void set_decrypting_public_key(std::string_view value_arg); + + const std::string* decrypting_algorithm() const; + void set_decrypting_algorithm(const std::string_view* value_arg); + void set_decrypting_algorithm(std::string_view value_arg); + + const int64_t* decrypting_key_size() const; + void set_decrypting_key_size(const int64_t* value_arg); + void set_decrypting_key_size(int64_t value_arg); + + const bool* is_hybrid_mode() const; + void set_is_hybrid_mode(const bool* value_arg); + void set_is_hybrid_mode(bool value_arg); + + private: + static KeyCreationResult FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class BiometricSignatureApi; + friend class PigeonInternalCodecSerializer; + std::optional public_key_; + std::optional> public_key_bytes_; + std::optional error_; + std::optional code_; + std::optional algorithm_; + std::optional key_size_; + std::optional decrypting_public_key_; + std::optional decrypting_algorithm_; + std::optional decrypting_key_size_; + std::optional is_hybrid_mode_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class SignatureResult { + public: + // Constructs an object setting all non-nullable fields. + SignatureResult(); + + // Constructs an object setting all fields. + explicit SignatureResult( + const std::string* signature, + const std::vector* signature_bytes, + const std::string* public_key, + const std::string* error, + const BiometricError* code, + const std::string* algorithm, + const int64_t* key_size); + + const std::string* signature() const; + void set_signature(const std::string_view* value_arg); + void set_signature(std::string_view value_arg); + + const std::vector* signature_bytes() const; + void set_signature_bytes(const std::vector* value_arg); + void set_signature_bytes(const std::vector& value_arg); + + const std::string* public_key() const; + void set_public_key(const std::string_view* value_arg); + void set_public_key(std::string_view value_arg); + + const std::string* error() const; + void set_error(const std::string_view* value_arg); + void set_error(std::string_view value_arg); + + const BiometricError* code() const; + void set_code(const BiometricError* value_arg); + void set_code(const BiometricError& value_arg); + + const std::string* algorithm() const; + void set_algorithm(const std::string_view* value_arg); + void set_algorithm(std::string_view value_arg); + + const int64_t* key_size() const; + void set_key_size(const int64_t* value_arg); + void set_key_size(int64_t value_arg); + + private: + static SignatureResult FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class BiometricSignatureApi; + friend class PigeonInternalCodecSerializer; + std::optional signature_; + std::optional> signature_bytes_; + std::optional public_key_; + std::optional error_; + std::optional code_; + std::optional algorithm_; + std::optional key_size_; +}; + + +// Generated class from Pigeon that represents data sent in messages. +class DecryptResult { + public: + // Constructs an object setting all non-nullable fields. + DecryptResult(); + + // Constructs an object setting all fields. + explicit DecryptResult( + const std::string* decrypted_data, + const std::string* error, + const BiometricError* code); + + const std::string* decrypted_data() const; + void set_decrypted_data(const std::string_view* value_arg); + void set_decrypted_data(std::string_view value_arg); + + const std::string* error() const; + void set_error(const std::string_view* value_arg); + void set_error(std::string_view value_arg); + + const BiometricError* code() const; + void set_code(const BiometricError* value_arg); + void set_code(const BiometricError& value_arg); + + private: + static DecryptResult FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class BiometricSignatureApi; + friend class PigeonInternalCodecSerializer; + std::optional decrypted_data_; + std::optional error_; + std::optional code_; +}; + + +// Detailed information about existing biometric keys. +// +// Generated class from Pigeon that represents data sent in messages. +class KeyInfo { + public: + // Constructs an object setting all non-nullable fields. + KeyInfo(); + + // Constructs an object setting all fields. + explicit KeyInfo( + const bool* exists, + const bool* is_valid, + const std::string* algorithm, + const int64_t* key_size, + const bool* is_hybrid_mode, + const std::string* public_key, + const std::string* decrypting_public_key, + const std::string* decrypting_algorithm, + const int64_t* decrypting_key_size); + + // Whether any biometric key exists on the device. + const bool* exists() const; + void set_exists(const bool* value_arg); + void set_exists(bool value_arg); + + // Whether the key is still valid (not invalidated by biometric changes). + // Only populated when `checkValidity: true` is passed. + const bool* is_valid() const; + void set_is_valid(const bool* value_arg); + void set_is_valid(bool value_arg); + + // The algorithm of the signing key (e.g., "RSA", "EC"). + const std::string* algorithm() const; + void set_algorithm(const std::string_view* value_arg); + void set_algorithm(std::string_view value_arg); + + // The key size in bits (e.g., 2048 for RSA, 256 for EC). + const int64_t* key_size() const; + void set_key_size(const int64_t* value_arg); + void set_key_size(int64_t value_arg); + + // Whether the key is in hybrid mode (separate signing and decryption keys). + const bool* is_hybrid_mode() const; + void set_is_hybrid_mode(const bool* value_arg); + void set_is_hybrid_mode(bool value_arg); + + // Signing key public key (formatted according to the requested format). + const std::string* public_key() const; + void set_public_key(const std::string_view* value_arg); + void set_public_key(std::string_view value_arg); + + // Decryption key public key for hybrid mode. + const std::string* decrypting_public_key() const; + void set_decrypting_public_key(const std::string_view* value_arg); + void set_decrypting_public_key(std::string_view value_arg); + + // Algorithm of the decryption key (hybrid mode only). + const std::string* decrypting_algorithm() const; + void set_decrypting_algorithm(const std::string_view* value_arg); + void set_decrypting_algorithm(std::string_view value_arg); + + // Key size of the decryption key in bits (hybrid mode only). + const int64_t* decrypting_key_size() const; + void set_decrypting_key_size(const int64_t* value_arg); + void set_decrypting_key_size(int64_t value_arg); + + private: + static KeyInfo FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class BiometricSignatureApi; + friend class PigeonInternalCodecSerializer; + std::optional exists_; + std::optional is_valid_; + std::optional algorithm_; + std::optional key_size_; + std::optional is_hybrid_mode_; + std::optional public_key_; + std::optional decrypting_public_key_; + std::optional decrypting_algorithm_; + std::optional decrypting_key_size_; +}; + + +// Configuration for key creation (all platforms). +// +// Fields are documented with which platform(s) they apply to. +// Windows ignores most fields as it only supports RSA with mandatory +// Windows Hello authentication. +// +// Generated class from Pigeon that represents data sent in messages. +class CreateKeysConfig { + public: + // Constructs an object setting all non-nullable fields. + CreateKeysConfig(); + + // Constructs an object setting all fields. + explicit CreateKeysConfig( + const SignatureType* signature_type, + const bool* enforce_biometric, + const bool* set_invalidated_by_biometric_enrollment, + const bool* use_device_credentials, + const bool* enable_decryption, + const std::string* prompt_subtitle, + const std::string* prompt_description, + const std::string* cancel_button_text); + + // [Android/iOS/macOS] The cryptographic algorithm to use. + // Windows only supports RSA and ignores this field. + const SignatureType* signature_type() const; + void set_signature_type(const SignatureType* value_arg); + void set_signature_type(const SignatureType& value_arg); + + // [Android/iOS/macOS] Whether to require biometric authentication + // during key creation. Windows always authenticates via Windows Hello. + const bool* enforce_biometric() const; + void set_enforce_biometric(const bool* value_arg); + void set_enforce_biometric(bool value_arg); + + // [Android/iOS/macOS] Whether to invalidate the key when new biometrics + // are enrolled. Not supported on Windows. + // + // **Security Note**: When `true`, keys become invalid if fingerprints/faces + // are added or removed, preventing unauthorized access if an attacker + // enrolls their own biometrics on a compromised device. + const bool* set_invalidated_by_biometric_enrollment() const; + void set_set_invalidated_by_biometric_enrollment(const bool* value_arg); + void set_set_invalidated_by_biometric_enrollment(bool value_arg); + + // [Android/iOS/macOS] Whether to allow device credentials (PIN/pattern/passcode) + // as fallback for biometric authentication. Not supported on Windows. + const bool* use_device_credentials() const; + void set_use_device_credentials(const bool* value_arg); + void set_use_device_credentials(bool value_arg); + + // [Android] Whether to enable decryption capability for the key. + // On iOS/macOS, decryption is always available with EC keys. + const bool* enable_decryption() const; + void set_enable_decryption(const bool* value_arg); + void set_enable_decryption(bool value_arg); + + // [Android] Subtitle text for the biometric prompt. + const std::string* prompt_subtitle() const; + void set_prompt_subtitle(const std::string_view* value_arg); + void set_prompt_subtitle(std::string_view value_arg); + + // [Android] Description text for the biometric prompt. + const std::string* prompt_description() const; + void set_prompt_description(const std::string_view* value_arg); + void set_prompt_description(std::string_view value_arg); + + // [Android] Text for the cancel button in the biometric prompt. + const std::string* cancel_button_text() const; + void set_cancel_button_text(const std::string_view* value_arg); + void set_cancel_button_text(std::string_view value_arg); + + private: + static CreateKeysConfig FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class BiometricSignatureApi; + friend class PigeonInternalCodecSerializer; + std::optional signature_type_; + std::optional enforce_biometric_; + std::optional set_invalidated_by_biometric_enrollment_; + std::optional use_device_credentials_; + std::optional enable_decryption_; + std::optional prompt_subtitle_; + std::optional prompt_description_; + std::optional cancel_button_text_; +}; + + +// Configuration for signature creation (all platforms). +// +// Fields are documented with which platform(s) they apply to. +// +// Generated class from Pigeon that represents data sent in messages. +class CreateSignatureConfig { + public: + // Constructs an object setting all non-nullable fields. + CreateSignatureConfig(); + + // Constructs an object setting all fields. + explicit CreateSignatureConfig( + const std::string* prompt_subtitle, + const std::string* prompt_description, + const std::string* cancel_button_text, + const bool* allow_device_credentials, + const bool* should_migrate); + + // [Android] Subtitle text for the biometric prompt. + const std::string* prompt_subtitle() const; + void set_prompt_subtitle(const std::string_view* value_arg); + void set_prompt_subtitle(std::string_view value_arg); + + // [Android] Description text for the biometric prompt. + const std::string* prompt_description() const; + void set_prompt_description(const std::string_view* value_arg); + void set_prompt_description(std::string_view value_arg); + + // [Android] Text for the cancel button in the biometric prompt. + const std::string* cancel_button_text() const; + void set_cancel_button_text(const std::string_view* value_arg); + void set_cancel_button_text(std::string_view value_arg); + + // [Android] Whether to allow device credentials (PIN/pattern) as fallback. + const bool* allow_device_credentials() const; + void set_allow_device_credentials(const bool* value_arg); + void set_allow_device_credentials(bool value_arg); + + // [iOS] Whether to migrate from legacy keychain storage. + const bool* should_migrate() const; + void set_should_migrate(const bool* value_arg); + void set_should_migrate(bool value_arg); + + private: + static CreateSignatureConfig FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class BiometricSignatureApi; + friend class PigeonInternalCodecSerializer; + std::optional prompt_subtitle_; + std::optional prompt_description_; + std::optional cancel_button_text_; + std::optional allow_device_credentials_; + std::optional should_migrate_; +}; + + +// Configuration for decryption (all platforms). +// +// Fields are documented with which platform(s) they apply to. +// Note: Decryption is not supported on Windows. +// +// Generated class from Pigeon that represents data sent in messages. +class DecryptConfig { + public: + // Constructs an object setting all non-nullable fields. + DecryptConfig(); + + // Constructs an object setting all fields. + explicit DecryptConfig( + const std::string* prompt_subtitle, + const std::string* prompt_description, + const std::string* cancel_button_text, + const bool* allow_device_credentials, + const bool* should_migrate); + + // [Android] Subtitle text for the biometric prompt. + const std::string* prompt_subtitle() const; + void set_prompt_subtitle(const std::string_view* value_arg); + void set_prompt_subtitle(std::string_view value_arg); + + // [Android] Description text for the biometric prompt. + const std::string* prompt_description() const; + void set_prompt_description(const std::string_view* value_arg); + void set_prompt_description(std::string_view value_arg); + + // [Android] Text for the cancel button in the biometric prompt. + const std::string* cancel_button_text() const; + void set_cancel_button_text(const std::string_view* value_arg); + void set_cancel_button_text(std::string_view value_arg); + + // [Android] Whether to allow device credentials (PIN/pattern) as fallback. + const bool* allow_device_credentials() const; + void set_allow_device_credentials(const bool* value_arg); + void set_allow_device_credentials(bool value_arg); + + // [iOS] Whether to migrate from legacy keychain storage. + const bool* should_migrate() const; + void set_should_migrate(const bool* value_arg); + void set_should_migrate(bool value_arg); + + private: + static DecryptConfig FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class BiometricSignatureApi; + friend class PigeonInternalCodecSerializer; + std::optional prompt_subtitle_; + std::optional prompt_description_; + std::optional cancel_button_text_; + std::optional allow_device_credentials_; + std::optional should_migrate_; +}; + + +class PigeonInternalCodecSerializer : public flutter::StandardCodecSerializer { + public: + PigeonInternalCodecSerializer(); + inline static PigeonInternalCodecSerializer& GetInstance() { + static PigeonInternalCodecSerializer sInstance; + return sInstance; + } + + void WriteValue( + const flutter::EncodableValue& value, + flutter::ByteStreamWriter* stream) const override; + protected: + flutter::EncodableValue ReadValueOfType( + uint8_t type, + flutter::ByteStreamReader* stream) const override; +}; + +// Generated interface from Pigeon that represents a handler of messages from Flutter. +class BiometricSignatureApi { + public: + BiometricSignatureApi(const BiometricSignatureApi&) = delete; + BiometricSignatureApi& operator=(const BiometricSignatureApi&) = delete; + virtual ~BiometricSignatureApi() {} + // Checks if biometric authentication is available. + virtual void BiometricAuthAvailable(std::function reply)> result) = 0; + // Creates a new key pair. + // + // [config] contains platform-specific options. See [CreateKeysConfig]. + // [keyFormat] specifies the output format for the public key. + // [promptMessage] is the message shown to the user during authentication. + virtual void CreateKeys( + const CreateKeysConfig* config, + const KeyFormat& key_format, + const std::string* prompt_message, + std::function reply)> result) = 0; + // Creates a signature. + // + // [payload] is the data to sign. + // [config] contains platform-specific options. See [CreateSignatureConfig]. + // [signatureFormat] specifies the output format for the signature. + // [keyFormat] specifies the output format for the public key. + // [promptMessage] is the message shown to the user during authentication. + virtual void CreateSignature( + const std::string& payload, + const CreateSignatureConfig* config, + const SignatureFormat& signature_format, + const KeyFormat& key_format, + const std::string* prompt_message, + std::function reply)> result) = 0; + // Decrypts data. + // + // Note: Not supported on Windows. + // [payload] is the encrypted data. + // [payloadFormat] specifies the format of the encrypted data. + // [config] contains platform-specific options. See [DecryptConfig]. + // [promptMessage] is the message shown to the user during authentication. + virtual void Decrypt( + const std::string& payload, + const PayloadFormat& payload_format, + const DecryptConfig* config, + const std::string* prompt_message, + std::function reply)> result) = 0; + // Deletes keys. + virtual void DeleteKeys(std::function reply)> result) = 0; + // Gets detailed information about existing biometric keys. + // + // Returns key metadata including algorithm, size, validity, and public keys. + virtual void GetKeyInfo( + bool check_validity, + const KeyFormat& key_format, + std::function reply)> result) = 0; + + // The codec used by BiometricSignatureApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `BiometricSignatureApi` to handle messages through the `binary_messenger`. + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + BiometricSignatureApi* api); + static void SetUp( + flutter::BinaryMessenger* binary_messenger, + BiometricSignatureApi* api, + const std::string& message_channel_suffix); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + protected: + BiometricSignatureApi() = default; +}; +} // namespace biometric_signature +#endif // PIGEON_MESSAGES_G_H_