773e47330f
The re-seal never ran and left no trace, because a branch not taken logs nothing. It logs now, verdict and reason, whether or not anything happens - the absence of a line was itself the thing that cost time. And the verdict may have been wrong: asking canAuthenticate for BIOMETRIC_STRONG together with DEVICE_CREDENTIAL can fail outright on a device with a pattern and no enrolled fingerprint, instead of keeping what exists. It now falls back to the credential alone, which is all the key needs, and AuthenticateThen presents whichever set answered - offering something else would draw the same refusal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
223 lines
7.4 KiB
ObjectPascal
223 lines
7.4 KiB
ObjectPascal
unit PMA.Biometric;
|
|
{
|
|
Fine couche au-dessus de FMX.BiometricAuth.
|
|
|
|
On passe par le service FMX plutot que par BiometricPrompt en JNI direct :
|
|
le rappel de BiometricPrompt exige une classe Java implementant une
|
|
interface, donc un .jar a compiler et embarquer. FMX fournit deja ce pont.
|
|
|
|
MODELE RETENU : la cle du Keystore est creee avec une validite APRES
|
|
authentification (quelques dizaines de secondes). L'utilisateur s'authentifie,
|
|
puis la cle devient utilisable pendant ce delai. C'est plus simple qu'un
|
|
CryptoObject lie a chaque operation, et suffisant ici : la fenetre est courte
|
|
et l'application est au premier plan pendant ce temps.
|
|
|
|
L'APPEL DOIT SE FAIRE SUR LE THREAD PRINCIPAL - il affiche un dialogue
|
|
systeme. Le travail lourd (dechiffrement) part ensuite dans un thread.
|
|
}
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils;
|
|
|
|
type
|
|
/// boUnavailable : l'appareil ne PEUT PAS authentifier - aucune empreinte,
|
|
/// aucun code. La cle sous authentification est alors definitivement
|
|
/// inutilisable, et le cache qu'elle protege aussi.
|
|
/// boFailed : la tentative a echoue pour une autre raison, y compris une
|
|
/// exception de la couche Java. Ne dit RIEN sur l'appareil, donc ne
|
|
/// justifie jamais de detruire quoi que ce soit.
|
|
TBioOutcome = (boOK, boUnavailable, boFailed, boCancelled);
|
|
|
|
/// L'appareil peut-il authentifier (empreinte, visage, ou code de
|
|
/// verrouillage) ? False signifie qu'aucune protection n'est configuree.
|
|
function BiometricAvailable: Boolean;
|
|
|
|
/// Pourquoi la derniere tentative a echoue. Vide apres un succes.
|
|
///
|
|
/// Existe parce qu'un echec avale sinon sa propre cause : un TBioOutcome ne
|
|
/// distingue pas « l'appareil n'a aucune protection » de « la couche Java a
|
|
/// leve quelque chose », et l'appelant prend des decisions opposees dans les
|
|
/// deux cas - dont celle d'effacer le cache.
|
|
function LastBiometricError: string;
|
|
|
|
/// Demande l'authentification puis appelle AThen. Non bloquant : le resultat
|
|
/// arrive par le rappel, sur le thread principal.
|
|
/// Si l'appareil ne sait pas authentifier, AThen est appele avec boUnavailable
|
|
/// SANS refuser l'acces - a l'appelant de decider. Un telephone sans
|
|
/// verrouillage ne peut de toute facon pas proteger la cle.
|
|
procedure AuthenticateThen(const APrompt: string; const AThen: TProc<TBioOutcome>);
|
|
|
|
implementation
|
|
|
|
// La clause uses de l'implementation doit venir AVANT toute declaration :
|
|
// placee apres, Delphi ne s'en plaint qu'a la derniere ligne du fichier.
|
|
{$IF DEFINED(ANDROID)}
|
|
uses
|
|
FMX.BiometricAuth, FMX.Platform;
|
|
{$ENDIF}
|
|
|
|
var
|
|
FLastError: string = '';
|
|
|
|
function LastBiometricError: string;
|
|
begin
|
|
Result := FLastError;
|
|
end;
|
|
|
|
{$IF DEFINED(ANDROID)}
|
|
const
|
|
// DeviceCredential inclus : un telephone protege par un schema ou un code,
|
|
// sans empreinte enrolee, doit rester utilisable. C'est le cas courant.
|
|
WANTED_STRENGTHS = [TBiometricStrength.Strong,
|
|
TBiometricStrength.DeviceCredential];
|
|
// Repli quand la combinaison ci-dessus est refusee. Observe sur un appareil
|
|
// avec schema et AUCUNE empreinte : selon les versions, demander
|
|
// BIOMETRIC_STRONG en meme temps que DEVICE_CREDENTIAL fait echouer
|
|
// l'ensemble au lieu de retenir ce qui existe. On redemande alors juste ce
|
|
// dont on a besoin.
|
|
CREDENTIAL_ONLY = [TBiometricStrength.DeviceCredential];
|
|
|
|
var
|
|
// Le jeu retenu par la derniere interrogation reussie. AuthenticateThen doit
|
|
// presenter EXACTEMENT celui-la : demander autre chose que ce qui a repondu
|
|
// « disponible » ramenerait le meme refus.
|
|
FStrengths: TBiometricStrengths = WANTED_STRENGTHS;
|
|
|
|
/// Recupere le service ET pose les forces d'authentification.
|
|
///
|
|
/// L'ORDRE N'EST PAS COSMETIQUE. Le service Android construit son masque
|
|
/// d'authentificateurs a partir de ces forces, et son champ demarre a
|
|
/// l'ensemble VIDE - le constructeur ne l'initialise pas. Interroger
|
|
/// CanAuthenticate avant de les poser revient donc a demander
|
|
/// canAuthenticate(0), qui ne repond jamais « disponible », quel que soit ce
|
|
/// que l'appareil sait faire.
|
|
function AcquireService(out ASvc: IFMXBiometricAuthService): Boolean;
|
|
begin
|
|
Result := TPlatformServices.Current.SupportsPlatformService(
|
|
IFMXBiometricAuthService, ASvc);
|
|
if Result then
|
|
ASvc.SetBiometricStrengths(FStrengths);
|
|
end;
|
|
{$ENDIF}
|
|
|
|
function BiometricAvailable: Boolean;
|
|
{$IF DEFINED(ANDROID)}
|
|
var
|
|
LSvc: IFMXBiometricAuthService;
|
|
begin
|
|
// try/except et non un simple test : CanAuthenticate LEVE une
|
|
// java.lang.SecurityException si USE_BIOMETRIC manque au manifeste, au lieu
|
|
// de repondre False. Sans cette garde, un manifeste incomplet ne se traduit
|
|
// pas par « pas de biometrie » mais par une boite d'exception en pleine
|
|
// ouverture du coffre.
|
|
FLastError := '';
|
|
try
|
|
FStrengths := WANTED_STRENGTHS;
|
|
if not AcquireService(LSvc) then
|
|
begin
|
|
FLastError := 'biometric service missing from the platform';
|
|
Exit(False);
|
|
end;
|
|
Result := LSvc.CanAuthenticate;
|
|
if not Result then
|
|
begin
|
|
// Second essai sans la biometrie forte : le schema seul suffit a
|
|
// deverrouiller la cle, et c'est tout ce qu'on demande.
|
|
FStrengths := CREDENTIAL_ONLY;
|
|
LSvc.SetBiometricStrengths(FStrengths);
|
|
Result := LSvc.CanAuthenticate;
|
|
if Result then
|
|
FLastError := 'device credential only (no biometric enrolled)'
|
|
else
|
|
begin
|
|
FStrengths := WANTED_STRENGTHS;
|
|
FLastError := 'CanAuthenticate: no usable protection';
|
|
end;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
FLastError := E.ClassName + ': ' + E.Message;
|
|
Result := False;
|
|
end;
|
|
end;
|
|
end;
|
|
{$ELSE}
|
|
begin
|
|
Result := False;
|
|
end;
|
|
{$ENDIF}
|
|
|
|
procedure AuthenticateThen(const APrompt: string; const AThen: TProc<TBioOutcome>);
|
|
{$IF DEFINED(ANDROID)}
|
|
var
|
|
LSvc: IFMXBiometricAuthService;
|
|
LProps: TBiometricPromptProperties;
|
|
begin
|
|
if not Assigned(AThen) then Exit;
|
|
|
|
// AcquireService pose les forces ; CanAuthenticate n'a de sens qu'apres.
|
|
FLastError := '';
|
|
if not AcquireService(LSvc) then
|
|
begin
|
|
FLastError := 'biometric service missing from the platform';
|
|
AThen(boFailed); // pas boUnavailable : on ne sait rien de l'appareil
|
|
Exit;
|
|
end;
|
|
try
|
|
if not LSvc.CanAuthenticate then
|
|
begin
|
|
// Reponse NETTE du systeme : l'appareil n'a rien pour authentifier.
|
|
FLastError := 'CanAuthenticate: no usable protection';
|
|
AThen(boUnavailable);
|
|
Exit;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
FLastError := E.ClassName + ': ' + E.Message;
|
|
AThen(boFailed);
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
LProps := Default(TBiometricPromptProperties);
|
|
LProps.Title := 'Unlock the vault';
|
|
LProps.Description := APrompt;
|
|
LProps.CancelButtonText := 'Cancel';
|
|
LSvc.SetPromptProperties(LProps);
|
|
|
|
// Meme raison que plus haut : ce qui remonte de la couche Java doit
|
|
// ressortir en TBioOutcome, jamais en exception - l'appelant a un chemin
|
|
// pour chaque issue, il n'en a aucun pour une exception.
|
|
try
|
|
LSvc.Authenticate(
|
|
procedure
|
|
begin
|
|
AThen(boOK);
|
|
end,
|
|
procedure(const AFailReason: TBiometricFailReason; const AMessage: string)
|
|
begin
|
|
if AFailReason = TBiometricFailReason.Cancelled then
|
|
AThen(boCancelled)
|
|
else
|
|
AThen(boFailed);
|
|
end);
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
FLastError := E.ClassName + ': ' + E.Message;
|
|
AThen(boFailed);
|
|
end;
|
|
end;
|
|
end;
|
|
{$ELSE}
|
|
begin
|
|
if Assigned(AThen) then AThen(boUnavailable);
|
|
end;
|
|
{$ENDIF}
|
|
|
|
end.
|