using MijnBesluit.AppUi; #if ANDROID using Android.App; using Android.Content; using Android.Hardware.Biometrics; using Android.OS; #elif IOS using LocalAuthentication; #endif namespace MijnBesluit.App; /// /// Vraagt het toestel om iemand te controleren: vingerafdruk, gezicht, of de pincode of het /// patroon van de telefoon zelf. /// /// Wat hier NIET gebeurt: de app vraagt geen eigen pincode en bewaart er ook geen. Er is dus /// niets bij te houden, niets te vergeten en niets te lekken. De telefoon doet de controle in /// een apart stuk hardware waar geen enkele app bij kan, en geeft alleen ja of nee terug. /// /// Vanaf Android 10. Daarvoor bestaat dit venster nog niet in de vorm waarin het ook de code /// van de telefoon accepteert, en dan zou een vingerafdrukloze telefoon er niet meer in komen. /// Op oudere toestellen is dus onwaar en logt de app uit zoals eerst. /// /// Op iOS doet LocalAuthentication hetzelfde, met dezelfde afspraak: Face ID of Touch ID, en /// de toestelcode als terugval. Dat laatste zit daar in de keuze van het beleid /// (DeviceOwnerAuthentication) in plaats van in een aparte schakelaar. /// public class Toestelslot : IToestelslot { #if ANDROID public bool Beschikbaar { get { // Zonder schermslot heeft vergrendelen geen zin: dan komt iedereen die de telefoon // in handen heeft er met één veeg weer in, en is uitloggen de enige echte grendel. if (!OperatingSystem.IsAndroidVersionAtLeast(29)) return false; var bewaking = (KeyguardManager?)Platform.AppContext.GetSystemService(Context.KeyguardService); return bewaking?.IsDeviceSecure == true; } } public Task OntgrendelAsync(string titel, string uitleg) { if (!Beschikbaar) return Task.FromResult(false); var activiteit = Platform.CurrentActivity; if (activiteit is null) return Task.FromResult(false); var klaar = new TaskCompletionSource(); try { // SetDeviceCredentialAllowed is vanaf Android 11 als verouderd gemarkeerd, maar het // is de enige manier die vanaf Android 10 werkt én de code van de telefoon als // terugval toelaat. De opvolger (SetAllowedAuthenticators) bestaat pas vanaf 11. // Zolang de app vanaf 10 werkt, hoort dit hier te staan. var venster = new BiometricPrompt.Builder(activiteit) .SetTitle(titel) .SetDescription(uitleg) .SetDeviceCredentialAllowed(true) .Build(); // Geen annuleersein dat we zelf afbreken: het venster heeft zijn eigen knop, en de // uitkomst daarvan komt via OnAuthenticationError binnen. venster.Authenticate(new CancellationSignal(), activiteit.MainExecutor!, new Antwoord(klaar)); } catch (Exception) { // Een toestel dat hier struikelt hoort de app niet te laten vallen. Het slot blijft // dan dicht en je kunt nog altijd helemaal uitloggen. klaar.TrySetResult(false); } return klaar.Task; } /// Wat het venster terugmeldt. private sealed class Antwoord : BiometricPrompt.AuthenticationCallback { private readonly TaskCompletionSource _klaar; public Antwoord(TaskCompletionSource klaar) => _klaar = klaar; public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult? uitkomst) => _klaar.TrySetResult(true); /// Afgebroken, te vaak mis, of het toestel kan het niet. Hoe dan ook: niet open. public override void OnAuthenticationError(BiometricErrorCode code, Java.Lang.ICharSequence? tekst) => _klaar.TrySetResult(false); /// /// Eén poging die niet klopte - een vinger die verkeerd lag. Het venster blijft staan en /// laat het opnieuw proberen, dus hier hoort niets te gebeuren. Pas als het toestel er /// mee ophoudt komt OnAuthenticationError. /// public override void OnAuthenticationFailed() { } } #elif IOS public bool Beschikbaar { get { // DeviceOwnerAuthentication en niet DeviceOwnerAuthenticationWithBiometrics: het // eerste is "Face ID of Touch ID, en anders de code van de telefoon", het tweede // eist een vingerafdruk of gezicht en niets anders. Dat laatste zou betekenen dat // een toestel met alleen een cijfercode er niet meer in komt - precies de gebruiker // die we op Android ook niet buitensluiten. using var vraag = new LAContext(); return vraag.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthentication, out _); } } public async Task OntgrendelAsync(string titel, string uitleg) { // iOS toont één regel tekst en geen kop, dus de titel valt hier weg. Wat er in beeld // komt is de uitleg; die hoort dus op zichzelf te kunnen staan. // // Let op: staat NSFaceIDUsageDescription niet in Info.plist, dan sluit iOS de app hier // ter plekke af - geen foutmelding, hij is weg. Zie Platforms/iOS/Info.plist; er is een // zelfcontrole die daarop let. using var vraag = new LAContext(); if (!vraag.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthentication, out _)) return false; // Afgebroken, te vaak mis, of het toestel kan het niet: dan komt hier onwaar uit en // blijft het slot dicht. De fout zelf laten we liggen - er valt voor de gebruiker niets // mee te doen, en op het slotscherm staat al wat hij nog wél kan. var (gelukt, _) = await vraag.EvaluatePolicyAsync(LAPolicy.DeviceOwnerAuthentication, uitleg); return gelukt; } #else public bool Beschikbaar => false; public Task OntgrendelAsync(string titel, string uitleg) => Task.FromResult(false); #endif }