Files
Password-Manager/android-client/PMA.Biometric.pas
T
r-zakarya 57a295ad36 fix(android): set the biometric strengths before asking what the device can do
A pattern lock reported "no authentication here", so the cache was written
under the plain key.

FMX builds its authenticator mask from FBiometricStrengths, and that field
starts as the EMPTY SET - the constructor never initialises it. On API 30+
CanAuthenticate therefore calls canAuthenticate(0), which never answers
"available", whatever the device can actually do. Asking before setting the
strengths cannot succeed.

Both entry points now go through AcquireService, which sets Strong +
DeviceCredential and only then hands back the service. DeviceCredential is
the point: a phone with a pattern or a PIN and no enrolled fingerprint is
the common case, not an edge one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:48:43 +01:00

147 lines
4.5 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;
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];
/// 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(WANTED_STRENGTHS);
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.
try
Result := AcquireService(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;
// AcquireService pose les forces ; CanAuthenticate n'a de sens qu'apres.
if not AcquireService(LSvc) then
begin
AThen(boUnavailable);
Exit;
end;
try
if not LSvc.CanAuthenticate then
begin
AThen(boUnavailable);
Exit;
end;
except
AThen(boUnavailable);
Exit;
end;
LProps := Default(TBiometricPromptProperties);
LProps.Title := 'Deverrouiller le coffre';
LProps.Description := APrompt;
LProps.CancelButtonText := 'Annuler';
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
AThen(boUnavailable);
end;
end;
{$ELSE}
begin
if Assigned(AThen) then AThen(boUnavailable);
end;
{$ENDIF}
end.