Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ cargo fmt -- --check
WalletKit is broken down into separate crates, offering the following functionality.

- `walletkit-core` - Enables basic usage of a World ID to generate ZKPs using different credentials.
- `walletkit-db` - Generic encrypted `SQLite` (`sqlite3mc`) wrapper providing safe connection, transaction, and statement types plus encrypted-open and plaintext export/import helpers. Used by `walletkit-core` for credential storage and consumable by sibling SDKs that need an encrypted on-device store.

### World ID Secret

Expand Down
3 changes: 1 addition & 2 deletions walletkit-core/src/storage/cache/maintenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ use std::path::Path;
use secrecy::SecretBox;

use crate::storage::error::StorageResult;
use walletkit_db::cipher;
use walletkit_db::Connection;
use walletkit_db::{cipher, Connection};

use super::schema;
use super::util::{map_db_err, map_io_err};
Expand Down
2 changes: 1 addition & 1 deletion walletkit-core/src/storage/credential_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1281,7 +1281,7 @@ mod tests {

#[test]
fn test_import_vault_backup_transaction_atomicity() {
use walletkit_db::cipher::BACKUP_TABLES;
use crate::storage::vault::BACKUP_TABLES;
use walletkit_db::Connection;
use world_id_core::Credential as CoreCredential;

Expand Down
20 changes: 17 additions & 3 deletions walletkit-core/src/storage/keys.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
//! Key hierarchy management for credential storage.
//!
//! ## Key structure
//!
//! - `K_device`: device-bound root key managed by `DeviceKeystore`.
//! - `account_keys.bin`: account key envelope stored via `AtomicBlobStore` and
//! containing `DeviceKeystore::seal` of `K_intermediate` with associated data
//! `worldid:account-key-envelope`.
//! - `K_intermediate`: 32-byte per-account key unsealed at init and kept in
//! memory for the lifetime of the storage handle.
//! - `SQLCipher` databases: `account.vault.sqlite` (authoritative) and
//! `account.cache.sqlite` (non-authoritative) are opened with `K_intermediate`.
//! - Derived keys: per relying-party session keys may be derived from
//! `K_intermediate` and cached in `account.cache.sqlite` for performance.

use rand::{rngs::OsRng, RngCore};
use secrecy::SecretBox;
Expand Down Expand Up @@ -48,9 +61,9 @@ impl StorageKeys {
})
} else {
let k_intermediate = random_key();
// TODO: At this moment, the key needs to be temporarily heap allocated in order
// to be bridged via UniFFI. This needs to be improved to use pointers that can
// be zeroized after use.
// TODO: At this moment, the key needs to be temporarily heap
// allocated in order to be bridged via UniFFI. This needs to be
// improved to use pointers that can be zeroized after use.
let wrapped_k_intermediate = keystore
.seal(ACCOUNT_KEY_ENVELOPE_AD.to_vec(), k_intermediate.to_vec())?;
let envelope = AccountKeyEnvelope::new(wrapped_k_intermediate, now);
Expand Down Expand Up @@ -90,6 +103,7 @@ fn parse_key_32(bytes: &[u8], label: &str) -> StorageResult<[u8; 32]> {
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::error::StorageError;
use crate::storage::lock::StorageLock;
use crate::storage::tests_utils::{InMemoryBlobStore, InMemoryKeystore};
use secrecy::ExposeSecret;
Expand Down
8 changes: 0 additions & 8 deletions walletkit-core/src/storage/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ use std::path::Path;

use super::error::StorageResult;

// WASM: no-op lock (single-threaded worker, SQLITE_THREADSAFE=0)

#[cfg(target_arch = "wasm32")]
mod imp {
use super::*;
Expand Down Expand Up @@ -43,8 +41,6 @@ mod imp {
}
}

// Native: file-backed exclusive lock (flock on Unix, LockFileEx on Windows)

#[cfg(not(target_arch = "wasm32"))]
mod imp {
use super::{Path, StorageResult};
Expand Down Expand Up @@ -125,8 +121,6 @@ mod imp {
StorageError::Lock(err.to_string())
}

// ── Unix flock ──────────────────────────────────────────────────────

#[cfg(unix)]
fn lock_exclusive(file: &File) -> std::io::Result<()> {
let fd = std::os::unix::io::AsRawFd::as_raw_fd(file);
Expand Down Expand Up @@ -180,8 +174,6 @@ mod imp {
fn flock(fd: c_int, operation: c_int) -> c_int;
}

// ── Windows LockFileEx ──────────────────────────────────────────────

#[cfg(windows)]
fn lock_exclusive(file: &File) -> std::io::Result<()> {
lock_file(file, 0)
Expand Down
14 changes: 0 additions & 14 deletions walletkit-core/src/storage/traits.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,4 @@
//! Platform interfaces for credential storage.
//!
//! ## Key structure
//!
//! - `K_device`: device-bound root key managed by `DeviceKeystore`.
//! - `account_keys.bin`: account key envelope stored via `AtomicBlobStore` and
//! containing `DeviceKeystore::seal` of `K_intermediate` with associated data
//! `worldid:account-key-envelope`.
//! - `K_intermediate`: 32-byte per-account key unsealed at init and kept in
//! memory for the lifetime of the storage handle.
//! - `SQLCipher` databases: `account.vault.sqlite` (authoritative) and
//! `account.cache.sqlite` (non-authoritative) are opened with `K_intermediate`.
//! - Derived keys: per relying-party session keys may be derived from
//! `K_intermediate` and cached in `account.cache.sqlite` for performance.
//! cached in `account.cache.sqlite` for performance.

use std::sync::Arc;

Expand Down
4 changes: 2 additions & 2 deletions walletkit-core/src/storage/vault/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ use walletkit_db::{DbError, Row};

const CONTENT_ID_PREFIX: &[u8] = b"worldid:blob";

pub(super) fn compute_content_id(blob_kind: BlobKind, plaintext: &[u8]) -> ContentId {
pub(super) fn compute_content_id(kind: BlobKind, plaintext: &[u8]) -> ContentId {
let mut hasher = Sha256::new();
hasher.update(CONTENT_ID_PREFIX);
hasher.update([blob_kind as u8]);
hasher.update([kind as u8]);
hasher.update(plaintext);
let digest = hasher.finalize();
let mut out = [0u8; 32];
Expand Down
20 changes: 14 additions & 6 deletions walletkit-core/src/storage/vault/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ use crate::storage::types::{BlobKind, CredentialRecord};
use helpers::{compute_content_id, map_db_err, map_record, to_i64, to_u64};
use schema::{ensure_schema, VAULT_SCHEMA_VERSION};
use secrecy::SecretBox;
use walletkit_db::cipher;
use walletkit_db::{params, Connection, StepResult, Value};
use walletkit_db::{cipher, params, Connection, StepResult, Value};

pub(crate) use schema::BACKUP_TABLES;

/// Encrypted vault database wrapper.
#[derive(Debug)]
Expand All @@ -35,7 +36,7 @@ impl VaultDb {
) -> StorageResult<Self> {
let conn = cipher::open_encrypted(path, k_intermediate, false)
.map_err(|e| map_db_err(&e))?;
ensure_schema(&conn)?;
ensure_schema(&conn).map_err(|err| map_db_err(&err))?;
let db = Self { conn };
if !db.check_integrity()? {
return Err(StorageError::CorruptedVault(
Expand Down Expand Up @@ -371,13 +372,13 @@ impl VaultDb {
dest: &Path,
_lock: &StorageLockGuard,
) -> StorageResult<()> {
// Remove any stale export from a previous failed run.
if dest.exists() {
std::fs::remove_file(dest).map_err(|e| {
StorageError::VaultDb(format!("failed to remove stale backup: {e}"))
})?;
}
cipher::export_plaintext_copy(&self.conn, dest).map_err(|e| map_db_err(&e))
cipher::export_plaintext_copy(&self.conn, dest, BACKUP_TABLES)
.map_err(|e| map_db_err(&e))
}

/// Imports credentials from a plaintext (unencrypted) vault backup into
Expand All @@ -394,6 +395,13 @@ impl VaultDb {
source: &Path,
_lock: &StorageLockGuard,
) -> StorageResult<()> {
cipher::import_plaintext_copy(&self.conn, source).map_err(|e| map_db_err(&e))
cipher::import_plaintext_copy(&self.conn, source, BACKUP_TABLES)
.map_err(|e| map_db_err(&e))
}

/// Borrows the underlying connection for direct SQL access. **Test-only.**
#[cfg(test)]
pub(super) const fn raw_connection(&self) -> &Connection {
&self.conn
}
}
29 changes: 17 additions & 12 deletions walletkit-core/src/storage/vault/schema.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
//! Vault database schema management.
//!
//! Owns the credential vault tables and backup-sensitive schema.

use crate::storage::error::StorageResult;
use walletkit_db::Connection;

use super::helpers::map_db_err;
use walletkit_db::{Connection, DbResult};

pub(super) const VAULT_SCHEMA_VERSION: i64 = 1;

/// Tables included in plaintext vault backups.
///
/// `vault_meta` is intentionally excluded: on restore, the destination vault
/// already has its own `vault_meta` (created by `ensure_schema` +
/// `init_leaf_index`) with the authoritative `leaf_index` from the
/// authenticator.
///
/// **Note:** New tables added to the vault schema must be added here too.
pub const BACKUP_TABLES: &[&str] = &["credential_records", "blob_objects"];

/// **Backup sensitivity:** Schema changes here affect vault backups made into the backup system.
/// - New tables must be added to `BACKUP_TABLES` in `walletkit-db/src/cipher.rs`.
/// - Column changes (especially new `NOT NULL` columns without defaults) will
/// break restoring older backups into a newer schema. See the schema migration
/// note on `import_plaintext_copy` in `walletkit-db/src/cipher.rs`.
pub(super) fn ensure_schema(conn: &Connection) -> StorageResult<()> {
/// - New tables must be added to [`BACKUP_TABLES`].
/// - Column changes (especially new `NOT NULL` columns without defaults) can
/// break restoring older backups into a newer schema.
pub(super) fn ensure_schema(conn: &Connection) -> DbResult<()> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS vault_meta (
schema_version INTEGER NOT NULL,
Expand Down Expand Up @@ -57,9 +65,6 @@ pub(super) fn ensure_schema(conn: &Connection) -> StorageResult<()> {
bytes BLOB NOT NULL,
PRIMARY KEY (content_id)
);

",
)
.map_err(|err| map_db_err(&err))?;
Ok(())
}
16 changes: 8 additions & 8 deletions walletkit-core/src/storage/vault/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ fn test_content_id_deduplication() {
)
.expect("store credential");
let count = db
.conn
.raw_connection()
.query_row("SELECT COUNT(*) FROM blob_objects", &[], |stmt| {
Ok(stmt.column_i64(0))
})
Expand All @@ -212,7 +212,7 @@ fn test_content_id_deduplication() {
.expect("delete first credential");

let count_after_first_delete = db
.conn
.raw_connection()
.query_row("SELECT COUNT(*) FROM blob_objects", &[], |stmt| {
Ok(stmt.column_i64(0))
})
Expand All @@ -224,7 +224,7 @@ fn test_content_id_deduplication() {
.expect("delete second credential");

let count_after_second_delete = db
.conn
.raw_connection()
.query_row("SELECT COUNT(*) FROM blob_objects", &[], |stmt| {
Ok(stmt.column_i64(0))
})
Expand Down Expand Up @@ -367,7 +367,7 @@ fn test_delete_credential_by_id() {
.expect("store credential");

let blob_count_before = db
.conn
.raw_connection()
.query_row("SELECT COUNT(*) FROM blob_objects", &[], |stmt| {
Ok(stmt.column_i64(0))
})
Expand All @@ -382,7 +382,7 @@ fn test_delete_credential_by_id() {
assert!(records.is_empty());

let blob_count_after = db
.conn
.raw_connection()
.query_row("SELECT COUNT(*) FROM blob_objects", &[], |stmt| {
Ok(stmt.column_i64(0))
})
Expand Down Expand Up @@ -429,7 +429,7 @@ fn test_delete_credential_cleans_up_orphaned_associated_data() {
.expect("store credential");

let associated_before = db
.conn
.raw_connection()
.query_row(
"SELECT COUNT(*) FROM blob_objects WHERE blob_kind = ?1",
params![BlobKind::AssociatedData.as_i64()],
Expand All @@ -443,7 +443,7 @@ fn test_delete_credential_cleans_up_orphaned_associated_data() {
.expect("delete credential");

let associated_after = db
.conn
.raw_connection()
.query_row(
"SELECT COUNT(*) FROM blob_objects WHERE blob_kind = ?1",
params![BlobKind::AssociatedData.as_i64()],
Expand Down Expand Up @@ -497,7 +497,7 @@ fn test_danger_delete_all_credentials() {
assert!(records.is_empty());

let blob_count = db
.conn
.raw_connection()
.query_row("SELECT COUNT(*) FROM blob_objects", &[], |stmt| {
Ok(stmt.column_i64(0))
})
Expand Down
2 changes: 1 addition & 1 deletion walletkit-db/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "walletkit-db"
description = "Internal SQLite wrapper crate for WalletKit storage."
description = "Generic encrypted SQLite wrapper backed by sqlite3mc."
publish = true
version.workspace = true
edition.workspace = true
Expand Down
36 changes: 12 additions & 24 deletions walletkit-db/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
//! Minimal safe `SQLite` wrapper backed by `sqlite3mc`.
//! Generic encrypted `SQLite` (`sqlite3mc`) wrapper.
//!
//! This crate provides a small, safe Rust API over the `SQLite` C FFI.
//! The raw symbols are resolved at compile time:
//! The public API:
//!
//! * **Native** (`not(wasm32)`): linked against the `sqlite3mc` static library
//! compiled from the downloaded amalgamation by `build.rs`.
//! * **WASM** (`wasm32`): delegated to `sqlite-wasm-rs` (with the `sqlite3mc`
//! feature) which ships its own WASM-compiled `sqlite3mc`.
//! - safe Rust connection / transaction / statement types
//! - encrypted open helpers and integrity checks
//! - plaintext export / import helpers parameterized by caller-owned tables
//!
//! Consumer code (vault, cache, cipher config) uses only the safe types
//! defined here and never touches raw FFI directly. The `ffi` module is the
//! **only** file that contains `unsafe` code or C types.
//! Raw FFI lives behind the [`sqlite`] module; consumer crates own their own
//! schema, queries, and higher-level storage policy.

mod ffi;
pub mod sqlite;

mod connection;
pub mod error;
mod statement;
mod transaction;
pub mod value;

pub mod cipher;

pub use connection::Connection;
pub use error::DbError;
pub use statement::{Row, Statement, StepResult};
pub use transaction::Transaction;
pub use value::Value;
pub use sqlite::{
cipher, Connection, Error as DbError, Result as DbResult, Row, Statement,
StepResult, Transaction, Value,
};

#[cfg(test)]
mod tests;
Loading
Loading