@vrtmrz/livesync-commonlib 0.1.28 → 0.1.30
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.
- package/README.md +5 -1
- package/dist/API/DirectFileManipulatorV2.d.ts +1 -0
- package/dist/API/DirectFileManipulatorV2.js +19 -2
- package/dist/API/processSetting.d.ts +179 -1
- package/dist/API/processSetting.js +5 -1
- package/dist/common/configForDoc.d.ts +1 -0
- package/dist/common/configForDoc.js +24 -7
- package/dist/common/models/db.type.d.ts +1 -0
- package/dist/common/models/setting.const.defaults.js +2 -0
- package/dist/common/models/setting.const.qr.js +2 -1
- package/dist/common/models/setting.lifecycle.js +3 -1
- package/dist/common/models/setting.type.d.ts +2 -0
- package/dist/common/models/shared.definition.configNames.js +4 -0
- package/dist/common/models/tweak.definition.d.ts +6 -0
- package/dist/common/models/tweak.definition.js +1 -0
- package/dist/common/settingConstants.d.ts +1 -0
- package/dist/common/utils.js +2 -1
- package/dist/pouchdb/LiveSyncDBFunctions.d.ts +1 -0
- package/dist/pouchdb/LiveSyncDBFunctions.js +12 -3
- package/dist/pouchdb/StreamingFetch.d.ts +2 -0
- package/dist/pouchdb/StreamingFetch.js +28 -0
- package/dist/pouchdb/encryption.d.ts +2 -2
- package/dist/pouchdb/encryption.js +25 -15
- package/dist/pouchdb/negotiation.d.ts +3 -2
- package/dist/pouchdb/negotiation.js +84 -32
- package/dist/pouchdb/remoteFeatureCompatibility.d.ts +25 -0
- package/dist/pouchdb/remoteFeatureCompatibility.js +52 -0
- package/dist/replication/couchdb/LiveSyncReplicator.js +21 -9
- package/dist/replication/index.d.ts +2 -0
- package/dist/replication/index.js +13 -1
- package/dist/replication/journal/LiveSyncJournalReplicator.js +2 -5
- package/dist/serviceModules/Rebuilder.js +11 -6
- package/dist/services/base/CommonlibMessages.d.ts +2 -0
- package/dist/services/base/CommonlibMessages.js +2 -0
- package/dist/services/base/RemoteConnection.d.ts +2 -0
- package/dist/services/base/RemoteService.js +9 -1
- package/dist/worker/bgWorker.js +1 -1
- package/docs/development.md +2 -0
- package/docs/proven-in-use.md +6 -0
- package/docs/remote-feature-compatibility.md +177 -0
- 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,12 +183,11 @@ async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase,
|
|
|
176
183
|
throw ex;
|
|
177
184
|
}
|
|
178
185
|
}
|
|
179
|
-
if (isObfuscatedEntry(loadDoc)) {
|
|
180
|
-
const path = getPath(loadDoc);
|
|
186
|
+
if (isObfuscatedEntry(loadDoc) || isInternalObfuscatedEntry(loadDoc) && isEncryptedMeta(loadDoc)) {
|
|
181
187
|
if (isEncryptedMeta(loadDoc)) {
|
|
182
188
|
const pbkdf2salt = await getPBKDF2Salt();
|
|
183
189
|
try {
|
|
184
|
-
const metadata = await decryptMetaWithHKDF(path, passphrase, pbkdf2salt);
|
|
190
|
+
const metadata = await decryptMetaWithHKDF(loadDoc.path, passphrase, pbkdf2salt);
|
|
185
191
|
for (const key of Object.keys(metadata)) {
|
|
186
192
|
loadDoc[key] = metadata[key];
|
|
187
193
|
}
|
|
@@ -190,13 +196,16 @@ async function outgoingDecryptHKDF(doc, migrationDecrypt, decrypted, passphrase,
|
|
|
190
196
|
Logger(ex);
|
|
191
197
|
throw ex;
|
|
192
198
|
}
|
|
193
|
-
} else
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
199
|
+
} else {
|
|
200
|
+
const path = getPath(loadDoc);
|
|
201
|
+
if (isPathProbablyObfuscated(path)) {
|
|
202
|
+
const decryptedPath = await tryDecryptV1AsFallback(path, passphrase, useDynamicIterationCount);
|
|
203
|
+
if (decryptedPath === false) {
|
|
204
|
+
Logger(`${MESSAGE_FALLBACK_DECRYPT_FAILED} on Path`, LOG_LEVEL_NOTICE);
|
|
205
|
+
throw new Error(MESSAGE_FALLBACK_DECRYPT_FAILED);
|
|
206
|
+
}
|
|
207
|
+
loadDoc.path = decryptedPath;
|
|
198
208
|
}
|
|
199
|
-
loadDoc.path = decryptedPath;
|
|
200
209
|
}
|
|
201
210
|
}
|
|
202
211
|
let readEden = {};
|
|
@@ -373,9 +382,9 @@ let preprocessOutgoing = async (doc) => {
|
|
|
373
382
|
let preprocessIncoming = async (doc) => {
|
|
374
383
|
return await Promise.resolve(doc);
|
|
375
384
|
};
|
|
376
|
-
function getConfiguredFunctionsForEncryption(passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm) {
|
|
385
|
+
function getConfiguredFunctionsForEncryption(passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm, encryptInternalMetadata = false) {
|
|
377
386
|
const decryptedCache = /* @__PURE__ */ new Map();
|
|
378
|
-
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);
|
|
379
388
|
const outgoing = (doc) => algorithm !== E2EEAlgorithms.ForceV1 ? outgoingDecryptHKDF(
|
|
380
389
|
doc,
|
|
381
390
|
migrationDecrypt,
|
|
@@ -389,13 +398,14 @@ function getConfiguredFunctionsForEncryption(passphrase, useDynamicIterationCoun
|
|
|
389
398
|
outgoing
|
|
390
399
|
};
|
|
391
400
|
}
|
|
392
|
-
const enableEncryption = (db, passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm) => {
|
|
401
|
+
const enableEncryption = (db, passphrase, useDynamicIterationCount, migrationDecrypt, getPBKDF2Salt, algorithm, encryptInternalMetadata = false) => {
|
|
393
402
|
const { incoming, outgoing } = getConfiguredFunctionsForEncryption(
|
|
394
403
|
passphrase,
|
|
395
404
|
useDynamicIterationCount,
|
|
396
405
|
migrationDecrypt,
|
|
397
406
|
getPBKDF2Salt,
|
|
398
|
-
algorithm
|
|
407
|
+
algorithm,
|
|
408
|
+
encryptInternalMetadata
|
|
399
409
|
);
|
|
400
410
|
db.transform({
|
|
401
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
25
|
-
return
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
460
|
-
|
|
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
|
-
{
|
|
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] =
|
|
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 {
|
|
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] =
|
|
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 {
|
|
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(
|
|
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();
|