diff --git a/AndroidManifest.template.xml b/AndroidManifest.template.xml
index c3a20c1..61006f1 100644
--- a/AndroidManifest.template.xml
+++ b/AndroidManifest.template.xml
@@ -8,6 +8,13 @@
android:installLocation="%installLocation%">
<%uses-permission%>
+
+
+
<%queries-child-elements%>
diff --git a/android-client/PMA.Biometric.pas b/android-client/PMA.Biometric.pas
index 9018096..6f413a6 100644
--- a/android-client/PMA.Biometric.pas
+++ b/android-client/PMA.Biometric.pas
@@ -47,8 +47,17 @@ function BiometricAvailable: Boolean;
var
LSvc: IFMXBiometricAuthService;
begin
- Result := TPlatformServices.Current.SupportsPlatformService(
- IFMXBiometricAuthService, LSvc) and LSvc.CanAuthenticate;
+ // 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
@@ -64,8 +73,13 @@ var
begin
if not Assigned(AThen) then Exit;
- if not (TPlatformServices.Current.SupportsPlatformService(
- IFMXBiometricAuthService, LSvc) and LSvc.CanAuthenticate) then
+ if not BiometricAvailable then
+ begin
+ AThen(boUnavailable);
+ Exit;
+ end;
+ if not TPlatformServices.Current.SupportsPlatformService(
+ IFMXBiometricAuthService, LSvc) then
begin
AThen(boUnavailable);
Exit;
@@ -81,18 +95,25 @@ begin
LSvc.SetBiometricStrengths([TBiometricStrength.Strong,
TBiometricStrength.DeviceCredential]);
- 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);
+ // 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
diff --git a/android-client/README.md b/android-client/README.md
index ceb886e..f214c10 100644
--- a/android-client/README.md
+++ b/android-client/README.md
@@ -213,6 +213,27 @@ Pour que le téléphone voie l'endpoint local :
adb reverse tcp:8080 tcp:80
```
+## Le piège de la porte sans clé
+
+`USE_BIOMETRIC` **ne fait pas partie de la liste de permissions par défaut de
+Delphi**. Sans elle, `CanAuthenticate` ne répond pas « pas de biométrie » : il
+lève une `java.lang.SecurityException`. Le défaut ne se voit donc ni à la
+compilation, ni au démarrage, mais à la première tentative d'ouverture — après
+que le cache a déjà été écrit sous une clé protégée par authentification.
+
+Le résultat est le pire cas possible : **un cache fermé derrière une porte que
+l'application ne sait pas ouvrir**. Trois corrections, dont une seule est la
+permission :
+
+1. Les deux `uses-permission` dans le modèle de manifeste.
+2. `BiometricAvailable` et `AuthenticateThen` attrapent ce qui remonte de la
+ couche Java. L'appelant a un chemin pour chaque `TBioOutcome`, il n'en a
+ aucun pour une exception.
+3. **`SaveVault` ne reçoit plus `True` en dur** mais `BiometricAvailable`. On
+ ne demande une clé sous authentification que si l'invite peut réellement
+ s'afficher — sinon le cache serait illisible pour toujours, et la seule
+ sortie serait de l'effacer.
+
## Piège de compilation rencontré
Cette installation de Delphi ne produisait **aucun** APK FireMonkey
diff --git a/mainApk.pas b/mainApk.pas
index 4828c65..d0274c2 100644
--- a/mainApk.pas
+++ b/mainApk.pas
@@ -782,9 +782,17 @@ end;
procedure TForm8.FetchAndDecrypt(const AUrl, ATok, APwd: string;
ASilent: Boolean);
+var
+ LCanAuth: Boolean;
begin
SetBusy(True);
if not ASilent then Status('Telechargement...');
+ // NE JAMAIS FERMER LE CACHE DERRIERE UNE PORTE QU'ON NE SAIT PAS OUVRIR.
+ // Demander une cle protegee par authentification alors que l'invite ne peut
+ // pas s'afficher (permission absente, aucune protection configuree) produit
+ // un fichier definitivement illisible. Evalue ici, sur le thread principal :
+ // l'interrogation du service passe par la couche Java.
+ LCanAuth := BiometricAvailable;
TThread.CreateAnonymousThread(
procedure
var
@@ -888,16 +896,15 @@ begin
// Ecrit APRES un dechiffrement reussi seulement, et avec son ETag :
// les deux doivent rester coherents.
try
- // True : on DEMANDE la protection par authentification. PMA.Store
- // retombe tout seul sur une cle simple si l'appareil n'a ni
- // empreinte ni code, plutot que de renoncer au cache.
// Le mot de passe part avec le coffre : c'est lui qui permettra au
// prochain lancement de rafraichir sans rien demander.
- SaveVault(LPlain, FETag, APwd, True);
+ SaveVault(LPlain, FETag, APwd, LCanAuth);
if VaultNeedsAuth then
Log(' cache ecrit - protege par biometrie/code')
+ else if LCanAuth then
+ Log(' cache ecrit - cle simple (Keystore a refuse la contrainte)')
else
- Log(' cache ecrit - AUCUNE protection sur cet appareil');
+ Log(' cache ecrit - AUCUNE protection : pas d''authentification ici');
except
// La classe compte autant que le message : "methode introuvable"
// designe une signature JNI fausse, pas un probleme de stockage.