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:
r-zakarya
2026-08-21 12:10:51 +01:00
parent 3e020acf24
commit a41eeb8bfa
3 changed files with 170 additions and 17 deletions
+15 -3
View File
@@ -261,6 +261,14 @@
type="password" autocomplete="off">
<div class="in-x" data-clear="pwd">&#215;</div>
</div>
<!-- JAMAIS PRE-REMPLI, contrairement aux trois autres : ce code protege
le cache local, et le retenir reviendrait a ne rien proteger. Il se
retape a chaque lancement. -->
<div class="in-wrap">
<input class="field-in" id="pin" placeholder="unlock code (local cache)"
type="password" inputmode="numeric" autocomplete="off">
<div class="in-x" data-clear="pin">&#215;</div>
</div>
</div>
<pre id="log" hidden></pre>
</div>
@@ -345,7 +353,8 @@ var entries = [], folders = [], folder = '', query = '', view = 'unlock';
// --- deverrouillage --------------------------------------------------------
var elUrl = document.getElementById('url'),
elTok = document.getElementById('tok'),
elPwd = document.getElementById('pwd');
elPwd = document.getElementById('pwd'),
elPin = document.getElementById('pin');
// Les trois valeurs remontent A LA PERTE DU FOCUS, pas a chaque touche : c'est
// une navigation par champ modifie au lieu d'une par caractere. Delphi les
@@ -353,14 +362,16 @@ var elUrl = document.getElementById('url'),
function pushConfig() {
send('cfg?u=' + encodeURIComponent(elUrl.value.trim())
+ '&t=' + encodeURIComponent(elTok.value.trim())
+ '&p=' + encodeURIComponent(elPwd.value));
+ '&p=' + encodeURIComponent(elPwd.value)
+ '&c=' + encodeURIComponent(elPin.value));
}
[elUrl, elTok, elPwd].forEach(function (el) {
[elUrl, elTok, elPwd, elPin].forEach(function (el) {
el.addEventListener('change', pushConfig);
el.addEventListener('blur', pushConfig);
});
document.getElementById('btnOpen').addEventListener('click', function () {
pushConfig(); // le code vient peut-etre d'etre tape
send('openvault');
});
document.getElementById('btnSync').addEventListener('click', function () {
@@ -640,6 +651,7 @@ var PM = {
s.style.opacity = busy ? .4 : 1;
},
setConfig: function (url, tok, pwd, show) {
// elPin volontairement absent : rien ne le restaure, jamais.
elUrl.value = url; elTok.value = tok; elPwd.value = pwd;
// Replier des champs vides revient a cacher la seule chose a faire.
document.getElementById('cfg').hidden = !show;