mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-15 04:30:12 +00:00
Require explicit opt-in for unencrypted content with E2EE
This commit is contained in:
1 parent
79ffe8f6b0
commit
8451102176
26 files changed
+1614
-532
No files matched your search
@@ -351,6 +351,13 @@ export const liveSyncProvisionalEnglishMessages = {
|
||||
"Connection settings": "Connection settings",
|
||||
"Saved connections": "Saved connections",
|
||||
"ID generation": "ID generation",
|
||||
"Accept unencrypted content": "Accept unencrypted content",
|
||||
"Enabled. Third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext; content sent by this plug-in remains encrypted.":
|
||||
"Enabled. Third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext; content sent by this plug-in remains encrypted.",
|
||||
"With this option enabled, third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext. This affects received content only; content sent by this plug-in remains encrypted.":
|
||||
"With this option enabled, third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext. This affects received content only; content sent by this plug-in remains encrypted.",
|
||||
"Accepting unencrypted content will be enabled. Third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext. This affects received content only; content sent by this plug-in remains encrypted.":
|
||||
"Accepting unencrypted content will be enabled. Third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext. This affects received content only; content sent by this plug-in remains encrypted.",
|
||||
"Keep current configuration": "Keep current configuration",
|
||||
"Set an ID key": "Set an ID key",
|
||||
"Current configuration: a saved ID key is used.": "Current configuration: a saved ID key is used.",
|
||||
|
||||
@@ -85,6 +85,7 @@ export function getCouchDBReplicatorConfigurationIdentity(settings: RemoteDBSett
|
||||
settings.useCouchDBCacheControlHeadersOnly === true,
|
||||
settings.disableRequestURI,
|
||||
projectRemoteSecurity(settings),
|
||||
settings.encrypt === true && settings.acceptUnencryptedContent === true,
|
||||
usesEncryptedInternalMetadata(settings),
|
||||
settings.enableCompression,
|
||||
]);
|
||||
|
||||
@@ -130,6 +130,20 @@ describe("active Replicator configuration identity", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("recreates an encrypted CouchDB connection when plaintext receipt becomes enabled", () => {
|
||||
const current = configuredSettings({ encrypt: true });
|
||||
const enabled = { ...current, acceptUnencryptedContent: true };
|
||||
|
||||
expect(getCouchDBReplicatorConfigurationIdentity(enabled)).not.toBe(
|
||||
getCouchDBReplicatorConfigurationIdentity(current)
|
||||
);
|
||||
|
||||
const disabledE2EE = { ...current, encrypt: false, acceptUnencryptedContent: false };
|
||||
expect(
|
||||
getCouchDBReplicatorConfigurationIdentity({ ...disabledE2EE, acceptUnencryptedContent: true })
|
||||
).toBe(getCouchDBReplicatorConfigurationIdentity(disabledE2EE));
|
||||
});
|
||||
|
||||
it("projects only the active CouchDB authentication mode", () => {
|
||||
const basic = configuredSettings({ useJWT: false, jwtKey: "inactive-a" });
|
||||
expect(getCouchDBReplicatorConfigurationIdentity({ ...basic, jwtKey: "inactive-b" })).toBe(
|
||||
|
||||
@@ -16,11 +16,7 @@ export class PluginDialogModal extends Modal {
|
||||
|
||||
override onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.contentEl.setCssStyles({
|
||||
overflow: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
});
|
||||
contentEl.addClass("sls-config-sync-modal-content");
|
||||
this.titleEl.setText("Customization Sync (Beta3)");
|
||||
if (!this.component) {
|
||||
this.component = mount(PluginPane, {
|
||||
|
||||
@@ -192,10 +192,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
contentEl.closest(".modal-container")?.classList.add("livesync-message-box-container");
|
||||
this.titleEl.setText(this.title);
|
||||
const div = contentEl.createDiv();
|
||||
div.setCssStyles({
|
||||
userSelect: "text",
|
||||
webkitUserSelect: "text",
|
||||
});
|
||||
div.addClass("sls-message-box-markdown");
|
||||
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.component);
|
||||
const buttonSetting = new Setting(contentEl);
|
||||
const labelWrapper = contentEl.createDiv();
|
||||
@@ -206,23 +203,18 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
labelEl.addClass("sls-dialogue-note-countdown");
|
||||
if (!this.timeout || !this.timer) {
|
||||
labelWrapper.empty();
|
||||
labelWrapper.setCssStyles({ display: "none" });
|
||||
labelWrapper.addClass("sls-hidden");
|
||||
}
|
||||
|
||||
buttonSetting.infoEl.setCssStyles({ display: "none" });
|
||||
buttonSetting.controlEl.setCssStyles({ flexWrap: "wrap" });
|
||||
buttonSetting.infoEl.addClass("sls-hidden");
|
||||
buttonSetting.controlEl.addClass("sls-message-box-button-control");
|
||||
if (this.wideButton) {
|
||||
buttonSetting.controlEl.setCssStyles({
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexGrow: "1",
|
||||
});
|
||||
buttonSetting.controlEl.addClass("sls-message-box-button-control-wide");
|
||||
}
|
||||
contentEl.addEventListener("click", () => {
|
||||
if (this.timer) {
|
||||
labelWrapper.empty();
|
||||
labelWrapper.setCssStyles({ display: "none" });
|
||||
labelWrapper.addClass("sls-hidden");
|
||||
compatGlobal.clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
this.defaultButtonComponent?.setButtonText(`${this.defaultAction}`);
|
||||
@@ -244,10 +236,7 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
|
||||
btn.setCta();
|
||||
}
|
||||
if (this.wideButton) {
|
||||
btn.buttonEl.setCssStyles({
|
||||
flexGrow: "1",
|
||||
width: "100%",
|
||||
});
|
||||
btn.buttonEl.addClass("sls-message-box-wide-button");
|
||||
}
|
||||
return btn;
|
||||
});
|
||||
|
||||
@@ -55,11 +55,13 @@ describe("ModuleObsidianSettingsAsMarkdown", () => {
|
||||
"respects the sharing option for new and existing files (existing: %s)",
|
||||
async (existingFile) => {
|
||||
const { module, settings, files, storageAccess } = createHarness(false, existingFile);
|
||||
(settings as any).acceptUnencryptedContent = true;
|
||||
settings.P2P_managedType = CLOUDFLARE_TURN_TYPE;
|
||||
await module.saveSettingToMarkdown(settings.settingSyncFile);
|
||||
const content = files.get(settings.settingSyncFile)!;
|
||||
expect(storageAccess.writeFileAuto).toHaveBeenCalled();
|
||||
const written = parse(module.extractSettingFromWholeText(content).body);
|
||||
expect(written.acceptUnencryptedContent).toBe(true);
|
||||
expect(written).not.toHaveProperty("couchDB_PASSWORD");
|
||||
expect(content).not.toContain(settings.encryptedCouchDBConnection);
|
||||
expect(Object.keys(written).filter((key) => ["accessKey", "secretKey"].includes(key))).toEqual([]);
|
||||
@@ -76,6 +78,7 @@ describe("ModuleObsidianSettingsAsMarkdown", () => {
|
||||
const { module, settings, files } = createHarness(true);
|
||||
settings.couchDB_USER = "synthetic-export-couch-user";
|
||||
settings.passphrase = "synthetic-export-passphrase";
|
||||
(settings as any).acceptUnencryptedContent = true;
|
||||
settings.idDerivationKey = "synthetic-export-id-key";
|
||||
settings.jwtKey = "synthetic-export-jwt-key";
|
||||
settings.jwtKid = "synthetic-export-jwt-kid";
|
||||
@@ -138,6 +141,7 @@ describe("ModuleObsidianSettingsAsMarkdown", () => {
|
||||
expect(written.couchDB_USER).toBe(settings.couchDB_USER);
|
||||
expect(written.couchDB_PASSWORD).toBe(settings.couchDB_PASSWORD);
|
||||
expect(written.passphrase).toBe(settings.passphrase);
|
||||
expect(written.acceptUnencryptedContent).toBe(true);
|
||||
expect(written.idDerivationKey).toBe(settings.idDerivationKey);
|
||||
expect(written.jwtKey).toBe(settings.jwtKey);
|
||||
expect(written.jwtKid).toBe(settings.jwtKid);
|
||||
@@ -231,6 +235,7 @@ describe("ModuleObsidianSettingsAsMarkdown", () => {
|
||||
couchDB_PASSWORD: "synthetic-local-password",
|
||||
couchDB_CustomHeaders: "X-Couch-Local: synthetic-couch-header",
|
||||
bucketCustomHeaders: "X-Bucket-Local: synthetic-bucket-header",
|
||||
acceptUnencryptedContent: true,
|
||||
jwtKey: "synthetic-local-jwt-key",
|
||||
jwtKid: "synthetic-local-kid",
|
||||
jwtSub: "synthetic-local-subject",
|
||||
@@ -305,6 +310,7 @@ describe("ModuleObsidianSettingsAsMarkdown", () => {
|
||||
|
||||
const appliedSettings = applyExternalSettings.mock.calls[0]![0];
|
||||
expect(appliedSettings.P2P_AutoStart).toBe(true);
|
||||
expect(appliedSettings.acceptUnencryptedContent).toBe(true);
|
||||
expect(appliedSettings.accessKey).toBe(currentSettings.accessKey);
|
||||
expect(appliedSettings.secretKey).toBe(currentSettings.secretKey);
|
||||
expect(appliedSettings.couchDB_USER).toBe(currentSettings.couchDB_USER);
|
||||
@@ -320,4 +326,53 @@ describe("ModuleObsidianSettingsAsMarkdown", () => {
|
||||
expect(appliedSettings.idDerivationVersion).toBe(currentSettings.idDerivationVersion);
|
||||
expect(appliedSettings.idDerivationKey).toBe(currentSettings.idDerivationKey);
|
||||
});
|
||||
|
||||
it("explains the plaintext receipt risk in the existing settings application dialogue", async () => {
|
||||
const filename = "LiveSync/plaintext-policy.md";
|
||||
const currentSettings = Object.assign(structuredClone(DEFAULT_SETTINGS), {
|
||||
encrypt: true,
|
||||
settingSyncFile: filename,
|
||||
acceptUnencryptedContent: false,
|
||||
});
|
||||
const markdownSettings = { encrypt: true, acceptUnencryptedContent: true };
|
||||
const document = "Synthetic note\n````yaml:livesync-setting\n" + stringify(markdownSettings) + "\n````\n";
|
||||
const files = new Map([[filename, document]]);
|
||||
const storageAccess = {
|
||||
isExists: vi.fn(async (path: string) => files.has(path)),
|
||||
readFileText: vi.fn(async (path: string) => files.get(path) ?? ""),
|
||||
};
|
||||
let openApplyDialogue: ((anchor: HTMLAnchorElement) => void) | undefined;
|
||||
let chooseApply: (() => void) | undefined;
|
||||
const askSelectStringDialogue = vi.fn(async (_message: string, _choices: string[]) => "Cancel");
|
||||
const module = Object.assign(Object.create(ModuleObsidianSettingsAsMarkdown.prototype), {
|
||||
core: {
|
||||
settings: currentSettings,
|
||||
storageAccess,
|
||||
confirm: {
|
||||
askInPopup: vi.fn((_key: string, _text: string, callback: (anchor: HTMLAnchorElement) => void) => {
|
||||
openApplyDialogue = callback;
|
||||
}),
|
||||
askSelectStringDialogue,
|
||||
},
|
||||
},
|
||||
_log: vi.fn(),
|
||||
}) as ModuleObsidianSettingsAsMarkdown;
|
||||
|
||||
await module.checkAndApplySettingFromMarkdown(filename, false);
|
||||
openApplyDialogue?.({
|
||||
set text(_value: string) {},
|
||||
addEventListener: (_type: string, listener: EventListenerOrEventListenerObject) => {
|
||||
chooseApply = listener as () => void;
|
||||
},
|
||||
} as HTMLAnchorElement);
|
||||
chooseApply?.();
|
||||
|
||||
await vi.waitFor(() => expect(askSelectStringDialogue).toHaveBeenCalledOnce());
|
||||
expect(askSelectStringDialogue.mock.calls[0]![0]).toContain(
|
||||
"Anyone with database write access can inject plaintext"
|
||||
);
|
||||
expect(askSelectStringDialogue.mock.calls[0]![0]).toContain(
|
||||
"Third-party tools can add content without the E2EE passphrase"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -159,8 +159,14 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractModule {
|
||||
const APPLY_AND_REBUILD = "Apply settings and restart obsidian with red_flag_rebuild.md";
|
||||
const APPLY_AND_FETCH = "Apply settings and restart obsidian with red_flag_fetch.md";
|
||||
const CANCEL = "Cancel";
|
||||
const enablesUnencryptedContent =
|
||||
settingToApply.encrypt === true &&
|
||||
settingToApply.acceptUnencryptedContent === true &&
|
||||
!(this.settings.encrypt === true && this.settings.acceptUnencryptedContent === true);
|
||||
const result = await this.core.confirm.askSelectStringDialogue(
|
||||
"Ready for apply the setting.",
|
||||
enablesUnencryptedContent
|
||||
? "Accepting unencrypted content will be enabled. Third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext. Content sent by this plug-in remains encrypted."
|
||||
: "Ready for apply the setting.",
|
||||
[APPLY_AND_RESTART, APPLY_ONLY, APPLY_AND_FETCH, APPLY_AND_REBUILD, CANCEL],
|
||||
{ defaultAction: APPLY_AND_RESTART }
|
||||
);
|
||||
|
||||
@@ -56,11 +56,9 @@ export function paneHelp(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
||||
lifetimeComponent
|
||||
);
|
||||
if (pageDisposed) return;
|
||||
troubleShootEl.querySelector<HTMLAnchorElement>(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({
|
||||
position: "sticky",
|
||||
top: "-1em",
|
||||
backgroundColor: "var(--modal-background)",
|
||||
});
|
||||
troubleShootEl
|
||||
.querySelector<HTMLAnchorElement>(".sls-troubleshoot-anchor")
|
||||
?.parentElement?.addClass("sls-troubleshoot-sticky");
|
||||
troubleShootEl.querySelectorAll<HTMLAnchorElement>("a.internal-link").forEach((anchorEl) => {
|
||||
anchorEl.addEventListener("click", (evt) => {
|
||||
fireAndForget(async () => {
|
||||
@@ -77,7 +75,7 @@ export function paneHelp(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
||||
uri.substring(1).toLowerCase()
|
||||
);
|
||||
if (target) {
|
||||
target.setCssStyles({ scrollMargin: "3em" });
|
||||
target.addClass("sls-troubleshoot-heading-target");
|
||||
target.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
@@ -86,7 +84,7 @@ export function paneHelp(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
||||
} else {
|
||||
evt.preventDefault();
|
||||
await loadMarkdownPage(uri, basePath);
|
||||
troubleShootEl.setCssStyles({ scrollMargin: "1em" });
|
||||
troubleShootEl.addClass("sls-troubleshoot-scroll-target");
|
||||
troubleShootEl.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
|
||||
@@ -145,9 +145,13 @@ export function paneRemoteConfig(
|
||||
if (applied) {
|
||||
this.editingSettings.encryptInternalMetadata =
|
||||
this.core.settings.encryptInternalMetadata;
|
||||
this.editingSettings.acceptUnencryptedContent =
|
||||
this.core.settings.acceptUnencryptedContent;
|
||||
if (this.initialSettings) {
|
||||
this.initialSettings.encryptInternalMetadata =
|
||||
this.core.settings.encryptInternalMetadata;
|
||||
this.initialSettings.acceptUnencryptedContent =
|
||||
this.core.settings.acceptUnencryptedContent;
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -258,4 +258,46 @@ describe("paneRemoteConfig", () => {
|
||||
expect(host.initialSettings.idDerivationKey).toBe(nextIdKey);
|
||||
expect(host.requestUpdate).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("copies the applied plaintext receipt preference into both dialogue buffers", async () => {
|
||||
const originalSettings = Object.assign(
|
||||
{
|
||||
encrypt: true,
|
||||
passphrase: "passphrase",
|
||||
E2EEAlgorithm: "v2",
|
||||
usePathObfuscation: true,
|
||||
encryptInternalMetadata: false,
|
||||
remoteConfigurations: {},
|
||||
},
|
||||
{ acceptUnencryptedContent: false }
|
||||
);
|
||||
const setupManager = {
|
||||
onlyE2EEConfiguration: vi.fn(() => {
|
||||
host.core.settings.acceptUnencryptedContent = true;
|
||||
return Promise.resolve(true);
|
||||
}),
|
||||
};
|
||||
const host = {
|
||||
editingSettings: { ...originalSettings },
|
||||
initialSettings: { ...originalSettings },
|
||||
core: {
|
||||
settings: { ...originalSettings },
|
||||
getModule: vi.fn(() => setupManager),
|
||||
},
|
||||
lifetimeComponent: { register: vi.fn() },
|
||||
requestUpdate: vi.fn(),
|
||||
};
|
||||
const addPanel = vi.fn((_parent: HTMLElement, heading: string) => ({
|
||||
then(callback: (paneEl: HTMLElement) => void) {
|
||||
if (heading === "E2EE Configuration") callback(createPanelElement());
|
||||
},
|
||||
}));
|
||||
|
||||
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||
await runtime.clickHandlers.find((handler) => handler.setting === "Configure E2EE")!.run();
|
||||
|
||||
expect(host.editingSettings.acceptUnencryptedContent).toBe(true);
|
||||
expect(host.initialSettings.acceptUnencryptedContent).toBe(true);
|
||||
expect(host.requestUpdate).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { LOG_LEVEL_NOTICE, Logger } from "octagonal-wheels/common/logger";
|
||||
import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
||||
import type PouchDB from "pouchdb-core";
|
||||
import type {} from "pouchdb-replication";
|
||||
import { $msg as translateMessage } from "@/common/translation";
|
||||
|
||||
/**
|
||||
* Generates a summary of P2P configuration settings
|
||||
@@ -56,7 +57,13 @@ export function getCouchDBConfigSummary(setting: ObsidianLiveSyncSettings, showA
|
||||
*/
|
||||
export function getE2EEConfigSummary(setting: ObsidianLiveSyncSettings, showAdvanced = false) {
|
||||
const settingTable: Partial<ObsidianLiveSyncSettings> = pickEncryptionSettings(setting);
|
||||
return getSummaryFromPartialSettings(settingTable, showAdvanced);
|
||||
const summary = getSummaryFromPartialSettings(settingTable, showAdvanced);
|
||||
if (setting.encrypt === true && setting.acceptUnencryptedContent === true) {
|
||||
summary[translateMessage("Accept unencrypted content")] = translateMessage(
|
||||
"Enabled. Third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext; content sent by this plug-in remains encrypted."
|
||||
);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import { getE2EEConfigSummary } from "./settingUtils";
|
||||
|
||||
describe("getE2EEConfigSummary", () => {
|
||||
it("shows the active plaintext receipt policy and its effect", () => {
|
||||
const settings = Object.assign(createNewVaultSettings(), {
|
||||
encrypt: true,
|
||||
acceptUnencryptedContent: true,
|
||||
});
|
||||
|
||||
expect(getE2EEConfigSummary(settings)["Accept unencrypted content"]).toContain(
|
||||
"Third-party tools can add content without the E2EE passphrase"
|
||||
);
|
||||
expect(getE2EEConfigSummary(settings)["Accept unencrypted content"]).toContain(
|
||||
"database write access can inject plaintext"
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[false, true],
|
||||
[true, false],
|
||||
])("omits the plaintext receipt policy when E2EE is off or the preference is false", (encrypt, accept) => {
|
||||
const settings = Object.assign(createNewVaultSettings(), {
|
||||
encrypt,
|
||||
acceptUnencryptedContent: accept,
|
||||
});
|
||||
|
||||
expect(getE2EEConfigSummary(settings)).not.toHaveProperty("Accept unencrypted content");
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
REMOTE_COUCHDB,
|
||||
REMOTE_MINIO,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { normaliseAcceptUnencryptedContent } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import { createNewVaultSettings } from "@/common/newVaultSettings";
|
||||
import { upsertRemoteConfigurationInPlace } from "@vrtmrz/livesync-commonlib/remote-configurations";
|
||||
import { isObjectDifferent } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
@@ -42,6 +43,7 @@ import type {
|
||||
UseSetupURIResultType,
|
||||
ApplySettingsInitialisationResultType,
|
||||
ApplySettingsInitialisationInitialData,
|
||||
OutroApplyInitialData,
|
||||
} from "./SetupWizard/dialogs/setupDialogTypes.ts";
|
||||
import {
|
||||
applySettingsAndFetchOnActivation,
|
||||
@@ -60,7 +62,7 @@ function copySettingsForRemoteProfileUpdate(settings: ObsidianLiveSyncSettings):
|
||||
};
|
||||
}
|
||||
|
||||
function normaliseImportedIdentitySettings(settings: ObsidianLiveSyncSettings): ObsidianLiveSyncSettings {
|
||||
function normaliseCompleteImportedSettings(settings: ObsidianLiveSyncSettings): ObsidianLiveSyncSettings {
|
||||
// Setup URIs are complete imports even when their encoder omitted default-valued fields.
|
||||
// Fill each missing half so a receiving device cannot supply the unrelated saved key.
|
||||
return {
|
||||
@@ -74,6 +76,8 @@ function normaliseImportedIdentitySettings(settings: ObsidianLiveSyncSettings):
|
||||
idDerivationKey: Object.prototype.hasOwnProperty.call(settings, "idDerivationKey")
|
||||
? settings.idDerivationKey
|
||||
: "",
|
||||
// Do not inherit a destination's preference when a complete import omits this newer field.
|
||||
acceptUnencryptedContent: normaliseAcceptUnencryptedContent(settings.acceptUnencryptedContent),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -238,7 +242,7 @@ export class SetupManager extends AbstractModule {
|
||||
return false;
|
||||
}
|
||||
this._log("Setup URI dialog closed.", LOG_LEVEL_VERBOSE);
|
||||
return await this.onConfirmApplySettingsFromWizard(normaliseImportedIdentitySettings(newSetting), userMode);
|
||||
return await this.onConfirmApplySettingsFromWizard(normaliseCompleteImportedSettings(newSetting), userMode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,16 +387,25 @@ export class SetupManager extends AbstractModule {
|
||||
this._log("E2EE configuration cancelled.", LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
}
|
||||
const onlyInternalMetadataPreferenceChanged =
|
||||
currentSetting.encryptInternalMetadata !== e2eeConf.encryptInternalMetadata &&
|
||||
const internalMetadataPreferenceChanged =
|
||||
currentSetting.encryptInternalMetadata !== e2eeConf.encryptInternalMetadata;
|
||||
const plaintextReceiptPreferenceChanged =
|
||||
normaliseAcceptUnencryptedContent(currentSetting.acceptUnencryptedContent) !==
|
||||
normaliseAcceptUnencryptedContent(e2eeConf.acceptUnencryptedContent);
|
||||
const onlyE2EEPreferenceChanges =
|
||||
(internalMetadataPreferenceChanged || plaintextReceiptPreferenceChanged) &&
|
||||
currentSetting.encrypt === e2eeConf.encrypt &&
|
||||
currentSetting.passphrase === e2eeConf.passphrase &&
|
||||
currentSetting.E2EEAlgorithm === e2eeConf.E2EEAlgorithm &&
|
||||
currentSetting.usePathObfuscation === e2eeConf.usePathObfuscation &&
|
||||
currentSetting.idDerivationVersion === e2eeConf.idDerivationVersion &&
|
||||
currentSetting.idDerivationKey === e2eeConf.idDerivationKey;
|
||||
if (userMode === UserMode.Update && onlyInternalMetadataPreferenceChanged) {
|
||||
if (e2eeConf.encryptInternalMetadata && currentSetting.remoteType === REMOTE_COUCHDB) {
|
||||
if (userMode === UserMode.Update && onlyE2EEPreferenceChanges) {
|
||||
if (
|
||||
internalMetadataPreferenceChanged &&
|
||||
e2eeConf.encryptInternalMetadata &&
|
||||
currentSetting.remoteType === REMOTE_COUCHDB
|
||||
) {
|
||||
const proceed = "Enable without rebuilding — update every other device first";
|
||||
const choice = await this.core.confirm.askSelectStringDialogue(
|
||||
"A manual remote Rebuild is strongly recommended to protect existing file properties. " +
|
||||
@@ -404,10 +417,16 @@ export class SetupManager extends AbstractModule {
|
||||
);
|
||||
if (choice !== proceed) return false;
|
||||
}
|
||||
await this.services.setting.applyPartial(
|
||||
{ encryptInternalMetadata: e2eeConf.encryptInternalMetadata },
|
||||
true
|
||||
);
|
||||
const partial: Partial<ObsidianLiveSyncSettings> = {};
|
||||
if (internalMetadataPreferenceChanged) {
|
||||
partial.encryptInternalMetadata = e2eeConf.encryptInternalMetadata;
|
||||
}
|
||||
if (plaintextReceiptPreferenceChanged) {
|
||||
partial.acceptUnencryptedContent = normaliseAcceptUnencryptedContent(
|
||||
e2eeConf.acceptUnencryptedContent
|
||||
);
|
||||
}
|
||||
await this.services.setting.applyPartial(partial, true);
|
||||
return true;
|
||||
}
|
||||
const newSetting = {
|
||||
@@ -480,6 +499,10 @@ export class SetupManager extends AbstractModule {
|
||||
...this.settings,
|
||||
...newConf,
|
||||
});
|
||||
const enableUnencryptedContent =
|
||||
newConf.encrypt === true &&
|
||||
normaliseAcceptUnencryptedContent(newConf.acceptUnencryptedContent) &&
|
||||
!(this.settings.encrypt === true && normaliseAcceptUnencryptedContent(this.settings.acceptUnencryptedContent));
|
||||
let userMode = _userMode;
|
||||
if (userMode === UserMode.Unknown) {
|
||||
if (isObjectDifferent(this.settings, newConf, true) === false) {
|
||||
@@ -505,8 +528,10 @@ export class SetupManager extends AbstractModule {
|
||||
if (applied) this._log("Settings from wizard applied.", LOG_LEVEL_NOTICE);
|
||||
return applied;
|
||||
} else {
|
||||
const userModeResult =
|
||||
await this.dialogManager.openWithExplicitCancel<OutroAskUserModeResultType>(OutroAskUserMode);
|
||||
const userModeResult = await this.dialogManager.openWithExplicitCancel<
|
||||
OutroAskUserModeResultType,
|
||||
OutroApplyInitialData
|
||||
>(OutroAskUserMode, { isP2P: isP2PMainRemote(newConf), enableUnencryptedContent });
|
||||
if (userModeResult === "new-user") {
|
||||
userMode = UserMode.NewUser;
|
||||
} else if (userModeResult === "existing-user") {
|
||||
@@ -525,8 +550,8 @@ export class SetupManager extends AbstractModule {
|
||||
const component = userMode === UserMode.NewUser ? OutroNewUser : OutroExistingUser;
|
||||
const confirm = await this.dialogManager.openWithExplicitCancel<
|
||||
OutroNewUserResultType | OutroExistingUserResultType,
|
||||
{ isP2P: boolean }
|
||||
>(component, { isP2P: isP2PMainRemote(newConf) });
|
||||
OutroApplyInitialData
|
||||
>(component, { isP2P: isP2PMainRemote(newConf), enableUnencryptedContent });
|
||||
if (confirm === "cancelled") {
|
||||
this._log("User cancelled applying settings from wizard..", LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
@@ -572,7 +597,7 @@ export class SetupManager extends AbstractModule {
|
||||
async decodeQR(qr: string) {
|
||||
let newSettings: ObsidianLiveSyncSettings;
|
||||
try {
|
||||
newSettings = normaliseImportedIdentitySettings(decodeSettingsFromQRCodeData(qr));
|
||||
newSettings = normaliseCompleteImportedSettings(decodeSettingsFromQRCodeData(qr));
|
||||
} catch {
|
||||
this._log("The QR configuration could not be decoded or contains unsupported settings.", LOG_LEVEL_NOTICE);
|
||||
return false;
|
||||
|
||||
@@ -301,7 +301,8 @@ describe("SetupManager", () => {
|
||||
pathNormalizationVersion: 1,
|
||||
idDerivationVersion: 1,
|
||||
idDerivationKey: savedKey,
|
||||
};
|
||||
acceptUnencryptedContent: true,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
const imported = {
|
||||
...createLegacyRemoteSetting(),
|
||||
isConfigured: true,
|
||||
@@ -318,6 +319,7 @@ describe("SetupManager", () => {
|
||||
expect(mergedSettings.pathNormalizationVersion).toBe(0);
|
||||
expect(mergedSettings.idDerivationVersion).toBe(0);
|
||||
expect(mergedSettings.idDerivationKey).toBe("");
|
||||
expect((mergedSettings as any).acceptUnencryptedContent).toBe(false);
|
||||
expect(setting.currentSettings().idDerivationKey).toBe(savedKey);
|
||||
});
|
||||
|
||||
@@ -329,7 +331,8 @@ describe("SetupManager", () => {
|
||||
isConfigured: true,
|
||||
idDerivationVersion: 1,
|
||||
idDerivationKey: savedKey,
|
||||
};
|
||||
acceptUnencryptedContent: true,
|
||||
} as ObsidianLiveSyncSettings;
|
||||
const imported = {
|
||||
...createLegacyRemoteSetting(),
|
||||
isConfigured: true,
|
||||
@@ -383,9 +386,99 @@ describe("SetupManager", () => {
|
||||
const mergedSettings = vi.mocked(setting.adjustSettings).mock.calls[0][0];
|
||||
expect(mergedSettings.idDerivationVersion).toBe(0);
|
||||
expect(mergedSettings.idDerivationKey).toBe("");
|
||||
expect((mergedSettings as any).acceptUnencryptedContent).toBe(false);
|
||||
expect(setting.currentSettings().idDerivationKey).toBe(savedKey);
|
||||
});
|
||||
|
||||
it("normalises a malformed plaintext receipt value in a Setup URI to false", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
setting.settings = Object.assign(createLegacyRemoteSetting(), { acceptUnencryptedContent: true });
|
||||
const imported = Object.assign(createLegacyRemoteSetting(), { acceptUnencryptedContent: "true" });
|
||||
vi.spyOn(setting, "adjustSettings").mockImplementation((settings) => Promise.resolve(settings));
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce(imported).mockResolvedValueOnce("cancelled");
|
||||
|
||||
await manager.onUseSetupURI(UserMode.Unknown, "mock-config://invalid-plaintext-policy");
|
||||
|
||||
expect((vi.mocked(setting.adjustSettings).mock.calls[0]![0] as any).acceptUnencryptedContent).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["setup-uri", "qr"] as const)("preserves an explicit plaintext receipt choice from %s", async (source) => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
const imported = Object.assign(createLegacyRemoteSetting(), { acceptUnencryptedContent: true });
|
||||
vi.spyOn(setting, "adjustSettings").mockImplementation((settings) => Promise.resolve(settings));
|
||||
if (source === "setup-uri") {
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce(imported).mockResolvedValueOnce("cancelled");
|
||||
await manager.onUseSetupURI(UserMode.Unknown, "mock-config://enabled-plaintext-policy");
|
||||
} else {
|
||||
vi.mocked(decodeSettingsFromQRCodeData).mockReturnValue(imported);
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled");
|
||||
await manager.decodeQR("qr-data");
|
||||
}
|
||||
|
||||
expect((vi.mocked(setting.adjustSettings).mock.calls[0]![0] as any).acceptUnencryptedContent).toBe(true);
|
||||
});
|
||||
|
||||
it("warns in the existing application confirmation when an import enables plaintext receipt", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
setting.settings = Object.assign(setting.currentSettings(), {
|
||||
encrypt: true,
|
||||
acceptUnencryptedContent: false,
|
||||
});
|
||||
const imported = { ...setting.currentSettings(), acceptUnencryptedContent: true };
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("cancelled");
|
||||
|
||||
await expect(manager.onConfirmApplySettingsFromWizard(imported, UserMode.ExistingUser)).resolves.toBe(false);
|
||||
|
||||
expect(dialogManager.openWithExplicitCancel).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.objectContaining({ enableUnencryptedContent: true })
|
||||
);
|
||||
});
|
||||
|
||||
it("applies a preference-only import to an existing vault without Fetch or Rebuild", async () => {
|
||||
const { manager, setting, dialogManager, core } = createSetupManager();
|
||||
setting.settings = Object.assign(setting.currentSettings(), {
|
||||
isConfigured: true,
|
||||
encrypt: true,
|
||||
acceptUnencryptedContent: false,
|
||||
});
|
||||
const imported = { ...setting.currentSettings(), acceptUnencryptedContent: true };
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("compatible-existing-user");
|
||||
|
||||
await expect(manager.onConfirmApplySettingsFromWizard(imported, UserMode.Unknown)).resolves.toBe(true);
|
||||
|
||||
expect((setting.currentSettings() as any).acceptUnencryptedContent).toBe(true);
|
||||
expect(core.rebuilder.scheduleFetch).not.toHaveBeenCalled();
|
||||
expect(core.rebuilder.scheduleRebuild).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes the warning through the mode choice before a compatible or new-user application", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
setting.settings = Object.assign(setting.currentSettings(), {
|
||||
encrypt: true,
|
||||
acceptUnencryptedContent: false,
|
||||
});
|
||||
const imported = {
|
||||
...setting.currentSettings(),
|
||||
couchDB_DBNAME: "imported-vault",
|
||||
acceptUnencryptedContent: true,
|
||||
};
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("new-user").mockResolvedValueOnce("cancelled");
|
||||
|
||||
await expect(manager.onConfirmApplySettingsFromWizard(imported, UserMode.Unknown)).resolves.toBe(false);
|
||||
|
||||
expect(dialogManager.openWithExplicitCancel).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.anything(),
|
||||
expect.objectContaining({ enableUnencryptedContent: true })
|
||||
);
|
||||
expect(dialogManager.openWithExplicitCancel).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.anything(),
|
||||
expect.objectContaining({ enableUnencryptedContent: true })
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid QR settings before applying them", async () => {
|
||||
const { manager, setting } = createSetupManager();
|
||||
vi.mocked(decodeSettingsFromQRCodeData).mockImplementationOnce(() => {
|
||||
@@ -481,6 +574,7 @@ describe("SetupManager", () => {
|
||||
|
||||
expect(dialogManager.openWithExplicitCancel).toHaveBeenCalledWith(expect.anything(), {
|
||||
isP2P: true,
|
||||
enableUnencryptedContent: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -922,3 +1016,129 @@ describe("internal Metadata configuration", () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("plaintext receipt configuration", () => {
|
||||
it("retains the saved preference when encryption is turned off", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
const current = Object.assign(setting.currentSettings(), {
|
||||
encrypt: true,
|
||||
acceptUnencryptedContent: true,
|
||||
});
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValue({ ...current, encrypt: false });
|
||||
const confirm = vi.spyOn(manager, "onConfirmApplySettingsFromWizard").mockResolvedValue(true);
|
||||
|
||||
await expect(manager.onlyE2EEConfiguration(UserMode.Update, current)).resolves.toBe(true);
|
||||
|
||||
expect(confirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ encrypt: false, acceptUnencryptedContent: true }),
|
||||
UserMode.Update
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the normal setup path when the E2EE passphrase changes with plaintext receipt", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
const current = Object.assign(setting.currentSettings(), {
|
||||
encrypt: true,
|
||||
passphrase: "old-passphrase",
|
||||
acceptUnencryptedContent: false,
|
||||
});
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValue({
|
||||
...current,
|
||||
passphrase: "new-passphrase",
|
||||
acceptUnencryptedContent: true,
|
||||
});
|
||||
const apply = vi.spyOn(setting, "applyPartial");
|
||||
const confirm = vi.spyOn(manager, "onConfirmApplySettingsFromWizard").mockResolvedValue(true);
|
||||
|
||||
await expect(manager.onlyE2EEConfiguration(UserMode.Update, current)).resolves.toBe(true);
|
||||
|
||||
expect(apply).not.toHaveBeenCalled();
|
||||
expect(confirm).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ passphrase: "new-passphrase", acceptUnencryptedContent: true }),
|
||||
UserMode.Update
|
||||
);
|
||||
});
|
||||
|
||||
it("applies a plaintext receipt preference alone without setup initialisation", async () => {
|
||||
const { manager, setting, dialogManager, core } = createSetupManager();
|
||||
const current = Object.assign(setting.currentSettings(), {
|
||||
encrypt: true,
|
||||
acceptUnencryptedContent: false,
|
||||
});
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValue({ ...current, acceptUnencryptedContent: true });
|
||||
const apply = vi.spyOn(setting, "applyPartial").mockImplementation(async (partial) => {
|
||||
Object.assign(setting.settings, partial);
|
||||
});
|
||||
const confirm = vi.spyOn(manager, "onConfirmApplySettingsFromWizard");
|
||||
|
||||
await expect(manager.onlyE2EEConfiguration(UserMode.Update, current)).resolves.toBe(true);
|
||||
|
||||
expect(apply).toHaveBeenCalledWith(expect.objectContaining({ acceptUnencryptedContent: true }), true);
|
||||
expect(confirm).not.toHaveBeenCalled();
|
||||
expect((setting.currentSettings() as any).acceptUnencryptedContent).toBe(true);
|
||||
expect(core.rebuilder.scheduleRebuild).not.toHaveBeenCalled();
|
||||
expect(core.rebuilder.scheduleFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not show the Metadata Rebuild warning when only plaintext receipt changes", async () => {
|
||||
const { manager, setting, dialogManager, core } = createSetupManager();
|
||||
const current = Object.assign(setting.currentSettings(), {
|
||||
encrypt: true,
|
||||
encryptInternalMetadata: true,
|
||||
acceptUnencryptedContent: false,
|
||||
remoteType: REMOTE_COUCHDB,
|
||||
});
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValue({ ...current, acceptUnencryptedContent: true });
|
||||
const ask = vi.fn(async (_message: string, choices: string[]) => choices[0]);
|
||||
core.confirm = { askSelectStringDialogue: ask };
|
||||
const apply = vi.spyOn(setting, "applyPartial").mockResolvedValue(undefined);
|
||||
|
||||
await expect(manager.onlyE2EEConfiguration(UserMode.Update, current)).resolves.toBe(true);
|
||||
|
||||
expect(ask).not.toHaveBeenCalled();
|
||||
expect(apply).toHaveBeenCalledWith(expect.objectContaining({ acceptUnencryptedContent: true }), true);
|
||||
expect(core.rebuilder.scheduleRebuild).not.toHaveBeenCalled();
|
||||
expect(core.rebuilder.scheduleFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"retains the internal Metadata warning when both preferences change (accepted: %s)",
|
||||
async (accept) => {
|
||||
const { manager, setting, dialogManager, core } = createSetupManager();
|
||||
const current = Object.assign(setting.currentSettings(), {
|
||||
encrypt: true,
|
||||
encryptInternalMetadata: false,
|
||||
acceptUnencryptedContent: false,
|
||||
remoteType: REMOTE_COUCHDB,
|
||||
});
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValue({
|
||||
...current,
|
||||
encryptInternalMetadata: true,
|
||||
acceptUnencryptedContent: true,
|
||||
});
|
||||
const ask = vi.fn(async (_message: string, choices: string[]) => (accept ? choices[0] : "Cancel"));
|
||||
core.confirm = { askSelectStringDialogue: ask };
|
||||
const apply = vi.spyOn(setting, "applyPartial").mockImplementation(async (partial) => {
|
||||
Object.assign(setting.settings, partial);
|
||||
});
|
||||
|
||||
await expect(manager.onlyE2EEConfiguration(UserMode.Update, current)).resolves.toBe(accept);
|
||||
|
||||
expect(ask).toHaveBeenCalledOnce();
|
||||
expect(ask.mock.calls[0]![0]).toContain("currently running LiveSync");
|
||||
expect(apply).toHaveBeenCalledTimes(accept ? 1 : 0);
|
||||
if (accept) {
|
||||
expect(apply.mock.calls[0]![0]).toEqual(
|
||||
expect.objectContaining({
|
||||
encryptInternalMetadata: true,
|
||||
acceptUnencryptedContent: true,
|
||||
})
|
||||
);
|
||||
expect(setting.currentSettings().encryptInternalMetadata).toBe(true);
|
||||
expect((setting.currentSettings() as any).acceptUnencryptedContent).toBe(true);
|
||||
}
|
||||
expect(core.rebuilder.scheduleRebuild).not.toHaveBeenCalled();
|
||||
expect(core.rebuilder.scheduleFetch).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
import Decision from "@/modules/services/LiveSyncUI/components/Decision.svelte";
|
||||
import Question from "@/modules/services/LiveSyncUI/components/Question.svelte";
|
||||
import Option from "@/modules/services/LiveSyncUI/components/Option.svelte";
|
||||
import PlaintextReceiptWarning from "./PlaintextReceiptWarning.svelte";
|
||||
import Instruction from "@/modules/services/LiveSyncUI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/modules/services/LiveSyncUI/components/UserDecisions.svelte";
|
||||
import InfoNote from "@/modules/services/LiveSyncUI/components/InfoNote.svelte";
|
||||
@@ -13,13 +14,16 @@
|
||||
TYPE_EXISTING,
|
||||
TYPE_NEW,
|
||||
TYPE_COMPATIBLE_EXISTING,
|
||||
type OutroApplyInitialData,
|
||||
} from "./setupDialogTypes";
|
||||
import { $msg as translateMessage } from "@/common/translation";
|
||||
|
||||
type Props = {
|
||||
setResult: (result: OutroAskUserModeResultType) => void;
|
||||
getInitialData?: () => OutroApplyInitialData | undefined;
|
||||
};
|
||||
const { setResult }: Props = $props();
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
const enableUnencryptedContent = $derived(getInitialData?.()?.enableUnencryptedContent === true);
|
||||
let userType = $state<OutroAskUserModeResultType>(TYPE_CANCELLED);
|
||||
const canProceed = $derived.by(() => {
|
||||
return userType === TYPE_EXISTING || userType === TYPE_NEW || userType === TYPE_COMPATIBLE_EXISTING;
|
||||
@@ -79,6 +83,9 @@
|
||||
"Unless you are certain, selecting this options is bit dangerous. It assumes that the server configuration is compatible with this device. If this is not the case, data loss may occur. Please ensure you know what you are doing."
|
||||
)}
|
||||
</InfoNote>
|
||||
{#if enableUnencryptedContent && userType === TYPE_COMPATIBLE_EXISTING}
|
||||
<PlaintextReceiptWarning />
|
||||
{/if}
|
||||
</Option>
|
||||
</Instruction>
|
||||
<UserDecisions>
|
||||
|
||||
@@ -5,15 +5,22 @@
|
||||
import Question from "@/modules/services/LiveSyncUI/components/Question.svelte";
|
||||
import Instruction from "@/modules/services/LiveSyncUI/components/Instruction.svelte";
|
||||
import UserDecisions from "@/modules/services/LiveSyncUI/components/UserDecisions.svelte";
|
||||
import PlaintextReceiptWarning from "./PlaintextReceiptWarning.svelte";
|
||||
import { $msg as translateMessage } from "@/common/translation";
|
||||
|
||||
import { TYPE_CANCELLED, TYPE_APPLY, type OutroExistingUserResultType } from "./setupDialogTypes";
|
||||
import {
|
||||
TYPE_CANCELLED,
|
||||
TYPE_APPLY,
|
||||
type OutroApplyInitialData,
|
||||
type OutroExistingUserResultType,
|
||||
} from "./setupDialogTypes";
|
||||
type Props = {
|
||||
setResult: (result: OutroExistingUserResultType) => void;
|
||||
getInitialData?: () => { isP2P?: boolean } | undefined;
|
||||
getInitialData?: () => OutroApplyInitialData | undefined;
|
||||
};
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
const isP2P = $derived(getInitialData?.()?.isP2P === true);
|
||||
const enableUnencryptedContent = $derived(getInitialData?.()?.enableUnencryptedContent === true);
|
||||
</script>
|
||||
|
||||
{#if isP2P}
|
||||
@@ -37,6 +44,9 @@
|
||||
{translateMessage("Restart this device, then choose the source device when P2P Rebuild opens.")}
|
||||
</Question>
|
||||
</Instruction>
|
||||
{#if enableUnencryptedContent}
|
||||
<PlaintextReceiptWarning />
|
||||
{/if}
|
||||
<UserDecisions>
|
||||
<Decision
|
||||
title={translateMessage("Restart and Select Source Device")}
|
||||
@@ -71,6 +81,9 @@
|
||||
)}</Question
|
||||
>
|
||||
</Instruction>
|
||||
{#if enableUnencryptedContent}
|
||||
<PlaintextReceiptWarning />
|
||||
{/if}
|
||||
<UserDecisions>
|
||||
<Decision
|
||||
title={translateMessage("Restart and Fetch Data")}
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
import Decision from "@/modules/services/LiveSyncUI/components/Decision.svelte";
|
||||
import Question from "@/modules/services/LiveSyncUI/components/Question.svelte";
|
||||
import Instruction from "@/modules/services/LiveSyncUI/components/Instruction.svelte";
|
||||
import PlaintextReceiptWarning from "./PlaintextReceiptWarning.svelte";
|
||||
import UserDecisions from "@/modules/services/LiveSyncUI/components/UserDecisions.svelte";
|
||||
import { $msg as msg } from "@/common/translation";
|
||||
import { TYPE_APPLY, TYPE_CANCELLED, type OutroNewUserResultType } from "./setupDialogTypes";
|
||||
import {
|
||||
TYPE_APPLY,
|
||||
TYPE_CANCELLED,
|
||||
type OutroApplyInitialData,
|
||||
type OutroNewUserResultType,
|
||||
} from "./setupDialogTypes";
|
||||
|
||||
type Props = {
|
||||
setResult: (result: OutroNewUserResultType) => void;
|
||||
getInitialData?: () => { isP2P?: boolean } | undefined;
|
||||
getInitialData?: () => OutroApplyInitialData | undefined;
|
||||
};
|
||||
const { setResult, getInitialData }: Props = $props();
|
||||
const isP2P = $derived(getInitialData?.()?.isP2P === true);
|
||||
const enableUnencryptedContent = $derived(getInitialData?.()?.enableUnencryptedContent === true);
|
||||
// let userType = $state<OutroNewUserResultType>(TYPE_CANCELLED);
|
||||
</script>
|
||||
|
||||
@@ -26,6 +33,9 @@
|
||||
<br />
|
||||
{msg("Ui.SetupWizard.OutroNewP2PUser.GuidanceNotice")}
|
||||
</p>
|
||||
{#if enableUnencryptedContent}
|
||||
<PlaintextReceiptWarning />
|
||||
{/if}
|
||||
</Guidance>
|
||||
<Instruction>
|
||||
<Question>{msg("Ui.SetupWizard.OutroNewP2PUser.Question")}</Question>
|
||||
@@ -55,6 +65,9 @@
|
||||
"After restarting, the data on this device will be uploaded to the server as the 'master copy'. Please be aware that any unintended data currently on the server will be completely overwritten."
|
||||
)}
|
||||
</p>
|
||||
{#if enableUnencryptedContent}
|
||||
<PlaintextReceiptWarning />
|
||||
{/if}
|
||||
</Guidance>
|
||||
<Instruction>
|
||||
<Question>{msg("Please select the button below to restart and proceed to the final confirmation.")}</Question>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import InfoNote from "@/modules/services/LiveSyncUI/components/InfoNote.svelte";
|
||||
import { $msg as translateMessage } from "@/common/translation";
|
||||
</script>
|
||||
|
||||
<InfoNote warning>
|
||||
{translateMessage(
|
||||
"Accepting unencrypted content will be enabled. Third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext. This affects received content only; content sent by this plug-in remains encrypted."
|
||||
)}
|
||||
</InfoNote>
|
||||
@@ -40,6 +40,7 @@
|
||||
E2EEAlgorithm: DEFAULT_SETTINGS.E2EEAlgorithm,
|
||||
usePathObfuscation: true,
|
||||
encryptInternalMetadata: true,
|
||||
acceptUnencryptedContent: DEFAULT_SETTINGS.acceptUnencryptedContent,
|
||||
idDerivationVersion: 0,
|
||||
idDerivationKey: "",
|
||||
};
|
||||
@@ -392,6 +393,18 @@
|
||||
"Changing the encryption algorithm will prevent access to any data previously encrypted with a different algorithm. Ensure that all your devices are configured to use the same algorithm to maintain access to your data."
|
||||
)}
|
||||
</InfoNote>
|
||||
<InputRow label={translateMessage("Accept unencrypted content")}>
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={encryptionSettings.acceptUnencryptedContent}
|
||||
disabled={!encryptionSettings.encrypt}
|
||||
/>
|
||||
</InputRow>
|
||||
<InfoNote warning>
|
||||
{translateMessage(
|
||||
"With this option enabled, third-party tools can add content without the E2EE passphrase. Anyone with database write access can inject plaintext. This affects received content only; content sent by this plug-in remains encrypted."
|
||||
)}
|
||||
</InfoNote>
|
||||
</ExtraItems>
|
||||
|
||||
<InfoNote warning>
|
||||
|
||||
@@ -85,6 +85,10 @@ export type OutroAskUserModeResultType =
|
||||
| typeof TYPE_CANCELLED;
|
||||
|
||||
export type OutroExistingUserResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED;
|
||||
export type OutroApplyInitialData = {
|
||||
isP2P: boolean;
|
||||
enableUnencryptedContent: boolean;
|
||||
};
|
||||
|
||||
export type OutroNewUserResultType = typeof TYPE_APPLY | typeof TYPE_CANCELLED;
|
||||
|
||||
|
||||
Reference in new issue
Block a user