Preserve adopted encryption settings when prerequisites change

This commit is contained in:
vorotamoroz
2026-10-17 08:43:04 +00:00
parent d31ad56211
commit 83d40a561f
3 changed files with 55 additions and 3 deletions
@@ -215,8 +215,17 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
): Promise<[TweakValues | boolean, boolean]> {
if (assessment.alignment === "matched") return [false, false];
if (this.requiresIdConfigurationReview(assessment)) return [false, false];
// Retain saved preferences when only their effective representation differs from the assessment.
// Retain saved preferences only when they leave the adopted effective values unchanged.
const assessedSettings = settingsAfterAdoption(assessment, "adoptPreferred");
const acceptedSettings = settingsAfterAdoption(assessment, "adoptPreferred", this.settings);
if (
this.settings.remoteType === REMOTE_COUCHDB &&
usesEncryptedInternalMetadata({ ...this.settings, ...acceptedSettings }) !==
usesEncryptedInternalMetadata({ ...this.settings, ...assessedSettings })
) {
// Adopting encryption prerequisites can activate a previously inactive preference.
acceptedSettings.encryptInternalMetadata = assessedSettings.encryptInternalMetadata;
}
const autoAcceptSide = await this._shouldAutoAcceptCompatibleLossy(assessment);
if (autoAcceptSide === "REMOTE") return [acceptedSettings, false];
if (autoAcceptSide === "CURRENT") return [true, false];
@@ -14,6 +14,7 @@ import {
CENTRAL_COMPATIBILITY_REJECTION_REASONS,
REMOTE_RESOURCE_KINDS,
USER_INITIATED_REPLICATION_AUTHORITY,
usesEncryptedInternalMetadata,
type ReplicationAttemptFailure,
} from "@vrtmrz/livesync-commonlib/replication";
@@ -74,6 +75,46 @@ function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
}
describe("ModuleResolvingMismatchedTweaks", () => {
it.each([
{ label: "E2EE", encrypt: false, usePathObfuscation: true },
{ label: "Property Encryption", encrypt: true, usePathObfuscation: false },
])(
"uses the remote Metadata encryption preference when adoption enables $label",
async ({ encrypt, usePathObfuscation }) => {
const { module, core, askSelectStringDialogue } = createModule({
autoAcceptCompatibleTweak: false,
encrypt,
usePathObfuscation,
E2EEAlgorithm: "v2",
encryptInternalMetadata: true,
idDerivationVersion: 0,
});
const current = {
...extractObject(TweakValuesTemplate, core.settings),
encryptInternalMetadata: usesEncryptedInternalMetadata(core.settings),
};
const preferred = { ...current, encrypt: true, usePathObfuscation: true, encryptInternalMetadata: false };
const assessment = assessTweakCompatibility(current, preferred);
core._services.tweakValue = {
checkAndAskResolvingMismatched: module._checkAndAskResolvingMismatchedTweaks.bind(module),
};
core.rebuilder = { $fetchLocal: vi.fn(async () => undefined) };
askSelectStringDialogue.mockResolvedValueOnce("Apply settings to this device, and fetch again");
const updateRemote = vi.fn(async () => true);
await expect(module._askResolvingMismatchedTweaks(preferred, updateRemote, assessment)).resolves.toBe(
"CHECKAGAIN"
);
expect(core.rebuilder.$fetchLocal).toHaveBeenCalledOnce();
expect(core.settings).toMatchObject({ encrypt: true, usePathObfuscation: true });
expect(usesEncryptedInternalMetadata(core.settings)).toBe(false);
expect(updateRemote).toHaveBeenCalledWith(
expect.objectContaining({ encrypt: true, usePathObfuscation: true, encryptInternalMetadata: false })
);
}
);
it.each([
{ label: "automatic alignment with E2EE disabled", autoAcceptCompatibleTweak: true, encrypt: false },
{ label: "manual review with E2EE disabled", autoAcceptCompatibleTweak: false, encrypt: false },