feat: Crypto and Secure Enclave implementation for Android and iOS#43
feat: Crypto and Secure Enclave implementation for Android and iOS#43FreakyAli wants to merge 36 commits into
Conversation
…eakyAli/Plugin.Maui.Biometric into r-9/feat/cryto-secure-enclave
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a secure biometric crypto surface (ISecureBiometric and SecureBiometricService with platform partials), Android KeyStore and BiometricPrompt helpers/callbacks, many crypto enums/models/validation helpers, refactors BiometricService/IBiometric, sample UI pages, project/nullable csproj updates, and multiple unit tests. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor App
participant API as SecureBiometricAuthenticationService.Default
participant Impl as SecureBiometricService (Android)
participant KS as AndroidKeyStoreHelpers
participant Helper as BiometricPromptHelpers
participant UI as Activity / IExecutor
participant BP as BiometricPrompt
participant CB as SecureAuthCallback
App->>API: EncryptAsync(request, token)
API->>Impl: EncryptAsync(request, token)
Impl->>KS: Ensure key / map algorithm & purpose
Impl->>Helper: ProcessCryptoAsync(request, EncryptMode, token)
Helper->>Helper: Init Cipher with transformation and key
Helper->>UI: Resolve Activity and main IExecutor
Helper->>BP: Authenticate with CryptoObject + PromptInfo (attach CB)
BP-->>CB: OnAuthenticationSucceeded / OnAuthenticationFailed / OnAuthenticationError
CB-->>Helper: Complete TaskCompletionSource with SecureAuthenticationResponse
Helper-->>Impl: Return SecureAuthenticationResponse
Impl-->>API: Return SecureAuthenticationResponse
API-->>App: SecureAuthenticationResponse
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Plugin.Maui.Biometric.csproj (1)
83-89: Update MAUI and AndroidX package versions to latest stable releases
- Change Microsoft.Maui.Controls to 9.0.100 (latest .NET 9 patch, August 13 2025) cite12
- Change Xamarin.AndroidX.Biometric to 1.1.0.30 (latest stable, supports MonoAndroid 9.0/API 28+) cite12
- Change Xamarin.AndroidX.Collection.Ktx to 1.5.0.3 (latest stable compatible release) cite12
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.net.cs (1)
1-20: Guard BiometricService.net.cs fallback behind platform conditionals
Platform-specific implementations in.android.csand.apple.cscollide with the un-guarded fallback in.net.cs(andGetIsPlatformSupported()always returns false). Enclose the entire fallback class in:+#if !ANDROID && !IOS && !WINDOWS && !MACCATALYST namespace Plugin.Maui.Biometric; internal partial class BiometricService : IBiometric { public partial Task<AuthenticationResponse> AuthenticateAsync(AuthenticationRequest request, CancellationToken token) { - throw new NotImplementedException(); + throw new System.PlatformNotSupportedException("Biometric authentication is not supported on this target."); } public partial Task<BiometricHwStatus> GetAuthenticationStatusAsync(AuthenticatorStrength authStrength) { - throw new NotImplementedException(); + throw new System.PlatformNotSupportedException("Biometric authentication is not supported on this target."); } public partial Task<BiometricType[]> GetEnrolledBiometricTypesAsync() { - throw new NotImplementedException(); + throw new System.PlatformNotSupportedException("Biometric authentication is not supported on this target."); } private static partial bool GetIsPlatformSupported() => false; } +#endifPlugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs (3)
107-113: Dispose BiometricPrompt to avoid leaks.
BiometricPromptwraps native resources; dispose it.- var biometricPrompt = new BiometricPrompt(activity, executor, authCallback); + using var biometricPrompt = new BiometricPrompt(activity, executor, authCallback);
91-99: Ensure NegativeButtonText is non-null when password auth is disabled.
SetNegativeButtonTextthrows on null/empty; provide a safe fallback.- allInfo.SetNegativeButtonText(request.NegativeText); + allInfo.SetNegativeButtonText(string.IsNullOrWhiteSpace(request.NegativeText) ? "Cancel" : request.NegativeText);
135-154: Don’t gate hardware detection on enrollment.
GetEnrolledBiometricTypesAsyncshould still report available hardware when none is enrolled. Today it returnsNonein that case.- if (canAuthenticate == BiometricManager.BiometricSuccess) + if (canAuthenticate == BiometricManager.BiometricSuccess + || canAuthenticate == BiometricManager.BiometricErrorNoneEnrolled) { var packageManager = activity.PackageManager;(Optional) Consider handling iris if relevant OEMs matter.
🧹 Nitpick comments (34)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Padding.cs (1)
3-9: Clarify usage and discourage legacy RSA padding
- Consider XML-docs per member to prevent misuse (e.g., Pkcs7 for AES-CBC, Oaep for RSA, avoid mixing).
- Optionally mark
Pkcs1as obsolete for RSA encryption in new code; preferOaep.public enum Padding { None, Pkcs7, - Pkcs1, + [Obsolete("Prefer OAEP for RSA encryption; keep PKCS#1 v1.5 only for legacy interop.")] + Pkcs1, Oaep }Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricAuthenticationService.cs (1)
5-6: Use ExecutionAndPublication for Lazy singleton
PublicationOnlycan create multiple instances under race; typical singleton semantics useExecutionAndPublication.- private static readonly Lazy<IBiometric> defaultImpl = - new(() => new BiometricService(), LazyThreadSafetyMode.PublicationOnly); + private static readonly Lazy<IBiometric> defaultImpl = + new(() => new BiometricService(), LazyThreadSafetyMode.ExecutionAndPublication);Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/CryptoOperation.cs (1)
3-11: Add None/All flags for ergonomic bitwise useHelps represent “no ops” and a convenient aggregate.
[Flags] public enum CryptoOperation { - Encrypt = 1 << 0, + None = 0, + Encrypt = 1 << 0, Decrypt = 1 << 1, Sign = 1 << 2, Verify = 1 << 3, - Mac = 1 << 4 + Mac = 1 << 4, + All = Encrypt | Decrypt | Sign | Verify | Mac }Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/AuthCallback.android.cs (1)
38-43: Confirm intent: log-only on failed attempt (no completion)Not setting
ResponseonOnAuthenticationFailedkeeps the prompt open for retries (good), but confirm all callers expect completion only on success/error/cancel. If a UI flow awaits a result per attempt, you may need a “retry” signal.Optionally standardize logging (Android.Util.Log) and include an event tag to integrate with existing diagnostics.
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
3-9: Prefer immutable result with factoriesReduce accidental mutation and improve readability.
- public sealed class KeyOperationResult - { - public bool Success { get; set; } - public string? ErrorMessage { get; set; } - public string? AdditionalInfo { get; set; } - internal string? SecurityLevelName { get; set; } - } +public sealed record KeyOperationResult +{ + public bool Success { get; init; } + public string? ErrorMessage { get; init; } + public string? AdditionalInfo { get; init; } + internal string? SecurityLevelName { get; init; } + + public static KeyOperationResult Ok(string? info = null, string? level = null) + => new() { Success = true, AdditionalInfo = info, SecurityLevelName = level }; + + public static KeyOperationResult Fail(string message) + => new() { Success = false, ErrorMessage = message }; +}Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs (1)
8-12: Avoid suppressing nullability; require non-null UI stringsUse required members to keep strong null-safety.
-#nullable disable - public string Title { get; set; } - public string Subtitle { get; set; } - public string NegativeText { get; set; } -#nullable restore + public required string Title { get; init; } + public required string Subtitle { get; init; } + public required string NegativeText { get; init; }Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (1)
4-7: Make the response immutable; hide ctor and prevent IV mutation post-creation.Immutability here avoids accidental tampering of sensitive crypto state after success.
public sealed class SecureAuthenticationResponse { + private SecureAuthenticationResponse() { } public bool WasSuccessful { get; private set; } public byte[]? OutputData { get; private set; } - public byte[]? IV { get; set; } + public byte[]? IV { get; private set; } public string? ErrorMessage { get; private set; } public static SecureAuthenticationResponse Success(byte[] outputData, byte[]? iv = null) { return new SecureAuthenticationResponse { WasSuccessful = true, OutputData = (byte[])outputData.Clone(), IV = iv is null ? null : (byte[])iv.Clone() }; }Also applies to: 10-18
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/KeyAlgorithm.cs (1)
3-8: Consistent acronym casing: consider EC instead of Ec.Minor naming nit to align with AES/RSA and BCL acronym style.
public enum KeyAlgorithm { Aes, Rsa, - Ec + EC }Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationRequest.cs (1)
3-5: Style nit: add space before base type.Keeps consistency with common C# formatting.
-public sealed class AuthenticationRequest: BaseAuthenticationRequest +public sealed class AuthenticationRequest : BaseAuthenticationRequest { }Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Digest.cs (1)
5-11: Discourage SHA-1 usage.Mark SHA-1 obsolete or document that it's unsupported for strong auth.
public enum Digest { None, - Sha1, + [System.Obsolete("SHA-1 is cryptographically broken and should not be used for new keys or operations.")] + Sha1, Sha224, Sha256, Sha384, Sha512 }Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationResponse.cs (1)
3-8: Unify error property naming with SecureAuthenticationResponse.Use ErrorMessage for consistency across models; optionally keep ErrorMsg as an obsolete alias for back-compat.
public sealed class AuthenticationResponse { public BiometricResponseStatus Status { get; set; } public AuthenticationType AuthenticationType { get; set; } - public string? ErrorMsg { get; set; } + public string? ErrorMessage { get; set; } }Optionally add this alias elsewhere in the class to preserve compatibility:
[Obsolete("Use ErrorMessage. This alias will be removed in a future release.")] public string? ErrorMsg { get => ErrorMessage; set => ErrorMessage = value; }Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricAuthenticationService.cs (1)
5-6: Prefer default Lazy thread-safety to avoid redundant instantiations.ExecutionAndPublication is safer than PublicationOnly here and avoids duplicate construction.
- private static readonly Lazy<ISecureBiometric> defaultImpl = - new(() => new SecureBiometricService(), LazyThreadSafetyMode.PublicationOnly); + private static readonly Lazy<ISecureBiometric> defaultImpl = + new(() => new SecureBiometricService());Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (2)
33-33: Indentation nit.- public Padding Padding { get; set; } = Padding.None; + public Padding Padding { get; set; } = Padding.None;
35-38: Digest is only meaningful for Sign/Verify.Consider validating that
Digestis ignored or prohibited unlessOperationincludes Sign/Verify.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs (2)
71-79: Minor: theis not IExecutortype check is redundant.
ContextCompat.GetMainExecutor(Activity)already returnsIExecutor. A null-check suffices. Not a blocker.- var activityExecutor = ContextCompat.GetMainExecutor(activity); - if (activityExecutor is not IExecutor executor) + var activityExecutor = ContextCompat.GetMainExecutor(activity); + if (activityExecutor is null) { // Executor creation fails return new AuthenticationResponse { Status = BiometricResponseStatus.Failure, ErrorMsg = BiometricPromptHelpers.ExecutorErrorMsg }; } + var executor = activityExecutor;
118-122: Avoid leaking stack traces in user-facing errors.Return message; log stack trace separately.
- ErrorMsg = ex.Message + ex.StackTrace + ErrorMsg = ex.MessagePlugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (2)
7-9: Default to AES-GCM for symmetric crypto to reduce surprises.
BlockMode = Nonemaps to GCM via helpers, which is implicit. Prefer making it explicit (and consistent with CryptoKeyOptions).- public BlockMode BlockMode { get; set; } = BlockMode.None; + public BlockMode BlockMode { get; set; } = BlockMode.Gcm;
10-10: Document IV usage.IV is essential for AES-GCM decrypt; ensure callers persist/provide it.
- public byte[]? IV { get; set; } + /// <summary>Initialization Vector for symmetric ciphers (e.g., AES-GCM). Required for decryption.</summary> + public byte[]? IV { get; set; }Please confirm
IVis actually used in the Android crypto path (cipher init for decrypt) and returned after encrypt. I can wire it throughProcessCryptoAsyncif needed.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs (3)
23-25: Remove redundant null-conditional — cipher is already checkedMinor cleanup; avoids misleading null-flow.
- var cipherData = cipher?.DoFinal(Request.InputData); + var cipherData = cipher.DoFinal(Request.InputData);
12-38: Avoid heavy crypto on the main executorDoFinal can be non-trivial for larger payloads. Because the prompt is created with ContextCompat.GetMainExecutor, these callbacks run on the UI thread. Prefer a background executor for the biometric prompt in the crypto flow, or offload DoFinal to a background thread.
- Option A: Construct BiometricPrompt with a background Executor (e.g., Java.Util.Concurrent.Executors.NewSingleThreadExecutor()) for the crypto path only.
- Option B: Keep main executor for UI but queue DoFinal onto a background Task and complete the TCS from there.
8-11: Add RunContinuationsAsynchronously to TaskCompletionSource instantiationIn Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (around line 118), update the TCS initialization:
- Response = new TaskCompletionSource<SecureAuthenticationResponse>() + Response = new TaskCompletionSource<SecureAuthenticationResponse>(TaskCreationOptions.RunContinuationsAsynchronously)This ensures continuations don’t run inline on the BiometricPrompt callback thread, avoiding potential deadlocks or UI jank.
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/ISecureBiometric.cs (2)
11-12: Consider adding CancellationToken to key management methods for consistencyKey creation/deletion/exists can be slow (I/O, system dialogs). Offer cancellation symmetry.
-Task<KeyOperationResult> CreateKeyAsync(string keyId, CryptoKeyOptions options); +Task<KeyOperationResult> CreateKeyAsync(string keyId, CryptoKeyOptions options, CancellationToken token = default); -Task<KeyOperationResult> DeleteKeyAsync(string keyId); +Task<KeyOperationResult> DeleteKeyAsync(string keyId, CancellationToken token = default); -Task<KeyOperationResult> KeyExistsAsync(string keyId); +Task<KeyOperationResult> KeyExistsAsync(string keyId, CancellationToken token = default);Also applies to: 19-20, 27-28
45-59: Remove commented-out API or add a tracked TODOCommented blocks drift and confuse public surface area.
- Remove commented methods, or
- Add a TODO with issue reference and expected design (algorithms, key purposes, return types).
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (1)
27-39: Consider adding length validation forInputDataWhile checking for null or empty
InputDatais good, consider adding a maximum size check to prevent potential memory issues with extremely large inputs. Many cryptographic operations have practical size limits.private static SecureAuthenticationResponse? ValidateRequest(SecureAuthenticationRequest request) { if (string.IsNullOrWhiteSpace(request.KeyId)) return SecureAuthenticationResponse.Failure("Key ID cannot be null or empty"); if (string.IsNullOrWhiteSpace(request.Transformation)) return SecureAuthenticationResponse.Failure("Transformation cannot be null or empty"); if (request.InputData == null || request.InputData.Length == 0) return SecureAuthenticationResponse.Failure("Input data cannot be null or empty"); + + // Reasonable upper limit for crypto operations (e.g., 1MB) + const int MaxInputSize = 1024 * 1024; + if (request.InputData.Length > MaxInputSize) + return SecureAuthenticationResponse.Failure($"Input data exceeds maximum size of {MaxInputSize} bytes"); return null; }Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs (1)
3-39: Consider adding TODO comments for future Windows implementationSince this is a stub implementation, adding TODO comments would help track what needs to be implemented for Windows platform support.
Would you like me to generate TODO comments outlining the Windows-specific implementation requirements using Windows Hello APIs for biometric authentication and DPAPI/CNG for cryptographic operations?
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs (1)
2-2: Make accessibility explicit to match other partials.Minor consistency nit: declare this part as internal like the others. Functionality unchanged.
-partial class SecureBiometricService +internal partial class SecureBiometricServicePlugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (3)
270-275: Consistency: avoid unnecessary async/await.Match EncryptAsync style by returning the task directly.
- public partial async Task<SecureAuthenticationResponse> DecryptAsync(SecureAuthenticationRequest request, CancellationToken token) - => await BiometricPromptHelpers.ProcessCryptoAsync(request, CipherMode.DecryptMode, token); + public partial Task<SecureAuthenticationResponse> DecryptAsync(SecureAuthenticationRequest request, CancellationToken token) + => BiometricPromptHelpers.ProcessCryptoAsync(request, CipherMode.DecryptMode, token);
229-234: Clarify KeyExistsAsync semantics (Success vs existence).Returning Success = exists blurs “operation succeeded” with “resource state.” Consider Success = true when the check succeeds, and convey existence via AdditionalInfo (or add a dedicated Exists flag).
276-285: Sign/Verify are placeholders; confirm roadmap or hide behind feature flags.These currently always fail. If shipping the API now, either implement minimal RSA/EC sign/verify or clearly mark as not supported on Android until a later release.
I can wire RSA/EC KeyPairGenerator + Signature (SHA256withRSA/ECDSA) and integrate prompt-gated PrivateKey usage if you want.
Also applies to: 287-299
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (5)
36-42: Consider adding documentation for biometric authentication behaviorThe comment explains the limitation well, but it would be helpful to document this behavior more prominently, as it's a significant security consideration. Users expecting biometric-only authentication on Android 10 and below will actually get biometric OR device credential authentication.
Consider adding XML documentation to the
TryCreateKeyWithSecurityLevelmethod to explain this limitation:+ /// <summary> + /// Creates a key in Android KeyStore with the specified security level. + /// </summary> + /// <remarks> + /// On Android 10 (API 29) and below, user authentication requirement accepts BOTH biometric + /// AND device credential (PIN/password/pattern). There's no way to restrict to biometric-only + /// in the KeyStore on these API levels. + /// </remarks> internal static KeyOperationResult TryCreateKeyWithSecurityLevel
72-72: Consider validating the generated keyAfter generating the key, it would be good to validate that it was successfully created and stored in the KeyStore before declaring success.
var secretKey = keyGen.GenerateKey(); + + // Validate key was created successfully + if (secretKey == null) + { + return new KeyOperationResult + { + Success = false, + ErrorMessage = "Failed to generate key - null key returned" + }; + }
88-104: Improve StrongBox fallback error handlingThe error handling for StrongBox could be more explicit about when to retry without StrongBox.
catch (ProviderException ex) when (ex.Message?.Contains("StrongBox") == true) { if (preferStrongBox) { // StrongBox failed, caller should retry without it return new KeyOperationResult { Success = false, ErrorMessage = $"StrongBox unavailable: {ex.Message}", + AdditionalInfo = "Retry without StrongBox preference" }; }
141-147: Add defensive handling for undefined SecurityLevel valuesThe switch expression should explicitly handle all possible SecurityLevel values rather than defaulting unknown values to "Software".
return keyInfo.SecurityLevel switch { (int)KeyStoreSecurityLevel.Strongbox => "StrongBox", (int)KeyStoreSecurityLevel.TrustedEnvironment => "TEE", (int)KeyStoreSecurityLevel.Software => "Software", - _ => "Software" + _ => $"Unknown ({keyInfo.SecurityLevel})" };
190-193: Consider validating transformation string componentsThe transformation string construction should validate that the components are not null or empty to avoid malformed transformation strings.
internal static string MapTransformation(string keyAlgorithm, string blockMode, string encryptionPadding) { + if (string.IsNullOrEmpty(keyAlgorithm) || string.IsNullOrEmpty(blockMode) || string.IsNullOrEmpty(encryptionPadding)) + throw new ArgumentException("Transformation components cannot be null or empty"); + return $"{keyAlgorithm}/{blockMode}/{encryptionPadding}"; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (30)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/AuthCallback.android.cs(2 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricAuthenticationService.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs(5 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.net.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/IBiometric.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/BiometricService.cs(0 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/ISecureBiometric.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricAuthenticationService.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BlockMode.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/CryptoOperation.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Digest.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/KeyAlgorithm.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Padding.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationRequest.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationResponse.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Plugin.Maui.Biometric.csproj(3 hunks)
💤 Files with no reviewable changes (1)
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/BiometricService.cs
🧰 Additional context used
🧬 Code graph analysis (17)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (7)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs (1)
SecureBiometricService(3-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
KeyOperationResult(3-9)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (3)
KeyOperationResult(11-129)AndroidKeyStoreHelpers(7-244)MapKeyAlgorithm(179-188)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (1)
CryptoKeyOptions(3-39)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (2)
SecureAuthenticationResponse(27-39)BiometricPromptHelpers(12-148)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (1)
SecureAuthenticationResponse(27-39)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricAuthenticationService.cs (5)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (1)
SecureBiometricService(6-300)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (1)
SecureBiometricService(2-38)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs (1)
SecureBiometricService(3-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs (1)
SecureBiometricService(2-38)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs (1)
SecureBiometricService(3-39)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs (4)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (3)
BiometricPrompt(75-97)SecureAuthenticationResponse(27-39)Cipher(55-61)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/AuthCallback.android.cs (3)
OnAuthenticationSucceeded(11-26)OnAuthenticationError(28-36)OnAuthenticationFailed(38-43)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
KeyOperationResult(11-129)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationRequest.cs (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs (1)
BaseAuthenticationRequest(4-19)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs (2)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (1)
BiometricPromptHelpers(12-148)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationResponse.cs (1)
AuthenticationResponse(3-8)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (2)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs (1)
BaseAuthenticationRequest(4-19)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (5)
AndroidKeyStoreHelpers(7-244)MapTransformation(190-193)MapKeyAlgorithm(179-188)MapBlockMode(214-222)MapPadding(224-232)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/AuthCallback.android.cs (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs (1)
OnAuthenticationFailed(46-50)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs (8)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (8)
SecureBiometricService(6-300)Task(8-125)Task(127-202)Task(204-268)Task(270-271)Task(273-274)Task(276-285)Task(287-299)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (8)
SecureBiometricService(2-38)Task(4-7)Task(9-12)Task(14-17)Task(19-22)Task(24-27)Task(29-32)Task(34-37)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs (3)
SecureBiometricService(2-38)Task(4-7)Task(9-12)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs (1)
SecureBiometricService(3-39)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
KeyOperationResult(3-9)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
KeyOperationResult(11-129)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (3)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
KeyOperationResult(3-9)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (1)
CryptoKeyOptions(3-39)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (1)
IKey(41-53)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs (9)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (8)
SecureBiometricService(6-300)Task(8-125)Task(127-202)Task(204-268)Task(270-271)Task(273-274)Task(276-285)Task(287-299)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (8)
SecureBiometricService(2-38)Task(4-7)Task(9-12)Task(14-17)Task(19-22)Task(24-27)Task(29-32)Task(34-37)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs (1)
SecureBiometricService(3-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs (3)
SecureBiometricService(2-38)Task(4-7)Task(9-12)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
KeyOperationResult(3-9)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
KeyOperationResult(11-129)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (1)
CryptoKeyOptions(3-39)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs (9)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (8)
SecureBiometricService(6-300)Task(8-125)Task(127-202)Task(204-268)Task(270-271)Task(273-274)Task(276-285)Task(287-299)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (8)
SecureBiometricService(2-38)Task(4-7)Task(9-12)Task(14-17)Task(19-22)Task(24-27)Task(29-32)Task(34-37)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs (1)
SecureBiometricService(3-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs (1)
SecureBiometricService(3-39)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
KeyOperationResult(3-9)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
KeyOperationResult(11-129)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (1)
CryptoKeyOptions(3-39)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (8)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (8)
SecureBiometricService(6-300)Task(8-125)Task(127-202)Task(204-268)Task(270-271)Task(273-274)Task(276-285)Task(287-299)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs (1)
SecureBiometricService(3-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs (8)
SecureBiometricService(2-38)Task(4-7)Task(9-12)Task(14-17)Task(19-22)Task(24-27)Task(29-32)Task(34-37)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs (1)
SecureBiometricService(3-39)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
KeyOperationResult(3-9)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (1)
CryptoKeyOptions(3-39)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (5)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
AndroidKeyStoreHelpers(7-244)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (7)
Task(8-125)Task(127-202)Task(204-268)Task(270-271)Task(273-274)Task(276-285)Task(287-299)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs (1)
SecureAuthCallback(6-51)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs (4)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs (5)
BiometricService(11-166)GetIsPlatformSupported(165-165)Task(13-55)Task(57-124)Task(126-163)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.net.cs (2)
BiometricService(2-20)GetIsPlatformSupported(19-19)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.apple.cs (2)
BiometricService(6-99)GetIsPlatformSupported(98-98)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.windows.cs (2)
BiometricService(7-76)GetIsPlatformSupported(75-75)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/ISecureBiometric.cs (5)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
KeyOperationResult(3-9)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
KeyOperationResult(11-129)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (1)
CryptoKeyOptions(3-39)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build-plugin-ci
- GitHub Check: build-plugin-ci
🔇 Additional comments (14)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Plugin.Maui.Biometric.csproj (1)
76-80: Confirm .net.cs exclusion intentThis excludes all
*.net.csexcept for exactnet9.0. Verify no shared stubs are needed bynet9.0-android/ios/maccatalyst.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (1)
15-16: Good defensive copies of sensitive buffers.Cloning OutputData and IV prevents caller-side mutation bugs. Keep this pattern.
Also applies to: 20-27
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (2)
5-9: LGTM on API shape and defaults for AES.The class is a clear container; defaults make sense for AES.
28-33: Clarify defaults for non-AES algorithms.
BlockMode = GcmandPadding = Noneare fine for AES-GCM, but will be incorrect for RSA/EC. Either document that these are AES-oriented defaults or compute algorithm-aware defaults at use sites (e.g., default to ECB for RSA).Do you want me to update the Android transformation builder to pick ECB automatically for RSA if
BlockMode == None?Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs (5)
7-7: Import for IExecutor is correct.
33-36: Null-safe feature checks are good.This avoids NREs on odd OEM PackageManager implementations.
68-70: Good: unified error messaging via BiometricPromptHelpers.
112-112: Nice: ConfigureAwait(false) on TCS await.
141-144: Good null-safety on PackageManager feature checks.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs (1)
3-16: LGTM — clean partial facade with platform gatingShape and partial signatures look good; default parameter on GetAuthenticationStatusAsync is fine with per-platform implementations.
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (1)
109-129: Inconsistent resource disposal patternsThe code uses
usingdeclarations for some resources (key,cipher,biometricPrompt,cryptoObject) but not consistently. Additionally, thekeyobject retrieved from the keystore should be carefully managed as it represents a sensitive cryptographic resource.Consider using a more consistent disposal pattern and ensuring all resources are properly disposed:
try { - using var key = GetKeyFromStore(request.KeyId); - using var cipher = InitCipher(request.Transformation, mode, key); + IKey? key = null; + Cipher? cipher = null; + BiometricPrompt? biometricPrompt = null; + BiometricPrompt.CryptoObject? cryptoObject = null; + + try + { + key = GetKeyFromStore(request.KeyId); + cipher = InitCipher(request.Transformation, mode, key, request.IV); - var (activity, executor) = GetActivityAndExecutor(); - var promptInfo = BuildPromptInfo(request); - var authCallback = new SecureAuthCallback - { - Request = request, - Response = new TaskCompletionSource<SecureAuthenticationResponse>() - }; + var (activity, executor) = GetActivityAndExecutor(); + var promptInfo = BuildPromptInfo(request); + var authCallback = new SecureAuthCallback + { + Request = request, + Response = new TaskCompletionSource<SecureAuthenticationResponse>() + }; - using var biometricPrompt = new BiometricPrompt(activity, executor, authCallback); - using var cryptoObject = new BiometricPrompt.CryptoObject(cipher); + biometricPrompt = new BiometricPrompt(activity, executor, authCallback); + cryptoObject = new BiometricPrompt.CryptoObject(cipher); - using (token.Register(() => biometricPrompt.CancelAuthentication())) - { - biometricPrompt.Authenticate(promptInfo, cryptoObject); - return await authCallback.Response.Task; + using (token.Register(() => biometricPrompt.CancelAuthentication())) + { + biometricPrompt.Authenticate(promptInfo, cryptoObject); + return await authCallback.Response.Task; + } + } + finally + { + cryptoObject?.Dispose(); + biometricPrompt?.Dispose(); + cipher?.Dispose(); + (key as IDisposable)?.Dispose(); } }Likely an incorrect or invalid review comment.
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs (1)
3-25: LGTM! Well-structured partial class implementationThe partial class design pattern is appropriately used here to enable platform-specific implementations while maintaining a clean public API surface through the
ISecureBiometricinterface. The CA1822 suppressions are correctly justified.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs (1)
4-7: Fallback throws are appropriate here.Throwing NotImplementedException for the net fallback is correct and avoids silent failures at runtime.
Also applies to: 9-12, 14-17, 19-22, 24-27, 29-32, 34-37
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
7-244: Well-structured Android KeyStore integrationThe implementation provides comprehensive Android KeyStore integration with good error handling, security level detection, and proper mapping of cryptographic parameters. The StrongBox fallback mechanism and version-specific handling are particularly well done.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs (1)
8-15: Align default parameter on the interface
IBiometric.AuthenticateAsync still declaresCancellationToken tokenwithout a default—add= defaultthere as well (or confirm you’re intentionally omitting it so callers via the interface remain explicit).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs
🧰 Additional context used
🧬 Code graph analysis (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs (4)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs (5)
BiometricService(11-166)GetIsPlatformSupported(165-165)Task(13-55)Task(57-124)Task(126-163)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.net.cs (2)
BiometricService(2-20)GetIsPlatformSupported(19-19)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.apple.cs (2)
BiometricService(6-99)GetIsPlatformSupported(98-98)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.windows.cs (2)
BiometricService(7-76)GetIsPlatformSupported(75-75)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-plugin-ci
🔇 Additional comments (2)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs (2)
1-4: Good façade shape and partial split.Internal sealed partial + interface-based surface looks fine and aligns with platform-specific implementations.
17-17: No missing implementations for GetIsPlatformSupported. All target TFMs—.net,.android,.windows, and.apple—provide the required partial definition, so linking will succeed.
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (3)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs (1)
47-52: Good: OnAuthenticationFailed no longer completes the TCSMatches Android guidance; avoids racing terminal callbacks.
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (2)
50-54: Catch Java.IO.IOException, not System.IO.IOExceptionKeyStore.Load throws Java.IO.IOException; current catch may miss it.
- catch (IOException ex) + catch (Java.IO.IOException ex) { return Task.FromResult(KeyOperationResult.Failure ($"Storage I/O error: {ex.GetFullMessage()}")); }
103-107: Same: switch to Java.IO.IOException here as wellConsistent handling.
- catch (IOException ex) + catch (Java.IO.IOException ex) { return Task.FromResult(KeyOperationResult.Failure ($"Storage I/O error: {ex.GetFullMessage()}")); }
🧹 Nitpick comments (13)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.cs (2)
5-17: Avoid recursion and repeated string concatenations in GetFullMessage.Use an iterative loop with a StringBuilder to prevent deep-recursion risks and reduce allocations.
- public static string GetFullMessage(this Exception ex) - { - if (ex is null) - return string.Empty; - - var message = ex.Message; - - if (ex.InnerException != null) - { - message += " --> " + ex.InnerException.GetFullMessage(); - } - return message; - } + public static string GetFullMessage(this Exception ex) + { + if (ex is null) return string.Empty; + var sb = new System.Text.StringBuilder(); + var current = ex; + while (current is not null) + { + if (sb.Length > 0) sb.Append(" --> "); + sb.Append(current.Message); + current = current.InnerException; + } + return sb.ToString(); + }
19-27: Tighten NSError formatting to skip empty fields.Some properties (e.g., LocalizedFailureReason, LocalizedRecoverySuggestion) may be null/empty. Build the message from non-empty parts to avoid noisy commas.
- return $"Domain: {error.Domain}, Code: {error.Code}, Description: {error.LocalizedDescription}, FailureReason: {error.LocalizedFailureReason}, RecoverySuggestion: {error.LocalizedRecoverySuggestion}"; + var parts = new List<string>(5) + { + $"Domain: {error.Domain}", + $"Code: {error.Code}" + }; + if (!string.IsNullOrWhiteSpace(error.LocalizedDescription)) + parts.Add($"Description: {error.LocalizedDescription}"); + if (!string.IsNullOrWhiteSpace(error.LocalizedFailureReason)) + parts.Add($"FailureReason: {error.LocalizedFailureReason}"); + if (!string.IsNullOrWhiteSpace(error.LocalizedRecoverySuggestion)) + parts.Add($"RecoverySuggestion: {error.LocalizedRecoverySuggestion}"); + return string.Join(", ", parts);Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.apple.cs (2)
9-11: Confirm intent: authStrength is unused.If platform status is independent of AuthenticatorStrength, consider documenting this or using the parameter to influence checks; otherwise, mark it intentionally unused.
36-41: Prefer structured NSError formatting for consistency.Leverage your GetErrorMessage() extension instead of ToString().
- response.ErrorMsg = callback.Item2?.ToString(); + response.ErrorMsg = callback.Item2?.GetErrorMessage();Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.cs (3)
5-5: Make helper class static.Class only contains static members; mark the type static to prevent accidental instantiation.
-internal class LAContextHelpers +internal static class LAContextHelpers
12-20: Improve status mapping using LA error codes (optional but more accurate).Differentiate NotEnrolled vs Unavailable via LAStatus from
authError.Code. This yields correct UX and diagnostics.- if (localAuthContext.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out var authError)) - { - if (localAuthContext.BiometryType != LABiometryType.None) - { - return (BiometricHwStatus.Success, null); - } - - return (BiometricHwStatus.NotEnrolled, authError?.GetErrorMessage()); - } - - return (BiometricHwStatus.Unavailable, authError.GetErrorMessage()); + if (!localAuthContext.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out var authError)) + { + var code = (LAStatus)(int)authError.Code; + return code switch + { + LAStatus.BiometryNotEnrolled => (BiometricHwStatus.NotEnrolled, authError.GetErrorMessage()), + LAStatus.BiometryNotAvailable => (BiometricHwStatus.Unavailable, authError.GetErrorMessage()), + _ => (BiometricHwStatus.Unavailable, authError.GetErrorMessage()) + }; + } + return (BiometricHwStatus.Success, null);Also applies to: 22-25
25-25: Clarify generic failure message.Consider exposing the specific
ownerErrormessage here for better diagnostics.- return (BiometricHwStatus.Failure, "Biometric authentication failed."); + return (BiometricHwStatus.Failure, "Biometric authentication failed."); + // Optionally: include more context, e.g., passcode not set, etc.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
6-9: Make KeyOperationResult immutable to prevent accidental mutationPublic setters allow flipping success to failure post-creation. Lock this down.
- public bool WasSuccessful { get; set; } - public string? ErrorMessage { get; set; } - public string? AdditionalInfo { get; set; } - internal string? SecurityLevelName { get; set; } + public bool WasSuccessful { get; private set; } + public string? ErrorMessage { get; private set; } + public string? AdditionalInfo { get; private set; } + internal string? SecurityLevelName { get; private set; }Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (2)
3-3: Make helpers class staticPrevents instantiation and clarifies intent.
-public class KeyCreationHelpers +public static class KeyCreationHelpers
47-55: Tighten key-size validation per algorithm (optional)Consider enforcing AES sizes {128,192,256} and RSA min {2048,3072,4096}. EC could validate common curves by key size {256,384,521}.
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs (1)
25-33: Minor: remove null-conditional on cipher.DoFinal after prior null-checkSlight cleanup.
- var cipherData = cipher?.DoFinal(Request.InputData); + var cipherData = cipher.DoFinal(Request.InputData);Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (2)
174-179: Remove unnecessary async/await in expression-bodied DecryptAsyncAvoids state machine allocation.
- public partial async Task<SecureAuthenticationResponse> DecryptAsync(SecureAuthenticationRequest request, CancellationToken token) - => await BiometricPromptHelpers.ProcessCryptoAsync(request, CipherMode.DecryptMode, token); + public partial Task<SecureAuthenticationResponse> DecryptAsync(SecureAuthenticationRequest request, CancellationToken token) + => BiometricPromptHelpers.ProcessCryptoAsync(request, CipherMode.DecryptMode, token);
180-203: Clarify placeholder responses for Sign/Verify or mark as not supportedReturn a clear NotSupported message to avoid confusion.
- return Task.FromResult(SecureAuthenticationResponse.Failure("Key not found or operation canceled.")); + return Task.FromResult(SecureAuthenticationResponse.Failure("Sign is not implemented on Android in this release."));- return Task.FromResult(SecureAuthenticationResponse.Failure("Key not found or operation canceled.")); + return Task.FromResult(SecureAuthenticationResponse.Failure("Verify is not implemented on Android in this release."));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.apple.cs(2 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/CryptoOperation.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/CryptoOperation.cs
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs
🧰 Additional context used
🧬 Code graph analysis (6)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (3)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
KeyOperationResult(11-100)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (4)
KeyOperationResult(3-26)KeyOperationResult(5-5)KeyOperationResult(11-17)KeyOperationResult(19-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (1)
CryptoKeyOptions(3-39)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.cs (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.cs (1)
GetErrorMessage(20-26)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.apple.cs (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.cs (1)
LAContextHelpers(5-27)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (8)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs (1)
SecureBiometricService(3-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (3)
KeyOperationResult(11-100)AndroidKeyStoreHelpers(7-215)MapKeyAlgorithm(150-159)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (2)
KeyOperationResult(5-58)KeyCreationHelpers(3-59)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (4)
KeyOperationResult(3-26)KeyOperationResult(5-5)KeyOperationResult(11-17)KeyOperationResult(19-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.cs (1)
GetFullMessage(5-17)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (2)
SecureAuthenticationResponse(27-39)BiometricPromptHelpers(12-148)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs (4)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (3)
BiometricPrompt(75-97)SecureAuthenticationResponse(27-39)Cipher(55-61)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-28)SecureAuthenticationResponse(10-18)SecureAuthenticationResponse(20-27)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/AuthCallback.android.cs (3)
OnAuthenticationSucceeded(11-26)OnAuthenticationError(28-36)OnAuthenticationFailed(38-43)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (2)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
KeyOperationResult(11-100)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (1)
KeyOperationResult(5-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-plugin-ci
🔇 Additional comments (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.apple.cs (1)
61-61: Good: dispose LAContext via using declaration.This prevents handle leaks and aligns with Apple APIs’ lifecycle expectations.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (1)
22-25: Guard asymmetric encrypt/decrypt (RSA/EC) until the flow supports itProcess flow uses symmetric Cipher; RSA/EC enc/dec will fail at runtime. Block both algorithms for Encrypt/Decrypt.
Apply:
- if (IsEcEncryptDecryptInvalid(options)) - return KeyOperationResult.Failure("EC keys cannot be used for encrypt/decrypt operations. Use RSA or AES instead."); + if (IsAsymmetricEncryptDecryptInvalid(options)) + return KeyOperationResult.Failure("Asymmetric (RSA/EC) encrypt/decrypt not supported in this release. Use AES for enc/dec; use RSA/EC for sign/verify.");- private static bool IsEcEncryptDecryptInvalid(CryptoKeyOptions options) => - options.Algorithm == KeyAlgorithm.Ec && - (options.Operation.HasFlag(CryptoOperation.Encrypt) || options.Operation.HasFlag(CryptoOperation.Decrypt)); + private static bool IsAsymmetricEncryptDecryptInvalid(CryptoKeyOptions options) => + (options.Algorithm == KeyAlgorithm.Ec || options.Algorithm == KeyAlgorithm.Rsa) && + (options.Operation.HasFlag(CryptoOperation.Encrypt) || options.Operation.HasFlag(CryptoOperation.Decrypt));Also applies to: 51-54
🧹 Nitpick comments (4)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (4)
28-29: Optional: Enforce “no block mode” for all RSA, not only OAEPRSA doesn’t use block modes. Today you only reject OAEP+BlockMode≠None; extend to all RSA paddings to prevent confusing configs.
Apply:
- if (IsRsaOaepBlockModeInvalid(options)) + if (IsRsaBlockModeInvalid(options)) + return KeyOperationResult.Failure("RSA does not use block modes. Set BlockMode to None."); + if (IsRsaOaepBlockModeInvalid(options)) return KeyOperationResult.Failure("RSA with OAEP padding cannot be used with a BlockMode. Set BlockMode to None.");private static bool IsRsaOaepBlockModeInvalid(CryptoKeyOptions options) => options.Algorithm == KeyAlgorithm.Rsa && options.Padding == Padding.Oaep && options.BlockMode != BlockMode.None; + + private static bool IsRsaBlockModeInvalid(CryptoKeyOptions options) => + options.Algorithm == KeyAlgorithm.Rsa && options.BlockMode != BlockMode.None;Also applies to: 63-67
68-76: Optional: Tighten algorithm-specific key sizesCurrent 128–8192 range allows invalid AES sizes (e.g., 129) and EC sizes. Consider per-algorithm guards.
Example:
- private static bool IsKeySizeInvalid(CryptoKeyOptions options) => - options.KeySize < 128 || options.KeySize > 8192; + private static bool IsKeySizeInvalid(CryptoKeyOptions options) => + options.KeySize < 128 || options.KeySize > 8192; // coarse bound + + private static bool IsAesKeySizeInvalid(CryptoKeyOptions options) => + options.Algorithm == KeyAlgorithm.Aes && options.KeySize is not (128 or 192 or 256); + + private static bool IsEcKeySizeInvalid(CryptoKeyOptions options) => + options.Algorithm == KeyAlgorithm.Ec && options.KeySize is not (256 or 384 or 521);And call IsAesKeySizeInvalid(options) before the RSA/EC size checks.
3-4: Make helper class staticIt only contains static members and shouldn’t be instantiated.
Apply:
-public class KeyCreationHelpers +public static class KeyCreationHelpers
45-47: Use CryptoOperation.None instead of magic 0Replace
options.Operation == 0withoptions.Operation == CryptoOperation.Nonefor clarity — CryptoOperation.None is defined in Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/CryptoOperation.cs (None = 0).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (2)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (4)
KeyOperationResult(3-26)KeyOperationResult(5-5)KeyOperationResult(11-17)KeyOperationResult(19-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs (1)
CryptoKeyOptions(3-39)
🔇 Additional comments (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (1)
14-21: LGTM: Early keyId/options/operation validation orderNull/empty keyId, null options, and empty operations are handled before accessing properties—prevents NREs and yields clear messages.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
1-26: Re-verify call sites don’t use “new KeyOperationResult.Success/Failure(…)”Past commits had constructor-style usages that won’t compile with factory methods. Quick sweep recommended.
#!/bin/bash # Find invalid constructor-style usages of the static factories rg -nP -g "*.cs" -C1 '\bnew\s+KeyOperationResult\.(Success|Failure)\s*\('
🧹 Nitpick comments (3)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (3)
6-9: Make result immutable (init-only) to preserve invariantsPublic setters allow post-creation mutation (e.g., flipping success to failure). Prefer init-only.
- public bool WasSuccessful { get; set; } - public string? ErrorMessage { get; set; } - public string? AdditionalInfo { get; set; } - internal string? SecurityLevelName { get; set; } + public bool WasSuccessful { get; init; } + public string? ErrorMessage { get; init; } + public string? AdditionalInfo { get; init; } + internal string? SecurityLevelName { get; init; }
19-25: Guard Failure(errorMessage) against null/whitespacePrevents meaningless failures and documents the contract.
- public static KeyOperationResult Failure(string errorMessage, string? additionalInfo = null) - => new() - { - WasSuccessful = false, - ErrorMessage = errorMessage, - AdditionalInfo = additionalInfo - }; + public static KeyOperationResult Failure(string errorMessage, string? additionalInfo = null) + { + if (string.IsNullOrWhiteSpace(errorMessage)) + throw new ArgumentException("errorMessage cannot be null or whitespace.", nameof(errorMessage)); + return new() + { + WasSuccessful = false, + ErrorMessage = errorMessage, + AdditionalInfo = additionalInfo + }; + }
9-9: Prefer a typed security level over stringly-typed nameIf feasible, expose an enum (e.g., SecurityLevel { StrongBox, TEE, Software }) and keep the user-friendly text in AdditionalInfo. Reduces string comparisons downstream.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs
🧰 Additional context used
🧬 Code graph analysis (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (2)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs (1)
KeyOperationResult(11-100)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (2)
KeyOperationResult(5-9)KeyOperationResult(12-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-plugin-ci
🔇 Additional comments (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
3-5: Good: sealed + private ctor + factories enforce construction pathPattern is clear and prevents “new” from outside.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (3)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (3)
15-21: Don’t return Success for an unimplemented Apple key-creation path.This simulates success without actually creating a key, which will mislead callers and can cause security regressions. Until implemented, return an explicit Failure (or throw) to make the state unambiguous.
Apply:
- // Key creation logic would go here. For now, we simulate success. - - return Task.FromResult(KeyOperationResult.Success()); + // Apple key creation not implemented yet; surface explicit failure. + return Task.FromResult( + KeyOperationResult.Failure("SecureBiometricService.CreateKeyAsync is not implemented for Apple yet."));
35-43: DeleteKeyAsync/KeyExistsAsync must not report Success when unimplemented.Returning Success() implies the key was deleted/exists, which is incorrect and dangerous. Return Failure (or throw) until the Apple implementation lands. Optionally validate keyId (null/whitespace) like creation does.
public partial Task<KeyOperationResult> DeleteKeyAsync(string keyId) { - return Task.FromResult(KeyOperationResult.Success()); + return Task.FromResult( + KeyOperationResult.Failure("SecureBiometricService.DeleteKeyAsync is not implemented for Apple yet.")); } public partial Task<KeyOperationResult> KeyExistsAsync(string keyId) { - return Task.FromResult(KeyOperationResult.Success()); + return Task.FromResult( + KeyOperationResult.Failure("SecureBiometricService.KeyExistsAsync is not implemented for Apple yet.")); }
45-63: Avoid ambiguous default SecureAuthenticationResponse instances; return explicit Failure (or throw).Constructing new SecureAuthenticationResponse() yields WasSuccessful = false with no error message, making failures hard to diagnose. Use the Failure factory with a clear message.
public partial Task<SecureAuthenticationResponse> DecryptAsync(SecureAuthenticationRequest request, CancellationToken token) { - return Task.FromResult(new SecureAuthenticationResponse()); + return Task.FromResult( + SecureAuthenticationResponse.Failure("SecureBiometricService.DecryptAsync is not implemented for Apple yet.")); } public partial Task<SecureAuthenticationResponse> EncryptAsync(SecureAuthenticationRequest request, CancellationToken token) { - return Task.FromResult(new SecureAuthenticationResponse()); + return Task.FromResult( + SecureAuthenticationResponse.Failure("SecureBiometricService.EncryptAsync is not implemented for Apple yet.")); } public partial Task<SecureAuthenticationResponse> SignAsync(string keyId, byte[] inputData, CancellationToken token) { - return Task.FromResult(new SecureAuthenticationResponse()); + return Task.FromResult( + SecureAuthenticationResponse.Failure("SecureBiometricService.SignAsync is not implemented for Apple yet.")); } public partial Task<SecureAuthenticationResponse> VerifyAsync(string keyId, byte[] inputData, byte[] signature, CancellationToken token) { - return Task.FromResult(new SecureAuthenticationResponse()); + return Task.FromResult( + SecureAuthenticationResponse.Failure("SecureBiometricService.VerifyAsync is not implemented for Apple yet.")); }
🧹 Nitpick comments (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (1)
45-63: Consider honoring CancellationToken.Optional: add
token.ThrowIfCancellationRequested()at the start of each async API to standardize behavior across platforms.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs(1 hunks)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.cs
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs
🧰 Additional context used
🧬 Code graph analysis (1)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (6)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (3)
KeyOperationResult(5-9)KeyOperationResult(12-37)KeyCreationHelpers(3-79)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (4)
KeyOperationResult(3-26)KeyOperationResult(5-5)KeyOperationResult(11-17)KeyOperationResult(19-25)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.cs (2)
LAContextHelpers(5-27)BiometricHwStatus(7-26)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.cs (1)
GetFullMessage(5-17)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (3)
SecureAuthenticationResponse(3-24)SecureAuthenticationResponse(10-16)SecureAuthenticationResponse(18-23)Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
SecureAuthenticationRequest(3-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-plugin-ci
🔇 Additional comments (2)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (2)
5-12: Good: upfront validation and short-circuit.Using KeyCreationHelpers.PerformKeyCreationValidation and returning the failure early is clean and consistent with the models.
3-3: Partial type accessibility matches (internal). SecureBiometricService.apple.cs, .android.cs and .windows.cs declareinternal; SecureBiometricService.net.cs usespartial(implicitinternal).
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
Plugin.Maui.Biometric/Samples/MauiProgram.cs (1)
18-19: Use the discard_for the unusedIServiceProviderlambda parameter.Neither registration uses the
eparameter;_is the idiomatic C# discard and removes any ambiguity about intent.♻️ Proposed refactor
- builder.Services.AddSingleton((e) => BiometricAuthenticationService.Default); - builder.Services.AddSingleton((e) => SecureBiometricAuthenticationService.Default); + builder.Services.AddSingleton(_ => BiometricAuthenticationService.Default); + builder.Services.AddSingleton(_ => SecureBiometricAuthenticationService.Default);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Samples/MauiProgram.cs` around lines 18 - 19, Replace the unused lambda parameter name `e` with the discard `_` in the service registrations to signal the IServiceProvider is unused; update the two calls to builder.Services.AddSingleton((e) => BiometricAuthenticationService.Default) and builder.Services.AddSingleton((e) => SecureBiometricAuthenticationService.Default) to use builder.Services.AddSingleton(_ => BiometricAuthenticationService.Default) and builder.Services.AddSingleton(_ => SecureBiometricAuthenticationService.Default).Plugin.Maui.Biometric/Samples/BiometricPage.xaml.cs (1)
37-39: Implement actual cancellation wiring (or remove the TODO-style comment).The comment says a valid token should be used, but the call still uses
CancellationToken.None. Adding aCancellationTokenSourcetied to a cancel action would make the sample behavior match the guidance.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Samples/BiometricPage.xaml.cs` around lines 37 - 39, The sample still passes CancellationToken.None to _biometric.AuthenticateAsync even though the comment instructs to use a real token; replace that with a CancellationTokenSource that you create and store (e.g., cts) and pass cts.Token into _biometric.AuthenticateAsync inside Dispatcher.DispatchAsync, and hook cts.Cancel() to the UI cancel action (button or command) so canceling the operation actually cancels the authentication; update any TODO comment accordingly and dispose the CancellationTokenSource when done.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Plugin.Maui.Biometric/Samples/SecureBiometricPage.xaml.cs`:
- Around line 20-39: Wrap the await calls to the biometric service in each async
void handler (OnCreateKeyClicked, OnEncryptClicked, OnDecryptClicked,
OnDeleteKeyClicked) in a try/catch block so exceptions don't escape the event
handler; inside the catch set ResultLabel.Text to a clear error message
(including ex.Message), optionally log the exception (Console.WriteLine or a
logger), and preserve the existing success/failure result flow by only using the
service result when no exception was thrown.
- Around line 59-64: The code uses null-forgiving operators on crypto payloads
(response.OutputData! and response.IV) after checking response.WasSuccessful;
instead add defensive null checks before assigning _encryptedData and _iv and
before calling Convert.ToBase64String: verify response.OutputData and
response.IV are non-null (and non-empty where appropriate), handle the null case
by setting a clear error ResultLabel text or logging and avoid calling
Convert.ToBase64String on null; apply the same null-guarding pattern used here
to the corresponding decrypt branch (lines referencing Decrypted output around
the 98-101 region) so both Encrypt/Decrypt paths validate payloads before use.
---
Nitpick comments:
In `@Plugin.Maui.Biometric/Samples/BiometricPage.xaml.cs`:
- Around line 37-39: The sample still passes CancellationToken.None to
_biometric.AuthenticateAsync even though the comment instructs to use a real
token; replace that with a CancellationTokenSource that you create and store
(e.g., cts) and pass cts.Token into _biometric.AuthenticateAsync inside
Dispatcher.DispatchAsync, and hook cts.Cancel() to the UI cancel action (button
or command) so canceling the operation actually cancels the authentication;
update any TODO comment accordingly and dispose the CancellationTokenSource when
done.
In `@Plugin.Maui.Biometric/Samples/MauiProgram.cs`:
- Around line 18-19: Replace the unused lambda parameter name `e` with the
discard `_` in the service registrations to signal the IServiceProvider is
unused; update the two calls to builder.Services.AddSingleton((e) =>
BiometricAuthenticationService.Default) and builder.Services.AddSingleton((e) =>
SecureBiometricAuthenticationService.Default) to use
builder.Services.AddSingleton(_ => BiometricAuthenticationService.Default) and
builder.Services.AddSingleton(_ =>
SecureBiometricAuthenticationService.Default).
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
Plugin.Maui.Biometric/Samples/AppShell.xamlPlugin.Maui.Biometric/Samples/AppShell.xaml.csPlugin.Maui.Biometric/Samples/BiometricPage.xamlPlugin.Maui.Biometric/Samples/BiometricPage.xaml.csPlugin.Maui.Biometric/Samples/HomePage.xamlPlugin.Maui.Biometric/Samples/HomePage.xaml.csPlugin.Maui.Biometric/Samples/MainPage.xamlPlugin.Maui.Biometric/Samples/MainPage.xaml.csPlugin.Maui.Biometric/Samples/MauiProgram.csPlugin.Maui.Biometric/Samples/SecureBiometricPage.xamlPlugin.Maui.Biometric/Samples/SecureBiometricPage.xaml.cs
💤 Files with no reviewable changes (2)
- Plugin.Maui.Biometric/Samples/MainPage.xaml.cs
- Plugin.Maui.Biometric/Samples/MainPage.xaml
| private async void OnCreateKeyClicked(object sender, EventArgs e) | ||
| { | ||
| var options = new CryptoKeyOptions | ||
| { | ||
| Algorithm = KeyAlgorithm.Aes, | ||
| KeySize = 256, | ||
| BlockMode = BlockMode.Gcm, | ||
| Padding = Padding.None, | ||
| Operation = CryptoOperation.Encrypt | CryptoOperation.Decrypt, | ||
| RequireUserAuthentication = true, | ||
| }; | ||
|
|
||
| var result = await _secureBiometric.CreateKeyAsync(KeyId, options); | ||
|
|
||
| ResultLabel.Text = result.WasSuccessful | ||
| ? $"Key created\n{result.AdditionalInfo}" | ||
| : $"Create failed: {result.ErrorMessage}"; | ||
|
|
||
| Console.WriteLine(ResultLabel.Text); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "SecureBiometricPage.xaml.cs" -type fRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 134
🏁 Script executed:
cat -n "Plugin.Maui.Biometric/Samples/SecureBiometricPage.xaml.cs"Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 5302
Add exception handling around biometric service calls.
All four event handlers (OnCreateKeyClicked, OnEncryptClicked, OnDecryptClicked, OnDeleteKeyClicked) await external operations without a local try/catch. Any thrown exception will bubble out of the async void handler and crash the application.
Proposed hardening pattern
private async void OnCreateKeyClicked(object sender, EventArgs e)
{
- var options = new CryptoKeyOptions
- {
- Algorithm = KeyAlgorithm.Aes,
- KeySize = 256,
- BlockMode = BlockMode.Gcm,
- Padding = Padding.None,
- Operation = CryptoOperation.Encrypt | CryptoOperation.Decrypt,
- RequireUserAuthentication = true,
- };
-
- var result = await _secureBiometric.CreateKeyAsync(KeyId, options);
-
- ResultLabel.Text = result.WasSuccessful
- ? $"Key created\n{result.AdditionalInfo}"
- : $"Create failed: {result.ErrorMessage}";
-
- Console.WriteLine(ResultLabel.Text);
+ try
+ {
+ var options = new CryptoKeyOptions
+ {
+ Algorithm = KeyAlgorithm.Aes,
+ KeySize = 256,
+ BlockMode = BlockMode.Gcm,
+ Padding = Padding.None,
+ Operation = CryptoOperation.Encrypt | CryptoOperation.Decrypt,
+ RequireUserAuthentication = true,
+ };
+
+ var result = await _secureBiometric.CreateKeyAsync(KeyId, options);
+ ResultLabel.Text = result.WasSuccessful
+ ? $"Key created\n{result.AdditionalInfo}"
+ : $"Create failed: {result.ErrorMessage}";
+ }
+ catch (Exception ex)
+ {
+ ResultLabel.Text = $"Unexpected error: {ex.Message}";
+ }
}Also applies to: OnEncryptClicked (lines 41–71), OnDecryptClicked (lines 73–109), OnDeleteKeyClicked (lines 111–123).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Plugin.Maui.Biometric/Samples/SecureBiometricPage.xaml.cs` around lines 20 -
39, Wrap the await calls to the biometric service in each async void handler
(OnCreateKeyClicked, OnEncryptClicked, OnDecryptClicked, OnDeleteKeyClicked) in
a try/catch block so exceptions don't escape the event handler; inside the catch
set ResultLabel.Text to a clear error message (including ex.Message), optionally
log the exception (Console.WriteLine or a logger), and preserve the existing
success/failure result flow by only using the service result when no exception
was thrown.
| if (response.WasSuccessful) | ||
| { | ||
| _encryptedData = response.OutputData; | ||
| _iv = response.IV; | ||
| ResultLabel.Text = $"Encrypted:\n{Convert.ToBase64String(_encryptedData!)}"; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "SecureBiometricPage.xaml.cs"Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 134
🏁 Script executed:
cat -n ./Plugin.Maui.Biometric/Samples/SecureBiometricPage.xaml.cs | head -110Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 4770
🏁 Script executed:
find . -type f -name "*.cs" | xargs grep -l "class.*Response\|record.*Response" | head -20Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 321
🏁 Script executed:
cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 944
🏁 Script executed:
find . -type f -name "*.cs" | xargs grep -l "EncryptAsync\|DecryptAsync" | grep -v Test | grep -v SampleRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 568
🏁 Script executed:
cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/ISecureBiometric.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 2982
🏁 Script executed:
cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs | head -150Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 6390
🏁 Script executed:
grep -n "partial Task<SecureAuthenticationResponse> EncryptAsync\|partial Task<SecureAuthenticationResponse> DecryptAsync" ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 207
🏁 Script executed:
sed -n '173,250p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1862
🏁 Script executed:
find . -type f -name "*.cs" | xargs grep -l "BiometricPromptHelpers" | grep -v TestRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 337
🏁 Script executed:
grep -n "ProcessCryptoAsync" ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 159
🏁 Script executed:
sed -n '100,200p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 2014
🏁 Script executed:
find . -type f -name "*.cs" | xargs grep -l "class SecureAuthCallback"Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 157
🏁 Script executed:
cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 2530
🏁 Script executed:
cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.apple.cs | head -60Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 187
🏁 Script executed:
find . -type f -name "SecureAuthCallback*.cs"Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 157
Validate encryption/decryption responses before using null-forgiving operators.
response.OutputData! and implicit trust in response.IV bypass null safety on crypto payloads. Although WasSuccessful indicates the operation completed, the response fields are typed as nullable and sample code should demonstrate defensive validation.
Safer payload validation
- if (response.WasSuccessful)
+ if (response.WasSuccessful &&
+ response.OutputData is { Length: > 0 } output &&
+ response.IV is { Length: > 0 } iv)
{
- _encryptedData = response.OutputData;
- _iv = response.IV;
- ResultLabel.Text = $"Encrypted:\n{Convert.ToBase64String(_encryptedData!)}";
+ _encryptedData = output;
+ _iv = iv;
+ ResultLabel.Text = $"Encrypted:\n{Convert.ToBase64String(output)}";
}
else
{
- ResultLabel.Text = $"Encrypt failed: {response.ErrorMessage}";
+ ResultLabel.Text = $"Encrypt failed: {response.ErrorMessage ?? "Missing encrypted payload"}";
}
- if (response.WasSuccessful)
+ if (response.WasSuccessful && response.OutputData is { Length: > 0 } decryptedBytes)
{
- var plainText = System.Text.Encoding.UTF8.GetString(response.OutputData!);
+ var plainText = System.Text.Encoding.UTF8.GetString(decryptedBytes);
ResultLabel.Text = $"Decrypted: {plainText}";
}
else
{
ResultLabel.Text = $"Decrypt failed: {response.ErrorMessage}";
}Also applies to: 98-101
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Plugin.Maui.Biometric/Samples/SecureBiometricPage.xaml.cs` around lines 59 -
64, The code uses null-forgiving operators on crypto payloads
(response.OutputData! and response.IV) after checking response.WasSuccessful;
instead add defensive null checks before assigning _encryptedData and _iv and
before calling Convert.ToBase64String: verify response.OutputData and
response.IV are non-null (and non-empty where appropriate), handle the null case
by setting a clear error ResultLabel text or logging and avoid calling
Convert.ToBase64String on null; apply the same null-guarding pattern used here
to the corresponding decrypt branch (lines referencing Decrypted output around
the 98-101 region) so both Encrypt/Decrypt paths validate payloads before use.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (6)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BlockMode.cs (1)
9-9:⚠️ Potential issue | 🟠 MajorKeep
Ecbas interop-only and discourage selection.Line 9 still publicly exposes an insecure mode without warnings/hiding. Please keep it only for compatibility and mark it as unsafe.
🔧 Proposed change
public enum BlockMode { None, Cbc, Gcm, Ctr, - Ecb + /// <summary> + /// Insecure mode retained only for interop compatibility. Do not select programmatically. + /// </summary> + [Obsolete("ECB is insecure and retained for interop only; do not use.")] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + Ecb }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BlockMode.cs` at line 9, The BlockMode enum currently exposes the insecure Ecb value publicly; mark Ecb as interop-only and discourage use by adding attributes and docs: annotate the Ecb enum member with [Obsolete("Interop-only: ECB is insecure, do not use.", false)] and [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)], add an XML summary comment stating "Interop-only, insecure — do not use", and ensure any codepaths selecting BlockMode.Ecb remain unchanged for compatibility; this keeps the member available for interop but visibly discouraged to consumers.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (1)
12-18:⚠️ Potential issue | 🟠 MajorThe
Transformationproperty generates invalid transformation strings for RSA encryption and decryption.When
AlgorithmisKeyAlgorithm.Rsa,MapBlockMode(BlockMode.None)defaults toKeyProperties.BlockModeGcm, producing an invalid transformation string like"RSA/GCM/PKCS1Padding". RSA does not use block modes; it requires ECB semantically. This causesCipher.GetInstance(transformation)to fail at runtime when RSA encryption/decryption is attempted with the defaultBlockMode.None.The
Transformationproperty must be algorithm-aware to mapBlockMode.Noneto ECB for RSA while preserving GCM for AES.🔧 Proposed algorithm-aware fix
`#if` ANDROID - internal string Transformation => - AndroidKeyStoreHelpers.MapTransformation( - AndroidKeyStoreHelpers.MapKeyAlgorithm(Algorithm), - AndroidKeyStoreHelpers.MapBlockMode(BlockMode), - AndroidKeyStoreHelpers.MapPadding(Padding)); + internal string Transformation + { + get + { + var alg = AndroidKeyStoreHelpers.MapKeyAlgorithm(Algorithm); + var mode = Algorithm == KeyAlgorithm.Rsa + ? AndroidKeyStoreHelpers.MapBlockMode(BlockMode == BlockMode.None ? BlockMode.Ecb : BlockMode) + : AndroidKeyStoreHelpers.MapBlockMode(BlockMode == BlockMode.None ? BlockMode.Gcm : BlockMode); + var pad = AndroidKeyStoreHelpers.MapPadding(Padding); + return AndroidKeyStoreHelpers.MapTransformation(alg, mode, pad); + } + } `#endif`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs` around lines 12 - 18, The Transformation property builds invalid strings for RSA because BlockMode.None is mapped to GCM; update Transformation to be algorithm-aware: when Algorithm == KeyAlgorithm.Rsa and BlockMode == BlockMode.None use ECB (e.g., KeyProperties.BlockModeEcb) for the block mode mapping, otherwise use AndroidKeyStoreHelpers.MapBlockMode(BlockMode); then call AndroidKeyStoreHelpers.MapKeyAlgorithm(Algorithm) and AndroidKeyStoreHelpers.MapPadding(Padding) as before and pass the chosen blockMode into MapTransformation so RSA transformations become "RSA/ECB/..." while AES still uses GCM when appropriate.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs (1)
30-35:⚠️ Potential issue | 🟠 MajorReject non-byte-aligned key sizes here.
IsKeySizeInvalidlets values like257through. The Apple AES path later doesoptions.KeySize / 8, which truncates to 32 bytes and silently creates a 256-bit key while reporting the requested size. Validate byte alignment before key generation.🐛 Minimal fix
- private static bool IsKeySizeInvalid(CryptoKeyOptions options) => - options.KeySize < 128 || options.KeySize > 8192; + private static bool IsKeySizeInvalid(CryptoKeyOptions options) => + options.KeySize < 128 || options.KeySize > 8192 || options.KeySize % 8 != 0;Also applies to: 71-72
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs` around lines 30 - 35, The validation currently allows non-byte-aligned key sizes (e.g., 257) so later code that does options.KeySize / 8 silently truncates the size; update the validators in KeyCreationHelpers (the checks using IsKeySizeInvalid, IsRsaKeySizeInvalid, IsEcKeySizeInvalid) to also reject sizes that are not divisible by 8 (options.KeySize % 8 != 0) and return a clear KeyOperationResult.Failure like "Key size must be a whole number of bytes (multiple of 8)." Apply the same additional check for RSA and EC paths so options.KeySize is always byte-aligned before any code that computes options.KeySize / 8.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs (1)
5-6:⚠️ Potential issue | 🟡 MinorFix typo and improve obsolete message.
- Typo: "obselete" → "obsolete"
- The message "library now supports all platforms" is misleading since the .NET fallback returns
NotImplemented. Consider neutral guidance pointing to runtime capability checks.Proposed fix
-[Obsolete("This property is now obselete since the library now supports all platforms.")] +[System.Obsolete("IsPlatformSupported is obsolete. Use GetAuthenticationStatusAsync() or GetEnrolledBiometricTypesAsync() for runtime capability checks.")] +[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public bool IsPlatformSupported { get; } = GetIsPlatformSupported();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs` around lines 5 - 6, Replace the Obsolete attribute on the IsPlatformSupported property to fix the "obselete" typo and change the message to a neutral guidance: indicate the property is obsolete and recommend using runtime capability checks (e.g., call GetIsPlatformSupported or handle PlatformNotImplemented/NotImplemented fallback) rather than claiming "library now supports all platforms"; update the attribute text on IsPlatformSupported to a corrected, concise message referencing GetIsPlatformSupported so callers know to use runtime checks instead.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs (2)
124-128:⚠️ Potential issue | 🟠 MajorComplete TaskCompletionSource on cancellation to prevent hanging tasks.
When the
CancellationTokenfires,CancelAuthentication()is called, but ifOnAuthenticationErrorisn't invoked (edge case), theTaskCompletionSourcecould remain incomplete indefinitely.Proposed fix
-using (token.Register(() => biometricPrompt.CancelAuthentication())) +using (token.Register(() => +{ + biometricPrompt.CancelAuthentication(); + authCallback.Response.TrySetResult( + SecureAuthenticationResponse.Failure("Operation was cancelled.")); +})) { biometricPrompt.Authenticate(promptInfo, cryptoObject); return await authCallback.Response.Task; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs` around lines 124 - 128, When registering the cancellation callback around biometricPrompt.Authenticate, also complete the authentication TaskCompletionSource so it cannot hang: update the token.Register callback (used where biometricPrompt.CancelAuthentication() is invoked) to call authCallback.Response.TrySetCanceled(token) (or TrySetCanceled() if token not available) after calling CancelAuthentication(); keep the using(token.Register(...)) pattern and use TrySetCanceled/TrySetException to avoid throwing if the TCS is already completed, referencing biometricPrompt.Authenticate, biometricPrompt.CancelAuthentication(), token.Register and authCallback.Response.Task.
55-61:⚠️ Potential issue | 🔴 CriticalMissing IV parameter handling for decryption operations.
InitCipherinitializes the cipher without an IV, but AES-GCM and AES-CBC modes require the IV from encryption to be provided during decryption. This will cause decryption to fail silently or produce incorrect output.Proposed fix
-private static Cipher InitCipher(string transformation, CipherMode mode, IKey key) +private static Cipher InitCipher(string transformation, CipherMode mode, IKey key, byte[]? iv = null) { var cipher = Cipher.GetInstance(transformation) ?? throw new InvalidOperationException("Failed to create cipher."); - cipher.Init(mode, key); + + if (mode == CipherMode.DecryptMode && iv is { Length: > 0 }) + { + // For GCM mode, use GCMParameterSpec with 128-bit auth tag + using var spec = new Javax.Crypto.Spec.GCMParameterSpec(128, iv); + cipher.Init(mode, key, spec); + } + else + { + cipher.Init(mode, key); + } return cipher; }Update the call site at line 111:
-using var cipher = InitCipher(request.Transformation, mode, key); +using var cipher = InitCipher(request.Transformation, mode, key, request.IV);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs` around lines 55 - 61, InitCipher currently initializes the Cipher without any IV which breaks decryption for modes like AES-GCM/AES-CBC; change the signature of InitCipher to accept a byte[] iv (nullable) and, inside the method, if iv is provided use the appropriate parameter spec when calling Cipher.Init — for AES-CBC use new IvParameterSpec(iv) and for AES-GCM use new GCMParameterSpec(128, iv) (or the correct TAG length) passed alongside the key; otherwise call Cipher.Init(mode, key) for cases that don’t need an IV. Update callers (e.g., the site mentioned around line 111) to pass the original encryption IV when mode == CipherMode.Decrypt.
🧹 Nitpick comments (13)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs (1)
7-7: Inconsistent:IVhas public setter while other properties have private setters.This breaks the immutability pattern established by the static factory methods. Consider making the setter private and setting it only through the
Successfactory.♻️ Proposed fix
- public byte[]? IV { get; set; } + public byte[]? IV { get; private set; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs` at line 7, The IV property on SecureAuthenticationResponse is the only one with a public setter, breaking the immutability pattern; change the IV property's setter to private (byte[]? IV { get; private set; }) and ensure the static factory method Success is the only place that assigns it (set IV inside the Success factory), keeping other properties' private setters unchanged.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs (1)
8-12: Reconsider#nullable disablefor required UI properties.Disabling nullability for
Title,Subtitle, andNegativeTextsuppresses compiler warnings but allows callers to leave these properties uninitialized. If these are required for biometric prompts (especiallyTitleandNegativeTexton Android), runtime failures could occur.Consider using
requiredmodifier or initializing with sensible defaults instead:♻️ Alternative approach
-#nullable disable - public string Title { get; set; } - public string Subtitle { get; set; } - public string NegativeText { get; set; } -#nullable restore + public required string Title { get; set; } + public string? Subtitle { get; set; } + public string NegativeText { get; set; } = "Cancel";If platform flexibility is the goal, document which properties are required per platform.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs` around lines 8 - 12, Remove the blanket "#nullable disable" and make the UI properties explicit: update BaseAuthenticationRequest to either mark Title and NegativeText as required (and Subtitle as required or nullable depending on platform needs) using the C# required modifier, or initialize them with sensible non-null defaults; ensure the properties Title, Subtitle, and NegativeText in BaseAuthenticationRequest reflect the chosen approach and add a short comment documenting which properties are required per platform (e.g., Android requires Title and NegativeText).Plugin.Maui.Biometric/Samples/BiometricPage.xaml.cs (2)
37-39: Comment is misleading - code passesCancellationToken.None.The comment suggests using a "valid token" for cancellation, but the code passes
CancellationToken.None. For a sample, consider either removing the comment or demonstrating actual cancellation token usage.♻️ Option: Remove misleading comment
- // You should also pass a valid token and use it to cancel biometric authentication var response = await this.Dispatcher.DispatchAsync(async () => await _biometric.AuthenticateAsync(req, CancellationToken.None));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Samples/BiometricPage.xaml.cs` around lines 37 - 39, The comment in BiometricPage.xaml.cs is misleading because the code calls _biometric.AuthenticateAsync(req, CancellationToken.None) inside this.Dispatcher.DispatchAsync; either remove or update the comment and sample to demonstrate real cancellation: create a CancellationTokenSource (e.g., cts), pass cts.Token into _biometric.AuthenticateAsync(req, cts.Token) and show cancelling via cts.Cancel(), or simply delete the “pass a valid token” line if you want to keep the sample minimal; update the nearby Dispatcher.DispatchAsync usage and any variable names (req, response) accordingly.
45-45:Console.Write(response)will likely print the type name only.Unless
AuthenticationResponseoverridesToString(), this will output something likePlugin.Maui.Biometric.AuthenticationResponserather than useful diagnostic information.♻️ More informative logging
- Console.Write(response); + Console.WriteLine($"Response: Status={response.Status}, AuthType={response.AuthenticationType}, Error={response.ErrorMsg}");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Samples/BiometricPage.xaml.cs` at line 45, Console.Write(response) will likely print the type name; replace it with a meaningful serialization or property-level log of the AuthenticationResponse so diagnostics are useful. Locate the Console.Write call in BiometricPage.xaml.cs and change it to either Console.WriteLine(JsonSerializer.Serialize(response)) (import System.Text.Json) or explicitly log key properties from the AuthenticationResponse (e.g., response.IsAuthenticated, response.ErrorMessage, response.Timestamp) to produce human-readable output.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.cs (1)
5-8: Consider aligning parameter nullability with the null check.The method handles
nullinternally (returningstring.Empty), but the parameter is declared as non-nullableException ex. Consider usingException? exto make the null-tolerance explicit in the signature.♻️ Suggested change
- public static string GetFullMessage(this Exception ex) + public static string GetFullMessage(this Exception? ex)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.cs` around lines 5 - 8, GetFullMessage currently accepts a non-nullable Exception but defensively handles null; change the parameter to a nullable Exception? ex in the GetFullMessage method signature to make null-tolerance explicit, update any XML docs or comments for GetFullMessage to reflect the nullable parameter, and review/correct all call sites (or enable nullable warnings) if they need adjustment for the new signature so the behavior (returning string.Empty for null) remains unchanged.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs (1)
5-25: MakeKeyOperationResultimmutable after construction.The private ctor + factories imply controlled creation, but the public setters still let callers flip
WasSuccessfulor combine success state with anErrorMessagelater.init/private setters would keep the result shape consistent.♻️ Suggested shape
- public bool WasSuccessful { get; set; } - public string? ErrorMessage { get; set; } - public string? AdditionalInfo { get; set; } - internal string? SecurityLevelName { get; set; } + public bool WasSuccessful { get; private init; } + public string? ErrorMessage { get; private init; } + public string? AdditionalInfo { get; private init; } + internal string? SecurityLevelName { get; private init; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs` around lines 5 - 25, Make KeyOperationResult immutable by removing public setters and only allowing values to be set at construction: change WasSuccessful, ErrorMessage, AdditionalInfo to init-only (or private set with a single internal constructor) and keep SecurityLevelName internal as before; update the Success and Failure factory methods to call the internal constructor (or object initializer with init) so callers cannot mutate properties after creation and the result shape remains consistent (refer to type KeyOperationResult and factory methods Success and Failure).Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs (1)
24-25: Remove redundant null-conditional operator.Line 19 already checks
cipher is nulland returns early, so thecipher?.DoFinalat line 25 can be simplified tocipher.DoFinal.Suggested fix
var iv = cipher.GetIV(); -var cipherData = cipher?.DoFinal(Request.InputData); +var cipherData = cipher.DoFinal(Request.InputData);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs` around lines 24 - 25, Remove the redundant null-conditional operator on cipher when calling DoFinal: since the method in SecureAuthCallback.android.cs already returns early if cipher is null, replace the cipher?.DoFinal(Request.InputData) usage with cipher.DoFinal(Request.InputData) (same for any similar calls in the same method) so the code is clearer and avoids unnecessary null checks.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextCryptoHelpers.apple.cs (1)
46-50: Consider including the NSError details when biometric is unavailable.
CanEvaluatePolicyoutputs anNSErrorthat provides specific reasons why biometric isn't available (e.g., no enrolled biometrics, passcode not set). Including this in the error message would help with debugging.Suggested improvement
-if (!context.CanEvaluatePolicy(policy, out NSError _)) +if (!context.CanEvaluatePolicy(policy, out NSError evalError)) { context.Dispose(); - return (null, "Biometric authentication is not available on this device."); + return (null, $"Biometric authentication is not available: {evalError?.LocalizedDescription ?? "Unknown reason"}"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextCryptoHelpers.apple.cs` around lines 46 - 50, Change the CanEvaluatePolicy call to capture the NSError output and include its description in the returned error string; specifically, when calling context.CanEvaluatePolicy(policy, out NSError err) inside the method containing this logic, ensure you use the captured err (e.g., err.LocalizedDescription or err.ToString()) in the return tuple instead of the static "Biometric authentication is not available..." message, and still dispose the context (context.Dispose()) before returning so the error contains the NSError details for debugging.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs (2)
62-78: VerifyAsync should accept CancellationToken for API consistency.The method signature includes
CancellationToken tokenbut it's not passed toProcessVerifyAsync. While verification is likely fast, consider propagating the token for API consistency withSignAsyncand other methods.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs` around lines 62 - 78, VerifyAsync currently accepts a CancellationToken parameter (token) but does not pass it to LAContextCryptoHelpers.ProcessVerifyAsync, so the cancellation intent isn't propagated; update the call in VerifyAsync to forward the token to ProcessVerifyAsync (the same way SignAsync and other methods do) by adding the token argument to the ProcessVerifyAsync invocation while keeping algorithm: KeyAlgorithm.Ec and digest: Digest.Sha256 unchanged.
44-60: Consider allowing configurable algorithm/digest for signing.
SignAsynchardcodesKeyAlgorithm.EcandDigest.Sha256. If the caller created an RSA key and wants to sign with it, this method would use mismatched algorithm settings. Consider accepting algorithm/digest as parameters or deriving them from the key type.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs` around lines 44 - 60, SignAsync currently hardcodes KeyAlgorithm.Ec and Digest.Sha256 which will fail for non-EC keys; modify SignAsync (and its callers) to either accept KeyAlgorithm and Digest parameters or look up the key type for keyId and map it to the correct algorithm/digest before calling LAContextCryptoHelpers.ProcessSignAsync; update the method signature (SignAsync) and call sites to pass those values and ensure the algorithm/digest enums (KeyAlgorithm, Digest) are used consistently when invoking ProcessSignAsync.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsKeyVaultHelpers.windows.cs (1)
126-140: Document the security model for symmetric key retrieval.The comment notes that callers "should gate this with
UserConsentVerifier" before calling, but this is easy to miss. Consider adding a more prominent warning or enforcing this check within the method itself.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsKeyVaultHelpers.windows.cs` around lines 126 - 140, RetrieveSymmetricKey currently returns a symmetric key without enforcing user biometric/consent checks; change it to require and verify user consent inside the method by calling the Windows UserConsentVerifier APIs (check availability with UserConsentVerifier.CheckAvailabilityAsync and call UserConsentVerifier.RequestVerificationAsync with a clear prompt) before accessing PasswordVault, and return an error string (e.g., "User consent required" or the verifier's failure message) if availability/verification fails so callers cannot accidentally retrieve keys without biometric/user consent.Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs (1)
173-177: Inconsistent async patterns betweenEncryptAsyncandDecryptAsync.
EncryptAsync(line 173) uses expression-body returningTaskdirectly, whileDecryptAsync(line 176) usesasync/await. Both delegate to the same helper and should use the same pattern for consistency.♻️ Proposed fix for consistency
Either both without async:
public partial Task<SecureAuthenticationResponse> EncryptAsync(SecureAuthenticationRequest request, CancellationToken token) => BiometricPromptHelpers.ProcessCryptoAsync(request, CipherMode.EncryptMode, token); - public partial async Task<SecureAuthenticationResponse> DecryptAsync(SecureAuthenticationRequest request, CancellationToken token) - => await BiometricPromptHelpers.ProcessCryptoAsync(request, CipherMode.DecryptMode, token); + public partial Task<SecureAuthenticationResponse> DecryptAsync(SecureAuthenticationRequest request, CancellationToken token) + => BiometricPromptHelpers.ProcessCryptoAsync(request, CipherMode.DecryptMode, token);The non-async version avoids the state machine overhead when simply returning the task.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs` around lines 173 - 177, EncryptAsync and DecryptAsync use inconsistent async patterns: EncryptAsync returns the Task directly while DecryptAsync uses async/await; change DecryptAsync to return the Task directly (remove async/await) so both methods delegate to BiometricPromptHelpers.ProcessCryptoAsync using the same non-async expression-bodied pattern, e.g., have DecryptAsync => BiometricPromptHelpers.ProcessCryptoAsync(request, CipherMode.DecryptMode, token).Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs (1)
30-40: EC algorithm falls through to RSA handler with a misleading error message.When
request.AlgorithmisKeyAlgorithm.Ec, the ternary routes toProcessRsaCryptoAsync, which returns an error mentioning RSA but not EC. Consider handling EC explicitly for a clearer error message.♻️ Proposed improvement
public partial Task<SecureAuthenticationResponse> EncryptAsync( SecureAuthenticationRequest request, CancellationToken token) - => request.Algorithm == KeyAlgorithm.Aes - ? WindowsHelloCryptoHelpers.ProcessAesCryptoAsync(request, encrypt: true, token) - : WindowsHelloCryptoHelpers.ProcessRsaCryptoAsync(request, encrypt: true, token); + => request.Algorithm switch + { + KeyAlgorithm.Aes => WindowsHelloCryptoHelpers.ProcessAesCryptoAsync(request, encrypt: true, token), + KeyAlgorithm.Ec => Task.FromResult(SecureAuthenticationResponse.Failure( + "EC keys cannot be used for encrypt/decrypt. Use Sign/Verify instead.")), + _ => WindowsHelloCryptoHelpers.ProcessRsaCryptoAsync(request, encrypt: true, token) + };Apply the same pattern to
DecryptAsync.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs` around lines 30 - 40, EncryptAsync and DecryptAsync currently use a ternary that sends any non-Aes algorithm (including KeyAlgorithm.Ec) to WindowsHelloCryptoHelpers.ProcessRsaCryptoAsync, producing misleading RSA error messages; update both EncryptAsync and DecryptAsync to explicitly handle KeyAlgorithm.Ec (e.g., add an explicit branch for KeyAlgorithm.Ec that calls a new WindowsHelloCryptoHelpers.ProcessEcCryptoAsync(request, encrypt, token) or returns a clear, EC-specific Task-based error/NotSupportedException) instead of falling through to ProcessRsaCryptoAsync so errors correctly reference EC.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs`:
- Around line 17-21: The code currently uses
KeyGenerator.GetInstance(keyAlgorithm, KeyStoreName) in
TryCreateKeyWithSecurityLevel which only supports symmetric algorithms (AES) and
will fail for RSA/EC; modify TryCreateKeyWithSecurityLevel to branch on
algorithm type (symmetric vs asymmetric) and call KeyGenerator.GetInstance for
AES/other symmetric algorithms but call
KeyPairGenerator.GetInstance(keyAlgorithm, KeyStoreName) for RSA/EC, and
implement a CreateAsymmetricKey helper that builds the proper
KeyGenParameterSpec for asymmetric keys (using KeyPairGenerator, appropriate key
size/algorithm parameters and purposes) and uses it to generate the key pair.
Ensure you still return KeyOperationResult success/failure consistently from
TryCreateKeyWithSecurityLevel and CreateAsymmetricKey.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.cs`:
- Around line 73-77: The key creation uses ApplicationTag
(privateKeyAttrs.ApplicationTag) but lookup/delete/existence/retrieval code
mistakenly uses ApplicationLabel; update all queries that currently set
ApplicationLabel = keyId to instead set ApplicationTag =
NSData.FromString(keyId) so the same application-defined tag is used for
creation and lookup (apply this change in the methods that perform deletion,
existence check, and retrieval to ensure consistent use of ApplicationTag).
- Around line 307-313: The MapEncryptAlgorithm method currently maps only
Padding.Oaep and silently coerces other Padding values (e.g., Padding.None,
Padding.Pkcs7) to SecKeyAlgorithm.RsaEncryptionPkcs1; update MapEncryptAlgorithm
to explicitly handle all Padding enum values used elsewhere (Padding.Oaep,
Padding.Pkcs1, Padding.None, Padding.Pkcs7) and throw an ArgumentException or
NotSupportedException for unsupported paddings instead of falling back to
PKCS#1, so callers get a clear error; reference the MapEncryptAlgorithm method
and the Padding enum and ensure returned SecKeyAlgorithm values are only
RsaEncryptionOaepSha256 for Oaep and RsaEncryptionPkcs1 for a valid Pkcs1 case,
otherwise throw.
- Around line 24-30: The asymmetric key creation path currently ignores
options.RequireUserAuthentication; update the branch that chooses access control
(the code that switches on useSecureEnclave and currently picks
BiometryCurrentSet or UserPresence) to also check
options.RequireUserAuthentication and, when false, create an unauthenticated
access variant (e.g., use kSecAttrAccessibleWhenUnlocked or the equivalent
access constant) instead of always applying biometric/presence controls; locate
this logic near the CreateSymmetricKey/CreateAsymmetricKey code and the
BuildAccessControl usage and ensure RequireUserAuthentication is honored for
asymmetric keys just like the symmetric path.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsHelloCryptoHelpers.windows.cs`:
- Around line 41-49: The cancellation token is currently ignored because
token.Register(() => { }) is a no-op; before calling
UserConsentVerifier.RequestVerificationAsync you should honor pre-cancelled
tokens by checking token.IsCancellationRequested or calling
token.ThrowIfCancellationRequested and throw an
OperationCanceledException(token) (or return an appropriate cancelled result)
instead of registering an empty callback; remove the empty token.Register and
add the pre-call cancellation check near where RequestVerificationAsync is
invoked, keeping the existing return logic that compares to
UserConsentVerificationResult.Verified.
- Around line 185-188: ProcessVerifyAsync currently hardcodes
AsymmetricAlgorithmNames.RsaSignPkcs1Sha256 (via
AsymmetricKeyAlgorithmProvider.OpenAlgorithm/ImportPublicKey) which breaks
verification for non‑RSA keys; change ProcessVerifyAsync to use the actual
algorithm that was used when signing instead of the hardcoded RsaSignPkcs1Sha256
— either persist the algorithm name alongside the signature in RequestSignAsync
and pass it into ProcessVerifyAsync, or implement reliable algorithm detection
from the public key buffer returned by RetrievePublicKey() and use that
algorithm string with AsymmetricKeyAlgorithmProvider.OpenAlgorithm before
calling ImportPublicKey; update the signature payload and call sites so
ProcessVerifyAsync receives/reads the algorithm and uses it for verification.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricAuthenticationService.cs`:
- Around line 5-10: The Lazy initializer for the library-wide default uses
PublicationOnly which can run the factory multiple times; change the Lazy
creation in the static field defaultImpl to use
LazyThreadSafetyMode.ExecutionAndPublication so the factory for new
SecureBiometricService is executed serially and only one instance is created and
published; update the expression that constructs defaultImpl (the
Lazy<ISecureBiometric> new(...) initializer referring to SecureBiometricService)
to use ExecutionAndPublication while leaving the Default getter returning
defaultImpl.Value unchanged.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs`:
- Around line 179-202: The Android implementations of SignAsync and VerifyAsync
in SecureBiometricService.android.cs are incomplete stubs that always return a
failure; replace the unconditional failure return with an explicit
NotImplemented signal: in the SignAsync and VerifyAsync methods
(SecureBiometricService.SignAsync and SecureBiometricService.VerifyAsync) remove
the Task.FromResult(... "Key not found or operation canceled.") and either
implement the platform-specific signing/verification logic or, if you cannot
implement it now, make the methods throw NotImplementedException (or return
Task.FromException<SecureAuthenticationResponse>(new NotImplementedException()))
so callers get a clear, explicit failure instead of a silent functional failure.
In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Digest.cs`:
- Line 6: Mark the Digest enum member Sha1 as deprecated for new usage by
annotating it with [Obsolete("SHA-1 is insecure; use stronger digests for new
code. Kept only for legacy interop.")] and hide it from autocompletion by adding
[EditorBrowsable(EditorBrowsableState.Never)] (or equivalent attribute in the
project language) while leaving the member in place; update any XML docs on the
Digest enum to note it's retained for legacy interop only so callers see the
guidance when hovering over Digest.Sha1.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs`:
- Around line 10-16: The Success factory method on SecureAuthenticationResponse
currently calls outputData.Clone() without validating outputData; add a null
check/guard at the start of SecureAuthenticationResponse.Success to throw
ArgumentNullException (or similar) when outputData is null, then continue to
clone and assign OutputData and IV as before so you avoid a
NullReferenceException from outputData.Clone().
---
Duplicate comments:
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs`:
- Around line 5-6: Replace the Obsolete attribute on the IsPlatformSupported
property to fix the "obselete" typo and change the message to a neutral
guidance: indicate the property is obsolete and recommend using runtime
capability checks (e.g., call GetIsPlatformSupported or handle
PlatformNotImplemented/NotImplemented fallback) rather than claiming "library
now supports all platforms"; update the attribute text on IsPlatformSupported to
a corrected, concise message referencing GetIsPlatformSupported so callers know
to use runtime checks instead.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs`:
- Around line 124-128: When registering the cancellation callback around
biometricPrompt.Authenticate, also complete the authentication
TaskCompletionSource so it cannot hang: update the token.Register callback (used
where biometricPrompt.CancelAuthentication() is invoked) to call
authCallback.Response.TrySetCanceled(token) (or TrySetCanceled() if token not
available) after calling CancelAuthentication(); keep the
using(token.Register(...)) pattern and use TrySetCanceled/TrySetException to
avoid throwing if the TCS is already completed, referencing
biometricPrompt.Authenticate, biometricPrompt.CancelAuthentication(),
token.Register and authCallback.Response.Task.
- Around line 55-61: InitCipher currently initializes the Cipher without any IV
which breaks decryption for modes like AES-GCM/AES-CBC; change the signature of
InitCipher to accept a byte[] iv (nullable) and, inside the method, if iv is
provided use the appropriate parameter spec when calling Cipher.Init — for
AES-CBC use new IvParameterSpec(iv) and for AES-GCM use new
GCMParameterSpec(128, iv) (or the correct TAG length) passed alongside the key;
otherwise call Cipher.Init(mode, key) for cases that don’t need an IV. Update
callers (e.g., the site mentioned around line 111) to pass the original
encryption IV when mode == CipherMode.Decrypt.
In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs`:
- Around line 30-35: The validation currently allows non-byte-aligned key sizes
(e.g., 257) so later code that does options.KeySize / 8 silently truncates the
size; update the validators in KeyCreationHelpers (the checks using
IsKeySizeInvalid, IsRsaKeySizeInvalid, IsEcKeySizeInvalid) to also reject sizes
that are not divisible by 8 (options.KeySize % 8 != 0) and return a clear
KeyOperationResult.Failure like "Key size must be a whole number of bytes
(multiple of 8)." Apply the same additional check for RSA and EC paths so
options.KeySize is always byte-aligned before any code that computes
options.KeySize / 8.
In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BlockMode.cs`:
- Line 9: The BlockMode enum currently exposes the insecure Ecb value publicly;
mark Ecb as interop-only and discourage use by adding attributes and docs:
annotate the Ecb enum member with [Obsolete("Interop-only: ECB is insecure, do
not use.", false)] and
[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)],
add an XML summary comment stating "Interop-only, insecure — do not use", and
ensure any codepaths selecting BlockMode.Ecb remain unchanged for compatibility;
this keeps the member available for interop but visibly discouraged to
consumers.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs`:
- Around line 12-18: The Transformation property builds invalid strings for RSA
because BlockMode.None is mapped to GCM; update Transformation to be
algorithm-aware: when Algorithm == KeyAlgorithm.Rsa and BlockMode ==
BlockMode.None use ECB (e.g., KeyProperties.BlockModeEcb) for the block mode
mapping, otherwise use AndroidKeyStoreHelpers.MapBlockMode(BlockMode); then call
AndroidKeyStoreHelpers.MapKeyAlgorithm(Algorithm) and
AndroidKeyStoreHelpers.MapPadding(Padding) as before and pass the chosen
blockMode into MapTransformation so RSA transformations become "RSA/ECB/..."
while AES still uses GCM when appropriate.
---
Nitpick comments:
In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.cs`:
- Around line 5-8: GetFullMessage currently accepts a non-nullable Exception but
defensively handles null; change the parameter to a nullable Exception? ex in
the GetFullMessage method signature to make null-tolerance explicit, update any
XML docs or comments for GetFullMessage to reflect the nullable parameter, and
review/correct all call sites (or enable nullable warnings) if they need
adjustment for the new signature so the behavior (returning string.Empty for
null) remains unchanged.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextCryptoHelpers.apple.cs`:
- Around line 46-50: Change the CanEvaluatePolicy call to capture the NSError
output and include its description in the returned error string; specifically,
when calling context.CanEvaluatePolicy(policy, out NSError err) inside the
method containing this logic, ensure you use the captured err (e.g.,
err.LocalizedDescription or err.ToString()) in the return tuple instead of the
static "Biometric authentication is not available..." message, and still dispose
the context (context.Dispose()) before returning so the error contains the
NSError details for debugging.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsKeyVaultHelpers.windows.cs`:
- Around line 126-140: RetrieveSymmetricKey currently returns a symmetric key
without enforcing user biometric/consent checks; change it to require and verify
user consent inside the method by calling the Windows UserConsentVerifier APIs
(check availability with UserConsentVerifier.CheckAvailabilityAsync and call
UserConsentVerifier.RequestVerificationAsync with a clear prompt) before
accessing PasswordVault, and return an error string (e.g., "User consent
required" or the verifier's failure message) if availability/verification fails
so callers cannot accidentally retrieve keys without biometric/user consent.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs`:
- Around line 24-25: Remove the redundant null-conditional operator on cipher
when calling DoFinal: since the method in SecureAuthCallback.android.cs already
returns early if cipher is null, replace the cipher?.DoFinal(Request.InputData)
usage with cipher.DoFinal(Request.InputData) (same for any similar calls in the
same method) so the code is clearer and avoids unnecessary null checks.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs`:
- Around line 173-177: EncryptAsync and DecryptAsync use inconsistent async
patterns: EncryptAsync returns the Task directly while DecryptAsync uses
async/await; change DecryptAsync to return the Task directly (remove
async/await) so both methods delegate to
BiometricPromptHelpers.ProcessCryptoAsync using the same non-async
expression-bodied pattern, e.g., have DecryptAsync =>
BiometricPromptHelpers.ProcessCryptoAsync(request, CipherMode.DecryptMode,
token).
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs`:
- Around line 62-78: VerifyAsync currently accepts a CancellationToken parameter
(token) but does not pass it to LAContextCryptoHelpers.ProcessVerifyAsync, so
the cancellation intent isn't propagated; update the call in VerifyAsync to
forward the token to ProcessVerifyAsync (the same way SignAsync and other
methods do) by adding the token argument to the ProcessVerifyAsync invocation
while keeping algorithm: KeyAlgorithm.Ec and digest: Digest.Sha256 unchanged.
- Around line 44-60: SignAsync currently hardcodes KeyAlgorithm.Ec and
Digest.Sha256 which will fail for non-EC keys; modify SignAsync (and its
callers) to either accept KeyAlgorithm and Digest parameters or look up the key
type for keyId and map it to the correct algorithm/digest before calling
LAContextCryptoHelpers.ProcessSignAsync; update the method signature (SignAsync)
and call sites to pass those values and ensure the algorithm/digest enums
(KeyAlgorithm, Digest) are used consistently when invoking ProcessSignAsync.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs`:
- Around line 30-40: EncryptAsync and DecryptAsync currently use a ternary that
sends any non-Aes algorithm (including KeyAlgorithm.Ec) to
WindowsHelloCryptoHelpers.ProcessRsaCryptoAsync, producing misleading RSA error
messages; update both EncryptAsync and DecryptAsync to explicitly handle
KeyAlgorithm.Ec (e.g., add an explicit branch for KeyAlgorithm.Ec that calls a
new WindowsHelloCryptoHelpers.ProcessEcCryptoAsync(request, encrypt, token) or
returns a clear, EC-specific Task-based error/NotSupportedException) instead of
falling through to ProcessRsaCryptoAsync so errors correctly reference EC.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs`:
- Around line 8-12: Remove the blanket "#nullable disable" and make the UI
properties explicit: update BaseAuthenticationRequest to either mark Title and
NegativeText as required (and Subtitle as required or nullable depending on
platform needs) using the C# required modifier, or initialize them with sensible
non-null defaults; ensure the properties Title, Subtitle, and NegativeText in
BaseAuthenticationRequest reflect the chosen approach and add a short comment
documenting which properties are required per platform (e.g., Android requires
Title and NegativeText).
In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs`:
- Around line 5-25: Make KeyOperationResult immutable by removing public setters
and only allowing values to be set at construction: change WasSuccessful,
ErrorMessage, AdditionalInfo to init-only (or private set with a single internal
constructor) and keep SecurityLevelName internal as before; update the Success
and Failure factory methods to call the internal constructor (or object
initializer with init) so callers cannot mutate properties after creation and
the result shape remains consistent (refer to type KeyOperationResult and
factory methods Success and Failure).
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs`:
- Line 7: The IV property on SecureAuthenticationResponse is the only one with a
public setter, breaking the immutability pattern; change the IV property's
setter to private (byte[]? IV { get; private set; }) and ensure the static
factory method Success is the only place that assigns it (set IV inside the
Success factory), keeping other properties' private setters unchanged.
In `@Plugin.Maui.Biometric/Samples/BiometricPage.xaml.cs`:
- Around line 37-39: The comment in BiometricPage.xaml.cs is misleading because
the code calls _biometric.AuthenticateAsync(req, CancellationToken.None) inside
this.Dispatcher.DispatchAsync; either remove or update the comment and sample to
demonstrate real cancellation: create a CancellationTokenSource (e.g., cts),
pass cts.Token into _biometric.AuthenticateAsync(req, cts.Token) and show
cancelling via cts.Cancel(), or simply delete the “pass a valid token” line if
you want to keep the sample minimal; update the nearby Dispatcher.DispatchAsync
usage and any variable names (req, response) accordingly.
- Line 45: Console.Write(response) will likely print the type name; replace it
with a meaningful serialization or property-level log of the
AuthenticationResponse so diagnostics are useful. Locate the Console.Write call
in BiometricPage.xaml.cs and change it to either
Console.WriteLine(JsonSerializer.Serialize(response)) (import System.Text.Json)
or explicitly log key properties from the AuthenticationResponse (e.g.,
response.IsAuthenticated, response.ErrorMessage, response.Timestamp) to produce
human-readable output.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b83d9b8d-3b97-4464-b498-1e892360615e
📒 Files selected for processing (65)
Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/CryptoKeyOptionsTests.csPlugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/ExceptionExtensionsTests.csPlugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/Fakes/FakeSecureBiometricService.csPlugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/KeyCreationHelpersTests.csPlugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/KeyOperationResultTests.csPlugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/Plugin.Maui.Biometric.Tests.csprojPlugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/SecureAuthenticationRequestTests.csPlugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/SecureAuthenticationResponseTests.csPlugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/SecureBiometricAuthenticationServiceTests.csPlugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/SecureBiometricServiceContractTests.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/AuthCallback.android.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricAuthenticationService.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.apple.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.net.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.windows.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/IBiometric.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/BiometricService.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextCryptoHelpers.apple.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsHelloCryptoHelpers.windows.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsKeyVaultHelpers.windows.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/ISecureBiometric.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricAuthenticationService.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/AuthenticationType.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/AuthenticatorStrength.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BiometricHwStatus.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BiometricResponseStatus.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BiometricType.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BlockMode.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/CryptoOperation.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Digest.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/KeyAlgorithm.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Padding.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationRequest.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationResponse.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.csPlugin.Maui.Biometric/Plugin.Maui.Biometric/Plugin.Maui.Biometric.csprojPlugin.Maui.Biometric/Samples/AppShell.xamlPlugin.Maui.Biometric/Samples/AppShell.xaml.csPlugin.Maui.Biometric/Samples/BiometricPage.xamlPlugin.Maui.Biometric/Samples/BiometricPage.xaml.csPlugin.Maui.Biometric/Samples/HomePage.xamlPlugin.Maui.Biometric/Samples/HomePage.xaml.csPlugin.Maui.Biometric/Samples/MainPage.xamlPlugin.Maui.Biometric/Samples/MainPage.xaml.csPlugin.Maui.Biometric/Samples/MauiProgram.csPlugin.Maui.Biometric/Samples/SecureBiometricPage.xamlPlugin.Maui.Biometric/Samples/SecureBiometricPage.xaml.cs
💤 Files with no reviewable changes (3)
- Plugin.Maui.Biometric/Plugin.Maui.Biometric/BiometricService.cs
- Plugin.Maui.Biometric/Samples/MainPage.xaml
- Plugin.Maui.Biometric/Samples/MainPage.xaml.cs
| using var keyGen = KeyGenerator.GetInstance(keyAlgorithm, KeyStoreName); | ||
| if (keyGen == null) | ||
| { | ||
| return KeyOperationResult.Failure($"Failed to create key generator for algorithm {keyAlgorithm}."); | ||
| } |
There was a problem hiding this comment.
KeyGenerator only supports symmetric algorithms; RSA/EC key creation will fail.
TryCreateKeyWithSecurityLevel uses KeyGenerator.GetInstance(keyAlgorithm, ...) which only works for AES. For RSA and EC keys, you must use KeyPairGenerator.GetInstance(...) instead. This will cause runtime failures when attempting to create asymmetric keys.
🔧 Suggested approach
Branch on the algorithm type:
internal static KeyOperationResult TryCreateKeyWithSecurityLevel
(string keyId, string keyAlgorithm, KeyStorePurpose purpose,
CryptoKeyOptions options, bool preferStrongBox)
{
try
{
- using var keyGen = KeyGenerator.GetInstance(keyAlgorithm, KeyStoreName);
- if (keyGen == null)
- {
- return KeyOperationResult.Failure($"Failed to create key generator for algorithm {keyAlgorithm}.");
- }
+ // For symmetric keys (AES), use KeyGenerator
+ // For asymmetric keys (RSA, EC), use KeyPairGenerator
+ if (options.Algorithm == KeyAlgorithm.Aes)
+ {
+ return CreateSymmetricKey(keyId, keyAlgorithm, purpose, options, preferStrongBox);
+ }
+ else
+ {
+ return CreateAsymmetricKey(keyId, keyAlgorithm, purpose, options, preferStrongBox);
+ }Then implement CreateAsymmetricKey using KeyPairGenerator.GetInstance(keyAlgorithm, KeyStoreName) with appropriate KeyGenParameterSpec configuration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs`
around lines 17 - 21, The code currently uses
KeyGenerator.GetInstance(keyAlgorithm, KeyStoreName) in
TryCreateKeyWithSecurityLevel which only supports symmetric algorithms (AES) and
will fail for RSA/EC; modify TryCreateKeyWithSecurityLevel to branch on
algorithm type (symmetric vs asymmetric) and call KeyGenerator.GetInstance for
AES/other symmetric algorithms but call
KeyPairGenerator.GetInstance(keyAlgorithm, KeyStoreName) for RSA/EC, and
implement a CreateAsymmetricKey helper that builds the proper
KeyGenParameterSpec for asymmetric keys (using KeyPairGenerator, appropriate key
size/algorithm parameters and purposes) and uses it to generate the key pair.
Ensure you still return KeyOperationResult success/failure consistently from
TryCreateKeyWithSecurityLevel and CreateAsymmetricKey.
| internal static KeyOperationResult CreateSymmetricKey(string keyId, CryptoKeyOptions options) | ||
| { | ||
| try | ||
| { | ||
| var keyBytes = RandomNumberGenerator.GetBytes(options.KeySize / 8); | ||
|
|
||
| var sac = BuildAccessControl(options.RequireUserAuthentication); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "AppleKeychainHelpers.apple.cs" -type fRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 157
🏁 Script executed:
cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 15200
🌐 Web query:
Apple SecAccessControlCreateFlags UserPresence vs BiometryCurrentSet security
💡 Result:
Apple's SecAccessControlCreateFlags provide kSecAccessControlUserPresence and kSecAccessControlBiometryCurrentSet as constraints for keychain item access. kSecAccessControlUserPresence requires either biometry (Touch ID/Face ID) or device passcode. Biometry does not need to be enrolled initially; if enrolled later, it works. Changes to biometrics (adding/removing fingers or Face ID re-enrollment) do not invalidate the item. It is equivalent to biometryAny OR devicePasscode. kSecAccessControlBiometryCurrentSet requires biometry with the currently enrolled set (Touch ID fingers or Face ID user). Biometry must be available and enrolled at creation. The item is invalidated if biometrics change (fingers added/removed or Face ID re-enrolled). It cannot fallback to passcode alone without additional flags like .or .devicePasscode, but creation may fail if biometry is not enrolled. Security comparison: BiometryCurrentSet is stricter/higher security because it enforces the exact enrolled biometric set at creation time and invalidates on changes, preventing access with altered biometrics. UserPresence is more flexible/lower security as it allows passcode fallback unconditionally and survives biometric changes. Use BiometryCurrentSet for sensitive data where biometric stability is critical; UserPresence for broader compatibility.
Citations:
- 1: https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/biometrycurrentset
- 2: https://developer.apple.com/documentation/security/secaccesscontrolcreateflags
- 3: https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/userpresence?language=objc
- 4: https://developer.apple.com/documentation/Security/SecAccessControlCreateFlags/userPresence
- 5: https://stackoverflow.com/questions/66577841/keychain-secitemadd-fails-when-using-secaccesscontrol-with-passcode-biometry
- 6: https://developer.apple.com/forums/thread/122531
- 7: https://mas.owasp.org/MASTG-DEMO-0045/
- 8: https://developer.apple.com/documentation/security/restricting-keychain-item-accessibility
🏁 Script executed:
rg -B 3 -A 3 "CreateAsymmetricKey" ./Plugin.Maui.Biometric/ --type csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 3675
🏁 Script executed:
sed -n '30,60p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1663
🏁 Script executed:
grep -B 10 "CreateSymmetricKey\|CreateAsymmetricKey" ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs | head -20Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 612
🏁 Script executed:
rg "RequireUserAuthentication" ./Plugin.Maui.Biometric/ --type cs -B 2 -A 2Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 4540
Asymmetric path ignores RequireUserAuthentication option entirely.
The symmetric path respects options.RequireUserAuthentication and passes it to BuildAccessControl() at line 30. However, the asymmetric path at lines 63–70 branches only on useSecureEnclave and has no conditional logic based on the option. This means callers cannot disable authentication for asymmetric keys—the option is silently ignored.
The asymmetric path should also branch on RequireUserAuthentication (lines 66–68), creating an unauthenticated access control variant (e.g., kSecAttrAccessibleWhenUnlocked) when the option is false, rather than always applying either BiometryCurrentSet or UserPresence.
Also applies to: 63–70
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.cs`
around lines 24 - 30, The asymmetric key creation path currently ignores
options.RequireUserAuthentication; update the branch that chooses access control
(the code that switches on useSecureEnclave and currently picks
BiometryCurrentSet or UserPresence) to also check
options.RequireUserAuthentication and, when false, create an unauthenticated
access variant (e.g., use kSecAttrAccessibleWhenUnlocked or the equivalent
access constant) instead of always applying biometric/presence controls; locate
this logic near the CreateSymmetricKey/CreateAsymmetricKey code and the
BuildAccessControl usage and ensure RequireUserAuthentication is honored for
asymmetric keys just like the symmetric path.
| var privateKeyAttrs = new SecKeyParameters | ||
| { | ||
| IsPermanent = true, | ||
| ApplicationTag = NSData.FromString(keyId), | ||
| AccessControl = sac |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's see the file structure and locate the file
find . -name "AppleKeychainHelpers.apple.cs" -type fRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 157
🏁 Script executed:
# Once located, let's read the relevant sections
# Lines 73-77 (creation), 123-126, 169-172, 223-228, 249-253
wc -l ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 161
🏁 Script executed:
# Read the file with specific line ranges
sed -n '60,90p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1358
🏁 Script executed:
# Check lines around 123-126
sed -n '110,140p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1302
🏁 Script executed:
# Check lines around 169-172
sed -n '155,185p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1180
🏁 Script executed:
# Check lines around 223-228
sed -n '210,240p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1176
🏁 Script executed:
# Check lines around 249-253
sed -n '235,260p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 910
🏁 Script executed:
# Search for all occurrences of ApplicationTag and ApplicationLabel
rg "ApplicationTag|ApplicationLabel" ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.cs -nRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 403
🌐 Web query:
Apple Security framework ApplicationTag vs ApplicationLabel kSecAttrApplicationTag kSecAttrApplicationLabel
💡 Result:
In Apple's Security framework, specifically for Keychain Services items of class kSecClassKey (cryptographic keys), kSecAttrApplicationTag and kSecAttrApplicationLabel are both attributes used to identify and uniquely distinguish keys. Both are components of the composite primary key that determines key uniqueness, along with attributes like kSecAttrKeyType, kSecAttrKeySizeInBits, kSecAttrKeyClass, kSecAttrEffectiveKeySize, etc. Duplicate items are not added if all primary key attributes match (errSecDuplicateItem). kSecAttrApplicationTag (type: CFData): - Arbitrary data set by the application to tag/identify the key. - Used primarily for app-defined lookup and retrieval (e.g., via SecItemCopyMatching with the tag). - On macOS legacy keychain, corresponds to kSecKeyApplicationTag; appears in Keychain Access "Comments" field. - Commonly used as a unique identifier string converted to data (e.g., "com.example.mykey".data(using: .utf8)!). kSecAttrApplicationLabel (type: CFData, often SHA-1 public key hash or UUID): - For asymmetric keys (kSecAttrKeyClassPublic or kSecAttrKeyClassPrivate), conventionally holds the SHA-1 hash of the public key's SubjectPublicKeyInfo (matching kSecAttrPublicKeyHash on associated certificates for digital identity formation). - Not human-readable; used for programmatic lookup. - On macOS legacy keychain, corresponds to kSecKeyLabel. - Apps should not arbitrarily override for asymmetric keys, as it may interfere with system expectations. In contrast, kSecAttrLabel (type: CFString) is human-readable (e.g., displays in Keychain Access "Name" column on macOS), not part of uniqueness, and optional for description. Both ApplicationTag and ApplicationLabel enable precise key retrieval but serve complementary roles: Tag for app-specific identification, Label for key-derived identity (especially asymmetric keys).
Citations:
- 1: https://developer.apple.com/documentation/security/ksecattrapplicationlabel
- 2: https://developer.apple.com/forums/thread/70959
- 3: https://developer.apple.com/documentation/security/ksecattrapplicationlabel?language=objc
- 4: https://developer.apple.com/documentation/security/keychain-item-attribute-constants-for-keys
- 5: https://developer.apple.com/documentation/security/ksecclasskey
- 6: https://developer.apple.com/documentation/security/key-generation-attributes?changes=l_3_5
- 7: https://developer.apple.com/documentation/security/storing-keys-in-the-keychain
🌐 Web query:
Apple Keychain Services SecRecord ApplicationLabel ApplicationTag difference SecKind.Key
💡 Result:
In Apple Keychain Services, SecRecord is a Xamarin.iOS/.NET for iOS class that wraps the CFDictionary used with SecItem functions (like SecItemAdd, SecItemCopyMatching) for interacting with keychain items. It corresponds to SecKind.Key for cryptographic key items (kSecClassKey). ApplicationLabel (kSecAttrApplicationLabel) and ApplicationTag (kSecAttrApplicationTag) are both CFData attributes used in SecRecord for key items: - Both are components of key uniqueness (along with kSecAttrKeyType, kSecAttrKeySizeInBits, kSecAttrKeyClass, etc.). Duplicate items are rejected only if all uniqueness attributes match. - ApplicationLabel ('klbl'): For asymmetric keys (public/private), automatically set to the SHA-1 hash of the public key's SubjectPublicKeyInfo bit string, enabling digital identity matching with certificates (must match kSecAttrPublicKeyHash in cert). Not user-visible. Apps should not override for asymmetric keys. - ApplicationTag ('atag'): App-defined private tag/opaque data for custom identification. Content is entirely up to the app (e.g., for retrieving specific keys). On macOS legacy keychain, appears in Comments (Get Info). Currently unused in some key constants docs but valid for uniqueness. kSecAttrLabel is separate: user-visible name (not uniqueness component), shows in Keychain Access Name column on macOS. For SecKind.Key items, use ApplicationTag to uniquely identify/retrieve app-specific keys; ApplicationLabel is system-managed for asymmetric keys.
Citations:
- 1: https://developer.apple.com/forums/thread/70959
- 2: https://developer.apple.com/documentation/security/ksecclasskey
- 3: https://developer.apple.com/documentation/security/keychain-item-attribute-constants-for-keys
- 4: https://developer.apple.com/forums/thread/113787
- 5: https://learn.microsoft.com/en-us/dotnet/api/security.secrecord?view=net-ios-26.2-10.0
🌐 Web query:
iOS macOS Keychain RSA EC key ApplicationLabel ApplicationTag which attribute query
💡 Result:
In iOS and macOS Keychain Services, RSA (kSecAttrKeyTypeRSA) and EC (kSecAttrKeyTypeEC) keys are stored as kSecClassKey items. Key attributes include kSecAttrApplicationLabel (CFData, 'klbl'; app-defined label, part of uniqueness for asymmetric keys often set to public key hash for identity matching) and kSecAttrApplicationTag (CFData, 'atag'; private app tag for identification, part of uniqueness, app-defined content like "com.example.key"). These are set during key generation (e.g., SecKeyCreateRandomKey parameters dictionary, or kSecPrivateKeyAttrs/kSecPublicKeyAttrs) or SecItemAdd. Both are components of the composite primary key for uniqueness (with kSecAttrKeyType, kSecAttrKeySizeInBits, kSecAttrKeyClass, etc.), preventing duplicates on SecItemAdd. To query RSA/EC keys by these attributes, use SecItemCopyMatching with a dictionary like: let query: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrApplicationTag as String: tagData, // NSData/CFData kSecAttrApplicationLabel as String: labelData, // optional, for more specificity kSecAttrKeyType as String: kSecAttrKeyTypeRSA, // or kSecAttrKeyTypeECSECPrimeRandomIndex kSecReturnRef as String: true ] var result: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &result) if status == errSecSuccess, let key = result as? SecKey { ... } kSecAttrApplicationTag is commonly used for retrieval (examples in Apple docs), as it's fully app-controlled. kSecAttrApplicationLabel may be auto-set for asymmetric keys (public key SHA-1 hash). Both can be specified in queries to find specific keys. Use kSecReturnAttributes: true to inspect returned attributes. Sources confirm these apply to RSA/EC keys (kSecClassKey attributes).
Citations:
- 1: https://developer.apple.com/documentation/security/storing-keys-in-the-keychain
- 2: https://developer.apple.com/documentation/security/keychain-item-attribute-constants-for-keys
- 3: https://developer.apple.com/documentation/security/key-generation-attributes?changes=l_3_5
- 4: https://developer.apple.com/documentation/security/ksecclasskey
- 5: https://developer.apple.com/forums/thread/70959
- 6: https://developer.apple.com/forums/thread/113787
- 7: https://developer.apple.com/documentation/security/seckeycreaterandomkey(_:_:)
Use ApplicationTag consistently for key lookup—ApplicationLabel is system-managed for asymmetric keys.
The key pair is created with ApplicationTag = NSData.FromString(keyId) (correct), but delete/existence/retrieval operations query using ApplicationLabel = keyId (incorrect). According to Apple's Keychain Services documentation, ApplicationLabel for asymmetric keys (RSA/EC) is automatically set to the SHA-1 hash of the public key's SubjectPublicKeyInfo—it is system-managed and should not be overridden by applications. In contrast, ApplicationTag is app-defined and is the correct attribute for custom key identification and retrieval. The current mismatch means the keys created won't be found by subsequent lookup queries. Use ApplicationTag consistently across creation, deletion, existence checks, and retrieval operations (lines 125, 171, 225, 251).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.cs`
around lines 73 - 77, The key creation uses ApplicationTag
(privateKeyAttrs.ApplicationTag) but lookup/delete/existence/retrieval code
mistakenly uses ApplicationLabel; update all queries that currently set
ApplicationLabel = keyId to instead set ApplicationTag =
NSData.FromString(keyId) so the same application-defined tag is used for
creation and lookup (apply this change in the methods that perform deletion,
existence check, and retrieval to ensure consistent use of ApplicationTag).
| /// <summary>Maps the requested padding to the correct RSA encryption algorithm.</summary> | ||
| internal static SecKeyAlgorithm MapEncryptAlgorithm(Padding padding) => | ||
| padding switch | ||
| { | ||
| Padding.Oaep => SecKeyAlgorithm.RsaEncryptionOaepSha256, | ||
| _ => SecKeyAlgorithm.RsaEncryptionPkcs1 | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find the Padding enum definition
find . -type f -name "*.cs" | head -20Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1787
🏁 Script executed:
# Search for Padding enum definition
rg "enum Padding" --type csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 155
🏁 Script executed:
# Look at the full AppleKeychainHelpers file context
fd "AppleKeychainHelpers" --type fRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 155
🏁 Script executed:
# Search for MapEncryptAlgorithm usages
rg "MapEncryptAlgorithm" --type cs -A 3 -B 3Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1769
🏁 Script executed:
# Get the full Padding enum definition
cat -n Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Padding.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 236
🏁 Script executed:
# Check ValidateRequest to see what validation happens
rg "ValidateRequest" --type cs -A 15 | head -60Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 7225
🏁 Script executed:
# Get the full AppleKeychainHelpers file to understand the context
wc -l Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 159
🏁 Script executed:
# Look at the context around MapEncryptAlgorithm and how it's called
sed -n '290,330p' Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1908
🏁 Script executed:
# Check LAContextCryptoHelpers to see the validation that occurs before MapEncryptAlgorithm is called
rg "ValidateRequest" Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextCryptoHelpers.apple.cs -A 40 | head -80Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 3502
🏁 Script executed:
# Check if there's validation of Padding anywhere in the codebase
rg "Padding\." --type cs | rg -v "test|Test" | head -40Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1885
🏁 Script executed:
# Check SecureAuthenticationRequest to see if it validates Padding
rg "class SecureAuthenticationRequest" --type cs -A 30Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 5621
🏁 Script executed:
# See if there are any tests that show expected behavior for invalid padding
rg "MapEncryptAlgorithm|Padding\.(None|Pkcs7)" --type cs -B 2 -A 2Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 11554
🏁 Script executed:
# Check if there's any validation of Padding values during request processing
rg "request\.Padding" --type cs -B 3 -A 3Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 2710
MapEncryptAlgorithm silently coerces unsupported RSA paddings to PKCS#1.
The method accepts all four Padding enum values but only handles Padding.Oaep explicitly. Invalid values like Padding.None and Padding.Pkcs7 (designed for symmetric encryption) are silently mapped to RsaEncryptionPkcs1, causing the encryption algorithm to diverge from the caller's request.
Unlike the Android implementation, which explicitly handles all Padding values, the Apple version provides no validation. Reject unsupported paddings during validation or throw here.
Suggested fix
internal static SecKeyAlgorithm MapEncryptAlgorithm(Padding padding) =>
padding switch
{
Padding.Oaep => SecKeyAlgorithm.RsaEncryptionOaepSha256,
- _ => SecKeyAlgorithm.RsaEncryptionPkcs1
+ Padding.Pkcs1 => SecKeyAlgorithm.RsaEncryptionPkcs1,
+ _ => throw new ArgumentOutOfRangeException(nameof(padding), padding, "Unsupported RSA padding.")
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /// <summary>Maps the requested padding to the correct RSA encryption algorithm.</summary> | |
| internal static SecKeyAlgorithm MapEncryptAlgorithm(Padding padding) => | |
| padding switch | |
| { | |
| Padding.Oaep => SecKeyAlgorithm.RsaEncryptionOaepSha256, | |
| _ => SecKeyAlgorithm.RsaEncryptionPkcs1 | |
| }; | |
| /// <summary>Maps the requested padding to the correct RSA encryption algorithm.</summary> | |
| internal static SecKeyAlgorithm MapEncryptAlgorithm(Padding padding) => | |
| padding switch | |
| { | |
| Padding.Oaep => SecKeyAlgorithm.RsaEncryptionOaepSha256, | |
| Padding.Pkcs1 => SecKeyAlgorithm.RsaEncryptionPkcs1, | |
| _ => throw new ArgumentOutOfRangeException(nameof(padding), padding, "Unsupported RSA padding.") | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.cs`
around lines 307 - 313, The MapEncryptAlgorithm method currently maps only
Padding.Oaep and silently coerces other Padding values (e.g., Padding.None,
Padding.Pkcs7) to SecKeyAlgorithm.RsaEncryptionPkcs1; update MapEncryptAlgorithm
to explicitly handle all Padding enum values used elsewhere (Padding.Oaep,
Padding.Pkcs1, Padding.None, Padding.Pkcs7) and throw an ArgumentException or
NotSupportedException for unsupported paddings instead of falling back to
PKCS#1, so callers get a clear error; reference the MapEncryptAlgorithm method
and the Padding enum and ensure returned SecKeyAlgorithm values are only
RsaEncryptionOaepSha256 for Oaep and RsaEncryptionPkcs1 for a valid Pkcs1 case,
otherwise throw.
| try | ||
| { | ||
| using (token.Register(() => { })) | ||
| { | ||
| var result = await UserConsentVerifier.RequestVerificationAsync(message); | ||
| return result == UserConsentVerificationResult.Verified | ||
| ? (true, null) | ||
| : (false, $"Authentication failed: {result}"); | ||
| } |
There was a problem hiding this comment.
Cancellation token has no effect on the authentication request.
The token.Register(() => { }) registers an empty callback that does nothing. UserConsentVerifier.RequestVerificationAsync is not cancellable via CancellationToken, so the token is effectively ignored. Consider documenting this limitation or throwing OperationCanceledException before calling the API if the token is already cancelled.
🛡️ Proposed fix to honor pre-cancelled tokens
private static async Task<(bool verified, string? error)> AuthenticateAsync(
string message, CancellationToken token)
{
+ token.ThrowIfCancellationRequested();
+
var availability = await UserConsentVerifier.CheckAvailabilityAsync();
if (availability != UserConsentVerifierAvailability.Available)
return (false, "Windows Hello is not available on this device.");
try
{
- using (token.Register(() => { }))
- {
- var result = await UserConsentVerifier.RequestVerificationAsync(message);
- return result == UserConsentVerificationResult.Verified
- ? (true, null)
- : (false, $"Authentication failed: {result}");
- }
+ var result = await UserConsentVerifier.RequestVerificationAsync(message);
+ return result == UserConsentVerificationResult.Verified
+ ? (true, null)
+ : (false, $"Authentication failed: {result}");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try | |
| { | |
| using (token.Register(() => { })) | |
| { | |
| var result = await UserConsentVerifier.RequestVerificationAsync(message); | |
| return result == UserConsentVerificationResult.Verified | |
| ? (true, null) | |
| : (false, $"Authentication failed: {result}"); | |
| } | |
| try | |
| { | |
| var result = await UserConsentVerifier.RequestVerificationAsync(message); | |
| return result == UserConsentVerificationResult.Verified | |
| ? (true, null) | |
| : (false, $"Authentication failed: {result}"); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsHelloCryptoHelpers.windows.cs`
around lines 41 - 49, The cancellation token is currently ignored because
token.Register(() => { }) is a no-op; before calling
UserConsentVerifier.RequestVerificationAsync you should honor pre-cancelled
tokens by checking token.IsCancellationRequested or calling
token.ThrowIfCancellationRequested and throw an
OperationCanceledException(token) (or return an appropriate cancelled result)
instead of registering an empty callback; remove the empty token.Register and
add the pre-call cancellation check near where RequestVerificationAsync is
invoked, keeping the existing return logic that compares to
UserConsentVerificationResult.Verified.
| var publicKey = credential.RetrievePublicKey(); | ||
| var algorithm = AsymmetricKeyAlgorithmProvider.OpenAlgorithm( | ||
| AsymmetricAlgorithmNames.RsaSignPkcs1Sha256); | ||
| var cryptoKey = algorithm.ImportPublicKey(publicKey); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if there's algorithm information stored/passed that could be used here
rg -n "AsymmetricAlgorithmNames" -g "*.cs" -C3Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1068
🏁 Script executed:
# First, let's see the file size and understand the method context
wc -l Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsHelloCryptoHelpers.windows.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 166
🏁 Script executed:
# Read the file to see the ProcessVerifyAsync method and surrounding context
cat -n Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsHelloCryptoHelpers.windows.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 10169
🏁 Script executed:
# Search for where credential keys are created and what algorithms might be available
rg -n "Windows\.Security\.Credentials" -g "*.cs" -A5 -B5Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 4361
🏁 Script executed:
# Check if there are other verification methods or algorithm handling
rg -n "ProcessVerify|Verify|Algorithm" Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsHelloCryptoHelpers.windows.cs -C2Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1260
🏁 Script executed:
# Search for key creation/generation methods
rg -n "CreateKeyAsync|GenerateKey|KeyCredentialRetrievalResult" -g "*.cs" -B3 -A10Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 23216
🏁 Script executed:
# Look for algorithm metadata or detection patterns
rg -n "KeyCredentialStatus|AsymmetricKeyAlgorithmProvider\." -g "*.cs" -B2 -A2Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 4696
🏁 Script executed:
# Check if there's any documentation or test code showing algorithm selection
rg -n "EcdsaP256|EcdsaP384|EcdsaP521|RsaSign" -g "*.cs" -B2 -A2Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 2462
🏁 Script executed:
# Find and read the WindowsKeyVaultHelpers file to see CreateAsymmetricKeyAsync
fd WindowsKeyVaultHelpers -type f -name "*.cs"Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 245
🏁 Script executed:
# Look for how the algorithm is determined/stored during key creation
rg -n "CreateAsymmetricKeyAsync|KeyAlgorithm" Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsKeyVaultHelpers.windows.cs -A10 -B3Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 796
🏁 Script executed:
# Check what happens when verification fails - is there error handling that might mask algorithm mismatch?
rg -n "ImportPublicKey|VerifySignature" -g "*.cs" -B2 -A2Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 3050
🏁 Script executed:
# Final confirmation: Check if there's any way algorithm info is embedded in the public key blob itself
rg -n "RetrievePublicKey" -g "*.cs" -B5 -A5Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1657
ProcessVerifyAsync hardcodes RsaSignPkcs1Sha256, breaking verification for non-RSA keys.
Windows Hello supports both RSA and ECDSA algorithms, and RequestSignAsync auto-selects the algorithm based on the key type. However, ProcessVerifyAsync hardcodes RsaSignPkcs1Sha256 during verification. If a key is created with ECDSA, the signature will verify successfully during signing but fail during verification with a generic "Verify failed" error, since an ECDSA signature cannot be verified using RSA parameters.
The RetrievePublicKey() method returns an opaque buffer with no algorithm metadata. Store or pass the algorithm used during key creation alongside the signature, or implement algorithm detection from the key parameters to support the full range of algorithms Windows Hello provides.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsHelloCryptoHelpers.windows.cs`
around lines 185 - 188, ProcessVerifyAsync currently hardcodes
AsymmetricAlgorithmNames.RsaSignPkcs1Sha256 (via
AsymmetricKeyAlgorithmProvider.OpenAlgorithm/ImportPublicKey) which breaks
verification for non‑RSA keys; change ProcessVerifyAsync to use the actual
algorithm that was used when signing instead of the hardcoded RsaSignPkcs1Sha256
— either persist the algorithm name alongside the signature in RequestSignAsync
and pass it into ProcessVerifyAsync, or implement reliable algorithm detection
from the public key buffer returned by RetrievePublicKey() and use that
algorithm string with AsymmetricKeyAlgorithmProvider.OpenAlgorithm before
calling ImportPublicKey; update the signature payload and call sites so
ProcessVerifyAsync receives/reads the algorithm and uses it for verification.
| private static readonly Lazy<ISecureBiometric> defaultImpl = | ||
| new(() => new SecureBiometricService(), LazyThreadSafetyMode.PublicationOnly); | ||
|
|
||
| public static ISecureBiometric Default | ||
| { | ||
| get => defaultImpl.Value; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What does LazyThreadSafetyMode.PublicationOnly guarantee in .NET Lazy<T>, and can it invoke the value factory more than once compared with ExecutionAndPublication?
💡 Result:
LazyThreadSafetyMode.PublicationOnly guarantees that when multiple threads try to initialize a Lazy<T> instance simultaneously, all threads are allowed to run the value factory (initialization method). The first thread to complete sets the value, which is then returned to other threads. Other threads' created instances are discarded. Publication of the value is thread-safe (only one value is used by all threads), but there is no synchronization to prevent concurrent factory execution. Yes, it can invoke the value factory more than once. Multiple threads execute the factory concurrently, unlike ExecutionAndPublication, which uses locks to ensure only one thread executes the factory (serializing execution and preventing multiple invocations during races).
Citations:
- 1: https://learn.microsoft.com/en-us/dotnet/api/system.threading.lazythreadsafetymode?view=net-10.0
- 2: http://msdn2.microsoft.com/system.threading.lazythreadsafetymode
- 3: https://learn.microsoft.com/en-us/dotnet/api/system.threading.lazythreadsafetymode?view=net-9.0
Use ExecutionAndPublication for the lazy initializer to guarantee a single shared instance.
LazyThreadSafetyMode.PublicationOnly allows the factory to execute concurrently on multiple threads during contention—only the first result is published, but other instances are created and discarded. For a library-wide default service, use ExecutionAndPublication to serialize factory execution and ensure a single initialization.
Suggested fix
private static readonly Lazy<ISecureBiometric> defaultImpl =
- new(() => new SecureBiometricService(), LazyThreadSafetyMode.PublicationOnly);
+ new(() => new SecureBiometricService(), LazyThreadSafetyMode.ExecutionAndPublication);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private static readonly Lazy<ISecureBiometric> defaultImpl = | |
| new(() => new SecureBiometricService(), LazyThreadSafetyMode.PublicationOnly); | |
| public static ISecureBiometric Default | |
| { | |
| get => defaultImpl.Value; | |
| private static readonly Lazy<ISecureBiometric> defaultImpl = | |
| new(() => new SecureBiometricService(), LazyThreadSafetyMode.ExecutionAndPublication); | |
| public static ISecureBiometric Default | |
| { | |
| get => defaultImpl.Value; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricAuthenticationService.cs`
around lines 5 - 10, The Lazy initializer for the library-wide default uses
PublicationOnly which can run the factory multiple times; change the Lazy
creation in the static field defaultImpl to use
LazyThreadSafetyMode.ExecutionAndPublication so the factory for new
SecureBiometricService is executed serially and only one instance is created and
published; update the expression that constructs defaultImpl (the
Lazy<ISecureBiometric> new(...) initializer referring to SecureBiometricService)
to use ExecutionAndPublication while leaving the Default getter returning
defaultImpl.Value unchanged.
| public partial Task<SecureAuthenticationResponse> SignAsync(string keyId, byte[] inputData, CancellationToken token) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(keyId)) | ||
| return Task.FromResult(SecureAuthenticationResponse.Failure("KeyId cannot be null or empty.")); | ||
|
|
||
| if (inputData is null || inputData.Length == 0) | ||
| return Task.FromResult(SecureAuthenticationResponse.Failure("Input data cannot be null or empty.")); | ||
|
|
||
| return Task.FromResult(SecureAuthenticationResponse.Failure("Key not found or operation canceled.")); | ||
| } | ||
|
|
||
| public partial Task<SecureAuthenticationResponse> VerifyAsync(string keyId, byte[] inputData, byte[] signature, CancellationToken token) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(keyId)) | ||
| return Task.FromResult(SecureAuthenticationResponse.Failure("KeyId cannot be null or empty.")); | ||
|
|
||
| if (inputData is null || inputData.Length == 0) | ||
| return Task.FromResult(SecureAuthenticationResponse.Failure("Input data cannot be null or empty.")); | ||
|
|
||
| if (signature is null || signature.Length == 0) | ||
| return Task.FromResult(SecureAuthenticationResponse.Failure("Signature cannot be null or empty.")); | ||
|
|
||
| return Task.FromResult(SecureAuthenticationResponse.Failure("Key not found or operation canceled.")); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if there's a TODO or implementation plan for Android signing
rg -n "Sign|Verify" -g "*android*.cs" -C3 | head -80Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 4602
🏁 Script executed:
cat -n Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs | sed -n '175,205p'Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1842
🏁 Script executed:
# Check for TODO, FIXME, or NotImplementedException in the file
rg -n "TODO|FIXME|NotImplemented" Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 57
🏁 Script executed:
# Check the interface/base class definitions
rg -n "class SecureBiometricService|interface" Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs | head -5Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 122
🏁 Script executed:
# Look for iOS or other platform implementations to compare
fd "SecureBiometricService" -e cs | grep -v androidRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 575
🏁 Script executed:
# Check the main SecureBiometricService.cs to understand the interface
cat Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs | head -100Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 2148
🏁 Script executed:
# Look for SignAsync and VerifyAsync in apple.cs to see how it's implemented there
rg -n "SignAsync|VerifyAsync" Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs -A5Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 1237
🏁 Script executed:
# Check for any XML comments or documentation on these methods
rg -B5 "public partial Task.*SignAsync|public partial Task.*VerifyAsync" Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.csRepository: FreakyAli/Plugin.Maui.Biometric
Length of output: 901
🏁 Script executed:
# Check if there's a TODO or NotImplementedException anywhere in the class or related helpers
rg -n "TODO|FIXME|NotImplemented" Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/ -g "*android*"Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 57
🏁 Script executed:
# Check net.cs and windows.cs for comparison
rg -n "SignAsync|VerifyAsync" Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs -A3Repository: FreakyAli/Plugin.Maui.Biometric
Length of output: 477
SignAsync and VerifyAsync are incomplete stubs that always fail on Android.
Both methods validate inputs but then unconditionally return failure with "Key not found or operation canceled." This is non-functional compared to the iOS implementation (which uses LAContextCryptoHelpers) and worse than the .NET implementation (which at least throws NotImplementedException). Android's approach silently fails after passing validation, suggesting the operation might succeed when it never will.
Either implement the Android signing/verification logic or throw NotImplementedException to be explicit about the missing feature.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs`
around lines 179 - 202, The Android implementations of SignAsync and VerifyAsync
in SecureBiometricService.android.cs are incomplete stubs that always return a
failure; replace the unconditional failure return with an explicit
NotImplemented signal: in the SignAsync and VerifyAsync methods
(SecureBiometricService.SignAsync and SecureBiometricService.VerifyAsync) remove
the Task.FromResult(... "Key not found or operation canceled.") and either
implement the platform-specific signing/verification logic or, if you cannot
implement it now, make the methods throw NotImplementedException (or return
Task.FromException<SecureAuthenticationResponse>(new NotImplementedException()))
so callers get a clear, explicit failure instead of a silent functional failure.
| public enum Digest | ||
| { | ||
| None, | ||
| Sha1, |
There was a problem hiding this comment.
Discourage Sha1 for new usage.
Line 6 exposes SHA-1 as a first-class option, which weakens crypto defaults/posture. Keep only for legacy interop and mark as obsolete/hidden.
🔧 Proposed change
public enum Digest
{
None,
- Sha1,
+ /// <summary>
+ /// Weak digest retained only for legacy compatibility. Do not use for new keys.
+ /// </summary>
+ [Obsolete("SHA-1 is deprecated and should not be used for new cryptographic operations.")]
+ [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
+ Sha1,
Sha224,
Sha256,
Sha384,
Sha512
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Sha1, | |
| public enum Digest | |
| { | |
| None, | |
| /// <summary> | |
| /// Weak digest retained only for legacy compatibility. Do not use for new keys. | |
| /// </summary> | |
| [Obsolete("SHA-1 is deprecated and should not be used for new cryptographic operations.")] | |
| [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] | |
| Sha1, | |
| Sha224, | |
| Sha256, | |
| Sha384, | |
| Sha512 | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Digest.cs` at line 6, Mark
the Digest enum member Sha1 as deprecated for new usage by annotating it with
[Obsolete("SHA-1 is insecure; use stronger digests for new code. Kept only for
legacy interop.")] and hide it from autocompletion by adding
[EditorBrowsable(EditorBrowsableState.Never)] (or equivalent attribute in the
project language) while leaving the member in place; update any XML docs on the
Digest enum to note it's retained for legacy interop only so callers see the
guidance when hovering over Digest.Sha1.
| public static SecureAuthenticationResponse Success(byte[] outputData, byte[]? iv = null) | ||
| => new() | ||
| { | ||
| WasSuccessful = true, | ||
| OutputData = (byte[])outputData.Clone(), | ||
| IV = iv is null ? null : (byte[])iv.Clone() | ||
| }; |
There was a problem hiding this comment.
Add null validation for outputData parameter.
The Success factory calls outputData.Clone() without null validation, which will throw a NullReferenceException if null is passed. Consider adding an explicit null check or using the null-forgiving pattern with a guard clause.
🛡️ Proposed fix
public static SecureAuthenticationResponse Success(byte[] outputData, byte[]? iv = null)
+ {
+ ArgumentNullException.ThrowIfNull(outputData);
+ return new()
+ {
+ WasSuccessful = true,
+ OutputData = (byte[])outputData.Clone(),
+ IV = iv is null ? null : (byte[])iv.Clone()
+ };
+ }
- => new()
- {
- WasSuccessful = true,
- OutputData = (byte[])outputData.Clone(),
- IV = iv is null ? null : (byte[])iv.Clone()
- };📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public static SecureAuthenticationResponse Success(byte[] outputData, byte[]? iv = null) | |
| => new() | |
| { | |
| WasSuccessful = true, | |
| OutputData = (byte[])outputData.Clone(), | |
| IV = iv is null ? null : (byte[])iv.Clone() | |
| }; | |
| public static SecureAuthenticationResponse Success(byte[] outputData, byte[]? iv = null) | |
| { | |
| ArgumentNullException.ThrowIfNull(outputData); | |
| return new() | |
| { | |
| WasSuccessful = true, | |
| OutputData = (byte[])outputData.Clone(), | |
| IV = iv is null ? null : (byte[])iv.Clone() | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs`
around lines 10 - 16, The Success factory method on SecureAuthenticationResponse
currently calls outputData.Clone() without validating outputData; add a null
check/guard at the start of SecureAuthenticationResponse.Success to throw
ArgumentNullException (or similar) when outputData is null, then continue to
clone and assign OutputData and IV as before so you avoid a
NullReferenceException from outputData.Clone().
Summary by CodeRabbit
New Features
Improvements
Deprecations
Bug Fixes