feat(android): wrap the cache key with an asymmetric Keystore pair
Measured on device: android.security.keystore.UserNotAuthenticatedException on every write. An AES key created with setUserAuthenticationRequired demands a recent authentication to ENCRYPT as well as to decrypt, and the write follows a silent sync with no prompt. The very first write only succeeded because the phone had just been unlocked - a test that passes, then stops passing. Now an RSA pair: the public half wraps a throwaway AES key with no prompt, the private half - the guarded one - unwraps it. Authentication is required at read time and nowhere else. Two documented traps handled: the public key returned by the Keystore carries the private key's constraints, so it is rebuilt from its encoded form; and that rebuild changes provider, so the OAEP parameters are passed explicitly on both sides - the Keystore defaults MGF1 to SHA-1 where the transformation name suggests SHA-256. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+407
-91
@@ -2,7 +2,7 @@ unit PMA.Store;
|
|||||||
{
|
{
|
||||||
Coffre garde localement, chiffre sous une cle du Android Keystore.
|
Coffre garde localement, chiffre sous une cle du Android Keystore.
|
||||||
|
|
||||||
POURQUOI : ouvrir un conteneur coute ~4 s (PBKDF2 600k, incompressible - le
|
POURQUOI : ouvrir un conteneur coute ~4,5 s (PBKDF2 600k, incompressible - le
|
||||||
nombre d'iterations appartient au format ecrit par le poste Windows). Payer
|
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
|
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
|
contenu n'a pas change. On ne le paie donc qu'a l'arrivee d'un instantane
|
||||||
@@ -10,20 +10,48 @@ unit PMA.Store;
|
|||||||
materiellement, donc instantane.
|
materiellement, donc instantane.
|
||||||
|
|
||||||
LA CLE NE QUITTE JAMAIS LE MATERIEL. Elle est generee dans le Keystore et
|
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
|
n'en sort pas. Un vol du fichier seul est donc inexploitable, meme root -
|
||||||
vol du fichier seul est donc inexploitable, meme root - il faut la puce.
|
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
|
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).
|
dechiffre, ET le mot de passe de sync (voir SaveVault). D'ou trois regles :
|
||||||
D'ou trois regles tenues ici :
|
|
||||||
- jamais hors du repertoire prive de l'application ;
|
- jamais hors du repertoire prive de l'application ;
|
||||||
- jamais de copie en clair sur le disque, meme temporaire ;
|
- jamais de copie en clair sur le disque, meme temporaire ;
|
||||||
- la sauvegarde automatique Android doit etre desactivee pour l'app
|
- la sauvegarde automatique Android doit rester desactivee pour l'app
|
||||||
(android:allowBackup="false"), sinon Google en recupere une copie.
|
(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
|
interface
|
||||||
@@ -47,10 +75,10 @@ function HasVault: Boolean;
|
|||||||
/// tous les mots de passe en clair ; celui-ci n'y ajoute presque rien en
|
/// tous les mots de passe en clair ; celui-ci n'y ajoute presque rien en
|
||||||
/// exposition, et rend la synchronisation silencieuse.
|
/// exposition, et rend la synchronisation silencieuse.
|
||||||
///
|
///
|
||||||
/// ARequireAuth demande une cle deverrouillee par biometrie ou code. Si
|
/// ARequireAuth demande la protection par authentification. Si l'appareil n'a
|
||||||
/// l'appareil n'a aucune de ces protections, on retombe silencieusement sur
|
/// ni empreinte ni code, on retombe sur une cle AES sans contrainte : mieux
|
||||||
/// une cle sans contrainte : mieux vaut un cache lie au materiel que pas de
|
/// vaut un cache lie au materiel que pas de cache du tout. VaultNeedsAuth dira
|
||||||
/// cache du tout. VaultNeedsAuth dira ensuite ce qui a reellement ete fait.
|
/// ensuite ce qui a reellement ete fait.
|
||||||
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
||||||
ARequireAuth: Boolean = False);
|
ARequireAuth: Boolean = False);
|
||||||
|
|
||||||
@@ -79,22 +107,27 @@ uses
|
|||||||
|
|
||||||
const
|
const
|
||||||
VAULT_FILE = 'vault.bin';
|
VAULT_FILE = 'vault.bin';
|
||||||
// DEUX alias : la contrainte d'authentification est figee a la creation de
|
// Un alias par schema : la contrainte d'authentification est figee a la
|
||||||
// la cle et ne peut pas etre ajoutee apres coup. Basculer d'un mode a
|
// creation de la cle et ne peut pas etre ajoutee apres coup.
|
||||||
// l'autre change donc de cle, ce qui rend l'ancien fichier illisible - il
|
KEY_ALIAS = 'pm_vault_key'; // AES, sans authentification
|
||||||
// sera efface et retelecharge, sans perte.
|
KEY_ALIAS_WRAP = 'pm_vault_wrap'; // RSA, privee sous authentification
|
||||||
KEY_ALIAS = 'pm_vault_key'; // sans authentification
|
// Duree pendant laquelle la cle PRIVEE reste utilisable apres une
|
||||||
KEY_ALIAS_BIO = 'pm_vault_key_bio'; // deverrouillee par biometrie/code
|
// authentification reussie. Ne concerne plus l'ecriture, qui n'en depend
|
||||||
// Duree pendant laquelle la cle reste utilisable apres une authentification
|
// pas : assez pour dechiffrer et afficher, trop court pour qu'un telephone
|
||||||
// reussie. Assez pour dechiffrer et afficher, trop court pour qu'un
|
// laisse sans surveillance reste exploitable.
|
||||||
// telephone laisse sans surveillance reste exploitable.
|
|
||||||
AUTH_VALIDITY_SECONDS = 30;
|
AUTH_VALIDITY_SECONDS = 30;
|
||||||
// Constantes d'android.security.keystore.KeyProperties, reprises en dur
|
// Constantes d'android.security.keystore.KeyProperties, reprises en dur
|
||||||
// plutot qu'importees : elles sont figees par la plateforme, et cela evite
|
// plutot qu'importees : elles sont figees par la plateforme.
|
||||||
// une declaration JNI de plus.
|
|
||||||
PURPOSE_ENCRYPT = 1;
|
PURPOSE_ENCRYPT = 1;
|
||||||
PURPOSE_DECRYPT = 2;
|
PURPOSE_DECRYPT = 2;
|
||||||
IV_LEN = 12; // GCM : 12 octets, la longueur recommandee
|
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';
|
||||||
|
|
||||||
function VaultPath: string;
|
function VaultPath: string;
|
||||||
begin
|
begin
|
||||||
@@ -124,6 +157,20 @@ type
|
|||||||
JGCMParameterSpecX = interface;
|
JGCMParameterSpecX = interface;
|
||||||
JAlgorithmParameterSpecX = interface;
|
JAlgorithmParameterSpecX = interface;
|
||||||
JKeyX = 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)
|
JKeyXClass = interface(IJavaClass)
|
||||||
['{2C1A0B3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}']
|
['{2C1A0B3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}']
|
||||||
@@ -131,6 +178,8 @@ type
|
|||||||
[JavaSignature('java/security/Key')]
|
[JavaSignature('java/security/Key')]
|
||||||
JKeyX = interface(IJavaInstance)
|
JKeyX = interface(IJavaInstance)
|
||||||
['{3D2B1C4E-5F60-7B8C-9DAE-1F2A3B4C5D6E}']
|
['{3D2B1C4E-5F60-7B8C-9DAE-1F2A3B4C5D6E}']
|
||||||
|
function getEncoded: TJavaArray<Byte>; cdecl;
|
||||||
|
function getAlgorithm: JString; cdecl;
|
||||||
end;
|
end;
|
||||||
TJKeyX = class(TJavaGenericImport<JKeyXClass, JKeyX>) end;
|
TJKeyX = class(TJavaGenericImport<JKeyXClass, JKeyX>) end;
|
||||||
|
|
||||||
@@ -143,6 +192,83 @@ type
|
|||||||
end;
|
end;
|
||||||
TJSecretKeyX = class(TJavaGenericImport<JSecretKeyXClass, JSecretKeyX>) 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)
|
JAlgorithmParameterSpecXClass = interface(IJavaClass)
|
||||||
['{605E4F71-8293-AEBF-C0D1-4C5D6E7F8091}']
|
['{605E4F71-8293-AEBF-C0D1-4C5D6E7F8091}']
|
||||||
end;
|
end;
|
||||||
@@ -152,6 +278,70 @@ type
|
|||||||
end;
|
end;
|
||||||
TJAlgorithmParameterSpecX = class(TJavaGenericImport<JAlgorithmParameterSpecXClass, JAlgorithmParameterSpecX>) 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)
|
JGCMParameterSpecXClass = interface(JAlgorithmParameterSpecXClass)
|
||||||
['{82706193-A4B5-C0D1-E2F3-6E7F8091A2B3}']
|
['{82706193-A4B5-C0D1-E2F3-6E7F8091A2B3}']
|
||||||
function init(tLen: Integer; src: TJavaArray<Byte>): JGCMParameterSpecX; cdecl;
|
function init(tLen: Integer; src: TJavaArray<Byte>): JGCMParameterSpecX; cdecl;
|
||||||
@@ -164,7 +354,7 @@ type
|
|||||||
|
|
||||||
// KeyStore.load a deux surcharges, et celle a un argument prend un
|
// KeyStore.load a deux surcharges, et celle a un argument prend un
|
||||||
// LoadStoreParameter - pas un Object. Declarer JObject faisait chercher au
|
// LoadStoreParameter - pas un Object. Declarer JObject faisait chercher au
|
||||||
// pont une methode inexistante : "methode introuvable" a l'execution, sans
|
// pont une methode inexistante : « methode introuvable » a l'execution, sans
|
||||||
// rien signaler a la compilation.
|
// rien signaler a la compilation.
|
||||||
JLoadStoreParameterClass = interface(IJavaClass)
|
JLoadStoreParameterClass = interface(IJavaClass)
|
||||||
['{1C2D3E4F-5061-7283-94A5-B6C7D8E9F001}']
|
['{1C2D3E4F-5061-7283-94A5-B6C7D8E9F001}']
|
||||||
@@ -184,6 +374,7 @@ type
|
|||||||
['{B5A394C6-D7E8-F304-1526-91A2B3C4D5E6}']
|
['{B5A394C6-D7E8-F304-1526-91A2B3C4D5E6}']
|
||||||
procedure load(param: JLoadStoreParameter); cdecl;
|
procedure load(param: JLoadStoreParameter); cdecl;
|
||||||
function getKey(alias: JString; password: TJavaArray<Char>): JKeyX; cdecl;
|
function getKey(alias: JString; password: TJavaArray<Char>): JKeyX; cdecl;
|
||||||
|
function getCertificate(alias: JString): JCertificateX; cdecl;
|
||||||
function containsAlias(alias: JString): Boolean; cdecl;
|
function containsAlias(alias: JString): Boolean; cdecl;
|
||||||
end;
|
end;
|
||||||
TJKeyStoreX = class(TJavaGenericImport<JKeyStoreXClass, JKeyStoreX>) end;
|
TJKeyStoreX = class(TJavaGenericImport<JKeyStoreXClass, JKeyStoreX>) end;
|
||||||
@@ -197,6 +388,7 @@ type
|
|||||||
['{D7C5B6E8-F90A-1526-3748-B3C4D5E6F708}']
|
['{D7C5B6E8-F90A-1526-3748-B3C4D5E6F708}']
|
||||||
function setBlockModes(blockModes: TJavaObjectArray<JString>): JKeyGenParameterSpec_Builder; cdecl;
|
function setBlockModes(blockModes: TJavaObjectArray<JString>): JKeyGenParameterSpec_Builder; cdecl;
|
||||||
function setEncryptionPaddings(paddings: 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 setKeySize(keySize: Integer): JKeyGenParameterSpec_Builder; cdecl;
|
||||||
function setUserAuthenticationRequired(required: Boolean): JKeyGenParameterSpec_Builder; cdecl;
|
function setUserAuthenticationRequired(required: Boolean): JKeyGenParameterSpec_Builder; cdecl;
|
||||||
// Deprecie depuis l'API 30 au profit de setUserAuthenticationParameters,
|
// Deprecie depuis l'API 30 au profit de setUserAuthenticationParameters,
|
||||||
@@ -261,24 +453,35 @@ begin
|
|||||||
for I := 0 to A.Length - 1 do Result[I] := A.Items[I];
|
for I := 0 to A.Length - 1 do Result[I] := A.Items[I];
|
||||||
end;
|
end;
|
||||||
|
|
||||||
/// La cle du Keystore, creee a la premiere utilisation. Elle n'est jamais
|
function OpenKeyStore: JKeyStoreX;
|
||||||
/// lue : on obtient une poignee vers un objet que seul le materiel peut
|
begin
|
||||||
/// utiliser.
|
Result := TJKeyStoreX.JavaClass.getInstance(StringToJString('AndroidKeyStore'));
|
||||||
function EnsureKey(ARequireAuth: Boolean): JKeyX;
|
Result.load(nil); // le magasin Android ne prend pas de flux
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// Parametres OAEP explicites. Sans eux, le Keystore et le fournisseur par
|
||||||
|
/// defaut ne s'accordent pas sur le MGF1 - SHA-1 d'un cote, SHA-256 de
|
||||||
|
/// l'autre - et le dechiffrement echoue sur un chiffre pourtant valide.
|
||||||
|
function OaepParams: JAlgorithmParameterSpecX;
|
||||||
|
begin
|
||||||
|
Result := TJOAEPParameterSpec.JavaClass.init(
|
||||||
|
StringToJString('SHA-256'),
|
||||||
|
StringToJString('MGF1'),
|
||||||
|
TJMGF1ParameterSpec.JavaClass.init(StringToJString('SHA-256')),
|
||||||
|
TJPSource_PSpecified.JavaClass.DEFAULT);
|
||||||
|
end;
|
||||||
|
|
||||||
|
/// La cle AES sans contrainte, pour les appareils sans protection d'ecran.
|
||||||
|
function EnsureAesKey: JKeyX;
|
||||||
var
|
var
|
||||||
LStore: JKeyStoreX;
|
LStore: JKeyStoreX;
|
||||||
LGen: JKeyGenerator;
|
LGen: JKeyGenerator;
|
||||||
LBuilder: JKeyGenParameterSpec_Builder;
|
LBuilder: JKeyGenParameterSpec_Builder;
|
||||||
LModes, LPaddings: TJavaObjectArray<JString>;
|
LModes, LPaddings: TJavaObjectArray<JString>;
|
||||||
LAlias: string;
|
|
||||||
begin
|
begin
|
||||||
if ARequireAuth then LAlias := KEY_ALIAS_BIO else LAlias := KEY_ALIAS;
|
LStore := OpenKeyStore;
|
||||||
|
if LStore.containsAlias(StringToJString(KEY_ALIAS)) then
|
||||||
LStore := TJKeyStoreX.JavaClass.getInstance(StringToJString('AndroidKeyStore'));
|
Exit(LStore.getKey(StringToJString(KEY_ALIAS), nil));
|
||||||
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 := TJavaObjectArray<JString>.Create(1);
|
||||||
LModes.Items[0] := StringToJString('GCM');
|
LModes.Items[0] := StringToJString('GCM');
|
||||||
@@ -286,24 +489,77 @@ begin
|
|||||||
LPaddings.Items[0] := StringToJString('NoPadding');
|
LPaddings.Items[0] := StringToJString('NoPadding');
|
||||||
|
|
||||||
LBuilder := TJKeyGenParameterSpec_Builder.JavaClass.init(
|
LBuilder := TJKeyGenParameterSpec_Builder.JavaClass.init(
|
||||||
StringToJString(LAlias), PURPOSE_ENCRYPT or PURPOSE_DECRYPT);
|
StringToJString(KEY_ALIAS), PURPOSE_ENCRYPT or PURPOSE_DECRYPT);
|
||||||
LBuilder := LBuilder.setBlockModes(LModes);
|
LBuilder := LBuilder.setBlockModes(LModes);
|
||||||
LBuilder := LBuilder.setEncryptionPaddings(LPaddings);
|
LBuilder := LBuilder.setEncryptionPaddings(LPaddings);
|
||||||
LBuilder := LBuilder.setKeySize(256);
|
LBuilder := LBuilder.setKeySize(256);
|
||||||
if ARequireAuth then
|
|
||||||
begin
|
|
||||||
LBuilder := LBuilder.setUserAuthenticationRequired(True);
|
|
||||||
LBuilder := LBuilder.setUserAuthenticationValidityDurationSeconds(AUTH_VALIDITY_SECONDS);
|
|
||||||
end;
|
|
||||||
|
|
||||||
LGen := TJKeyGenerator.JavaClass.getInstance(
|
LGen := TJKeyGenerator.JavaClass.getInstance(
|
||||||
StringToJString('AES'), StringToJString('AndroidKeyStore'));
|
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);
|
LGen.init(LBuilder.build);
|
||||||
Result := LGen.generateKey; // stockee sous l'alias, pas retournee en clair
|
Result := LGen.generateKey; // stockee sous l'alias, pas retournee en clair
|
||||||
end;
|
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;
|
||||||
|
var
|
||||||
|
LStore: JKeyStoreX;
|
||||||
|
LGen: JKeyPairGeneratorX;
|
||||||
|
LBuilder: JKeyGenParameterSpec_Builder;
|
||||||
|
LPaddings, LDigests: TJavaObjectArray<JString>;
|
||||||
|
begin
|
||||||
|
LStore := OpenKeyStore;
|
||||||
|
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('Cle publique introuvable dans le 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}
|
{$ENDIF}
|
||||||
|
|
||||||
function VaultNeedsAuth: Boolean;
|
function VaultNeedsAuth: Boolean;
|
||||||
@@ -317,7 +573,7 @@ begin
|
|||||||
try
|
try
|
||||||
if LFs.Size < 1 then Exit;
|
if LFs.Size < 1 then Exit;
|
||||||
LFs.ReadBuffer(LVer, 1);
|
LFs.ReadBuffer(LVer, 1);
|
||||||
Result := LVer = 2; // 1 = cle simple, 2 = cle sous authentification
|
Result := LVer = VER_WRAP;
|
||||||
finally
|
finally
|
||||||
LFs.Free;
|
LFs.Free;
|
||||||
end;
|
end;
|
||||||
@@ -328,14 +584,14 @@ procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
|||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
var
|
var
|
||||||
LWrap: TJSONObject;
|
LWrap: TJSONObject;
|
||||||
LPlain, LIv, LCt, LOut: TBytes;
|
LPlain, LIv, LCt, LOut, LAesRaw, LWrapped: TBytes;
|
||||||
LCipher: JCipherX;
|
LCipher: JCipherX;
|
||||||
LKey: JKeyX;
|
|
||||||
LGotAuth: Boolean;
|
LGotAuth: Boolean;
|
||||||
|
LOfs: Integer;
|
||||||
begin
|
begin
|
||||||
// ETag et coffre chiffres ENSEMBLE : ils doivent rester coherents. Un ETag
|
// ETag, mot de passe et coffre chiffres ENSEMBLE : ils doivent rester
|
||||||
// qui survivrait a un coffre illisible ferait repondre 304 au serveur alors
|
// coherents. Un ETag qui survivrait a un coffre illisible ferait repondre
|
||||||
// que l'application n'a rien a afficher.
|
// 304 au serveur alors que l'application n'a rien a afficher.
|
||||||
LWrap := TJSONObject.Create;
|
LWrap := TJSONObject.Create;
|
||||||
try
|
try
|
||||||
LWrap.AddPair('etag', AETag);
|
LWrap.AddPair('etag', AETag);
|
||||||
@@ -346,33 +602,61 @@ begin
|
|||||||
LWrap.Free;
|
LWrap.Free;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Repli explicite : un appareil sans empreinte NI code ne peut pas porter
|
|
||||||
// une cle protegee par authentification, et sa creation echouerait.
|
|
||||||
LGotAuth := False;
|
LGotAuth := False;
|
||||||
LKey := nil;
|
LWrapped := nil;
|
||||||
if ARequireAuth then
|
if ARequireAuth then
|
||||||
try
|
try
|
||||||
LKey := EnsureKey(True);
|
EnsureWrapKey;
|
||||||
|
// 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;
|
LGotAuth := True;
|
||||||
except
|
except
|
||||||
LKey := nil;
|
LWrapped := nil; // appareil sans protection, ou refus du Keystore
|
||||||
end;
|
end;
|
||||||
if LKey = nil then
|
|
||||||
LKey := EnsureKey(False);
|
|
||||||
|
|
||||||
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
if LGotAuth then
|
||||||
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE, LKey);
|
begin
|
||||||
// L'IV est GENERE par le Keystore, jamais choisi par nous : le reutiliser
|
// AES logicielle sous la cle jetable : rien ici ne passe par le Keystore,
|
||||||
// avec la meme cle romprait GCM.
|
// donc rien n'exige d'authentification.
|
||||||
LIv := FromJava(LCipher.getIV);
|
LIv := RandomBytes(IV_LEN);
|
||||||
LCt := FromJava(LCipher.doFinal(ToJava(LPlain)));
|
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;
|
||||||
|
|
||||||
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);
|
TFile.WriteAllBytes(VaultPath, LOut);
|
||||||
end;
|
end;
|
||||||
{$ELSE}
|
{$ELSE}
|
||||||
@@ -384,9 +668,11 @@ end;
|
|||||||
function LoadVault(out APlainJson, AETag, ASyncPwd: string): Boolean;
|
function LoadVault(out APlainJson, AETag, ASyncPwd: string): Boolean;
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
var
|
var
|
||||||
LRaw, LIv, LCt, LPlain: TBytes;
|
LRaw, LIv, LCt, LPlain, LWrapped, LAesRaw: TBytes;
|
||||||
LCipher: JCipherX;
|
LCipher: JCipherX;
|
||||||
LWrap: TJSONObject;
|
LWrap: TJSONObject;
|
||||||
|
LStore: JKeyStoreX;
|
||||||
|
LLen, LOfs: Integer;
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
APlainJson := '';
|
APlainJson := '';
|
||||||
@@ -396,22 +682,52 @@ begin
|
|||||||
|
|
||||||
try
|
try
|
||||||
LRaw := TFile.ReadAllBytes(VaultPath);
|
LRaw := TFile.ReadAllBytes(VaultPath);
|
||||||
if (Length(LRaw) < 1 + IV_LEN + 16) then Exit;
|
if Length(LRaw) < 1 then Exit;
|
||||||
if not (LRaw[0] in [1, 2]) then Exit;
|
|
||||||
|
|
||||||
SetLength(LIv, IV_LEN);
|
if LRaw[0] = VER_WRAP then
|
||||||
Move(LRaw[1], LIv[0], IV_LEN);
|
begin
|
||||||
SetLength(LCt, Length(LRaw) - 1 - IV_LEN);
|
if Length(LRaw) < 3 then Exit;
|
||||||
Move(LRaw[1 + IV_LEN], LCt[0], Length(LCt));
|
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));
|
||||||
|
|
||||||
LCipher := TJCipherX.JavaClass.getInstance(StringToJString('AES/GCM/NoPadding'));
|
// C'EST ICI que l'authentification est exigee, et nulle part ailleurs.
|
||||||
// La cle est choisie d'apres l'en-tete. Si elle exige une
|
// Si elle n'a pas eu lieu recemment, l'init leve
|
||||||
// authentification et qu'aucune n'a eu lieu recemment, l'init leve
|
// UserNotAuthenticatedException - attrapee plus bas, sans effacer.
|
||||||
// UserNotAuthenticatedException - attrapee plus bas, et le cache est
|
LStore := OpenKeyStore;
|
||||||
// efface. L'appelant doit donc authentifier AVANT (voir VaultNeedsAuth).
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString(RSA_TRANSFORM));
|
||||||
LCipher.init(TJCipherX.JavaClass.DECRYPT_MODE, EnsureKey(LRaw[0] = 2),
|
LCipher.init(TJCipherX.JavaClass.DECRYPT_MODE,
|
||||||
TJGCMParameterSpecX.JavaClass.init(128, ToJava(LIv)));
|
LStore.getKey(StringToJString(KEY_ALIAS_WRAP), nil),
|
||||||
LPlain := FromJava(LCipher.doFinal(ToJava(LCt)));
|
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;
|
LWrap := TJSONObject.ParseJSONValue(TEncoding.UTF8.GetString(LPlain)) as TJSONObject;
|
||||||
if LWrap = nil then Exit;
|
if LWrap = nil then Exit;
|
||||||
@@ -428,9 +744,9 @@ begin
|
|||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
// NE PAS effacer sur un simple defaut d'authentification : le coffre est
|
// 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
|
// intact, c'est l'utilisateur qui n'a pas presente son schema ou qui a
|
||||||
// a laisse expirer le delai. L'effacer imposerait un retelechargement et
|
// laisse expirer le delai. L'effacer imposerait un retelechargement et
|
||||||
// 4 secondes de derivation pour une empreinte annulee.
|
// 4,5 secondes de derivation pour une empreinte annulee.
|
||||||
//
|
//
|
||||||
// On efface en revanche quand la cle est REELLEMENT perdue
|
// On efface en revanche quand la cle est REELLEMENT perdue
|
||||||
// (reinstallation, Keystore vide, nouvelle empreinte enrolee qui
|
// (reinstallation, Keystore vide, nouvelle empreinte enrolee qui
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
.\Android64\Debug\pmserverApk\classes\pmserverApk.classes
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-anydpi-v31\splash_vector.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-anydpi-v26\ic_launcher.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-hdpi\ic_launcher.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-night-anydpi-v21\splash_vector.xml
|
|
||||||
.\Android64\Debug\pmserverApk\AndroidManifest.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-mdpi\ic_launcher.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-xxhdpi\ic_launcher.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-xxxhdpi\ic_launcher.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\values-night-v21\colors.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-xhdpi\ic_notification.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-large\splash_image.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable\ic_launcher_monochrome.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-anydpi-v21\splash_image_def.xml
|
|
||||||
.\Android64\Debug\pmserverApk\library\lib\armeabi\libpmserverApk.so
|
|
||||||
.\Android64\Debug\pmserverApk\res\values-v31\styles.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\values\colors.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable\splash_image_def.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-hdpi\ic_notification.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-xxxhdpi\ic_notification.png
|
|
||||||
.\Android64\Debug\pmserverApk\library\lib\mips\libpmserverApk.so
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-xhdpi\ic_launcher.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable\splash_vector.xml
|
|
||||||
.\Android64\Debug\pmserverApk\library\lib\armeabi-v7a\libpmserverApk.so
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-mdpi\ic_notification.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-ldpi\ic_launcher.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-xlarge\splash_image.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-night-anydpi-v31\splash_vector.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\values\styles.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-small\splash_image.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-anydpi-v24\ic_notification.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\values\strings.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\values-v21\styles.xml
|
|
||||||
.\Android64\Debug\pmserverApk\library\lib\arm64-v8a\libpmserverApk.so
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-xxhdpi\ic_notification.png
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable\ic_launcher_foreground.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable\ic_launcher_background.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-anydpi-v33\ic_launcher.xml
|
|
||||||
.\Android64\Debug\pmserverApk\res\drawable-normal\splash_image.png
|
|
||||||
Reference in New Issue
Block a user