20ba2438df
The detail screen showed site, username and password and silently dropped everything else - custom fields, notes, 2FA. It is now built per entry rather than from a fixed template: every row is a caption, a value, and whether it is secret, so reveal and copy exist once instead of once per kind of field. Two rows stay special on purpose. A note's body is stored in the password column, so it gets its own caption and no mask. The 2FA code is the only live row - refreshed every second, placed first, since it is what you pull a phone out for. PMA.Totp takes bare base32 or an otpauth:// URI; both travel in snapshots depending on how the entry was created. Checked against all six RFC 6238 vectors, including T=20000000000, which overflows 32 bits. The rows hold plaintext, so clearing them is part of locking, not of navigating away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
151 lines
4.7 KiB
ObjectPascal
151 lines
4.7 KiB
ObjectPascal
program selftest;
|
|
{
|
|
Verification de la chaine crypto contre le vecteur connu, en console.
|
|
|
|
Remplace le bouton "Test crypto" qu'avait le banc d'essai : l'interface est
|
|
devenue celle d'un vrai client, et un aller-retour d'APK pour verifier une
|
|
constante est absurde. Ici la reponse arrive en quelques secondes, sur le
|
|
poste, avec un code de sortie exploitable.
|
|
|
|
Ce qu'il isole, dans cet ordre - une etape fausse rend inutile de lire les
|
|
suivantes :
|
|
1. PBKDF2 en Pascal -> la cle attendue ?
|
|
2. PBKDF2 natif (CNG) -> la MEME cle ? (l'encodage du mot de passe)
|
|
3. AES-GCM -> le clair attendu ?
|
|
|
|
Compilation :
|
|
dcc32 -B android-client\selftest.dpr
|
|
}
|
|
|
|
{$APPTYPE CONSOLE}
|
|
|
|
uses
|
|
System.SysUtils, System.Hash, System.NetEncoding, System.Diagnostics,
|
|
PMA.Crypto in 'PMA.Crypto.pas',
|
|
PMA.TestVector in 'PMA.TestVector.pas',
|
|
PMA.Totp in 'PMA.Totp.pas';
|
|
|
|
var
|
|
GFailures: Integer = 0;
|
|
|
|
procedure Check(const AName, AGot, AWant: string);
|
|
begin
|
|
if AGot = AWant then
|
|
Writeln(' OK ', AName)
|
|
else
|
|
begin
|
|
Writeln(' ECHEC ', AName);
|
|
Writeln(' obtenu : ', AGot);
|
|
Writeln(' attendu: ', AWant);
|
|
Inc(GFailures);
|
|
end;
|
|
end;
|
|
|
|
// URI plutot que base32 nu : elle porte les 8 chiffres de la RFC, et fait
|
|
// passer l'analyseur d'otpauth:// dans le meme mouvement.
|
|
const
|
|
RFC6238_URI = 'otpauth://totp/rfc?secret=' +
|
|
'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&digits=8';
|
|
|
|
procedure CheckTotp(AUnix: Int64; const AWant: string);
|
|
var
|
|
LLeft, LPeriod: Integer;
|
|
begin
|
|
Check('T=' + AUnix.ToString, TotpAt(RFC6238_URI, AUnix, LLeft, LPeriod), AWant);
|
|
end;
|
|
|
|
var
|
|
LSalt, LKey: TBytes;
|
|
LPlain: string;
|
|
LSw: TStopwatch;
|
|
begin
|
|
try
|
|
// Le sel vient du conteneur lui-meme : si le vecteur est regenere, le
|
|
// test suit sans qu'on ait a recopier quoi que ce soit.
|
|
LSalt := TNetEncoding.Base64.DecodeStringToBytes(
|
|
'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=');
|
|
|
|
Writeln('Mot de passe : ',
|
|
Length(TEncoding.UTF8.GetBytes(TESTVECTOR_PASSWORD)),
|
|
' octets UTF-8 (attendu 25)');
|
|
|
|
Writeln('PBKDF2 Pascal 600k...');
|
|
LSw := TStopwatch.StartNew;
|
|
LKey := DeriveKeyPBKDF2(TESTVECTOR_PASSWORD, LSalt, 600000, 32);
|
|
LSw.Stop;
|
|
Writeln(' ', LSw.ElapsedMilliseconds, ' ms');
|
|
Check('cle derivee', LowerCase(THash.DigestAsString(LKey)),
|
|
TESTVECTOR_KEY_HEX);
|
|
|
|
Writeln('PBKDF2 natif...');
|
|
LSw := TStopwatch.StartNew;
|
|
LKey := DeriveKeyPBKDF2Native(TESTVECTOR_PASSWORD, LSalt, 600000, 32);
|
|
LSw.Stop;
|
|
if LKey = nil then
|
|
begin
|
|
Writeln(' indisponible : ', LastNativeError);
|
|
Inc(GFailures);
|
|
end
|
|
else
|
|
begin
|
|
Writeln(' ', LSw.ElapsedMilliseconds, ' ms');
|
|
// Le point sensible : le mot de passe est non-ASCII, donc une cle
|
|
// identique prouve que le fournisseur encode bien en UTF-8.
|
|
Check('cle native identique', LowerCase(THash.DigestAsString(LKey)),
|
|
TESTVECTOR_KEY_HEX);
|
|
end;
|
|
|
|
Writeln('Conteneur AES-GCM...');
|
|
LPlain := DecryptContainer(TESTVECTOR_JSON, TESTVECTOR_PASSWORD);
|
|
Check('longueur du clair',
|
|
Length(TEncoding.UTF8.GetBytes(LPlain)).ToString,
|
|
TESTVECTOR_PLAIN_LEN.ToString);
|
|
Check('empreinte du clair',
|
|
LowerCase(THashSHA2.GetHashString(LPlain, THashSHA2.TSHA2Version.SHA256)),
|
|
TESTVECTOR_PLAIN_SHA);
|
|
|
|
// Un mot de passe faux DOIT echouer : sans cette verification, un
|
|
// dechiffrement qui rendrait n'importe quoi passerait pour un succes.
|
|
try
|
|
DecryptContainer(TESTVECTOR_JSON, 'mauvais mot de passe');
|
|
Writeln(' ECHEC un mot de passe faux a ete accepte');
|
|
Inc(GFailures);
|
|
except
|
|
on E: EPMACrypto do Writeln(' OK mot de passe faux rejete');
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Writeln('ERREUR ', E.ClassName, ' : ', E.Message);
|
|
Inc(GFailures);
|
|
end;
|
|
end;
|
|
|
|
// ---- TOTP, vecteurs de la RFC 6238 --------------------------------------
|
|
// Secret "12345678901234567890" en base32, 8 chiffres, aux instants publies
|
|
// par la RFC. C'est le seul moyen de verifier a la fois la base32, le HMAC
|
|
// et la troncature - un code plausible peut etre faux.
|
|
Writeln('TOTP (RFC 6238)...');
|
|
try
|
|
CheckTotp(59, '94287082');
|
|
CheckTotp(1111111109, '07081804');
|
|
CheckTotp(1111111111, '14050471');
|
|
CheckTotp(1234567890, '89005924');
|
|
CheckTotp(2000000000, '69279037');
|
|
CheckTotp(20000000000, '65353130');
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Writeln(' ERREUR ', E.ClassName, ' : ', E.Message);
|
|
Inc(GFailures);
|
|
end;
|
|
end;
|
|
|
|
Writeln;
|
|
if GFailures = 0 then
|
|
Writeln('Tout est bon.')
|
|
else
|
|
Writeln(GFailures, ' echec(s).');
|
|
ExitCode := GFailures;
|
|
end.
|