feat(android): entries can be created on the phone
A vault you cannot add to is not a vault, so an account created here was useless without this. A plus button next to the padlock, a form, and a kind - login or note, which the vault distinguishes and whose body is the password field either way. One save path now, two transports, and the rule that decides between them is: if a local container exists it is the authority. A vault born here has no repository; a vault pulled from one has no local container. The case where both exist - a local vault that later gains a repository - needs a merge, and that is section 7 step 4. The patch arrives as a function rather than an already-patched snapshot: the remote path must modify WHAT IT HAS JUST PULLED, or it would overwrite whatever another device published in between. That constraint is why the two transports could not simply share a finished payload. Two traps closed while passing: the form left the typed password sitting in the document after the save, and a refusal left its Save button dead with the text behind it. Verified on the device that the button and the form appear and read correctly. Saving was not exercised - the vault on this phone syncs to a real repository, and a test entry would have been published to it with no way to delete it from here yet.
This commit is contained in:
+150
-71
@@ -204,7 +204,12 @@ type
|
||||
procedure PatchSelfCheck;
|
||||
{$ENDIF}
|
||||
procedure OpenEntry(AVaultIndex: Integer);
|
||||
procedure CommitChange(const ALabel, AComment, AReopenUuid: string;
|
||||
const APatch: TFunc<string, string>);
|
||||
procedure SaveEntryPassword(const ANewValue: string);
|
||||
/// Ajoute une entree au coffre. Le mot de passe voyage par le pont,
|
||||
/// comme 'setpwd' - meme regle, meme journalisation muette.
|
||||
procedure CreateEntry(const ATitle, ASite, AUser, APwd, AKind: string);
|
||||
procedure ReopenEntryByUuid(const AUuid: string);
|
||||
procedure OnClipTimer(Sender: TObject);
|
||||
procedure OnDeadline(Sender: TObject);
|
||||
@@ -601,9 +606,20 @@ end;
|
||||
/// tout - y compris le dechiffrement, parce que le contenu a change. Trois
|
||||
/// essais : au-dela, c'est que l'autre appareil publie en boucle, et insister
|
||||
/// n'y changerait rien.
|
||||
procedure TForm8.SaveEntryPassword(const ANewValue: string);
|
||||
var
|
||||
LUuid, LTitle: string;
|
||||
/// ENREGISTRER UNE MODIFICATION - un seul chemin, deux transports.
|
||||
///
|
||||
/// La regle qui tranche : SI UN CONTENEUR LOCAL EXISTE, C'EST LUI L'AUTORITE.
|
||||
/// Un coffre ne ici n'a pas de depot ; un coffre tire d'un depot n'a pas de
|
||||
/// conteneur local. Le cas ou les deux coexistent - un coffre local auquel on
|
||||
/// ajoute ensuite un depot - demande une fusion, et c'est l'etape 4 de la §7.
|
||||
///
|
||||
/// APatch recoit le clair et rend le clair modifie. Il ne peut pas etre
|
||||
/// applique d'avance : le chemin distant doit modifier CE QU'IL VIENT DE
|
||||
/// TIRER, sinon il ecraserait ce qu'un autre appareil a publie entre-temps.
|
||||
///
|
||||
/// AReopenUuid vide = revenir a la liste plutot qu'a une entree.
|
||||
procedure TForm8.CommitChange(const ALabel, AComment, AReopenUuid: string;
|
||||
const APatch: TFunc<string, string>);
|
||||
begin
|
||||
// CHAQUE REFUS REND LA MAIN A LA PAGE. Ces gardes se contentaient d'un
|
||||
// message : l'editeur restait ouvert avec ses boutons desactives, donc
|
||||
@@ -614,20 +630,12 @@ begin
|
||||
WebCall('PM.editDone()');
|
||||
Exit;
|
||||
end;
|
||||
if FCurrent.Uuid = '' then
|
||||
begin
|
||||
WebCall('PM.toast("This entry has no uuid - cannot be updated")');
|
||||
WebCall('PM.editDone()');
|
||||
Exit;
|
||||
end;
|
||||
if FSyncPwd = '' then
|
||||
begin
|
||||
WebCall('PM.toast("Sync password missing - open the vault first")');
|
||||
WebCall('PM.toast("Master password missing - open the vault first")');
|
||||
WebCall('PM.editDone()');
|
||||
Exit;
|
||||
end;
|
||||
LUuid := FCurrent.Uuid;
|
||||
LTitle := FCurrent.Display;
|
||||
SetBusy(True);
|
||||
|
||||
TThread.CreateAnonymousThread(
|
||||
@@ -641,88 +649,115 @@ begin
|
||||
LSw: TStopwatch;
|
||||
LDone: Boolean;
|
||||
begin
|
||||
LClient := TSyncClient.Create(FUrl, FToken);
|
||||
LDone := False;
|
||||
LErr := '';
|
||||
LClient := nil;
|
||||
try
|
||||
try
|
||||
for LAttempt := 1 to 3 do
|
||||
if HasLocalContainer then
|
||||
begin
|
||||
Log(Format('save: attempt %d - GET', [LAttempt]));
|
||||
// Sans empreinte connue : on VEUT le corps, pas un 304.
|
||||
LGet := LClient.Fetch;
|
||||
if LGet.Status <> ssOK then
|
||||
begin
|
||||
LErr := Format('read failed (HTTP %d) %s', [LGet.HttpCode, LGet.Error]);
|
||||
Break;
|
||||
end;
|
||||
|
||||
try
|
||||
// UNE SEULE DERIVATION PAR ENREGISTREMENT : la variante Ex rend la
|
||||
// cle, son sel et les iterations, que le rechiffrement reutilise.
|
||||
// Deriver deux fois le meme mot de passe coutait une seconde fois
|
||||
// quatre a six secondes, pour une cle equivalente.
|
||||
LSw := TStopwatch.StartNew;
|
||||
LPlain := DecryptContainerEx(LGet.Container, FSyncPwd,
|
||||
LKey, LSalt, LIter);
|
||||
LNew := PatchEntryField(LPlain, LUuid, 'password', ANewValue);
|
||||
Log(Format('save: decrypt+patch in %d ms', [LSw.ElapsedMilliseconds]));
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
LErr := E.Message;
|
||||
Break;
|
||||
end;
|
||||
end;
|
||||
|
||||
// COFFRE LOCAL : rien a tirer, rien a publier, aucun conflit possible
|
||||
// - il n'y a qu'un ecrivain. Le conteneur est reecrit d'abord, le
|
||||
// cache ensuite : l'inverse laisserait un cache en avance sur
|
||||
// l'autorite si l'ecriture echouait entre les deux.
|
||||
LSw := TStopwatch.StartNew;
|
||||
LPlain := DecryptContainerEx(LoadLocalContainer, FSyncPwd,
|
||||
LKey, LSalt, LIter);
|
||||
LNew := APatch(LPlain);
|
||||
Log(Format('save: decrypt+patch in %d ms', [LSw.ElapsedMilliseconds]));
|
||||
LSw := TStopwatch.StartNew;
|
||||
LBody := EncryptContainerWithKey(LNew, LKey, LSalt, LIter);
|
||||
Log(Format('save: re-encrypt in %d ms', [LSw.ElapsedMilliseconds]));
|
||||
LPut := LClient.Put(LBody, LGet.ETag, 'password change from phone');
|
||||
if LPut.Status = ssOK then
|
||||
SaveLocalContainer(LBody);
|
||||
try
|
||||
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False, FPin);
|
||||
except
|
||||
on E: Exception do Log(' cache not written: ' + E.Message);
|
||||
end;
|
||||
Log('save: written locally');
|
||||
LDone := True;
|
||||
end
|
||||
else
|
||||
begin
|
||||
LClient := TSyncClient.Create(FUrl, FToken);
|
||||
for LAttempt := 1 to 3 do
|
||||
begin
|
||||
Log('save: published, new sha ' + LPut.ETag);
|
||||
// Le cache local suit le distant, avec SON empreinte : les laisser
|
||||
// diverger ferait retelecharger et redechiffrer a la prochaine
|
||||
// ouverture pour retrouver ce qu'on vient d'ecrire.
|
||||
FETag := LPut.ETag;
|
||||
try
|
||||
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False, FPin);
|
||||
except
|
||||
on E: Exception do Log(' cache not written: ' + E.Message);
|
||||
Log(Format('save: attempt %d - GET', [LAttempt]));
|
||||
// Sans empreinte connue : on VEUT le corps, pas un 304.
|
||||
LGet := LClient.Fetch;
|
||||
if LGet.Status <> ssOK then
|
||||
begin
|
||||
LErr := Format('read failed (HTTP %d) %s', [LGet.HttpCode, LGet.Error]);
|
||||
Break;
|
||||
end;
|
||||
LDone := True;
|
||||
|
||||
try
|
||||
// UNE SEULE DERIVATION PAR ENREGISTREMENT : la variante Ex rend
|
||||
// la cle, son sel et les iterations, que le rechiffrement
|
||||
// reutilise. Deriver deux fois le meme mot de passe coutait une
|
||||
// seconde fois quatre a six secondes, pour une cle equivalente.
|
||||
LSw := TStopwatch.StartNew;
|
||||
LPlain := DecryptContainerEx(LGet.Container, FSyncPwd,
|
||||
LKey, LSalt, LIter);
|
||||
LNew := APatch(LPlain);
|
||||
Log(Format('save: decrypt+patch in %d ms', [LSw.ElapsedMilliseconds]));
|
||||
except
|
||||
on E: Exception do
|
||||
begin
|
||||
LErr := E.Message;
|
||||
Break;
|
||||
end;
|
||||
end;
|
||||
|
||||
LSw := TStopwatch.StartNew;
|
||||
LBody := EncryptContainerWithKey(LNew, LKey, LSalt, LIter);
|
||||
Log(Format('save: re-encrypt in %d ms', [LSw.ElapsedMilliseconds]));
|
||||
LPut := LClient.Put(LBody, LGet.ETag, AComment);
|
||||
if LPut.Status = ssOK then
|
||||
begin
|
||||
Log('save: published, new sha ' + LPut.ETag);
|
||||
// Le cache local suit le distant, avec SON empreinte : les
|
||||
// laisser diverger ferait retelecharger et redechiffrer a la
|
||||
// prochaine ouverture pour retrouver ce qu'on vient d'ecrire.
|
||||
FETag := LPut.ETag;
|
||||
try
|
||||
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False, FPin);
|
||||
except
|
||||
on E: Exception do Log(' cache not written: ' + E.Message);
|
||||
end;
|
||||
LDone := True;
|
||||
Break;
|
||||
end;
|
||||
|
||||
if LPut.Status = ssConflict then
|
||||
begin
|
||||
Log('save: 409 - another device published, re-reading');
|
||||
Continue;
|
||||
end;
|
||||
|
||||
LErr := Format('write failed (HTTP %d) %s', [LPut.HttpCode, LPut.Error]);
|
||||
Break;
|
||||
end;
|
||||
|
||||
if LPut.Status = ssConflict then
|
||||
begin
|
||||
Log('save: 409 - another device published, re-reading');
|
||||
Continue;
|
||||
end;
|
||||
|
||||
LErr := Format('write failed (HTTP %d) %s', [LPut.HttpCode, LPut.Error]);
|
||||
Break;
|
||||
if (not LDone) and (LErr = '') then
|
||||
LErr := 'the remote kept changing - try again';
|
||||
end;
|
||||
|
||||
if (not LDone) and (LErr = '') then
|
||||
LErr := 'the remote kept changing - try again';
|
||||
|
||||
TThread.Queue(nil,
|
||||
procedure
|
||||
begin
|
||||
if LDone then
|
||||
begin
|
||||
AdoptVault(LNew);
|
||||
WebCall('PM.toast(' + JsArg('Saved: ' + LTitle) + ')');
|
||||
WebCall('PM.toast(' + JsArg(ALabel) + ')');
|
||||
end
|
||||
else
|
||||
WebCall('PM.toast(' + JsArg('Not saved - ' + LErr) + ')');
|
||||
end);
|
||||
except
|
||||
// FILET. Le rechiffrement et le PUT sont hors du try qui
|
||||
// couvre le dechiffrement : une exception y sortait sans un mot,
|
||||
// et l'echec devenait indiscernable d'une lenteur.
|
||||
// FILET. Le rechiffrement et le PUT sont hors du try qui couvre le
|
||||
// dechiffrement : une exception y sortait sans un mot, et l'echec
|
||||
// devenait indiscernable d'une lenteur.
|
||||
on E: Exception do
|
||||
begin
|
||||
LErr := E.ClassName + ': ' + E.Message;
|
||||
@@ -735,20 +770,57 @@ begin
|
||||
LClient.Free;
|
||||
// LA REMISE EN ETAT EST ICI, PAS DANS LE CHEMIN NOMINAL. Elle etait
|
||||
// apres la boucle : une exception au rechiffrement ou au PUT la
|
||||
// sautait, et la ligne restait sur « Saving... » pour toujours - sans
|
||||
// sautait, et la ligne restait sur "Saving..." pour toujours - sans
|
||||
// message, puisque le message aussi etait dans la partie sautee.
|
||||
// Quoi qu'il arrive au-dessus, l'ecran redevient utilisable.
|
||||
TThread.Queue(nil,
|
||||
procedure
|
||||
begin
|
||||
SetBusy(False);
|
||||
ReopenEntryByUuid(LUuid);
|
||||
if AReopenUuid <> '' then ReopenEntryByUuid(AReopenUuid)
|
||||
// Rien a rouvrir : c'est une creation. La page doit quand meme
|
||||
// reprendre la main, sinon un echec laisse son bouton mort.
|
||||
else WebCall('PM.editDone()');
|
||||
end);
|
||||
end;
|
||||
end).Start;
|
||||
end;
|
||||
|
||||
/// Descend les coordonnees du serveur dans les champs de la page.
|
||||
procedure TForm8.SaveEntryPassword(const ANewValue: string);
|
||||
var
|
||||
LUuid, LTitle: string;
|
||||
begin
|
||||
if FCurrent.Uuid = '' then
|
||||
begin
|
||||
WebCall('PM.toast("This entry has no uuid - cannot be updated")');
|
||||
WebCall('PM.editDone()');
|
||||
Exit;
|
||||
end;
|
||||
LUuid := FCurrent.Uuid;
|
||||
LTitle := FCurrent.Display;
|
||||
CommitChange('Saved: ' + LTitle, 'password change from phone', LUuid,
|
||||
function (APlain: string): string
|
||||
begin
|
||||
Result := PatchEntryField(APlain, LUuid, 'password', ANewValue);
|
||||
end);
|
||||
end;
|
||||
|
||||
procedure TForm8.CreateEntry(const ATitle, ASite, AUser, APwd, AKind: string);
|
||||
begin
|
||||
// Une entree sans rien pour la reconnaitre serait une ligne muette dans la
|
||||
// liste, et rien ne permettrait de la retrouver pour la corriger.
|
||||
if (ATitle = '') and (ASite = '') and (AUser = '') then
|
||||
begin
|
||||
WebCall('PM.toast("Give it at least a name")');
|
||||
Exit;
|
||||
end;
|
||||
CommitChange('Added: ' + ATitle, 'new entry from phone', '',
|
||||
function (APlain: string): string
|
||||
begin
|
||||
Result := AddEntry(APlain, ATitle, ASite, AUser, APwd, AKind);
|
||||
end);
|
||||
end;
|
||||
|
||||
procedure TForm8.PushConfig;
|
||||
var
|
||||
LUrl, LToken: string;
|
||||
@@ -886,6 +958,13 @@ begin
|
||||
// La NOUVELLE valeur remonte par le pont - c'est la seule commande qui
|
||||
// transporte un secret. Elle ne quitte pas le processus, l'iframe qui la
|
||||
// porte est detruite, et HandleWebCommand ne journalise que l'action.
|
||||
else if LAction = 'newentry' then
|
||||
CreateEntry(TNetEncoding.URL.Decode(Param('t')),
|
||||
TNetEncoding.URL.Decode(Param('s')),
|
||||
TNetEncoding.URL.Decode(Param('u')),
|
||||
TNetEncoding.URL.Decode(Param('p')),
|
||||
TNetEncoding.URL.Decode(Param('k')))
|
||||
|
||||
else if LAction = 'setpwd' then
|
||||
SaveEntryPassword(TNetEncoding.URL.Decode(Param('v')))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user