Architecture#
This page describes the internal design of SafePassage and the reasoning behind its key implementation choices.
WebAssembly Argon2 engine#
KeePass KDBX4 databases derive their master key using Argon2, which is
computationally expensive by design. SafePassage registers a
hash-wasm-backed Argon2
implementation with kdbxweb’s crypto engine, so the key derivation runs
as WebAssembly instead of pure JavaScript. This keeps large iteration/memory
parameters from freezing Obsidian’s UI thread or exhausting memory on
lower-powered devices such as phones and tablets.
Vault-relative file access#
Database and key files are read and written exclusively through Obsidian’s
Vault API (vault.readBinary / vault.modifyBinary) using
vault-relative, normalized paths — never Node’s fs module directly.
Because the Vault API abstracts over the underlying storage, the exact same
profile configuration (a path like Secrets/vault.kdbx) resolves
correctly on desktop and on mobile (iOS/Android), where the plugin has no
direct filesystem access.
In-memory session keyring#
Unlocked databases are kept in a Map inside KdbxService, keyed by
profile ID, entirely in JavaScript heap memory — never written to disk.
Master passwords typed into the unlock modal are cached the same way in
KeyringService so a profile can be silently re-unlocked in the
background (for example when a note with an inline chip is opened again)
without re-prompting the user, for as long as its session is alive.
Session expiry and locking#
SessionService runs a setTimeout per unlocked profile based on the
profile’s Session Expiry Lifetime setting: immediate (locks again after
a single lookup), 5 minutes, 15 minutes, or indefinitely until Obsidian
closes or the profile is locked manually. When a timer fires, the
corresponding entry is removed from both the active-database map and the
in-memory keyring, so its secrets stop being reachable from that point on.
Protected field values#
Sensitive KeePass fields (passwords, and any field marked “protected” in
the database) are wrapped by kdbxweb in a ProtectedValue container
rather than being kept as a plain string. SafePassage calls
ProtectedValue.getText() only at the point a value is actually needed —
to render a masked chip’s copy action, to populate a table cell, or to
write a new entry — and does not retain the decoded plain string beyond
that call.
Clipboard sanitation#
When a secret is copied to the clipboard, ClipboardService starts a
timer (length configurable per profile) and remembers the exact value it
wrote. When the timer fires, it first re-reads the clipboard and only
overwrites it with an empty string if the contents still match what
SafePassage copied — so a value the user has since replaced with something
else is never clobbered.
Inline chip and table rendering#
Notes reference secrets through two mechanisms that both resolve against
the same KdbxService:
Inline tokens (
{{sp:<profile>/<entry>#<field>}}) are matched by a CodeMirror decoration in Live Preview and by a Markdown post-processor in Reading view, both rendering the same masked chip component.safe-passagefenced code blocks are handled by a dedicated block processor that parses the YAML body and renders a full credential table.
Both paths only ever display a masked placeholder in the DOM; the
underlying secret is fetched from KdbxService on demand — for example
when the user clicks a chip to copy its value — rather than being embedded
in the rendered HTML.