{"id":51896873,"url":"https://github.com/s00d/tauri-plugin-keyring-store","last_synced_at":"2026-07-26T11:00:58.174Z","repository":{"id":357557893,"uuid":"1237459759","full_name":"s00d/tauri-plugin-keyring-store","owner":"s00d","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-22T06:45:16.000Z","size":579,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T14:58:44.417Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/s00d.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"custom":["https://www.donationalerts.com/r/s00d88"]}},"created_at":"2026-05-13T07:49:15.000Z","updated_at":"2026-05-22T06:45:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/s00d/tauri-plugin-keyring-store","commit_stats":null,"previous_names":["s00d/tauri-plugin-keyring-store"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/s00d/tauri-plugin-keyring-store","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s00d%2Ftauri-plugin-keyring-store","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s00d%2Ftauri-plugin-keyring-store/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s00d%2Ftauri-plugin-keyring-store/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s00d%2Ftauri-plugin-keyring-store/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/s00d","download_url":"https://codeload.github.com/s00d/tauri-plugin-keyring-store/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/s00d%2Ftauri-plugin-keyring-store/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35911748,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-26T02:00:06.503Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-26T11:00:57.501Z","updated_at":"2026-07-26T11:00:58.167Z","avatar_url":"https://github.com/s00d.png","language":"Rust","funding_links":["https://www.donationalerts.com/r/s00d88"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/docs-logo.svg\" width=\"128\" height=\"128\" alt=\"tauri-plugin-keyring-store logo\" /\u003e\n\u003c/p\u003e\n\n[![npm version](https://img.shields.io/npm/v/tauri-plugin-keyring-store-api/latest?style=for-the-badge)](https://www.npmjs.com/package/tauri-plugin-keyring-store-api)\n[![Crates.io](https://img.shields.io/crates/v/tauri-plugin-keyring-store?style=for-the-badge)](https://crates.io/crates/tauri-plugin-keyring-store)\n[![Documentation](https://img.shields.io/badge/docs-docs.rs-blue?style=for-the-badge)](https://docs.rs/tauri-plugin-keyring-store/)\n[![GitHub issues](https://img.shields.io/github/issues/s00d/tauri-plugin-keyring-store?style=for-the-badge)](https://github.com/s00d/tauri-plugin-keyring-store/issues)\n[![GitHub stars](https://img.shields.io/github/stars/s00d/tauri-plugin-keyring-store?style=for-the-badge)](https://github.com/s00d/tauri-plugin-keyring-store/stargazers)\n[![Donate](https://img.shields.io/badge/Donate-Donationalerts-ff4081?style=for-the-badge)](https://www.donationalerts.com/r/s00d88)\n\n# Tauri Plugin Keyring Store\n\nStore 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`](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/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).\n\n---\n\n## Table of contents\n\n1. [Features](#features)\n2. [Platform support](#platform-support)\n3. [iOS Data Protection](#ios-data-protection)\n4. [Installation](#installation)\n5. [Usage](#usage)\n6. [Direct account API](#direct-account-api-bulk-exists-naming-backup)\n7. [Cargo features](#cargo-features)\n8. [Permissions](#permissions)\n9. [Relationship to Stronghold](#relationship-to-tauri-plugin-stronghold)\n10. [Development](#development)\n11. [Testing](#testing)\n12. [Contributing](#contributing)\n13. [Partners](#partners)\n14. [License](#license)\n\n---\n\n## Features\n\n- **Cross-platform keyring** via [`keyring-core`](https://crates.io/crates/keyring-core) `1.x` and official backend crates (native stores only — no silent in-memory fallback).\n- **Rust-first API**: `app.keyring()` exposes [`KeyringPlugin`] with [`KeyringStore`] for backend code without IPC.\n- **Stronghold-shaped JS API**: [`KeyringSession`](guest-js/index.ts), [`KeyringClient`](guest-js/index.ts), [`KeyringStoreView`](guest-js/index.ts), [`KeyringVault`](guest-js/index.ts) + SLIP10 / BIP39 / Ed25519 procedures when the `crypto` feature is enabled.\n- **Optional `crypto` feature** (default): SLIP10/BIP39/Ed25519 via [`iota-crypto`](https://crates.io/crates/iota-crypto); secrets stored as Base64 in the OS vault.\n\n---\n\n## Platform support\n\n| Platform | Backend |\n|----------|---------|\n| macOS | Login Keychain |\n| iOS | Protected (Data Protection) Keychain |\n| Windows | Credential Manager |\n| Linux | Secret Service (DBus; `crypto-rust` — no host OpenSSL required to **build**) |\n| Android | Android Keystore + SharedPreferences |\n\nLinux 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](#testing)). On Android, transitive deps may pull OpenSSL; your app may need `openssl-sys` with `vendored` for cross-builds (see Subly-style setups).\n\n---\n\n## iOS Data Protection\n\nOn 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.\n\n| Layer | Responsibility |\n|-------|----------------|\n| **This plugin** | `keyring-core`, iOS `access-policy` on write, `availability()`, `Error::KeychainLocked`, `*_for_background` helpers |\n| **Host app** (e.g. Subly) | Service name, `OnceLock\u003cKeyringStore\u003e`, map `Error` → app errors, call `get_password_for_background` from sync |\n\n### Write policy\n\n```rust,no_run\nuse tauri_plugin_keyring_store::{Builder, WriteAccessibility};\n\n// Plugin init (Tauri)\nBuilder::new()\n    .ios_write_accessibility(WriteAccessibility::AfterFirstUnlock)\n    .build();\n\n// Or on a standalone KeyringStore\nlet store = KeyringStore::new(\"com.example.app\")\n    .with_write_accessibility(WriteAccessibility::AfterFirstUnlock);\n```\n\nReads 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.\n\n**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.\n\n### Locked device vs real errors\n\n- Foreground: `get_password` → `Err(Error::KeychainLocked)` when the device is locked.\n- Background sync: `get_password_for_background` → `Ok(None)` when [`availability()`](https://docs.rs/tauri-plugin-keyring-store/latest/tauri_plugin_keyring_store/struct.KeyringStore.html#method.availability) is `Locked`.\n- `exists_nonempty_for_background` → `Ok(false)` when locked.\n\n```rust,no_run\nuse tauri_plugin_keyring_store::{Error, KeyringAvailability, KeyringStore};\n\n# fn example(store: \u0026KeyringStore) -\u003e Result\u003c(), Error\u003e {\nmatch store.availability() {\n    KeyringAvailability::Available =\u003e { /* normal UI read */ }\n    KeyringAvailability::Locked =\u003e { /* prompt unlock or defer */ }\n}\nlet secret = store.get_password_for_background(\"oauth-token\")?;\n# Ok(())\n# }\n```\n\nIPC commands are unchanged in 0.2.0; Rust backends can use [`KeyringStore`](https://docs.rs/tauri-plugin-keyring-store/latest/tauri_plugin_keyring_store/struct.KeyringStore.html) directly without new Tauri commands.\n\n---\n\n## Installation\n\n### Automatic (recommended)\n\nFrom your **Tauri app root** (where `package.json` and the `tauri` script live):\n\n```bash\npnpm run tauri add keyring-store\n```\n\nThe CLI wires the Rust crate into `src-tauri` and adds the [`tauri-plugin-keyring-store-api`](https://www.npmjs.com/package/tauri-plugin-keyring-store-api) npm package when needed.\n\nOther package managers:\n\n```bash\nnpm run tauri add keyring-store\nyarn tauri add keyring-store\n```\n\nWith the CLI installed via Cargo: `cargo tauri add keyring-store`.\n\n### Manual — Rust (`src-tauri`)\n\n```bash\ncd src-tauri\ncargo add tauri-plugin-keyring-store\n```\n\nOr in `Cargo.toml`:\n\n```toml\ntauri-plugin-keyring-store = \"0.2.0\"\n```\n\nDisable the SLIP10/BIP39/Ed25519 stack (storage + backup IPC only):\n\n```toml\ntauri-plugin-keyring-store = { version = \"0.2.0\", default-features = false }\n```\n\n### Manual — JavaScript\n\n```bash\npnpm add tauri-plugin-keyring-store-api\n```\n\nYou still need `.plugin(tauri_plugin_keyring_store::init())` (or [`Builder`](https://docs.rs/tauri-plugin-keyring-store/latest/tauri_plugin_keyring_store/struct.Builder.html)) in Rust.\n\n---\n\n## Usage\n\n### Backend\n\n```rust\nfn main() {\n  tauri::Builder::default()\n    .plugin(tauri_plugin_keyring_store::init())\n    .run(tauri::generate_context!())\n    .expect(\"error while running tauri application\");\n}\n```\n\nCustom **service** name (defaults to `identifier` in `tauri.conf.json`):\n\n```rust\ntauri_plugin_keyring_store::Builder::new()\n  .service(\"com.mycompany.myapp.credentials\")\n  .build()\n```\n\n### Rust — access the store from commands / plugins\n\n```rust\nuse tauri::Manager;\nuse tauri_plugin_keyring_store::KeyringExt;\n\n#[tauri::command]\nfn save_api_token(app: tauri::AppHandle, token: String) -\u003e Result\u003c(), String\u003e {\n  app.keyring().store\n    .set_password(\"manual.example.token\", \u0026token)\n    .map_err(|e| e.to_string())\n}\n```\n\nSessions opened from the frontend (`initialize`) are tracked separately; low-level [`KeyringStore`](https://docs.rs/tauri-plugin-keyring-store/latest/tauri_plugin_keyring_store/struct.KeyringStore.html) calls use whatever account string you pass.\n\n### Snapshot path (first argument of `KeyringSession.load`)\n\nThis 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).\n\n| You choose | Effect |\n|------------|--------|\n| Same string on every platform | Same secrets namespace everywhere (typical). |\n| Different strings | Different isolated namespaces (e.g. per user or per “wallet”). |\n| Looks like a path, e.g. `'/wallet/main'` | Fine — purely a label; no requirement that the folder exists. |\n\nUse 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.\n\n### Frontend — minimal flow\n\n```typescript\nimport { KeyringSession } from 'tauri-plugin-keyring-store-api'\n\nconst session = await KeyringSession.load('/wallet/main', '')\nconst client = await session.createClient('main')\nawait client.getStore().insert('prefs', [...new TextEncoder().encode('{}')])\nawait session.unload()\n```\n\n### JavaScript API reference\n\nInvokes use `plugin:keyring-store|\u003ccommand\u003e`. SLIP10 / BIP39 / Ed25519 helpers on [`KeyringVault`](guest-js/index.ts) require the Rust crate’s **`crypto`** feature (enabled by default).\n\n#### `ping`\n\n```typescript\nimport { ping } from 'tauri-plugin-keyring-store-api'\n\nconst value = await ping('hello') // string | null\n```\n\n#### `KeyringSession`\n\n| Method | Purpose |\n|--------|---------|\n| `KeyringSession.load(snapshotPath, password)` | Registers the session (`initialize` IPC). |\n| `session.unload()` | Drops session tracking (`destroy`). |\n| `session.createClient(client)` | First-time client namespace (`create_client`). |\n| `session.loadClient(client)` | Existing client (`load_client`). |\n| `session.save()` | **No-op** on keyring (`save` IPC for Stronghold parity). |\n\n```typescript\nimport { KeyringSession } from 'tauri-plugin-keyring-store-api'\n\nconst session = await KeyringSession.load('/app/secrets', '')\nconst created = await session.createClient('desktop')\nconst again = await session.loadClient('desktop')\nawait session.save()\nawait session.unload()\n```\n\n#### `KeyringClient`\n\n| Method | Purpose |\n|--------|---------|\n| `client.getStore()` | JSON-like byte records (`get_store_record` / `save_store_record` / `remove_store_record`). |\n| `client.getVault(name)` | Binary vault + crypto procedures (`save_secret` / `remove_secret` / `execute_procedure`). |\n\n#### `KeyringStoreView` (from `client.getStore()`)\n\n| Method | Purpose |\n|--------|---------|\n| `get(key)` | Read bytes or `null`. |\n| `insert(key, value, lifetime?)` | Write bytes; `lifetime` is ignored (Stronghold compat). |\n| `remove(key)` | Delete record; returns previous bytes or `null`. |\n\n```typescript\nconst store = client.getStore()\nconst raw = await store.get('prefs')\nawait store.insert('prefs', [...new TextEncoder().encode('{}')])\nawait store.remove('prefs')\n```\n\n#### `Location` and `KeyringVault`\n\nBuild locations for vault records and procedure outputs:\n\n```typescript\nimport { Location } from 'tauri-plugin-keyring-store-api'\n\nconst generic = Location.generic('WALLET', 'seed.bin')\nconst row = Location.counter('WALLET', 0)\n```\n\n| `KeyringVault` method | IPC / behavior |\n|----------------------|----------------|\n| `insert(recordPath, secret)` | `save_secret` |\n| `remove(location)` | `remove_secret` (pass `Location.generic` or `Location.counter`) |\n| `generateSLIP10Seed(output, sizeBytes?)` | `execute_procedure` SLIP10Generate |\n| `deriveSLIP10(chain, 'Seed' \\| 'Key', src, output)` | SLIP10Derive |\n| `recoverBIP39(mnemonic, output, passphrase?)` | BIP39Recover |\n| `generateBIP39(output, passphrase?)` | BIP39Generate |\n| `getEd25519PublicKey(privateKeyLocation)` | PublicKey (Ed25519) |\n| `signEd25519(privateKeyLocation, msg)` | Ed25519Sign (`msg` is UTF-8) |\n\n```typescript\nimport { KeyringSession, Location } from 'tauri-plugin-keyring-store-api'\n\nconst session = await KeyringSession.load('/vault-a', '')\nconst vault = (await session.createClient('c1')).getVault('PRIMARY')\nconst out = Location.generic('PRIMARY', 'slip10-master')\nawait vault.generateSLIP10Seed(out, 32)\nawait session.unload()\n```\n\nBinary vault records (not the procedure helpers above):\n\n```typescript\nimport { KeyringSession, Location } from 'tauri-plugin-keyring-store-api'\n\nconst session = await KeyringSession.load('/vault-a', '')\nconst vault = (await session.createClient('c1')).getVault('SECRETS')\nawait vault.insert('blob.bin', [0xde, 0xad])\nawait vault.remove(Location.generic('SECRETS', 'blob.bin'))\nawait session.unload()\n```\n\n#### Naming helpers\n\n```typescript\nimport { joinKeyPrefix, splitKeyPrefix, KEYRING_PREFIX_SEPARATOR } from 'tauri-plugin-keyring-store-api'\n\nconst account = joinKeyPrefix('billing', 'stripe_sk')\nconst [prefix, name] = splitKeyPrefix(account)\nvoid KEYRING_PREFIX_SEPARATOR // '.'\n```\n\n---\n\n## Direct account API (bulk, exists, naming, backup)\n\nRaw **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.\n\n| IPC command | Purpose |\n|-------------|---------|\n| `get_passwords` | Read many UTF-8 secrets (parallel `Vec`, max **256** accounts per call). |\n| `set_passwords` | Write many `{ account, secret }` pairs. |\n| `delete_passwords` | Delete many accounts. |\n| `password_exists` | `true` if a non-empty secret exists (`exists_nonempty`). |\n| `export_passwords_plain` / `import_passwords_plain` | JSON backup blob over IPC. |\n| `export_passwords_encrypted` / `import_passwords_encrypted` | Argon2id + ChaCha20-Poly1305 envelope (always compiled; independent of the `crypto` feature). |\n\n**Naming (application convention):** use `prefix.name` with a single dot — helpers [`join_prefix`](https://docs.rs/tauri-plugin-keyring-store/latest/tauri_plugin_keyring_store/fn.join_prefix.html) / [`split_prefixed`](https://docs.rs/tauri-plugin-keyring-store/latest/tauri_plugin_keyring_store/fn.split_prefixed.html) in Rust, and `joinKeyPrefix` / `splitKeyPrefix` in guest-js (see [Usage → Naming helpers](#naming-helpers)). The OS keyring still does **not** support listing by prefix; keep your own index of logical keys if needed.\n\n**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.\n\n### Guest-js examples\n\n```typescript\nimport {\n  getPasswords,\n  setPasswords,\n  deletePasswords,\n  passwordExists,\n  exportPasswordsPlain,\n  importPasswordsPlain,\n  exportPasswordsEncrypted,\n  importPasswordsEncrypted,\n  joinKeyPrefix,\n} from 'tauri-plugin-keyring-store-api'\n\nconst account = joinKeyPrefix('app', 'api_token')\n\nawait setPasswords([{ account, secret: 'secret-value' }])\nconst values = await getPasswords([account]) // (string | null)[]\nconst exists = await passwordExists(account)\n\nconst plain = await exportPasswordsPlain([account])\nawait importPasswordsPlain(plain)\n\nconst enc = await exportPasswordsEncrypted([account], 'user-passphrase')\nawait importPasswordsEncrypted(enc, 'user-passphrase')\n\nawait deletePasswords([account])\n```\n\n---\n\n## Cargo features\n\n| Feature | Default | Description |\n|---------|---------|-------------|\n| `crypto` | yes | SLIP10 / BIP39 / Ed25519 `execute_procedure` via [`iota-crypto`](https://crates.io/crates/iota-crypto). Encrypted backup (Argon2 + ChaCha) is **always** available without this flag. |\n\n---\n\n## Permissions\n\nUse `keyring-store:default` or granular `keyring-store:allow-*` (see [`permissions/default.toml`](permissions/default.toml)). Commands: `plugin:keyring-store|\u003ccommand\u003e`.\n\n---\n\n## Relationship to `tauri-plugin-stronghold`\n\n| Stronghold | This plugin |\n|------------|-------------|\n| Password-derived snapshot | No snapshot file; OS stores secrets |\n| `save()` writes snapshot | `save()` is a **no-op** (compat) |\n| Procedures in Stronghold VM | In-process crypto; outputs in keyring |\n\n---\n\n## Development\n\n```bash\ncargo fmt --all\ncargo clippy --all-targets --all-features -- -D warnings\ncargo test\ncargo build --no-default-features\n\npnpm install\npnpm build\npnpm test\n```\n\nRustdoc logo (after push to `main`): PNG is generated from [`assets/docs-logo.svg`](assets/docs-logo.svg):\n\n```bash\nrsvg-convert -w 128 -h 128 assets/docs-logo.svg -o assets/docs-logo.png\n```\n\n---\n\n## Testing\n\n- **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.\n- **JavaScript**: `pnpm test` (Vitest) mocks `@tauri-apps/api/core`.\n\n---\n\n## Contributing\n\nIssues and pull requests are welcome on [GitHub](https://github.com/s00d/tauri-plugin-keyring-store).\n\n---\n\n## Partners\n\nContributions and sponsorship help maintain this and related plugins. Thank you for your support.\n\n---\n\n## License\n\nLicensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option.\n\n`SPDX-License-Identifier: MIT OR Apache-2.0`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fs00d%2Ftauri-plugin-keyring-store","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fs00d%2Ftauri-plugin-keyring-store","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fs00d%2Ftauri-plugin-keyring-store/lists"}