feat(android): an unlock code that survives a screen-lock change
The reported pain: changing the pattern destroys the local cache every time. That is Android doing exactly what it promises - a key created with setUserAuthenticationRequired is invalidated the moment the screen lock changes, so a new lock cannot open data sealed under the old one. No storage format changes that, which is why SQLite would not have helped: the problem is what the key is tied to, not what the file looks like. So the cache gains a third sealing format, VER_PIN, with two layers: inner - AES-GCM under PBKDF2(code, 100k) : it takes the code outer - the Keystore AES key WITHOUT the auth constraint : it takes the device The outer key is never invalidated, so the file survives any change to the screen lock; the code restores the protection that removing the constraint would otherwise cost. 100k iterations rather than 600k because a four to six digit code carries little entropy - more rounds mostly slow the honest user, and the real barrier is the outer layer requiring this phone. The code lives in a fourth settings field and is NEVER stored: empty at every launch, typed to open, and used to re-seal on every write. That is what separates it from the token, whose persistence is a convenience with no consequence since it only opens ciphertext. Built but NOT installed - the phone went off the network. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+14
-5
@@ -114,6 +114,14 @@ type
|
||||
/// Coordonnees du serveur. Delphi en est le detenteur ; la page ne fait
|
||||
/// que les editer et les lui renvoyer quand un champ est quitte.
|
||||
FUrl, FToken, FPwd: string;
|
||||
/// Code d'ouverture du cache local, choisi dans l'application. JAMAIS
|
||||
/// ENREGISTRE : il est vide a chaque lancement et doit etre retape. C'est
|
||||
/// ce qui le distingue du jeton, dont la persistance est un confort sans
|
||||
/// consequence puisqu'il n'ouvre que du chiffre.
|
||||
///
|
||||
/// Il remplace la protection par verrouillage d'ecran, qu'Android detruit
|
||||
/// des qu'on change le schema - par conception, et sans recours.
|
||||
FPin: string;
|
||||
|
||||
// --- l'interface entiere, rendue par le moteur du systeme
|
||||
FLayWeb: TLayout;
|
||||
@@ -639,7 +647,7 @@ begin
|
||||
// ouverture pour retrouver ce qu'on vient d'ecrire.
|
||||
FETag := LPut.ETag;
|
||||
try
|
||||
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable);
|
||||
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False, FPin);
|
||||
except
|
||||
on E: Exception do Log(' cache not written: ' + E.Message);
|
||||
end;
|
||||
@@ -869,6 +877,7 @@ begin
|
||||
FUrl := TNetEncoding.URL.Decode(Param('u')).Trim;
|
||||
FToken := TNetEncoding.URL.Decode(Param('t')).Trim;
|
||||
FPwd := TNetEncoding.URL.Decode(Param('p'));
|
||||
FPin := TNetEncoding.URL.Decode(Param('c'));
|
||||
// L'empreinte du mot de passe AUSSI : si une saisie destinee au jeton
|
||||
// atterrit dans le champ voisin - les deux sont masques - c'est celle-ci
|
||||
// qui bouge, et le jeton reste inchange sans que rien ne l'explique.
|
||||
@@ -981,7 +990,7 @@ begin
|
||||
// scelle, la mise en forme vers la page, ou le rafraichissement de fond qui
|
||||
// suit - sinon on cherche au hasard.
|
||||
LSw := TStopwatch.StartNew;
|
||||
if LoadVault(LPlain, LETag, LPwd) then
|
||||
if LoadVault(LPlain, LETag, LPwd, FPin) then
|
||||
begin
|
||||
Log('Vault opened from cache in ' + LSw.ElapsedMilliseconds.ToString + ' ms');
|
||||
FETag := LETag;
|
||||
@@ -1011,7 +1020,7 @@ begin
|
||||
// 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);
|
||||
SaveVault(LPlain, LETag, LPwd, True, True, FPin);
|
||||
if VaultNeedsAuth then
|
||||
Log('Cache re-sealed under authentication')
|
||||
else
|
||||
@@ -1194,7 +1203,7 @@ begin
|
||||
begin
|
||||
// Le coffre local d'abord : s'il est la, on connait deja l'ETag et on
|
||||
// evite les 4 secondes de derivation quand rien n'a change.
|
||||
if (FETag = '') and LoadVault(LCached, LCachedETag, LCachedPwd) then
|
||||
if (FETag = '') and LoadVault(LCached, LCachedETag, LCachedPwd, FPin) then
|
||||
FETag := LCachedETag;
|
||||
|
||||
LClient := TSyncClient.Create(AUrl, ATok);
|
||||
@@ -1341,7 +1350,7 @@ begin
|
||||
try
|
||||
// Le mot de passe part avec le coffre : c'est lui qui permettra au
|
||||
// prochain lancement de rafraichir sans rien demander.
|
||||
SaveVault(LPlain, FETag, APwd, LCanAuth);
|
||||
SaveVault(LPlain, FETag, APwd, LCanAuth, False, FPin);
|
||||
Log(' cache now holds sha: ' + FETag);
|
||||
if VaultNeedsAuth then
|
||||
Log(' cache written - protected by biometrics/PIN')
|
||||
|
||||
Reference in New Issue
Block a user