Tauri Plugin Keyring Store
Store secrets and wallet-style procedures using the OS credential store (macOS Keychain, Windows Credential Manager, Linux Secret Service, Android Keystore, iOS Data Protection). The guest API mirrors tauri-plugin-stronghold sessions, clients, store, vault, and crypto procedures — but there is no encrypted snapshot file: everything maps to hashed keyring entries under your app service name (defaults to the Tauri bundle identifier).
Table of contents
- Features
- Platform support
- Installation
- Usage
- Direct account API
- Cargo features
- Permissions
- Relationship to Stronghold
- Development
- Testing
- Contributing
- Partners
- License
Features
- Cross-platform keyring via
keyring-core1.xand official backend crates (native stores only — no silent in-memory fallback). - Rust-first API:
app.keyring()exposes [KeyringPlugin] with [KeyringStore] for backend code without IPC. - Stronghold-shaped JS API:
KeyringSession,KeyringClient,KeyringStoreView,KeyringVault+ SLIP10 / BIP39 / Ed25519 procedures when thecryptofeature is enabled. - Optional
cryptofeature (default): SLIP10/BIP39/Ed25519 viaiota-crypto; secrets stored as Base64 in the OS vault.
Platform support
| Platform | Backend |
|---|---|
| macOS | Login Keychain |
| iOS | Protected (Data Protection) Keychain |
| Windows | Credential Manager |
| Linux | Secret Service (DBus; crypto-rust — no host OpenSSL required to build) |
| Android | Android Keystore + SharedPreferences |
Linux desktops need a Secret Service (e.g. GNOME Keyring / KWallet). Headless CI often has no user session — avoid relying on the live keyring there (see Testing). On Android, transitive deps may pull OpenSSL; your app may need openssl-sys with vendored for cross-builds (see Subly-style setups).
Installation
Automatic (recommended)
From your Tauri app root (where package.json and the tauri script live):
The CLI wires the Rust crate into src-tauri and adds the tauri-plugin-keyring-store-api npm package when needed.
Other package managers:
With the CLI installed via Cargo: cargo tauri add keyring-store.
Manual — Rust (src-tauri)
Or in Cargo.toml:
= "0.1.5"
Disable the SLIP10/BIP39/Ed25519 stack (storage + backup IPC only):
= { = "0.1.5", = false }
Manual — JavaScript
You still need .plugin(tauri_plugin_keyring_store::init()) (or Builder) in Rust.
Usage
Backend
Custom service name (defaults to identifier in tauri.conf.json):
new
.service
.build
Rust — access the store from commands / plugins
use Manager;
use KeyringExt;
Sessions opened from the frontend (initialize) are tracked separately; low-level KeyringStore calls use whatever account string you pass.
Snapshot path (first argument of KeyringSession.load)
This string is not a path to a file on disk on any OS (not the macOS Keychain file, not a Windows “vault” path, not a Linux D-Bus socket path). It is a logical session id: the plugin hashes it together with client / vault / record names into stable OS keyring account strings under your app service (default: Tauri bundle identifier).
| You choose | Effect |
|---|---|
| Same string on every platform | Same secrets namespace everywhere (typical). |
| Different strings | Different isolated namespaces (e.g. per user or per “wallet”). |
Looks like a path, e.g. '/wallet/main' |
Fine — purely a label; no requirement that the folder exists. |
Use stable ASCII-ish identifiers for portability. The second argument is the Stronghold-compatible password: it is not used to unlock a snapshot file here; Rust zeroizes it. Use '' or any placeholder if you are not migrating from Stronghold.
Frontend — minimal flow
import { KeyringSession } from 'tauri-plugin-keyring-store-api'
const session = await KeyringSession.load('/wallet/main', '')
const client = await session.createClient('main')
await client.getStore().insert('prefs', [...new TextEncoder().encode('{}')])
await session.unload()
JavaScript API reference
Invokes use plugin:keyring-store|<command>. SLIP10 / BIP39 / Ed25519 helpers on KeyringVault require the Rust crate’s crypto feature (enabled by default).
ping
import { ping } from 'tauri-plugin-keyring-store-api'
const value = await ping('hello') // string | null
KeyringSession
| Method | Purpose |
|---|---|
KeyringSession.load(snapshotPath, password) |
Registers the session (initialize IPC). |
session.unload() |
Drops session tracking (destroy). |
session.createClient(client) |
First-time client namespace (create_client). |
session.loadClient(client) |
Existing client (load_client). |
session.save() |
No-op on keyring (save IPC for Stronghold parity). |
import { KeyringSession } from 'tauri-plugin-keyring-store-api'
const session = await KeyringSession.load('/app/secrets', '')
const created = await session.createClient('desktop')
const again = await session.loadClient('desktop')
await session.save()
await session.unload()
KeyringClient
| Method | Purpose |
|---|---|
client.getStore() |
JSON-like byte records (get_store_record / save_store_record / remove_store_record). |
client.getVault(name) |
Binary vault + crypto procedures (save_secret / remove_secret / execute_procedure). |
KeyringStoreView (from client.getStore())
| Method | Purpose |
|---|---|
get(key) |
Read bytes or null. |
insert(key, value, lifetime?) |
Write bytes; lifetime is ignored (Stronghold compat). |
remove(key) |
Delete record; returns previous bytes or null. |
const store = client.getStore()
const raw = await store.get('prefs')
await store.insert('prefs', [...new TextEncoder().encode('{}')])
await store.remove('prefs')
Location and KeyringVault
Build locations for vault records and procedure outputs:
import { Location } from 'tauri-plugin-keyring-store-api'
const generic = Location.generic('WALLET', 'seed.bin')
const row = Location.counter('WALLET', 0)
KeyringVault method |
IPC / behavior |
|---|---|
insert(recordPath, secret) |
save_secret |
remove(location) |
remove_secret (pass Location.generic or Location.counter) |
generateSLIP10Seed(output, sizeBytes?) |
execute_procedure SLIP10Generate |
deriveSLIP10(chain, 'Seed' | 'Key', src, output) |
SLIP10Derive |
recoverBIP39(mnemonic, output, passphrase?) |
BIP39Recover |
generateBIP39(output, passphrase?) |
BIP39Generate |
getEd25519PublicKey(privateKeyLocation) |
PublicKey (Ed25519) |
signEd25519(privateKeyLocation, msg) |
Ed25519Sign (msg is UTF-8) |
import { KeyringSession, Location } from 'tauri-plugin-keyring-store-api'
const session = await KeyringSession.load('/vault-a', '')
const vault = (await session.createClient('c1')).getVault('PRIMARY')
const out = Location.generic('PRIMARY', 'slip10-master')
await vault.generateSLIP10Seed(out, 32)
await session.unload()
Binary vault records (not the procedure helpers above):
import { KeyringSession, Location } from 'tauri-plugin-keyring-store-api'
const session = await KeyringSession.load('/vault-a', '')
const vault = (await session.createClient('c1')).getVault('SECRETS')
await vault.insert('blob.bin', [0xde, 0xad])
await vault.remove(Location.generic('SECRETS', 'blob.bin'))
await session.unload()
Naming helpers
import { joinKeyPrefix, splitKeyPrefix, KEYRING_PREFIX_SEPARATOR } from 'tauri-plugin-keyring-store-api'
const account = joinKeyPrefix('billing', 'stripe_sk')
const [prefix, name] = splitKeyPrefix(account)
void KEYRING_PREFIX_SEPARATOR // '.'
Direct account API (bulk, exists, naming, backup)
Raw account strings are the OS keyring entry names under your app service (defaults to the bundle identifier). These commands avoid session hashing — useful for app-controlled keys.
| IPC command | Purpose |
|---|---|
get_passwords |
Read many UTF-8 secrets (parallel Vec, max 256 accounts per call). |
set_passwords |
Write many { account, secret } pairs. |
delete_passwords |
Delete many accounts. |
password_exists |
true if a non-empty secret exists (exists_nonempty). |
export_passwords_plain / import_passwords_plain |
JSON backup blob over IPC. |
export_passwords_encrypted / import_passwords_encrypted |
Argon2id + ChaCha20-Poly1305 envelope (always compiled; independent of the crypto feature). |
Naming (application convention): use prefix.name with a single dot — helpers join_prefix / split_prefixed in Rust, and joinKeyPrefix / splitKeyPrefix in guest-js (see Usage → Naming helpers). The OS keyring still does not support listing by prefix; keep your own index of logical keys if needed.
Security — plaintext backup: export_passwords_plain / import_passwords_plain move secrets in the clear across IPC to the webview. Use only in trusted UI flows, or prefer export_passwords_encrypted / disk encryption.
Guest-js examples
import {
getPasswords,
setPasswords,
deletePasswords,
passwordExists,
exportPasswordsPlain,
importPasswordsPlain,
exportPasswordsEncrypted,
importPasswordsEncrypted,
joinKeyPrefix,
} from 'tauri-plugin-keyring-store-api'
const account = joinKeyPrefix('app', 'api_token')
await setPasswords([{ account, secret: 'secret-value' }])
const values = await getPasswords([account]) // (string | null)[]
const exists = await passwordExists(account)
const plain = await exportPasswordsPlain([account])
await importPasswordsPlain(plain)
const enc = await exportPasswordsEncrypted([account], 'user-passphrase')
await importPasswordsEncrypted(enc, 'user-passphrase')
await deletePasswords([account])
Cargo features
| Feature | Default | Description |
|---|---|---|
crypto |
yes | SLIP10 / BIP39 / Ed25519 execute_procedure via iota-crypto. Encrypted backup (Argon2 + ChaCha) is always available without this flag. |
Permissions
Use keyring-store:default or granular keyring-store:allow-* (see permissions/default.toml). Commands: plugin:keyring-store|<command>.
Relationship to tauri-plugin-stronghold
| Stronghold | This plugin |
|---|---|
| Password-derived snapshot | No snapshot file; OS stores secrets |
save() writes snapshot |
save() is a no-op (compat) |
| Procedures in Stronghold VM | In-process crypto; outputs in keyring |
Development
Rustdoc logo (after push to main): PNG is generated from assets/docs-logo.svg:
Testing
- Rust:
cargo test— deterministic account-key tests and serde roundtrips do not need D-Bus. Tests that call the real OS store are#[ignore]; run locally where Secret Service / Keychain is available. - JavaScript:
pnpm test(Vitest) mocks@tauri-apps/api/core.
Contributing
Issues and pull requests are welcome on GitHub.
Partners
Contributions and sponsorship help maintain this and related plugins. Thank you for your support.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
SPDX-License-Identifier: MIT OR Apache-2.0