fix(android): encryption cannot see a dead key, so stop pretending it can
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>
This commit is contained in:
@@ -79,8 +79,12 @@ function HasVault: Boolean;
|
|||||||
/// ni empreinte ni code, on retombe sur une cle AES sans contrainte : mieux
|
/// 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
|
/// vaut un cache lie au materiel que pas de cache du tout. VaultNeedsAuth dira
|
||||||
/// ensuite ce qui a reellement ete fait.
|
/// 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;
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
||||||
ARequireAuth: Boolean = False);
|
ARequireAuth: Boolean = False; AFreshKey: Boolean = False);
|
||||||
|
|
||||||
/// Le coffre local exige-t-il une authentification pour etre lu ? Lu dans
|
/// Le coffre local exige-t-il une authentification pour etre lu ? Lu dans
|
||||||
/// l'en-tete du fichier, sans dechiffrer quoi que ce soit.
|
/// l'en-tete du fichier, sans dechiffrer quoi que ce soit.
|
||||||
@@ -732,14 +736,14 @@ begin
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
procedure SaveVault(const APlainJson, AETag, ASyncPwd: string;
|
||||||
ARequireAuth: Boolean);
|
ARequireAuth, AFreshKey: Boolean);
|
||||||
{$IF DEFINED(ANDROID)}
|
{$IF DEFINED(ANDROID)}
|
||||||
var
|
var
|
||||||
LWrap: TJSONObject;
|
LWrap: TJSONObject;
|
||||||
LPlain, LIv, LCt, LOut, LAesRaw, LWrapped: TBytes;
|
LPlain, LIv, LCt, LOut, LAesRaw, LWrapped: TBytes;
|
||||||
LCipher: JCipherX;
|
LCipher: JCipherX;
|
||||||
LGotAuth: Boolean;
|
LGotAuth: Boolean;
|
||||||
LOfs, LAttempt: Integer;
|
LOfs: 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
|
||||||
@@ -758,10 +762,20 @@ begin
|
|||||||
LWrapped := nil;
|
LWrapped := nil;
|
||||||
if ARequireAuth then
|
if ARequireAuth then
|
||||||
begin
|
begin
|
||||||
// Deux essais : le second apres avoir jete une cle devenue inutilisable.
|
// UN SEUL ESSAI, et AFreshKey decide de tout.
|
||||||
for LAttempt := 0 to 1 do
|
//
|
||||||
|
// 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
|
try
|
||||||
EnsureWrapKey(LAttempt = 1);
|
EnsureWrapKey(AFreshKey);
|
||||||
// Cle AES jetable, tiree du generateur cryptographique de la
|
// Cle AES jetable, tiree du generateur cryptographique de la
|
||||||
// plateforme - jamais de Random ici.
|
// plateforme - jamais de Random ici.
|
||||||
LAesRaw := RandomBytes(AES_LEN);
|
LAesRaw := RandomBytes(AES_LEN);
|
||||||
@@ -772,11 +786,10 @@ begin
|
|||||||
OaepParams);
|
OaepParams);
|
||||||
LWrapped := FromJava(LCipher.doFinal(ToJava(LAesRaw)));
|
LWrapped := FromJava(LCipher.doFinal(ToJava(LAesRaw)));
|
||||||
LGotAuth := True;
|
LGotAuth := True;
|
||||||
Break;
|
|
||||||
except
|
except
|
||||||
on E: Exception do
|
on E: Exception do
|
||||||
begin
|
begin
|
||||||
LWrapped := nil; // appareil sans protection, ou cle morte
|
LWrapped := nil; // appareil sans protection d'ecran configuree
|
||||||
FLastStoreError := E.ClassName + ': ' + E.Message;
|
FLastStoreError := E.ClassName + ': ' + E.Message;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -905,11 +918,26 @@ begin
|
|||||||
on E: Exception do
|
on E: Exception do
|
||||||
begin
|
begin
|
||||||
Result := False;
|
Result := False;
|
||||||
// ON N'EFFACE PAS. Le tri « cle perdue / pas authentifie » se faisait
|
// ON N'EFFACE PAS LE COFFRE. Le tri « cle perdue / pas authentifie » se
|
||||||
// ici sur le texte du message, et une erreur de tri detruisait le cache
|
// faisait ici sur le texte du message, et une erreur de tri detruisait
|
||||||
// en emportant la seule trace de sa cause. L'appelant a le journal et le
|
// le cache en emportant la seule trace de sa cause. L'appelant a le
|
||||||
// contexte - lui seul sait si l'utilisateur a demande quelque chose.
|
// journal et le contexte - lui seul sait si l'utilisateur a demande
|
||||||
|
// quelque chose.
|
||||||
FLastStoreError := E.ClassName + ': ' + E.Message;
|
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;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|||||||
+5
-2
@@ -119,7 +119,6 @@ type
|
|||||||
FLayDetail: TLayout;
|
FLayDetail: TLayout;
|
||||||
FDetBody: TVertScrollBox;
|
FDetBody: TVertScrollBox;
|
||||||
FDetTitle, FDetToast: TLabel;
|
FDetTitle, FDetToast: TLabel;
|
||||||
FBtnReveal: TRectangle;
|
|
||||||
FRows: TArray<TDetRow>;
|
FRows: TArray<TDetRow>;
|
||||||
FRowCtls: TArray<TLayout>;
|
FRowCtls: TArray<TLayout>;
|
||||||
FRowValues: TArray<TLabel>;
|
FRowValues: TArray<TLabel>;
|
||||||
@@ -843,7 +842,11 @@ begin
|
|||||||
if (not VaultNeedsAuth) and BiometricAvailable then
|
if (not VaultNeedsAuth) and BiometricAvailable then
|
||||||
begin
|
begin
|
||||||
try
|
try
|
||||||
SaveVault(LPlain, LETag, LPwd, True);
|
// Cle NEUVE, obligatoirement : le changement qui vient de ramener la
|
||||||
|
// protection a tres probablement invalide l'ancienne, et le
|
||||||
|
// chiffrement ne saurait pas le voir - on scellerait un fichier
|
||||||
|
// impossible a rouvrir.
|
||||||
|
SaveVault(LPlain, LETag, LPwd, True, True);
|
||||||
if VaultNeedsAuth then
|
if VaultNeedsAuth then
|
||||||
Log('Cache re-sealed under authentication')
|
Log('Cache re-sealed under authentication')
|
||||||
else
|
else
|
||||||
|
|||||||
Reference in New Issue
Block a user