db-keystore
Platform-independent, SQLite-backed credential store for the keyring-core API, with optional encryption-at-rest. Built with turso.
This crate implements keyring-core::CredentialStoreApi and CredentialApi.
There is a binary maintenance tool for adjusting db settings and rotating encryption keys.
Features
- Rust-native SQLite implementation by turso, with open source encryption.
- WAL + busy timeout for safety in multi-process environments.
- Optional uniqueness enforcement on
(service, user). - UUID and optional
commentattributes exposed via the credential API. - UUIDs created with format v7, allowing sort-by-time for ambiguous entries.
- Search with regex filters over
service,user,uuid, andcomment. - Several Examples
Configuration
Keystore settings can be configured either with modifiers (string key-value pairs, for use with the keyring crate), or struct DbKeyStoreConfig.
Modifier keys (all optional):
path: path to the SQLite database file. Defaults to$XDG_STATE_HOME/keystore.dbif $XDG_STATE_HOME is defined, or$HOME/.local/state/keystore.dballow-ambiguity(aliasallow_ambiguity):"true"or"false". Defaultfalse. Allows storage of more than one match for the pair (service,user). Individual pairs can be identified by the unique uuid or comment. When false, aUNIQUEindex is created to enforce uniqueness.encryption-cipher(aliascipher): Cipher name (requireshexkey). See below for list of supported ciphers.encryption-hexkey(aliashexkey): Encryption key as 64 hex digits (256-bit key) or 32 hex digits (128-bit key) (requirescipher).index-always(aliasindex_always):"true"or"false". Defaultfalse. Adds an index on(service,user)even whenallow-ambiguityis true.vfs: Optional VFS selection ("memory","syscall", or"io_uring").- "memory": In-memory database. Data is entirely in RAM, and data is lost when process exits. When vfs=memory,
pathand encryption options are ignored. - "syscall": Generic syscall backend. Uses standard POSIX system calls for file I/O. This is the most portable mode.
- "io_uring": Linux io_uring backend. Uses Linux's modern async I/O interface for better performance. Only available on Linux.
- "memory": In-memory database. Data is entirely in RAM, and data is lost when process exits. When vfs=memory,
Entry modifiers supported by build:
uuid: Explicit credential UUID (allows creating ambiguous entries when allowed).comment: Initial comment value stored with the credential.
Examples
Configure and open
use ;
use ;
use ;
/// Open in default location (~/.local/state/keystore.db)
/// Open encrypted database in custom folder
/// Open in-memory db
Save and lookup secrets
use ;
use ;
use ;
/// Verify secret. Returns true if there is a matching password for the service+user
/// Search using optional regex filters. Returns matches
Changing database settings
To change the database configuration after the database has been created, use the db-keystore maintenance tool.
Changing encryption
To change database encryption (adding encryption, removing encryption, rotating the key, or changing the cipher), use db-keystore rekey, or the library API DbKeyStore::rekey (see the rekey module docs). Rekey writes to a freshly created destination file (mode 0600, never overwriting an existing file), verifies every copied record exactly against the source before reporting success, and leaves the source unchanged on any failure. On Linux, rekey_at offers a descriptor-relative variant for callers that need to pin the source and destination directories.
Changing allow_ambiguous from false to true
To change a database to allow ambiguous entries, use db-keystore allow-ambiguous to remove the unique index. Then the db can be opened with DbKeyStoreConfig::allow_ambiguous: true.
Changing allow_ambiguous from true to false
To change a database to disable ambiguity, you must first ensure there are no ambiguous entries in the keystore, or the creation of the unique index will fail.
List the ambiguous entries with db-keystore list --ambiguous and use db-keystore delete ... to remove conflicts. When there are no more results from db-keystore list --ambiguous, it is safe to open the database with DbKeyStoreConfig::allow_ambiguous: false.
Maintenance tool
Install with cargo install db-keystore (or from source cargo install --path .)
# Global args
# --path PATH # path to keystore. defaults to ~/.local/state/keystore.db
# --cipher CIPHER # encryption cipher
# --hexkey HEXKEY # encryption key
# --json # output list commands in json instead of tsv
# list credentials in store (does not display secrets)
# list ambiguous credentials (non-unique sets of (service,user))
# delete one or more credentials in keystore
# rotate keys (`rekey` can also be used to add or remove encryption)
Encryption
Supported ciphers
aegis256 is recommended for most applications. See Turso Database Encryption for more information about available ciphers and recommendations.
| Key length | Aegis | AES |
|---|---|---|
| 128-bit | aegis128laegis128x2aegis128x4 | aes128gcm (AES-128-GCM) |
| 256-bit | aegis256aegis256x2aegis256x4 | aes256gcm (AES-256-GCM) |
Key generation
# generate 256-bit key as 64 hex digits
# generate 128-bit key as 32 hex digits
/// generate 256-bit key as 64 hex digits
Important: Store your encryption key securely. If you lose the key, your encrypted data cannot be recovered.
Encryption Security
The ciphers are considered strong modern AEAD-based ciphers. The database is encrypted at the page level (default page size 4096 bytes), where each page has a unique nonce. The first 100 bytes, containing a Turso version header and sqlite metadata, are not encrypted.
When using this or any encrypted storage, keep in mind that the greatest risks for stored secrets are usually related to key generation and key management:
- Low entropy keys can enable brute-force attacks.
- Reusing the same key across databases increases the blast radius if a key leaks.
- Generating keys from user input without a strong KDF (Argon2/scrypt) weakens security.
- Storing encryption keys on disk with the database (or leaked in logs or environment) diminishes the benefits of encryption.
Hardening recommendations
db-keystore guards all secrets and the db encryption key with zeroizing wrappers to prevent
secrets being accidentally logged or leaking into heap, however we can only follow this practice
on our side of the turso API boundary. turso uses ordinary String for the db encryption key
and Vec<u8> for returning encrypted data, so secrets can leak into the heap when these objects
are freed. In addition, turso may cache unencrypted pages in memory. To prevent unencrypted secrets
from being swapped to disk, we strongly recommend disabling swap for the process, disabling
swap for the system, and using an encrypted disk partition.
- Keep the unit out of swap. For systemd services, add
MemorySwapMax=0to the[Service]section (needs cgroup v2 with memory accounting, the default on current distros). The kernel then never swaps the pages. - Disable or encrypt swap host-wide. The simplest thing is to disable swap entirely.
If the host needs swap, encrypt it with a random per-boot key:
swapDevices.*.randomEncryption.enable = trueon NixOS, or acrypttabentry keyed from/dev/urandomwith theswapoption elsewhere. Settingvm.swappinessis only advisory and not sufficient. - Disable core dumps for the unit. A crash dump of the process contains its heap, with cache
pages. Set
LimitCORE=0in the unit and, wheresystemd-coredumpis active, setStorage=noneincoredump.conf(or mask the socket) so nothing writes the heap to disk. - Use drive encryption. If you do all of the above you've kept the system from writing secrets from memory to disk, except for hibernation. A hibernation image contains all of RAM, even locked pages. Using full drive encryption is usually recommended anyway, but if your system has hibernation enabled, the drive, or at least the hibernation image file, needs to be encrypted so secrets are encrypted at rest.
Release Notes
See CHANGELOG for latest changes.
See Issues for known issues.
Schema version
Schema version stored to enable future schema migrations.
Length limits
Secrets are limited to 65536 bytes, and service and user names are limited to 1024 characters each. These limits are somewhat arbitrary, sanity checks to prevent accidental blow-up of the database. If you need longer keys, submit an issue, and we can increase the length or make it a config setting.
Ambiguity and database size
When allow_ambiguity is false (the default), the pair (service,user) is required to be unique and enforced in the database:
- a UNIQUE index is created on the
service, usercolumns - UPSERT is used for
set_secretandset_password. - operations return
Error::Ambiguousif multiple credentials match a single(service, user)pair.
If allow_ambiguity is true, the UNIQUE index is not created. If you expect the keystore to hold ~1000 or more secrets, consider setting DbKeyStoreConfig::index_always to create an index on (service,user) to improve lookup performance. The index isn't normally created because it can roughly double the size of the database, and for small keystores doesn't make a significant difference for latency. This is an edge case, as allow_ambiguity is default false and does use an index. There are tradeoffs - benchmark on your target platform.
Testing
# Run unit tests
# Performance and stress tests skipped to avoid extra wear on SSDs
# Include performance tests.
# To adjust number of iterations, search for "count =" in tests/stress.rs
PERF_INDEX=1
# Include stress tests with multi-process locking and transactions.
# To adjust duration of stress test, change DEFAULT_STRESS_SECONDS
License
MIT OR Apache-2.0