feat(android): container crypto unit plus a known-answer test vector

PBKDF2 is implemented in Pascal rather than through javax.crypto SecretKeyFactory: PBEKeySpec takes a char[] and the provider decides how those chars become bytes, so a non-ASCII sync password would derive a different key than the browser and fail as 'wrong password'. AES-GCM does go through JNI - one call, hardware-backed, and WebCrypto's tag-appended layout is exactly what Cipher expects.

The vector is generated with WebCrypto itself and uses a non-ASCII password on purpose; an ASCII-only vector would have hidden that bug. It records the derived key separately from the plaintext so a failure points at one stage instead of the whole chain.
This commit is contained in:
r-zakarya
2026-08-14 18:04:39 +01:00
parent 90ac75525a
commit 02d89c58d2
4 changed files with 419 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
# Vecteur de test — conteneur `pm-encrypted-export-v1`
`testvector.json` est produit par `make-testvector.js` avec **WebCrypto**, la
primitive exacte du navigateur. Sel et IV sont figés, donc le fichier est
reproductible : une différence signale un vrai changement de comportement, pas
du bruit.
> Le fichier de test réutilise un couple sel/IV fixe. C'est acceptable pour un
> vecteur, **jamais** dans l'application : réutiliser un IV avec la même clé
> détruit la sécurité de GCM.
## Paramètres
| | |
|---|---|
| Mot de passe | `sync-test-password-éàü` |
| KDF | PBKDF2-SHA256, 600 000 itérations, sel de 32 octets |
| Chiffrement | AES-256-GCM, IV de 12 octets, tag de 16 octets **en fin de `ciphertext`** |
Le mot de passe est **volontairement non-ASCII**. C'est le seul cas qui
distingue une implémentation correcte d'une implémentation approximative : Java
`PBEKeySpec` prend un `char[]` et laisse le fournisseur choisir l'encodage, ce
qui donne une clé différente pour `é`, `à`, `ü`. Un vecteur en pur ASCII aurait
laissé passer le bug.
## Valeurs attendues
Vérifier **dans cet ordre** — chaque étage isole une cause :
**1. Clé dérivée** (`DeriveKeyPBKDF2`, avant tout déchiffrement)
```
6bd7683ea78e163062f02681b03f2906fcf5a3a25025005af4e71982e6e2b286
```
Si elle diffère, le problème est dans PBKDF2 ou dans l'encodage du mot de passe
— surtout pas dans AES. Contrôler d'abord que le mot de passe est bien encodé
en UTF-8 (`sync-test-password-éàü` fait **25 octets**, pas 22 caractères).
**2. Texte clair** (`AesGcmDecrypt` puis `DecryptContainer`)
```
longueur UTF-8 : 572 octets
SHA-256 : ddaee44f8ac8374f1ece6d9ee2abd104d59cd283556f1c4c07afa95ae1dbd2a7
début : {"version":1,"exported_at":"2026-08-14T12:00:00.000Z","username":"test
```
Si la clé est bonne et que le déchiffrement échoue, chercher du côté de la
longueur du tag (128 **bits**) ou d'un tag qui aurait été séparé du corps —
WebCrypto le concatène, `javax.crypto` l'attend concaténé, il n'y a rien à
découper.
## Contenu
2 entrées, dont **une en corbeille** (`deleted: 1` avec son `deleted_at`), et
un dossier avec sa couleur. De quoi vérifier que le client mobile affiche la
corbeille séparément au lieu de la mélanger à la liste.
## Régénérer
```bash
node make-testvector.js > testvector.json
```
En PowerShell, la redirection ajoute un **BOM** qui rend le JSON illisible.
Utiliser `cmd`, ou écrire le fichier sans BOM.