fix(android): the unlock code no longer changes by itself

It went from 0000 to 00000000 with nobody deciding it. The typed code and the
code the cache is sealed under were the same variable, so every write of the
cache re-sealed under whatever sat in the field - a sync, which asks for no
code at all, was enough to change it, silently.

They are two things now. Only opening the vault touches the seal, because
that is the one moment we know the current code is known: the decryption just
proved it. Everything else - sync, background refresh, saving an entry -
rewrites the cache under the code already in place.

Hence a second field, shown only once a code exists: the current one must open
the vault for the new one to take. Without it this fix would have locked the
device under the code set by accident, which is exactly the harm it is meant
to prevent.
This commit is contained in:
r-zakarya
2026-08-21 16:30:57 +01:00
parent 5107ebf7b2
commit 5522089939
3 changed files with 90 additions and 11 deletions
+18
View File
@@ -813,6 +813,24 @@ Deux corrections antérieures, posées par raisonnement sans instrument,
reposaient toutes deux sur une mesure qui vaut zéro ici. La leçon vaut d'être reposaient toutes deux sur une mesure qui vaut zéro ici. La leçon vaut d'être
écrite : sur cet écran, **instrumenter d'abord**. écrite : sur cet écran, **instrumenter d'abord**.
### Le code se changeait tout seul — corrigé 2026-08-21
Le code du cache est passé de `0000` à `00000000` sans que personne ne le
décide. Cause : **le code tapé et le code sous lequel le cache est scellé
étaient la même variable**, et toute écriture du cache rescellait donc sous ce
qui traînait dans le champ. Une synchronisation — qui ne demande pourtant aucun
code — suffisait à le changer, en silence.
Les deux sont séparés maintenant. Seule **l'ouverture du coffre** touche au
sceau, parce que c'est le seul moment où l'on sait que l'utilisateur connaît le
code courant : le déchiffrement vient de le prouver. Tout le reste — sync,
rafraîchissement de fond, enregistrement d'entrée — réécrit le cache sous le
code déjà en place.
D'où un second champ, *new unlock code*, visible seulement quand un code existe :
il faut ouvrir avec l'actuel pour que le nouveau prenne. Sans lui, la correction
aurait enfermé l'appareil sous le code posé par accident.
### Ce qui devient facultatif ### Ce qui devient facultatif
Sans dépôt configuré, *Sync* n'a rien à faire : les réglages du serveur Sans dépôt configuré, *Sync* n'a rien à faire : les réglages du serveur
+22 -2
View File
@@ -289,6 +289,18 @@
type="password" inputmode="numeric" autocomplete="off"> type="password" inputmode="numeric" autocomplete="off">
<div class="in-x" data-clear="pin">&#215;</div> <div class="in-x" data-clear="pin">&#215;</div>
</div> </div>
<!-- CHANGER LE CODE demande de connaitre l'actuel : le champ du dessus
doit ouvrir le coffre pour que celui-ci prenne effet. Sans cette
regle, toute ecriture du cache rescellait sous ce qui trainait dans
le champ - une synchronisation, qui ne demande pourtant aucun code,
suffisait a le changer sans qu'un mot le dise.
Cache tant qu'aucun code n'est pose : on n'en change pas un qui
n'existe pas, on le pose avec le champ du dessus. -->
<div class="in-wrap" id="pin2wrap" hidden>
<input class="field-in" id="pin2" placeholder="new unlock code"
type="password" inputmode="numeric" autocomplete="off">
<div class="in-x" data-clear="pin2">&#215;</div>
</div>
</div> </div>
<!-- UNE CROIX PAR CHAMP. Vider par la touche retour demande une quarantaine <!-- UNE CROIX PAR CHAMP. Vider par la touche retour demande une quarantaine
d'appuis sur un jeton, et laisse facilement un caractere - ce qui donne d'appuis sur un jeton, et laisse facilement un caractere - ce qui donne
@@ -423,9 +435,10 @@ function pushConfig() {
send('cfg?u=' + encodeURIComponent(elUrl.value.trim()) send('cfg?u=' + encodeURIComponent(elUrl.value.trim())
+ '&t=' + encodeURIComponent(elTok.value.trim()) + '&t=' + encodeURIComponent(elTok.value.trim())
+ '&p=' + encodeURIComponent(elPwd.value) + '&p=' + encodeURIComponent(elPwd.value)
+ '&c=' + encodeURIComponent(elPin.value)); + '&c=' + encodeURIComponent(elPin.value)
+ '&c2=' + encodeURIComponent(document.getElementById('pin2').value));
} }
[elUrl, elTok, elPwd, elPin].forEach(function (el) { [elUrl, elTok, elPwd, elPin, document.getElementById('pin2')].forEach(function (el) {
el.addEventListener('change', pushConfig); el.addEventListener('change', pushConfig);
el.addEventListener('blur', pushConfig); el.addEventListener('blur', pushConfig);
}); });
@@ -954,6 +967,13 @@ var PM = {
setPinNeeded: function (on) { setPinNeeded: function (on) {
document.getElementById('pin').placeholder = document.getElementById('pin').placeholder =
on ? 'unlock code' : 'set an unlock code (optional)'; on ? 'unlock code' : 'set an unlock code (optional)';
document.getElementById('pin2wrap').hidden = !on;
},
/// Le code vient d'etre change : le champ du nouveau n'a plus rien a dire,
/// et le laisser rempli le reproposerait a chaque ouverture.
pinChanged: function () {
document.getElementById('pin2').value = '';
document.getElementById('pin').value = '';
}, },
focusPin: function () { document.getElementById('pin').focus(); }, focusPin: function () { document.getElementById('pin').focus(); },
log: function (line) { log: function (line) {
+50 -9
View File
@@ -150,6 +150,20 @@ type
/// Il remplace la protection par verrouillage d'ecran, qu'Android detruit /// Il remplace la protection par verrouillage d'ecran, qu'Android detruit
/// des qu'on change le schema - par conception, et sans recours. /// des qu'on change le schema - par conception, et sans recours.
FPin: string; FPin: string;
/// LE CODE SOUS LEQUEL LE CACHE EST REELLEMENT SCELLE - pas celui qui est
/// tape a l'instant.
///
/// Les deux etaient confondus, et toute ecriture du cache rescellait donc
/// sous ce qui trainait dans le champ : une synchronisation, qui ne demande
/// pourtant aucun code, changeait le code. Une frappe de trop suffisait a
/// remplacer 0000 par 00000000, sans que personne ne l'ait decide et sans
/// qu'un mot le dise.
///
/// Seule l'ouverture du coffre y touche desormais - c'est le seul moment ou
/// l'on sait que l'utilisateur connait le code courant.
FPinSealed: string;
/// Le NOUVEAU code demande, quand on veut en changer. Vide sinon.
FPinNew: string;
// --- l'interface entiere, rendue par le moteur du systeme // --- l'interface entiere, rendue par le moteur du systeme
FLayWeb: TLayout; FLayWeb: TLayout;
@@ -733,8 +747,8 @@ begin
Log(Format('save: re-encrypt in %d ms', [LSw.ElapsedMilliseconds])); Log(Format('save: re-encrypt in %d ms', [LSw.ElapsedMilliseconds]));
SaveLocalContainer(LBody); SaveLocalContainer(LBody);
try try
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False, FPin, SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False,
FKey, FKeySalt, FKeyIters); FPinSealed, FKey, FKeySalt, FKeyIters);
except except
on E: Exception do Log(' cache not written: ' + E.Message); on E: Exception do Log(' cache not written: ' + E.Message);
end; end;
@@ -807,8 +821,8 @@ begin
// prochaine ouverture pour retrouver ce qu'on vient d'ecrire. // prochaine ouverture pour retrouver ce qu'on vient d'ecrire.
FETag := LPut.ETag; FETag := LPut.ETag;
try try
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False, FPin, SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False,
FKey, FKeySalt, FKeyIters); FPinSealed, FKey, FKeySalt, FKeyIters);
except except
on E: Exception do Log(' cache not written: ' + E.Message); on E: Exception do Log(' cache not written: ' + E.Message);
end; end;
@@ -1057,6 +1071,7 @@ begin
FToken := TNetEncoding.URL.Decode(Param('t')).Trim; FToken := TNetEncoding.URL.Decode(Param('t')).Trim;
FPwd := TNetEncoding.URL.Decode(Param('p')); FPwd := TNetEncoding.URL.Decode(Param('p'));
FPin := TNetEncoding.URL.Decode(Param('c')); FPin := TNetEncoding.URL.Decode(Param('c'));
FPinNew := TNetEncoding.URL.Decode(Param('c2'));
// L'empreinte du mot de passe AUSSI : si une saisie destinee au jeton // 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 // 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. // qui bouge, et le jeton reste inchange sans que rien ne l'explique.
@@ -1164,6 +1179,9 @@ begin
FKeySalt := nil; FKeySalt := nil;
FKeyIters := 0; FKeyIters := 0;
FPlain := ''; FPlain := '';
// Le code aussi : le retenir apres un verrouillage reviendrait a ne pas
// verrouiller. Le garde de SaveVault protege le cache d'ici la.
FPinSealed := '';
ShowUnlock; ShowUnlock;
end; end;
@@ -1235,6 +1253,7 @@ begin
// un coffre vide. // un coffre vide.
SaveVault(LPlain, '', APwd, BiometricAvailable, False, FPin, SaveVault(LPlain, '', APwd, BiometricAvailable, False, FPin,
FKey, FKeySalt, FKeyIters); FKey, FKeySalt, FKeyIters);
FPinSealed := FPin; // coffre neuf : ce code est desormais le sien
TThread.Queue(nil, TThread.Queue(nil,
procedure procedure
begin begin
@@ -1304,7 +1323,7 @@ begin
TThread.Queue(nil, procedure TThread.Queue(nil, procedure
begin Log('Vault belongs to ' + LOwner + ', not ' + AUser); end); begin Log('Vault belongs to ' + LOwner + ', not ' + AUser); end);
SaveVault(LPlain, FETag, APwd, BiometricAvailable, False, FPin, SaveVault(LPlain, FETag, APwd, BiometricAvailable, False, FPinSealed,
FKey, FKeySalt, FKeyIters); FKey, FKeySalt, FKeyIters);
TThread.Queue(nil, TThread.Queue(nil,
procedure procedure
@@ -1372,11 +1391,33 @@ begin
// synchroniser ne suffisait pas : une synchronisation qui repond 304 // synchroniser ne suffisait pas : une synchronisation qui repond 304
// n'ecrit rien, donc le cache restait dans son ancienne forme et le code // n'ecrit rien, donc le cache restait dans son ancienne forme et le code
// semblait sans effet. L'ouverture, elle, tient le clair a chaque fois. // semblait sans effet. L'ouverture, elle, tient le clair a chaque fois.
if (FPin <> '') and (not VaultNeedsPin) then // Le cache vient de s'ouvrir : le code tape est donc le bon, ou il n'y en
// avait pas. C'est la seule certitude de ce genre dans toute l'application.
if VaultNeedsPin then FPinSealed := FPin;
// CHANGER LE CODE - ici, et ici seulement. Il faut avoir ouvert le coffre,
// donc connaitre le code courant : sans cela une frappe distraite suffirait
// a rendre le cache inouvrable pour son proprietaire.
if (FPinNew <> '') and (FPinNew <> FPinSealed) then
begin
try
SaveVault(LPlain, LETag, LPwd, False, False, FPinNew,
FKey, FKeySalt, FKeyIters);
FPinSealed := FPinNew;
FPinNew := '';
Log('Unlock code changed');
WebCall('PM.toast("Unlock code changed")');
WebCall('PM.pinChanged()');
except
on E: Exception do Log('Unlock code not changed: ' + E.Message);
end;
end
else if (FPin <> '') and (not VaultNeedsPin) then
begin begin
try try
SaveVault(LPlain, LETag, LPwd, False, False, FPin, SaveVault(LPlain, LETag, LPwd, False, False, FPin,
FKey, FKeySalt, FKeyIters); FKey, FKeySalt, FKeyIters);
FPinSealed := FPin;
if VaultNeedsPin then if VaultNeedsPin then
Log('Cache re-sealed under the unlock code') Log('Cache re-sealed under the unlock code')
else else
@@ -1392,7 +1433,7 @@ begin
// protection a tres probablement invalide l'ancienne, et le // protection a tres probablement invalide l'ancienne, et le
// chiffrement ne saurait pas le voir - on scellerait un fichier // chiffrement ne saurait pas le voir - on scellerait un fichier
// impossible a rouvrir. // impossible a rouvrir.
SaveVault(LPlain, LETag, LPwd, True, True, FPin, SaveVault(LPlain, LETag, LPwd, True, True, FPinSealed,
FKey, FKeySalt, FKeyIters); FKey, FKeySalt, FKeyIters);
if VaultNeedsAuth then if VaultNeedsAuth then
Log('Cache re-sealed under authentication') Log('Cache re-sealed under authentication')
@@ -1744,8 +1785,8 @@ begin
try try
// Le mot de passe part avec le coffre : c'est lui qui permettra au // Le mot de passe part avec le coffre : c'est lui qui permettra au
// prochain lancement de rafraichir sans rien demander. // prochain lancement de rafraichir sans rien demander.
SaveVault(LPlain, FETag, APwd, LCanAuth, False, FPin, SaveVault(LPlain, FETag, APwd, LCanAuth, False, FPinSealed,
FKey, FKeySalt, FKeyIters); FKey, FKeySalt, FKeyIters);
Log(' cache now holds sha: ' + FETag); Log(' cache now holds sha: ' + FETag);
if VaultNeedsAuth then if VaultNeedsAuth then
Log(' cache written - protected by biometrics/PIN') Log(' cache written - protected by biometrics/PIN')