chore(android): section 5 - ten permissions down to three, no secrets in release
The manifest asked for fine and coarse location, camera, phone, phone state and external storage - Delphi's defaults, none of them used. On a password vault those are exactly the wrong signals: whoever reads the app's listing saw a request for position and camera with no possible explanation. The list is written by hand now. A trap on the way: naming the substitution marker inside the comment that explains its removal is enough to bring it back. The substitution is textual and does not care that it lands in a comment. The bench credentials are no longer compiled into a release - the local include is debug-only. Verified in the binary in UTF-8 AND UTF-16: Delphi stores strings as UTF-16, so an ASCII grep returned zero on both sides and proved nothing. The log leaves the home screen in release without leaving the application: a 1.5 s press on the status line brings it back. Nothing secret is in it, but a permanent Log button is development noise, and diagnostics must stay reachable for the day something goes wrong remotely. And the Release configuration did not compile at all, which nobody knew: the two self-checks had their declarations under IFDEF DEBUG and their bodies outside. Invisible as long as one only ever builds Debug. Signing is what remains, and the keystore password is the user's to hold.
This commit is contained in:
@@ -7,7 +7,23 @@
|
|||||||
android:versionName="%versionName%"
|
android:versionName="%versionName%"
|
||||||
android:installLocation="%installLocation%">
|
android:installLocation="%installLocation%">
|
||||||
<uses-sdk android:minSdkVersion="%minSdkVersion%" android:targetSdkVersion="%targetSdkVersion%" />
|
<uses-sdk android:minSdkVersion="%minSdkVersion%" android:targetSdkVersion="%targetSdkVersion%" />
|
||||||
<%uses-permission%>
|
<!-- LA LISTE EST ECRITE ICI, PAS HERITEE. Le marqueur de substitution qui
|
||||||
|
se trouvait a cette place y deversait les defauts de Delphi :
|
||||||
|
localisation fine et grossiere,
|
||||||
|
camera, telephone, etat du telephone, stockage externe en lecture et
|
||||||
|
en ecriture. Aucune n'est utilisee, et sur un coffre a mots de passe
|
||||||
|
ce sont exactement les mauvais signaux - celui qui lit la fiche de
|
||||||
|
l'application y voit une demande de position et d'appareil photo sans
|
||||||
|
explication possible.
|
||||||
|
|
||||||
|
Il n'en reste qu'une : le reseau, pour parler au depot. Les deux de
|
||||||
|
biometrie ci-dessous sont deja explicites depuis longtemps. Ajouter
|
||||||
|
une permission demande donc de l'ecrire ici, ce qui est le but.
|
||||||
|
|
||||||
|
NE PAS ECRIRE LE NOM DU MARQUEUR DANS CE COMMENTAIRE : la substitution
|
||||||
|
est textuelle et ne regarde pas si elle tombe dans un commentaire -
|
||||||
|
le nommer pour l'expliquer suffit a le faire revenir. -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<!-- Absente de la liste par defaut de Delphi, et sans elle BiometricPrompt
|
<!-- Absente de la liste par defaut de Delphi, et sans elle BiometricPrompt
|
||||||
leve java.lang.SecurityException a la premiere invite - pas au
|
leve java.lang.SecurityException a la premiere invite - pas au
|
||||||
demarrage, donc le defaut ne se voit qu'en essayant d'ouvrir le coffre.
|
demarrage, donc le defaut ne se voit qu'en essayant d'ouvrir le coffre.
|
||||||
|
|||||||
+53
-6
@@ -460,12 +460,59 @@ tombstones ni génération d'uuid.
|
|||||||
|
|
||||||
## 5. Empaquetage
|
## 5. Empaquetage
|
||||||
|
|
||||||
- Icône, nom d'application, numéro de version.
|
### Fait — 2026-08-21
|
||||||
- Signature avec un magasin de clés dédié, pas la clé de débogage.
|
|
||||||
- Revue des permissions : `INTERNET` suffit.
|
- **Icône, nom, version** : « Coffre », `versionCode=1`, `versionName=1.0.0`.
|
||||||
- Retirer le journal de diagnostic de l'écran de déverrouillage, ou le mettre
|
- **Permissions : dix devenues trois.** Le manifeste demandait localisation
|
||||||
derrière un appui long.
|
fine et grossière, caméra, téléphone, état du téléphone et stockage externe —
|
||||||
- Le mot de passe préchargé depuis `PMA.LocalConfig.inc` doit disparaître.
|
les défauts de Delphi, aucun utilisé. Sur un coffre à mots de passe ce sont
|
||||||
|
exactement les mauvais signaux : celui qui lit la fiche y voyait une demande
|
||||||
|
de position et d'appareil photo sans explication possible. La liste est
|
||||||
|
désormais **écrite à la main** dans le modèle : `INTERNET`, `USE_BIOMETRIC`,
|
||||||
|
`USE_FINGERPRINT`.
|
||||||
|
*Piège rencontré* : nommer le marqueur de substitution dans le commentaire qui
|
||||||
|
explique son retrait suffit à le faire revenir — la substitution est
|
||||||
|
textuelle et ne regarde pas si elle tombe dans un commentaire.
|
||||||
|
- **Les secrets de banc d'essai ne sont plus compilés en release.**
|
||||||
|
`PMA.LocalConfig.inc` n'est inclus qu'en débogage ; en release les deux
|
||||||
|
constantes valent la chaîne vide. Vérifié dans le binaire, en UTF-8 **et en
|
||||||
|
UTF-16** — Delphi range ses chaînes en UTF-16, et un grep ASCII rendait zéro
|
||||||
|
des deux côtés, donc ne prouvait rien :
|
||||||
|
|
||||||
|
```
|
||||||
|
debug jeton=1 motdepasse=1
|
||||||
|
release jeton=0 motdepasse=0
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Le journal quitte l'écran d'accueil en release**, sans quitter
|
||||||
|
l'application : un appui long de 1,5 s sur la ligne d'état le ramène. Il ne
|
||||||
|
contient rien de secret — le jeton n'y figure que par empreinte — mais un
|
||||||
|
bouton « Log » permanent est du bruit de développement, et les diagnostics
|
||||||
|
doivent rester atteignables le jour où quelque chose cloche à distance.
|
||||||
|
- **La configuration Release compilait pas du tout**, et personne ne le savait :
|
||||||
|
les déclarations des deux auto-contrôles étaient sous `{$IFDEF DEBUG}`, leurs
|
||||||
|
**implémentations** non. La classe ne les déclarant plus, leurs corps
|
||||||
|
devenaient des membres inconnus. Invisible tant qu'on ne construit qu'en
|
||||||
|
débogage — ce qui a été le cas jusqu'ici.
|
||||||
|
|
||||||
|
### Reste : la signature
|
||||||
|
|
||||||
|
Un magasin de clés dédié, jamais la clé de débogage — celle-ci est partagée par
|
||||||
|
toutes les installations de Delphi, donc n'authentifie rien. Le mot de passe du
|
||||||
|
magasin appartient à l'utilisateur ; il n'a sa place ni ici ni dans le dépôt.
|
||||||
|
|
||||||
|
```
|
||||||
|
keytool -genkeypair -v -keystore pmserver-release.jks -alias pmserver -keyalg RSA -keysize 4096 -validity 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis, dans Delphi : *Project > Options > Provisioning*, configuration **Release**,
|
||||||
|
pointer le fichier `.jks` et l'alias. Le magasin se range **hors du dépôt** et
|
||||||
|
se sauvegarde ailleurs : le perdre interdit toute mise à jour de l'application
|
||||||
|
déjà installée, Android refusant une signature différente.
|
||||||
|
|
||||||
|
Une release se construit ensuite comme le débogage, en changeant la
|
||||||
|
configuration ; `android-client/build-apk.ps1` ne connaît que Debug pour
|
||||||
|
l'instant, et ne saura signer que le jour où le magasin existe.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -955,6 +955,28 @@ var PM = {
|
|||||||
if (show !== !document.getElementById('cfg').hidden) togglePanel('cfg');
|
if (show !== !document.getElementById('cfg').hidden) togglePanel('cfg');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// LE JOURNAL SORT DE LA VUE EN RELEASE, sans disparaitre.
|
||||||
|
///
|
||||||
|
/// Il ne contient rien de secret - le jeton n'y figure que par empreinte -
|
||||||
|
/// mais un bouton "Log" en permanence sur l'ecran d'accueil d'un coffre a
|
||||||
|
/// mots de passe est du bruit de developpement, et il invite a lire ce qu'on
|
||||||
|
/// n'a pas a lire. Un appui long sur la ligne d'etat le ramene : les
|
||||||
|
/// diagnostics restent atteignables le jour ou quelque chose cloche a
|
||||||
|
/// distance, ce qui arrivera.
|
||||||
|
hideLog: function () {
|
||||||
|
document.getElementById('btnLog').hidden = true;
|
||||||
|
var st = document.getElementById('status'), timer = null;
|
||||||
|
st.addEventListener('touchstart', function () {
|
||||||
|
timer = setTimeout(function () {
|
||||||
|
document.getElementById('btnLog').hidden = false;
|
||||||
|
toast('Log available');
|
||||||
|
}, 1500);
|
||||||
|
});
|
||||||
|
['touchend', 'touchmove', 'touchcancel'].forEach(function (e) {
|
||||||
|
st.addEventListener(e, function () { clearTimeout(timer); });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/// Le coffre local est scelle sous un code : il faut le retaper pour
|
/// Le coffre local est scelle sous un code : il faut le retaper pour
|
||||||
/// l'ouvrir. Delphi le sait en lisant le premier octet du cache.
|
/// l'ouvrir. Delphi le sait en lisant le premier octet du cache.
|
||||||
/// TOUJOURS VISIBLE, seule l'indication change. Le cacher quand aucun code
|
/// TOUJOURS VISIBLE, seule l'indication change. Le cacher quand aucun code
|
||||||
|
|||||||
+27
@@ -89,7 +89,20 @@ const
|
|||||||
// jeton authentifie l'appareil, le mot de passe ouvre le coffre entier. Ni
|
// jeton authentifie l'appareil, le mot de passe ouvre le coffre entier. Ni
|
||||||
// l'un ni l'autre n'a sa place dans un depot, meme sans remote. Absents apres
|
// l'un ni l'autre n'a sa place dans un depot, meme sans remote. Absents apres
|
||||||
// un clone -> recopier PMA.LocalConfig.template.inc.
|
// un clone -> recopier PMA.LocalConfig.template.inc.
|
||||||
|
//
|
||||||
|
// EN RELEASE ILS N'EXISTENT PAS. Ce sont des commodites de banc d'essai : elles
|
||||||
|
// evitent de retaper un jeton de quarante caracteres a chaque reinstallation.
|
||||||
|
// Compilees dans un APK distribue, elles y placeraient en clair de quoi lire le
|
||||||
|
// depot ET de quoi ouvrir le coffre - il suffirait de decompresser le fichier.
|
||||||
|
// Le fichier local n'est donc meme pas inclus : une release se compile sans
|
||||||
|
// lui, et un clone qui ne l'a pas construit quand meme.
|
||||||
|
{$IFDEF DEBUG}
|
||||||
{$I android-client\PMA.LocalConfig.inc}
|
{$I android-client\PMA.LocalConfig.inc}
|
||||||
|
{$ELSE}
|
||||||
|
const
|
||||||
|
FIRSTRUN_TOKEN = '';
|
||||||
|
TEST_PWD = '';
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
type
|
type
|
||||||
TForm8 = class(TForm)
|
TForm8 = class(TForm)
|
||||||
@@ -358,6 +371,11 @@ begin
|
|||||||
|
|
||||||
PushConfig;
|
PushConfig;
|
||||||
ShowUnlock;
|
ShowUnlock;
|
||||||
|
{$IFNDEF DEBUG}
|
||||||
|
// Le journal quitte l'ecran d'accueil, sans quitter l'application : un appui
|
||||||
|
// long sur la ligne d'etat le ramene. Voir PM.hideLog.
|
||||||
|
WebCall('PM.hideLog()');
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
{$IFDEF DEBUG}
|
{$IFDEF DEBUG}
|
||||||
// VERIFICATION DU CHIFFREMENT AVANT TOUTE ECRITURE REELLE (§4). Elle depose
|
// VERIFICATION DU CHIFFREMENT AVANT TOUTE ECRITURE REELLE (§4). Elle depose
|
||||||
@@ -530,6 +548,13 @@ begin
|
|||||||
if ASecret = '' then Exit('(vide)');
|
if ASecret = '' then Exit('(vide)');
|
||||||
Result := Copy(THashSHA2.GetHashString(ASecret), 1, 8);
|
Result := Copy(THashSHA2.GetHashString(ASecret), 1, 8);
|
||||||
end;
|
end;
|
||||||
|
{$IFDEF DEBUG}
|
||||||
|
/// LES DEUX AUTO-CONTROLES SONT DE DEBOGAGE, corps compris. Leurs declarations
|
||||||
|
/// l'etaient deja ; leurs implementations ne l'etaient pas, et une release ne
|
||||||
|
/// compilait donc pas du tout - la classe ne les declarant plus, le corps
|
||||||
|
/// devenait un membre inconnu. Defaut invisible tant qu'on ne construit qu'en
|
||||||
|
/// debogage, ce qui a ete le cas jusqu'a l'empaquetage.
|
||||||
|
///
|
||||||
/// Chiffre un petit conteneur, le relit, et le laisse sur le disque prive pour
|
/// Chiffre un petit conteneur, le relit, et le laisse sur le disque prive pour
|
||||||
/// qu'une autre implementation le verifie. Iterations volontairement basses :
|
/// qu'une autre implementation le verifie. Iterations volontairement basses :
|
||||||
/// ce qu'on eprouve ici est le CABLAGE - cle, IV, place du tag - pas le cout
|
/// ce qu'on eprouve ici est le CABLAGE - cle, IV, place du tag - pas le cout
|
||||||
@@ -629,6 +654,8 @@ end;
|
|||||||
/// PAR UUID ET NON PAR INDICE : le coffre vient d'etre remplace par celui du
|
/// PAR UUID ET NON PAR INDICE : le coffre vient d'etre remplace par celui du
|
||||||
/// serveur, et rien ne garantit que la meme entree occupe le meme rang - un
|
/// serveur, et rien ne garantit que la meme entree occupe le meme rang - un
|
||||||
/// tri se fait sur le nom affiche, qu'une autre machine a pu changer.
|
/// tri se fait sur le nom affiche, qu'une autre machine a pu changer.
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
procedure TForm8.ReopenEntryByUuid(const AUuid: string);
|
procedure TForm8.ReopenEntryByUuid(const AUuid: string);
|
||||||
var
|
var
|
||||||
I: Integer;
|
I: Integer;
|
||||||
|
|||||||
Reference in New Issue
Block a user