d472dd4ef8
The retry I added yesterday never fired, and could not: the public key is rebuilt through KeyFactory, so it is an ordinary RSA key with no link to the Keystore. It encrypts perfectly well while its private half is permanently invalidated. The re-seal therefore sealed the cache with a key nothing would ever open again - the app asked for the sync password instead of the pattern. Invalidation is observable at exactly one moment: reading with the private key. LoadVault now drops the alias when the system names the cause, so the next write mints a fresh pair instead of sealing another unopenable file. And the caller that re-seals a plain cache asks for a fresh key outright: protection reappearing on the device is precisely the event that invalidated the old one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
954 lines
37 KiB
ObjectPascal
954 lines
37 KiB
ObjectPascal
unit PMA.Store;
|
|
{
|
|
Coffre garde localement, chiffre sous une cle du Android Keystore.
|
|
|
|
POURQUOI : ouvrir un conteneur coute ~4,5 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. Un vol du fichier seul est donc inexploitable, meme root -
|
|
il faut la puce.
|
|
|
|
---------------------------------------------------------------------------
|
|
POURQUOI UNE PAIRE DE CLES ET NON UNE CLE AES
|
|
|
|
Mesure sur appareil, 2026-08-15 :
|
|
|
|
cache non ecrit : EJNIException -
|
|
android.security.keystore.UserNotAuthenticatedException: User not authenticated
|
|
|
|
Une cle AES creee avec setUserAuthenticationRequired exige une
|
|
authentification recente **pour chiffrer autant que pour dechiffrer**. Or
|
|
l'ecriture suit une synchronisation silencieuse, sans invite : passe la
|
|
fenetre de validite, Cipher.init(ENCRYPT_MODE) leve, et le cache ne s'ecrit
|
|
jamais. La toute premiere ecriture ne reussissait que parce que le telephone
|
|
venait d'etre deverrouille - un test qui passe, puis ne passe plus.
|
|
|
|
Le motif documente par Android pour « ecrire a tout moment, lire sous
|
|
authentification » est une paire de cles : la partie publique chiffre sans
|
|
invite, la partie privee - elle protegee - dechiffre. RSA ne chiffrant pas
|
|
16 Ko, c'est un chiffrement hybride : une cle AES aleatoire par ecriture,
|
|
enveloppee par la cle publique.
|
|
|
|
DEUX PIEGES, tous deux corriges ici :
|
|
|
|
1. La cle publique rendue par le Keystore PORTE LES CONTRAINTES de la privee :
|
|
s'en servir telle quelle pour chiffrer releve la meme exception. Il faut la
|
|
reconstruire depuis sa forme encodee (KeyFactory + X509EncodedKeySpec), ce
|
|
qui donne une cle publique ordinaire, sans contrainte.
|
|
2. Cette reconstruction change de fournisseur, et « OAEPWithSHA-256AndMGF1
|
|
Padding » ne designe PAS le meme MGF1 des deux cotes - le Keystore prend
|
|
SHA-1 par defaut la ou le nom suggere SHA-256. Les parametres OAEP sont
|
|
donc passes EXPLICITEMENT au chiffrement comme au dechiffrement.
|
|
|
|
---------------------------------------------------------------------------
|
|
CE QUE CE FICHIER CONTIENT : tous les mots de passe en clair une fois
|
|
dechiffre, ET le mot de passe de sync (voir SaveVault). D'ou trois regles :
|
|
- jamais hors du repertoire prive de l'application ;
|
|
- jamais de copie en clair sur le disque, meme temporaire ;
|
|
- la sauvegarde automatique Android doit rester desactivee pour l'app
|
|
(android:allowBackup="false"), sinon Google en recupere une copie.
|
|
}
|
|
|
|
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 la protection par authentification. Si l'appareil n'a
|
|
/// ni empreinte ni code, on retombe sur une cle AES sans contrainte : mieux
|
|
/// vaut un cache lie au materiel que pas de cache du tout. VaultNeedsAuth dira
|
|
/// ensuite ce qui a reellement ete fait.
|
|
/// AFreshKey jette la paire existante avant d'ecrire. A poser quand la
|
|
/// protection vient de reapparaitre sur l'appareil : le changement qui l'a
|
|
/// ramenee a tres probablement invalide l'ancienne cle, et ca ne se verrait
|
|
/// qu'a la prochaine lecture - trop tard, le fichier serait deja scelle.
|
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
|
ARequireAuth: Boolean = False; AFreshKey: 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;
|
|
|
|
/// Pourquoi la derniere lecture a echoue. Vide apres un succes.
|
|
///
|
|
/// LoadVault N'EFFACE RIEN de lui-meme. Un cache detruit par une routine de
|
|
/// lecture, c'est un diagnostic errone qui coute un retelechargement et
|
|
/// 4,5 s de derivation - et qui emporte la cause avec lui. Elle rapporte,
|
|
/// l'appelant decide.
|
|
function LastStoreError: string;
|
|
|
|
/// Efface le coffre local. Seul un geste explicite de l'utilisateur, ou une
|
|
/// certitude, devrait l'appeler : il se retelecharge, mais pas gratuitement.
|
|
procedure ClearVault;
|
|
|
|
/// Chemin du fichier, pour le journal et les tests.
|
|
function VaultPath: string;
|
|
|
|
/// Reglages du serveur : URL et jeton Bearer.
|
|
///
|
|
/// FICHIER SEPARE, ET PROTECTION PLUS FAIBLE, DELIBEREMENT. Ils sont chiffres
|
|
/// par la cle AES du Keystore - liee au materiel, mais lisible SANS
|
|
/// authentification. Deux raisons :
|
|
/// - il faut pouvoir les relire pour reconstruire un coffre efface, alors
|
|
/// que la cle qui protege le coffre ne sert a rien s'il n'existe plus ;
|
|
/// - le jeton ne donne acces qu'a du CHIFFRE. Sans le mot de passe de sync,
|
|
/// qui lui reste dans le coffre protege, il ne vaut rien.
|
|
/// Retaper un jeton de 64 caracteres apres chaque purge de cache serait le
|
|
/// vrai risque : celui de le noter ailleurs.
|
|
procedure SaveDeviceSettings(const AUrl, AToken: string);
|
|
function LoadDeviceSettings(out AUrl, AToken: string): Boolean;
|
|
|
|
implementation
|
|
|
|
{$IF DEFINED(ANDROID)}
|
|
uses
|
|
Androidapi.Jni, Androidapi.JNIBridge, Androidapi.JNI.JavaTypes, Androidapi.Helpers;
|
|
{$ENDIF}
|
|
|
|
const
|
|
VAULT_FILE = 'vault.bin';
|
|
SETTINGS_FILE = 'settings.bin';
|
|
// Un alias par schema : la contrainte d'authentification est figee a la
|
|
// creation de la cle et ne peut pas etre ajoutee apres coup.
|
|
KEY_ALIAS = 'pm_vault_key'; // AES, sans authentification
|
|
KEY_ALIAS_WRAP = 'pm_vault_wrap'; // RSA, privee sous authentification
|
|
// Duree pendant laquelle la cle PRIVEE reste utilisable apres une
|
|
// authentification reussie. Ne concerne plus l'ecriture, qui n'en depend
|
|
// pas : 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.
|
|
PURPOSE_ENCRYPT = 1;
|
|
PURPOSE_DECRYPT = 2;
|
|
IV_LEN = 12; // GCM : 12 octets, la longueur recommandee
|
|
AES_LEN = 32; // AES-256
|
|
RSA_BITS = 2048;
|
|
// Octet d'en-tete : dit quel schema a ecrit le fichier, donc comment le lire
|
|
// et s'il faut authentifier d'abord.
|
|
VER_PLAIN = 1; // AES Keystore sans contrainte
|
|
VER_WRAP = 3; // AES aleatoire enveloppee par la cle publique RSA
|
|
RSA_TRANSFORM = 'RSA/ECB/OAEPWithSHA-256AndMGF1Padding';
|
|
|
|
var
|
|
FLastStoreError: string = '';
|
|
|
|
function LastStoreError: string;
|
|
begin
|
|
Result := FLastStoreError;
|
|
end;
|
|
|
|
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;
|
|
JPublicKeyX = interface;
|
|
JPrivateKeyX = interface;
|
|
JCertificateX = interface;
|
|
JKeySpecX = interface;
|
|
JX509EncodedKeySpec = interface;
|
|
JKeyFactoryX = interface;
|
|
JKeyPairX = interface;
|
|
JKeyPairGeneratorX = interface;
|
|
JSecureRandomX = interface;
|
|
JSecretKeySpecX = interface;
|
|
JMGF1ParameterSpec = interface;
|
|
JPSourceX = interface;
|
|
JPSource_PSpecified = interface;
|
|
JOAEPParameterSpec = interface;
|
|
|
|
JKeyXClass = interface(IJavaClass)
|
|
['{2C1A0B3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}']
|
|
end;
|
|
[JavaSignature('java/security/Key')]
|
|
JKeyX = interface(IJavaInstance)
|
|
['{3D2B1C4E-5F60-7B8C-9DAE-1F2A3B4C5D6E}']
|
|
function getEncoded: TJavaArray<Byte>; cdecl;
|
|
function getAlgorithm: JString; cdecl;
|
|
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;
|
|
|
|
JSecretKeySpecXClass = interface(JSecretKeyXClass)
|
|
['{11223344-5566-7788-99AA-BBCCDDEEFF00}']
|
|
function init(key: TJavaArray<Byte>; algorithm: JString): JSecretKeySpecX; cdecl;
|
|
end;
|
|
[JavaSignature('javax/crypto/spec/SecretKeySpec')]
|
|
JSecretKeySpecX = interface(JSecretKeyX)
|
|
['{22334455-6677-8899-AABB-CCDDEEFF0011}']
|
|
end;
|
|
TJSecretKeySpecX = class(TJavaGenericImport<JSecretKeySpecXClass, JSecretKeySpecX>) end;
|
|
|
|
JPublicKeyXClass = interface(JKeyXClass)
|
|
['{33445566-7788-99AA-BBCC-DDEEFF001122}']
|
|
end;
|
|
[JavaSignature('java/security/PublicKey')]
|
|
JPublicKeyX = interface(JKeyX)
|
|
['{44556677-8899-AABB-CCDD-EEFF00112233}']
|
|
end;
|
|
TJPublicKeyX = class(TJavaGenericImport<JPublicKeyXClass, JPublicKeyX>) end;
|
|
|
|
JPrivateKeyXClass = interface(JKeyXClass)
|
|
['{55667788-99AA-BBCC-DDEE-FF0011223344}']
|
|
end;
|
|
[JavaSignature('java/security/PrivateKey')]
|
|
JPrivateKeyX = interface(JKeyX)
|
|
['{66778899-AABB-CCDD-EEFF-001122334455}']
|
|
end;
|
|
TJPrivateKeyX = class(TJavaGenericImport<JPrivateKeyXClass, JPrivateKeyX>) end;
|
|
|
|
JCertificateXClass = interface(JObjectClass)
|
|
['{778899AA-BBCC-DDEE-FF00-112233445566}']
|
|
end;
|
|
[JavaSignature('java/security/cert/Certificate')]
|
|
JCertificateX = interface(JObject)
|
|
['{8899AABB-CCDD-EEFF-0011-223344556677}']
|
|
function getPublicKey: JPublicKeyX; cdecl;
|
|
end;
|
|
TJCertificateX = class(TJavaGenericImport<JCertificateXClass, JCertificateX>) end;
|
|
|
|
JKeySpecXClass = interface(IJavaClass)
|
|
['{99AABBCC-DDEE-FF00-1122-334455667788}']
|
|
end;
|
|
[JavaSignature('java/security/spec/KeySpec')]
|
|
JKeySpecX = interface(IJavaInstance)
|
|
['{AABBCCDD-EEFF-0011-2233-445566778899}']
|
|
end;
|
|
TJKeySpecX = class(TJavaGenericImport<JKeySpecXClass, JKeySpecX>) end;
|
|
|
|
JX509EncodedKeySpecClass = interface(JKeySpecXClass)
|
|
['{BBCCDDEE-FF00-1122-3344-5566778899AA}']
|
|
function init(encodedKey: TJavaArray<Byte>): JX509EncodedKeySpec; cdecl;
|
|
end;
|
|
[JavaSignature('java/security/spec/X509EncodedKeySpec')]
|
|
JX509EncodedKeySpec = interface(JKeySpecX)
|
|
['{CCDDEEFF-0011-2233-4455-66778899AABB}']
|
|
end;
|
|
TJX509EncodedKeySpec = class(TJavaGenericImport<JX509EncodedKeySpecClass, JX509EncodedKeySpec>) end;
|
|
|
|
JKeyFactoryXClass = interface(JObjectClass)
|
|
['{DDEEFF00-1122-3344-5566-778899AABBCC}']
|
|
function getInstance(algorithm: JString): JKeyFactoryX; cdecl;
|
|
end;
|
|
[JavaSignature('java/security/KeyFactory')]
|
|
JKeyFactoryX = interface(JObject)
|
|
['{EEFF0011-2233-4455-6677-8899AABBCCDD}']
|
|
function generatePublic(keySpec: JKeySpecX): JPublicKeyX; cdecl;
|
|
end;
|
|
TJKeyFactoryX = class(TJavaGenericImport<JKeyFactoryXClass, JKeyFactoryX>) end;
|
|
|
|
JKeyPairXClass = interface(JObjectClass)
|
|
['{FF001122-3344-5566-7788-99AABBCCDDEE}']
|
|
end;
|
|
[JavaSignature('java/security/KeyPair')]
|
|
JKeyPairX = interface(JObject)
|
|
['{00112233-4455-6677-8899-AABBCCDDEEFF}']
|
|
end;
|
|
TJKeyPairX = class(TJavaGenericImport<JKeyPairXClass, JKeyPairX>) 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;
|
|
|
|
JKeyPairGeneratorXClass = interface(JObjectClass)
|
|
['{12233445-5667-7889-9AAB-BCCDDEEFF001}']
|
|
function getInstance(algorithm: JString; provider: JString): JKeyPairGeneratorX; cdecl;
|
|
end;
|
|
[JavaSignature('java/security/KeyPairGenerator')]
|
|
JKeyPairGeneratorX = interface(JObject)
|
|
['{23344556-6778-899A-ABBC-CDDEEFF00112}']
|
|
procedure initialize(params: JAlgorithmParameterSpecX); cdecl;
|
|
function generateKeyPair: JKeyPairX; cdecl;
|
|
end;
|
|
TJKeyPairGeneratorX = class(TJavaGenericImport<JKeyPairGeneratorXClass, JKeyPairGeneratorX>) end;
|
|
|
|
JSecureRandomXClass = interface(JObjectClass)
|
|
['{34455667-7889-9AAB-BCCD-DEEFF0011223}']
|
|
function init: JSecureRandomX; cdecl;
|
|
end;
|
|
[JavaSignature('java/security/SecureRandom')]
|
|
JSecureRandomX = interface(JObject)
|
|
['{45566778-899A-ABBC-CDDE-EFF001122334}']
|
|
procedure nextBytes(bytes: TJavaArray<Byte>); cdecl;
|
|
end;
|
|
TJSecureRandomX = class(TJavaGenericImport<JSecureRandomXClass, JSecureRandomX>) end;
|
|
|
|
JMGF1ParameterSpecClass = interface(JAlgorithmParameterSpecXClass)
|
|
['{56677889-9AAB-BCCD-DEEF-F00112233445}']
|
|
function init(mdName: JString): JMGF1ParameterSpec; cdecl;
|
|
end;
|
|
[JavaSignature('java/security/spec/MGF1ParameterSpec')]
|
|
JMGF1ParameterSpec = interface(JAlgorithmParameterSpecX)
|
|
['{6778899A-ABBC-CDDE-EFF0-011223344556}']
|
|
end;
|
|
TJMGF1ParameterSpec = class(TJavaGenericImport<JMGF1ParameterSpecClass, JMGF1ParameterSpec>) end;
|
|
|
|
JPSourceXClass = interface(JObjectClass)
|
|
['{78899AAB-BCCD-DEEF-F001-122334455667}']
|
|
end;
|
|
[JavaSignature('javax/crypto/spec/PSource')]
|
|
JPSourceX = interface(JObject)
|
|
['{8899AABB-CCDD-EEFF-0011-223344556678}']
|
|
end;
|
|
TJPSourceX = class(TJavaGenericImport<JPSourceXClass, JPSourceX>) end;
|
|
|
|
JPSource_PSpecifiedClass = interface(JPSourceXClass)
|
|
['{99AABBCC-DDEE-FF00-1122-334455667789}']
|
|
function _GetDEFAULT: JPSource_PSpecified; cdecl;
|
|
property DEFAULT: JPSource_PSpecified read _GetDEFAULT;
|
|
end;
|
|
[JavaSignature('javax/crypto/spec/PSource$PSpecified')]
|
|
JPSource_PSpecified = interface(JPSourceX)
|
|
['{AABBCCDD-EEFF-0011-2233-44556677889A}']
|
|
end;
|
|
TJPSource_PSpecified = class(TJavaGenericImport<JPSource_PSpecifiedClass, JPSource_PSpecified>) end;
|
|
|
|
JOAEPParameterSpecClass = interface(JAlgorithmParameterSpecXClass)
|
|
['{BBCCDDEE-FF00-1122-3344-556677889AAB}']
|
|
function init(mdName: JString; mgfName: JString;
|
|
mgfSpec: JAlgorithmParameterSpecX; pSrc: JPSourceX): JOAEPParameterSpec; cdecl;
|
|
end;
|
|
[JavaSignature('javax/crypto/spec/OAEPParameterSpec')]
|
|
JOAEPParameterSpec = interface(JAlgorithmParameterSpecX)
|
|
['{CCDDEEFF-0011-2233-4455-66778899AABC}']
|
|
end;
|
|
TJOAEPParameterSpec = class(TJavaGenericImport<JOAEPParameterSpecClass, JOAEPParameterSpec>) 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 getCertificate(alias: JString): JCertificateX; cdecl;
|
|
function containsAlias(alias: JString): Boolean; cdecl;
|
|
procedure deleteEntry(alias: JString); 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 setDigests(digests: 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;
|
|
|
|
function OpenKeyStore: JKeyStoreX;
|
|
begin
|
|
Result := TJKeyStoreX.JavaClass.getInstance(StringToJString('AndroidKeyStore'));
|
|
Result.load(nil); // le magasin Android ne prend pas de flux
|
|
end;
|
|
|
|
/// Parametres OAEP explicites, identiques au chiffrement et au dechiffrement.
|
|
///
|
|
/// DEUX DIGESTS DIFFERENTS, ET C'EST VOULU : SHA-256 pour le message, SHA-1
|
|
/// pour le MGF1.
|
|
///
|
|
/// setDigests() n'autorise que le digest DU MESSAGE. Celui du MGF1 relevait
|
|
/// d'une autorisation distincte, dont le defaut est SHA-1 et le reste tant
|
|
/// qu'on n'appelle pas setMgf1Digests (API 31+). Demander MGF1-SHA-256 au
|
|
/// dechiffrement donne donc une cle qui refuse :
|
|
///
|
|
/// KeymasterUtils: rsp_header->status: -28
|
|
/// javax.crypto.IllegalBlockSizeException
|
|
///
|
|
/// Le chiffrement, lui, passait : la cle publique reconstruite est servie par
|
|
/// le fournisseur logiciel, qui accepte n'importe quel MGF1. D'ou un chiffre
|
|
/// parfaitement valide que le materiel refuse de lire - le pire des symptomes,
|
|
/// puisqu'il n'apparait qu'a la relecture.
|
|
///
|
|
/// SHA-1 en MGF1 n'affaiblit pas OAEP : la fonction de masque n'a pas besoin
|
|
/// de resistance aux collisions.
|
|
function OaepParams: JAlgorithmParameterSpecX;
|
|
begin
|
|
Result := TJOAEPParameterSpec.JavaClass.init(
|
|
StringToJString('SHA-256'),
|
|
StringToJString('MGF1'),
|
|
TJMGF1ParameterSpec.JavaClass.init(StringToJString('SHA-1')),
|
|
TJPSource_PSpecified.JavaClass.DEFAULT);
|
|
end;
|
|
|
|
/// La cle AES sans contrainte, pour les appareils sans protection d'ecran.
|
|
function EnsureAesKey: JKeyX;
|
|
var
|
|
LStore: JKeyStoreX;
|
|
LGen: JKeyGenerator;
|
|
LBuilder: JKeyGenParameterSpec_Builder;
|
|
LModes, LPaddings: TJavaObjectArray<JString>;
|
|
begin
|
|
LStore := OpenKeyStore;
|
|
if LStore.containsAlias(StringToJString(KEY_ALIAS)) then
|
|
Exit(LStore.getKey(StringToJString(KEY_ALIAS), 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(KEY_ALIAS), PURPOSE_ENCRYPT or PURPOSE_DECRYPT);
|
|
LBuilder := LBuilder.setBlockModes(LModes);
|
|
LBuilder := LBuilder.setEncryptionPaddings(LPaddings);
|
|
LBuilder := LBuilder.setKeySize(256);
|
|
|
|
LGen := TJKeyGenerator.JavaClass.getInstance(
|
|
StringToJString('AES'), StringToJString('AndroidKeyStore'));
|
|
LGen.init(LBuilder.build);
|
|
Result := LGen.generateKey; // stockee sous l'alias, pas retournee en clair
|
|
end;
|
|
|
|
/// Cree la paire RSA si elle manque. Leve si l'appareil n'a NI empreinte NI
|
|
/// code : une cle privee protegee par authentification n'aurait alors rien
|
|
/// pour la deverrouiller. L'appelant retombe sur la cle AES.
|
|
procedure EnsureWrapKey(AForceNew: Boolean = False);
|
|
var
|
|
LStore: JKeyStoreX;
|
|
LGen: JKeyPairGeneratorX;
|
|
LBuilder: JKeyGenParameterSpec_Builder;
|
|
LPaddings, LDigests: TJavaObjectArray<JString>;
|
|
begin
|
|
LStore := OpenKeyStore;
|
|
// AForceNew : la cle existe mais ne fonctionne plus. Retirer le
|
|
// verrouillage d'ecran, ou enroler une nouvelle empreinte, invalide
|
|
// DEFINITIVEMENT une cle protegee - or l'alias, lui, subsiste. Sans cette
|
|
// suppression on la reutiliserait indefiniment, chaque chiffrement
|
|
// echouerait, et on retomberait pour toujours sur la cle simple.
|
|
if AForceNew and LStore.containsAlias(StringToJString(KEY_ALIAS_WRAP)) then
|
|
LStore.deleteEntry(StringToJString(KEY_ALIAS_WRAP))
|
|
else if LStore.containsAlias(StringToJString(KEY_ALIAS_WRAP)) then
|
|
Exit;
|
|
|
|
LPaddings := TJavaObjectArray<JString>.Create(1);
|
|
LPaddings.Items[0] := StringToJString('OAEPPadding');
|
|
LDigests := TJavaObjectArray<JString>.Create(1);
|
|
LDigests.Items[0] := StringToJString('SHA-256');
|
|
|
|
LBuilder := TJKeyGenParameterSpec_Builder.JavaClass.init(
|
|
StringToJString(KEY_ALIAS_WRAP), PURPOSE_ENCRYPT or PURPOSE_DECRYPT);
|
|
LBuilder := LBuilder.setEncryptionPaddings(LPaddings);
|
|
LBuilder := LBuilder.setDigests(LDigests);
|
|
LBuilder := LBuilder.setKeySize(RSA_BITS);
|
|
LBuilder := LBuilder.setUserAuthenticationRequired(True);
|
|
LBuilder := LBuilder.setUserAuthenticationValidityDurationSeconds(AUTH_VALIDITY_SECONDS);
|
|
|
|
LGen := TJKeyPairGeneratorX.JavaClass.getInstance(
|
|
StringToJString('RSA'), StringToJString('AndroidKeyStore'));
|
|
LGen.initialize(LBuilder.build);
|
|
LGen.generateKeyPair;
|
|
end;
|
|
|
|
/// La cle publique DEBARRASSEE des contraintes de sa jumelle privee.
|
|
/// Telle que rendue par le Keystore elle les porte, et chiffrer avec elle
|
|
/// leverait UserNotAuthenticatedException - exactement ce qu'on fuit.
|
|
function UnrestrictedPublicKey: JPublicKeyX;
|
|
var
|
|
LStore: JKeyStoreX;
|
|
LCert: JCertificateX;
|
|
LPub: JPublicKeyX;
|
|
LFactory: JKeyFactoryX;
|
|
begin
|
|
LStore := OpenKeyStore;
|
|
LCert := LStore.getCertificate(StringToJString(KEY_ALIAS_WRAP));
|
|
if LCert = nil then
|
|
raise EPMAStore.Create('Public key not found in the Keystore');
|
|
LPub := LCert.getPublicKey;
|
|
LFactory := TJKeyFactoryX.JavaClass.getInstance(LPub.getAlgorithm);
|
|
Result := LFactory.generatePublic(
|
|
TJX509EncodedKeySpec.JavaClass.init(LPub.getEncoded));
|
|
end;
|
|
|
|
function RandomBytes(ACount: Integer): TBytes;
|
|
var
|
|
LArr: TJavaArray<Byte>;
|
|
begin
|
|
LArr := TJavaArray<Byte>.Create(ACount);
|
|
TJSecureRandomX.JavaClass.init.nextBytes(LArr);
|
|
Result := FromJava(LArr);
|
|
end;
|
|
{$ENDIF}
|
|
|
|
{$IF DEFINED(ANDROID)}
|
|
/// Chiffre sous la cle AES du Keystore, sans contrainte d'authentification.
|
|
/// Format : [IV][chiffre+tag]. Sert aux reglages, pas au coffre.
|
|
function AesSeal(const APlain: TBytes): TBytes;
|
|
var
|
|
LCipher: JCipherX;
|
|
LIv, LCt: TBytes;
|
|
begin
|
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
|
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE, EnsureAesKey);
|
|
LIv := FromJava(LCipher.getIV); // genere par le Keystore, jamais choisi ici
|
|
LCt := FromJava(LCipher.doFinal(ToJava(APlain)));
|
|
SetLength(Result, Length(LIv) + Length(LCt));
|
|
Move(LIv[0], Result[0], Length(LIv));
|
|
Move(LCt[0], Result[Length(LIv)], Length(LCt));
|
|
end;
|
|
|
|
function AesOpen(const ARaw: TBytes): TBytes;
|
|
var
|
|
LCipher: JCipherX;
|
|
LIv, LCt: TBytes;
|
|
begin
|
|
if Length(ARaw) < IV_LEN + 16 then Exit(nil);
|
|
SetLength(LIv, IV_LEN);
|
|
Move(ARaw[0], LIv[0], IV_LEN);
|
|
SetLength(LCt, Length(ARaw) - IV_LEN);
|
|
Move(ARaw[IV_LEN], LCt[0], Length(LCt));
|
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
|
LCipher.init(TJCipherX.JavaClass.DECRYPT_MODE, EnsureAesKey,
|
|
TJGCMParameterSpecX.JavaClass.init(128, ToJava(LIv)));
|
|
Result := FromJava(LCipher.doFinal(ToJava(LCt)));
|
|
end;
|
|
{$ENDIF}
|
|
|
|
procedure SaveDeviceSettings(const AUrl, AToken: string);
|
|
{$IF DEFINED(ANDROID)}
|
|
var
|
|
LObj: TJSONObject;
|
|
begin
|
|
LObj := TJSONObject.Create;
|
|
try
|
|
LObj.AddPair('url', AUrl);
|
|
LObj.AddPair('token', AToken);
|
|
try
|
|
TFile.WriteAllBytes(TPath.Combine(TPath.GetHomePath, SETTINGS_FILE),
|
|
AesSeal(TEncoding.UTF8.GetBytes(LObj.ToJSON)));
|
|
except
|
|
// Des reglages non enregistres se retapent ; ce n'est pas une raison
|
|
// d'interrompre une synchronisation qui, elle, a reussi.
|
|
on E: Exception do FLastStoreError := E.ClassName + ': ' + E.Message;
|
|
end;
|
|
finally
|
|
LObj.Free;
|
|
end;
|
|
end;
|
|
{$ELSE}
|
|
begin
|
|
end;
|
|
{$ENDIF}
|
|
|
|
function LoadDeviceSettings(out AUrl, AToken: string): Boolean;
|
|
{$IF DEFINED(ANDROID)}
|
|
var
|
|
LPath: string;
|
|
LObj: TJSONObject;
|
|
begin
|
|
Result := False;
|
|
AUrl := '';
|
|
AToken := '';
|
|
LPath := TPath.Combine(TPath.GetHomePath, SETTINGS_FILE);
|
|
if not TFile.Exists(LPath) then Exit;
|
|
try
|
|
LObj := TJSONObject.ParseJSONValue(
|
|
TEncoding.UTF8.GetString(AesOpen(TFile.ReadAllBytes(LPath)))) as TJSONObject;
|
|
if LObj = nil then Exit;
|
|
try
|
|
AUrl := LObj.GetValue<string>('url', '');
|
|
AToken := LObj.GetValue<string>('token', '');
|
|
Result := AUrl <> '';
|
|
finally
|
|
LObj.Free;
|
|
end;
|
|
except
|
|
Result := False;
|
|
end;
|
|
end;
|
|
{$ELSE}
|
|
begin
|
|
Result := False;
|
|
AUrl := '';
|
|
AToken := '';
|
|
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 = VER_WRAP;
|
|
finally
|
|
LFs.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
|
ARequireAuth, AFreshKey: Boolean);
|
|
{$IF DEFINED(ANDROID)}
|
|
var
|
|
LWrap: TJSONObject;
|
|
LPlain, LIv, LCt, LOut, LAesRaw, LWrapped: TBytes;
|
|
LCipher: JCipherX;
|
|
LGotAuth: Boolean;
|
|
LOfs: Integer;
|
|
begin
|
|
// ETag, mot de passe 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;
|
|
|
|
LGotAuth := False;
|
|
LWrapped := nil;
|
|
if ARequireAuth then
|
|
begin
|
|
// UN SEUL ESSAI, et AFreshKey decide de tout.
|
|
//
|
|
// Le chiffrement NE PEUT PAS detecter une cle invalidee : la cle publique
|
|
// est reconstruite par KeyFactory, c'est une cle RSA ordinaire sans lien
|
|
// avec le Keystore, et elle chiffre parfaitement meme quand sa moitie
|
|
// privee est morte. Une boucle de reprise ici ne se declencherait jamais -
|
|
// elle produirait un fichier scelle qu'on ne pourrait plus jamais ouvrir.
|
|
//
|
|
// L'invalidation ne se constate qu'a la LECTURE, ou LoadVault jette
|
|
// l'alias. Et l'appelant qui rescelle un cache reste passe True, parce que
|
|
// la protection vient justement de reapparaitre - donc l'ancienne cle a
|
|
// tres probablement ete invalidee par ce meme changement.
|
|
try
|
|
EnsureWrapKey(AFreshKey);
|
|
// Cle AES jetable, tiree du generateur cryptographique de la
|
|
// plateforme - jamais de Random ici.
|
|
LAesRaw := RandomBytes(AES_LEN);
|
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString(RSA_TRANSFORM));
|
|
// AUCUNE authentification requise : la cle publique reconstruite ne
|
|
// porte plus les contraintes de la privee. C'est tout l'interet.
|
|
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE, UnrestrictedPublicKey,
|
|
OaepParams);
|
|
LWrapped := FromJava(LCipher.doFinal(ToJava(LAesRaw)));
|
|
LGotAuth := True;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
LWrapped := nil; // appareil sans protection d'ecran configuree
|
|
FLastStoreError := E.ClassName + ': ' + E.Message;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
if LGotAuth then
|
|
begin
|
|
// AES logicielle sous la cle jetable : rien ici ne passe par le Keystore,
|
|
// donc rien n'exige d'authentification.
|
|
LIv := RandomBytes(IV_LEN);
|
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
|
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE,
|
|
TJSecretKeySpecX.JavaClass.init(ToJava(LAesRaw), StringToJString('AES')),
|
|
TJGCMParameterSpecX.JavaClass.init(128, ToJava(LIv)));
|
|
LCt := FromJava(LCipher.doFinal(ToJava(LPlain)));
|
|
|
|
SetLength(LOut, 1 + 2 + Length(LWrapped) + IV_LEN + Length(LCt));
|
|
LOut[0] := VER_WRAP;
|
|
LOut[1] := Byte(Length(LWrapped) shr 8);
|
|
LOut[2] := Byte(Length(LWrapped) and $FF);
|
|
LOfs := 3;
|
|
Move(LWrapped[0], LOut[LOfs], Length(LWrapped)); Inc(LOfs, Length(LWrapped));
|
|
Move(LIv[0], LOut[LOfs], IV_LEN); Inc(LOfs, IV_LEN);
|
|
Move(LCt[0], LOut[LOfs], Length(LCt));
|
|
end
|
|
else
|
|
begin
|
|
// Repli : cle AES du Keystore, sans contrainte. Liee au materiel, mais
|
|
// pas a l'utilisateur.
|
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
|
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE, EnsureAesKey);
|
|
// L'IV est GENERE par le Keystore ici, jamais choisi par nous.
|
|
LIv := FromJava(LCipher.getIV);
|
|
LCt := FromJava(LCipher.doFinal(ToJava(LPlain)));
|
|
|
|
SetLength(LOut, 1 + Length(LIv) + Length(LCt));
|
|
LOut[0] := VER_PLAIN;
|
|
Move(LIv[0], LOut[1], Length(LIv));
|
|
Move(LCt[0], LOut[1 + Length(LIv)], Length(LCt));
|
|
end;
|
|
|
|
TFile.WriteAllBytes(VaultPath, LOut);
|
|
end;
|
|
{$ELSE}
|
|
begin
|
|
raise EPMAStore.Create('Keystore storage is Android-only');
|
|
end;
|
|
{$ENDIF}
|
|
|
|
function LoadVault(out APlainJson, AETag, ASyncPwd: string): Boolean;
|
|
{$IF DEFINED(ANDROID)}
|
|
var
|
|
LRaw, LIv, LCt, LPlain, LWrapped, LAesRaw: TBytes;
|
|
LCipher: JCipherX;
|
|
LWrap: TJSONObject;
|
|
LStore: JKeyStoreX;
|
|
LLen, LOfs: Integer;
|
|
begin
|
|
Result := False;
|
|
APlainJson := '';
|
|
AETag := '';
|
|
ASyncPwd := '';
|
|
FLastStoreError := '';
|
|
if not TFile.Exists(VaultPath) then Exit;
|
|
|
|
try
|
|
LRaw := TFile.ReadAllBytes(VaultPath);
|
|
if Length(LRaw) < 1 then Exit;
|
|
|
|
if LRaw[0] = VER_WRAP then
|
|
begin
|
|
if Length(LRaw) < 3 then Exit;
|
|
LLen := (LRaw[1] shl 8) or LRaw[2];
|
|
if Length(LRaw) < 3 + LLen + IV_LEN + 16 then Exit;
|
|
LOfs := 3;
|
|
SetLength(LWrapped, LLen);
|
|
Move(LRaw[LOfs], LWrapped[0], LLen); Inc(LOfs, LLen);
|
|
SetLength(LIv, IV_LEN);
|
|
Move(LRaw[LOfs], LIv[0], IV_LEN); Inc(LOfs, IV_LEN);
|
|
SetLength(LCt, Length(LRaw) - LOfs);
|
|
Move(LRaw[LOfs], LCt[0], Length(LCt));
|
|
|
|
// C'EST ICI que l'authentification est exigee, et nulle part ailleurs.
|
|
// Si elle n'a pas eu lieu recemment, l'init leve
|
|
// UserNotAuthenticatedException - attrapee plus bas, sans effacer.
|
|
LStore := OpenKeyStore;
|
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString(RSA_TRANSFORM));
|
|
LCipher.init(TJCipherX.JavaClass.DECRYPT_MODE,
|
|
LStore.getKey(StringToJString(KEY_ALIAS_WRAP), nil),
|
|
OaepParams);
|
|
LAesRaw := FromJava(LCipher.doFinal(ToJava(LWrapped)));
|
|
|
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
|
LCipher.init(TJCipherX.JavaClass.DECRYPT_MODE,
|
|
TJSecretKeySpecX.JavaClass.init(ToJava(LAesRaw), StringToJString('AES')),
|
|
TJGCMParameterSpecX.JavaClass.init(128, ToJava(LIv)));
|
|
LPlain := FromJava(LCipher.doFinal(ToJava(LCt)));
|
|
end
|
|
else if LRaw[0] = VER_PLAIN then
|
|
begin
|
|
if Length(LRaw) < 1 + IV_LEN + 16 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'));
|
|
LCipher.init(TJCipherX.JavaClass.DECRYPT_MODE, EnsureAesKey,
|
|
TJGCMParameterSpecX.JavaClass.init(128, ToJava(LIv)));
|
|
LPlain := FromJava(LCipher.doFinal(ToJava(LCt)));
|
|
end
|
|
else
|
|
Exit; // en-tete inconnu, y compris l'ancien schema AES protege (2)
|
|
|
|
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;
|
|
// ON N'EFFACE PAS LE COFFRE. Le tri « cle perdue / pas authentifie » se
|
|
// faisait ici sur le texte du message, et une erreur de tri detruisait
|
|
// le cache en emportant la seule trace de sa cause. L'appelant a le
|
|
// journal et le contexte - lui seul sait si l'utilisateur a demande
|
|
// quelque chose.
|
|
FLastStoreError := E.ClassName + ': ' + E.Message;
|
|
|
|
// ON JETTE EN REVANCHE LA CLE, quand le systeme dit lui-meme qu'elle est
|
|
// morte. C'est le SEUL moment ou l'invalidation se constate : le
|
|
// chiffrement, lui, reussit toujours - la cle publique reconstruite est
|
|
// une cle RSA ordinaire, sans lien avec le Keystore. Ne pas la jeter ici
|
|
// condamnerait toutes les ecritures suivantes a sceller des fichiers que
|
|
// rien ne pourra rouvrir.
|
|
if Pos('KeyPermanentlyInvalidated', E.ClassName + E.Message) > 0 then
|
|
try
|
|
OpenKeyStore.deleteEntry(StringToJString(KEY_ALIAS_WRAP));
|
|
FLastStoreError := FLastStoreError + ' (cle invalidee, jetee)';
|
|
except
|
|
// Rien : le message d'origine reste le plus utile.
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
{$ELSE}
|
|
begin
|
|
Result := False;
|
|
APlainJson := '';
|
|
AETag := '';
|
|
ASyncPwd := '';
|
|
end;
|
|
{$ENDIF}
|
|
|
|
end.
|