@vrtmrz/livesync-commonlib 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +5 -1
  2. package/dist/API/DirectFileManipulatorV2.d.ts +1 -0
  3. package/dist/API/DirectFileManipulatorV2.js +19 -2
  4. package/dist/API/processSetting.d.ts +179 -1
  5. package/dist/API/processSetting.js +5 -1
  6. package/dist/common/configForDoc.d.ts +1 -0
  7. package/dist/common/configForDoc.js +24 -7
  8. package/dist/common/models/db.type.d.ts +1 -0
  9. package/dist/common/models/setting.const.defaults.js +2 -0
  10. package/dist/common/models/setting.const.qr.js +2 -1
  11. package/dist/common/models/setting.lifecycle.js +3 -1
  12. package/dist/common/models/setting.type.d.ts +2 -0
  13. package/dist/common/models/shared.definition.configNames.js +4 -0
  14. package/dist/common/models/tweak.definition.d.ts +6 -0
  15. package/dist/common/models/tweak.definition.js +1 -0
  16. package/dist/common/settingConstants.d.ts +1 -0
  17. package/dist/common/utils.js +2 -1
  18. package/dist/pouchdb/LiveSyncDBFunctions.d.ts +1 -0
  19. package/dist/pouchdb/LiveSyncDBFunctions.js +12 -3
  20. package/dist/pouchdb/StreamingFetch.d.ts +2 -0
  21. package/dist/pouchdb/StreamingFetch.js +28 -0
  22. package/dist/pouchdb/encryption.d.ts +2 -2
  23. package/dist/pouchdb/encryption.js +15 -7
  24. package/dist/pouchdb/negotiation.d.ts +3 -2
  25. package/dist/pouchdb/negotiation.js +84 -32
  26. package/dist/pouchdb/remoteFeatureCompatibility.d.ts +25 -0
  27. package/dist/pouchdb/remoteFeatureCompatibility.js +52 -0
  28. package/dist/replication/couchdb/LiveSyncReplicator.js +21 -9
  29. package/dist/replication/index.d.ts +2 -0
  30. package/dist/replication/index.js +13 -1
  31. package/dist/replication/journal/LiveSyncJournalReplicator.js +2 -5
  32. package/dist/serviceModules/Rebuilder.js +11 -6
  33. package/dist/services/base/CommonlibMessages.d.ts +2 -0
  34. package/dist/services/base/CommonlibMessages.js +2 -0
  35. package/dist/services/base/RemoteConnection.d.ts +2 -0
  36. package/dist/services/base/RemoteService.js +9 -1
  37. package/dist/services/base/SettingService.d.ts +2 -0
  38. package/dist/services/base/SettingService.js +10 -3
  39. package/dist/worker/bgWorker.js +1 -1
  40. package/docs/proven-in-use.md +4 -0
  41. package/docs/remote-feature-compatibility.md +177 -0
  42. package/package.json +1 -1
