fix(android): protection catches up with the cache, and a dead key gets replaced
After the screen lock was removed and put back, the app stopped asking for the pattern. Two gaps, both mine. The cache had been rewritten while the device had no lock, so under the plain key - and nothing ever moves it back. Protection was decided at write time only, and a write happens only when a new snapshot arrives; a 304 left the file in the clear indefinitely. It is now re-sealed on open, where the plaintext is already in hand. And the invalidated RSA key was never replaced. Removing a lock, or enrolling a new fingerprint, permanently invalidates a protected key while its alias survives. EnsureWrapKey only created the pair when the alias was absent, so it kept reusing a dead key: every encryption failed and every write fell back to the plain key, forever. It now deletes and regenerates on a second attempt. Measured on device: header 1 before, 3 after the first launch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -408,6 +408,7 @@ type
|
|||||||
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 getCertificate(alias: JString): JCertificateX; cdecl;
|
||||||
function containsAlias(alias: JString): Boolean; cdecl;
|
function containsAlias(alias: JString): Boolean; cdecl;
|
||||||
|
procedure deleteEntry(alias: JString); cdecl;
|
||||||
end;
|
end;
|
||||||
TJKeyStoreX = class(TJavaGenericImport<JKeyStoreXClass, JKeyStoreX>) end;
|
TJKeyStoreX = class(TJavaGenericImport<JKeyStoreXClass, JKeyStoreX>) end;
|
||||||
|
|
||||||
@@ -552,7 +553,7 @@ end;
|
|||||||
/// Cree la paire RSA si elle manque. Leve si l'appareil n'a NI empreinte NI
|
/// 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
|
/// code : une cle privee protegee par authentification n'aurait alors rien
|
||||||
/// pour la deverrouiller. L'appelant retombe sur la cle AES.
|
/// pour la deverrouiller. L'appelant retombe sur la cle AES.
|
||||||
procedure EnsureWrapKey;
|
procedure EnsureWrapKey(AForceNew: Boolean = False);
|
||||||
var
|
var
|
||||||
LStore: JKeyStoreX;
|
LStore: JKeyStoreX;
|
||||||
LGen: JKeyPairGeneratorX;
|
LGen: JKeyPairGeneratorX;
|
||||||
@@ -560,7 +561,15 @@ var
|
|||||||
LPaddings, LDigests: TJavaObjectArray<JString>;
|
LPaddings, LDigests: TJavaObjectArray<JString>;
|
||||||
begin
|
begin
|
||||||
LStore := OpenKeyStore;
|
LStore := OpenKeyStore;
|
||||||
if LStore.containsAlias(StringToJString(KEY_ALIAS_WRAP)) then Exit;
|
// 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 := TJavaObjectArray<JString>.Create(1);
|
||||||
LPaddings.Items[0] := StringToJString('OAEPPadding');
|
LPaddings.Items[0] := StringToJString('OAEPPadding');
|
||||||
@@ -730,7 +739,7 @@ var
|
|||||||
LPlain, LIv, LCt, LOut, LAesRaw, LWrapped: TBytes;
|
LPlain, LIv, LCt, LOut, LAesRaw, LWrapped: TBytes;
|
||||||
LCipher: JCipherX;
|
LCipher: JCipherX;
|
||||||
LGotAuth: Boolean;
|
LGotAuth: Boolean;
|
||||||
LOfs: Integer;
|
LOfs, LAttempt: Integer;
|
||||||
begin
|
begin
|
||||||
// ETag, mot de passe et coffre chiffres ENSEMBLE : ils doivent rester
|
// ETag, mot de passe et coffre chiffres ENSEMBLE : ils doivent rester
|
||||||
// coherents. Un ETag qui survivrait a un coffre illisible ferait repondre
|
// coherents. Un ETag qui survivrait a un coffre illisible ferait repondre
|
||||||
@@ -748,21 +757,30 @@ begin
|
|||||||
LGotAuth := False;
|
LGotAuth := False;
|
||||||
LWrapped := nil;
|
LWrapped := nil;
|
||||||
if ARequireAuth then
|
if ARequireAuth then
|
||||||
try
|
begin
|
||||||
EnsureWrapKey;
|
// Deux essais : le second apres avoir jete une cle devenue inutilisable.
|
||||||
// Cle AES jetable, tiree du generateur cryptographique de la plateforme
|
for LAttempt := 0 to 1 do
|
||||||
// - jamais de Random ici.
|
try
|
||||||
LAesRaw := RandomBytes(AES_LEN);
|
EnsureWrapKey(LAttempt = 1);
|
||||||
LCipher := TJCipherX.JavaClass.getInstance(StringToJString(RSA_TRANSFORM));
|
// Cle AES jetable, tiree du generateur cryptographique de la
|
||||||
// AUCUNE authentification requise : la cle publique reconstruite ne
|
// plateforme - jamais de Random ici.
|
||||||
// porte plus les contraintes de la privee. C'est tout l'interet.
|
LAesRaw := RandomBytes(AES_LEN);
|
||||||
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE, UnrestrictedPublicKey,
|
LCipher := TJCipherX.JavaClass.getInstance(StringToJString(RSA_TRANSFORM));
|
||||||
OaepParams);
|
// AUCUNE authentification requise : la cle publique reconstruite ne
|
||||||
LWrapped := FromJava(LCipher.doFinal(ToJava(LAesRaw)));
|
// porte plus les contraintes de la privee. C'est tout l'interet.
|
||||||
LGotAuth := True;
|
LCipher.init(TJCipherX.JavaClass.ENCRYPT_MODE, UnrestrictedPublicKey,
|
||||||
except
|
OaepParams);
|
||||||
LWrapped := nil; // appareil sans protection, ou refus du Keystore
|
LWrapped := FromJava(LCipher.doFinal(ToJava(LAesRaw)));
|
||||||
end;
|
LGotAuth := True;
|
||||||
|
Break;
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
LWrapped := nil; // appareil sans protection, ou cle morte
|
||||||
|
FLastStoreError := E.ClassName + ': ' + E.Message;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
if LGotAuth then
|
if LGotAuth then
|
||||||
begin
|
begin
|
||||||
|
|||||||
+19
@@ -834,6 +834,25 @@ begin
|
|||||||
begin
|
begin
|
||||||
FETag := LETag;
|
FETag := LETag;
|
||||||
if LPwd <> '' then FSyncPwd := LPwd;
|
if LPwd <> '' then FSyncPwd := LPwd;
|
||||||
|
|
||||||
|
// LA PROTECTION RATTRAPE LE CACHE. Elle se decidait a l'ecriture seule,
|
||||||
|
// et une ecriture n'arrive qu'avec un instantane nouveau : un cache pose
|
||||||
|
// pendant que l'appareil n'avait pas de verrouillage restait en clair
|
||||||
|
// indefiniment, meme apres en avoir remis un. On le rescelle ici, ou l'on
|
||||||
|
// tient justement le clair.
|
||||||
|
if (not VaultNeedsAuth) and BiometricAvailable then
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
SaveVault(LPlain, LETag, LPwd, True);
|
||||||
|
if VaultNeedsAuth then
|
||||||
|
Log('Cache re-sealed under authentication')
|
||||||
|
else
|
||||||
|
Log('Re-seal refused: ' + LastStoreError);
|
||||||
|
except
|
||||||
|
on E: Exception do Log('Re-seal failed: ' + E.Message);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
try
|
try
|
||||||
AdoptVault(LPlain);
|
AdoptVault(LPlain);
|
||||||
// Afficher d'abord, synchroniser ensuite : le coffre est a l'ecran, le
|
// Afficher d'abord, synchroniser ensuite : le coffre est a l'ecran, le
|
||||||
|
|||||||
Reference in New Issue
Block a user