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:
@@ -40,6 +40,17 @@ function PatchEntryField(const APlainJson, AUuid, AField, AValue: string): strin
|
|||||||
/// naisse pas dans un dialecte a part.
|
/// naisse pas dans un dialecte a part.
|
||||||
function NewEmptySnapshot(const AUsername: string): string;
|
function NewEmptySnapshot(const AUsername: string): string;
|
||||||
|
|
||||||
|
/// Ajoute une entree a l'instantane et rend le JSON complet.
|
||||||
|
///
|
||||||
|
/// LA MEME FORME QUE CE QU'ECRIT LE POSTE, cles vides comprises : une entree
|
||||||
|
/// nee ici sera relue la-bas, et une cle absente ne se comporte pas toujours
|
||||||
|
/// comme une cle vide. `folder` vaut 'All' parce que c'est le dossier par
|
||||||
|
/// defaut du poste, pas parce que c'est joli.
|
||||||
|
///
|
||||||
|
/// Un uuid neuf, et lui seul : pas de tombstone, on ne supprime pas encore.
|
||||||
|
function AddEntry(const APlainJson, ATitle, ASite, AUsername, APassword,
|
||||||
|
AKind: string): string;
|
||||||
|
|
||||||
/// Horodatage UTC au format que tout le reste emploie (toISOString cote JS).
|
/// Horodatage UTC au format que tout le reste emploie (toISOString cote JS).
|
||||||
/// MELANGER heure locale et UTC a deja decale l'arbitrage de la fusion une
|
/// MELANGER heure locale et UTC a deja decale l'arbitrage de la fusion une
|
||||||
/// fois : il n'y a qu'une source d'heure ici, et elle est en UTC.
|
/// fois : il n'y a qu'une source d'heure ici, et elle est en UTC.
|
||||||
@@ -75,6 +86,50 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function AddEntry(const APlainJson, ATitle, ASite, AUsername, APassword,
|
||||||
|
AKind: string): string;
|
||||||
|
var
|
||||||
|
LRoot, LEntry: TJSONObject;
|
||||||
|
LEntries: TJSONArray;
|
||||||
|
LNow: string;
|
||||||
|
begin
|
||||||
|
LRoot := TJSONObject.ParseJSONValue(APlainJson) as TJSONObject;
|
||||||
|
if LRoot = nil then
|
||||||
|
raise EPMAPatch.Create('Snapshot is not a JSON object');
|
||||||
|
try
|
||||||
|
LEntries := LRoot.GetValue('entries') as TJSONArray;
|
||||||
|
if LEntries = nil then
|
||||||
|
raise EPMAPatch.Create('Snapshot has no entries array');
|
||||||
|
|
||||||
|
LNow := NowUtcIso;
|
||||||
|
LEntry := TJSONObject.Create;
|
||||||
|
// Minuscules et sans accolades : la forme que tout le reste emploie.
|
||||||
|
LEntry.AddPair('uuid', LowerCase(Copy(GUIDToString(TGUID.NewGuid), 2, 36)));
|
||||||
|
LEntry.AddPair('title', ATitle);
|
||||||
|
LEntry.AddPair('site', ASite);
|
||||||
|
LEntry.AddPair('username', AUsername);
|
||||||
|
// Le corps d'une note EST son mot de passe cote stockage - il n'y a pas de
|
||||||
|
// colonne separee, ni ici ni sur le poste.
|
||||||
|
LEntry.AddPair('password', APassword);
|
||||||
|
LEntry.AddPair('folder', 'All');
|
||||||
|
LEntry.AddPair('tags', TJSONArray.Create);
|
||||||
|
LEntry.AddPair('totp_secret', '');
|
||||||
|
LEntry.AddPair('kind', AKind);
|
||||||
|
LEntry.AddPair('template', '');
|
||||||
|
LEntry.AddPair('custom_fields', TJSONArray.Create);
|
||||||
|
LEntry.AddPair('attachments', TJSONArray.Create);
|
||||||
|
LEntry.AddPair('icon_b64', '');
|
||||||
|
LEntry.AddPair('icon_custom', TJSONNumber.Create(0));
|
||||||
|
LEntry.AddPair('created_at', LNow);
|
||||||
|
LEntry.AddPair('updated_at', LNow);
|
||||||
|
LEntries.AddElement(LEntry);
|
||||||
|
|
||||||
|
Result := LRoot.ToJSON;
|
||||||
|
finally
|
||||||
|
LRoot.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
function PatchEntryField(const APlainJson, AUuid, AField, AValue: string): string;
|
function PatchEntryField(const APlainJson, AUuid, AField, AValue: string): string;
|
||||||
var
|
var
|
||||||
LRoot, LEntry: TJSONObject;
|
LRoot, LEntry: TJSONObject;
|
||||||
|
|||||||
@@ -323,6 +323,12 @@
|
|||||||
autocapitalize="none" spellcheck="false">
|
autocapitalize="none" spellcheck="false">
|
||||||
<div class="clear" id="qx">×</div>
|
<div class="clear" id="qx">×</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="ico" id="new" aria-label="New entry">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round">
|
||||||
|
<path d="M12 5v14M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button class="ico" id="lock" aria-label="Lock vault">
|
<button class="ico" id="lock" aria-label="Lock vault">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
stroke-width="2" stroke-linecap="round">
|
stroke-width="2" stroke-linecap="round">
|
||||||
@@ -689,6 +695,80 @@ addEventListener('keydown', function (ev) {
|
|||||||
if (save && !save.disabled) save.click();
|
if (save && !save.disabled) save.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NOUVELLE ENTREE - dans l'ecran de DETAIL, pas dans une quatrieme vue.
|
||||||
|
//
|
||||||
|
// Le rail en porte trois, et chacune coute ses regles de transformation. Le
|
||||||
|
// detail a deja tout ce qu'il faut : un titre, un retour, une colonne qui
|
||||||
|
// defile. Une entree qu'on cree et une entree qu'on consulte occupent la meme
|
||||||
|
// place a l'ecran, et c'est exactement ce que cette vue veut dire.
|
||||||
|
var newKind = 'login';
|
||||||
|
|
||||||
|
function newField(id, label, type) {
|
||||||
|
return '<div class="field"><div class="cap">' + label + '</div>'
|
||||||
|
+ '<div class="line"><input class="field-in" id="' + id + '" type="'
|
||||||
|
+ type + '" autocomplete="off" autocorrect="off" autocapitalize="none"'
|
||||||
|
+ ' spellcheck="false" style="margin:0;flex:1;min-width:0;width:auto">'
|
||||||
|
+ '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNewEntry() {
|
||||||
|
newKind = 'login';
|
||||||
|
detailRows = [];
|
||||||
|
document.getElementById('dtitle').textContent = 'New entry';
|
||||||
|
document.getElementById('fields').innerHTML =
|
||||||
|
'<div class="field"><div class="cap">Kind</div><div class="line">'
|
||||||
|
+ '<button class="btn main" data-kind="login">Login</button>'
|
||||||
|
+ '<button class="btn" data-kind="note">Note</button></div></div>'
|
||||||
|
+ newField('nTitle', 'Name', 'text')
|
||||||
|
+ newField('nSite', 'Site', 'text')
|
||||||
|
+ newField('nUser', 'Username', 'text')
|
||||||
|
// EN CLAIR pendant la saisie, comme l'editeur : on tape un mot de passe
|
||||||
|
// qu'aucune confirmation ne rattrape et qu'on ne peut relire nulle part.
|
||||||
|
+ newField('nPwd', 'Password', 'text')
|
||||||
|
+ '<div class="field"><div class="line">'
|
||||||
|
// flex:1 et non .wide : .wide ne regle que le rembourrage, et la largeur
|
||||||
|
// pleine vient d'etre etire dans son conteneur.
|
||||||
|
+ '<button class="btn main wide" id="nSave" style="flex:1">Save</button>'
|
||||||
|
+ '</div></div>';
|
||||||
|
document.body.classList.remove('on-list');
|
||||||
|
document.body.classList.add('on-detail');
|
||||||
|
view = 'detail';
|
||||||
|
send('state?view=detail');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Un seul ecouteur pour le formulaire : ses boutons naissent et meurent avec
|
||||||
|
// lui, et en attacher a chaque ouverture les empilerait.
|
||||||
|
document.getElementById('fields').addEventListener('click', function (ev) {
|
||||||
|
var kind = ev.target.getAttribute('data-kind');
|
||||||
|
if (kind) {
|
||||||
|
newKind = kind;
|
||||||
|
var all = document.querySelectorAll('[data-kind]');
|
||||||
|
for (var i = 0; i < all.length; i++)
|
||||||
|
all[i].classList.toggle('main', all[i].getAttribute('data-kind') === kind);
|
||||||
|
// Le corps d'une note EST son mot de passe cote stockage : meme champ,
|
||||||
|
// autre intitule, et rien a masquer.
|
||||||
|
var caps = document.querySelectorAll('#fields .cap');
|
||||||
|
for (i = 0; i < caps.length; i++) {
|
||||||
|
if (caps[i].textContent === 'Password' || caps[i].textContent === 'Content')
|
||||||
|
caps[i].textContent = (kind === 'note') ? 'Content' : 'Password';
|
||||||
|
// Une note n'a ni site ni identifiant. Les laisser proposes revient a
|
||||||
|
// demander de remplir ce qui ne sera jamais affiche.
|
||||||
|
if (caps[i].textContent === 'Site' || caps[i].textContent === 'Username')
|
||||||
|
caps[i].parentNode.hidden = (kind === 'note');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.target.id !== 'nSave') return;
|
||||||
|
var v = function (id) { return document.getElementById(id).value.trim(); };
|
||||||
|
ev.target.disabled = true;
|
||||||
|
send('newentry?t=' + encodeURIComponent(v('nTitle'))
|
||||||
|
+ '&s=' + encodeURIComponent(v('nSite'))
|
||||||
|
+ '&u=' + encodeURIComponent(v('nUser'))
|
||||||
|
+ '&p=' + encodeURIComponent(document.getElementById('nPwd').value)
|
||||||
|
+ '&k=' + newKind);
|
||||||
|
});
|
||||||
|
document.getElementById('new').addEventListener('click', openNewEntry);
|
||||||
|
|
||||||
function goList() {
|
function goList() {
|
||||||
document.body.classList.remove('on-detail');
|
document.body.classList.remove('on-detail');
|
||||||
document.body.classList.add('on-list');
|
document.body.classList.add('on-list');
|
||||||
@@ -710,6 +790,11 @@ var PM = {
|
|||||||
q.value = ''; query = '';
|
q.value = ''; query = '';
|
||||||
document.body.classList.remove('searching', 'on-detail');
|
document.body.classList.remove('searching', 'on-detail');
|
||||||
document.body.classList.add('on-list');
|
document.body.classList.add('on-list');
|
||||||
|
// LE FORMULAIRE PART AVEC LA VUE, pas seulement de l'ecran. Il garde le
|
||||||
|
// mot de passe qu'on vient de taper ; le laisser dans le document apres
|
||||||
|
// l'enregistrement le laisserait la jusqu'au verrouillage.
|
||||||
|
detailRows = [];
|
||||||
|
document.getElementById('fields').innerHTML = '';
|
||||||
view = 'list';
|
view = 'list';
|
||||||
renderFolders();
|
renderFolders();
|
||||||
renderRows();
|
renderRows();
|
||||||
@@ -719,6 +804,11 @@ var PM = {
|
|||||||
/// immediats : l'editeur y restait ouvert, boutons desactives, et Cancel
|
/// immediats : l'editeur y restait ouvert, boutons desactives, et Cancel
|
||||||
/// lui-meme ne repondait plus.
|
/// lui-meme ne repondait plus.
|
||||||
editDone: function () {
|
editDone: function () {
|
||||||
|
// Le formulaire de creation passe par ici aussi : son bouton se desactive
|
||||||
|
// a l'envoi, et sans cela un refus le laissait mort avec le texte saisi
|
||||||
|
// derriere - a retaper sans savoir pourquoi.
|
||||||
|
var save = document.getElementById('nSave');
|
||||||
|
if (save) { save.disabled = false; return; }
|
||||||
if (lastDetail) renderDetail(lastDetail);
|
if (lastDetail) renderDetail(lastDetail);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+150
-71
@@ -204,7 +204,12 @@ type
|
|||||||
procedure PatchSelfCheck;
|
procedure PatchSelfCheck;
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
procedure OpenEntry(AVaultIndex: Integer);
|
procedure OpenEntry(AVaultIndex: Integer);
|
||||||
|
procedure CommitChange(const ALabel, AComment, AReopenUuid: string;
|
||||||
|
const APatch: TFunc<string, string>);
|
||||||
procedure SaveEntryPassword(const ANewValue: 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 ReopenEntryByUuid(const AUuid: string);
|
||||||
procedure OnClipTimer(Sender: TObject);
|
procedure OnClipTimer(Sender: TObject);
|
||||||
procedure OnDeadline(Sender: TObject);
|
procedure OnDeadline(Sender: TObject);
|
||||||
@@ -601,9 +606,20 @@ end;
|
|||||||
/// tout - y compris le dechiffrement, parce que le contenu a change. Trois
|
/// 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
|
/// essais : au-dela, c'est que l'autre appareil publie en boucle, et insister
|
||||||
/// n'y changerait rien.
|
/// n'y changerait rien.
|
||||||
procedure TForm8.SaveEntryPassword(const ANewValue: string);
|
/// ENREGISTRER UNE MODIFICATION - un seul chemin, deux transports.
|
||||||
var
|
///
|
||||||
LUuid, LTitle: string;
|
/// 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
|
begin
|
||||||
// CHAQUE REFUS REND LA MAIN A LA PAGE. Ces gardes se contentaient d'un
|
// CHAQUE REFUS REND LA MAIN A LA PAGE. Ces gardes se contentaient d'un
|
||||||
// message : l'editeur restait ouvert avec ses boutons desactives, donc
|
// message : l'editeur restait ouvert avec ses boutons desactives, donc
|
||||||
@@ -614,20 +630,12 @@ begin
|
|||||||
WebCall('PM.editDone()');
|
WebCall('PM.editDone()');
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
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
|
if FSyncPwd = '' then
|
||||||
begin
|
begin
|
||||||
WebCall('PM.toast("Sync password missing - open the vault first")');
|
WebCall('PM.toast("Master password missing - open the vault first")');
|
||||||
WebCall('PM.editDone()');
|
WebCall('PM.editDone()');
|
||||||
Exit;
|
Exit;
|
||||||
end;
|
end;
|
||||||
LUuid := FCurrent.Uuid;
|
|
||||||
LTitle := FCurrent.Display;
|
|
||||||
SetBusy(True);
|
SetBusy(True);
|
||||||
|
|
||||||
TThread.CreateAnonymousThread(
|
TThread.CreateAnonymousThread(
|
||||||
@@ -641,88 +649,115 @@ begin
|
|||||||
LSw: TStopwatch;
|
LSw: TStopwatch;
|
||||||
LDone: Boolean;
|
LDone: Boolean;
|
||||||
begin
|
begin
|
||||||
LClient := TSyncClient.Create(FUrl, FToken);
|
|
||||||
LDone := False;
|
LDone := False;
|
||||||
LErr := '';
|
LErr := '';
|
||||||
|
LClient := nil;
|
||||||
try
|
try
|
||||||
try
|
try
|
||||||
for LAttempt := 1 to 3 do
|
if HasLocalContainer then
|
||||||
begin
|
begin
|
||||||
Log(Format('save: attempt %d - GET', [LAttempt]));
|
// COFFRE LOCAL : rien a tirer, rien a publier, aucun conflit possible
|
||||||
// Sans empreinte connue : on VEUT le corps, pas un 304.
|
// - il n'y a qu'un ecrivain. Le conteneur est reecrit d'abord, le
|
||||||
LGet := LClient.Fetch;
|
// cache ensuite : l'inverse laisserait un cache en avance sur
|
||||||
if LGet.Status <> ssOK then
|
// l'autorite si l'ecriture echouait entre les deux.
|
||||||
begin
|
LSw := TStopwatch.StartNew;
|
||||||
LErr := Format('read failed (HTTP %d) %s', [LGet.HttpCode, LGet.Error]);
|
LPlain := DecryptContainerEx(LoadLocalContainer, FSyncPwd,
|
||||||
Break;
|
LKey, LSalt, LIter);
|
||||||
end;
|
LNew := APatch(LPlain);
|
||||||
|
Log(Format('save: decrypt+patch in %d ms', [LSw.ElapsedMilliseconds]));
|
||||||
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;
|
|
||||||
|
|
||||||
LSw := TStopwatch.StartNew;
|
LSw := TStopwatch.StartNew;
|
||||||
LBody := EncryptContainerWithKey(LNew, LKey, LSalt, LIter);
|
LBody := EncryptContainerWithKey(LNew, LKey, LSalt, LIter);
|
||||||
Log(Format('save: re-encrypt in %d ms', [LSw.ElapsedMilliseconds]));
|
Log(Format('save: re-encrypt in %d ms', [LSw.ElapsedMilliseconds]));
|
||||||
LPut := LClient.Put(LBody, LGet.ETag, 'password change from phone');
|
SaveLocalContainer(LBody);
|
||||||
if LPut.Status = ssOK then
|
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
|
begin
|
||||||
Log('save: published, new sha ' + LPut.ETag);
|
Log(Format('save: attempt %d - GET', [LAttempt]));
|
||||||
// Le cache local suit le distant, avec SON empreinte : les laisser
|
// Sans empreinte connue : on VEUT le corps, pas un 304.
|
||||||
// diverger ferait retelecharger et redechiffrer a la prochaine
|
LGet := LClient.Fetch;
|
||||||
// ouverture pour retrouver ce qu'on vient d'ecrire.
|
if LGet.Status <> ssOK then
|
||||||
FETag := LPut.ETag;
|
begin
|
||||||
try
|
LErr := Format('read failed (HTTP %d) %s', [LGet.HttpCode, LGet.Error]);
|
||||||
SaveVault(LNew, FETag, FSyncPwd, BiometricAvailable, False, FPin);
|
Break;
|
||||||
except
|
|
||||||
on E: Exception do Log(' cache not written: ' + E.Message);
|
|
||||||
end;
|
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;
|
Break;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if LPut.Status = ssConflict then
|
if (not LDone) and (LErr = '') then
|
||||||
begin
|
LErr := 'the remote kept changing - try again';
|
||||||
Log('save: 409 - another device published, re-reading');
|
|
||||||
Continue;
|
|
||||||
end;
|
|
||||||
|
|
||||||
LErr := Format('write failed (HTTP %d) %s', [LPut.HttpCode, LPut.Error]);
|
|
||||||
Break;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if (not LDone) and (LErr = '') then
|
|
||||||
LErr := 'the remote kept changing - try again';
|
|
||||||
|
|
||||||
TThread.Queue(nil,
|
TThread.Queue(nil,
|
||||||
procedure
|
procedure
|
||||||
begin
|
begin
|
||||||
if LDone then
|
if LDone then
|
||||||
begin
|
begin
|
||||||
AdoptVault(LNew);
|
AdoptVault(LNew);
|
||||||
WebCall('PM.toast(' + JsArg('Saved: ' + LTitle) + ')');
|
WebCall('PM.toast(' + JsArg(ALabel) + ')');
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
WebCall('PM.toast(' + JsArg('Not saved - ' + LErr) + ')');
|
WebCall('PM.toast(' + JsArg('Not saved - ' + LErr) + ')');
|
||||||
end);
|
end);
|
||||||
except
|
except
|
||||||
// FILET. Le rechiffrement et le PUT sont hors du try qui
|
// FILET. Le rechiffrement et le PUT sont hors du try qui couvre le
|
||||||
// couvre le dechiffrement : une exception y sortait sans un mot,
|
// dechiffrement : une exception y sortait sans un mot, et l'echec
|
||||||
// et l'echec devenait indiscernable d'une lenteur.
|
// devenait indiscernable d'une lenteur.
|
||||||
on E: Exception do
|
on E: Exception do
|
||||||
begin
|
begin
|
||||||
LErr := E.ClassName + ': ' + E.Message;
|
LErr := E.ClassName + ': ' + E.Message;
|
||||||
@@ -735,20 +770,57 @@ begin
|
|||||||
LClient.Free;
|
LClient.Free;
|
||||||
// LA REMISE EN ETAT EST ICI, PAS DANS LE CHEMIN NOMINAL. Elle etait
|
// LA REMISE EN ETAT EST ICI, PAS DANS LE CHEMIN NOMINAL. Elle etait
|
||||||
// apres la boucle : une exception au rechiffrement ou au PUT la
|
// 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.
|
// message, puisque le message aussi etait dans la partie sautee.
|
||||||
// Quoi qu'il arrive au-dessus, l'ecran redevient utilisable.
|
// Quoi qu'il arrive au-dessus, l'ecran redevient utilisable.
|
||||||
TThread.Queue(nil,
|
TThread.Queue(nil,
|
||||||
procedure
|
procedure
|
||||||
begin
|
begin
|
||||||
SetBusy(False);
|
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;
|
end;
|
||||||
end).Start;
|
end).Start;
|
||||||
end;
|
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;
|
procedure TForm8.PushConfig;
|
||||||
var
|
var
|
||||||
LUrl, LToken: string;
|
LUrl, LToken: string;
|
||||||
@@ -886,6 +958,13 @@ begin
|
|||||||
// La NOUVELLE valeur remonte par le pont - c'est la seule commande qui
|
// 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
|
// transporte un secret. Elle ne quitte pas le processus, l'iframe qui la
|
||||||
// porte est detruite, et HandleWebCommand ne journalise que l'action.
|
// 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
|
else if LAction = 'setpwd' then
|
||||||
SaveEntryPassword(TNetEncoding.URL.Decode(Param('v')))
|
SaveEntryPassword(TNetEncoding.URL.Decode(Param('v')))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user