diff --git a/android-client/PMA.Store.pas b/android-client/PMA.Store.pas
index b67b716..4233aab 100644
--- a/android-client/PMA.Store.pas
+++ b/android-client/PMA.Store.pas
@@ -797,6 +797,22 @@ begin
// ETag, mot de passe et coffre chiffres ENSEMBLE : ils doivent rester
// coherents. Un ETag qui survivrait a un coffre illisible ferait repondre
// 304 au serveur alors que l'application n'a rien a afficher.
+ // UN CACHE SCELLE SOUS UN CODE NE SE REECRIT JAMAIS SANS LUI. Sans cette
+ // regle, le premier rafraichissement venu le reecrivait dans l'ancienne
+ // forme - le code etait pose, puis defait en silence quelques secondes plus
+ // tard, sans que rien ne le dise. Le refuser fait perdre un instantane, ce
+ // que la prochaine ouverture rattrape ; l'accepter fait perdre la
+ // protection, ce que rien ne rattrape.
+ //
+ // Retirer le code demanderait donc un geste explicite, qui n'existe pas
+ // encore : aujourd'hui on le remplace en le retapant.
+ if (APin = '') and VaultNeedsPin then
+ begin
+ FLastStoreError := 'cache sealed under an unlock code; refusing to ' +
+ 'rewrite it without one';
+ Exit;
+ end;
+
LWrap := TJSONObject.Create;
try
LWrap.AddPair('etag', AETag);
diff --git a/android-client/web/app.html b/android-client/web/app.html
index 1dfdc2d..bf0af06 100644
--- a/android-client/web/app.html
+++ b/android-client/web/app.html
@@ -187,6 +187,16 @@
font: inherit; outline: none;
}
.field-in::placeholder { color: var(--muted); }
+ /* La fleche dit si le panneau est deplie, et de quel bouton il descend. */
+ .chev { display: inline-block; margin-left: 6px; font-size: 11px;
+ opacity: .7; transition: transform .18s ease; }
+ .btn.open .chev { transform: rotate(180deg); }
+ /* Un panneau est visiblement ATTACHE au bouton qui l'ouvre : sans ce trait
+ et ce retrait, les reglages et le journal se ressemblaient trop pour
+ qu'on sache lequel etait deplie. */
+ .panel { margin-top: 8px; padding-left: 10px;
+ border-left: 2px solid rgba(255,255,255,.18); }
+
.in-wrap { position: relative; }
/* Le champ laisse la place a la croix, sinon le texte passe dessous. */
.in-wrap .field-in { padding-right: 44px; }
@@ -221,8 +231,24 @@
-
-
+
+
+
+
+
+
+
+
+
+
×
-
+
×
-
-
-
-
×
-
-
+
@@ -375,8 +393,11 @@ document.getElementById('btnSync').addEventListener('click', function () {
pushConfig(); // ce qui est tape mais pas encore quitte compte aussi
send('sync');
});
-// Un seul ecouteur pour les trois croix : elles ne different que par leur cible.
-document.getElementById('cfg').addEventListener('click', function (ev) {
+// Un seul ecouteur pour les quatre croix : elles ne different que par leur
+// cible. Pose sur l'ECRAN, pas sur le groupe des reglages - le code a son
+// propre bloc depuis qu'il n'appartient plus au serveur, et sa croix serait
+// restee sans effet.
+document.getElementById('unlock').addEventListener('click', function (ev) {
var id = ev.target.getAttribute('data-clear');
if (!id) return;
var el = document.getElementById(id);
@@ -387,13 +408,21 @@ document.getElementById('cfg').addEventListener('click', function (ev) {
pushConfig();
});
+// UN SEUL PANNEAU DEPLIE A LA FOIS. Les deux s'ouvraient l'un sous l'autre,
+// dans la meme colonne et avec le meme fond : le journal se lisait comme la
+// suite des reglages.
+function togglePanel(name) {
+ ['cfg', 'log'].forEach(function (n) {
+ var open = (n === name) && document.getElementById(n).hidden;
+ document.getElementById(n).hidden = !open;
+ document.querySelector('[data-panel="' + n + '"]').classList.toggle('open', open);
+ });
+}
document.getElementById('btnCfg').addEventListener('click', function () {
- var c = document.getElementById('cfg');
- c.hidden = !c.hidden;
+ togglePanel('cfg');
});
document.getElementById('btnLog').addEventListener('click', function () {
- var l = document.getElementById('log');
- l.hidden = !l.hidden;
+ togglePanel('log');
});
function esc(s) {
@@ -682,8 +711,20 @@ var PM = {
// 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;
+ if (show !== !document.getElementById('cfg').hidden) togglePanel('cfg');
},
+
+ /// Le coffre local est scelle sous un code : il faut le retaper pour
+ /// l'ouvrir. Delphi le sait en lisant le premier octet du cache.
+ /// TOUJOURS VISIBLE, seule l'indication change. Le cacher quand aucun code
+ /// n'est pose supprimerait le seul endroit ou en poser un ; le montrer sans
+ /// rien dire laisserait croire qu'il en faut un pour ouvrir. C'est donc le
+ /// texte d'aide qui porte la difference.
+ setPinNeeded: function (on) {
+ document.getElementById('pin').placeholder =
+ on ? 'unlock code' : 'set an unlock code (optional)';
+ },
+ focusPin: function () { document.getElementById('pin').focus(); },
log: function (line) {
var l = document.getElementById('log');
l.textContent += line + '\n';
diff --git a/mainApk.pas b/mainApk.pas
index 37259be..8a2c569 100644
--- a/mainApk.pas
+++ b/mainApk.pas
@@ -334,10 +334,19 @@ begin
// Le bouton d'ouverture n'a de sens que si un coffre local existe ET qu'il
// s'est deja laisse ouvrir ; sinon seul le telechargement peut en produire
// un utilisable.
+ // LE CODE SE DEMANDE ICI, SUR L'ECRAN D'ACCUEIL. Il etait range dans les
+ // reglages du serveur, ou rien n'indiquait qu'il fallait le retaper : le
+ // coffre scelle sous un code s'ouvrait donc sans code, echouait, et l'echec
+ // etait pris pour une corruption - l'appareil proposait de retelecharger un
+ // coffre qu'il avait parfaitement en main.
+ WebCall(Format('PM.setPinNeeded(%s)', [LowerCase(BoolToStr(VaultNeedsPin, True))]));
+
LCanOpen := HasVault and not FVaultUnreadable;
if LCanOpen then
begin
- if VaultNeedsAuth then
+ if VaultNeedsPin then
+ Status('Vault on this device - unlock code required')
+ else if VaultNeedsAuth then
Status('Vault on this device - protected')
else
Status('Vault on this device');
@@ -1025,8 +1034,18 @@ begin
begin
// ShowUnlock AVANT le message : il repositionne le statut selon ce que
// l'appareil detient, et ecraserait donc l'explication.
+ // Un code faux se retape ; le cache, lui, n'a rien perdu. Le marquer
+ // illisible retirerait le bouton d'ouverture pour le reste de la session.
+ if VaultNeedsPin then
+ begin
+ Log('Vault did not open under this unlock code');
+ ShowUnlock;
+ Status('Wrong unlock code');
+ WebCall('PM.focusPin()');
+ Exit;
+ end;
FVaultUnreadable := True;
- ShowUnlock;
+ ShowUnlock;
Log('Local vault unreadable: ' + LastStoreError);
Status('Local vault unreadable - see log');
end;
@@ -1040,6 +1059,16 @@ end;
procedure TForm8.OpenVault(AAuto: Boolean);
begin
if FBusy then Exit;
+ // NE PAS TENTER CE QUI NE PEUT QUE RATER. Un code absent n'est pas un coffre
+ // abime : l'essayer quand meme marquait le cache illisible et transformait le
+ // bouton en "Download vault", ce qui donnait a croire que le code avait
+ // detruit le cache alors qu'il le protegeait.
+ if VaultNeedsPin and (FPin = '') then
+ begin
+ Status('Unlock code required');
+ WebCall('PM.focusPin()');
+ Exit;
+ end;
// L'authentification affiche un dialogue systeme : elle doit rester sur le
// thread principal, et PRECEDER toute lecture du coffre.
if not VaultNeedsAuth then