3bb6f6f903
The local vault now opens by itself at launch and the GET runs behind it, silently. A 304 shows nothing; a network failure shows nothing either - offline is a normal state, and an alert on every launch only teaches people to dismiss alerts. Only a new snapshot manifests, by refreshing the list. That silence is what puts the sync password inside the Keystore-encrypted file next to the vault: without it a new snapshot cannot be opened, so every edit made on the desktop would raise a password prompt at launch. The file already holds every password in plaintext; this one adds almost nothing to its exposure. A typed password still wins over the stored one, which is how you fix one that no longer works. Adopting a snapshot while the app is already open rebuilds the list in place instead of navigating - mandatory even from the detail screen, since list rows carry indices into the old vault and a tap would open the wrong entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
455 lines
17 KiB
ObjectPascal
455 lines
17 KiB
ObjectPascal
unit PMA.Store;
|
|
{
|
|
Coffre garde localement, chiffre sous une cle du Android Keystore.
|
|
|
|
POURQUOI : ouvrir un conteneur coute ~4 s (PBKDF2 600k, incompressible - le
|
|
nombre d'iterations appartient au format ecrit par le poste Windows). Payer
|
|
ce prix a chaque ouverture de l'application serait absurde alors que le
|
|
contenu n'a pas change. On ne le paie donc qu'a l'arrivee d'un instantane
|
|
NOUVEAU, et on relit ensuite un fichier local chiffre en AES - accelere
|
|
materiellement, donc instantane.
|
|
|
|
LA CLE NE QUITTE JAMAIS LE MATERIEL. Elle est generee dans le Keystore et
|
|
n'en sort pas : on envoie des donnees a chiffrer, on recoit le resultat. Un
|
|
vol du fichier seul est donc inexploitable, meme root - il faut la puce.
|
|
|
|
CE QUE CE FICHIER CONTIENT : tous les mots de passe en clair une fois
|
|
dechiffre, ET le mot de passe de sync (voir SaveVault pour l'arbitrage).
|
|
D'ou trois regles tenues ici :
|
|
- jamais hors du repertoire prive de l'application ;
|
|
- jamais de copie en clair sur le disque, meme temporaire ;
|
|
- la sauvegarde automatique Android doit etre desactivee pour l'app
|
|
(android:allowBackup="false"), sinon Google en recupere une copie.
|
|
|
|
La biometrie est demandee a la lecture quand la cle a pu etre creee avec
|
|
setUserAuthenticationRequired - deux alias existent pour cela, et l'en-tete
|
|
du fichier dit lequel a servi.
|
|
}
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils, System.Classes, System.JSON, System.IOUtils;
|
|
|
|
type
|
|
EPMAStore = class(Exception);
|
|
|
|
/// Le coffre local existe-t-il ?
|
|
function HasVault: Boolean;
|
|
|
|
/// Ecrit le coffre, l'ETag qui l'accompagne et le mot de passe de sync,
|
|
/// chiffres ensemble.
|
|
///
|
|
/// POURQUOI LE MOT DE PASSE EST LA : sans lui, un instantane nouveau ne peut
|
|
/// pas etre ouvert sans intervention, et chaque modification faite sur le
|
|
/// poste ferait apparaitre une demande de mot de passe au lancement - que
|
|
/// l'utilisateur finirait par expedier sans la lire. Le fichier contient deja
|
|
/// tous les mots de passe en clair ; celui-ci n'y ajoute presque rien en
|
|
/// exposition, et rend la synchronisation silencieuse.
|
|
///
|
|
/// ARequireAuth demande une cle deverrouillee par biometrie ou code. Si
|
|
/// l'appareil n'a aucune de ces protections, on retombe silencieusement sur
|
|
/// une cle sans contrainte : mieux vaut un cache lie au materiel que pas de
|
|
/// cache du tout. VaultNeedsAuth dira ensuite ce qui a reellement ete fait.
|
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
|
ARequireAuth: Boolean = False);
|
|
|
|
/// Le coffre local exige-t-il une authentification pour etre lu ? Lu dans
|
|
/// l'en-tete du fichier, sans dechiffrer quoi que ce soit.
|
|
function VaultNeedsAuth: Boolean;
|
|
|
|
/// Relit le coffre. False si absent ou illisible (cle perdue apres une
|
|
/// reinstallation, fichier altere) - dans ce cas il faut retelecharger.
|
|
/// ASyncPwd est vide pour un fichier ecrit avant que le mot de passe y soit
|
|
/// range : l'appelant redemande alors, et le prochain enregistrement le pose.
|
|
function LoadVault(out APlainJson, AETag, ASyncPwd: string): Boolean;
|
|
|
|
/// Efface le coffre local. A appeler au moindre doute : il se retelecharge.
|
|
procedure ClearVault;
|
|
|
|
/// Chemin du fichier, pour le journal et les tests.
|
|
function VaultPath: string;
|
|
|
|
implementation
|
|
|
|
{$IF DEFINED(ANDROID)}
|
|
uses
|
|
Androidapi.Jni, Androidapi.JNIBridge, Androidapi.JNI.JavaTypes, Androidapi.Helpers;
|
|
{$ENDIF}
|
|
|
|
const
|
|
VAULT_FILE = 'vault.bin';
|
|
// DEUX alias : la contrainte d'authentification est figee a la creation de
|
|
// la cle et ne peut pas etre ajoutee apres coup. Basculer d'un mode a
|
|
// l'autre change donc de cle, ce qui rend l'ancien fichier illisible - il
|
|
// sera efface et retelecharge, sans perte.
|
|
KEY_ALIAS = 'pm_vault_key'; // sans authentification
|
|
KEY_ALIAS_BIO = 'pm_vault_key_bio'; // deverrouillee par biometrie/code
|
|
// Duree pendant laquelle la cle reste utilisable apres une authentification
|
|
// reussie. Assez pour dechiffrer et afficher, trop court pour qu'un
|
|
// telephone laisse sans surveillance reste exploitable.
|
|
AUTH_VALIDITY_SECONDS = 30;
|
|
// Constantes d'android.security.keystore.KeyProperties, reprises en dur
|
|
// plutot qu'importees : elles sont figees par la plateforme, et cela evite
|
|
// une declaration JNI de plus.
|
|
PURPOSE_ENCRYPT = 1;
|
|
PURPOSE_DECRYPT = 2;
|
|
IV_LEN = 12; // GCM : 12 octets, la longueur recommandee
|
|
|
|
function VaultPath: string;
|
|
begin
|
|
// GetHomePath sur Android = /data/data/<paquet>/files, prive a l'app.
|
|
Result := TPath.Combine(TPath.GetHomePath, VAULT_FILE);
|
|
end;
|
|
|
|
function HasVault: Boolean;
|
|
begin
|
|
Result := TFile.Exists(VaultPath);
|
|
end;
|
|
|
|
procedure ClearVault;
|
|
begin
|
|
if TFile.Exists(VaultPath) then
|
|
TFile.Delete(VaultPath);
|
|
end;
|
|
|
|
{$IF DEFINED(ANDROID)}
|
|
type
|
|
JKeyStoreX = interface;
|
|
JSecretKeyX = interface;
|
|
JKeyGenerator = interface;
|
|
JKeyGenParameterSpec = interface;
|
|
JKeyGenParameterSpec_Builder = interface;
|
|
JCipherX = interface;
|
|
JGCMParameterSpecX = interface;
|
|
JAlgorithmParameterSpecX = interface;
|
|
JKeyX = interface;
|
|
|
|
JKeyXClass = interface(IJavaClass)
|
|
['{2C1A0B3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}']
|
|
end;
|
|
[JavaSignature('java/security/Key')]
|
|
JKeyX = interface(IJavaInstance)
|
|
['{3D2B1C4E-5F60-7B8C-9DAE-1F2A3B4C5D6E}']
|
|
end;
|
|
TJKeyX = class(TJavaGenericImport<JKeyXClass, JKeyX>) end;
|
|
|
|
JSecretKeyXClass = interface(JKeyXClass)
|
|
['{4E3C2D5F-6071-8C9D-AEBF-2A3B4C5D6E7F}']
|
|
end;
|
|
[JavaSignature('javax/crypto/SecretKey')]
|
|
JSecretKeyX = interface(JKeyX)
|
|
['{5F4D3E60-7182-9DAE-BFC0-3B4C5D6E7F80}']
|
|
end;
|
|
TJSecretKeyX = class(TJavaGenericImport<JSecretKeyXClass, JSecretKeyX>) end;
|
|
|
|
JAlgorithmParameterSpecXClass = interface(IJavaClass)
|
|
['{605E4F71-8293-AEBF-C0D1-4C5D6E7F8091}']
|
|
end;
|
|
[JavaSignature('java/security/spec/AlgorithmParameterSpec')]
|
|
JAlgorithmParameterSpecX = interface(IJavaInstance)
|
|
['{716F5082-93A4-BFC0-D1E2-5D6E7F8091A2}']
|
|
end;
|
|
TJAlgorithmParameterSpecX = class(TJavaGenericImport<JAlgorithmParameterSpecXClass, JAlgorithmParameterSpecX>) end;
|
|
|
|
JGCMParameterSpecXClass = interface(JAlgorithmParameterSpecXClass)
|
|
['{82706193-A4B5-C0D1-E2F3-6E7F8091A2B3}']
|
|
function init(tLen: Integer; src: TJavaArray<Byte>): JGCMParameterSpecX; cdecl;
|
|
end;
|
|
[JavaSignature('javax/crypto/spec/GCMParameterSpec')]
|
|
JGCMParameterSpecX = interface(JAlgorithmParameterSpecX)
|
|
['{938172A4-B5C6-D1E2-F304-7F8091A2B3C4}']
|
|
end;
|
|
TJGCMParameterSpecX = class(TJavaGenericImport<JGCMParameterSpecXClass, JGCMParameterSpecX>) end;
|
|
|
|
// KeyStore.load a deux surcharges, et celle a un argument prend un
|
|
// LoadStoreParameter - pas un Object. Declarer JObject faisait chercher au
|
|
// pont une methode inexistante : "methode introuvable" a l'execution, sans
|
|
// rien signaler a la compilation.
|
|
JLoadStoreParameterClass = interface(IJavaClass)
|
|
['{1C2D3E4F-5061-7283-94A5-B6C7D8E9F001}']
|
|
end;
|
|
[JavaSignature('java/security/KeyStore$LoadStoreParameter')]
|
|
JLoadStoreParameter = interface(IJavaInstance)
|
|
['{2D3E4F50-6172-8394-A5B6-C7D8E9F00112}']
|
|
end;
|
|
TJLoadStoreParameter = class(TJavaGenericImport<JLoadStoreParameterClass, JLoadStoreParameter>) end;
|
|
|
|
JKeyStoreXClass = interface(JObjectClass)
|
|
['{A49283B5-C6D7-E2F3-0415-8091A2B3C4D5}']
|
|
function getInstance(&type: JString): JKeyStoreX; cdecl;
|
|
end;
|
|
[JavaSignature('java/security/KeyStore')]
|
|
JKeyStoreX = interface(JObject)
|
|
['{B5A394C6-D7E8-F304-1526-91A2B3C4D5E6}']
|
|
procedure load(param: JLoadStoreParameter); cdecl;
|
|
function getKey(alias: JString; password: TJavaArray<Char>): JKeyX; cdecl;
|
|
function containsAlias(alias: JString): Boolean; cdecl;
|
|
end;
|
|
TJKeyStoreX = class(TJavaGenericImport<JKeyStoreXClass, JKeyStoreX>) end;
|
|
|
|
JKeyGenParameterSpec_BuilderClass = interface(JObjectClass)
|
|
['{C6B4A5D7-E8F9-0415-2637-A2B3C4D5E6F7}']
|
|
function init(keystoreAlias: JString; purposes: Integer): JKeyGenParameterSpec_Builder; cdecl;
|
|
end;
|
|
[JavaSignature('android/security/keystore/KeyGenParameterSpec$Builder')]
|
|
JKeyGenParameterSpec_Builder = interface(JObject)
|
|
['{D7C5B6E8-F90A-1526-3748-B3C4D5E6F708}']
|
|
function setBlockModes(blockModes: TJavaObjectArray<JString>): JKeyGenParameterSpec_Builder; cdecl;
|
|
function setEncryptionPaddings(paddings: TJavaObjectArray<JString>): JKeyGenParameterSpec_Builder; cdecl;
|
|
function setKeySize(keySize: Integer): JKeyGenParameterSpec_Builder; cdecl;
|
|
function setUserAuthenticationRequired(required: Boolean): JKeyGenParameterSpec_Builder; cdecl;
|
|
// Deprecie depuis l'API 30 au profit de setUserAuthenticationParameters,
|
|
// mais toujours fonctionnel et nettement plus simple a declarer.
|
|
function setUserAuthenticationValidityDurationSeconds(seconds: Integer): JKeyGenParameterSpec_Builder; cdecl;
|
|
function build: JKeyGenParameterSpec; cdecl;
|
|
end;
|
|
TJKeyGenParameterSpec_Builder = class(TJavaGenericImport<JKeyGenParameterSpec_BuilderClass, JKeyGenParameterSpec_Builder>) end;
|
|
|
|
JKeyGenParameterSpecClass = interface(JAlgorithmParameterSpecXClass)
|
|
['{E8D6C7F9-0A1B-2637-4859-C4D5E6F70819}']
|
|
end;
|
|
[JavaSignature('android/security/keystore/KeyGenParameterSpec')]
|
|
JKeyGenParameterSpec = interface(JAlgorithmParameterSpecX)
|
|
['{F9E7D80A-1B2C-3748-5960-D5E6F7081920}']
|
|
end;
|
|
TJKeyGenParameterSpec = class(TJavaGenericImport<JKeyGenParameterSpecClass, JKeyGenParameterSpec>) end;
|
|
|
|
JKeyGeneratorClass = interface(JObjectClass)
|
|
['{0AF8E91B-2C3D-4859-6A71-E6F708192031}']
|
|
function getInstance(algorithm: JString; provider: JString): JKeyGenerator; cdecl;
|
|
end;
|
|
[JavaSignature('javax/crypto/KeyGenerator')]
|
|
JKeyGenerator = interface(JObject)
|
|
['{1B09FA2C-3D4E-5960-7B82-F70819203142}']
|
|
procedure init(params: JAlgorithmParameterSpecX); cdecl;
|
|
function generateKey: JSecretKeyX; cdecl;
|
|
end;
|
|
TJKeyGenerator = class(TJavaGenericImport<JKeyGeneratorClass, JKeyGenerator>) end;
|
|
|
|
JCipherXClass = interface(JObjectClass)
|
|
['{2C1A0B3D-4E5F-6A71-8C93-081920314253}']
|
|
function _GetENCRYPT_MODE: Integer; cdecl;
|
|
function _GetDECRYPT_MODE: Integer; cdecl;
|
|
function getInstance(transformation: JString): JCipherX; cdecl;
|
|
property ENCRYPT_MODE: Integer read _GetENCRYPT_MODE;
|
|
property DECRYPT_MODE: Integer read _GetDECRYPT_MODE;
|
|
end;
|
|
[JavaSignature('javax/crypto/Cipher')]
|
|
JCipherX = interface(JObject)
|
|
['{3D2B1C4E-5F60-7B82-9DA4-192031425364}']
|
|
// Les DEUX surcharges doivent porter la directive : Delphi refuse une
|
|
// seconde declaration si la premiere ne l'a pas.
|
|
procedure init(opmode: Integer; key: JKeyX); cdecl; overload;
|
|
procedure init(opmode: Integer; key: JKeyX; params: JAlgorithmParameterSpecX); cdecl; overload;
|
|
function doFinal(input: TJavaArray<Byte>): TJavaArray<Byte>; cdecl;
|
|
function getIV: TJavaArray<Byte>; cdecl;
|
|
end;
|
|
TJCipherX = class(TJavaGenericImport<JCipherXClass, JCipherX>) end;
|
|
|
|
function ToJava(const B: TBytes): TJavaArray<Byte>;
|
|
var I: Integer;
|
|
begin
|
|
Result := TJavaArray<Byte>.Create(Length(B));
|
|
for I := 0 to High(B) do Result.Items[I] := B[I];
|
|
end;
|
|
|
|
function FromJava(const A: TJavaArray<Byte>): TBytes;
|
|
var I: Integer;
|
|
begin
|
|
SetLength(Result, A.Length);
|
|
for I := 0 to A.Length - 1 do Result[I] := A.Items[I];
|
|
end;
|
|
|
|
/// La cle du Keystore, creee a la premiere utilisation. Elle n'est jamais
|
|
/// lue : on obtient une poignee vers un objet que seul le materiel peut
|
|
/// utiliser.
|
|
function EnsureKey(ARequireAuth: Boolean): JKeyX;
|
|
var
|
|
LStore: JKeyStoreX;
|
|
LGen: JKeyGenerator;
|
|
LBuilder: JKeyGenParameterSpec_Builder;
|
|
LModes, LPaddings: TJavaObjectArray<JString>;
|
|
LAlias: string;
|
|
begin
|
|
if ARequireAuth then LAlias := KEY_ALIAS_BIO else LAlias := KEY_ALIAS;
|
|
|
|
LStore := TJKeyStoreX.JavaClass.getInstance(StringToJString('AndroidKeyStore'));
|
|
LStore.load(nil); // le magasin Android ne prend pas de flux
|
|
|
|
if LStore.containsAlias(StringToJString(LAlias)) then
|
|
Exit(LStore.getKey(StringToJString(LAlias), nil));
|
|
|
|
LModes := TJavaObjectArray<JString>.Create(1);
|
|
LModes.Items[0] := StringToJString('GCM');
|
|
LPaddings := TJavaObjectArray<JString>.Create(1);
|
|
LPaddings.Items[0] := StringToJString('NoPadding');
|
|
|
|
LBuilder := TJKeyGenParameterSpec_Builder.JavaClass.init(
|
|
StringToJString(LAlias), PURPOSE_ENCRYPT or PURPOSE_DECRYPT);
|
|
LBuilder := LBuilder.setBlockModes(LModes);
|
|
LBuilder := LBuilder.setEncryptionPaddings(LPaddings);
|
|
LBuilder := LBuilder.setKeySize(256);
|
|
if ARequireAuth then
|
|
begin
|
|
LBuilder := LBuilder.setUserAuthenticationRequired(True);
|
|
LBuilder := LBuilder.setUserAuthenticationValidityDurationSeconds(AUTH_VALIDITY_SECONDS);
|
|
end;
|
|
|
|
LGen := TJKeyGenerator.JavaClass.getInstance(
|
|
StringToJString('AES'), StringToJString('AndroidKeyStore'));
|
|
// Leve si l'appareil n'a NI empreinte enrolee NI code de verrouillage : une
|
|
// cle protegee par authentification n'aurait alors rien pour la deverrouiller.
|
|
// L'appelant retombe sur la cle sans contrainte.
|
|
LGen.init(LBuilder.build);
|
|
Result := LGen.generateKey; // stockee sous l'alias, pas retournee en clair
|
|
end;
|
|
{$ENDIF}
|
|
|
|
function VaultNeedsAuth: Boolean;
|
|
var
|
|
LFs: TFileStream;
|
|
LVer: Byte;
|
|
begin
|
|
Result := False;
|
|
if not TFile.Exists(VaultPath) then Exit;
|
|
LFs := TFileStream.Create(VaultPath, fmOpenRead or fmShareDenyWrite);
|
|
try
|
|
if LFs.Size < 1 then Exit;
|
|
LFs.ReadBuffer(LVer, 1);
|
|
Result := LVer = 2; // 1 = cle simple, 2 = cle sous authentification
|
|
finally
|
|
LFs.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
|
ARequireAuth: Boolean);
|
|
{$IF DEFINED(ANDROID)}
|
|
var
|
|
LWrap: TJSONObject;
|
|
LPlain, LIv, LCt, LOut: TBytes;
|
|
LCipher: JCipherX;
|
|
LKey: JKeyX;
|
|
LGotAuth: Boolean;
|
|
begin
|
|
// ETag et coffre chiffres ENSEMBLE : ils doivent rester coherents. Un ETag
|
|
// qui survivrait a un coffre illisible ferait repondre 304 au serveur alors
|
|
// que l'application n'a rien a afficher.
|
|
LWrap := TJSONObject.Create;
|
|
try
|
|
LWrap.AddPair('etag', AETag);
|
|
LWrap.AddPair('pwd', ASyncPwd);
|
|
LWrap.AddPair('vault', APlainJson);
|
|
LPlain := TEncoding.UTF8.GetBytes(LWrap.ToJSON);
|
|
finally
|
|
LWrap.Free;
|
|
end;
|
|
|
|
// Repli explicite : un appareil sans empreinte NI code ne peut pas porter
|
|
// une cle protegee par authentification, et sa creation echouerait.
|
|
LGotAuth := False;
|
|
LKey := nil;
|
|
if ARequireAuth then
|
|
try
|
|
LKey := EnsureKey(True);
|
|
LGotAuth := True;
|
|
except
|
|
LKey := nil;
|
|
end;
|
|
if LKey = nil then
|
|
LKey := EnsureKey(False);
|
|
|
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
|
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE, LKey);
|
|
// L'IV est GENERE par le Keystore, jamais choisi par nous : le reutiliser
|
|
// avec la meme cle romprait GCM.
|
|
LIv := FromJava(LCipher.getIV);
|
|
LCt := FromJava(LCipher.doFinal(ToJava(LPlain)));
|
|
|
|
SetLength(LOut, 1 + Length(LIv) + Length(LCt));
|
|
// La version dit quelle cle a servi : sans elle, la relecture ne saurait
|
|
// pas s'il faut authentifier d'abord.
|
|
if LGotAuth then LOut[0] := 2 else LOut[0] := 1;
|
|
Move(LIv[0], LOut[1], Length(LIv));
|
|
Move(LCt[0], LOut[1 + Length(LIv)], Length(LCt));
|
|
TFile.WriteAllBytes(VaultPath, LOut);
|
|
end;
|
|
{$ELSE}
|
|
begin
|
|
raise EPMAStore.Create('Le stockage Keystore est propre a Android');
|
|
end;
|
|
{$ENDIF}
|
|
|
|
function LoadVault(out APlainJson, AETag, ASyncPwd: string): Boolean;
|
|
{$IF DEFINED(ANDROID)}
|
|
var
|
|
LRaw, LIv, LCt, LPlain: TBytes;
|
|
LCipher: JCipherX;
|
|
LWrap: TJSONObject;
|
|
begin
|
|
Result := False;
|
|
APlainJson := '';
|
|
AETag := '';
|
|
ASyncPwd := '';
|
|
if not TFile.Exists(VaultPath) then Exit;
|
|
|
|
try
|
|
LRaw := TFile.ReadAllBytes(VaultPath);
|
|
if (Length(LRaw) < 1 + IV_LEN + 16) then Exit;
|
|
if not (LRaw[0] in [1, 2]) then Exit;
|
|
|
|
SetLength(LIv, IV_LEN);
|
|
Move(LRaw[1], LIv[0], IV_LEN);
|
|
SetLength(LCt, Length(LRaw) - 1 - IV_LEN);
|
|
Move(LRaw[1 + IV_LEN], LCt[0], Length(LCt));
|
|
|
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
|
// La cle est choisie d'apres l'en-tete. Si elle exige une
|
|
// authentification et qu'aucune n'a eu lieu recemment, l'init leve
|
|
// UserNotAuthenticatedException - attrapee plus bas, et le cache est
|
|
// efface. L'appelant doit donc authentifier AVANT (voir VaultNeedsAuth).
|
|
LCipher.init(TJCipherX.JavaClass.DECRYPT_MODE, EnsureKey(LRaw[0] = 2),
|
|
TJGCMParameterSpecX.JavaClass.init(128, ToJava(LIv)));
|
|
LPlain := FromJava(LCipher.doFinal(ToJava(LCt)));
|
|
|
|
LWrap := TJSONObject.ParseJSONValue(TEncoding.UTF8.GetString(LPlain)) as TJSONObject;
|
|
if LWrap = nil then Exit;
|
|
try
|
|
AETag := LWrap.GetValue<string>('etag', '');
|
|
ASyncPwd := LWrap.GetValue<string>('pwd', '');
|
|
APlainJson := LWrap.GetValue<string>('vault', '');
|
|
Result := APlainJson <> '';
|
|
finally
|
|
LWrap.Free;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Result := False;
|
|
// NE PAS effacer sur un simple defaut d'authentification : le coffre est
|
|
// intact, c'est l'utilisateur qui n'a pas presente son empreinte ou qui
|
|
// a laisse expirer le delai. L'effacer imposerait un retelechargement et
|
|
// 4 secondes de derivation pour une empreinte annulee.
|
|
//
|
|
// On efface en revanche quand la cle est REELLEMENT perdue
|
|
// (reinstallation, Keystore vide, nouvelle empreinte enrolee qui
|
|
// invalide la cle) ou quand le fichier est altere : dans ces cas il ne
|
|
// redeviendra jamais lisible.
|
|
if (Pos('UserNotAuthenticated', E.Message) = 0) and
|
|
(Pos('NotAuthenticated', E.ClassName) = 0) then
|
|
ClearVault;
|
|
end;
|
|
end;
|
|
end;
|
|
{$ELSE}
|
|
begin
|
|
Result := False;
|
|
APlainJson := '';
|
|
AETag := '';
|
|
ASyncPwd := '';
|
|
end;
|
|
{$ENDIF}
|
|
|
|
end.
|