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
+4 -2
View File
@@ -69,8 +69,10 @@ Central replication can assess an effective value which differs from a saved
preference, such as internal Metadata encryption whose prerequisites are inactive. preference, such as internal Metadata encryption whose prerequisites are inactive.
Recovery accepts the current saved or effective snapshot without treating that Recovery accepts the current saved or effective snapshot without treating that
representation difference as a settings change. Applying assessed changes retains representation difference as a settings change. Applying assessed changes retains
saved preferences for settings whose effective values already agree. The full saved preferences only when they leave the adopted effective values unchanged.
settings signature still detects a preference changed while awaiting a decision. If adoption activates encryption prerequisites, the adopted encryption value
takes precedence. The full settings signature still detects a preference changed
while awaiting a decision.
The active publication reservation is not held while waiting for the dialogue. The active publication reservation is not held while waiting for the dialogue.
Remote writes use the failed attempt's publication guard. Fetch and Rebuild use Remote writes use the failed attempt's publication guard. Fetch and Rebuild use
@@ -215,8 +215,17 @@ export class ModuleResolvingMismatchedTweaks extends AbstractModule {
): Promise<[TweakValues | boolean, boolean]> { ): Promise<[TweakValues | boolean, boolean]> {
if (assessment.alignment === "matched") return [false, false]; if (assessment.alignment === "matched") return [false, false];
if (this.requiresIdConfigurationReview(assessment)) 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); 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); const autoAcceptSide = await this._shouldAutoAcceptCompatibleLossy(assessment);
if (autoAcceptSide === "REMOTE") return [acceptedSettings, false]; if (autoAcceptSide === "REMOTE") return [acceptedSettings, false];
if (autoAcceptSide === "CURRENT") return [true, false]; if (autoAcceptSide === "CURRENT") return [true, false];
@@ -14,6 +14,7 @@ import {
CENTRAL_COMPATIBILITY_REJECTION_REASONS, CENTRAL_COMPATIBILITY_REJECTION_REASONS,
REMOTE_RESOURCE_KINDS, REMOTE_RESOURCE_KINDS,
USER_INITIATED_REPLICATION_AUTHORITY, USER_INITIATED_REPLICATION_AUTHORITY,
usesEncryptedInternalMetadata,
type ReplicationAttemptFailure, type ReplicationAttemptFailure,
} from "@vrtmrz/livesync-commonlib/replication"; } from "@vrtmrz/livesync-commonlib/replication";
@@ -74,6 +75,46 @@ function createModule(settingsOverride: Partial<typeof DEFAULT_SETTINGS> = {}) {
} }
describe("ModuleResolvingMismatchedTweaks", () => { 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([ it.each([
{ label: "automatic alignment with E2EE disabled", autoAcceptCompatibleTweak: true, encrypt: false }, { label: "automatic alignment with E2EE disabled", autoAcceptCompatibleTweak: true, encrypt: false },
{ label: "manual review with E2EE disabled", autoAcceptCompatibleTweak: false, encrypt: false }, { label: "manual review with E2EE disabled", autoAcceptCompatibleTweak: false, encrypt: false },