@@ -6,6 +6,8 @@ import {
6
6
  import { isEncryptedChunkEntry, isSyncInfoEntry, isObfuscatedEntry } from "../common/utils.js";
7
7
  import { isPathProbablyObfuscated, obfuscatePath } from "octagonal-wheels/encryption/encryption";
8
8
  import { getPath } from "../string_and_binary/path.js";
9
+ import { ICHeader, ICXHeader, PSCHeader } from "../common/models/fileaccess.const.js";
10
+ import { PREFIX_OBFUSCATED } from "../common/types.js";
9
11
  import { encryptWorker, decryptWorker, encryptHKDFWorker, decryptHKDFWorker } from "#worker";
10
12
  const encrypt = encryptWorker;
11
13
  const decrypt = decryptWorker;
@@ -52,6 +54,11 @@ const ENCRYPTED_META_PREFIX = "/\\:";
52
54
  function isEncryptedMeta(doc) {
53
55
  return "path" in doc && doc.path.startsWith(ENCRYPTED_META_PREFIX);
54
56
  }
57
+ function isInternalObfuscatedEntry(doc) {
58
+ return [ICHeader, ICXHeader, PSCHeader].some(
59
+ (prefix) => doc._id.startsWith(`${prefix}${PREFIX_OBFUSCATED}`)
60
+ );
61
+ }
55
62
  async function encryptMetaWithHKDF(doc, passphrase, pbkdf2Salt) {
56
63
  if (isEncryptedMeta(doc)) {
57
64
  return doc.path;
@@ -80,7 +87,7 @@ const MESSAGE_FALLBACK_DECRYPT_FAILED = "Failed to decrypt the data with V1 meth
80
87
  const ENCRYPTION_HKDF_FAILED = "Encryption with HKDF failed.";
81
88
  const DECRYPTION_HKDF_FAILED = "Decryption with HKDF failed.";
82
89
  const DECRYPTION_FALLBACK_FAILED = "Decryption with fallback failed.";
83
- async function incomingEncryptHKDF(doc, passphrase, useDynamicIterationCount, getPBKDF2Salt) {
90
+ async function incomingEncryptHKDF(doc, passphrase, useDynamicIterationCount, getPBKDF2Salt, encryptInternalMetadata) {
84
91
  const saveDoc = {
85
92
  ...doc
86
93
  };
@@ -124,7 +131,7 @@ async function incomingEncryptHKDF(doc, passphrase, useDynamicIterationCount, ge
124
131
  throw ex;
125
132
  }
126
133
  }
127
- if (isObfuscatedEntry(saveDoc)) {
134
+ if (isObfuscatedEntry(saveDoc) || encryptInternalMetadata && isInternalObfuscatedEntry(saveDoc)) {
128
135
  const pbkdf2salt = await getPBKDF2Salt();
129
136
  if (!isEncryptedMeta(saveDoc)) {
130
137
  try {
@@ -176,7 +183,7 @@ async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase,
176
183
  throw ex;
177
184
  }
178
185
  }
179
- if (isObfuscatedEntry(loadDoc)) {
186
+ if (isObfuscatedEntry(loadDoc) || isInternalObfuscatedEntry(loadDoc) && isEncryptedMeta(loadDoc)) {
180
187
  if (isEncryptedMeta(loadDoc)) {
181
188
  const pbkdf2salt = await getPBKDF2Salt();
182
189
  try {
@@ -375,9 +382,9 @@ let preprocessOutgoing = async (doc) => {
375
382
  let preprocessIncoming = async (doc) => {
376
383
  return await Promise.resolve(doc);
377
384
  };
378
- function getConfiguredFunctionsForEncryption(passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm) {
385
+ function getConfiguredFunctionsForEncryption(passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm, encryptInternalMetadata = false) {
379
386
  const decryptedCache = /* @__PURE__ */ new Map();
380
- const incoming = (doc) => algorithm === E2EEAlgorithms.V2 ? incomingEncryptHKDF(doc, passphrase, useDynamicIterationCount, getPBKDF2Salt) : incomingEncryptV1(doc, passphrase, useDynamicIterationCount);
387
+ const incoming = (doc) => algorithm === E2EEAlgorithms.V2 ? incomingEncryptHKDF(doc, passphrase, useDynamicIterationCount, getPBKDF2Salt, encryptInternalMetadata) : incomingEncryptV1(doc, passphrase, useDynamicIterationCount);
381
388
  const outgoing = (doc) => algorithm !== E2EEAlgorithms.ForceV1 ? outgoingDecryptHKDF(
382
389
  doc,
383
390
  migrationDecrypt,
@@ -391,13 +398,14 @@ function getConfiguredFunctionsForEncryption(passphrase, useDynamicIterationCoun
391
398
  outgoing
392
399
  };
393
400
  }
394
- const enableEncryption = (db, passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm) => {
401
+ const enableEncryption = (db, passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm, encryptInternalMetadata = false) => {
395
402
  const { incoming, outgoing } = getConfiguredFunctionsForEncryption(
396
403
  passphrase,
397
404
  useDynamicIterationCount,
398
405
  migrationDecrypt,
399
406
  getPBKDF2Salt,
400
- algorithm
407
+ algorithm,
408
+ encryptInternalMetadata
401
409
  );
402
410
  db.transform({
403
411
  incoming,
@@ -1,5 +1,6 @@
1
- export declare const checkRemoteVersion: (db: PouchDB.Database, migrate: (from: number, to: number) => Promise<boolean>, barrier?: number) => Promise<boolean>;
2
- export declare const bumpRemoteVersion: (db: PouchDB.Database, barrier?: number) => Promise<boolean>;
1
+ export declare const checkRemoteVersion: (db: PouchDB.Database, migrate: (from: number, to: number) => Promise<boolean>, barrier?: number, requiredFeatures?: readonly string[]) => Promise<boolean>;
2
+ export declare const bumpRemoteVersion: (db: PouchDB.Database, barrier?: number, usedFeatures?: readonly string[]) => Promise<boolean>;
3
+ export declare function declareRemoteFeatures(db: PouchDB.Database, requiredFeatures: readonly string[]): Promise<boolean>;
3
4
  export declare const checkSyncInfo: (db: PouchDB.Database) => Promise<boolean>;
4
5
  /**
5
6
  * Counts the number of remote (potentially) compromised chunks in the database.
@@ -1,53 +1,104 @@
1
- import { LOG_LEVEL_INFO, Logger } from "../common/logger.js";
1
+ import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, Logger } from "../common/logger.js";
2
2
  import {
3
3
  LOG_LEVEL_VERBOSE,
4
4
  SYNCINFO_ID,
5
5
  VER,
6
6
  VERSIONING_DOCID
7
7
  } from "../common/types.js";
8
- import { resolveWithIgnoreKnownError } from "../common/utils.js";
9
8
  import { isErrorOfMissingDoc } from "./utils_couchdb.js";
10
- const checkRemoteVersion = async (db, migrate, barrier = VER) => {
9
+ import {
10
+ assessRemoteFeatureDocument,
11
+ describeRemoteFeatureRejection,
12
+ ENCRYPTED_INTERNAL_METADATA_FEATURE,
13
+ REMOTE_FEATURE_GENERATION
14
+ } from "./remoteFeatureCompatibility.js";
15
+ const checkRemoteVersion = async (db, migrate, barrier = VER, requiredFeatures = []) => {
11
16
  try {
12
17
  const versionInfo = await db.get(VERSIONING_DOCID);
13
- if (versionInfo.type != "versioninfo") {
14
- return false;
18
+ const assessment = assessRemoteFeatureDocument(versionInfo);
19
+ if (assessment.status === "older-generation") {
20
+ if (assessment.version >= barrier) return true;
21
+ if (!await migrate(assessment.version, barrier)) return false;
22
+ if (!await bumpRemoteVersion(db, barrier)) return false;
23
+ return requiredFeatures.length === 0 || await declareRemoteFeatures(db, requiredFeatures);
15
24
  }
16
- const version = versionInfo.version;
17
- if (version < barrier) {
18
- const versionUpResult = await migrate(version, barrier);
19
- if (versionUpResult) {
20
- await bumpRemoteVersion(db);
21
- return true;
22
- }
25
+ if (assessment.status !== "supported") {
26
+ Logger(describeRemoteFeatureRejection(assessment), LOG_LEVEL_NOTICE);
27
+ return false;
23
28
  }
24
- if (version == barrier) return true;
25
- return false;
29
+ if (versionInfo.version < barrier) return false;
30
+ return requiredFeatures.length === 0 || await declareRemoteFeatures(db, requiredFeatures);
26
31
  } catch (ex) {
27
32
  if (isErrorOfMissingDoc(ex)) {
28
- if (await bumpRemoteVersion(db)) {
29
- return true;
30
- }
31
- return false;
33
+ const info = await db.info();
34
+ if (info.doc_count > 0) return false;
35
+ return await bumpRemoteVersion(db, requiredFeatures.length ? REMOTE_FEATURE_GENERATION : VER, requiredFeatures);
32
36
  }
33
37
  throw ex;
34
38
  }
35
39
  };
36
- const bumpRemoteVersion = async (db, barrier = VER) => {
37
- const vi = {
38
- _id: VERSIONING_DOCID,
39
- version: barrier,
40
- type: "versioninfo"
41
- // pbkdf2salt: "", // this will be set later.
42
- };
43
- const versionInfo = await resolveWithIgnoreKnownError(db.get(VERSIONING_DOCID), vi);
44
- if (versionInfo.type != "versioninfo") {
45
- return false;
40
+ const bumpRemoteVersion = async (db, barrier = VER, usedFeatures = []) => {
41
+ for (let attempt = 0; attempt < 4; attempt++) {
42
+ let current;
43
+ try {
44
+ current = await db.get(VERSIONING_DOCID);
45
+ } catch (error) {
46
+ if (!isErrorOfMissingDoc(error)) throw error;
47
+ }
48
+ if (current) {
49
+ const assessment = assessRemoteFeatureDocument(current);
50
+ if (assessment.status !== "supported" && assessment.status !== "older-generation") {
51
+ Logger(describeRemoteFeatureRejection(assessment), LOG_LEVEL_NOTICE);
52
+ return false;
53
+ }
54
+ if (current.version >= barrier) {
55
+ return usedFeatures.length === 0 || await declareRemoteFeatures(db, usedFeatures);
56
+ }
57
+ }
58
+ const next = {
59
+ ...current,
60
+ _id: VERSIONING_DOCID,
61
+ version: barrier,
62
+ type: "versioninfo",
63
+ ...barrier >= REMOTE_FEATURE_GENERATION ? { used_features: [...new Set(usedFeatures)] } : {}
64
+ };
65
+ try {
66
+ await db.put(next);
67
+ return true;
68
+ } catch (error) {
69
+ if (typeof error !== "object" || error === null || !("status" in error) || error.status !== 409) {
70
+ throw error;
71
+ }
72
+ }
46
73
  }
47
- vi._rev = versionInfo._rev;
48
- await db.put(vi);
49
- return true;
74
+ return false;
50
75
  };
76
+ async function declareRemoteFeatures(db, requiredFeatures) {
77
+ const requested = [...new Set(requiredFeatures)];
78
+ if (requested.some((name) => name !== ENCRYPTED_INTERNAL_METADATA_FEATURE)) {
79
+ throw new Error("A writer requested an unsupported remote feature.");
80
+ }
81
+ if (requested.length === 0) return true;
82
+ for (let attempt = 0; attempt < 4; attempt++) {
83
+ const current = await db.get(VERSIONING_DOCID);
84
+ const assessment = assessRemoteFeatureDocument(current);
85
+ if (assessment.status !== "supported") {
86
+ Logger(describeRemoteFeatureRejection(assessment), LOG_LEVEL_NOTICE);
87
+ return false;
88
+ }
89
+ const usedFeatures = [.../* @__PURE__ */ new Set([...assessment.usedFeatures, ...requested])];
90
+ if (current.version === REMOTE_FEATURE_GENERATION && usedFeatures.length === assessment.usedFeatures.length) {
91
+ return true;
92
+ }
93
+ try {
94
+ await db.put({ ...current, version: REMOTE_FEATURE_GENERATION, used_features: usedFeatures });
95
+ return true;
96
+ } catch (ex) {
97
+ if (typeof ex !== "object" || ex === null || !("status" in ex) || ex.status !== 409) throw ex;
98
+ }
99
+ }
100
+ return false;
101
+ }
51
102
  const checkSyncInfo = async (db) => {
52
103
  try {
53
104
  const syncinfo = await db.get(SYNCINFO_ID);
@@ -105,5 +156,6 @@ export {
105
156
  bumpRemoteVersion,
106
157
  checkRemoteVersion,
107
158
  checkSyncInfo,
108
- countCompromisedChunks
159
+ countCompromisedChunks,
160
+ declareRemoteFeatures
109
161
  };
@@ -0,0 +1,25 @@
1
+ export declare const REMOTE_FEATURE_GENERATION = 13;
2
+ export declare const ENCRYPTED_INTERNAL_METADATA_FEATURE = "encrypted-internal-metadata-v1";
3
+ export declare function usesEncryptedInternalMetadata(setting: {
4
+ encrypt?: boolean;
5
+ usePathObfuscation?: boolean;
6
+ E2EEAlgorithm?: string;
7
+ encryptInternalMetadata?: boolean;
8
+ }): boolean;
9
+ export type RemoteFeatureAssessment = {
10
+ status: "supported";
11
+ usedFeatures: readonly string[];
12
+ } | {
13
+ status: "older-generation";
14
+ version: number;
15
+ } | {
16
+ status: "unsupported-generation";
17
+ version: number;
18
+ } | {
19
+ status: "unknown-features";
20
+ identifiers: readonly string[];
21
+ } | {
22
+ status: "invalid-control";
23
+ };
24
+ export declare function assessRemoteFeatureDocument(document: unknown): RemoteFeatureAssessment;
25
+ export declare function describeRemoteFeatureRejection(assessment: RemoteFeatureAssessment): string;
@@ -0,0 +1,52 @@
1
+ import { E2EEAlgorithms, VERSIONING_DOCID } from "../common/types.js";
2
+ const REMOTE_FEATURE_GENERATION = 13;
3
+ const ENCRYPTED_INTERNAL_METADATA_FEATURE = "encrypted-internal-metadata-v1";
4
+ const supportedFeatures = /* @__PURE__ */ new Set([ENCRYPTED_INTERNAL_METADATA_FEATURE]);
5
+ function usesEncryptedInternalMetadata(setting) {
6
+ return setting.encryptInternalMetadata === true && setting.encrypt === true && setting.usePathObfuscation === true && setting.E2EEAlgorithm === E2EEAlgorithms.V2;
7
+ }
8
+ function assessRemoteFeatureDocument(document) {
9
+ if (typeof document !== "object" || document === null) return { status: "invalid-control" };
10
+ const value = document;
11
+ if (value._id !== VERSIONING_DOCID || value.type !== "versioninfo" || value._deleted === true) {
12
+ return { status: "invalid-control" };
13
+ }
14
+ if (typeof value.version !== "number" || !Number.isSafeInteger(value.version) || value.version < 0) {
15
+ return { status: "invalid-control" };
16
+ }
17
+ const version = value.version;
18
+ if (version < 12) {
19
+ return "used_features" in value ? { status: "invalid-control" } : { status: "older-generation", version };
20
+ }
21
+ if (version > REMOTE_FEATURE_GENERATION) return { status: "unsupported-generation", version };
22
+ if (version === 12) {
23
+ return "used_features" in value ? { status: "invalid-control" } : { status: "supported", usedFeatures: [] };
24
+ }
25
+ if (!Array.isArray(value.used_features) || !value.used_features.every((name) => typeof name === "string" && name.length > 0)) {
26
+ return { status: "invalid-control" };
27
+ }
28
+ const usedFeatures = [...new Set(value.used_features)];
29
+ const identifiers = usedFeatures.filter((name) => !supportedFeatures.has(name));
30
+ return identifiers.length > 0 ? { status: "unknown-features", identifiers } : { status: "supported", usedFeatures };
31
+ }
32
+ function describeRemoteFeatureRejection(assessment) {
33
+ switch (assessment.status) {
34
+ case "unknown-features":
35
+ return `Unknown features are in use: ${assessment.identifiers.join(", ")}`;
36
+ case "unsupported-generation":
37
+ return `Unsupported remote database generation: ${assessment.version}`;
38
+ case "invalid-control":
39
+ return "The remote database version document is invalid.";
40
+ case "older-generation":
41
+ return `The remote database requires migration from generation ${assessment.version}.`;
42
+ case "supported":
43
+ return "";
44
+ }
45
+ }
46
+ export {
47
+ ENCRYPTED_INTERNAL_METADATA_FEATURE,
48
+ REMOTE_FEATURE_GENERATION,
49
+ assessRemoteFeatureDocument,
50
+ describeRemoteFeatureRejection,
51
+ usesEncryptedInternalMetadata
52
+ };
@@ -6,7 +6,6 @@ import {
6
6
  LOG_LEVEL_NOTICE,
7
7
  LOG_LEVEL_VERBOSE,
8
8
  DEVICE_ID_PREFERRED,
9
- TweakValuesTemplate,
10
9
  DOCID_SYNC_PARAMETERS,
11
10
  DEFAULT_SYNC_PARAMETERS,
12
11
  ProtocolVersions,
@@ -16,17 +15,20 @@ import {
16
15
  import {
17
16
  resolveWithIgnoreKnownError,
18
17
  globalConcurrencyController,
19
- extractObject,
20
18
  wrapException,
21
19
  sizeToHumanReadable,
22
20
  arrayToChunkedArray,
23
21
  parseHeaderValues
24
22
  } from "../../common/utils.js";
25
23
  import { Logger } from "../../common/logger.js";
26
- import { checkRemoteVersion, countCompromisedChunks } from "../../pouchdb/negotiation.js";
24
+ import { checkRemoteVersion, countCompromisedChunks, declareRemoteFeatures } from "../../pouchdb/negotiation.js";
25
+ import {
26
+ ENCRYPTED_INTERNAL_METADATA_FEATURE,
27
+ usesEncryptedInternalMetadata
28
+ } from "../../pouchdb/remoteFeatureCompatibility.js";
27
29
  import { isErrorOfMissingDoc } from "../../pouchdb/utils_couchdb.js";
28
30
  import { preprocessOutgoing } from "../../pouchdb/encryption.js";
29
- import { ensureDatabaseIsCompatible } from "../../pouchdb/LiveSyncDBFunctions.js";
31
+ import { ensureDatabaseIsCompatible, getEffectiveTweakValues } from "../../pouchdb/LiveSyncDBFunctions.js";
30
32
  import {
31
33
  LiveSyncAbstractReplicator
32
34
  } from "../LiveSyncAbstractReplicator.js";
@@ -456,9 +458,8 @@ class LiveSyncCouchDBReplicator extends LiveSyncAbstractReplicator {
456
458
  return await this.withRemoteConnection(d, async (db) => this.sendChunks(setting, db, showResult, fromSeq));
457
459
  }
458
460
  const connectivity = await this.checkReplicationConnectivity(setting, false, false, false, false);
459
- if (connectivity !== false) {
460
- await this.closeRemoteConnection(connectivity);
461
- }
461
+ if (connectivity === false) return false;
462
+ await this.closeRemoteConnection(connectivity);
462
463
  Logger(`Bulk sending chunks to remote database...`, showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO, "fetch");
463
464
  const remoteMilestone = await remoteDB.get(MILESTONE_DOCID);
464
465
  const remoteID = remoteMilestone?.created ?? 0;
@@ -966,6 +967,12 @@ ${sendingDocs}/(${sentDocsCount} done)`,
966
967
  this.preferredTweakValue = ensure[1];
967
968
  return false;
968
969
  }
970
+ if (usesEncryptedInternalMetadata(setting) && !await declareRemoteFeatures(dbRet.db, [ENCRYPTED_INTERNAL_METADATA_FEATURE])) {
971
+ recordCompatibilityDecision?.(
972
+ centralCompatibilityRejected(CENTRAL_COMPATIBILITY_REJECTION_REASONS.INCOMPATIBLE_VERSION)
973
+ );
974
+ return false;
975
+ }
969
976
  recordCompatibilityDecision?.(CENTRAL_COMPATIBILITY_ACCEPTED);
970
977
  }
971
978
  const syncOptionBase = {
@@ -1306,7 +1313,11 @@ ${sendingDocs}/(${sentDocsCount} done)`,
1306
1313
  customHeaders,
1307
1314
  settings.useRequestAPI,
1308
1315
  async () => await this.getReplicationPBKDF2Salt(settings),
1309
- { ...connectionOptions, encryptionAlgorithm: settings.E2EEAlgorithm }
1316
+ {
1317
+ ...connectionOptions,
1318
+ encryptionAlgorithm: settings.E2EEAlgorithm,
1319
+ encryptInternalMetadata: usesEncryptedInternalMetadata(settings)
1320
+ }
1310
1321
  );
1311
1322
  }
1312
1323
  async openOwnedConnection(settings, performSetup = false) {
@@ -1380,6 +1391,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
1380
1391
  return false;
1381
1392
  }
1382
1393
  return await this.withRemoteConnection(ret, async (db) => {
1394
+ if (!await checkRemoteVersion(db, this.migrate.bind(this), VER)) return false;
1383
1395
  const remoteChunks = await db.allDocs({ keys: missingChunks, include_docs: true });
1384
1396
  const errorRows = remoteChunks.rows.filter((e) => "error" in e);
1385
1397
  if (errorRows.length > 0) {
@@ -1452,7 +1464,7 @@ ${sendingDocs}/(${sentDocsCount} done)`,
1452
1464
  }
1453
1465
  try {
1454
1466
  const remoteMilestone = await db.get(MILESTONE_DOCID);
1455
- remoteMilestone.tweak_values[DEVICE_ID_PREFERRED] = extractObject(TweakValuesTemplate, { ...setting });
1467
+ remoteMilestone.tweak_values[DEVICE_ID_PREFERRED] = getEffectiveTweakValues(setting);
1456
1468
  await db.put(remoteMilestone);
1457
1469
  Logger(`Preferred tweak values has been registered`, LOG_LEVEL_VERBOSE);
1458
1470
  } catch (ex) {
@@ -8,3 +8,5 @@ export { CENTRAL_REMOTE_ADMINISTRATION_ACTIONS, CENTRAL_REMOTE_ADMINISTRATION_FA
8
8
  export type { MilestoneCentralRemoteAdministrationObservation, CentralRemoteAdministrationAction, CentralRemoteAdministrationFailureReason, CentralRemoteAdministrationObservation, CentralRemoteAdministrationRequest, CentralRemoteAdministrationReplicator, CentralRemoteAdministrationResult, CentralRemoteAdministrationRunner, CentralRemoteAdministrationVerificationFailed, CentralRemoteAdministrationVerified, } from "./CentralRemoteAdministration.js";
9
9
  export { CENTRAL_COMPATIBILITY_REJECTION_REASONS } from "./CentralCompatibility.js";
10
10
  export type { CentralCompatibilityRecoveryHint, CentralCompatibilityRejectionReason } from "./CentralCompatibility.js";
11
+ export { assessRemoteFeatureDocument, describeRemoteFeatureRejection, ENCRYPTED_INTERNAL_METADATA_FEATURE, REMOTE_FEATURE_GENERATION, usesEncryptedInternalMetadata, } from "../pouchdb/remoteFeatureCompatibility.js";
12
+ export type { RemoteFeatureAssessment } from "../pouchdb/remoteFeatureCompatibility.js";
@@ -35,6 +35,13 @@ import {
35
35
  centralRemoteAdministrationVerified
36
36
  } from "./CentralRemoteAdministration.js";
37
37
  import { CENTRAL_COMPATIBILITY_REJECTION_REASONS } from "./CentralCompatibility.js";
38
+ import {
39
+ assessRemoteFeatureDocument,
40
+ describeRemoteFeatureRejection,
41
+ ENCRYPTED_INTERNAL_METADATA_FEATURE,
42
+ REMOTE_FEATURE_GENERATION,
43
+ usesEncryptedInternalMetadata
44
+ } from "../pouchdb/remoteFeatureCompatibility.js";
38
45
  export {
39
46
  CAPABILITY_NOT_APPLICABLE,
40
47
  CAPABILITY_NOT_IMPLEMENTED,
@@ -46,19 +53,23 @@ export {
46
53
  CENTRAL_REMOTE_ADMINISTRATION_OBSERVATION_KINDS,
47
54
  CENTRAL_REMOTE_ADMINISTRATION_RESULT_STATUSES,
48
55
  CENTRAL_REMOTE_REPLICATION_READINESS,
56
+ ENCRYPTED_INTERNAL_METADATA_FEATURE,
49
57
  NO_INTERACTION,
50
58
  NO_REMOTE_RESOURCE_CAPABILITIES,
51
59
  PEER_REPLICATION_READINESS,
52
60
  PROVIDER_OWNED_CENTRAL_REMOTE_REPLICATION_READINESS,
61
+ REMOTE_FEATURE_GENERATION,
53
62
  REMOTE_RESOURCE_KINDS,
54
63
  REPLICATION_CANCELLED,
55
64
  REPLICATION_COMPLETED,
56
65
  REPLICATION_PROGRESS_PRESENTATIONS,
57
66
  USER_INITIATED_REPLICATION_AUTHORITY,
58
67
  applyCentralRemoteAdministrationMutation,
68
+ assessRemoteFeatureDocument,
59
69
  centralRemoteAdministrationVerificationFailed,
60
70
  centralRemoteAdministrationVerified,
61
71
  defineReplicatorProviderDefinitions,
72
+ describeRemoteFeatureRejection,
62
73
  isCentralRemoteAdministrationVerified,
63
74
  isReplicationCompleted,
64
75
  milestoneSatisfiesCentralRemoteAdministration,
@@ -70,5 +81,6 @@ export {
70
81
  supportedOpenReplicationContinuous,
71
82
  supportedOpenReplicationOneShot,
72
83
  supportedOpenReplicationUnattended,
73
- supportedStopActiveTransfer
84
+ supportedStopActiveTransfer,
85
+ usesEncryptedInternalMetadata
74
86
  };
@@ -2,7 +2,6 @@ import {
2
2
  LOG_LEVEL_NOTICE,
3
3
  LOG_LEVEL_VERBOSE,
4
4
  DEVICE_ID_PREFERRED,
5
- TweakValuesTemplate,
6
5
  RemotePreferredTweakNotConfiguredReasons,
7
6
  RemotePreferredTweakStatuses
8
7
  } from "../../common/types.js";
@@ -11,7 +10,7 @@ import { JournalSyncCore } from "./JournalSyncCore.js";
11
10
  import { MinioStorageAdapter } from "./objectstore/MinioStorageAdapter.js";
12
11
  import { LiveSyncAbstractReplicator } from "../LiveSyncAbstractReplicator.js";
13
12
  import { ensureRemoteIsCompatible } from "../../pouchdb/LiveSyncDBFunctions.js";
14
- import { extractObject } from "../../common/utils.js";
13
+ import { getEffectiveTweakValues } from "../../pouchdb/LiveSyncDBFunctions.js";
15
14
  import { JournalStorageReadStatuses } from "./objectstore/JournalStorageAdapter.js";
16
15
  import {
17
16
  CENTRAL_COMPATIBILITY_ACCEPTED,
@@ -462,9 +461,7 @@ ${ex.message}`, LOG_LEVEL_NOTICE);
462
461
  if (!remoteMilestone) {
463
462
  throw new Error("Missing remote milestone");
464
463
  }
465
- remoteMilestone.tweak_values[DEVICE_ID_PREFERRED] = extractObject(TweakValuesTemplate, {
466
- ...setting
467
- });
464
+ remoteMilestone.tweak_values[DEVICE_ID_PREFERRED] = getEffectiveTweakValues(setting);
468
465
  Logger(`Preferred tweak values have been registered`, LOG_LEVEL_VERBOSE);
469
466
  if (!await trialClient.uploadJson(MILSTONE_DOCID, remoteMilestone)) {
470
467
  throw new Error("Could not upload remote milestone");
@@ -5,7 +5,11 @@ import { LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "octagonal-wheels/common/log
5
5
  import { delay } from "octagonal-wheels/promises";
6
6
  import { EVENT_DATABASE_REBUILT } from "../events/coreEvents.js";
7
7
  import { ServiceModuleBase } from "./ServiceModuleBase.js";
8
- import { fetchChangesForInitialSync, isRetryableStreamingFetchFailure } from "../pouchdb/StreamingFetch.js";
8
+ import {
9
+ checkRemoteFeaturesForInitialSync,
10
+ fetchChangesForInitialSync,
11
+ isRetryableStreamingFetchFailure
12
+ } from "../pouchdb/StreamingFetch.js";
9
13
  import { getConfiguredFunctionsForEncryption } from "../pouchdb/encryption.js";
10
14
  import { AuthorizationHeaderGenerator, generateCredentialObject } from "../replication/httplib.js";
11
15
  import { isRemediationModeActive, parseHeaderValues } from "../common/utils.js";
@@ -384,8 +388,13 @@ Are you sure you wish to proceed?`;
384
388
  });
385
389
  }
386
390
  async performFetchLocalDBFast(settings, autoResume) {
387
- this.appLifecycle.resetIsReady();
388
391
  const remote = settings.couchDB_URI.replace(/\/+$/, "") + (settings.couchDB_DBNAME == "" ? "" : "/" + settings.couchDB_DBNAME);
392
+ const authHeader = await new AuthorizationHeaderGenerator().getAuthorizationHeader(
393
+ generateCredentialObject(settings)
394
+ );
395
+ const customHeaders = parseHeaderValues(settings.couchDB_CustomHeaders);
396
+ await checkRemoteFeaturesForInitialSync(remote, authHeader, customHeaders);
397
+ this.appLifecycle.resetIsReady();
389
398
  let checkpoint = this.getFastFetchCheckpoint(remote);
390
399
  await this.suspendReflectingDatabase();
391
400
  await this.control.applySettings();
@@ -431,10 +440,6 @@ Are you sure you wish to proceed?`;
431
440
  () => securitySeed.read(),
432
441
  settings.E2EEAlgorithm
433
442
  );
434
- const authHeader = await new AuthorizationHeaderGenerator().getAuthorizationHeader(
435
- generateCredentialObject(settings)
436
- );
437
- const customHeaders = parseHeaderValues(settings.couchDB_CustomHeaders);
438
443
  for (let attempt = 0; ; attempt++) {
439
444
  try {
440
445
  await fetchChangesForInitialSync(
@@ -83,6 +83,8 @@ export declare const commonlibEnglishMessages: {
83
83
  readonly "Enable this if your Object Storage doesn't support CORS": "Enable this if your Object Storage doesn't support CORS";
84
84
  readonly "Enable this option to automatically apply the most recent change to documents even when it conflicts": "Enable this option to automatically apply the most recent change to documents even when it conflicts";
85
85
  readonly "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.";
86
+ readonly "Encrypt internal file Properties": "Encrypt internal file Properties";
87
+ readonly "Encrypt file properties (paths, times, sizes, and Chunk references) used by Hidden File Sync and Customisation Sync. This affects future writes; manually rebuild the remote database to protect existing properties. Requires E2EE V2, Property Encryption, and compatible clients on every device.": "Encrypt file properties (paths, times, sizes, and Chunk references) used by Hidden File Sync and Customisation Sync. This affects future writes; manually rebuild the remote database to protect existing properties. Requires E2EE V2, Property Encryption, and compatible clients on every device.";
86
88
  readonly "Encrypting sensitive configuration items": "Encrypting sensitive configuration items";
87
89
  readonly "Encryption passphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": "Encryption passphrase. If changed, you should overwrite the server's database with the new (encrypted) files.";
88
90
  readonly "End-to-End Encryption": "End-to-End Encryption";
@@ -77,6 +77,8 @@ const commonlibEnglishMessages = {
77
77
  "Enable this if your Object Storage doesn't support CORS": "Enable this if your Object Storage doesn't support CORS",
78
78
  "Enable this option to automatically apply the most recent change to documents even when it conflicts": "Enable this option to automatically apply the most recent change to documents even when it conflicts",
79
79
  "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.": "Encrypt contents on the remote database. If you use the plugin's synchronization feature, enabling this is recommended.",
80
+ "Encrypt internal file Properties": "Encrypt internal file Properties",
81
+ "Encrypt file properties (paths, times, sizes, and Chunk references) used by Hidden File Sync and Customisation Sync. This affects future writes; manually rebuild the remote database to protect existing properties. Requires E2EE V2, Property Encryption, and compatible clients on every device.": "Encrypt file properties (paths, times, sizes, and Chunk references) used by Hidden File Sync and Customisation Sync. This affects future writes; manually rebuild the remote database to protect existing properties. Requires E2EE V2, Property Encryption, and compatible clients on every device.",
80
82
  "Encrypting sensitive configuration items": "Encrypting sensitive configuration items",
81
83
  "Encryption passphrase. If changed, you should overwrite the server's database with the new (encrypted) files.": "Encryption passphrase. If changed, you should overwrite the server's database with the new (encrypted) files.",
82
84
  "End-to-End Encryption": "End-to-End Encryption",
@@ -6,6 +6,8 @@ import type { E2EEAlgorithm } from "../../common/types.js";
6
6
  export interface RemoteConnectionOpenOptions {
7
7
  /** Encryption algorithm captured with the settings which own this connection. */
8
8
  readonly encryptionAlgorithm?: E2EEAlgorithm;
9
+ /** Apply HKDF Metadata encryption to obfuscated internal entries on writes. */
10
+ readonly encryptInternalMetadata?: boolean;
9
11
  /**
10
12
  * Cancels abort-capable requests made through the connection when the
11
13
  * owning operation is cancelled.
@@ -297,7 +297,15 @@ class RemoteService extends ServiceBase {
297
297
  replicationFilter(db, compression);
298
298
  disableEncryption();
299
299
  if (passphrase !== "false" && typeof passphrase === "string") {
300
- enableEncryption(db, passphrase, useDynamicIterationCount, false, getPBKDF2Salt, encryptionAlgorithm);
300
+ enableEncryption(
301
+ db,
302
+ passphrase,
303
+ useDynamicIterationCount,
304
+ false,
305
+ getPBKDF2Salt,
306
+ encryptionAlgorithm,
307
+ connectionOptions.encryptInternalMetadata ?? false
308
+ );
301
309
  }
302
310
  } catch (ex) {
303
311
  await closeAfterConnectionFailure();
@@ -110,6 +110,8 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
110
110
  clearUsedPassphrase(): void;
111
111
  decryptConfigurationItem(encrypted: string, passphrase: string): Promise<string | false>;
112
112
  encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings): Promise<string>;
113
+ private encryptPlainConfigurationItem;
114
+ private encryptConfigurationItemWith;
113
115
  /**
114
116
  * Decrypt the given settings.
115
117
  * @param settings The settings to decrypt.
@@ -13,6 +13,7 @@ import { ServiceBase } from "./ServiceBase.js";
13
13
  import { createInstanceLogFunction } from "../lib/logUtils.js";
14
14
  import { isCloudantURI } from "../../pouchdb/utils_couchdb.js";
15
15
  import { decryptString, encryptString } from "../../encryption/stringEncryption.js";
16
+ import { encryptWithEphemeralSalt } from "octagonal-wheels/encryption/hkdf";
16
17
  import {
17
18
  activateP2PRemoteConfiguration,
18
19
  activateRemoteConfiguration,
@@ -207,7 +208,7 @@ class SettingService extends ServiceBase {
207
208
  settings.endpoint = "";
208
209
  }
209
210
  if (settings.encrypt && settings.passphrase != "") {
210
- settings.encryptedPassphrase = await this.encryptConfigurationItem(settings.passphrase, settings);
211
+ settings.encryptedPassphrase = await this.encryptPlainConfigurationItem(settings.passphrase, settings);
211
212
  settings.passphrase = "";
212
213
  }
213
214
  await this.encryptRemoteConfigurationUris(settings);
@@ -405,8 +406,14 @@ class SettingService extends ServiceBase {
405
406
  return false;
406
407
  }
407
408
  async encryptConfigurationItem(src, settings) {
409
+ return this.encryptConfigurationItemWith(src, settings, encryptString);
410
+ }
411
+ async encryptPlainConfigurationItem(src, settings) {
412
+ return this.encryptConfigurationItemWith(src, settings, encryptWithEphemeralSalt);
413
+ }
414
+ async encryptConfigurationItemWith(src, settings, encrypt) {
408
415
  if (this.usedPassphrase != "") {
409
- return await encryptString(src, this.usedPassphrase + SALT_OF_PASSPHRASE);
416
+ return await encrypt(src, this.usedPassphrase + SALT_OF_PASSPHRASE);
410
417
  }
411
418
  const passphrase = await this.getPassphrase(settings);
412
419
  if (passphrase === false) {
@@ -416,7 +423,7 @@ class SettingService extends ServiceBase {
416
423
  );
417
424
  return "";
418
425
  }
419
- const dec = await encryptString(src, passphrase + SALT_OF_PASSPHRASE);
426
+ const dec = await encrypt(src, passphrase + SALT_OF_PASSPHRASE);
420
427
  if (dec) {
421
428
  this.usedPassphrase = passphrase;
422
429
  return dec;