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