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
+61 -1
View File
@@ -32,7 +32,7 @@ interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes,
System.Diagnostics, System.Rtti, System.NetEncoding, System.DateUtils,
System.Generics.Collections,
System.Generics.Collections, System.IOUtils,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform,
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects,
@@ -185,6 +185,9 @@ type
procedure WebCall(const AJavaScript: string);
procedure PushVault;
procedure PushConfig;
{$IFDEF DEBUG}
procedure CryptoSelfCheck;
{$ENDIF}
procedure OpenEntry(AVaultIndex: Integer);
procedure OnClipTimer(Sender: TObject);
procedure OnDeadline(Sender: TObject);
@@ -286,6 +289,15 @@ begin
PushConfig;
ShowUnlock;
{$IFDEF DEBUG}
// VERIFICATION DU CHIFFREMENT AVANT TOUTE ECRITURE REELLE (§4). Elle depose
// aussi un conteneur sur le disque prive, pour qu'il soit relu par l'AUTRE
// implementation : un aller-retour avec soi-meme ne prouve que la coherence
// interne, et c'est la compatibilite avec le poste qui decide si une
// publication depuis le telephone est lisible ou perdue.
TThread.ForceQueue(nil, procedure begin CryptoSelfCheck; end);
{$ENDIF}
// Ouverture automatique du coffre local. Differee apres la construction :
// l'invite biometrique s'afficherait sinon avant la premiere peinture de la
// fenetre, sur un fond vide.
@@ -404,6 +416,54 @@ begin
SetLength(FWebPending, 0);
end;
/// Chiffre un petit conteneur, le relit, et le laisse sur le disque prive pour
/// 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
/// de la derivation, qui a son propre vecteur de test.
procedure TForm8.CryptoSelfCheck;
const
// Points de code : ce fichier n'a pas de BOM, un accent ecrit tel quel y
// devient deux glyphes et la cle derivee ne serait plus celle qu'on croit.
PWD = 'selfcheck-password-'#$00E9#$00E0#$00FC;
ITERS = 1000;
var
LPlain, LContainer, LBack, LPath: string;
LSw: TStopwatch;
begin
try
// Accents EN POINTS DE CODE, comme le mot de passe : le controle croise a
// montre que ce litteral-ci sortait deja abime du compilateur, et un
// aller-retour avec soi-meme ne pouvait pas le voir.
LPlain := '{"version":1,"entries":[{"uuid":"u1","title":"accent '#$00E9#$00E0'"}]}';
LSw := TStopwatch.StartNew;
LContainer := EncryptContainer(LPlain, PWD, ITERS);
LBack := DecryptContainer(LContainer, PWD);
LSw.Stop;
if LBack = LPlain then
Log(Format('selfcheck: encrypt/decrypt round-trip OK (%d ms)',
[LSw.ElapsedMilliseconds]))
else
Log('selfcheck: ROUND-TRIP MISMATCH - encryption is not usable');
// Depose pour relecture externe. Le mot de passe et les iterations sont
// dans le commentaire de tete du fichier de test cote PC, pas ici.
// GetHomePath, comme PMA.Store : sur Android c'est /data/data/<paquet>/
// files, prive a l'application. Documents n'existe pas ici.
//
// QUALIFIE : FMX.Objects declare aussi un TPath - une forme graphique - et
// il vient apres System.IOUtils dans la clause uses, donc c'est lui que le
// compilateur choisit. Le message parle alors d'un « Combine » inconnu, ce
// qui n'oriente pas vers un conflit de noms.
LPath := System.IOUtils.TPath.Combine(
System.IOUtils.TPath.GetHomePath, 'selfcheck.json');
TFile.WriteAllText(LPath, LContainer, TEncoding.UTF8);
Log('selfcheck: container written for cross-check');
except
on E: Exception do
Log('selfcheck FAILED: ' + E.ClassName + ' - ' + E.Message);
end;
end;
/// Descend les coordonnees du serveur dans les champs de la page.
procedure TForm8.PushConfig;
var