feat(android): end-to-end read path validated on device, add the container diagnostic

GET in 80 ms, decryption in 3893 ms, entries and trash counted, second call answers 304. try-open.js opens a container with Node so a failure can be attributed: a GCM tag that does not verify means wrong password OR altered file, and the app cannot tell which.
This commit is contained in:
r-zakarya
2026-08-15 00:31:01 +01:00
parent a641669150
commit 3c1525e46f
3 changed files with 226 additions and 499 deletions
+29
View File
@@ -42,6 +42,35 @@ ce qui est acceptable et compréhensible pour l'utilisateur.
> jamais toucher le disque autrement que chiffré sous la clé du Keystore, et la
> sauvegarde automatique Android doit être désactivée pour l'application.
## Chaîne validée de bout en bout
Sur appareil : `GET` authentifié (80 ms) → déchiffrement (3893 ms) → lecture du
JSON, entrées vivantes et corbeille comptées séparément. Second appel : **304**,
rien de retéléchargé, rien de re-déchiffré.
**L'ETag n'est retenu que si le déchiffrement a réussi.** Le conserver dès la
réception paraît naturel et c'est un piège : après un échec, le prochain appel
répond 304 et l'application se retrouve sans données tout en se croyant à jour.
L'ETag appartient au **clair détenu**, pas au téléchargement.
## Diagnostiquer un échec de déchiffrement
`try-open.js` ouvre un conteneur avec Node, donc avec les primitives du
navigateur :
```bash
node try-open.js <fichier> <mot-de-passe>
```
Il sépare deux causes que l'application ne peut pas distinguer — un tag GCM qui
ne vérifie pas signifie *mot de passe faux OU fichier altéré*, sans plus. Si
Node ouvre le fichier et pas le client, le défaut est dans le client.
> Le mot de passe de sync n'est **pas** le mot de passe maître. Il n'est affiché
> nulle part : `prefs.bin` le garde chiffré par DPAPI, et l'application sait
> seulement le définir. En cas d'oubli, le redéfinir puis relancer un sync pour
> rechiffrer l'instantané distant.
## Fichiers
| | |
+51
View File
@@ -0,0 +1,51 @@
// Ouvre un conteneur pm-encrypted-export-v1 avec Node, pour distinguer un
// mauvais mot de passe d'un defaut du client Delphi.
//
// node try-open.js <fichier> <mot-de-passe>
//
// Utilise les memes primitives que le navigateur. Si Node y arrive et pas
// Delphi, le probleme est dans le client ; si les deux echouent, c'est le
// mot de passe.
const fs = require('node:fs');
const c = require('node:crypto');
const [, , file, pwd] = process.argv;
if (!file || pwd === undefined) {
console.error('usage: node try-open.js <fichier> <mot-de-passe>');
process.exit(2);
}
const raw = fs.readFileSync(file, 'utf8').replace(/^/, '');
let v;
try {
v = JSON.parse(raw);
} catch (e) {
console.error('Ce fichier n\'est pas du JSON : ' + e.message);
process.exit(1);
}
console.log('format : ' + v.format);
console.log('kdf : ' + v.kdf + ' / ' + v.kdf_iterations + ' iterations');
console.log('mot de passe : ' + pwd.length + ' caracteres, ' +
Buffer.byteLength(pwd, 'utf8') + ' octets UTF-8');
const salt = Buffer.from(v.kdf_salt, 'base64');
const iv = Buffer.from(v.iv, 'base64');
const ct = Buffer.from(v.ciphertext, 'base64');
const key = c.pbkdf2Sync(Buffer.from(pwd, 'utf8'), salt, v.kdf_iterations || 600000, 32, 'sha256');
console.log('cle derivee : ' + key.toString('hex'));
try {
const d = c.createDecipheriv('aes-256-gcm', key, iv);
d.setAuthTag(ct.subarray(ct.length - 16));
const pt = Buffer.concat([d.update(ct.subarray(0, ct.length - 16)), d.final()]).toString('utf8');
const obj = JSON.parse(pt);
const live = (obj.entries || []).filter(e => !e.deleted).length;
const trash = (obj.entries || []).filter(e => e.deleted).length;
console.log('==> OUVERT : ' + live + ' entrees, ' + trash + ' en corbeille');
console.log(' exporte le ' + (obj.exported_at || obj.snapshot_at || '?'));
} catch (e) {
console.log('==> ECHEC : ' + e.message);
console.log(' Le tag GCM ne verifie pas : mot de passe faux, ou fichier altere.');
}