feat(android): encryption, verified by a third implementation

Section 4 step 2. EncryptContainer produces the exact container the desktop
writes: PBKDF2-SHA256, AES-256-GCM with the tag appended as WebCrypto does, a
32-byte salt and a 12-byte IV drawn per call from SecureRandom on Android and
BCryptGenRandom on Windows - never Random, since a predictable salt or IV ruins
the encryption that uses it and nothing in the output would show it.

The check matters as much as the code. A round trip with itself only proves
internal consistency, and it passed while the container carried two defects.
Node - the same primitives as WebCrypto - found both by reading a container the
phone had produced:

- Delphi's base64 wraps at 76 characters; WebCrypto's never does. Lenient
  decoders accept it, but depending on their leniency is not a guarantee.
- the accented literal in the check came out of the COMPILER already broken:
  this source has no byte-order mark, so Delphi reads it as ANSI. The defect
  was in the test rather than the encryption, and only the cross-check could
  tell those apart.

The check runs at launch in DEBUG and leaves selfcheck.json in the private
directory so it can be re-read from the PC.

Also fixed on the way: TPath resolves to FMX.Objects.TPath - a drawing shape -
because FMX.Objects comes later in the uses clause, and the error then talks
about an unknown Combine rather than a name collision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-17 17:08:34 +01:00
parent c73ac66313
commit ae48fadb6b
3 changed files with 288 additions and 6 deletions
+22 -4
View File
@@ -356,10 +356,28 @@ d'interface d'édition.
1. ~~**`updated_at` dans `TVaultEntry`**~~ — **fait**, et déjà affiché comme
*Modifié le* dans le détail. Rien n'était arbitrable sans lui.
2. **AES-GCM en chiffrement.** On n'a que le déchiffrement, des deux côtés
(JNI et CNG). Il faut aussi un sel de 32 octets et un IV de 12 octets tirés
d'un générateur cryptographique — `SecureRandom` côté Android,
`BCryptGenRandom` côté Windows. **Jamais** `Random`.
2. ~~**AES-GCM en chiffrement**~~ — **fait le 2026-08-17**, et vérifié par une
implémentation tierce. `EncryptContainer` produit le conteneur exact du
poste ; sel de 32 octets et IV de 12 tirés de `SecureRandom` (Android) ou
`BCryptGenRandom` (Windows), jamais `Random`.
**La vérification compte autant que le code.** Un aller-retour avec
soi-même ne prouve que la cohérence interne : il passait alors que le
conteneur portait deux défauts. C'est Node — mêmes primitives que WebCrypto
— qui les a montrés en relisant un conteneur produit par le téléphone :
- le base64 de Delphi est **coupé à 76 caractères**, celui de WebCrypto ne
l'est jamais. Les décodeurs tolérants l'acceptent, mais dépendre de leur
tolérance n'est pas une garantie ; `B64NoWrap` produit désormais la même
forme.
- le littéral accentué du contrôle sortait **déjà abîmé du compilateur** —
ce fichier n'a pas de marque d'ordre d'octets, et Delphi le lit en ANSI.
Le défaut était dans le test, pas dans le chiffrement, mais seul le
contrôle croisé pouvait le distinguer.
Le contrôle tourne au lancement en `DEBUG` et dépose `selfcheck.json` dans
le répertoire privé, pour être relu depuis le PC :
`adb shell run-as … cat …/files/selfcheck.json`.
3. **`PUT` dans [sync.php](../server-php/sync.php)**, qui répond aujourd'hui
405. Même authentification, `If-Match` obligatoire, **412** si l'ETag ne
correspond plus, écriture atomique (fichier temporaire puis renommage —