142 lines
5.9 KiB
C#
142 lines
5.9 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="Beschikbaar"/> 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.
|
|
/// </summary>
|
|
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<bool> 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<bool>();
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>Wat het venster terugmeldt.</summary>
|
|
private sealed class Antwoord : BiometricPrompt.AuthenticationCallback
|
|
{
|
|
private readonly TaskCompletionSource<bool> _klaar;
|
|
|
|
public Antwoord(TaskCompletionSource<bool> klaar) => _klaar = klaar;
|
|
|
|
public override void OnAuthenticationSucceeded(BiometricPrompt.AuthenticationResult? uitkomst) =>
|
|
_klaar.TrySetResult(true);
|
|
|
|
/// <summary>Afgebroken, te vaak mis, of het toestel kan het niet. Hoe dan ook: niet open.</summary>
|
|
public override void OnAuthenticationError(BiometricErrorCode code, Java.Lang.ICharSequence? tekst) =>
|
|
_klaar.TrySetResult(false);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<bool> 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<bool> OntgrendelAsync(string titel, string uitleg) => Task.FromResult(false);
|
|
#endif
|
|
}
|