mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-05 00:42:31 +00:00
Merge main and verify ID keys through Setup URI and QR
This commit is contained in:
@@ -34,6 +34,22 @@ Deno.test("generates an Object Storage Setup URI with a selected S3 profile", as
|
||||
);
|
||||
assert(decoded, "Commonlib could not decode the Object Storage Setup URI");
|
||||
const effective = { ...DEFAULT_SETTINGS, ...decoded };
|
||||
const recoveryCode = generated.idRecoveryCode;
|
||||
assert(
|
||||
typeof recoveryCode === "string" && recoveryCode.startsWith("sls-id-v1:"),
|
||||
"the generator did not return an ID recovery code",
|
||||
);
|
||||
assert(
|
||||
(effective as typeof effective & { idDerivationVersion?: number })
|
||||
.idDerivationVersion === 1,
|
||||
"the Setup URI did not enable independent IDs",
|
||||
);
|
||||
assert(
|
||||
(effective as typeof effective & { idDerivationKey?: string })
|
||||
.idDerivationKey ===
|
||||
recoveryCode.slice("sls-id-v1:".length),
|
||||
"the Setup URI did not contain the generated ID key",
|
||||
);
|
||||
assert(
|
||||
effective.isConfigured,
|
||||
"the Setup URI left the imported device unconfigured",
|
||||
@@ -82,6 +98,12 @@ Deno.test("generates a random-room P2P Setup URI without copying a device identi
|
||||
);
|
||||
assert(decoded, "Commonlib could not decode the P2P Setup URI");
|
||||
const effective = { ...DEFAULT_SETTINGS, ...decoded };
|
||||
assert(
|
||||
(effective as typeof effective & { idDerivationKey?: string })
|
||||
.idDerivationKey ===
|
||||
generated.idRecoveryCode?.slice("sls-id-v1:".length),
|
||||
"the P2P Setup URI did not contain the generated ID key",
|
||||
);
|
||||
assert(
|
||||
/^\d{3}-\d{3}-\d{3}-[a-z0-9]{3}$/.test(effective.P2P_roomID),
|
||||
"Commonlib did not generate the expected random room ID",
|
||||
@@ -159,3 +181,80 @@ Deno.test("rejects an unknown Setup URI mode", async () => {
|
||||
}
|
||||
assert(rejected, "the generator accepted an unknown Setup URI mode");
|
||||
});
|
||||
|
||||
for (const mode of ["ephemeral", "persistent"] as const) {
|
||||
Deno.test(`preserves ID recovery and explicit legacy IDs in ${mode} URIs`, async () => {
|
||||
const environment = {
|
||||
remote_type: "p2p",
|
||||
uri_mode: mode,
|
||||
passphrase: "vault-secret",
|
||||
uri_passphrase: "setup-secret",
|
||||
};
|
||||
const first = await generateSetupURI(environment);
|
||||
const second = await generateSetupURI({
|
||||
...environment,
|
||||
id_recovery_code: first.idRecoveryCode,
|
||||
});
|
||||
const independentlyGenerated = await generateSetupURI(environment);
|
||||
assert(
|
||||
second.idRecoveryCode === first.idRecoveryCode,
|
||||
"the recovery code changed on repeat generation",
|
||||
);
|
||||
assert(
|
||||
independentlyGenerated.idRecoveryCode !== first.idRecoveryCode,
|
||||
"the default ID key was reused",
|
||||
);
|
||||
const repeatedSettings = await decodeSettingsFromSetupURI(
|
||||
second.setupURI,
|
||||
second.setupPassphrase,
|
||||
);
|
||||
assert(repeatedSettings, "the repeated Setup URI could not be decoded");
|
||||
assert(
|
||||
(repeatedSettings as typeof repeatedSettings & {
|
||||
idDerivationKey?: string;
|
||||
}).idDerivationKey ===
|
||||
first.idRecoveryCode?.slice("sls-id-v1:".length),
|
||||
"the recovery code did not restore the original ID key",
|
||||
);
|
||||
|
||||
const legacy = await generateSetupURI({
|
||||
...environment,
|
||||
id_mode: "legacy",
|
||||
});
|
||||
const decoded = await decodeSettingsFromSetupURI(
|
||||
legacy.setupURI,
|
||||
legacy.setupPassphrase,
|
||||
);
|
||||
assert(decoded, "the legacy Setup URI could not be decoded");
|
||||
assert(
|
||||
legacy.idRecoveryCode === undefined,
|
||||
"legacy mode returned an ID recovery code",
|
||||
);
|
||||
assert(
|
||||
(decoded as typeof decoded & { idDerivationVersion?: number })
|
||||
.idDerivationVersion !== 1,
|
||||
"legacy mode enabled independent IDs",
|
||||
);
|
||||
let rejected = false;
|
||||
try {
|
||||
await generateSetupURI({
|
||||
...environment,
|
||||
id_recovery_code: "sls-id-v1:wrong",
|
||||
});
|
||||
} catch {
|
||||
rejected = true;
|
||||
}
|
||||
assert(rejected, "an invalid recovery code was accepted");
|
||||
rejected = false;
|
||||
try {
|
||||
await generateSetupURI({
|
||||
...environment,
|
||||
id_mode: "legacy",
|
||||
id_recovery_code: first.idRecoveryCode,
|
||||
});
|
||||
} catch {
|
||||
rejected = true;
|
||||
}
|
||||
assert(rejected, "legacy mode silently ignored a recovery code");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,6 +23,42 @@ export interface GeneratedSetupURI {
|
||||
setupPassphrase: string;
|
||||
mode: TimeBoundSetupURIMode;
|
||||
usableUntil: number | null;
|
||||
idRecoveryCode?: string;
|
||||
}
|
||||
|
||||
const ID_RECOVERY_CODE_PREFIX = "sls-id-v1:";
|
||||
const ID_RECOVERY_CODE_PATTERN = /^sls-id-v1:([0-9a-f]{64})$/u;
|
||||
|
||||
function generateRandomIdKey(): string {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
||||
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
function configureIdDerivation(
|
||||
settings: ObsidianLiveSyncSettings,
|
||||
environment: SetupGeneratorEnvironment,
|
||||
): string | undefined {
|
||||
const mode = environment.id_mode?.trim().toLowerCase() || "random";
|
||||
if (mode !== "random" && mode !== "legacy") {
|
||||
throw new Error("id_mode must be random or legacy");
|
||||
}
|
||||
const suppliedCode = environment.id_recovery_code?.trim();
|
||||
if (mode === "legacy") {
|
||||
if (suppliedCode) {
|
||||
throw new Error("id_recovery_code cannot be used with id_mode=legacy");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const key = suppliedCode
|
||||
? ID_RECOVERY_CODE_PATTERN.exec(suppliedCode)?.[1]
|
||||
: generateRandomIdKey();
|
||||
if (!key) {
|
||||
throw new Error("id_recovery_code must be a valid sls-id-v1 recovery code");
|
||||
}
|
||||
Object.assign(settings, { idDerivationVersion: 1, idDerivationKey: key });
|
||||
return `${ID_RECOVERY_CODE_PREFIX}${key}`;
|
||||
}
|
||||
|
||||
function requireValue(
|
||||
@@ -179,6 +215,7 @@ export async function generateSetupURI(
|
||||
generateSecret();
|
||||
const mode = parseSetupURIMode(environment);
|
||||
const { remoteType, settings } = createSetupSettings(environment);
|
||||
const idRecoveryCode = configureIdDerivation(settings, environment);
|
||||
const { uri, usableUntil } = await encodeTimeBoundSetupURI(
|
||||
settings,
|
||||
setupPassphrase,
|
||||
@@ -200,6 +237,7 @@ export async function generateSetupURI(
|
||||
setupPassphrase,
|
||||
mode,
|
||||
usableUntil,
|
||||
idRecoveryCode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -230,6 +268,12 @@ export async function runSetupURIGenerator(
|
||||
generated.setupPassphrase,
|
||||
);
|
||||
console.log("This passphrase is never shown again, so store it safely.");
|
||||
if (generated.idRecoveryCode) {
|
||||
console.log("ID recovery code:", generated.idRecoveryCode);
|
||||
console.log(
|
||||
"Use id_recovery_code with this value and reuse the same remote settings when generating another Setup URI for the same Vault.",
|
||||
);
|
||||
}
|
||||
console.log(generated.setupURI);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user