feat(android): deux secrets separes, comme sur le poste

Le mot de passe de creation scellait le conteneur local ET pretendait
sceller l'instantane du depot. Un coffre cree ici publiait donc sous son
propre mot de passe, que le poste ne savait plus ouvrir - et le
rafraichissement echouait avec "wrong password" sur un depot pourtant
intact.

FVaultPwd ouvre le conteneur local et s'arrete la. Le secret du depot vit
dans les reglages, doit etre le meme sur tous les appareils, et c'est lui
seul qui ouvre et rescelle l'instantane.

Les deux ne se rencontrent qu'a UN endroit, la reconciliation : le
conteneur local s'ouvre sous le sien, puis la cle est jetee avant de
toucher au depot - la garder rescellerait le depot sous le mauvais
secret. Ailleurs ils ne se croisent jamais : un coffre ne ici ne publie
pas, un coffre publie n'a plus de conteneur local.

Migration : rien a rechiffrer. Les coffres existants ont simplement les
deux valeurs egales, et cessent d'etre forces de le rester.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-28 15:58:17 +01:00
parent 17cfc11084
commit ed7ccdd2d6
2 changed files with 90 additions and 38 deletions
+11 -4
View File
@@ -395,7 +395,11 @@
<div class="in-x" data-clear="user">&#215;</div>
</div>
<div class="in-wrap">
<input class="field-in" id="pass" placeholder="master password"
<!-- DEUX SECRETS, DEUX NOMS. Celui-ci ouvre le coffre garde sur cet
appareil. Il s'appelait "master password" et servait aussi de
secret du depot - un coffre cree ici pretendait alors sceller le
depot avec lui, que le poste ne savait plus ouvrir. -->
<input class="field-in" id="pass" placeholder="vault password"
type="password" autocomplete="off">
<div class="in-x" data-clear="pass">&#215;</div>
</div>
@@ -535,12 +539,15 @@
<div class="in-x" data-clear="sTok">&#215;</div>
</div>
<div class="in-wrap">
<input class="field-in" id="sPwd" placeholder="master password"
<input class="field-in" id="sPwd" placeholder="sync password"
type="password" autocomplete="off">
<div class="in-x" data-clear="sPwd">&#215;</div>
</div>
<div class="note">The master password never leaves this device. It is what
decrypts the vault; the token only opens the repository.</div>
<div class="note">The sync password never leaves this device. It is what
encrypts the copy kept in the repository, so it must be the same on every
device that shares it; the token only opens the repository. It is not the
password that opens a vault created on this phone - that one is asked for
on the home screen.</div>
<h2>Unlock code</h2>
<div class="in-wrap">
+79 -34
View File
@@ -134,6 +134,17 @@ type
// rafraichissement de fond silencieux : sans lui, chaque modification
// faite sur le poste ferait apparaitre une invite au lancement.
FSyncPwd: string;
/// LE MOT DE PASSE DU COFFRE LOCAL, et lui seul.
///
/// Il ouvre `vault-local.json` - un coffre ne sur ce telephone et pas
/// encore publie. Il n'a RIEN a voir avec le secret du depot, meme si
/// l'utilisateur choisit la meme valeur pour les deux : ce sont deux roles,
/// et les confondre faisait qu'un coffre cree ici pretendait sceller le
/// depot avec son propre mot de passe.
///
/// Vide des que le conteneur local disparait - c'est-a-dire des que la
/// reconciliation a passe l'autorite au depot.
FVaultPwd: string;
/// Coordonnees du serveur. Delphi en est le detenteur ; la page ne fait
/// que les editer et les lui renvoyer quand un champ est quitte.
@@ -895,9 +906,20 @@ begin
RefuseChange('Busy - wait for the current operation');
Exit;
end;
if FSyncPwd = '' then
// DEUX SECRETS, DEUX EXIGENCES. Un coffre ne ici n'ecrit que son conteneur
// local et n'a donc pas besoin du secret du depot ; un coffre publie n'a pas
// de conteneur local et n'a donc pas besoin du sien.
if HasLocalContainer then
begin
RefuseChange('Master password missing - open the vault first');
if FVaultPwd = '' then
begin
RefuseChange('Vault password missing - open the vault first');
Exit;
end;
end
else if FSyncPwd = '' then
begin
RefuseChange('Sync password missing - open the vault first');
Exit;
end;
SetBusy(True);
@@ -929,7 +951,7 @@ begin
// donc le cache avant toute chose, et le depot suit quand il peut.
// ---------------------------------------------------------------
LSw := TStopwatch.StartNew;
if LLocalOnly then LPlain := OpenContainer(LoadLocalContainer, FSyncPwd)
if LLocalOnly then LPlain := OpenContainer(LoadLocalContainer, FVaultPwd)
else LPlain := FPlain;
if LPlain = '' then
raise Exception.Create('no snapshot in hand - open the vault again');
@@ -1225,11 +1247,23 @@ begin
Status('No local vault to reconcile');
Exit;
end;
if FSyncPwd = '' then
// LES DEUX SONT REQUIS ICI, ET NULLE PART AILLEURS : c'est le seul moment ou
// le conteneur local et l'instantane du depot se rencontrent.
if FVaultPwd = '' then
begin
Status('Open the vault first');
Exit;
end;
// CE QUI EST DANS LES REGLAGES FAIT FOI quand le coffre n'en porte pas encore.
// Un coffre ne ici n'a jamais vu le depot : son cache ne contient donc aucun
// secret de sync, et c'est le champ des reglages qui le donne. Meme
// precedence que DoSync, pour que les deux ne divergent pas.
if FSyncPwd = '' then FSyncPwd := FPwd;
if FSyncPwd = '' then
begin
Status('Set the sync password in Settings');
Exit;
end;
SetBusy(True);
Status('Reconciling...');
@@ -1248,7 +1282,17 @@ begin
LErr := '';
try
try
LLocal := OpenContainer(LoadLocalContainer, FSyncPwd);
// 1. LE COFFRE LOCAL, sous SON mot de passe.
LLocal := OpenContainer(LoadLocalContainer, FVaultPwd);
// 2. ON JETTE LA CLE QUI VIENT DE SERVIR. Elle appartient au conteneur
// local ; tout ce qui suit concerne le depot, scelle par un autre
// secret. La garder ferait resceller le depot sous le mot de passe du
// coffre local - c'est exactement la confusion qu'on defait ici, et
// elle rendrait l'instantane illisible pour le poste.
FKey := nil;
FKeySalt := nil;
FKeyIters := 0;
for LAttempt := 1 to 3 do
begin
@@ -1268,6 +1312,8 @@ begin
end
else if LGet.Status = ssOK then
begin
// Ouvrir le distant pose la cle DU DEPOT dans FKey : c'est elle
// qui servira a le resceller quelques lignes plus bas.
LRemote := OpenContainer(LGet.Container, FSyncPwd);
LMerged := MergeSnapshots(LLocal, LRemote);
end
@@ -1277,7 +1323,11 @@ begin
Break;
end;
LBody := EncryptContainerWithKey(LMerged, FKey, FKeySalt, FKeyIters);
// Depot vide : aucune cle n'a ete tiree de lui, il faut donc la
// deriver du mot de passe de sync. C'est le seul chemin qui paie les
// quelques secondes, et il n'arrive qu'une fois.
if FKey = nil then LBody := EncryptContainer(LMerged, FSyncPwd)
else LBody := EncryptContainerWithKey(LMerged, FKey, FKeySalt, FKeyIters);
LPut := LClient.Put(LBody, LGet.ETag, 'reconciled from phone');
if LPut.Status = ssOK then
begin
@@ -1298,7 +1348,11 @@ begin
// PUBLIE, DONC PLUS BESOIN DU CONTENEUR LOCAL. C'est ce geste qui
// passe l'autorite au depot ; le faire avant la publication aurait
// perdu le coffre.
// Le conteneur local n'existe plus : son mot de passe non plus.
// L'autorite est passee au depot, et c'est le secret de sync qui
// commande desormais.
DropLocalContainer;
FVaultPwd := '';
try
SaveVault(LMerged, FETag, FSyncPwd, BiometricAvailable, False,
FPinSealed, FKey, FKeySalt, FKeyIters);
@@ -1747,6 +1801,9 @@ begin
FKeyIters := 0;
FPlain := '';
FHistory := nil;
// Le mot de passe du coffre local part avec le clair : le garder ouvrirait
// le conteneur sans rien demander.
FVaultPwd := '';
// Le code aussi : le retenir apres un verrouillage reviendrait a ne pas
// verrouiller. Le garde de SaveVault protege le cache d'ici la.
FPinSealed := '';
@@ -1827,19 +1884,12 @@ begin
begin
SetBusy(False);
FUser := AUser;
FSyncPwd := APwd;
// LE COFFRE QUI VIENT DE S'OUVRIR DIT LE MOT DE PASSE DE SYNC.
//
// Sans cette ligne, FPwd gardait la valeur de premier lancement -
// en debogage, une constante ; en release, du vide - et les
// reglages affichaient donc un mot de passe qui n'etait pas celui
// du coffre detenu. Pire, DoSync la PREFERE a celle du coffre au
// nom de "ce qui est tape l'emporte" : or rien n'avait ete tape.
// Les deux chemins de synchronisation travaillaient ainsi avec
// deux mots de passe differents sur le meme coffre.
FPwd := APwd;
WebCall(Format('PM.setConfig(%s, %s, %s, false)',
[JsArg(FUrl), JsArg(FToken), JsArg(FPwd)]));
// CE MOT DE PASSE NE TOUCHE PAS AU DEPOT. Il scelle le conteneur
// local, rien d'autre. Le secret du depot vit dans les reglages et
// doit etre le meme sur tous les appareils - le remplacer par
// celui-ci ferait publier un instantane que le poste ne saurait
// plus ouvrir.
FVaultPwd := APwd;
Log('Vault created locally for ' + AUser);
AdoptVault(LPlain);
end);
@@ -1887,6 +1937,7 @@ begin
LDoc: TJSONObject;
begin
try
FVaultPwd := APwd; // avant l'ouverture : c'est lui qui la fait
LPlain := OpenContainer(LoadLocalContainer, APwd);
LOwner := '';
LDoc := TJSONObject.ParseJSONValue(LPlain) as TJSONObject;
@@ -1910,19 +1961,9 @@ begin
begin
SetBusy(False);
if LOwner <> '' then FUser := LOwner else FUser := AUser;
FSyncPwd := APwd;
// LE COFFRE QUI VIENT DE S'OUVRIR DIT LE MOT DE PASSE DE SYNC.
//
// Sans cette ligne, FPwd gardait la valeur de premier lancement -
// en debogage, une constante ; en release, du vide - et les
// reglages affichaient donc un mot de passe qui n'etait pas celui
// du coffre detenu. Pire, DoSync la PREFERE a celle du coffre au
// nom de "ce qui est tape l'emporte" : or rien n'avait ete tape.
// Les deux chemins de synchronisation travaillaient ainsi avec
// deux mots de passe differents sur le meme coffre.
FPwd := APwd;
WebCall(Format('PM.setConfig(%s, %s, %s, false)',
[JsArg(FUrl), JsArg(FToken), JsArg(FPwd)]));
// Meme regle qu'a la creation : ce mot de passe ouvre le conteneur
// local et s'arrete la.
FVaultPwd := APwd;
AdoptVault(LPlain);
end);
@@ -2815,7 +2856,9 @@ begin
ClearUnlockCode;
Exit;
end;
if (FPlain = '') or (FSyncPwd = '') then
// Le secret du depot n'entre pas ici : le code scelle le CACHE, et un coffre
// ne sur ce telephone n'a pas encore de depot.
if FPlain = '' then
begin
WebCall('PM.toast("Open the vault first")');
Exit;
@@ -2855,7 +2898,9 @@ begin
WebCall('PM.toast("No unlock code to remove")');
Exit;
end;
if (FPlain = '') or (FSyncPwd = '') then
// Le secret du depot n'entre pas ici : le code scelle le CACHE, et un coffre
// ne sur ce telephone n'a pas encore de depot.
if FPlain = '' then
begin
WebCall('PM.toast("Open the vault first")');
Exit;