4258e9b559
Opening the vault threw java.lang.SecurityException: USE_BIOMETRIC is not in Delphi default permission list, and CanAuthenticate raises instead of answering false. So the fault showed up neither at compile time nor at startup, but on the first unlock - after the cache had already been written under an auth-protected key. The permission is the shallow half. The deep half is that SaveVault was called with a hard-coded True: we asked for a key guarded by a prompt that could not be shown, producing a file that was unreadable forever. It now receives BiometricAvailable, evaluated on the main thread before the worker starts. BiometricAvailable and AuthenticateThen also swallow what comes up from the Java layer and map it to boUnavailable. The caller has a path for every TBioOutcome and none for an exception. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
3.8 KiB
ObjectPascal
125 lines
3.8 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
|
|
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;
|
|
|
|
/// 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
|
|
|
|
{$IF DEFINED(ANDROID)}
|
|
uses
|
|
FMX.BiometricAuth, FMX.Platform;
|
|
{$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.
|
|
try
|
|
Result := TPlatformServices.Current.SupportsPlatformService(
|
|
IFMXBiometricAuthService, LSvc) and LSvc.CanAuthenticate;
|
|
except
|
|
Result := False;
|
|
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;
|
|
|
|
if not BiometricAvailable then
|
|
begin
|
|
AThen(boUnavailable);
|
|
Exit;
|
|
end;
|
|
if not TPlatformServices.Current.SupportsPlatformService(
|
|
IFMXBiometricAuthService, LSvc) then
|
|
begin
|
|
AThen(boUnavailable);
|
|
Exit;
|
|
end;
|
|
|
|
LProps := Default(TBiometricPromptProperties);
|
|
LProps.Title := 'Deverrouiller le coffre';
|
|
LProps.Description := APrompt;
|
|
LProps.CancelButtonText := 'Annuler';
|
|
LSvc.SetPromptProperties(LProps);
|
|
// DeviceCredential inclus : un telephone sans empreinte enrolee mais avec
|
|
// un code de verrouillage reste utilisable, au lieu d'etre bloque.
|
|
LSvc.SetBiometricStrengths([TBiometricStrength.Strong,
|
|
TBiometricStrength.DeviceCredential]);
|
|
|
|
// 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
|
|
AThen(boUnavailable);
|
|
end;
|
|
end;
|
|
{$ELSE}
|
|
begin
|
|
if Assigned(AThen) then AThen(boUnavailable);
|
|
end;
|
|
{$ENDIF}
|
|
|
|
end.
|