npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tauri-plugin-keyring-store-api

v0.2.0

Published

JavaScript API for tauri-plugin-keyring-store — OS keychain / credential manager storage with Stronghold-compatible sessions.

Readme

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. iOS Data Protection
  4. Installation
  5. Usage
  6. Direct account API
  7. Cargo features
  8. Permissions
  9. Relationship to Stronghold
  10. Development
  11. Testing
  12. Contributing
  13. Partners
  14. 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).


iOS Data Protection

On iOS the plugin uses the protected Keychain backend. 0.2.0 writes new secrets with AfterFirstUnlock by default so background tasks can read them after the first unlock of the day, even if the screen is locked again.

| Layer | Responsibility | |-------|----------------| | This plugin | keyring-core, iOS access-policy on write, availability(), Error::KeychainLocked, *_for_background helpers | | Host app (e.g. Subly) | Service name, OnceLock<KeyringStore>, map Error → app errors, call get_password_for_background from sync |

Write policy

use tauri_plugin_keyring_store::{Builder, WriteAccessibility};

// Plugin init (Tauri)
Builder::new()
    .ios_write_accessibility(WriteAccessibility::AfterFirstUnlock)
    .build();

// Or on a standalone KeyringStore
let store = KeyringStore::new("com.example.app")
    .with_write_accessibility(WriteAccessibility::AfterFirstUnlock);

Reads use Entry::new (same service/account). Writes use Entry::new_with_modifiers with access-policy: after-first-unlock on iOS only. macOS Login Keychain ignores WriteAccessibility for writes.

Migration: entries created before 0.2.0 may still use WhenUnlocked until the user re-saves them (set_password / set_bytes). There is no bulk rekey command in this release.

Locked device vs real errors

  • Foreground: get_passwordErr(Error::KeychainLocked) when the device is locked.
  • Background sync: get_password_for_backgroundOk(None) when availability() is Locked.
  • exists_nonempty_for_backgroundOk(false) when locked.
use tauri_plugin_keyring_store::{Error, KeyringAvailability, KeyringStore};

# fn example(store: &KeyringStore) -> Result<(), Error> {
match store.availability() {
    KeyringAvailability::Available => { /* normal UI read */ }
    KeyringAvailability::Locked => { /* prompt unlock or defer */ }
}
let secret = store.get_password_for_background("oauth-token")?;
# Ok(())
# }

IPC commands are unchanged in 0.2.0; Rust backends can use KeyringStore directly without new Tauri commands.


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.2.0"

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

tauri-plugin-keyring-store = { version = "0.2.0", 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, map_keyring_err unit 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