mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-29 20:43:20 +00:00
434 lines
24 KiB
Markdown
434 lines
24 KiB
Markdown
---
|
|
date: 2026-09-28
|
|
commonlib-version: "0.1.33"
|
|
feasibility-probe-version: "0.1.27"
|
|
self-hosted-livesync-version: "1.0.32"
|
|
status: released
|
|
---
|
|
|
|
# Time-bound Setup URIs
|
|
|
|
## Purpose and status
|
|
|
|
The published Obsidian generator offers two choices when creating a [Setup URI](../glossary.md):
|
|
|
|
- **Time-bound** (the `ephemeral` derivation mode), selected by default,
|
|
derives an effective passphrase from the entered passphrase and the current
|
|
fixed time window.
|
|
- **Compatible (no time limit)** (the `persistent` derivation mode) uses the
|
|
entered passphrase and the existing URI format unchanged. It also serves as
|
|
the compatibility mode for older readers.
|
|
|
|
The Time-bound choice shows the exact time until which the URI can be opened
|
|
through the ordinary reader. Import still requires only the URI and the entered
|
|
passphrase. Neither the mode nor a timestamp is stored in the URI.
|
|
|
|
Both URI availability choices shipped in Self-hosted LiveSync 1.0.33. The
|
|
initial executable protocol probe passed 22 cases against Commonlib 0.1.27 and
|
|
octagonal-wheels 0.1.54. The implementation was first published as Commonlib
|
|
0.1.32 on the npm `next` tag. Current Self-hosted LiveSync 1.0.42 uses Commonlib
|
|
0.2.2. A mobile performance bound remains outstanding.
|
|
|
|
This document records the protocol and the integration plan used for the
|
|
published feature. The companion probe remains a standalone design check; it
|
|
does not exercise an application entry point.
|
|
|
|
## Scope
|
|
|
|
The published protocol offers the Ephemeral and Persistent modes only.
|
|
Ephemeral uses one fixed seven-day window shared by every implementation.
|
|
Arbitrary start dates, custom end dates, selectable window lengths, and rolling
|
|
seven-day lifetimes are outside this version.
|
|
|
|
Keep the current settings filtering, remote profiles, main and P2P selections,
|
|
and short/full export variants. Time binding affects opening the exported
|
|
settings; it does not alter Vault encryption, remote credentials, replication,
|
|
or settings already imported by another device.
|
|
|
|
The existing `settingsQR` format and QR aggregator are separate sharing paths.
|
|
They remain outside this design and must not be presented as time-bound.
|
|
|
|
## Time window
|
|
|
|
Use Unix milliseconds and the following protocol constants:
|
|
|
|
```text
|
|
windowMilliseconds = 604800000
|
|
windowNumber = floor(nowMilliseconds / windowMilliseconds)
|
|
windowStart = windowNumber * windowMilliseconds
|
|
usableUntil = (windowNumber + 1) * windowMilliseconds
|
|
```
|
|
|
|
The anchor is the Unix epoch, `1970-01-01T00:00:00Z`. Consequently, boundaries
|
|
fall on Thursdays at 00:00 UTC. This is a protocol convention, independent of
|
|
the device's locale, time zone, daylight-saving rules, and calendar week.
|
|
The anchor and window length are part of the derivation profile. Changing them
|
|
requires an explicit compatibility design; there is no visible version field
|
|
from which an importer can discover a different time condition.
|
|
|
|
For example, a URI generated at `2026-09-28T12:00:00Z` belongs to the window
|
|
`[2026-09-24T00:00:00Z, 2026-10-01T00:00:00Z)`. Ordinary import rejects it at
|
|
the end instant. Generating it one second before that instant leaves one
|
|
second of availability. The UI must not describe this as seven days from
|
|
generation.
|
|
|
|
Use safe, non-negative integer timestamps without 32-bit coercion. The
|
|
reader tries only its current window. There is no previous-window allowance,
|
|
future-window search, modulo wraparound, or grace period. Devices whose clocks
|
|
fall on different sides of a boundary can disagree; local date formatting
|
|
does not affect the calculation.
|
|
|
|
## Passphrase derivation and encryption
|
|
|
|
The time factor follows the counter construction used by
|
|
[TOTP, RFC 6238 section 4.2](https://www.rfc-editor.org/rfc/rfc6238.html#section-4.2).
|
|
This is a time-bound encryption format, not a TOTP authentication protocol:
|
|
there is no six-digit code, trusted validation server, or single-use state.
|
|
|
|
Reuse Commonlib's current `encryptString` and `decryptString` path. It uses
|
|
octagonal-wheels' salted PBKDF2/HKDF and AES-GCM implementation. Persistent
|
|
passes the entered passphrase directly to that path. Only Ephemeral applies
|
|
a full-length transformation:
|
|
|
|
```text
|
|
if mode == "persistent":
|
|
effectivePassphrase = enteredPassphrase
|
|
else:
|
|
hmacKey = SHA-256(UTF-8(enteredPassphrase))
|
|
context = JSON.stringify([
|
|
"livesync/setup-uri",
|
|
"tb1",
|
|
"ephemeral",
|
|
windowNumber
|
|
])
|
|
effectivePassphrase = lowercaseHex(HMAC-SHA-256(hmacKey, UTF-8(context)))
|
|
encryptedSettings = encryptString(preparedSettingsJSON, effectivePassphrase)
|
|
```
|
|
|
|
The mode strings are exactly `ephemeral` and `persistent`. JSON encoding,
|
|
field order, lowercase hexadecimal, and the domain/profile strings are part
|
|
of the proposed Ephemeral derivation. `tb1` is an internal derivation identifier;
|
|
it is not a URI prefix or stored field. Persistent applies no new transformation.
|
|
|
|
The initial SHA-256 provides a fixed-length HMAC key, including for an empty
|
|
low-level input. It does not replace password stretching: the existing
|
|
encryption function still performs PBKDF2. Do not trim or normalise the
|
|
entered passphrase within this transformation. Existing host requirements for
|
|
a non-empty passphrase remain in force; this design adds no new password
|
|
policy.
|
|
|
|
At the assessed dependency versions, the encryption path uses PBKDF2-SHA-256
|
|
with 310,000 iterations, HKDF-SHA-256, and AES-256-GCM with a 128-bit tag. Its
|
|
`%$` payload includes the material needed for decryption. The PBKDF2 salt may
|
|
be reused within a session; the IV and HKDF salt are generated for each
|
|
encryption. Retain these existing semantics rather than claiming a new
|
|
per-URI PBKDF2 salt or introducing another encryption implementation.
|
|
|
|
For the synthetic passphrase `test-passphrase`, fixed derivation vectors are:
|
|
|
|
```text
|
|
context: ["livesync/setup-uri","tb1","ephemeral",1234]
|
|
effectivePassphrase: b39361c51f0b7bd835554db1dffbc9a540fb30789aa62bf53e39e07d1073013b
|
|
|
|
mode: persistent
|
|
effectivePassphrase: test-passphrase
|
|
```
|
|
|
|
The probe checks the Ephemeral Web Crypto output against a value calculated
|
|
separately through Node's SHA-256 and HMAC interface, and checks that Persistent
|
|
retains the exact input. Encryption remains randomised; these vectors specify
|
|
the effective passphrase, not the complete URI.
|
|
|
|
This composition needs Commonlib changes but no new cryptographic dependency
|
|
or remote service. The prototype verifies interoperability with the existing
|
|
encryption path; it is not an independent cryptographic audit.
|
|
|
|
## URI format and decoding
|
|
|
|
Keep the existing URI prefix and encrypted representation without a new marker:
|
|
|
|
```text
|
|
obsidian://setuplivesync?settings=<encodeURIComponent(encryptedSettings)>
|
|
```
|
|
|
|
Both modes use the existing `%$` encrypted representation. There is no mode
|
|
flag, timestamp, window number, duration, derivation-version marker, or separate
|
|
query parameter. For identical prepared settings, both modes have the same
|
|
binary layout and ciphertext length. Percent-encoded text length may vary with
|
|
the random ciphertext; equal URI text length is not a requirement.
|
|
|
|
Persistent delegates to the existing encoder with the entered passphrase and
|
|
the same settings-filtering options. A new marker or a transformed Persistent
|
|
passphrase would defeat older readers. Ephemeral uses the same external format
|
|
to preserve mode hiding. The protocol handler continues to reconstruct the URI
|
|
from the existing `settings` query value.
|
|
|
|
For a `%$` payload, including an existing URI made before this feature, the
|
|
updated decoder:
|
|
|
|
1. validates the envelope and captures the current window;
|
|
2. derives the Ephemeral candidate for that window and takes the entered
|
|
passphrase unchanged as the Persistent candidate;
|
|
3. attempts authenticated decryption with both candidates, without returning
|
|
early after the first success;
|
|
4. checks the current window again before returning an Ephemeral result; and
|
|
5. returns settings only if exactly one candidate is accepted.
|
|
|
|
Keep expected candidate failures internal. Two failed candidates produce one
|
|
generic opening failure. A wrong passphrase, an out-of-window Ephemeral URI,
|
|
and damaged authenticated ciphertext do not receive different user messages.
|
|
There is no information from which to report a definite historical end time
|
|
for an unreadable URI.
|
|
|
|
The raw-passphrase attempt is an intentional part of the two-candidate reader.
|
|
An Ephemeral URI from another window still fails both candidates with the
|
|
entered passphrase; this attempt does not remove its time condition. There is
|
|
no plaintext recovery, time-window search, or additional key fallback.
|
|
|
|
Older encryption representations other than `%$` retain their existing
|
|
legacy-only decoder and passphrase handling. Unsupported prefixes remain
|
|
unsupported. Malformed `%$` payloads fail authentication with both candidates.
|
|
Bound time-bound candidate work to two attempts; the existing decoder continues
|
|
to own any historical encryption-format compatibility trials.
|
|
|
|
Because the derivation profile is not stored, an older reader cannot identify
|
|
an Ephemeral URI and give an upgrade-specific error. It reports a decryption
|
|
failure. A future change to the Ephemeral profile must define its bounded
|
|
candidate policy explicitly; the reader must not infer arbitrary profiles or
|
|
scan them without a limit.
|
|
|
|
Two trials make the amount of cryptographic work predictable, but do not
|
|
constitute a constant-time implementation. This design does not claim to hide
|
|
the mode from a caller who can instrument the decoder and knows the passphrase.
|
|
|
|
## Generation and import interaction
|
|
|
|
Keep the existing passphrase prompt. After it, use the existing confirmation
|
|
dialogue to choose Time-bound (the Ephemeral encoder mode) or Compatible (the
|
|
Persistent encoder mode). Select Time-bound by default, show its exact absolute
|
|
end in the device's local time zone with its time-zone name or UTC offset, and
|
|
explain that this is the end of a fixed UTC window rather than seven days from
|
|
generation. Compatible has no time condition and remains readable by older
|
|
clients. No synchronised setting or permanent Vault preference is needed.
|
|
|
|
Commonlib exposes the current window end for this pre-generation choice.
|
|
Compare it with the `usableUntil` returned alongside the generated URI. If
|
|
the window changed during selection or encryption, show a fresh choice with
|
|
the new end before opening the existing copy dialogue. Persistent generation
|
|
does not consult the clock and returns `null` for `usableUntil`.
|
|
|
|
The existing copy dialogue does not monitor the clock. If it stays open across
|
|
the boundary, it can still display and copy a URI whose window has ended;
|
|
the reader will reject that URI. The end time was shown at the mode choice.
|
|
The copy dialogue does not claim a rolling week or silently switch to
|
|
Compatible.
|
|
|
|
Import requires no mode selector or date input. Recheck the window at the
|
|
end of decryption so that an Ephemeral operation crossing the boundary does
|
|
not return settings. Once settings have been returned successfully, subsequent
|
|
confirmation, setup, and synchronisation do not remain time-bound: the URI
|
|
has already been opened. This is an import boundary, not remote revocation.
|
|
|
|
## Security and privacy boundary
|
|
|
|
The hidden mode is an external-format property. A passive holder of the URI
|
|
does not receive an explicit time condition or Persistent marker. Randomised
|
|
encryption and the unchanged prepared settings representation avoid adding
|
|
a mode-dependent field or length difference.
|
|
|
|
The clock and reader are controlled by the receiving device. Anyone with the
|
|
correct passphrase can reproduce historical candidates, modify the reader,
|
|
or change the supplied clock. Including Persistent adds one possible
|
|
candidate; it does not make a small calendar search cryptographically hard.
|
|
Time binding adds no independent secret and is not a substitute for a strong
|
|
Setup URI passphrase.
|
|
|
|
The same URI can be opened repeatedly within its window. A replayed clock
|
|
earlier than generation, but inside that same window, also derives the same
|
|
key. Successfully copied settings and remote credentials remain usable after
|
|
the window ends, subject to their own remote policies.
|
|
|
|
No URI, entered or effective passphrase, derived key, decrypted settings, or
|
|
candidate-specific authentication result should enter ordinary logs or reports.
|
|
The generation screen necessarily reveals the selected mode and end time to
|
|
its user; hiding information in the URI does not hide that screen or a
|
|
separately shared message.
|
|
|
|
## Ownership and compatibility
|
|
|
|
Commonlib owns preparation of exported settings, the time-window calculation,
|
|
passphrase derivation, format parsing, and dual-candidate decoding. These
|
|
changes were made in the Commonlib repository. LiveSync consumes the reviewed
|
|
Commonlib package, following
|
|
[the Commonlib dependency workflow](../../devs.md#commonlib-dependency).
|
|
|
|
The implementation preserves the existing positional encoder API. Its third
|
|
and fourth arguments retain their meanings: properties to remove and whether
|
|
to omit default values. It adds a separate `encodeTimeBoundSetupURI` encoder
|
|
with an options object containing the mode and the existing export options.
|
|
Its result is:
|
|
|
|
```typescript
|
|
type TimeBoundSetupURIResult = {
|
|
uri: string;
|
|
usableUntil: number | null;
|
|
};
|
|
```
|
|
|
|
The new encoder defaults to Ephemeral. The old encoder retains its legacy
|
|
behaviour for callers which have not migrated. The updated
|
|
`decodeSettingsFromSetupURI` tries both candidates for `%$` while retaining its
|
|
settings return contract and historical format handling. Persistent generation
|
|
through the new API delegates to the old encoder. Time injection remains in
|
|
internal test seams, not an import option exposed to users.
|
|
|
|
| Surface | Published integration |
|
|
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| [Obsidian generation](../../src/serviceFeatures/setupObsidian/setupUri.ts) | Reuses the passphrase and copy dialogues, presents the mode choice and exact current window end, and uses the encoder for all copy variants and the copy event while preserving each variant's export filters. |
|
|
| [Obsidian import](../../src/modules/features/SetupWizard/dialogs/UseSetupURI.svelte) | Consumes the updated decoder and retains one authentication failure message. The existing prefix check indicates URI shape, not successful authentication. |
|
|
| [Protocol handler](../../src/serviceFeatures/setupObsidian/setupProtocol.ts) | Retains the existing `settings` reconstruction; encoded delimiters remain part of protocol validation for both modes. |
|
|
| [CLI setup](../../src/apps/cli/commands/runCommand.ts) | Uses the updated decoder and leaves settings unchanged after a rejection. |
|
|
| [WebPeer generator](../../src/apps/webpeer/src/P2PCheckSetup.ts) | Explicitly selects Time-bound mode and returns availability to its result UI; this generator does not offer a Persistent choice. |
|
|
| [Setup utility](../../utils/setup/generate_setup_uri.ts) | Accepts an explicit mode, defaults to Time-bound, and returns or prints the end time. Persistent is selected for durable provisioning output. |
|
|
| [Setup utility package facade](../../utils/setup/livesync-commonlib.ts) | Retains its independent Commonlib pin and associated lockfiles, separate from the root npm dependency. |
|
|
|
|
The Fly.io wrapper delegates to the setup utility and inherits the same
|
|
contract. The initial plan required each maintained generator to select a mode
|
|
explicitly. Non-interactive automation which needs a durable URI selects
|
|
Persistent.
|
|
|
|
WebPeer hides an Ephemeral URI and disables its copy and additional-device
|
|
actions after the window ends. Its connection monitor remains available for a
|
|
device that imported the URI before the end; opening the URI does not expire
|
|
the imported P2P credentials.
|
|
|
|
| URI | Updated reader | Reader without time-bound support |
|
|
| -------------- | ------------------------------------------------------- | --------------------------------- |
|
|
| Legacy | Existing behaviour, without a time condition | Existing behaviour |
|
|
| New Ephemeral | Opens in the current window with the correct passphrase | Rejected |
|
|
| New Persistent | Opens with the correct passphrase | Existing behaviour |
|
|
|
|
The assessed, unchanged Commonlib 0.1.27 decoder opens Persistent with the
|
|
entered passphrase and rejects Ephemeral with that same passphrase. Persistent
|
|
compatibility means compatibility with readers which already support the
|
|
ordinary generated `%$` representation; it does not add that representation
|
|
to still older readers. This design does not establish compatibility with every
|
|
historical reader or settings schema; compatibility fixtures are needed before
|
|
making broader claims about them.
|
|
|
|
Keep the original raw-passphrase behaviour of existing URIs. They are
|
|
indistinguishable from newly generated Persistent URIs to the updated reader.
|
|
The previous marked-envelope sketch rejected an added Persistent compatibility
|
|
case with `Unsupported encryption format`. The same synthetic settings and
|
|
entered passphrase now open through the unchanged decoder, while separate
|
|
tests retain old-reader rejection of Ephemeral and rejection outside its window.
|
|
|
|
## Feasibility evidence and validation
|
|
|
|
The [executable design probe](time_bound_setup_uri.probe.mjs) uses Web Crypto
|
|
for the proposed transformation and the actual Commonlib encoder/decoder for
|
|
settings filtering and encryption. It is an isolated specification exercise;
|
|
it is not imported by an application or registered in the production test suite.
|
|
|
|
Run it after installing the dependency versions assessed by this document:
|
|
|
|
```sh
|
|
NODE_OPTIONS=--max-old-space-size=512 node docs/design_docs/time_bound_setup_uri.probe.mjs
|
|
```
|
|
|
|
Alternatively, pass the directory of an unpacked Commonlib 0.1.27 package as
|
|
the first argument. Its module resolution must supply octagonal-wheels 0.1.54
|
|
and the remaining declared dependencies. The probe verifies the Commonlib
|
|
version before running; it does not install or download packages.
|
|
|
|
The assessment used Node.js 24.18.0 and registry artefacts for Commonlib 0.1.27
|
|
and octagonal-wheels 0.1.54, verified against the lockfile's SHA-512 values.
|
|
All 22 cases passed:
|
|
|
|
- fixed Ephemeral derivation checked through Web Crypto and Node HMAC, and an
|
|
unchanged Persistent passphrase;
|
|
- current-window success, previous/later-window rejection, and exact end;
|
|
- Persistent across distant timestamps and both candidate attempts on success;
|
|
- wrong-passphrase rejection and a shared external format;
|
|
- stable end-time metadata and randomised repeated encryption;
|
|
- legacy import, old-reader acceptance of Persistent, and old-reader rejection
|
|
of Ephemeral;
|
|
- unsupported-prefix, malformed-ciphertext, and ciphertext-tampering rejection;
|
|
- Persistent generation without reading the clock;
|
|
- protocol query decoding/re-encoding;
|
|
- generation and import crossing a boundary;
|
|
- equivalent timestamps expressed with different UTC offsets;
|
|
- Unicode, whitespace, empty low-level passphrase input, and distinct Ephemeral
|
|
and raw-passphrase candidates;
|
|
- the deliberate limitation that clock rollback reproduces an old key.
|
|
|
|
The probe demonstrates feasibility and baseline interoperability. Application
|
|
integration is recorded above; the probe does not establish formal mode
|
|
indistinguishability or a mobile latency bound. The encryption API may perform
|
|
two PBKDF2 derivations on a cold import. Do not infer production performance
|
|
from this small probe, whose operations can reuse the dependency's in-memory
|
|
key cache.
|
|
|
|
The original implementation validation plan covered Commonlib unit and
|
|
packed-package tests, plus shared fixed-input protocol vectors across consumers.
|
|
It called for targeted LiveSync tests of generation metadata, cancellation, a
|
|
window change during mode selection, encoded deep links, and rejected imports
|
|
without settings writes, alongside CLI, WebPeer, and Deno setup-tool round trips
|
|
for both modes.
|
|
|
|
The plan also called for the LiveSync checks, unit suite, and production build,
|
|
one broad process at a time with `NODE_OPTIONS=--max-old-space-size=3072` and
|
|
bounded test workers. It used the focused real-Obsidian Setup URI workflow for
|
|
copy/paste, deep-link import, cancellation, and successful setup. Selection-
|
|
boundary handling was to use an injected test clock; mobile behaviour requires
|
|
a supported runtime and must not change the host's system clock. Protocol tests
|
|
do not require a remote database service.
|
|
|
|
The original protocol assessment found no blocker in the Time-bound URI
|
|
protocol. The Commonlib candidate based on 0.1.31 passed its type, unit,
|
|
boundary, release-process, and packed-package gates before the 0.1.32 release. A
|
|
clean `npm ci` against the published 0.1.32 artefact and the frozen Deno tool
|
|
lock resolved the same registry integrity. In the original implementation
|
|
baseline, LiveSync passed its source checks, production build, and 1,034 unit
|
|
tests on a local stack with [PR #1222](https://github.com/vrtmrz/obsidian-livesync/pull/1222).
|
|
The two CLI installer tests needed a runner which permits child processes; both
|
|
passed under normal process permissions. The Deno setup-tool suite, CLI setup
|
|
and file-operation contract, and WebPeer browser tests passed before stacking.
|
|
|
|
The recorded real-Obsidian generation checks passed on desktop and emulated mobile,
|
|
including the displayed Time-bound end, Compatible output, and mobile touch
|
|
targets. A two-device CouchDB workflow passed on the local stack: the
|
|
provisioning tool created a generation 12 database, the first device generated
|
|
a Time-bound Setup URI after declaring its required generation 13 feature, and
|
|
the second device imported it. An ordinary note completed a round trip, and a
|
|
hidden snippet synchronised. The separate real-Obsidian tests for live remote
|
|
feature changes, internal Metadata migration, and a percent-prefixed E2EE
|
|
passphrase also passed with published Commonlib 0.1.32. Unit and protocol tests
|
|
covered window boundaries and preserved the existing distinction between an
|
|
empty passphrase and cancellation. The WebPeer browser test confirmed that its
|
|
monitor remained available after the URI window ended for a device which had
|
|
already imported the URI.
|
|
|
|
The Commonlib 0.1.33 integration included the merged host changes from
|
|
PR #1222 and PR #1225. At that baseline, npm 10 and normal clean installations
|
|
preserved the lockfile; source checks, the production build, 1,064 unit tests,
|
|
and ten Deno setup-tool tests passed. Both URI modes preserved ID recovery and
|
|
explicit legacy ID selection.
|
|
|
|
Recorded real-Obsidian tests covered independent ID keys through Time-bound,
|
|
Compatible, and QR setup, encrypted local persistence, natural restarts of both
|
|
devices, and bidirectional note transfers with stable document and Chunk IDs.
|
|
Incorrect passphrases and past-window URIs left runtime and persisted settings
|
|
unchanged. The past clock belonged to an isolated fixture worker, not Obsidian
|
|
or the host. CouchDB tests also covered a custom ID source and recovery code,
|
|
rejection of incompatible document keys before remote writes, and the Doctor's
|
|
decline, reminder, dismissal, and later acceptance paths. The detailed commands,
|
|
remaining published-artefact checks, and physical-device checks are recorded in the
|
|
[real Obsidian test guide](../../test/e2e-obsidian/README.md#combined-setup-and-security-regression-checks).
|
|
|
|
Compatible preserves the URI encryption format; each receiving client must
|
|
still support the shared settings and the remote's declared features. The host
|
|
compatibility checks from PR #1222 remain necessary. These results do not
|
|
establish a mobile performance bound. The initial implementation changed only
|
|
primary-language resources; translation changes remain a separate scope.
|