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); 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); {$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.