feat(android): crypto self-test harness on the new FMX project
Two stages tested separately, because a global failure says nothing about where: PBKDF2 against the known derived key - which also runs on Win64, so that stage validates in seconds without deploying to a phone - then AES-GCM against the plaintext hash, Android only. The vector is embedded as a unit rather than read from a file: no network, no deployment, so a first-run failure can only come from the decryption itself. Work runs on a thread; 600k HMAC rounds on the UI thread would have Android kill the app for not responding.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
unit PMA.TestVector;
|
||||
{
|
||||
The testvector.json container, embedded verbatim.
|
||||
|
||||
Generated by a script - do not edit by hand. It exists for the very first
|
||||
run on a device, where the point is to isolate the crypto: no network, no
|
||||
file to deploy, so a failure can only come from the decryption itself.
|
||||
}
|
||||
|
||||
interface
|
||||
|
||||
const
|
||||
// Vector password. NON-ASCII on purpose: that is the only case telling a
|
||||
// correct PBKDF2 from an approximate one (see testvector.md). Written as
|
||||
// escapes so this file stays pure ASCII and no editor can mangle it.
|
||||
// e-acute = U+00E9, a-grave = U+00E0, u-diaeresis = U+00FC
|
||||
TESTVECTOR_PASSWORD = 'sync-test-password-'#$00E9#$00E0#$00FC;
|
||||
|
||||
// Expected values, to be checked IN THIS ORDER: the key first, the plaintext
|
||||
// second. A wrong key points at PBKDF2 or the password encoding; a correct
|
||||
// key with a failed decryption points at AES-GCM.
|
||||
TESTVECTOR_KEY_HEX = '6bd7683ea78e163062f02681b03f2906fcf5a3a25025005af4e71982e6e2b286';
|
||||
TESTVECTOR_PLAIN_SHA = 'ddaee44f8ac8374f1ece6d9ee2abd104d59cd283556f1c4c07afa95ae1dbd2a7';
|
||||
TESTVECTOR_PLAIN_LEN = 572; // UTF-8 bytes
|
||||
|
||||
TESTVECTOR_JSON =
|
||||
'{' +
|
||||
' "format": "pm-encrypted-export-v1",' +
|
||||
' "kdf": "pbkdf2-sha256",' +
|
||||
' "kdf_iterations": 600000,' +
|
||||
' "kdf_salt": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",' +
|
||||
' "iv": "yMnKy8zNzs/Q0dLT",' +
|
||||
' "ciphertext": "31AqOJ83L2TEFOcYaKEdpqKWGY42I4NUq+jdO7pA3j8+v023ECxjSaEnB0NvbufW6dMCgkK9oFwRvoQlLVSvK6bNBwBinJ4ltlbwKqATR2HGHh5eqiqGZayNNdRIFNJZw/jw9lYy28R6uK421uaMRx+mUBs4LGX9U+F2ayFfVF/VlsaHb5I3YMhLCN2zyMxs3j7Y1s69N09Uonm0LGKQ+SiteuXTHWJzAvHFIduLlxPJhCfEKUap4PolQ5PMohJeJYt+sutg7UY6i0QclxpmF2MY2qpbh3gA2mmSntoI2LN8Q41NMqyVn6JY6kXXpEfD1aNVw6dDCqAw2FQJ9HxE+/GBirt009rdS/l59KEnIYRvgBFnQqRTJZNNncHXXh3TsnQ3opKEyYe5qCxnX6E7wGzRawD/sQn2NNWjR8wwQQ68Cs74/RR8stAeuD/VJhJnkwxXPh8eBXHWFjLgFZuan+Ja2FwkjMhkHvIqbfJpbNwnu1mJ8ABtbGqmEHYSGUvLovIwCoQWjBqewlCFvRg0l4h63mUxuLaXx0SrIA3xhS7HqnYVTBp5/EVAu715jKmeDHviiZw6iws5ujd/u0IDUtJY1uANaF1xTBwcfkCnuonBlI8eMbIlcndrLSUN2slm6KxeUjZ3RSpiE9Izh5ix4kann8lygCAZVGkjswB83LmB1dFNH3oqG3+4qUgv7qR0TRZUSMoopCaSxhsVRd6/uBWu+82Nkbij1jMIzaB1+vTxBEHi5DGBNDfV/0evTgoIkC9wJtlEbz/OVCwC",' +
|
||||
' "created_at": "2026-08-14T12:00:00.000Z"' +
|
||||
'}';
|
||||
|
||||
implementation
|
||||
|
||||
end.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
object Form8: TForm8
|
||||
Left = 0
|
||||
Top = 0
|
||||
Caption = 'Form8'
|
||||
ClientHeight = 480
|
||||
ClientWidth = 640
|
||||
FormFactor.Width = 320
|
||||
FormFactor.Height = 480
|
||||
FormFactor.Devices = [Desktop, iPhone, iPad]
|
||||
DesignerMasterStyle = 0
|
||||
end
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
unit mainApk;
|
||||
{
|
||||
Banc d'essai de la crypto du conteneur, avant toute interface reelle.
|
||||
|
||||
Deux etages testes SEPAREMENT, parce qu'un echec global ne dit pas ou :
|
||||
|
||||
1. PBKDF2 -> compare la cle derivee a TESTVECTOR_KEY_HEX.
|
||||
Tourne AUSSI sur Windows : compile en Win64 pour valider
|
||||
cet etage en quelques secondes, sans deployer sur telephone.
|
||||
2. AES-GCM -> dechiffre et compare le SHA-256 du clair.
|
||||
Android uniquement (javax.crypto via JNI).
|
||||
|
||||
Le calcul tourne dans un thread : 600 000 iterations HMAC gelent l'interface
|
||||
sinon, et sur Android le systeme tuerait l'application pour non-reponse.
|
||||
}
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
|
||||
System.Hash, System.Diagnostics, System.NetEncoding,
|
||||
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
|
||||
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
|
||||
FMX.ScrollBox, FMX.Layouts;
|
||||
|
||||
type
|
||||
TForm8 = class(TForm)
|
||||
private
|
||||
FLog: TMemo;
|
||||
FBtn: TButton;
|
||||
procedure RunSelfTest(Sender: TObject);
|
||||
procedure Log(const AMsg: string);
|
||||
public
|
||||
constructor Create(AOwner: TComponent); override;
|
||||
end;
|
||||
|
||||
var
|
||||
Form8: TForm8;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
PMA.Crypto, PMA.TestVector;
|
||||
|
||||
{$R *.fmx}
|
||||
|
||||
constructor TForm8.Create(AOwner: TComponent);
|
||||
begin
|
||||
inherited;
|
||||
Caption := 'PM — crypto self-test';
|
||||
|
||||
FBtn := TButton.Create(Self);
|
||||
FBtn.Parent := Self;
|
||||
FBtn.Align := TAlignLayout.Top;
|
||||
FBtn.Margins.Rect := RectF(8, 8, 8, 8);
|
||||
FBtn.Height := 48;
|
||||
FBtn.Text := 'Lancer le test';
|
||||
FBtn.OnClick := RunSelfTest;
|
||||
|
||||
FLog := TMemo.Create(Self);
|
||||
FLog.Parent := Self;
|
||||
FLog.Align := TAlignLayout.Client;
|
||||
FLog.Margins.Rect := RectF(8, 0, 8, 8);
|
||||
FLog.ReadOnly := True;
|
||||
FLog.TextSettings.Font.Family := 'monospace';
|
||||
end;
|
||||
|
||||
procedure TForm8.Log(const AMsg: string);
|
||||
begin
|
||||
// Appele depuis le thread de calcul : la mise a jour d'un controle FMX doit
|
||||
// repasser par le thread principal.
|
||||
TThread.Queue(nil,
|
||||
procedure
|
||||
begin
|
||||
FLog.Lines.Add(AMsg);
|
||||
FLog.GoToTextEnd;
|
||||
end);
|
||||
end;
|
||||
|
||||
procedure TForm8.RunSelfTest(Sender: TObject);
|
||||
begin
|
||||
FLog.Lines.Clear;
|
||||
FBtn.Enabled := False;
|
||||
|
||||
TThread.CreateAnonymousThread(
|
||||
procedure
|
||||
var
|
||||
LSalt, LKey: TBytes;
|
||||
LKeyHex, LPlain, LSha: string;
|
||||
LSw: TStopwatch;
|
||||
begin
|
||||
try
|
||||
// ---- Etage 1 : PBKDF2 -------------------------------------------
|
||||
// Le sel vient du conteneur lui-meme, pas d'une copie : si le
|
||||
// conteneur change, le test suit.
|
||||
LSalt := TNetEncoding.Base64.DecodeStringToBytes(
|
||||
'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=');
|
||||
|
||||
Log('Mot de passe : ' + IntToStr(Length(TESTVECTOR_PASSWORD)) +
|
||||
' caracteres, ' +
|
||||
IntToStr(Length(TEncoding.UTF8.GetBytes(TESTVECTOR_PASSWORD))) +
|
||||
' octets UTF-8 (attendu 25)');
|
||||
|
||||
Log('PBKDF2 600k...');
|
||||
LSw := TStopwatch.StartNew;
|
||||
LKey := DeriveKeyPBKDF2(TESTVECTOR_PASSWORD, LSalt, 600000, 32);
|
||||
LSw.Stop;
|
||||
|
||||
LKeyHex := LowerCase(THash.DigestAsString(LKey));
|
||||
Log(' duree : ' + IntToStr(LSw.ElapsedMilliseconds) + ' ms');
|
||||
Log(' obtenu : ' + LKeyHex);
|
||||
if LKeyHex = TESTVECTOR_KEY_HEX then
|
||||
Log(' ==> PBKDF2 OK')
|
||||
else
|
||||
begin
|
||||
Log(' attendu: ' + TESTVECTOR_KEY_HEX);
|
||||
Log(' ==> PBKDF2 FAUX — verifier l''encodage UTF-8 du mot de passe');
|
||||
Exit; // inutile de tester AES avec une mauvaise cle
|
||||
end;
|
||||
|
||||
// ---- Etage 2 : AES-GCM + conteneur ------------------------------
|
||||
Log('');
|
||||
Log('Dechiffrement du conteneur...');
|
||||
LPlain := DecryptContainer(TESTVECTOR_JSON, TESTVECTOR_PASSWORD);
|
||||
LSha := LowerCase(THashSHA2.GetHashString(LPlain, THashSHA2.TSHA2Version.SHA256));
|
||||
|
||||
Log(' longueur : ' +
|
||||
IntToStr(Length(TEncoding.UTF8.GetBytes(LPlain))) +
|
||||
' octets (attendu ' + IntToStr(TESTVECTOR_PLAIN_LEN) + ')');
|
||||
Log(' sha256 : ' + LSha);
|
||||
if LSha = TESTVECTOR_PLAIN_SHA then
|
||||
Log(' ==> AES-GCM OK — la chaine complete est bonne')
|
||||
else
|
||||
Log(' ==> SHA DIFFERENT — comparer avec testvector.md');
|
||||
|
||||
Log('');
|
||||
Log('Debut du clair : ' + Copy(LPlain, 1, 60));
|
||||
except
|
||||
on E: Exception do
|
||||
Log('ERREUR ' + E.ClassName + ' : ' + E.Message);
|
||||
end;
|
||||
TThread.Queue(nil, procedure begin FBtn.Enabled := True; end);
|
||||
end).Start;
|
||||
end;
|
||||
|
||||
end.
|
||||
@@ -0,0 +1,16 @@
|
||||
program pmserverApk;
|
||||
|
||||
uses
|
||||
System.StartUpCopy,
|
||||
FMX.Forms,
|
||||
mainApk in 'mainApk.pas' {Form8},
|
||||
PMA.Crypto in 'android-client\PMA.Crypto.pas',
|
||||
PMA.TestVector in 'android-client\PMA.TestVector.pas';
|
||||
|
||||
{$R *.res}
|
||||
|
||||
begin
|
||||
Application.Initialize;
|
||||
Application.CreateForm(TForm8, Form8);
|
||||
Application.Run;
|
||||
end.
|
||||
+1312
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user