@vrtmrz/livesync-commonlib 0.1.36 → 0.1.39
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/dist/common/settingConstants.js +1 -1
- package/dist/services/base/CommonlibMessages.d.ts +7 -0
- package/dist/services/base/CommonlibMessages.js +7 -0
- package/dist/services/base/IService.d.ts +3 -1
- package/dist/services/base/SettingService.d.ts +19 -3
- package/dist/services/base/SettingService.js +263 -48
- package/dist/worker/bgWorker.js +1 -1
- package/docs/proven-in-use.md +2 -0
- package/docs/settings-lifecycle.md +6 -0
- package/package.json +4 -4
|
@@ -71,7 +71,7 @@ const SettingInformation = {
|
|
|
71
71
|
},
|
|
72
72
|
configPassphrase: {
|
|
73
73
|
name: "Passphrase of sensitive configuration items",
|
|
74
|
-
desc: "
|
|
74
|
+
desc: "SettingService.ConfigurationPassphraseDeviceLocalDescription"
|
|
75
75
|
},
|
|
76
76
|
configPassphraseStore: {
|
|
77
77
|
name: "Encrypting sensitive configuration items"
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* these messages so that symbolic keys are never exposed to users.
|
|
6
6
|
*/
|
|
7
7
|
export declare const commonlibEnglishMessages: {
|
|
8
|
+
readonly "SettingService.ConfigurationPassphraseDeviceLocalDescription": "This passphrase stays on this device. When another device needs to decrypt protected settings without a saved key, enter the same passphrase at start-up.";
|
|
9
|
+
readonly "SettingService.ConfigurationPassphraseTitle": "Configuration passphrase";
|
|
10
|
+
readonly "SettingService.ConfigurationPassphraseRetryTitle": "Configuration could not be decrypted. Enter the configuration passphrase again.";
|
|
11
|
+
readonly "SettingService.ConfigurationPassphraseCancelled": "Configuration passphrase entry was cancelled or unavailable. Start-up was stopped; saved settings were retained.";
|
|
12
|
+
readonly "SettingService.ConfigurationEncryptionSaveFailed": "Configuration encryption could not be saved. Verify the saved settings and device key before retrying.";
|
|
13
|
+
readonly "SettingService.ConfigurationPassphraseRollbackFailed": "The configuration passphrase could not be restored after a write failure. Verify the saved settings and device key before retrying.";
|
|
14
|
+
readonly "SettingService.ConfigurationCouldNotBeDecrypted": "Existing configuration could not be decrypted. The configuration key was not changed.";
|
|
8
15
|
readonly "(BETA) Always overwrite with a newer file": "(BETA) Always overwrite with a newer file";
|
|
9
16
|
readonly "(Beta) Use ignore files": "(Beta) Use ignore files";
|
|
10
17
|
readonly "(Days passed, 0 to disable automatic-deletion)": "(Days passed, 0 to disable automatic-deletion)";
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
const commonlibEnglishMessages = {
|
|
2
|
+
"SettingService.ConfigurationPassphraseDeviceLocalDescription": "This passphrase stays on this device. When another device needs to decrypt protected settings without a saved key, enter the same passphrase at start-up.",
|
|
3
|
+
"SettingService.ConfigurationPassphraseTitle": "Configuration passphrase",
|
|
4
|
+
"SettingService.ConfigurationPassphraseRetryTitle": "Configuration could not be decrypted. Enter the configuration passphrase again.",
|
|
5
|
+
"SettingService.ConfigurationPassphraseCancelled": "Configuration passphrase entry was cancelled or unavailable. Start-up was stopped; saved settings were retained.",
|
|
6
|
+
"SettingService.ConfigurationEncryptionSaveFailed": "Configuration encryption could not be saved. Verify the saved settings and device key before retrying.",
|
|
7
|
+
"SettingService.ConfigurationPassphraseRollbackFailed": "The configuration passphrase could not be restored after a write failure. Verify the saved settings and device key before retrying.",
|
|
8
|
+
"SettingService.ConfigurationCouldNotBeDecrypted": "Existing configuration could not be decrypted. The configuration key was not changed.",
|
|
2
9
|
"(BETA) Always overwrite with a newer file": "(BETA) Always overwrite with a newer file",
|
|
3
10
|
"(Beta) Use ignore files": "(Beta) Use ignore files",
|
|
4
11
|
"(Days passed, 0 to disable automatic-deletion)": "(Days passed, 0 to disable automatic-deletion)",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FetchHttpHandler } from "@smithy/fetch-http-handler";
|
|
2
2
|
import { type LOG_LEVEL } from "octagonal-wheels/common/logger";
|
|
3
|
-
import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, RemotePreferredTweakResult, SettingsMigrationState, TweakAssessment, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
|
|
3
|
+
import type { AnyEntry, AUTO_MERGED, CouchDBCredentials, ConfigPassphraseStore, diff_result, DocumentID, EntryDoc, EntryHasPath, FileEventItem, FilePath, FilePathWithPrefix, LoadedEntry, MetaEntry, MISSING_OR_ERROR, ObsidianLiveSyncSettings, RemoteDBSettings, RemotePreferredTweakResult, SettingsMigrationState, TweakAssessment, TweakValues, UXFileInfo, UXFileInfoStub } from "../../common/types.js";
|
|
4
4
|
import type { LiveSyncLocalDB } from "../../pouchdb/LiveSyncLocalDB.js";
|
|
5
5
|
import type { ReplicatorInstance } from "../../replication/ReplicatorInstance.js";
|
|
6
6
|
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
|
|
@@ -323,6 +323,8 @@ export interface ISettingService {
|
|
|
323
323
|
deleteDeviceLocalConfig(key: string): void;
|
|
324
324
|
onBeforeSaveSettingData(nextSettings: ObsidianLiveSyncSettings, previousSettings: ObsidianLiveSyncSettings): Promise<(Partial<ObsidianLiveSyncSettings> | void)[]>;
|
|
325
325
|
saveSettingData(): Promise<void>;
|
|
326
|
+
/** Save a new configuration wrapper and its device-local key, retaining the protected values. */
|
|
327
|
+
changeConfigurationEncryption(store: ConfigPassphraseStore, passphrase?: string): Promise<void>;
|
|
326
328
|
currentSettings(): ObsidianLiveSyncSettings;
|
|
327
329
|
updateSettings(updateFn: (current: ObsidianLiveSyncSettings) => ObsidianLiveSyncSettings, saveImmediately?: boolean): Promise<void>;
|
|
328
330
|
applyExternalSettings(partial: Partial<ObsidianLiveSyncSettings>, saveImmediately?: boolean): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ObsidianLiveSyncSettings, type SettingsMigrationState } from "../../common/types.js";
|
|
1
|
+
import { type ConfigPassphraseStore, type ObsidianLiveSyncSettings, type SettingsMigrationState } from "../../common/types.js";
|
|
2
2
|
import type { IAPIService, ISettingService } from "./IService.js";
|
|
3
3
|
import { ServiceBase, type ServiceContext } from "./ServiceBase.js";
|
|
4
4
|
import { createInstanceLogFunction } from "../lib/logUtils.js";
|
|
@@ -22,6 +22,7 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
|
|
|
22
22
|
protected abstract loadData(): Promise<ObsidianLiveSyncSettings | undefined>;
|
|
23
23
|
private _lastPersistedSettings?;
|
|
24
24
|
private _settingsMigrationState?;
|
|
25
|
+
private saveQueue;
|
|
25
26
|
getSettingsMigrationState(): SettingsMigrationState | undefined;
|
|
26
27
|
_log: ReturnType<typeof createInstanceLogFunction>;
|
|
27
28
|
constructor(context: T, dependencies: SettingServiceDependencies);
|
|
@@ -63,6 +64,14 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
|
|
|
63
64
|
* Save the current settings to storage.
|
|
64
65
|
*/
|
|
65
66
|
saveSettingData(): Promise<void>;
|
|
67
|
+
private serialiseSave;
|
|
68
|
+
/** Change the configuration wrapper without changing the protected plaintext values. */
|
|
69
|
+
changeConfigurationEncryption(store: ConfigPassphraseStore, passphrase?: string): Promise<void>;
|
|
70
|
+
private restoreConfigurationPassphrase;
|
|
71
|
+
private prepareConfigurationKeyChange;
|
|
72
|
+
private prepareSettingSave;
|
|
73
|
+
private writePreparedSettings;
|
|
74
|
+
private commitPreparedSettings;
|
|
66
75
|
private encryptRemoteConfigurationUris;
|
|
67
76
|
private hasManagedP2PProfileURI;
|
|
68
77
|
private decryptRemoteConfigurationUris;
|
|
@@ -104,13 +113,15 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
|
|
|
104
113
|
applyExternalSettings(partial: Partial<ObsidianLiveSyncSettings>, saveImmediately?: boolean): Promise<void>;
|
|
105
114
|
applyPartial(partial: Partial<ObsidianLiveSyncSettings>, saveImmediately?: boolean): Promise<void>;
|
|
106
115
|
getPassphrase(settings: ObsidianLiveSyncSettings): Promise<string | false>;
|
|
107
|
-
private usedPassphrase
|
|
116
|
+
private usedPassphrase?;
|
|
117
|
+
private getSavingPassphrase;
|
|
118
|
+
private rememberPassphrase;
|
|
108
119
|
/**
|
|
109
120
|
* Clear any used passphrase from memory.
|
|
110
121
|
*/
|
|
111
122
|
clearUsedPassphrase(): void;
|
|
112
123
|
decryptConfigurationItem(encrypted: string, passphrase: string): Promise<string | false>;
|
|
113
|
-
encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings): Promise<string>;
|
|
124
|
+
encryptConfigurationItem(src: string, settings: ObsidianLiveSyncSettings, passphrase?: string): Promise<string>;
|
|
114
125
|
private encryptPlainConfigurationItem;
|
|
115
126
|
private encryptConfigurationItemWith;
|
|
116
127
|
/**
|
|
@@ -118,6 +129,11 @@ export declare abstract class SettingService<T extends ServiceContext = ServiceC
|
|
|
118
129
|
* @param settings The settings to decrypt.
|
|
119
130
|
*/
|
|
120
131
|
decryptSettings(settings: ObsidianLiveSyncSettings): Promise<ObsidianLiveSyncSettings>;
|
|
132
|
+
private decryptSettingsWithPassphrase;
|
|
133
|
+
private hasProtectedConfiguration;
|
|
134
|
+
private validateStoredIdConfiguration;
|
|
135
|
+
private isPlainConfigurationUri;
|
|
136
|
+
private loadConfiguration;
|
|
121
137
|
loadSettings(): Promise<void>;
|
|
122
138
|
private tryDecodeJson;
|
|
123
139
|
private cloneSettings;
|
|
@@ -13,7 +13,12 @@ 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
|
+
import { encryptWithEphemeralSalt, HKDF_SALTED_ENCRYPTED_PREFIX } from "octagonal-wheels/encryption/hkdf";
|
|
17
|
+
import {
|
|
18
|
+
ENCRYPT_V1_PREFIX_PROBABLY,
|
|
19
|
+
ENCRYPT_V2_PREFIX,
|
|
20
|
+
ENCRYPT_V3_PREFIX
|
|
21
|
+
} from "octagonal-wheels/encryption/encryption";
|
|
17
22
|
import {
|
|
18
23
|
activateP2PRemoteConfiguration,
|
|
19
24
|
activateRemoteConfiguration,
|
|
@@ -31,6 +36,8 @@ import {
|
|
|
31
36
|
restoreConnectionSettings,
|
|
32
37
|
selectConnectionSettings
|
|
33
38
|
} from "../../common/models/setting.policy.js";
|
|
39
|
+
class ConfigurationPassphraseError extends Error {
|
|
40
|
+
}
|
|
34
41
|
class SettingService extends ServiceBase {
|
|
35
42
|
deviceAndVaultName = "";
|
|
36
43
|
APIService;
|
|
@@ -44,6 +51,7 @@ class SettingService extends ServiceBase {
|
|
|
44
51
|
}
|
|
45
52
|
_lastPersistedSettings;
|
|
46
53
|
_settingsMigrationState;
|
|
54
|
+
saveQueue = Promise.resolve();
|
|
47
55
|
getSettingsMigrationState() {
|
|
48
56
|
return this._settingsMigrationState;
|
|
49
57
|
}
|
|
@@ -152,31 +160,131 @@ class SettingService extends ServiceBase {
|
|
|
152
160
|
/**
|
|
153
161
|
* Save the current settings to storage.
|
|
154
162
|
*/
|
|
155
|
-
|
|
156
|
-
this.
|
|
163
|
+
saveSettingData() {
|
|
164
|
+
return this.serialiseSave(async () => {
|
|
165
|
+
const prepared = await this.prepareSettingSave(this.cloneSettings(this.settings));
|
|
166
|
+
await this.writePreparedSettings(prepared);
|
|
167
|
+
this.commitPreparedSettings(prepared);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
serialiseSave(operation) {
|
|
171
|
+
const result = this.saveQueue.then(operation);
|
|
172
|
+
this.saveQueue = result.catch(() => void 0);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/** Change the configuration wrapper without changing the protected plaintext values. */
|
|
176
|
+
changeConfigurationEncryption(store, passphrase) {
|
|
177
|
+
const result = this.serialiseSave(async () => {
|
|
178
|
+
if (store !== "" && store !== "LOCALSTORAGE" && store !== "ASK_AT_LAUNCH") {
|
|
179
|
+
throw new Error("The configuration passphrase storage mode is unsupported.");
|
|
180
|
+
}
|
|
181
|
+
const oldLocalKey = this.getDeviceLocalConfig("ls-setting-passphrase");
|
|
182
|
+
const settings = this.cloneSettings(this.settings);
|
|
183
|
+
let targetKey;
|
|
184
|
+
if (store === "LOCALSTORAGE") {
|
|
185
|
+
targetKey = passphrase ?? oldLocalKey ?? false;
|
|
186
|
+
} else if (store === "") {
|
|
187
|
+
targetKey = "*";
|
|
188
|
+
} else {
|
|
189
|
+
targetKey = await this.getSavingPassphrase({ ...settings, configPassphraseStore: store });
|
|
190
|
+
}
|
|
191
|
+
if (!targetKey) {
|
|
192
|
+
throw new Error("A configuration passphrase is required. Settings were not saved.");
|
|
193
|
+
}
|
|
194
|
+
await this.prepareConfigurationKeyChange(settings, targetKey);
|
|
195
|
+
settings.configPassphraseStore = store;
|
|
196
|
+
const prepared = await this.prepareSettingSave(settings, targetKey);
|
|
197
|
+
const changesLocalKey = store === "LOCALSTORAGE" && targetKey !== oldLocalKey;
|
|
198
|
+
try {
|
|
199
|
+
if (changesLocalKey) this.setDeviceLocalConfig("ls-setting-passphrase", targetKey);
|
|
200
|
+
await this.writePreparedSettings(prepared);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (changesLocalKey) this.restoreConfigurationPassphrase(oldLocalKey, error);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
this.settings.configPassphraseStore = store;
|
|
206
|
+
this.commitPreparedSettings(prepared);
|
|
207
|
+
});
|
|
208
|
+
return result.catch((error) => {
|
|
209
|
+
this._log(this.context.translate("SettingService.ConfigurationEncryptionSaveFailed"), LOG_LEVEL_URGENT);
|
|
210
|
+
throw error;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
restoreConfigurationPassphrase(previous, cause) {
|
|
214
|
+
try {
|
|
215
|
+
if (previous) this.setDeviceLocalConfig("ls-setting-passphrase", previous);
|
|
216
|
+
else this.deleteDeviceLocalConfig("ls-setting-passphrase");
|
|
217
|
+
} catch {
|
|
218
|
+
const message = this.context.translate("SettingService.ConfigurationPassphraseRollbackFailed");
|
|
219
|
+
this._log(message, LOG_LEVEL_URGENT);
|
|
220
|
+
throw new Error(message, { cause });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async prepareConfigurationKeyChange(settings, targetKey) {
|
|
224
|
+
for (const config of Object.values(settings.remoteConfigurations)) {
|
|
225
|
+
if (config.isEncrypted && this.isPlainConfigurationUri(config.uri)) config.isEncrypted = false;
|
|
226
|
+
}
|
|
227
|
+
const hasCiphertext = this.hasProtectedConfiguration(settings);
|
|
228
|
+
if (!hasCiphertext) return;
|
|
229
|
+
const oldKey = await this.getSavingPassphrase(settings);
|
|
230
|
+
const failure = () => new Error(this.context.translate("SettingService.ConfigurationCouldNotBeDecrypted"));
|
|
231
|
+
if (!oldKey) throw failure();
|
|
232
|
+
if (settings.encryptedCouchDBConnection) {
|
|
233
|
+
const connection = this.tryDecodeJson(
|
|
234
|
+
await this.decryptConfigurationItem(settings.encryptedCouchDBConnection, oldKey)
|
|
235
|
+
);
|
|
236
|
+
if (!restoreConnectionSettings(this.cloneSettings(settings), connection)) throw failure();
|
|
237
|
+
settings.encryptedCouchDBConnection = "";
|
|
238
|
+
}
|
|
239
|
+
if (settings.encryptedPassphrase) {
|
|
240
|
+
const plaintext = await this.decryptConfigurationItem(settings.encryptedPassphrase, oldKey);
|
|
241
|
+
if (plaintext === false) throw failure();
|
|
242
|
+
settings.encryptedPassphrase = requireConfigurationCiphertext(
|
|
243
|
+
await this.encryptPlainConfigurationItem(plaintext, settings, targetKey)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (settings.idDerivationVersion === 1 && settings.encryptedIdDerivationKey) {
|
|
247
|
+
const idKey = await this.decryptConfigurationItem(settings.encryptedIdDerivationKey, oldKey);
|
|
248
|
+
if (idKey === false) throw failure();
|
|
249
|
+
configuredIdKey({ ...settings, idDerivationKey: idKey });
|
|
250
|
+
}
|
|
251
|
+
for (const config of Object.values(settings.remoteConfigurations)) {
|
|
252
|
+
if (!config.isEncrypted) continue;
|
|
253
|
+
const plaintext = await this.decryptConfigurationItem(config.uri, oldKey);
|
|
254
|
+
if (plaintext === false) {
|
|
255
|
+
try {
|
|
256
|
+
ConnectionStringParser.parse(config.uri);
|
|
257
|
+
} catch {
|
|
258
|
+
throw failure();
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
config.uri = plaintext;
|
|
262
|
+
}
|
|
263
|
+
config.isEncrypted = false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async prepareSettingSave(settings, selectedPassphrase) {
|
|
157
267
|
const previousSettings = this._lastPersistedSettings ?? this.cloneSettings(this.settings);
|
|
158
|
-
const settings = this.cloneSettings(this.settings);
|
|
159
268
|
const hookResults = await this.onBeforeSaveSettingData(settings, previousSettings);
|
|
269
|
+
const savedPatch = {};
|
|
160
270
|
for (const patch of hookResults) {
|
|
161
271
|
if (patch instanceof Error || !patch) continue;
|
|
162
272
|
Object.assign(settings, patch);
|
|
163
|
-
Object.assign(
|
|
273
|
+
Object.assign(savedPatch, patch);
|
|
164
274
|
}
|
|
275
|
+
const runtime = this.cloneSettings(settings);
|
|
165
276
|
const idKey = configuredIdKey(settings);
|
|
277
|
+
const configurationPassphrase = selectedPassphrase ?? await this.getSavingPassphrase(settings);
|
|
166
278
|
if (settings.idDerivationVersion === 1) {
|
|
167
279
|
if (idKey === false) {
|
|
168
280
|
throw new Error("The configured ID derivation key is unavailable.");
|
|
169
281
|
}
|
|
170
|
-
|
|
171
|
-
if (passphrase === "") {
|
|
172
|
-
passphrase = await this.getPassphrase(settings);
|
|
173
|
-
}
|
|
174
|
-
if (passphrase === false || passphrase === "") {
|
|
282
|
+
if (!configurationPassphrase) {
|
|
175
283
|
const message = "Failed to retrieve a passphrase for the ID derivation key. Settings were not saved.";
|
|
176
284
|
this._log(message, LOG_LEVEL_URGENT);
|
|
177
285
|
throw new Error(message);
|
|
178
286
|
}
|
|
179
|
-
const encryptedIdKey = await encryptString(idKey,
|
|
287
|
+
const encryptedIdKey = await encryptString(idKey, configurationPassphrase + SALT_OF_PASSPHRASE);
|
|
180
288
|
if (encryptedIdKey === "") {
|
|
181
289
|
const message = "Failed to encrypt the ID derivation key. Settings were not saved.";
|
|
182
290
|
this._log(message, LOG_LEVEL_URGENT);
|
|
@@ -184,18 +292,14 @@ class SettingService extends ServiceBase {
|
|
|
184
292
|
}
|
|
185
293
|
settings.encryptedIdDerivationKey = encryptedIdKey;
|
|
186
294
|
settings.idDerivationKey = "";
|
|
187
|
-
this.usedPassphrase = passphrase;
|
|
188
295
|
} else {
|
|
189
296
|
settings.encryptedIdDerivationKey = "";
|
|
190
297
|
}
|
|
191
298
|
settings.deviceAndVaultName = "";
|
|
192
299
|
if (settings.P2P_DevicePeerName && settings.P2P_DevicePeerName.trim() !== "") {
|
|
193
|
-
this._log("Saving device peer name to small config");
|
|
194
|
-
this.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, settings.P2P_DevicePeerName.trim());
|
|
195
300
|
settings.P2P_DevicePeerName = "";
|
|
196
301
|
}
|
|
197
302
|
omitUnpersistedSettings(settings);
|
|
198
|
-
const configurationPassphrase = this.usedPassphrase || await this.getPassphrase(settings);
|
|
199
303
|
if (!configurationPassphrase) {
|
|
200
304
|
if (Object.values(settings.remoteConfigurations).some((config) => this.hasManagedP2PProfileURI(config.uri))) {
|
|
201
305
|
const message = "Failed to retrieve a passphrase for managed P2P source data. Settings were not saved.";
|
|
@@ -213,7 +317,11 @@ class SettingService extends ServiceBase {
|
|
|
213
317
|
} else {
|
|
214
318
|
if (hasConnectionSettings(settings)) {
|
|
215
319
|
settings.encryptedCouchDBConnection = requireConfigurationCiphertext(
|
|
216
|
-
await this.encryptConfigurationItem(
|
|
320
|
+
await this.encryptConfigurationItem(
|
|
321
|
+
JSON.stringify(selectConnectionSettings(settings)),
|
|
322
|
+
settings,
|
|
323
|
+
configurationPassphrase
|
|
324
|
+
)
|
|
217
325
|
);
|
|
218
326
|
clearConnectionSettings(settings);
|
|
219
327
|
} else if (settings.encryptedCouchDBConnection) {
|
|
@@ -226,18 +334,32 @@ class SettingService extends ServiceBase {
|
|
|
226
334
|
}
|
|
227
335
|
if (settings.encrypt && settings.passphrase != "") {
|
|
228
336
|
settings.encryptedPassphrase = requireConfigurationCiphertext(
|
|
229
|
-
await this.encryptPlainConfigurationItem(settings.passphrase, settings)
|
|
337
|
+
await this.encryptPlainConfigurationItem(settings.passphrase, settings, configurationPassphrase)
|
|
230
338
|
);
|
|
231
339
|
settings.passphrase = "";
|
|
232
340
|
}
|
|
233
|
-
await this.encryptRemoteConfigurationUris(settings);
|
|
341
|
+
await this.encryptRemoteConfigurationUris(settings, configurationPassphrase);
|
|
234
342
|
}
|
|
235
343
|
const persisted = prepareSettingsForPersistence(settings);
|
|
236
|
-
|
|
237
|
-
this._lastPersistedSettings = this.cloneSettings(this.settings);
|
|
238
|
-
void this.onSettingSaved(settings);
|
|
344
|
+
return { persisted, runtime, patch: savedPatch, passphrase: configurationPassphrase };
|
|
239
345
|
}
|
|
240
|
-
async
|
|
346
|
+
async writePreparedSettings(prepared) {
|
|
347
|
+
this.saveDeviceAndVaultName();
|
|
348
|
+
if (prepared.runtime.P2P_DevicePeerName?.trim()) {
|
|
349
|
+
this.setSmallConfig(SETTING_KEY_P2P_DEVICE_NAME, prepared.runtime.P2P_DevicePeerName.trim());
|
|
350
|
+
}
|
|
351
|
+
await this.saveData(prepared.persisted);
|
|
352
|
+
}
|
|
353
|
+
commitPreparedSettings(prepared) {
|
|
354
|
+
Object.assign(this.settings, prepared.patch);
|
|
355
|
+
this.settings.encryptedCouchDBConnection = prepared.persisted.encryptedCouchDBConnection;
|
|
356
|
+
this.settings.encryptedPassphrase = prepared.persisted.encryptedPassphrase;
|
|
357
|
+
this.settings.encryptedIdDerivationKey = prepared.persisted.encryptedIdDerivationKey;
|
|
358
|
+
this._lastPersistedSettings = prepared.runtime;
|
|
359
|
+
if (prepared.passphrase) this.rememberPassphrase(prepared.runtime, prepared.passphrase);
|
|
360
|
+
void this.onSettingSaved(prepared.persisted);
|
|
361
|
+
}
|
|
362
|
+
async encryptRemoteConfigurationUris(settings, passphrase) {
|
|
241
363
|
const configs = settings.remoteConfigurations || {};
|
|
242
364
|
for (const [id, config] of Object.entries(configs)) {
|
|
243
365
|
if (config.isEncrypted || config.uri.trim() === "") {
|
|
@@ -246,7 +368,7 @@ class SettingService extends ServiceBase {
|
|
|
246
368
|
const managedP2PProfile = this.hasManagedP2PProfileURI(config.uri);
|
|
247
369
|
let encryptedURI;
|
|
248
370
|
try {
|
|
249
|
-
encryptedURI = await this.encryptConfigurationItem(config.uri, settings);
|
|
371
|
+
encryptedURI = await this.encryptConfigurationItem(config.uri, settings, passphrase);
|
|
250
372
|
} catch (error) {
|
|
251
373
|
if (managedP2PProfile) {
|
|
252
374
|
const message = `Failed to encrypt managed P2P remote configuration '${id}'. Settings were not saved.`;
|
|
@@ -288,6 +410,7 @@ class SettingService extends ServiceBase {
|
|
|
288
410
|
return (new URLSearchParams(query).get("managedType") ?? "").trim().length > 0;
|
|
289
411
|
}
|
|
290
412
|
async decryptRemoteConfigurationUris(settings, passphrase) {
|
|
413
|
+
let verified = false;
|
|
291
414
|
const configs = settings.remoteConfigurations || {};
|
|
292
415
|
for (const [id, config] of Object.entries(configs)) {
|
|
293
416
|
if (!config.isEncrypted) {
|
|
@@ -319,7 +442,9 @@ class SettingService extends ServiceBase {
|
|
|
319
442
|
uri: decryptedURI,
|
|
320
443
|
isEncrypted: false
|
|
321
444
|
};
|
|
445
|
+
verified = true;
|
|
322
446
|
}
|
|
447
|
+
return verified;
|
|
323
448
|
}
|
|
324
449
|
/**
|
|
325
450
|
* Event triggered before realising the settings.
|
|
@@ -404,24 +529,37 @@ class SettingService extends ServiceBase {
|
|
|
404
529
|
const methods = {
|
|
405
530
|
"": () => Promise.resolve("*"),
|
|
406
531
|
LOCALSTORAGE: () => Promise.resolve(this.getDeviceLocalConfig("ls-setting-passphrase") ?? false),
|
|
407
|
-
ASK_AT_LAUNCH: () => this.APIService.confirm.askString(
|
|
532
|
+
ASK_AT_LAUNCH: () => this.APIService.confirm.askString(
|
|
533
|
+
this.context.translate("SettingService.ConfigurationPassphraseTitle"),
|
|
534
|
+
"passphrase",
|
|
535
|
+
"",
|
|
536
|
+
true
|
|
537
|
+
)
|
|
408
538
|
};
|
|
409
539
|
const method = settings.configPassphraseStore;
|
|
410
540
|
const methodFunc = method in methods ? methods[method] : methods[""];
|
|
411
541
|
return methodFunc();
|
|
412
542
|
}
|
|
413
|
-
usedPassphrase
|
|
543
|
+
usedPassphrase;
|
|
544
|
+
getSavingPassphrase(settings) {
|
|
545
|
+
if (settings.configPassphraseStore === "ASK_AT_LAUNCH" && this.usedPassphrase?.store === "ASK_AT_LAUNCH") {
|
|
546
|
+
return Promise.resolve(this.usedPassphrase.value);
|
|
547
|
+
}
|
|
548
|
+
return this.getPassphrase(settings);
|
|
549
|
+
}
|
|
550
|
+
rememberPassphrase(settings, passphrase) {
|
|
551
|
+
this.usedPassphrase = { store: settings.configPassphraseStore, value: passphrase };
|
|
552
|
+
}
|
|
414
553
|
/**
|
|
415
554
|
* Clear any used passphrase from memory.
|
|
416
555
|
*/
|
|
417
556
|
clearUsedPassphrase() {
|
|
418
|
-
this.usedPassphrase =
|
|
557
|
+
this.usedPassphrase = void 0;
|
|
419
558
|
}
|
|
420
559
|
async decryptConfigurationItem(encrypted, passphrase) {
|
|
421
560
|
try {
|
|
422
561
|
const dec = await decryptString(encrypted, passphrase + SALT_OF_PASSPHRASE);
|
|
423
562
|
if (dec) {
|
|
424
|
-
this.usedPassphrase = passphrase;
|
|
425
563
|
return dec;
|
|
426
564
|
}
|
|
427
565
|
} catch (ex) {
|
|
@@ -430,18 +568,15 @@ class SettingService extends ServiceBase {
|
|
|
430
568
|
}
|
|
431
569
|
return false;
|
|
432
570
|
}
|
|
433
|
-
async encryptConfigurationItem(src, settings) {
|
|
434
|
-
return this.encryptConfigurationItemWith(src, settings, encryptString);
|
|
571
|
+
async encryptConfigurationItem(src, settings, passphrase) {
|
|
572
|
+
return this.encryptConfigurationItemWith(src, settings, encryptString, passphrase);
|
|
435
573
|
}
|
|
436
|
-
async encryptPlainConfigurationItem(src, settings) {
|
|
437
|
-
return this.encryptConfigurationItemWith(src, settings, encryptWithEphemeralSalt);
|
|
574
|
+
async encryptPlainConfigurationItem(src, settings, passphrase) {
|
|
575
|
+
return this.encryptConfigurationItemWith(src, settings, encryptWithEphemeralSalt, passphrase);
|
|
438
576
|
}
|
|
439
|
-
async encryptConfigurationItemWith(src, settings, encrypt) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
const passphrase = await this.getPassphrase(settings);
|
|
444
|
-
if (passphrase === false) {
|
|
577
|
+
async encryptConfigurationItemWith(src, settings, encrypt, selectedPassphrase) {
|
|
578
|
+
const passphrase = selectedPassphrase ?? await this.getSavingPassphrase(settings);
|
|
579
|
+
if (!passphrase) {
|
|
445
580
|
this._log(
|
|
446
581
|
"Failed to obtain passphrase when saving data.json! Please verify the configuration.",
|
|
447
582
|
LOG_LEVEL_URGENT
|
|
@@ -450,7 +585,7 @@ class SettingService extends ServiceBase {
|
|
|
450
585
|
}
|
|
451
586
|
const dec = await encrypt(src, passphrase + SALT_OF_PASSPHRASE);
|
|
452
587
|
if (dec) {
|
|
453
|
-
this.
|
|
588
|
+
if (selectedPassphrase === void 0) this.rememberPassphrase(settings, passphrase);
|
|
454
589
|
return dec;
|
|
455
590
|
}
|
|
456
591
|
return "";
|
|
@@ -460,19 +595,24 @@ class SettingService extends ServiceBase {
|
|
|
460
595
|
* @param settings The settings to decrypt.
|
|
461
596
|
*/
|
|
462
597
|
async decryptSettings(settings) {
|
|
463
|
-
|
|
464
|
-
configuredIdKey(settings);
|
|
465
|
-
} else if (!settings.encryptedIdDerivationKey) {
|
|
466
|
-
throw new Error("The configured ID derivation key is missing or unavailable.");
|
|
467
|
-
}
|
|
598
|
+
this.validateStoredIdConfiguration(settings);
|
|
468
599
|
const passphrase = await this.getPassphrase(settings);
|
|
600
|
+
const result = await this.decryptSettingsWithPassphrase(settings, passphrase);
|
|
601
|
+
if (result.verified && passphrase) this.rememberPassphrase(settings, passphrase);
|
|
602
|
+
return result.settings;
|
|
603
|
+
}
|
|
604
|
+
async decryptSettingsWithPassphrase(settings, passphrase) {
|
|
605
|
+
let verified = false;
|
|
606
|
+
this.validateStoredIdConfiguration(settings);
|
|
469
607
|
if (settings.idDerivationVersion === 1) {
|
|
470
608
|
if (passphrase === false || passphrase === "") {
|
|
471
|
-
throw new
|
|
609
|
+
throw new ConfigurationPassphraseError(
|
|
610
|
+
"The configured ID derivation key cannot be decrypted without a passphrase."
|
|
611
|
+
);
|
|
472
612
|
}
|
|
473
613
|
const decryptedIdKey = await this.decryptConfigurationItem(settings.encryptedIdDerivationKey, passphrase);
|
|
474
614
|
if (decryptedIdKey === false) {
|
|
475
|
-
throw new
|
|
615
|
+
throw new ConfigurationPassphraseError("The configured ID derivation key could not be decrypted.");
|
|
476
616
|
}
|
|
477
617
|
try {
|
|
478
618
|
configuredIdKey({ ...settings, idDerivationKey: decryptedIdKey });
|
|
@@ -480,6 +620,7 @@ class SettingService extends ServiceBase {
|
|
|
480
620
|
throw new Error("The configured ID derivation key is invalid.");
|
|
481
621
|
}
|
|
482
622
|
settings.idDerivationKey = decryptedIdKey;
|
|
623
|
+
verified = true;
|
|
483
624
|
}
|
|
484
625
|
if (passphrase === false) {
|
|
485
626
|
this._log("No passphrase found for data.json! Verify configuration before syncing.", LOG_LEVEL_URGENT);
|
|
@@ -503,6 +644,8 @@ class SettingService extends ServiceBase {
|
|
|
503
644
|
LOG_LEVEL_URGENT
|
|
504
645
|
);
|
|
505
646
|
clearConnectionSettings(settings);
|
|
647
|
+
} else {
|
|
648
|
+
verified = true;
|
|
506
649
|
}
|
|
507
650
|
}
|
|
508
651
|
if (settings.encrypt && settings.encryptedPassphrase) {
|
|
@@ -510,6 +653,7 @@ class SettingService extends ServiceBase {
|
|
|
510
653
|
const decrypted = await this.decryptConfigurationItem(encrypted, passphrase);
|
|
511
654
|
if (decrypted) {
|
|
512
655
|
settings.passphrase = decrypted;
|
|
656
|
+
verified = true;
|
|
513
657
|
} else {
|
|
514
658
|
this._log(
|
|
515
659
|
"Failed to decrypt passphrase from data.json! Ensure configuration is correct before syncing with remote.",
|
|
@@ -518,9 +662,80 @@ class SettingService extends ServiceBase {
|
|
|
518
662
|
settings.passphrase = "";
|
|
519
663
|
}
|
|
520
664
|
}
|
|
521
|
-
await this.decryptRemoteConfigurationUris(settings, passphrase);
|
|
665
|
+
verified = await this.decryptRemoteConfigurationUris(settings, passphrase) || verified;
|
|
666
|
+
}
|
|
667
|
+
return { settings, verified };
|
|
668
|
+
}
|
|
669
|
+
hasProtectedConfiguration(settings) {
|
|
670
|
+
return !!(settings.encryptedCouchDBConnection || settings.encrypt && settings.encryptedPassphrase || settings.encryptedIdDerivationKey || Object.values(settings.remoteConfigurations || {}).some(
|
|
671
|
+
(config) => config.isEncrypted && !this.isPlainConfigurationUri(config.uri)
|
|
672
|
+
));
|
|
673
|
+
}
|
|
674
|
+
validateStoredIdConfiguration(settings) {
|
|
675
|
+
if (settings.idDerivationVersion !== 1) {
|
|
676
|
+
configuredIdKey(settings);
|
|
677
|
+
} else if (!settings.encryptedIdDerivationKey) {
|
|
678
|
+
throw new Error("The configured ID derivation key is missing or unavailable.");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
isPlainConfigurationUri(uri) {
|
|
682
|
+
try {
|
|
683
|
+
ConnectionStringParser.parse(uri);
|
|
684
|
+
return true;
|
|
685
|
+
} catch {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async loadConfiguration(settings) {
|
|
690
|
+
if (settings.configPassphraseStore !== "LOCALSTORAGE" || !this.hasProtectedConfiguration(settings)) {
|
|
691
|
+
return this.decryptSettings(settings);
|
|
692
|
+
}
|
|
693
|
+
if (settings.idDerivationVersion === 1) {
|
|
694
|
+
const ciphertext = settings.encryptedIdDerivationKey;
|
|
695
|
+
if (![HKDF_SALTED_ENCRYPTED_PREFIX, ENCRYPT_V1_PREFIX_PROBABLY, ENCRYPT_V2_PREFIX, ENCRYPT_V3_PREFIX].some(
|
|
696
|
+
(prefix) => ciphertext?.startsWith(prefix)
|
|
697
|
+
)) {
|
|
698
|
+
throw new Error("The configured ID derivation key is missing or unavailable.");
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
configuredIdKey(settings);
|
|
702
|
+
}
|
|
703
|
+
const previous = this.getDeviceLocalConfig("ls-setting-passphrase");
|
|
704
|
+
let candidate = previous || false;
|
|
705
|
+
let retry = false;
|
|
706
|
+
while (true) {
|
|
707
|
+
if (candidate) {
|
|
708
|
+
try {
|
|
709
|
+
const result = await this.decryptSettingsWithPassphrase(this.cloneSettings(settings), candidate);
|
|
710
|
+
if (result.verified) {
|
|
711
|
+
if (candidate !== previous) {
|
|
712
|
+
try {
|
|
713
|
+
this.setDeviceLocalConfig("ls-setting-passphrase", candidate);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
this.restoreConfigurationPassphrase(previous, error);
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
this.rememberPassphrase(settings, candidate);
|
|
720
|
+
return result.settings;
|
|
721
|
+
}
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (!(error instanceof ConfigurationPassphraseError)) throw error;
|
|
724
|
+
}
|
|
725
|
+
retry = true;
|
|
726
|
+
}
|
|
727
|
+
candidate = await this.APIService.confirm.askString(
|
|
728
|
+
this.context.translate(
|
|
729
|
+
retry ? "SettingService.ConfigurationPassphraseRetryTitle" : "SettingService.ConfigurationPassphraseTitle"
|
|
730
|
+
),
|
|
731
|
+
"passphrase",
|
|
732
|
+
"",
|
|
733
|
+
true
|
|
734
|
+
);
|
|
735
|
+
if (!candidate) {
|
|
736
|
+
throw new Error(this.context.translate("SettingService.ConfigurationPassphraseCancelled"));
|
|
737
|
+
}
|
|
522
738
|
}
|
|
523
|
-
return settings;
|
|
524
739
|
}
|
|
525
740
|
async loadSettings() {
|
|
526
741
|
const prepared = prepareSettingsForLoad(await this.loadData());
|
|
@@ -536,7 +751,7 @@ class SettingService extends ServiceBase {
|
|
|
536
751
|
settings.isConfigured = false;
|
|
537
752
|
}
|
|
538
753
|
}
|
|
539
|
-
this.settings = await this.
|
|
754
|
+
this.settings = await this.loadConfiguration(settings);
|
|
540
755
|
this.onDisplayLanguageChanged?.(this.settings.displayLanguage);
|
|
541
756
|
await this.adjustSettings(this.settings);
|
|
542
757
|
const migratedLegacyRemoteConfigurations = !hadRemoteConfigurations && Object.keys(this.settings.remoteConfigurations ?? {}).length > 0;
|