mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-21 16:52:32 +00:00
Fix tweak recovery for effective encryption settings
This commit is contained in:
@@ -16,11 +16,12 @@ import { AbstractModule } from "@/modules/AbstractModule.ts";
|
||||
import { $msg, translateIfAvailable } from "@/common/translation";
|
||||
import type { InjectableServiceHub } from "@vrtmrz/livesync-commonlib/compat/services/implements/injectable/InjectableServiceHub";
|
||||
import type { LiveSyncCore } from "@/main.ts";
|
||||
import { REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
import { REMOTE_COUCHDB, REMOTE_P2P } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
import { withOwnedRemoteResource } from "@/common/ownedRemoteResource";
|
||||
import {
|
||||
CENTRAL_COMPATIBILITY_REJECTION_REASONS,
|
||||
REMOTE_RESOURCE_KINDS,
|
||||
usesEncryptedInternalMetadata,
|
||||
type ReplicationAttemptFailure,
|
||||
type ReplicatorInstance,
|
||||
} from "@vrtmrz/livesync-commonlib/replication";
|
||||
@@ -62,10 +63,14 @@ function definedTweaks(values: TweakValues): TweakValues {
|
||||
);
|
||||
}
|
||||
|
||||
function settingsAfterAdoption(assessment: TweakAssessment, direction: "adoptPreferred" | "adoptCurrent"): TweakValues {
|
||||
function settingsAfterAdoption(
|
||||
assessment: TweakAssessment,
|
||||
direction: "adoptPreferred" | "adoptCurrent",
|
||||
currentSettings: TweakValues = assessment.currentValues
|
||||
): TweakValues {
|
||||
const comparedKeys = new Set<string>(assessment.entries.map((entry) => entry.key));
|
||||
const source = direction === "adoptPreferred" ? assessment.preferredValues : assessment.currentValues;
|
||||
const target = direction === "adoptPreferred" ? assessment.currentValues : assessment.preferredValues;
|
||||
const target = direction === "adoptPreferred" ? currentSettings : assessment.preferredValues;
|
||||
const recommendations = Object.fromEntries(
|
||||
Object.entries(source).filter(([key, value]) => !comparedKeys.has(key) && value !== undefined)
|
||||
);
|
||||
@@ -100,7 +105,9 @@ function resolutionSettingsSignature(settings: ObsidianLiveSyncSettings): string
|
||||
|
||||
export class ModuleResolvingMismatchedTweaks extends AbstractModule {
|
||||
private requiresIdConfigurationReview(assessment: TweakAssessment): boolean {
|
||||
if (!assessment.entries.some(({ key, relation }) => key === "idDerivationVersion" && relation === "different")) {
|
||||
if (
|
||||
!assessment.entries.some(({ key, relation }) => key === "idDerivationVersion" && relation === "different")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
Logger(
|
||||
@@ -208,7 +215,8 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
|
||||
): Promise<[TweakValues | boolean, boolean]> {
|
||||
if (assessment.alignment === "matched") return [false, false];
|
||||
if (this.requiresIdConfigurationReview(assessment)) return [false, false];
|
||||
const acceptedSettings = settingsAfterAdoption(assessment, "adoptPreferred");
|
||||
// Retain saved preferences when only their effective representation differs from the assessment.
|
||||
const acceptedSettings = settingsAfterAdoption(assessment, "adoptPreferred", this.settings);
|
||||
const autoAcceptSide = await this._shouldAutoAcceptCompatibleLossy(assessment);
|
||||
if (autoAcceptSide === "REMOTE") return [acceptedSettings, false];
|
||||
if (autoAcceptSide === "CURRENT") return [true, false];
|
||||
@@ -279,8 +287,15 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
|
||||
const signature = resolutionSettingsSignature(this.settings);
|
||||
const publication = await this.services.replicator.acquireActiveReplicatorContext();
|
||||
if (resolutionSettingsSignature(this.settings) !== signature) return "IGNORE";
|
||||
const currentTweaks = JSON.stringify(extractObject(TweakValuesTemplate, this.settings));
|
||||
if (JSON.stringify(extractObject(TweakValuesTemplate, assessment.currentValues)) !== currentTweaks) {
|
||||
const currentTweaks = extractObject(TweakValuesTemplate, this.settings);
|
||||
const effectiveTweaks = {
|
||||
...currentTweaks,
|
||||
encryptInternalMetadata:
|
||||
this.settings.remoteType === REMOTE_COUCHDB && usesEncryptedInternalMetadata(this.settings),
|
||||
};
|
||||
const assessedTweaks = JSON.stringify(extractObject(TweakValuesTemplate, assessment.currentValues));
|
||||
// Replication supplies effective values; legacy callers can supply saved preferences.
|
||||
if (assessedTweaks !== JSON.stringify(currentTweaks) && assessedTweaks !== JSON.stringify(effectiveTweaks)) {
|
||||
return "IGNORE";
|
||||
}
|
||||
const [conf, rebuildRequired] = await this.services.tweakValue.checkAndAskResolvingMismatched(
|
||||
|
||||
@@ -74,6 +74,130 @@ function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
|
||||
}
|
||||
|
||||
describe("ModuleResolvingMismatchedTweaks", () => {
|
||||
it.each([
|
||||
{ label: "automatic alignment with E2EE disabled", autoAcceptCompatibleTweak: true, encrypt: false },
|
||||
{ label: "manual review with E2EE disabled", autoAcceptCompatibleTweak: false, encrypt: false },
|
||||
{
|
||||
label: "automatic alignment with Property Encryption disabled",
|
||||
autoAcceptCompatibleTweak: true,
|
||||
encrypt: true,
|
||||
},
|
||||
])(
|
||||
"resolves an effective chunk mismatch while retaining an inactive encryption preference: $label",
|
||||
async ({ autoAcceptCompatibleTweak, encrypt }) => {
|
||||
const { module, core, askSelectStringDialogue, reinitialise } = createModule({
|
||||
autoAcceptCompatibleTweak,
|
||||
encrypt,
|
||||
usePathObfuscation: false,
|
||||
encryptInternalMetadata: true,
|
||||
customChunkSize: 60,
|
||||
tweakModified: 100,
|
||||
});
|
||||
// CouchDB assesses the effective encryption value, while the saved preference remains enabled.
|
||||
const current = { ...extractObject(TweakValuesTemplate, core.settings), encryptInternalMetadata: false };
|
||||
const preferred = { ...current, customChunkSize: 0, tweakModified: 200 };
|
||||
const assessment = assessTweakCompatibility(current, preferred);
|
||||
core._services.tweakValue = {
|
||||
checkAndAskResolvingMismatched: module._checkAndAskResolvingMismatchedTweaks.bind(module),
|
||||
};
|
||||
askSelectStringDialogue.mockResolvedValueOnce("Apply settings to this device");
|
||||
const updateRemote = vi.fn(async () => true);
|
||||
|
||||
await expect(module._askResolvingMismatchedTweaks(preferred, updateRemote, assessment)).resolves.toBe(
|
||||
"CHECKAGAIN"
|
||||
);
|
||||
|
||||
expect(core.settings).toMatchObject({ customChunkSize: 0, encryptInternalMetadata: true });
|
||||
expect(core._services.setting.saveSettingData).toHaveBeenCalledOnce();
|
||||
expect(reinitialise).toHaveBeenCalledOnce();
|
||||
expect(updateRemote).toHaveBeenCalledOnce();
|
||||
expect(askSelectStringDialogue).toHaveBeenCalledTimes(autoAcceptCompatibleTweak ? 0 : 1);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(["settings", "publication"] as const)(
|
||||
"discards an effective assessment when %s change while awaiting a decision",
|
||||
async (changed) => {
|
||||
const { module, core, askSelectStringDialogue, reinitialise } = createModule({
|
||||
autoAcceptCompatibleTweak: false,
|
||||
encrypt: false,
|
||||
encryptInternalMetadata: true,
|
||||
customChunkSize: 60,
|
||||
});
|
||||
const current = { ...extractObject(TweakValuesTemplate, core.settings), encryptInternalMetadata: false };
|
||||
const preferred = { ...current, customChunkSize: 0 };
|
||||
const assessment = assessTweakCompatibility(current, preferred);
|
||||
core._services.tweakValue = {
|
||||
checkAndAskResolvingMismatched: module._checkAndAskResolvingMismatchedTweaks.bind(module),
|
||||
};
|
||||
askSelectStringDialogue.mockImplementation(async () => {
|
||||
if (changed === "settings") {
|
||||
// The effective value stays false, but the user's saved preference has changed.
|
||||
core.settings.encryptInternalMetadata = false;
|
||||
} else {
|
||||
core._services.replicator.acquireActiveReplicatorContext.mockResolvedValue({});
|
||||
}
|
||||
return "Apply settings to this device";
|
||||
});
|
||||
const updateRemote = vi.fn(async () => true);
|
||||
|
||||
await expect(module._askResolvingMismatchedTweaks(preferred, updateRemote, assessment)).resolves.toBe(
|
||||
"IGNORE"
|
||||
);
|
||||
|
||||
expect(askSelectStringDialogue).toHaveBeenCalledOnce();
|
||||
expect(core.settings.customChunkSize).toBe(60);
|
||||
expect(core._services.setting.saveSettingData).not.toHaveBeenCalled();
|
||||
expect(reinitialise).not.toHaveBeenCalled();
|
||||
expect(updateRemote).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
it("discards an effective assessment of an earlier chunk setting before opening a dialogue", async () => {
|
||||
const { module, core, askSelectStringDialogue } = createModule({
|
||||
autoAcceptCompatibleTweak: false,
|
||||
encrypt: false,
|
||||
encryptInternalMetadata: true,
|
||||
customChunkSize: 60,
|
||||
});
|
||||
const current = { ...extractObject(TweakValuesTemplate, core.settings), encryptInternalMetadata: false };
|
||||
const preferred = { ...current, customChunkSize: 0 };
|
||||
const assessment = assessTweakCompatibility(current, preferred);
|
||||
core.settings.customChunkSize = 72;
|
||||
core._services.tweakValue = {
|
||||
checkAndAskResolvingMismatched: module._checkAndAskResolvingMismatchedTweaks.bind(module),
|
||||
};
|
||||
const updateRemote = vi.fn(async () => true);
|
||||
|
||||
await expect(module._askResolvingMismatchedTweaks(preferred, updateRemote, assessment)).resolves.toBe("IGNORE");
|
||||
|
||||
expect(askSelectStringDialogue).not.toHaveBeenCalled();
|
||||
expect(core.settings.customChunkSize).toBe(72);
|
||||
expect(core._services.setting.saveSettingData).not.toHaveBeenCalled();
|
||||
expect(updateRemote).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies an explicit decision when effective internal Metadata encryption differs", async () => {
|
||||
const { module, core, askSelectStringDialogue } = createModule({
|
||||
autoAcceptCompatibleTweak: true,
|
||||
encrypt: true,
|
||||
usePathObfuscation: true,
|
||||
E2EEAlgorithm: "v2",
|
||||
encryptInternalMetadata: true,
|
||||
});
|
||||
const preferred = { ...extractObject(TweakValuesTemplate, core.settings), encryptInternalMetadata: false };
|
||||
core._services.tweakValue = {
|
||||
checkAndAskResolvingMismatched: module._checkAndAskResolvingMismatchedTweaks.bind(module),
|
||||
};
|
||||
askSelectStringDialogue.mockResolvedValueOnce("Apply settings to this device");
|
||||
|
||||
await expect(module._askResolvingMismatchedTweaks(preferred, async () => true)).resolves.toBe("CHECKAGAIN");
|
||||
|
||||
expect(askSelectStringDialogue).toHaveBeenCalledOnce();
|
||||
expect(core.settings.encryptInternalMetadata).toBe(false);
|
||||
expect(core._services.setting.saveSettingData).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([0, 1] as const)(
|
||||
"keeps ID configuration %s when automatically aligning Chunk settings",
|
||||
async (idDerivationVersion) => {
|
||||
|
||||
Reference in New Issue
Block a user