diff --git a/android-client/PMA.Store.pas b/android-client/PMA.Store.pas index 20154c5..f5c53b0 100644 --- a/android-client/PMA.Store.pas +++ b/android-client/PMA.Store.pas @@ -79,8 +79,12 @@ function HasVault: Boolean; /// 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); + 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. @@ -732,14 +736,14 @@ begin end; procedure SaveVault(const APlainJson, AETag, ASyncPwd: string; - ARequireAuth: Boolean); + ARequireAuth, AFreshKey: Boolean); {$IF DEFINED(ANDROID)} var LWrap: TJSONObject; LPlain, LIv, LCt, LOut, LAesRaw, LWrapped: TBytes; LCipher: JCipherX; LGotAuth: Boolean; - LOfs, LAttempt: Integer; + 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 @@ -758,28 +762,37 @@ begin LWrapped := nil; if ARequireAuth then begin - // Deux essais : le second apres avoir jete une cle devenue inutilisable. - for LAttempt := 0 to 1 do - try - EnsureWrapKey(LAttempt = 1); - // 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; - Break; - except - on E: Exception do - begin - LWrapped := nil; // appareil sans protection, ou cle morte - FLastStoreError := E.ClassName + ': ' + E.Message; - end; + // 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 @@ -905,11 +918,26 @@ begin on E: Exception do begin Result := False; - // ON N'EFFACE PAS. 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. + // 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; diff --git a/mainApk.pas b/mainApk.pas index 1fbb169..90370e9 100644 --- a/mainApk.pas +++ b/mainApk.pas @@ -119,7 +119,6 @@ type FLayDetail: TLayout; FDetBody: TVertScrollBox; FDetTitle, FDetToast: TLabel; - FBtnReveal: TRectangle; FRows: TArray; FRowCtls: TArray; FRowValues: TArray; @@ -843,7 +842,11 @@ begin if (not VaultNeedsAuth) and BiometricAvailable then begin 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 Log('Cache re-sealed under authentication') else