Architecture¶
flowchart TD
classDef local fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000;
classDef secure fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#000;
classDef remote fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#000;
classDef wasm fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px,color:#000;
classDef plugin fill:#ede7f6,stroke:#4527a0,stroke-width:2px,color:#000;
subgraph Device ["Your machine — full trust"]
direction TB
Client["Desktop client\n(Tauri · Rust + React)\nDecryption keys + plaintext\nvault in memory only"]:::secure
Vault[("Local vault file\n$APP_DATA/voltius/secrets.enc\nXChaCha20-Poly1305 ciphertext")]:::local
subgraph PluginBox ["Plugins (renderer — full app privileges)"]
Plugins["Bundled + installed ESM plugins\nRun in-process; PluginAPI is the\nsupported surface, not a sandbox"]:::plugin
end
Client <==>|"enc_key over Tauri IPC\nencrypt / decrypt"| Vault
Client -->|"PluginAPI (supported surface);\ntrust is established at install time"| Plugins
end
subgraph Cloud ["Remote services — zero knowledge"]
direction TB
Auth[("Auth server\nauth.voltius.app\nauth_key hashes · JWTs")]:::remote
Relay[("Sync relay\nsync.voltius.app\nEncrypted CRDT payloads")]:::remote
Portal["Web portal\napp.voltius.app (Next.js)\nvoltius-crypto → WASM"]:::wasm
Gist[("GitHub Gist\ngist.github.com\nEncrypted app-state blobs")]:::remote
end
Client -->|"email + auth_key + JWT\n(never password or enc_key)"| Auth
Client <==>|"ciphertext only"| Relay
Client <==>|"encrypted blobs + your PAT"| Gist
Portal -.->|"same crate, same account —\nWASM, no local vault"| Auth
Voltius runs as three independent components plus the optional sync layer.
Components¶
| Component | Where it runs | What it holds |
|---|---|---|
| Desktop client | Your machine (Tauri / Rust + React) | Decryption keys, plaintext vault in memory only |
| Local vault file | $APP_DATA/voltius/secrets.enc |
XChaCha20-Poly1305 ciphertext, on disk |
| Auth server | auth.voltius.app (or your self-host) |
auth_key hashes, account metadata, JWTs |
| Sync relay | sync.voltius.app (or your self-host) |
Encrypted CRDT payloads |
| Web portal | app.voltius.app (Next.js) |
Same voltius-crypto crate, compiled to WASM |
| Gist host (Gist sync only) | gist.github.com (your account) |
Encrypted per-device app-state blobs |
Trust boundaries¶
- Inside the Tauri process — full trust. The Rust backend never exposes raw secrets to the JS frontend except via Tauri IPC, and even then only when explicitly needed (e.g. to display a password in the UI).
- The auth server — sees
auth_key(an Argon2id derivation), email, machine fingerprints, JWTs. Never sees the password orenc_key. - The sync relay — sees encrypted blobs. Cannot decrypt them.
- GitHub Gist — same: encrypted blobs only, plus the PAT you provided.
Key separation¶
Three independent keys are derived from the same password:
| Key | Use |
|---|---|
auth_key |
Sent to the auth server for login. Server stores a hash of this — not the password. |
enc_key |
Encrypts the local vault. Never leaves the device. |
gist_enc_key |
Encrypts Gist-sync blobs. Derived from a passphrase + manifest salt; distinct from enc_key. |
flowchart LR
classDef cleartext fill:#ffebee,stroke:#c62828,stroke-width:2px,color:#000;
classDef secure fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#000;
classDef remote fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#000;
classDef local fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000;
Pass["Password"]:::cleartext
KDF["Argon2id + HKDF-SHA256"]:::secure
Pass --> KDF
KDF -->|"login only"| AuthKey(("auth_key")):::secure
KDF -->|"vault only"| EncKey(("enc_key")):::secure
KDF -->|"+ manifest salt"| GistKey(("gist_enc_key")):::secure
AuthKey -->|"hash stored"| Server[("Auth server")]:::remote
EncKey -->|"never leaves device"| Disk[("secrets.enc")]:::local
GistKey -->|"never leaves device"| Blobs[("Gist blobs")]:::local
Compromise of one does not yield the others.
Plugins¶
Plugins run as ESM modules in the renderer process, with the app's full privileges — they are not sandboxed or isolated from the host. PluginAPI is the supported surface: stable across releases, permission-declared, and rendered consistently. It intentionally omits terminal I/O, another plugin's vault keys, direct Tauri commands, and SSH tunnels — but those omissions are scope, not a security boundary. A plugin is trusted code, the same as a VS Code or Obsidian extension.
The trust boundary is therefore install time, not runtime:
- Marketplace submissions are reviewed before listing.
- Marketplace installs land disabled, with the plugin's declared permissions shown first.
- When a listing carries a content hash of its bundle, the install is verified against it and refused on mismatch — binding the reviewed artifact to the executed one (installs from listings without a bound hash are marked Unverified; hash binding is rolling out across the marketplace).
- Only install plugins from a source you trust.
See Plugins → Developing for what the supported surface does and doesn't cover.