Skip to content

feat: Crypto and Secure Enclave implementation for Android and iOS#43

Open
FreakyAli wants to merge 36 commits into
masterfrom
r-9/feat/cryto-secure-enclave
Open

feat: Crypto and Secure Enclave implementation for Android and iOS#43
FreakyAli wants to merge 36 commits into
masterfrom
r-9/feat/cryto-secure-enclave

Conversation

@FreakyAli

@FreakyAli FreakyAli commented Sep 10, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Public secure-biometric APIs for hardware-backed key management and crypto (create/delete/check keys, encrypt/decrypt, sign/verify), plus sample UI pages and navigation for biometric and secure-biometric flows.
  • Improvements

    • Safer Android keystore/executor handling, clearer security-level reporting, consolidated Apple biometric status checks, and enhanced diagnostic messages.
  • Deprecations

    • IsPlatformSupported property marked obsolete.
  • Bug Fixes

    • Null-safe checks to reduce crashes when platform services are unavailable.

@coderabbitai

coderabbitai Bot commented Sep 10, 2025

Copy link
Copy Markdown

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Android Auth & Crypto Callbacks
Plugin.Maui.Biometric/.../Authentication/AuthCallback.android.cs, Plugin.Maui.Biometric/.../Crypto/SecureAuthCallback.android.cs
AuthCallback made sealed and now overrides OnAuthenticationFailed() with debug logs; new SecureAuthCallback added to complete authentication TaskCompletionSource and extract cipher/IV on success. Review logging and TCS completion logic.
BiometricService API surface & platform wiring
Plugin.Maui.Biometric/.../Authentication/BiometricService.cs, .../Authentication/BiometricService.android.cs, .../Authentication/BiometricAuthenticationService.cs, .../Authentication/BiometricService.net.cs, Plugin.Maui.Biometric/Plugin.Maui.Biometric/BiometricService.cs (deleted), .../Authentication/IBiometric.cs, .../Authentication/BiometricService.apple.cs
Introduces a sealed partial BiometricService facade, marks IBiometric.IsPlatformSupported obsolete, removes/relocates old BiometricService file, changes Android executor resolution and null-safe feature checks, moves net-target gating for shared file. Check partial parity and platform inclusion.
Secure biometric API & implementations
Plugin.Maui.Biometric/.../Crypto/ISecureBiometric.cs, .../Crypto/SecureBiometricAuthenticationService.cs, .../Crypto/SecureBiometricService*.{cs,android.cs,apple.cs,net.cs,windows.cs}
Adds ISecureBiometric interface, SecureBiometricAuthenticationService.Default lazy provider, and SecureBiometricService partial signatures; platform-specific implementations for Android/Apple/Windows and net fallback (some throw NotImplemented). Verify API contract consistency across partials.
Android Keystore & BiometricPrompt helpers
Plugin.Maui.Biometric/.../Common/AndroidKeyStoreHelpers.android.cs, .../Common/BiometricPromptHelpers.android.cs
New helpers for Android KeyStore key generation, security-level detection, and BiometricPrompt-based crypto flow (ProcessCryptoAsync). Review algorithm-to-transformation mappings, StrongBox handling, executor/activity resolution, and exception/error messages.
Crypto enums & models
Plugin.Maui.Biometric/.../Enums/*, Plugin.Maui.Biometric/.../Models/*
Adds enums (BlockMode, CryptoOperation, Digest, KeyAlgorithm, Padding) and models (CryptoKeyOptions, KeyOperationResult, SecureAuthenticationRequest/Response, Crypto-related factories). Also converts AuthenticationRequest→BaseAuthenticationRequest and seals AuthenticationResponse; note nullability changes.
Validation & platform helpers
Plugin.Maui.Biometric/.../Common/KeyCreationHelpers.cs, .../Common/ExceptionExtensions.cs, .../Common/LAContextHelpers.apple.cs, .../Common/LAContextCryptoHelpers.apple.cs, .../Common/WindowsHelloCryptoHelpers.windows.cs, .../Common/WindowsKeyVaultHelpers.windows.cs, .../Common/AppleKeychainHelpers.apple.cs
Adds key-creation validation rules, exception message helpers, and extensive platform crypto/key helpers for Apple and Windows. High complexity — review validation rules, security assumptions and platform API usage.
Samples & UI
Plugin.Maui.Biometric/Samples/*
Adds HomePage, BiometricPage, SecureBiometricPage, updates AppShell/AppShell.xaml.cs, registers SecureBiometric service in MauiProgram, removes old MainPage. Verify navigation routes and DI registrations.
Tests & test helpers
Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/*, .../Tests/Fakes/FakeSecureBiometricService.cs
Adds broad unit test coverage for new enums/models/helpers/services and a fake secure-biometric test double. Review test expectations and platform-specific assumptions.
Project & build config
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Plugin.Maui.Biometric.csproj, Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/Plugin.Maui.Biometric.Tests.csproj
Enables nullable, updates warning suppressions and conditional .net.cs inclusion, adds InternalsVisibleTo for tests, and updates test project packages/output. Confirm build inclusions per target and test runner compatibility.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

"I nibble keys beneath the moonlit code,
I hum a prompt where ciphers grow,
StrongBox dreams and tiny hops of cheer,
I guard your bytes with floppy ear,
A rabbit's wink — secure and near. 🐇🔐"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: it adds cryptographic and Secure Enclave support across Android and iOS platforms, which represents the core objective of this substantial PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch r-9/feat/cryto-secure-enclave

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) cite12
  • Change Xamarin.AndroidX.Biometric to 1.1.0.30 (latest stable, supports MonoAndroid 9.0/API 28+) cite12
  • Change Xamarin.AndroidX.Collection.Ktx to 1.5.0.3 (latest stable compatible release) cite12
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.cs and .apple.cs collide with the un-guarded fallback in .net.cs (and GetIsPlatformSupported() 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;
 }
+#endif
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs (3)

107-113: Dispose BiometricPrompt to avoid leaks.

BiometricPrompt wraps 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.

SetNegativeButtonText throws 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.

GetEnrolledBiometricTypesAsync should still report available hardware when none is enrolled. Today it returns None in 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 Pkcs1 as obsolete for RSA encryption in new code; prefer Oaep.
 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

PublicationOnly can create multiple instances under race; typical singleton semantics use ExecutionAndPublication.

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

Helps 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 Response on OnAuthenticationFailed keeps 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 factories

Reduce 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 strings

Use 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 Digest is ignored or prohibited unless Operation includes Sign/Verify.

Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs (2)

71-79: Minor: the is not IExecutor type check is redundant.

ContextCompat.GetMainExecutor(Activity) already returns IExecutor. 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.Message
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs (2)

7-9: Default to AES-GCM for symmetric crypto to reduce surprises.

BlockMode = None maps 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 IV is actually used in the Android crypto path (cipher init for decrypt) and returned after encrypt. I can wire it through ProcessCryptoAsync if needed.

Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs (3)

23-25: Remove redundant null-conditional — cipher is already checked

Minor 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 executor

DoFinal 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 instantiation

In 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 consistency

Key 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 TODO

Commented 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 for InputData

While checking for null or empty InputData is 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 implementation

Since 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 SecureBiometricService
Plugin.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 behavior

The 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 TryCreateKeyWithSecurityLevel method 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 key

After 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 handling

The 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 values

The 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 components

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between fa5fb51 and 007d701.

📒 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 intent

This excludes all *.net.cs except for exact net9.0. Verify no shared stubs are needed by net9.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 = Gcm and Padding = None are 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 gating

Shape 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 patterns

The code uses using declarations for some resources (key, cipher, biometricPrompt, cryptoObject) but not consistently. Additionally, the key object 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 implementation

The partial class design pattern is appropriately used here to enable platform-specific implementations while maintaining a clean public API surface through the ISecureBiometric interface. 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 integration

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

Comment thread Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BlockMode.cs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 declares CancellationToken token without a default—add = default there 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

📥 Commits

Reviewing files that changed from the base of the PR and between 007d701 and 8eac3a9.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TCS

Matches 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.IOException

KeyStore.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 well

Consistent 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 ownerError message 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 mutation

Public 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 static

Prevents 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-check

Slight 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 DecryptAsync

Avoids 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 supported

Return 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8eac3a9 and 09c3256.

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

Comment thread Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs Outdated
Comment thread Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs Outdated
Comment thread Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 it

Process 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 OAEP

RSA 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 sizes

Current 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 static

It 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 0

Replace options.Operation == 0 with options.Operation == CryptoOperation.None for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 09c3256 and 20cea79.

📒 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 order

Null/empty keyId, null options, and empty operations are handled before accessing properties—prevents NREs and yields clear messages.

Comment thread Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 invariants

Public 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/whitespace

Prevents 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 name

If 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

📥 Commits

Reviewing files that changed from the base of the PR and between 20cea79 and 83c03b3.

📒 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 path

Pattern is clear and prevents “new” from outside.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 83c03b3 and b18afc1.

📒 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 declare internal; SecureBiometricService.net.cs uses partial (implicit internal).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
Plugin.Maui.Biometric/Samples/MauiProgram.cs (1)

18-19: Use the discard _ for the unused IServiceProvider lambda parameter.

Neither registration uses the e parameter; _ 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 a CancellationTokenSource tied 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

📥 Commits

Reviewing files that changed from the base of the PR and between b18afc1 and d445555.

📒 Files selected for processing (11)
  • Plugin.Maui.Biometric/Samples/AppShell.xaml
  • Plugin.Maui.Biometric/Samples/AppShell.xaml.cs
  • Plugin.Maui.Biometric/Samples/BiometricPage.xaml
  • Plugin.Maui.Biometric/Samples/BiometricPage.xaml.cs
  • Plugin.Maui.Biometric/Samples/HomePage.xaml
  • Plugin.Maui.Biometric/Samples/HomePage.xaml.cs
  • Plugin.Maui.Biometric/Samples/MainPage.xaml
  • Plugin.Maui.Biometric/Samples/MainPage.xaml.cs
  • Plugin.Maui.Biometric/Samples/MauiProgram.cs
  • Plugin.Maui.Biometric/Samples/SecureBiometricPage.xaml
  • Plugin.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

Comment on lines +20 to +39
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "SecureBiometricPage.xaml.cs" -type f

Repository: 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.

Comment on lines +59 to +64
if (response.WasSuccessful)
{
_encryptedData = response.OutputData;
_iv = response.IV;
ResultLabel.Text = $"Encrypted:\n{Convert.ToBase64String(_encryptedData!)}";
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -110

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 4770


🏁 Script executed:

find . -type f -name "*.cs" | xargs grep -l "class.*Response\|record.*Response" | head -20

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 321


🏁 Script executed:

cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs

Repository: 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 Sample

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 568


🏁 Script executed:

cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/ISecureBiometric.cs

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 2982


🏁 Script executed:

cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs | head -150

Repository: 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.cs

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 207


🏁 Script executed:

sed -n '173,250p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 1862


🏁 Script executed:

find . -type f -name "*.cs" | xargs grep -l "BiometricPromptHelpers" | grep -v Test

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 337


🏁 Script executed:

grep -n "ProcessCryptoAsync" ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 159


🏁 Script executed:

sed -n '100,200p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs

Repository: 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.cs

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 2530


🏁 Script executed:

cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.apple.cs | head -60

Repository: 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.

@FreakyAli

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

♻️ Duplicate comments (6)
Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BlockMode.cs (1)

9-9: ⚠️ Potential issue | 🟠 Major

Keep Ecb as 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 | 🟠 Major

The Transformation property generates invalid transformation strings for RSA encryption and decryption.

When Algorithm is KeyAlgorithm.Rsa, MapBlockMode(BlockMode.None) defaults to KeyProperties.BlockModeGcm, producing an invalid transformation string like "RSA/GCM/PKCS1Padding". RSA does not use block modes; it requires ECB semantically. This causes Cipher.GetInstance(transformation) to fail at runtime when RSA encryption/decryption is attempted with the default BlockMode.None.

The Transformation property must be algorithm-aware to map BlockMode.None to 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 | 🟠 Major

Reject non-byte-aligned key sizes here.

IsKeySizeInvalid lets values like 257 through. The Apple AES path later does options.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 | 🟡 Minor

Fix 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 | 🟠 Major

Complete TaskCompletionSource on cancellation to prevent hanging tasks.

When the CancellationToken fires, CancelAuthentication() is called, but if OnAuthenticationError isn't invoked (edge case), the TaskCompletionSource could 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 | 🔴 Critical

Missing IV parameter handling for decryption operations.

InitCipher initializes 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: IV has 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 Success factory.

♻️ 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 disable for required UI properties.

Disabling nullability for Title, Subtitle, and NegativeText suppresses compiler warnings but allows callers to leave these properties uninitialized. If these are required for biometric prompts (especially Title and NegativeText on Android), runtime failures could occur.

Consider using required modifier 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 passes CancellationToken.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 AuthenticationResponse overrides ToString(), this will output something like Plugin.Maui.Biometric.AuthenticationResponse rather 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 null internally (returning string.Empty), but the parameter is declared as non-nullable Exception ex. Consider using Exception? ex to 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: Make KeyOperationResult immutable after construction.

The private ctor + factories imply controlled creation, but the public setters still let callers flip WasSuccessful or combine success state with an ErrorMessage later. 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 null and returns early, so the cipher?.DoFinal at line 25 can be simplified to cipher.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.

CanEvaluatePolicy outputs an NSError that 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 token but it's not passed to ProcessVerifyAsync. While verification is likely fast, consider propagating the token for API consistency with SignAsync and 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.

SignAsync hardcodes KeyAlgorithm.Ec and Digest.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 between EncryptAsync and DecryptAsync.

EncryptAsync (line 173) uses expression-body returning Task directly, while DecryptAsync (line 176) uses async/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.Algorithm is KeyAlgorithm.Ec, the ternary routes to ProcessRsaCryptoAsync, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8dcd5d3 and 5531e25.

📒 Files selected for processing (65)
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/CryptoKeyOptionsTests.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/ExceptionExtensionsTests.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/Fakes/FakeSecureBiometricService.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/KeyCreationHelpersTests.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/KeyOperationResultTests.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/Plugin.Maui.Biometric.Tests.csproj
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/SecureAuthenticationRequestTests.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/SecureAuthenticationResponseTests.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/SecureBiometricAuthenticationServiceTests.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric.Tests/SecureBiometricServiceContractTests.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/AuthCallback.android.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricAuthenticationService.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.android.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.apple.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.net.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/BiometricService.windows.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Authentication/IBiometric.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/BiometricService.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AndroidKeyStoreHelpers.android.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/BiometricPromptHelpers.android.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/ExceptionExtensions.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/KeyCreationHelpers.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextCryptoHelpers.apple.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/LAContextHelpers.apple.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsHelloCryptoHelpers.windows.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/WindowsKeyVaultHelpers.windows.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/ISecureBiometric.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureAuthCallback.android.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricAuthenticationService.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.android.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.net.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.windows.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/AuthenticationType.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/AuthenticatorStrength.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BiometricHwStatus.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BiometricResponseStatus.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BiometricType.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/BlockMode.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/CryptoOperation.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Digest.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/KeyAlgorithm.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Enums/Padding.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationRequest.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/AuthenticationResponse.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/BaseAuthenticationRequest.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/CryptoKeyOptions.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/KeyOperationResult.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationRequest.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Models/SecureAuthenticationResponse.cs
  • Plugin.Maui.Biometric/Plugin.Maui.Biometric/Plugin.Maui.Biometric.csproj
  • Plugin.Maui.Biometric/Samples/AppShell.xaml
  • Plugin.Maui.Biometric/Samples/AppShell.xaml.cs
  • Plugin.Maui.Biometric/Samples/BiometricPage.xaml
  • Plugin.Maui.Biometric/Samples/BiometricPage.xaml.cs
  • Plugin.Maui.Biometric/Samples/HomePage.xaml
  • Plugin.Maui.Biometric/Samples/HomePage.xaml.cs
  • Plugin.Maui.Biometric/Samples/MainPage.xaml
  • Plugin.Maui.Biometric/Samples/MainPage.xaml.cs
  • Plugin.Maui.Biometric/Samples/MauiProgram.cs
  • Plugin.Maui.Biometric/Samples/SecureBiometricPage.xaml
  • Plugin.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

Comment on lines +17 to +21
using var keyGen = KeyGenerator.GetInstance(keyAlgorithm, KeyStoreName);
if (keyGen == null)
{
return KeyOperationResult.Failure($"Failed to create key generator for algorithm {keyAlgorithm}.");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +24 to +30
internal static KeyOperationResult CreateSymmetricKey(string keyId, CryptoKeyOptions options)
{
try
{
var keyBytes = RandomNumberGenerator.GetBytes(options.KeySize / 8);

var sac = BuildAccessControl(options.RequireUserAuthentication);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "AppleKeychainHelpers.apple.cs" -type f

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 157


🏁 Script executed:

cat -n ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Common/AppleKeychainHelpers.apple.cs

Repository: 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:


🏁 Script executed:

rg -B 3 -A 3 "CreateAsymmetricKey" ./Plugin.Maui.Biometric/ --type cs

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 3675


🏁 Script executed:

sed -n '30,60p' ./Plugin.Maui.Biometric/Plugin.Maui.Biometric/Crypto/SecureBiometricService.apple.cs

Repository: 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 -20

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 612


🏁 Script executed:

rg "RequireUserAuthentication" ./Plugin.Maui.Biometric/ --type cs -B 2 -A 2

Repository: 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.

Comment on lines +73 to +77
var privateKeyAttrs = new SecKeyParameters
{
IsPermanent = true,
ApplicationTag = NSData.FromString(keyId),
AccessControl = sac

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's see the file structure and locate the file
find . -name "AppleKeychainHelpers.apple.cs" -type f

Repository: 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.cs

Repository: 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.cs

Repository: 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.cs

Repository: 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.cs

Repository: 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.cs

Repository: 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.cs

Repository: 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 -n

Repository: 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:


🌐 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:


🌐 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:


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

Comment on lines +307 to +313
/// <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
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the Padding enum definition
find . -type f -name "*.cs" | head -20

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 1787


🏁 Script executed:

# Search for Padding enum definition
rg "enum Padding" --type cs

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 155


🏁 Script executed:

# Look at the full AppleKeychainHelpers file context
fd "AppleKeychainHelpers" --type f

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 155


🏁 Script executed:

# Search for MapEncryptAlgorithm usages
rg "MapEncryptAlgorithm" --type cs -A 3 -B 3

Repository: 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.cs

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 236


🏁 Script executed:

# Check ValidateRequest to see what validation happens
rg "ValidateRequest" --type cs -A 15 | head -60

Repository: 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.cs

Repository: 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.cs

Repository: 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 -80

Repository: 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 -40

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 1885


🏁 Script executed:

# Check SecureAuthenticationRequest to see if it validates Padding
rg "class SecureAuthenticationRequest" --type cs -A 30

Repository: 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 2

Repository: 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 3

Repository: 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.

Suggested change
/// <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.

Comment on lines +41 to +49
try
{
using (token.Register(() => { }))
{
var result = await UserConsentVerifier.RequestVerificationAsync(message);
return result == UserConsentVerificationResult.Verified
? (true, null)
: (false, $"Authentication failed: {result}");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +185 to +188
var publicKey = credential.RetrievePublicKey();
var algorithm = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(
AsymmetricAlgorithmNames.RsaSignPkcs1Sha256);
var cryptoKey = algorithm.ImportPublicKey(publicKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's algorithm information stored/passed that could be used here
rg -n "AsymmetricAlgorithmNames" -g "*.cs" -C3

Repository: 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.cs

Repository: 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.cs

Repository: 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 -B5

Repository: 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 -C2

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 1260


🏁 Script executed:

# Search for key creation/generation methods
rg -n "CreateKeyAsync|GenerateKey|KeyCredentialRetrievalResult" -g "*.cs" -B3 -A10

Repository: FreakyAli/Plugin.Maui.Biometric

Length of output: 23216


🏁 Script executed:

# Look for algorithm metadata or detection patterns
rg -n "KeyCredentialStatus|AsymmetricKeyAlgorithmProvider\." -g "*.cs" -B2 -A2

Repository: 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 -A2

Repository: 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 -B3

Repository: 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 -A2

Repository: 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 -A5

Repository: 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.

Comment on lines +5 to +10
private static readonly Lazy<ISecureBiometric> defaultImpl =
new(() => new SecureBiometricService(), LazyThreadSafetyMode.PublicationOnly);

public static ISecureBiometric Default
{
get => defaultImpl.Value;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


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.

Suggested change
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.

Comment on lines +179 to +202
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."));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -80

Repository: 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.cs

Repository: 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 -5

Repository: 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 android

Repository: 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 -100

Repository: 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 -A5

Repository: 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.cs

Repository: 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 -A3

Repository: 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +10 to +16
public static SecureAuthenticationResponse Success(byte[] outputData, byte[]? iv = null)
=> new()
{
WasSuccessful = true,
OutputData = (byte[])outputData.Clone(),
IV = iv is null ? null : (byte[])iv.Clone()
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants