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:
+22
-4
@@ -356,10 +356,28 @@ d'interface d'édition.
|
|||||||
|
|
||||||
1. ~~**`updated_at` dans `TVaultEntry`**~~ — **fait**, et déjà affiché comme
|
1. ~~**`updated_at` dans `TVaultEntry`**~~ — **fait**, et déjà affiché comme
|
||||||
*Modifié le* dans le détail. Rien n'était arbitrable sans lui.
|
*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
|
2. ~~**AES-GCM en chiffrement**~~ — **fait le 2026-08-17**, et vérifié par une
|
||||||
(JNI et CNG). Il faut aussi un sel de 32 octets et un IV de 12 octets tirés
|
implémentation tierce. `EncryptContainer` produit le conteneur exact du
|
||||||
d'un générateur cryptographique — `SecureRandom` côté Android,
|
poste ; sel de 32 octets et IV de 12 tirés de `SecureRandom` (Android) ou
|
||||||
`BCryptGenRandom` côté Windows. **Jamais** `Random`.
|
`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
|
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
|
405. Même authentification, `If-Match` obligatoire, **412** si l'ETag ne
|
||||||
correspond plus, écriture atomique (fichier temporaire puis renommage —
|
correspond plus, écriture atomique (fichier temporaire puis renommage —
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ unit PMA.Crypto;
|
|||||||
interface
|
interface
|
||||||
|
|
||||||
uses
|
uses
|
||||||
System.SysUtils, System.Classes, System.JSON, System.NetEncoding, System.Hash;
|
System.SysUtils, System.Classes, System.JSON, System.NetEncoding, System.Hash,
|
||||||
|
// System.DateUtils : TTimeZone, pour horodater le conteneur en UTC.
|
||||||
|
System.DateUtils;
|
||||||
|
|
||||||
type
|
type
|
||||||
EPMACrypto = class(Exception);
|
EPMACrypto = class(Exception);
|
||||||
@@ -72,6 +74,23 @@ function AesGcmDecrypt(const AKey, AIv, ACipherText: TBytes): TBytes;
|
|||||||
/// than as "wrong password".
|
/// than as "wrong password".
|
||||||
function DecryptContainer(const AContainerJson, APassword: string): string;
|
function DecryptContainer(const AContainerJson, APassword: string): string;
|
||||||
|
|
||||||
|
/// N octets d'un generateur CRYPTOGRAPHIQUE - SecureRandom sur Android,
|
||||||
|
/// BCryptGenRandom sur Windows. JAMAIS Random : un sel ou un IV previsible
|
||||||
|
/// ruine le chiffrement qui les emploie, et rien dans le resultat ne le
|
||||||
|
/// montrerait. Leve plutot que de rendre des octets faibles.
|
||||||
|
function SecureRandomBytes(ACount: Integer): TBytes;
|
||||||
|
|
||||||
|
/// AES-256-GCM chiffrement. Le tag de 16 octets est colle A LA FIN du
|
||||||
|
/// resultat, comme le fait WebCrypto - c'est ce que DecryptContainer et le
|
||||||
|
/// poste Windows attendent tous les deux.
|
||||||
|
function AesGcmEncrypt(const AKey, AIv, APlainText: TBytes): TBytes;
|
||||||
|
|
||||||
|
/// Texte JSON -> conteneur complet, dans le format exact que le poste ecrit
|
||||||
|
/// (`pm-encrypted-export-v1`). Sel et IV tires a chaque appel : deux
|
||||||
|
/// chiffrements du meme coffre ne se ressemblent pas, et c'est voulu.
|
||||||
|
function EncryptContainer(const APlainJson, APassword: string;
|
||||||
|
AIterations: Integer = 600000): string;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
@@ -119,6 +138,12 @@ function BCryptGenerateSymmetricKey(hAlgorithm: Pointer; var phKey: Pointer;
|
|||||||
pbKeyObject: PByte; cbKeyObject: Cardinal; pbSecret: PByte;
|
pbKeyObject: PByte; cbKeyObject: Cardinal; pbSecret: PByte;
|
||||||
cbSecret, dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL;
|
cbSecret, dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL;
|
||||||
function BCryptDestroyKey(hKey: Pointer): Integer; stdcall; external BCRYPT_DLL;
|
function BCryptDestroyKey(hKey: Pointer): Integer; stdcall; external BCRYPT_DLL;
|
||||||
|
function BCryptEncrypt(hKey: Pointer; pbInput: PByte; cbInput: Cardinal;
|
||||||
|
pPaddingInfo: Pointer; pbIV: PByte; cbIV: Cardinal; pbOutput: PByte;
|
||||||
|
cbOutput: Cardinal; var pcbResult: Cardinal; dwFlags: Cardinal): Integer;
|
||||||
|
stdcall; external BCRYPT_DLL;
|
||||||
|
function BCryptGenRandom(hAlgorithm: Pointer; pbBuffer: PByte;
|
||||||
|
cbBuffer: Cardinal; dwFlags: Cardinal): Integer; stdcall; external BCRYPT_DLL;
|
||||||
function BCryptDecrypt(hKey: Pointer; pbInput: PByte; cbInput: Cardinal;
|
function BCryptDecrypt(hKey: Pointer; pbInput: PByte; cbInput: Cardinal;
|
||||||
pPaddingInfo: Pointer; pbIV: PByte; cbIV: Cardinal; pbOutput: PByte;
|
pPaddingInfo: Pointer; pbIV: PByte; cbIV: Cardinal; pbOutput: PByte;
|
||||||
cbOutput: Cardinal; var pcbResult: Cardinal; dwFlags: Cardinal): Integer;
|
cbOutput: Cardinal; var pcbResult: Cardinal; dwFlags: Cardinal): Integer;
|
||||||
@@ -295,8 +320,10 @@ type
|
|||||||
JCipherClass = interface(JObjectClass)
|
JCipherClass = interface(JObjectClass)
|
||||||
['{E081D312-94A5-B6C7-02FD-1E8F90011223}']
|
['{E081D312-94A5-B6C7-02FD-1E8F90011223}']
|
||||||
function _GetDECRYPT_MODE: Integer; cdecl;
|
function _GetDECRYPT_MODE: Integer; cdecl;
|
||||||
|
function _GetENCRYPT_MODE: Integer; cdecl;
|
||||||
function getInstance(transformation: JString): JCipher; cdecl;
|
function getInstance(transformation: JString): JCipher; cdecl;
|
||||||
property DECRYPT_MODE: Integer read _GetDECRYPT_MODE;
|
property DECRYPT_MODE: Integer read _GetDECRYPT_MODE;
|
||||||
|
property ENCRYPT_MODE: Integer read _GetENCRYPT_MODE;
|
||||||
end;
|
end;
|
||||||
[JavaSignature('javax/crypto/Cipher')]
|
[JavaSignature('javax/crypto/Cipher')]
|
||||||
JCipher = interface(JObject)
|
JCipher = interface(JObject)
|
||||||
@@ -306,6 +333,21 @@ type
|
|||||||
end;
|
end;
|
||||||
TJCipher = class(TJavaGenericImport<JCipherClass, JCipher>) end;
|
TJCipher = class(TJavaGenericImport<JCipherClass, JCipher>) end;
|
||||||
|
|
||||||
|
// Declare ICI plutot qu'en important Androidapi.JNI.Java.Security : cette
|
||||||
|
// unite redeclarerait JKey et consorts, et Delphi resout par le dernier uses
|
||||||
|
// - de quoi relier silencieusement le code existant a d'autres interfaces.
|
||||||
|
JSecureRandom = interface;
|
||||||
|
JSecureRandomClass = interface(JObjectClass)
|
||||||
|
['{9C1D2E3F-4A5B-6C7D-8E9F-0A1B2C3D4E5F}']
|
||||||
|
function init: JSecureRandom; cdecl;
|
||||||
|
end;
|
||||||
|
[JavaSignature('java/security/SecureRandom')]
|
||||||
|
JSecureRandom = interface(JObject)
|
||||||
|
['{AD2E3F40-5B6C-7D8E-9F0A-1B2C3D4E5F60}']
|
||||||
|
procedure nextBytes(bytes: TJavaArray<Byte>); cdecl;
|
||||||
|
end;
|
||||||
|
TJSecureRandom = class(TJavaGenericImport<JSecureRandomClass, JSecureRandom>) end;
|
||||||
|
|
||||||
function ToJavaBytes(const B: TBytes): TJavaArray<Byte>;
|
function ToJavaBytes(const B: TBytes): TJavaArray<Byte>;
|
||||||
var
|
var
|
||||||
I: Integer;
|
I: Integer;
|
||||||
@@ -484,10 +526,172 @@ begin
|
|||||||
end;
|
end;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Alea et chiffrement
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function SecureRandomBytes(ACount: Integer): TBytes;
|
||||||
|
{$IF DEFINED(ANDROID)}
|
||||||
|
var
|
||||||
|
LRnd: JSecureRandom;
|
||||||
|
LArr: TJavaArray<Byte>;
|
||||||
|
begin
|
||||||
|
if ACount <= 0 then Exit(nil);
|
||||||
|
// Le constructeur sans argument s'ensemence tout seul depuis le systeme ;
|
||||||
|
// fournir une graine soi-meme ne ferait que l'affaiblir.
|
||||||
|
LRnd := TJSecureRandom.JavaClass.init;
|
||||||
|
LArr := TJavaArray<Byte>.Create(ACount);
|
||||||
|
try
|
||||||
|
LRnd.nextBytes(LArr);
|
||||||
|
Result := FromJavaBytes(LArr);
|
||||||
|
finally
|
||||||
|
LArr.Free;
|
||||||
|
end;
|
||||||
|
if Length(Result) <> ACount then
|
||||||
|
raise EPMACrypto.Create('SecureRandom returned the wrong length');
|
||||||
|
end;
|
||||||
|
{$ELSEIF DEFINED(MSWINDOWS)}
|
||||||
|
const
|
||||||
|
BCRYPT_USE_SYSTEM_PREFERRED_RNG = $00000002;
|
||||||
|
begin
|
||||||
|
if ACount <= 0 then Exit(nil);
|
||||||
|
SetLength(Result, ACount);
|
||||||
|
// Handle nul + drapeau systeme : on prend le generateur par defaut de
|
||||||
|
// Windows sans avoir a ouvrir puis fermer un fournisseur.
|
||||||
|
if BCryptGenRandom(nil, PByte(Result), ACount,
|
||||||
|
BCRYPT_USE_SYSTEM_PREFERRED_RNG) <> 0 then
|
||||||
|
raise EPMACrypto.Create('BCryptGenRandom failed');
|
||||||
|
end;
|
||||||
|
{$ELSE}
|
||||||
|
begin
|
||||||
|
raise EPMACrypto.Create('No secure random source on this platform');
|
||||||
|
end;
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
|
function AesGcmEncrypt(const AKey, AIv, APlainText: TBytes): TBytes;
|
||||||
|
{$IF DEFINED(ANDROID)}
|
||||||
|
var
|
||||||
|
LCipher: JCipher;
|
||||||
|
LKeySpec: JSecretKeySpec;
|
||||||
|
LGcm: JGCMParameterSpec;
|
||||||
|
begin
|
||||||
|
LKeySpec := TJSecretKeySpec.JavaClass.init(ToJavaBytes(AKey),
|
||||||
|
StringToJString('AES'));
|
||||||
|
// 128 bits de tag, comme au dechiffrement et comme WebCrypto.
|
||||||
|
LGcm := TJGCMParameterSpec.JavaClass.init(128, ToJavaBytes(AIv));
|
||||||
|
LCipher := TJCipher.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
||||||
|
LCipher.init(TJCipher.JavaClass.ENCRYPT_MODE, LKeySpec, LGcm);
|
||||||
|
// Java rend deja le tag colle a la fin - meme disposition que WebCrypto.
|
||||||
|
Result := FromJavaBytes(LCipher.doFinal(ToJavaBytes(APlainText)));
|
||||||
|
end;
|
||||||
|
{$ELSEIF DEFINED(MSWINDOWS)}
|
||||||
|
var
|
||||||
|
LAlg, LKey: Pointer;
|
||||||
|
LInfo: TBCryptAuthCipherModeInfo;
|
||||||
|
LTag, LCt: TBytes;
|
||||||
|
LOutLen: Cardinal;
|
||||||
|
LMode: string;
|
||||||
|
begin
|
||||||
|
LAlg := nil;
|
||||||
|
LKey := nil;
|
||||||
|
if BCryptOpenAlgorithmProvider(LAlg, 'AES', nil, 0) <> 0 then
|
||||||
|
raise EPMACrypto.Create('BCryptOpenAlgorithmProvider(AES) failed');
|
||||||
|
try
|
||||||
|
LMode := 'ChainingModeGCM';
|
||||||
|
if BCryptSetProperty(LAlg, 'ChainingMode', PByte(PWideChar(LMode)),
|
||||||
|
(Length(LMode) + 1) * SizeOf(WideChar), 0) <> 0 then
|
||||||
|
raise EPMACrypto.Create('BCryptSetProperty(GCM) failed');
|
||||||
|
if BCryptGenerateSymmetricKey(LAlg, LKey, nil, 0,
|
||||||
|
PByte(AKey), Length(AKey), 0) <> 0 then
|
||||||
|
raise EPMACrypto.Create('BCryptGenerateSymmetricKey failed');
|
||||||
|
|
||||||
|
// CNG veut le tag DANS la structure, pas dans le flux : on l'y recueille,
|
||||||
|
// puis on le colle a la fin pour rendre la meme forme que WebCrypto.
|
||||||
|
SetLength(LTag, 16);
|
||||||
|
SetLength(LCt, Length(APlainText));
|
||||||
|
FillChar(LInfo, SizeOf(LInfo), 0);
|
||||||
|
LInfo.cbSize := SizeOf(LInfo);
|
||||||
|
LInfo.dwInfoVersion := AUTH_MODE_INFO_VERSION;
|
||||||
|
LInfo.pbNonce := PByte(AIv);
|
||||||
|
LInfo.cbNonce := Length(AIv);
|
||||||
|
LInfo.pbTag := PByte(LTag);
|
||||||
|
LInfo.cbTag := Length(LTag);
|
||||||
|
|
||||||
|
LOutLen := 0;
|
||||||
|
if BCryptEncrypt(LKey, PByte(APlainText), Length(APlainText), @LInfo,
|
||||||
|
nil, 0, PByte(LCt), Length(LCt), LOutLen, 0) <> 0 then
|
||||||
|
raise EPMACrypto.Create('BCryptEncrypt failed');
|
||||||
|
SetLength(LCt, LOutLen);
|
||||||
|
Result := Concat(LCt, LTag);
|
||||||
|
finally
|
||||||
|
if LKey <> nil then BCryptDestroyKey(LKey);
|
||||||
|
BCryptCloseAlgorithmProvider(LAlg, 0);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
{$ELSE}
|
||||||
|
begin
|
||||||
|
raise EPMACrypto.Create('AES-GCM is not wired for this platform');
|
||||||
|
end;
|
||||||
|
{$ENDIF}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Container
|
// Container
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Base64 SANS retour a la ligne. TNetEncoding.Base64 coupe a 76 caracteres,
|
||||||
|
/// comme le courrier electronique ; WebCrypto n'en met aucun. Les decodeurs
|
||||||
|
/// tolerants ignorent ces retours - `atob` retire les blancs, Node aussi - mais
|
||||||
|
/// produire exactement la meme forme que l'autre implementation retire une
|
||||||
|
/// dependance a leur tolerance.
|
||||||
|
function B64NoWrap(const ABytes: TBytes): string;
|
||||||
|
var
|
||||||
|
LEnc: TBase64Encoding;
|
||||||
|
begin
|
||||||
|
LEnc := TBase64Encoding.Create(0); // 0 = jamais de coupure
|
||||||
|
try
|
||||||
|
Result := LEnc.EncodeBytesToString(ABytes);
|
||||||
|
finally
|
||||||
|
LEnc.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function EncryptContainer(const APlainJson, APassword: string;
|
||||||
|
AIterations: Integer): string;
|
||||||
|
var
|
||||||
|
LObj: TJSONObject;
|
||||||
|
LSalt, LIv, LKey, LCt: TBytes;
|
||||||
|
begin
|
||||||
|
if APassword = '' then
|
||||||
|
raise EPMACrypto.Create('Refusing to encrypt with an empty password');
|
||||||
|
|
||||||
|
// TIRES A CHAQUE APPEL, tous les deux. Reutiliser un IV avec la meme cle
|
||||||
|
// casse GCM entierement - ce n'est pas une precaution de style.
|
||||||
|
LSalt := SecureRandomBytes(32);
|
||||||
|
LIv := SecureRandomBytes(12);
|
||||||
|
|
||||||
|
LKey := DeriveKeyPBKDF2Native(APassword, LSalt, AIterations, 32);
|
||||||
|
if LKey = nil then
|
||||||
|
LKey := DeriveKeyPBKDF2(APassword, LSalt, AIterations, 32);
|
||||||
|
LCt := AesGcmEncrypt(LKey, LIv, TEncoding.UTF8.GetBytes(APlainJson));
|
||||||
|
|
||||||
|
LObj := TJSONObject.Create;
|
||||||
|
try
|
||||||
|
// Memes cles, memes valeurs que encryptExportPayload cote poste : c'est ce
|
||||||
|
// conteneur-la que les deux clients savent relire.
|
||||||
|
LObj.AddPair('format', 'pm-encrypted-export-v1');
|
||||||
|
LObj.AddPair('kdf', 'pbkdf2-sha256');
|
||||||
|
LObj.AddPair('kdf_iterations', TJSONNumber.Create(AIterations));
|
||||||
|
LObj.AddPair('kdf_salt', B64NoWrap(LSalt));
|
||||||
|
LObj.AddPair('iv', B64NoWrap(LIv));
|
||||||
|
LObj.AddPair('ciphertext', B64NoWrap(LCt));
|
||||||
|
LObj.AddPair('created_at', FormatDateTime('yyyy-mm-dd"T"hh:nn:ss"Z"',
|
||||||
|
TTimeZone.Local.ToUniversalTime(Now)));
|
||||||
|
Result := LObj.ToJSON;
|
||||||
|
finally
|
||||||
|
LObj.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function DecryptContainer(const AContainerJson, APassword: string): string;
|
function DecryptContainer(const AContainerJson, APassword: string): string;
|
||||||
var
|
var
|
||||||
LObj: TJSONObject;
|
LObj: TJSONObject;
|
||||||
|
|||||||
+61
-1
@@ -32,7 +32,7 @@ interface
|
|||||||
uses
|
uses
|
||||||
System.SysUtils, System.Types, System.UITypes, System.Classes,
|
System.SysUtils, System.Types, System.UITypes, System.Classes,
|
||||||
System.Diagnostics, System.Rtti, System.NetEncoding, System.DateUtils,
|
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.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Platform,
|
||||||
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
|
FMX.Controls.Presentation, FMX.StdCtrls, FMX.Memo, FMX.Memo.Types,
|
||||||
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects,
|
FMX.ScrollBox, FMX.Layouts, FMX.Edit, FMX.ListBox, FMX.Objects,
|
||||||
@@ -185,6 +185,9 @@ type
|
|||||||
procedure WebCall(const AJavaScript: string);
|
procedure WebCall(const AJavaScript: string);
|
||||||
procedure PushVault;
|
procedure PushVault;
|
||||||
procedure PushConfig;
|
procedure PushConfig;
|
||||||
|
{$IFDEF DEBUG}
|
||||||
|
procedure CryptoSelfCheck;
|
||||||
|
{$ENDIF}
|
||||||
procedure OpenEntry(AVaultIndex: Integer);
|
procedure OpenEntry(AVaultIndex: Integer);
|
||||||
procedure OnClipTimer(Sender: TObject);
|
procedure OnClipTimer(Sender: TObject);
|
||||||
procedure OnDeadline(Sender: TObject);
|
procedure OnDeadline(Sender: TObject);
|
||||||
@@ -286,6 +289,15 @@ begin
|
|||||||
PushConfig;
|
PushConfig;
|
||||||
ShowUnlock;
|
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 :
|
// Ouverture automatique du coffre local. Differee apres la construction :
|
||||||
// l'invite biometrique s'afficherait sinon avant la premiere peinture de la
|
// l'invite biometrique s'afficherait sinon avant la premiere peinture de la
|
||||||
// fenetre, sur un fond vide.
|
// fenetre, sur un fond vide.
|
||||||
@@ -404,6 +416,54 @@ begin
|
|||||||
SetLength(FWebPending, 0);
|
SetLength(FWebPending, 0);
|
||||||
end;
|
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.
|
/// Descend les coordonnees du serveur dans les champs de la page.
|
||||||
procedure TForm8.PushConfig;
|
procedure TForm8.PushConfig;
|
||||||
var
|
var
|
||||||
|
|||||||
Reference in New Issue
Block a user