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.