mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-04 16:32:31 +00:00
Add independent ID key configuration and recovery
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Keep CouchDB database-version negotiation isolated from Setup URI generation.
|
||||
// The exact release must match utils/livesync-commonlib-version.ts; the setup
|
||||
// tool suite checks every static specifier before release.
|
||||
export { checkRemoteVersion } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/pouchdb/negotiation";
|
||||
export { PouchDB } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/pouchdb/pouchdb-browser";
|
||||
export { checkRemoteVersion } from "npm:@vrtmrz/livesync-commonlib@0.1.32/compat/pouchdb/negotiation";
|
||||
export { PouchDB } from "npm:@vrtmrz/livesync-commonlib@0.1.32/compat/pouchdb/pouchdb-browser";
|
||||
|
||||
Generated
+5
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4": "0.1.0-rc.4"
|
||||
"npm:@vrtmrz/livesync-commonlib@0.1.32": "0.1.32"
|
||||
},
|
||||
"npm": {
|
||||
"@aws-sdk/checksums@3.1000.18": {
|
||||
@@ -284,8 +284,8 @@
|
||||
"@trystero-p2p/core"
|
||||
]
|
||||
},
|
||||
"@vrtmrz/livesync-commonlib@0.1.0-rc.4": {
|
||||
"integrity": "sha512-u4FdbjnYg7lAf38z7eUv4eq4vxEdrl4rFMxiDZiJ7T701awKiflkXGIJQnaHdLaMa3zkRBU15qqEo7GtextmlA==",
|
||||
"@vrtmrz/livesync-commonlib@0.1.32": {
|
||||
"integrity": "sha512-gzjhd7bg+DKHhd/24WGxBM7557wsw3HJkc0YjKll1n8/8vuhqPnlLuZmM33fj+4bv9nGFUnDlnoVowpQrTns6w==",
|
||||
"dependencies": [
|
||||
"@aws-sdk/client-s3",
|
||||
"@smithy/fetch-http-handler",
|
||||
@@ -509,8 +509,8 @@
|
||||
"whatwg-url"
|
||||
]
|
||||
},
|
||||
"octagonal-wheels@0.1.51": {
|
||||
"integrity": "sha512-KTlfqKPjobHJg/t3A539srnFf+VHr1aXkHSmsNDDpiI5UFC7FamZ95dWpJfGE2EI/HULR5hveQDgkazmz8SAcg==",
|
||||
"octagonal-wheels@0.1.54": {
|
||||
"integrity": "sha512-Je3ancYhjKX7UY2K19T/qTjG8C9nK8YVrACr5naIf78mN4bbjQkYyWmlj+ooifV/moWVsQrp4fEWz/7mv6It3A==",
|
||||
"dependencies": [
|
||||
"idb"
|
||||
]
|
||||
|
||||
@@ -35,6 +35,13 @@ Deno.test("generates a current self-hosted Setup URI through the published Commo
|
||||
const decoded = await decodeSettingsFromSetupURI(setupURI, "setup-secret");
|
||||
assert(decoded, "Commonlib could not decode the generated Setup URI");
|
||||
const effectiveSettings = { ...DEFAULT_SETTINGS, ...decoded };
|
||||
const recoveryCode = stdout.match(/sls-id-v1:[0-9a-f]{64}/u)?.[0];
|
||||
assert(recoveryCode, "the generator did not print an ID recovery code");
|
||||
assert(
|
||||
(effectiveSettings as typeof effectiveSettings & { idDerivationKey?: string }).idDerivationKey ===
|
||||
recoveryCode.slice("sls-id-v1:".length),
|
||||
"the CouchDB Setup URI did not contain the generated ID key",
|
||||
);
|
||||
assert(
|
||||
effectiveSettings.isConfigured,
|
||||
"the CouchDB Setup URI left the imported device unconfigured",
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
// Commonlib registry release. Static npm specifiers cannot interpolate this
|
||||
// value, so livesync-commonlib-version.test.ts verifies the domain-specific
|
||||
// facades against it.
|
||||
export const LIVESYNC_COMMONLIB_VERSION = "0.1.0-rc.4";
|
||||
export const LIVESYNC_COMMONLIB_VERSION = "0.1.32";
|
||||
|
||||
@@ -31,6 +31,8 @@ Authentication and other non-retryable HTTP failures stop immediately. Network a
|
||||
|
||||
The existing `flyio/generate_setupuri.ts` path remains a CouchDB-only compatibility wrapper for the Fly.io deployment script.
|
||||
|
||||
The generator creates a fresh random ID key by default and includes it in the encrypted Setup URI. It prints a tagged `sls-id-v1:` recovery code. Set `id_recovery_code` to that code when generating another URI for the same Vault; a new run without it creates a different key. This restores only the ID key: reuse the original connection details too. For P2P, provide the original `p2p_room_id` and `p2p_passphrase` because omitted values are generated afresh. Set `id_mode=legacy` to generate a URI with the previous ID behaviour. `id_mode=legacy` and `id_recovery_code` cannot be combined. Keep the recovery code private and retain it if every device might be lost.
|
||||
|
||||
### CouchDB
|
||||
|
||||
```sh
|
||||
|
||||
@@ -26,6 +26,20 @@ 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",
|
||||
@@ -74,6 +88,11 @@ 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",
|
||||
@@ -121,3 +140,46 @@ Deno.test("generates a random-room P2P Setup URI without copying a device identi
|
||||
"the selected profile was not a P2P connection URI",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("reuses the ID key from a recovery code and permits explicit legacy IDs", async () => {
|
||||
const environment = {
|
||||
remote_type: "p2p",
|
||||
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");
|
||||
});
|
||||
|
||||
@@ -19,6 +19,36 @@ export interface GeneratedSetupURI {
|
||||
remoteType: SetupRemoteType;
|
||||
setupURI: string;
|
||||
setupPassphrase: string;
|
||||
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(
|
||||
@@ -166,11 +196,12 @@ export async function generateSetupURI(
|
||||
const setupPassphrase = environment.uri_passphrase?.trim() ||
|
||||
generateSecret();
|
||||
const { remoteType, settings } = createSetupSettings(environment);
|
||||
const idRecoveryCode = configureIdDerivation(settings, environment);
|
||||
const setupURI = await encodeSettingsToSetupURI(settings, setupPassphrase, [
|
||||
"pluginSyncExtendedSetting",
|
||||
"doNotUseFixedRevisionForChunks",
|
||||
], true);
|
||||
return { remoteType, setupURI: setupURI.trim(), setupPassphrase };
|
||||
return { remoteType, setupURI: setupURI.trim(), setupPassphrase, idRecoveryCode };
|
||||
}
|
||||
|
||||
export async function runSetupURIGenerator(
|
||||
@@ -183,6 +214,10 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
export {
|
||||
decodeSettingsFromSetupURI,
|
||||
encodeSettingsToSetupURI,
|
||||
} from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/API/processSetting";
|
||||
export { generateP2PRoomId } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/compat/common/utils";
|
||||
export { upsertRemoteConfigurationInPlace } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/remote-configurations";
|
||||
} from "npm:@vrtmrz/livesync-commonlib@0.1.32/compat/API/processSetting";
|
||||
export { generateP2PRoomId } from "npm:@vrtmrz/livesync-commonlib@0.1.32/compat/common/utils";
|
||||
export { upsertRemoteConfigurationInPlace } from "npm:@vrtmrz/livesync-commonlib@0.1.32/remote-configurations";
|
||||
export {
|
||||
createNewVaultSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
@@ -14,5 +14,5 @@ export {
|
||||
PREFERRED_BASE,
|
||||
PREFERRED_JOURNAL_SYNC,
|
||||
PREFERRED_SETTING_SELF_HOSTED,
|
||||
} from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/settings";
|
||||
export type { ObsidianLiveSyncSettings } from "npm:@vrtmrz/livesync-commonlib@0.1.0-rc.4/settings";
|
||||
} from "npm:@vrtmrz/livesync-commonlib@0.1.32/settings";
|
||||
export type { ObsidianLiveSyncSettings } from "npm:@vrtmrz/livesync-commonlib@0.1.32/settings";
|
||||
|
||||
Reference in New Issue
Block a user