fix(import): la restauration dupliquait l'historique, comme la sync
Le meme defaut, par une autre porte : restaurer une sauvegarde par-dessus des entrees existantes passe aussi par encryptImportEntry puis PUT, donc re-chiffre un mot de passe inchange et fait croire au serveur qu'il a change. La sync etait la cause de ce qui se voyait, pas la seule. La regle est extraite en une fonction, keepCiphertextIfSamePassword, appelee par les deux. Les re-publications partielles - tag, dossier, migration des metadonnees - sont hors de cause : elles reutilisent deja le chiffre stocke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+30
-1
@@ -448,6 +448,34 @@ function normalizeImportTimestamp(s) {
|
|||||||
return d.toISOString().slice(0, 19).replace('T', ' ');
|
return d.toISOString().slice(0, 19).replace('T', ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rend à `enc` le chiffré d'origine quand le mot de passe n'a pas changé.
|
||||||
|
///
|
||||||
|
/// POURQUOI CE N'EST PAS COSMÉTIQUE. Un conteneur — instantané de sync ou
|
||||||
|
/// sauvegarde — transporte le mot de passe EN CLAIR ; `encryptImportEntry` le
|
||||||
|
/// re-chiffre avec un IV neuf, donc le chiffré diffère TOUJOURS, même quand
|
||||||
|
/// rien n'a bougé. Or le serveur décide d'archiver une version d'historique en
|
||||||
|
/// comparant les chiffrés (`encrypted_password <> :newenc`) : il ne voit que du
|
||||||
|
/// chiffré et croit à un changement. Chaque synchronisation, chaque restauration
|
||||||
|
/// déposait donc un doublon daté du jour, et l'historique montrait la même
|
||||||
|
/// valeur plusieurs fois à des jours d'écart.
|
||||||
|
///
|
||||||
|
/// Seul le client connaît les deux clairs. Il compare ici, et rend le chiffré
|
||||||
|
/// stocké quand ils sont égaux — ce que le commentaire du handler appelle déjà
|
||||||
|
/// « JS reuses originalEncrypted bit-for-bit ». Le garde du serveur redevient
|
||||||
|
/// exact sans qu'il ait à déchiffrer quoi que ce soit.
|
||||||
|
///
|
||||||
|
/// À appeler AVANT tout PUT construit par encryptImportEntry sur une entrée qui
|
||||||
|
/// existe déjà. Les re-publications partielles (tag, dossier, migration) sont
|
||||||
|
/// hors de cause : elles réutilisent déjà le chiffré stocké.
|
||||||
|
async function keepCiphertextIfSamePassword(enc, local, plainPassword) {
|
||||||
|
if (!local || !local.encrypted_password || !local.iv) return enc;
|
||||||
|
const before = await decryptPwd(local.encrypted_password, local.iv);
|
||||||
|
if (before === '[ERROR]' || before !== (plainPassword || '')) return enc;
|
||||||
|
enc.encrypted_password = local.encrypted_password;
|
||||||
|
enc.iv = local.iv;
|
||||||
|
return enc;
|
||||||
|
}
|
||||||
|
|
||||||
async function encryptImportEntry(plain) {
|
async function encryptImportEntry(plain) {
|
||||||
const pw = await encryptPwd(plain.password);
|
const pw = await encryptPwd(plain.password);
|
||||||
let totpEnc = '', totpIv = '';
|
let totpEnc = '', totpIv = '';
|
||||||
@@ -733,7 +761,8 @@ async function doImport() {
|
|||||||
try {
|
try {
|
||||||
const local = localByUuid.get(src.uuid);
|
const local = localByUuid.get(src.uuid);
|
||||||
if (!local) continue;
|
if (!local) continue;
|
||||||
const enc = await encryptImportEntry(src);
|
const enc = await keepCiphertextIfSamePassword(
|
||||||
|
await encryptImportEntry(src), local, src.password);
|
||||||
await api('/entries/' + local.id, {
|
await api('/entries/' + local.id, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
headers: authHeaders({ 'Content-Type': 'application/json' }),
|
||||||
|
|||||||
+5
-24
@@ -539,30 +539,11 @@ async function applyRemoteSnapshot(remote) {
|
|||||||
const remoteWins = (r.updated_at || '') > (local.updated_at || '');
|
const remoteWins = (r.updated_at || '') > (local.updated_at || '');
|
||||||
if (!remoteWins) continue;
|
if (!remoteWins) continue;
|
||||||
try {
|
try {
|
||||||
const enc = await encryptImportEntry(r);
|
// Le chiffré d'origine si le mot de passe n'a pas bougé :
|
||||||
// LE MOT DE PASSE INCHANGÉ GARDE SON CHIFFRÉ, BIT POUR BIT.
|
// sans cela le serveur archive une version d'historique à
|
||||||
//
|
// chaque synchronisation. Voir keepCiphertextIfSamePassword.
|
||||||
// L'instantané transporte le clair, et encryptImportEntry le
|
const enc = await keepCiphertextIfSamePassword(
|
||||||
// re-chiffre avec un IV neuf : le chiffré diffère donc TOUJOURS,
|
await encryptImportEntry(r), local, r.password);
|
||||||
// même quand le mot de passe n'a pas bougé. Or le serveur décide
|
|
||||||
// d'écrire une version dans l'historique en comparant les
|
|
||||||
// chiffrés (`encrypted_password <> :newenc`) — il voyait une
|
|
||||||
// différence là où il n'y en avait aucune, et chaque
|
|
||||||
// synchronisation qui redescendait une entrée y déposait un
|
|
||||||
// doublon daté du jour.
|
|
||||||
//
|
|
||||||
// Le serveur ne peut pas trancher : il ne voit que du chiffré.
|
|
||||||
// Seul le client connaît les deux clairs. On compare ici, et on
|
|
||||||
// rend son chiffré d'origine quand ils sont égaux — ce que le
|
|
||||||
// commentaire du handler appelle déjà « JS reuses
|
|
||||||
// originalEncrypted bit-for-bit ».
|
|
||||||
if (local.encrypted_password && local.iv) {
|
|
||||||
const before = await decryptPwd(local.encrypted_password, local.iv);
|
|
||||||
if (before !== '[ERROR]' && before === (r.password || '')) {
|
|
||||||
enc.encrypted_password = local.encrypted_password;
|
|
||||||
enc.iv = local.iv;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// PUT keeps the existing id but accepts the encrypted blobs.
|
// PUT keeps the existing id but accepts the encrypted blobs.
|
||||||
// template + uuid not touched here (server preserves columns
|
// template + uuid not touched here (server preserves columns
|
||||||
// when fields aren't in body — uuid is immutable anyway).
|
// when fields aren't in body — uuid is immutable anyway).
|
||||||
|
|||||||
Reference in New Issue
Block a user