tauri-plugin-keyring-store 0.1.5

Stronghold-inspired secrets and crypto helpers backed by the OS credential store (keyring-core).
Documentation

npm version Crates.io Documentation GitHub issues GitHub stars Donate

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

  1. Features
  2. Platform support
  3. Installation
  4. Usage
  5. Direct account API
  6. Cargo features
  7. Permissions
  8. Relationship to Stronghold
  9. Development
  10. Testing
  11. Contributing
  12. Partners
  13. License

Features

  • Cross-platform keyring via keyring-core 1.x and 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 the crypto feature is enabled.
  • Optional crypto feature (default): SLIP10/BIP39/Ed25519 via iota-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):

pnpm run tauri add keyring-store

The CLI wires the Rust crate into src-tauri and adds the tauri-plugin-keyring-store-api npm package when needed.

Other package managers:

npm run tauri add keyring-store
yarn tauri add keyring-store

With the CLI installed via Cargo: cargo tauri add keyring-store.

Manual — Rust (src-tauri)

cd src-tauri
cargo add tauri-plugin-keyring-store

Or in Cargo.toml:

tauri-plugin-keyring-store = "0.1.5"

Disable the SLIP10/BIP39/Ed25519 stack (storage + backup IPC only):

tauri-plugin-keyring-store = { version = "0.1.5", default-features = false }

Manual — JavaScript

pnpm add tauri-plugin-keyring-store-api

You still need .plugin(tauri_plugin_keyring_store::init()) (or Builder) in Rust.


Usage

Backend

fn main() {
  tauri::Builder::default()
    .plugin(tauri_plugin_keyring_store::init())
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

Custom service name (defaults to identifier in tauri.conf.json):

tauri_plugin_keyring_store::Builder::new()
  .service("com.mycompany.myapp.credentials")
  .build()

Rust — access the store from commands / plugins

use tauri::Manager;
use tauri_plugin_keyring_store::KeyringExt;

#[tauri::command]
fn save_api_token(app: tauri::AppHandle, token: String) -> Result<(), String> {
  app.keyring().store
    .set_password("manual.example.token", &token)
    .map_err(|e| e.to_string())
}

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

cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo build --no-default-features

pnpm install
pnpm build
pnpm test

Rustdoc logo (after push to main): PNG is generated from assets/docs-logo.svg:

rsvg-convert -w 128 -h 128 assets/docs-logo.svg -o assets/docs-logo.png

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