From af9ea57469d3c998628ceb8fa4015771db012d10 Mon Sep 17 00:00:00 2001 From: james Date: Thu, 3 Sep 2026 20:47:43 -0700 Subject: [PATCH 1/7] Introduce Credential Activity History Store (#481) Introduces a sharable on-device store for Android and iOS to retain credential activity. We're re-using some components from the Wallet storage here, while maintaining a separate database for the local activity as it is a separate concern and should never be backed up with the existing systems. I've modeled the changes after how I typically use rust; mod.rs is mainly imports with most of the code in dedicated files, and tests in their related files. This system is expected to evolve over time so i've prioritized a sensible migration strategy as part of these changes. Tested integration on iOS. Will do Android later and follow up with any PRs that might be necessary for their integration. No runtime differences until the host apps are updated to use the new system. --- .../src/storage/cache/activity.rs | 366 ++++++++++++++++++ .../walletkit-core/src/storage/cache/mod.rs | 170 ++++++++ .../src/storage/cache/schema.rs | 21 + .../walletkit-core/src/storage/cache/util.rs | 6 + .../src/storage/credential_storage.rs | 227 ++++++++++- crates/walletkit-core/src/storage/error.rs | 8 + crates/walletkit-core/src/storage/mod.rs | 8 +- crates/walletkit-core/src/storage/traits.rs | 25 ++ crates/walletkit-core/src/storage/types.rs | 96 +++++ crates/walletkit-db/src/sqlite/transaction.rs | 14 + 10 files changed, 935 insertions(+), 6 deletions(-) create mode 100644 crates/walletkit-core/src/storage/cache/activity.rs diff --git a/crates/walletkit-core/src/storage/cache/activity.rs b/crates/walletkit-core/src/storage/cache/activity.rs new file mode 100644 index 000000000..8ab15a3df --- /dev/null +++ b/crates/walletkit-core/src/storage/cache/activity.rs @@ -0,0 +1,366 @@ +use crate::storage::error::{StorageError, StorageResult}; +use crate::storage::types::{ + ActivityEntry, ActivityFailureReason, ActivityMetadata, ActivityOutcome, + ActivityQuery, ProtocolVersion, +}; +use walletkit_sqlite::{params, Connection, Row, StepResult, Value}; + +use super::util::{map_db_err, to_i64, to_u64}; + +pub(super) fn record( + conn: &Connection, + entry: &ActivityEntry, + now: u64, +) -> StorageResult { + match (entry.outcome, entry.failure_reason) { + (ActivityOutcome::Failed, None) => { + return Err(StorageError::ActivityInvalidRecord( + "failure_reason must be present when outcome is Failed".to_string(), + )); + } + (outcome, Some(_)) if outcome != ActivityOutcome::Failed => { + return Err(StorageError::ActivityInvalidRecord( + "failure_reason must be absent unless outcome is Failed".to_string(), + )); + } + _ => {} + } + + let now_i64 = to_i64(now, "now")?; + + let failure_reason_value = entry.failure_reason.map_or(Value::Null, |reason| { + Value::Integer(failure_reason_to_i64(reason)) + }); + + let entry_id = conn + .query_row( + "INSERT INTO activity_entries ( + client_id, protocol, created_at, + outcome, app_identifier, issuer_schema_ids, failure_reason + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + RETURNING entry_id", + params![ + entry.client_id.as_str(), + entry.protocol.as_i64(), + now_i64, + entry.outcome.to_string(), + entry.rp_id.to_string(), + encode_issuer_schema_ids(&entry.issuer_schema_ids), + failure_reason_value, + ], + |stmt| Ok(stmt.column_i64(0)), + ) + .map_err(|err| map_db_err(&err))?; + + to_u64(entry_id, "entry_id") +} + +/// Lists activity entries, most recent first. +pub(super) fn list( + conn: &Connection, + query: ActivityQuery, + limit: u32, + offset: u32, +) -> StorageResult> { + let _ = query; + let limit_i64 = i64::from(limit); + let offset_i64 = i64::from(offset); + + let sql = "SELECT entry_id, client_id, protocol, created_at, outcome, + app_identifier, issuer_schema_ids, failure_reason + FROM activity_entries + ORDER BY created_at DESC, entry_id DESC + LIMIT ?1 OFFSET ?2"; + + let mut entries = Vec::new(); + + let mut stmt = conn.prepare(sql).map_err(|err| map_db_err(&err))?; + + stmt.bind_values(params![limit_i64, offset_i64]) + .map_err(|err| map_db_err(&err))?; + + while let StepResult::Row(row) = stmt.step().map_err(|err| map_db_err(&err))? { + entries.push(map_entry(&row)?); + } + + Ok(entries) +} + +/// Returns aggregate activity metadata. +pub(super) fn metadata(conn: &Connection) -> StorageResult { + let total_count = conn + .query_row("SELECT COUNT(*) FROM activity_entries", &[], |stmt| { + Ok(stmt.column_i64(0)) + }) + .map_err(|err| map_db_err(&err))?; + + Ok(ActivityMetadata { + total_count: to_u64(total_count, "total_count")?, + }) +} + +pub(super) fn clear(conn: &Connection) -> StorageResult { + let deleted = conn + .execute("DELETE FROM activity_entries", &[]) + .map_err(|err| map_db_err(&err))?; + + Ok(deleted as u64) +} + +fn encode_issuer_schema_ids(issuer_schema_ids: &[u64]) -> Vec { + let mut bytes = Vec::with_capacity(issuer_schema_ids.len() * 8); + for id in issuer_schema_ids { + bytes.extend_from_slice(&id.to_be_bytes()); + } + bytes +} + +fn decode_issuer_schema_ids(bytes: &[u8]) -> StorageResult> { + if !bytes.len().is_multiple_of(8) { + return Err(StorageError::ActivityDb(format!( + "invalid issuer_schema_ids blob length: {}", + bytes.len() + ))); + } + + Ok(bytes + .chunks_exact(8) + .map(|chunk| { + let mut buf = [0u8; 8]; + buf.copy_from_slice(chunk); + u64::from_be_bytes(buf) + }) + .collect()) +} + +fn map_entry(row: &Row<'_, '_>) -> StorageResult { + let id = to_u64(row.column_i64(0), "entry_id")?; + let client_id = row.column_text(1); + let protocol = ProtocolVersion::try_from(row.column_i64(2))?; + let timestamp = to_u64(row.column_i64(3), "created_at")?; + let outcome_text = row.column_text(4); + let outcome: ActivityOutcome = outcome_text.parse().map_err(|_| { + StorageError::ActivityDb(format!("invalid outcome: {outcome_text}")) + })?; + let rp_id = parse_rp_id(&row.column_text(5))?; + let issuer_schema_ids = decode_issuer_schema_ids(&row.column_blob(6))?; + let failure_reason = if row.is_column_null(7) { + None + } else { + Some(i64_to_failure_reason(row.column_i64(7))?) + }; + + Ok(ActivityEntry { + id: Some(id), + client_id, + protocol, + timestamp: Some(timestamp), + outcome, + rp_id, + issuer_schema_ids, + failure_reason, + }) +} + +fn parse_rp_id(text: &str) -> StorageResult { + text.parse().map_err(|_| { + StorageError::ActivityDb(format!("invalid app_identifier: {text}")) + }) +} + +const fn failure_reason_to_i64(reason: ActivityFailureReason) -> i64 { + match reason { + ActivityFailureReason::NetworkError => 1, + ActivityFailureReason::Timeout => 2, + ActivityFailureReason::DeviceAuthenticationFailed => 3, + ActivityFailureReason::ProofGenerationFailed => 4, + ActivityFailureReason::RelyingPartyRejected => 5, + } +} + +fn i64_to_failure_reason(value: i64) -> StorageResult { + match value { + 1 => Ok(ActivityFailureReason::NetworkError), + 2 => Ok(ActivityFailureReason::Timeout), + 3 => Ok(ActivityFailureReason::DeviceAuthenticationFailed), + 4 => Ok(ActivityFailureReason::ProofGenerationFailed), + 5 => Ok(ActivityFailureReason::RelyingPartyRejected), + other => Err(StorageError::ActivityDb(format!( + "invalid failure reason: {other}" + ))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::cache::CacheDb; + use secrecy::SecretBox; + use std::fs; + use std::path::{Path, PathBuf}; + use uuid::Uuid; + + fn temp_cache_path() -> PathBuf { + let mut path = std::env::temp_dir(); + path.push(format!( + "walletkit-cache-activity-{}.sqlite", + Uuid::new_v4() + )); + path + } + + fn cleanup_cache_files(path: &Path) { + let _ = fs::remove_file(path); + let _ = fs::remove_file(path.with_extension("sqlite-wal")); + let _ = fs::remove_file(path.with_extension("sqlite-shm")); + } + + fn sample_entry() -> ActivityEntry { + ActivityEntry { + id: None, + rp_id: 1, + client_id: "request-uuid-1".to_string(), + protocol: ProtocolVersion::V3, + timestamp: None, + issuer_schema_ids: vec![10], + outcome: ActivityOutcome::Completed, + failure_reason: None, + } + } + + #[test] + fn test_record_and_list_activity() { + let path = temp_cache_path(); + let key = SecretBox::init_with(|| [0x42u8; 32]); + let db = CacheDb::new(&path, &key).expect("create cache"); + + let entry_id = db + .record_activity(&sample_entry(), 1000) + .expect("record activity"); + + let entries = db + .list_activities(ActivityQuery::default(), 10, 0) + .expect("list activities"); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].id, Some(entry_id)); + assert_eq!(entries[0].outcome, ActivityOutcome::Completed); + assert_eq!(entries[0].issuer_schema_ids.len(), 1); + + cleanup_cache_files(&path); + } + + #[test] + fn test_record_activity_failed_requires_failure_reason() { + let path = temp_cache_path(); + let key = SecretBox::init_with(|| [0x02u8; 32]); + let db = CacheDb::new(&path, &key).expect("create cache"); + + let entry = ActivityEntry { + outcome: ActivityOutcome::Failed, + failure_reason: None, + ..sample_entry() + }; + + let err = db + .record_activity(&entry, 1000) + .expect_err("Failed without failure_reason should be rejected"); + + assert!(matches!(err, StorageError::ActivityInvalidRecord(_))); + + cleanup_cache_files(&path); + } + + #[test] + fn test_record_activity_rejects_failure_reason_without_failed_outcome() { + let path = temp_cache_path(); + let key = SecretBox::init_with(|| [0x03u8; 32]); + let db = CacheDb::new(&path, &key).expect("create cache"); + + let entry = ActivityEntry { + outcome: ActivityOutcome::Completed, + failure_reason: Some(ActivityFailureReason::NetworkError), + ..sample_entry() + }; + + let err = db + .record_activity(&entry, 1000) + .expect_err("failure_reason without Failed outcome should be rejected"); + + assert!(matches!(err, StorageError::ActivityInvalidRecord(_))); + + cleanup_cache_files(&path); + } + + #[test] + fn test_list_activities_paginates_with_offset() { + let path = temp_cache_path(); + let key = SecretBox::init_with(|| [0x05u8; 32]); + let db = CacheDb::new(&path, &key).expect("create cache"); + + for i in 0..5u64 { + db.record_activity(&sample_entry(), 1000 + i) + .expect("record activity"); + } + + let page1 = db + .list_activities(ActivityQuery::default(), 2, 0) + .expect("list page 1"); + let page2 = db + .list_activities(ActivityQuery::default(), 2, 2) + .expect("list page 2"); + let page3 = db + .list_activities(ActivityQuery::default(), 2, 4) + .expect("list page 3"); + + assert_eq!(page1.len(), 2); + assert_eq!(page2.len(), 2); + assert_eq!(page3.len(), 1); + + assert_eq!(page1[0].timestamp, Some(1004)); + assert_eq!(page1[1].timestamp, Some(1003)); + assert_eq!(page2[0].timestamp, Some(1002)); + assert_eq!(page3[0].timestamp, Some(1000)); + + cleanup_cache_files(&path); + } + + #[test] + fn test_activity_metadata_total_count() { + let path = temp_cache_path(); + let key = SecretBox::init_with(|| [0x06u8; 32]); + let db = CacheDb::new(&path, &key).expect("create cache"); + + assert_eq!(db.activity_metadata().expect("metadata").total_count, 0); + + db.record_activity(&sample_entry(), 1000) + .expect("record activity"); + db.record_activity(&sample_entry(), 1001) + .expect("record activity"); + + assert_eq!(db.activity_metadata().expect("metadata").total_count, 2); + + cleanup_cache_files(&path); + } + + #[test] + fn test_activity_survives_cache_reopen() { + let path = temp_cache_path(); + let key = SecretBox::init_with(|| [0x07u8; 32]); + let db = CacheDb::new(&path, &key).expect("create cache"); + db.record_activity(&sample_entry(), 1000) + .expect("record activity"); + drop(db); + + let db = CacheDb::new(&path, &key).expect("reopen cache"); + let entries = db + .list_activities(ActivityQuery::default(), 10, 0) + .expect("list after reopen"); + assert_eq!( + entries.len(), + 1, + "activity history must survive a cache reopen" + ); + + cleanup_cache_files(&path); + } +} diff --git a/crates/walletkit-core/src/storage/cache/mod.rs b/crates/walletkit-core/src/storage/cache/mod.rs index 64956a485..d27d0b006 100644 --- a/crates/walletkit-core/src/storage/cache/mod.rs +++ b/crates/walletkit-core/src/storage/cache/mod.rs @@ -3,9 +3,11 @@ use std::path::Path; use crate::storage::error::StorageResult; +use crate::storage::types::{ActivityEntry, ActivityMetadata, ActivityQuery}; use secrecy::SecretBox; use walletkit_db::Vault; +mod activity; mod maintenance; mod merkle; mod nullifiers; @@ -133,16 +135,75 @@ impl CacheDb { pub fn replay_guard_set(&self, nullifier: [u8; 32], now: u64) -> StorageResult<()> { nullifiers::replay_guard_set(self.vault.connection(), nullifier, now) } + + /// Records an activity entry. + /// + /// # Errors + /// + /// Returns an error if the entry is misconfigured or the insert fails. + pub fn record_activity( + &self, + entry: &ActivityEntry, + now: u64, + ) -> StorageResult { + activity::record(self.vault.connection(), entry, now) + } + + /// Lists activity entries, most recent first. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub fn list_activities( + &self, + query: ActivityQuery, + limit: u32, + offset: u32, + ) -> StorageResult> { + activity::list(self.vault.connection(), query, limit, offset) + } + + /// Returns aggregate activity metadata. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub fn activity_metadata(&self) -> StorageResult { + activity::metadata(self.vault.connection()) + } + + /// Deletes all activity entries. Returns the number of entries deleted. + /// + /// # Errors + /// + /// Returns an error if the delete fails. + pub fn clear_activities(&self) -> StorageResult { + activity::clear(self.vault.connection()) + } } #[cfg(test)] mod tests { use super::*; + use crate::storage::types::{ActivityOutcome, ProtocolVersion}; use secrecy::SecretBox; use std::fs; use std::path::PathBuf; use uuid::Uuid; + fn sample_new_activity_entry() -> ActivityEntry { + ActivityEntry { + id: None, + rp_id: 1, + client_id: "req-1".to_string(), + protocol: ProtocolVersion::V3, + timestamp: None, + issuer_schema_ids: vec![], + outcome: ActivityOutcome::Completed, + failure_reason: None, + } + } + fn temp_cache_path() -> PathBuf { let mut path = std::env::temp_dir(); path.push(format!("walletkit-cache-{}.sqlite", Uuid::new_v4())); @@ -235,4 +296,113 @@ mod tests { cleanup_cache_files(&path); cleanup_lock_file(&lock_path); } + + #[test] + fn test_activity_survives_disposable_cache_reset() { + let path = temp_cache_path(); + let key = SecretBox::init_with(|| [0x77u8; 32]); + let lock_path = temp_lock_path(); + let db = CacheDb::new(&path, &key).expect("create cache"); + + db.record_activity(&sample_new_activity_entry(), 1000) + .expect("record activity"); + + db.session_seed_put([0x01u8; 32], [0x02u8; 32], 1000, 1000) + .expect("put session seed"); + + drop(db); + + let conn = walletkit_sqlite::cipher::open_encrypted(&path, &key, false) + .expect("open raw connection"); + conn.execute( + "UPDATE cache_meta SET schema_version = schema_version + 1", + &[], + ) + .expect("bump schema version"); + drop(conn); + + let db = CacheDb::new(&path, &key).expect("reopen cache after version bump"); + + let seed = db + .session_seed_get([0x01u8; 32], 1000) + .expect("get session seed"); + + assert!( + seed.is_none(), + "disposable cache_entries should be wiped on a schema version mismatch" + ); + + let entries = db + .list_activities(ActivityQuery::default(), 10, 0) + .expect("list activities after version bump"); + + assert_eq!( + entries.len(), + 1, + "activity history must survive a disposable-cache schema reset" + ); + + cleanup_cache_files(&path); + cleanup_lock_file(&lock_path); + } + + #[test] + fn test_activity_migration_applies_to_preexisting_cache_file() { + let path = temp_cache_path(); + let key = SecretBox::init_with(|| [0x88u8; 32]); + let lock_path = temp_lock_path(); + + let conn = walletkit_sqlite::cipher::open_encrypted(&path, &key, false) + .expect("create raw connection"); + conn.execute_batch( + "CREATE TABLE cache_meta ( + schema_version INTEGER NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ); + CREATE TABLE cache_entries ( + key_bytes BLOB NOT NULL, + value_bytes BLOB NOT NULL, + inserted_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL, + PRIMARY KEY (key_bytes) + ); + INSERT INTO cache_meta (schema_version, created_at, updated_at) + VALUES (2, 1000, 1000); + INSERT INTO cache_entries (key_bytes, value_bytes, inserted_at, expires_at) + VALUES (X'AA', X'BB', 1000, 999999999);", + ) + .expect("seed legacy cache schema"); + drop(conn); + + let db = CacheDb::new(&path, &key).expect("open legacy cache file"); + + db.record_activity(&sample_new_activity_entry(), 1000) + .expect("record activity after migration"); + + let entries = db + .list_activities(ActivityQuery::default(), 10, 0) + .expect("list activities"); + + assert_eq!(entries.len(), 1, "migration should add activity_entries"); + + drop(db); + + let conn = walletkit_sqlite::cipher::open_encrypted(&path, &key, false) + .expect("reopen raw connection"); + + let count = conn + .query_row("SELECT COUNT(*) FROM cache_entries", &[], |stmt| { + Ok(stmt.column_i64(0)) + }) + .expect("count cache_entries"); + + assert_eq!( + count, 1, + "pre-existing cache_entries row must survive the activity migration" + ); + + cleanup_cache_files(&path); + cleanup_lock_file(&lock_path); + } } diff --git a/crates/walletkit-core/src/storage/cache/schema.rs b/crates/walletkit-core/src/storage/cache/schema.rs index 95673b94e..aebc57053 100644 --- a/crates/walletkit-core/src/storage/cache/schema.rs +++ b/crates/walletkit-core/src/storage/cache/schema.rs @@ -49,6 +49,9 @@ pub(super) fn ensure_schema(conn: &Connection) -> DbResult<()> { insert_meta(conn)?; } } + + ensure_activity_schema(conn)?; + Ok(()) } @@ -88,3 +91,21 @@ fn insert_meta(conn: &Connection) -> DbResult<()> { )?; Ok(()) } + +pub(super) fn ensure_activity_schema(conn: &Connection) -> DbResult<()> { + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS activity_entries ( + entry_id INTEGER PRIMARY KEY, + client_id TEXT NOT NULL, + protocol INTEGER NOT NULL, + created_at INTEGER NOT NULL, + outcome TEXT NOT NULL, + app_identifier TEXT NOT NULL, + issuer_schema_ids BLOB NOT NULL, + failure_reason INTEGER NULL + ); + + CREATE INDEX IF NOT EXISTS idx_activity_entries_created_at + ON activity_entries (created_at DESC);", + ) +} diff --git a/crates/walletkit-core/src/storage/cache/util.rs b/crates/walletkit-core/src/storage/cache/util.rs index af0df2eb4..6cd144e31 100644 --- a/crates/walletkit-core/src/storage/cache/util.rs +++ b/crates/walletkit-core/src/storage/cache/util.rs @@ -237,3 +237,9 @@ pub(super) fn to_i64(value: u64, label: &str) -> StorageResult { StorageError::CacheDb(format!("{label} out of range for i64: {value}")) }) } + +pub(super) fn to_u64(value: i64, label: &str) -> StorageResult { + u64::try_from(value).map_err(|_| { + StorageError::CacheDb(format!("{label} out of range for u64: {value}")) + }) +} diff --git a/crates/walletkit-core/src/storage/credential_storage.rs b/crates/walletkit-core/src/storage/credential_storage.rs index c62b82ecd..cdebc0006 100644 --- a/crates/walletkit-core/src/storage/credential_storage.rs +++ b/crates/walletkit-core/src/storage/credential_storage.rs @@ -11,9 +11,9 @@ use super::keys::StorageKeys; use super::paths::StoragePaths; use super::traits::StorageProvider; #[cfg(not(target_arch = "wasm32"))] -use super::traits::VaultChangedListener; +use super::traits::{ActivityChangedListener, VaultChangedListener}; use super::traits::{AtomicBlobStore, DeviceKeystore}; -use super::types::CredentialRecord; +use super::types::{ActivityEntry, ActivityMetadata, ActivityQuery, CredentialRecord}; use super::ACCOUNT_KEYS_FILENAME; use super::{CacheDb, CredentialVault}; use super::{StorageLock, StorageLockGuard}; @@ -68,6 +68,8 @@ pub struct CredentialStore { /// Kept outside `inner` so we can notify after releasing the storage mutex. #[cfg(not(target_arch = "wasm32"))] vault_changed_tx: Mutex>>, + #[cfg(not(target_arch = "wasm32"))] + activity_changed_tx: Mutex>>, } impl std::fmt::Debug for CredentialStore { @@ -159,6 +161,8 @@ impl CredentialStore { inner: Mutex::new(inner), #[cfg(not(target_arch = "wasm32"))] vault_changed_tx: Mutex::new(None), + #[cfg(not(target_arch = "wasm32"))] + activity_changed_tx: Mutex::new(None), }) } @@ -177,6 +181,8 @@ impl CredentialStore { inner: Mutex::new(inner), #[cfg(not(target_arch = "wasm32"))] vault_changed_tx: Mutex::new(None), + #[cfg(not(target_arch = "wasm32"))] + activity_changed_tx: Mutex::new(None), }) } @@ -295,6 +301,63 @@ impl CredentialStore { pub fn danger_delete_all_credentials(&self) -> StorageResult { self.lock_inner()?.danger_delete_all_credentials() } + + /// Records a new activity entry. + /// + /// # Errors + /// + /// Returns an error if the store is not initialized or the query fails. + pub fn record_activity( + &self, + entry: &ActivityEntry, + now: u64, + ) -> StorageResult { + let result = self.lock_inner()?.record_activity(entry, now); + + if result.is_ok() { + self.notify_activity_changed(); + } + + result + } + + /// Lists activity entries, most recent first. + /// + /// # Errors + /// + /// Returns an error if the store is not initialized or the query fails. + pub fn list_activities( + &self, + query: ActivityQuery, + limit: u32, + offset: u32, + ) -> StorageResult> { + self.lock_inner()?.list_activities(query, limit, offset) + } + + /// Returns aggregate credential-activity metadata. + /// + /// # Errors + /// + /// Returns an error if the store is not initialized or the query fails. + pub fn activity_metadata(&self) -> StorageResult { + self.lock_inner()?.activity_metadata() + } + + /// Deletes all activity entries. Returns the number of entries deleted. + /// + /// # Errors + /// + /// Returns an error if the store is not initialized or the delete fails. + pub fn clear_activities(&self) -> StorageResult { + let result = self.lock_inner()?.clear_activities(); + + if result.is_ok() { + self.notify_activity_changed(); + } + + result + } } #[uniffi::export] @@ -394,6 +457,35 @@ impl CredentialStore { } } } + + /// Registers a listener that is called after activity history changes. + /// Listeners must not call back into the store or a deadlock will occur. + #[cfg(not(target_arch = "wasm32"))] + pub fn set_activity_changed_listener( + &self, + listener: Arc, + ) { + let (tx, rx) = mpsc::sync_channel(1); + + let spawn_result = std::thread::Builder::new() + .name("walletkit-activity-notify".into()) + .spawn(move || { + for () in rx { + listener.on_activity_changed(); + } + }); + + match spawn_result { + Ok(_) => { + if let Ok(mut guard) = self.activity_changed_tx.lock() { + *guard = Some(tx); + } + } + Err(e) => { + tracing::error!("failed to spawn activity notification thread: {e}"); + } + } + } } /// Implementation not exposed to foreign bindings @@ -473,6 +565,28 @@ impl CredentialStore { } } + /// Notify to the registered activity-changed listener there has been changes. + fn notify_activity_changed(&self) { + #[cfg(not(target_arch = "wasm32"))] + match self.activity_changed_tx.lock() { + Ok(guard) => { + if let Some(tx) = guard.as_ref() { + match tx.try_send(()) { + Ok(()) | Err(mpsc::TrySendError::Full(())) => {} + Err(mpsc::TrySendError::Disconnected(())) => { + tracing::warn!("activity-changed listener disconnected"); + } + } + } + } + Err(_) => { + tracing::warn!( + "activity-changed-tx mutex poisoned; dropping notification" + ); + } + } + } + fn lock_inner( &self, ) -> StorageResult> { @@ -629,6 +743,35 @@ impl CredentialStoreInner { ) } + fn record_activity( + &mut self, + entry: &ActivityEntry, + now: u64, + ) -> StorageResult { + let state = self.state_mut()?; + state.cache.record_activity(entry, now) + } + + fn list_activities( + &self, + query: ActivityQuery, + limit: u32, + offset: u32, + ) -> StorageResult> { + let state = self.state()?; + state.cache.list_activities(query, limit, offset) + } + + fn activity_metadata(&self) -> StorageResult { + let state = self.state()?; + state.cache.activity_metadata() + } + + fn clear_activities(&mut self) -> StorageResult { + let state = self.state_mut()?; + state.cache.clear_activities() + } + fn store_session_seed( &mut self, oprf_seed: CoreFieldElement, @@ -819,7 +962,6 @@ impl CredentialStoreInner { /// Permanently destroys all storage data: encryption keys, vault, and cache. fn destroy_storage(&mut self) -> StorageResult<()> { let _guard = self.guard()?; - // Drop in-memory state: zeroizes keys, closes database connections. self.state = None; // Delete the encryption key envelope. Without this key the database // files are unreadable even if file deletion below fails. @@ -846,6 +988,8 @@ impl CredentialStore { inner: Mutex::new(inner), #[cfg(not(target_arch = "wasm32"))] vault_changed_tx: Mutex::new(None), + #[cfg(not(target_arch = "wasm32"))] + activity_changed_tx: Mutex::new(None), }) } @@ -864,6 +1008,8 @@ impl CredentialStore { inner: Mutex::new(inner), #[cfg(not(target_arch = "wasm32"))] vault_changed_tx: Mutex::new(None), + #[cfg(not(target_arch = "wasm32"))] + activity_changed_tx: Mutex::new(None), }) } @@ -883,6 +1029,7 @@ mod tests { use crate::storage::tests_utils::{ cleanup_test_storage, temp_root_path, InMemoryStorageProvider, }; + use crate::storage::types::{ActivityOutcome, ProtocolVersion}; use std::sync::atomic::{AtomicU32, Ordering}; @@ -894,6 +1041,27 @@ mod tests { } } + struct TestActivityListener(Arc); + + impl ActivityChangedListener for TestActivityListener { + fn on_activity_changed(&self) { + self.0.fetch_add(1, Ordering::SeqCst); + } + } + + fn sample_new_activity_entry() -> ActivityEntry { + ActivityEntry { + id: None, + rp_id: 1, + client_id: "bridge-request-1".to_string(), + protocol: ProtocolVersion::V3, + timestamp: None, + issuer_schema_ids: vec![], + outcome: ActivityOutcome::Completed, + failure_reason: None, + } + } + fn wait_for_listener_count(count: &AtomicU32, expected: u32) { let deadline = std::time::Instant::now() + std::time::Duration::from_secs(1); @@ -1760,6 +1928,59 @@ mod tests { cleanup_test_storage(&root); } + #[test] + fn test_activity_changed_listener_notified_on_record() { + let root = temp_root_path(); + let provider = InMemoryStorageProvider::new(&root); + let store = CredentialStore::from_provider(&provider).expect("create store"); + store.init(42, 1000).expect("init storage"); + + let count = Arc::new(AtomicU32::new(0)); + store.set_activity_changed_listener(Arc::new(TestActivityListener( + Arc::clone(&count), + ))); + + store + .record_activity(&sample_new_activity_entry(), 1000) + .expect("record activity"); + + wait_for_listener_count(&count, 1); + + cleanup_test_storage(&root); + } + + #[test] + fn test_activity_changed_listener_not_notified_on_failure() { + let root = temp_root_path(); + let provider = InMemoryStorageProvider::new(&root); + let store = CredentialStore::from_provider(&provider).expect("create store"); + store.init(42, 1000).expect("init storage"); + + let count = Arc::new(AtomicU32::new(0)); + store.set_activity_changed_listener(Arc::new(TestActivityListener( + Arc::clone(&count), + ))); + + let invalid_entry = ActivityEntry { + outcome: ActivityOutcome::Failed, + failure_reason: None, + ..sample_new_activity_entry() + }; + + let result = store.record_activity(&invalid_entry, 1000); + assert!(result.is_err()); + + std::thread::sleep(std::time::Duration::from_millis(50)); + + assert_eq!( + count.load(Ordering::SeqCst), + 0, + "listener should not be notified when record_activity fails" + ); + + cleanup_test_storage(&root); + } + #[test] fn test_no_listener_does_not_panic() { use world_id_core::Credential as CoreCredential; diff --git a/crates/walletkit-core/src/storage/error.rs b/crates/walletkit-core/src/storage/error.rs index 8871c7bde..aed03622d 100644 --- a/crates/walletkit-core/src/storage/error.rs +++ b/crates/walletkit-core/src/storage/error.rs @@ -83,6 +83,14 @@ pub enum StorageError { key_prefix: u8, }, + /// Errors coming from the activity database. + #[error("activity db error: {0}")] + ActivityDb(String), + + /// An `ActivityEntry` violated the `failure_reason`/`outcome` invariant. + #[error("invalid activity record: {0}")] + ActivityInvalidRecord(String), + /// Unexpected `UniFFI` callback error. #[error("unexpected uniffi callback error: {0}")] UnexpectedUniFFICallbackError(String), diff --git a/crates/walletkit-core/src/storage/mod.rs b/crates/walletkit-core/src/storage/mod.rs index 9bdbeaab0..9ebc1c0ef 100644 --- a/crates/walletkit-core/src/storage/mod.rs +++ b/crates/walletkit-core/src/storage/mod.rs @@ -59,11 +59,13 @@ pub use error::{StorageError, StorageResult}; pub use keys::StorageKeys; pub use paths::StoragePaths; pub use traits::{ - AtomicBlobStore, DeviceKeystore, StorageProvider, VaultChangedListener, + ActivityChangedListener, AtomicBlobStore, DeviceKeystore, StorageProvider, + VaultChangedListener, }; pub use types::{ - BlobKind, ContentId, CredentialRecord, Nullifier, ReplayGuardKind, - ReplayGuardResult, RequestId, + ActivityEntry, ActivityFailureReason, ActivityMetadata, ActivityOutcome, + ActivityQuery, BlobKind, ContentId, CredentialRecord, Nullifier, ProtocolVersion, + ReplayGuardKind, ReplayGuardResult, RequestId, }; pub use walletkit_db::{Lock as StorageLock, LockGuard as StorageLockGuard}; diff --git a/crates/walletkit-core/src/storage/traits.rs b/crates/walletkit-core/src/storage/traits.rs index 69f6a04b7..6ae72fb8b 100644 --- a/crates/walletkit-core/src/storage/traits.rs +++ b/crates/walletkit-core/src/storage/traits.rs @@ -115,3 +115,28 @@ pub trait VaultChangedListener: Send + Sync { /// Called after a credential is added or removed. fn on_vault_changed(&self); } + +/// Listener notified when credential-activity history changes. +/// +/// Register via [`super::CredentialStore::set_activity_changed_listener`]. The +/// callback is delivered on a dedicated background thread to avoid re-entering +/// the `UniFFI` call stack (see `logger.rs` for rationale). +/// +/// This is only called when an activity entry is recorded. +/// +/// # Expected usage +/// +/// The host app should treat this as a trigger to refresh from the store. It +/// is a signal only and is not intended to carry the changed data with it. +/// +/// # Safety +/// +/// **Warning:** implementors **must not** call back into +/// [`super::CredentialStore`] from +/// [`on_activity_changed`](ActivityChangedListener::on_activity_changed) — +/// doing so will deadlock. +#[cfg_attr(not(target_arch = "wasm32"), uniffi::export(with_foreign))] +pub trait ActivityChangedListener: Send + Sync { + /// Called after an activity entry is recorded, finalized, or reconciled. + fn on_activity_changed(&self); +} diff --git a/crates/walletkit-core/src/storage/types.rs b/crates/walletkit-core/src/storage/types.rs index 7758cae0c..592f1d6ac 100644 --- a/crates/walletkit-core/src/storage/types.rs +++ b/crates/walletkit-core/src/storage/types.rs @@ -1,5 +1,7 @@ //! Public types for credential storage. +use strum::{Display, EnumString}; + use super::error::{StorageError, StorageResult}; /// Kind of blob stored in the vault. @@ -78,3 +80,97 @@ pub struct ReplayGuardResult { /// Stored proof package bytes. pub bytes: Vec, } + +/// Which World ID protocol handled a proof-share request. +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +#[repr(u8)] +pub enum ProtocolVersion { + /// Legacy Semaphore-based protocol. + V3 = 3, + /// Current. Reference: + V4 = 4, +} + +impl ProtocolVersion { + pub(crate) const fn as_i64(self) -> i64 { + self as i64 + } +} + +impl TryFrom for ProtocolVersion { + type Error = StorageError; + + fn try_from(value: i64) -> StorageResult { + match value { + 3 => Ok(Self::V3), + 4 => Ok(Self::V4), + _ => Err(StorageError::ActivityDb(format!( + "invalid protocol version {value}" + ))), + } + } +} + +/// Terminal outcome of a proof-share request. +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display, uniffi::Enum)] +#[strum(serialize_all = "lowercase")] +pub enum ActivityOutcome { + /// Proof request was completed successfully. + Completed, + /// The user declined the request. + Declined, + /// The user cancelled or dismissed the request without an explicit decline. + Cancelled, + /// The request failed (see [`ActivityFailureReason`]). + Failed, + /// The request never reached a terminal outcome (e.g. the app was killed + /// or backgrounded before completion). + Incomplete, +} + +/// Reasons a proof fails. +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +pub enum ActivityFailureReason { + /// A network request failed. + NetworkError, + /// The request timed out. + Timeout, + /// Device authentication (e.g. Face ID/passcode) failed. + DeviceAuthenticationFailed, + /// Proof generation itself failed. + ProofGenerationFailed, + /// The relying party rejected the proof. + RelyingPartyRejected, +} + +/// A single row of credential activity history. +#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)] +pub struct ActivityEntry { + /// Unique identifier for this entry. + pub id: Option, + /// The relying party identifier. + pub rp_id: u64, + /// Host-app-defined identifier correlating this entry with its request. + pub client_id: String, + /// Protocol used for this request. + pub protocol: ProtocolVersion, + /// Activity time. + pub timestamp: Option, + /// The result of the activity. + pub outcome: ActivityOutcome, + /// The credentials which produced an output proof for the request. + pub issuer_schema_ids: Vec, + /// Present only when `outcome` is `Failed`. + pub failure_reason: Option, +} + +/// Aggregate counts over credential activity history. +#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Record)] +pub struct ActivityMetadata { + /// Total number of recorded entries. + pub total_count: u64, +} + +/// Filtering/sorting options for [`super::CredentialStore::list_activities`]. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, uniffi::Record)] +pub struct ActivityQuery {} diff --git a/crates/walletkit-db/src/sqlite/transaction.rs b/crates/walletkit-db/src/sqlite/transaction.rs index 125833488..d3380a36f 100644 --- a/crates/walletkit-db/src/sqlite/transaction.rs +++ b/crates/walletkit-db/src/sqlite/transaction.rs @@ -81,6 +81,20 @@ impl<'conn> Transaction<'conn> { self.conn.query_row(sql, params, mapper) } + /// See [`Connection::query_row_optional`]. + /// + /// # Errors + /// + /// Returns `Error` if preparation, execution, or the mapper fails. + pub fn query_row_optional( + &self, + sql: &str, + params: &[Value], + mapper: impl FnOnce(&Row<'_, '_>) -> DbResult, + ) -> DbResult> { + self.conn.query_row_optional(sql, params, mapper) + } + /// See [`Connection::prepare`]. /// /// # Errors From 8af79bc2835562e6f8a30c510fa331a71f53868a Mon Sep 17 00:00:00 2001 From: Paolo D'Amico Date: Fri, 4 Sep 2026 07:06:33 -0700 Subject: [PATCH 2/7] feat: minor improvements to activity history (#506) * Introduce Credential Activity History Store Introduces a sharable on-device store for Android and iOS to retain credential activity. We're re-using some components from the Wallet storage here, while maintaining a separate database for the local activity as it is a separate concern and should never be backed up with the existing systems. I've modeled the changes after how I typically use rust; mod.rs is mainly imports with most of the code in dedicated files, and tests in their related files. This system is expected to evolve over time so i've prioritized a sensible migration strategy as part of these changes. Tested integration on iOS. Will do Android later and follow up with any PRs that might be necessary for their integration. No runtime differences until the host apps are updated to use the new system. * feaat: minor improvements to activity history * feaat: minor improvements to activity history * Update schema.rs --------- Co-authored-by: James Michael --- .../src/storage/cache/activity.rs | 51 +++++++------------ .../walletkit-core/src/storage/cache/mod.rs | 6 +-- .../src/storage/cache/schema.rs | 7 +-- crates/walletkit-core/src/storage/types.rs | 3 +- 4 files changed, 25 insertions(+), 42 deletions(-) diff --git a/crates/walletkit-core/src/storage/cache/activity.rs b/crates/walletkit-core/src/storage/cache/activity.rs index 8ab15a3df..33d900d28 100644 --- a/crates/walletkit-core/src/storage/cache/activity.rs +++ b/crates/walletkit-core/src/storage/cache/activity.rs @@ -1,9 +1,9 @@ use crate::storage::error::{StorageError, StorageResult}; use crate::storage::types::{ - ActivityEntry, ActivityFailureReason, ActivityMetadata, ActivityOutcome, - ActivityQuery, ProtocolVersion, + ActivityEntry, ActivityMetadata, ActivityOutcome, ActivityQuery, ProtocolVersion, }; -use walletkit_sqlite::{params, Connection, Row, StepResult, Value}; +use crate::storage::ActivityFailureReason; +use walletkit_sqlite::{params, Connection, Row, StepResult}; use super::util::{map_db_err, to_i64, to_u64}; @@ -28,10 +28,6 @@ pub(super) fn record( let now_i64 = to_i64(now, "now")?; - let failure_reason_value = entry.failure_reason.map_or(Value::Null, |reason| { - Value::Integer(failure_reason_to_i64(reason)) - }); - let entry_id = conn .query_row( "INSERT INTO activity_entries ( @@ -46,7 +42,10 @@ pub(super) fn record( entry.outcome.to_string(), entry.rp_id.to_string(), encode_issuer_schema_ids(&entry.issuer_schema_ids), - failure_reason_value, + entry + .failure_reason + .map(|v| v.to_string()) + .unwrap_or_default(), ], |stmt| Ok(stmt.column_i64(0)), ) @@ -144,10 +143,19 @@ fn map_entry(row: &Row<'_, '_>) -> StorageResult { })?; let rp_id = parse_rp_id(&row.column_text(5))?; let issuer_schema_ids = decode_issuer_schema_ids(&row.column_blob(6))?; - let failure_reason = if row.is_column_null(7) { + + let failure_reason = row.column_text(7); + + let failure_reason = if failure_reason.is_empty() { None } else { - Some(i64_to_failure_reason(row.column_i64(7))?) + Some( + row.column_text(7) + .parse::() + .map_err(|_| { + StorageError::ActivityDb("invalid failure_reason in db".to_string()) + })?, + ) }; Ok(ActivityEntry { @@ -168,29 +176,6 @@ fn parse_rp_id(text: &str) -> StorageResult { }) } -const fn failure_reason_to_i64(reason: ActivityFailureReason) -> i64 { - match reason { - ActivityFailureReason::NetworkError => 1, - ActivityFailureReason::Timeout => 2, - ActivityFailureReason::DeviceAuthenticationFailed => 3, - ActivityFailureReason::ProofGenerationFailed => 4, - ActivityFailureReason::RelyingPartyRejected => 5, - } -} - -fn i64_to_failure_reason(value: i64) -> StorageResult { - match value { - 1 => Ok(ActivityFailureReason::NetworkError), - 2 => Ok(ActivityFailureReason::Timeout), - 3 => Ok(ActivityFailureReason::DeviceAuthenticationFailed), - 4 => Ok(ActivityFailureReason::ProofGenerationFailed), - 5 => Ok(ActivityFailureReason::RelyingPartyRejected), - other => Err(StorageError::ActivityDb(format!( - "invalid failure reason: {other}" - ))), - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/walletkit-core/src/storage/cache/mod.rs b/crates/walletkit-core/src/storage/cache/mod.rs index d27d0b006..563c32584 100644 --- a/crates/walletkit-core/src/storage/cache/mod.rs +++ b/crates/walletkit-core/src/storage/cache/mod.rs @@ -336,11 +336,7 @@ mod tests { .list_activities(ActivityQuery::default(), 10, 0) .expect("list activities after version bump"); - assert_eq!( - entries.len(), - 1, - "activity history must survive a disposable-cache schema reset" - ); + assert_eq!(entries.len(), 1); cleanup_cache_files(&path); cleanup_lock_file(&lock_path); diff --git a/crates/walletkit-core/src/storage/cache/schema.rs b/crates/walletkit-core/src/storage/cache/schema.rs index aebc57053..fa9836162 100644 --- a/crates/walletkit-core/src/storage/cache/schema.rs +++ b/crates/walletkit-core/src/storage/cache/schema.rs @@ -42,7 +42,7 @@ pub(super) fn ensure_schema(conn: &Connection) -> DbResult<()> { ensure_entries_schema(conn)?; } Some(_) => { - reset_schema(conn)?; + reset_cache_schema(conn)?; } None => { ensure_entries_schema(conn)?; @@ -70,13 +70,14 @@ fn ensure_entries_schema(conn: &Connection) -> DbResult<()> { ) } -fn reset_schema(conn: &Connection) -> DbResult<()> { +fn reset_cache_schema(conn: &Connection) -> DbResult<()> { conn.execute_batch( "DROP TABLE IF EXISTS used_nullifiers; DROP TABLE IF EXISTS merkle_proof_cache; DROP TABLE IF EXISTS session_keys; DROP TABLE IF EXISTS cache_entries;", )?; + // NOTE it currently skips the activity history (not part of the same migration schema) ensure_entries_schema(conn)?; conn.execute("DELETE FROM cache_meta;", &[])?; insert_meta(conn)?; @@ -102,7 +103,7 @@ pub(super) fn ensure_activity_schema(conn: &Connection) -> DbResult<()> { outcome TEXT NOT NULL, app_identifier TEXT NOT NULL, issuer_schema_ids BLOB NOT NULL, - failure_reason INTEGER NULL + failure_reason TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_activity_entries_created_at diff --git a/crates/walletkit-core/src/storage/types.rs b/crates/walletkit-core/src/storage/types.rs index 592f1d6ac..24478b24b 100644 --- a/crates/walletkit-core/src/storage/types.rs +++ b/crates/walletkit-core/src/storage/types.rs @@ -129,7 +129,8 @@ pub enum ActivityOutcome { } /// Reasons a proof fails. -#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display, uniffi::Enum)] +#[strum(serialize_all = "lowercase")] pub enum ActivityFailureReason { /// A network request failed. NetworkError, From b6e3d2b37bdab894c0f36988c9f987181e3a2530 Mon Sep 17 00:00:00 2001 From: vansh Date: Thu, 10 Sep 2026 14:31:27 -0700 Subject: [PATCH 3/7] fix(db): isolate SQLite in activity-only backport --- .github/workflows/ci.yml | 5 + ACTIVITY_BACKPORT.md | 95 ++++++++++++++++++ .../src/storage/cache/activity.rs | 2 +- .../walletkit-core/src/storage/cache/mod.rs | 6 +- crates/walletkit-db/Cargo.toml | 4 + crates/walletkit-db/build.rs | 7 +- .../walletkit-db/examples/native_link_host.c | 33 +++++++ .../examples/native_link_probe.rs | 69 +++++++++++++ .../examples/test_native_linking.sh | 42 ++++++++ crates/walletkit-db/src/native_sqlite.c | 97 +++++++++++++++++++ crates/walletkit-db/src/sqlite/cipher.rs | 14 +++ crates/walletkit-db/src/sqlite/ffi.rs | 21 ++++ 12 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 ACTIVITY_BACKPORT.md create mode 100644 crates/walletkit-db/examples/native_link_host.c create mode 100644 crates/walletkit-db/examples/native_link_probe.rs create mode 100644 crates/walletkit-db/examples/test_native_linking.sh create mode 100644 crates/walletkit-db/src/native_sqlite.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5aec8d38f..0c4a440a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,6 +132,11 @@ jobs: # See https://github.com/worldfnd/provekit, a specific nargo toolchain version is required toolchain: v1.0.0-beta.11 + - name: Test native SQLite link isolation + run: | + bash crates/walletkit-db/examples/test_native_linking.sh + bash crates/walletkit-db/examples/test_native_linking.sh release + # Includes temporary downstream UniFFI callback vtable patch: # https://github.com/mozilla/uniffi-rs/pull/2821 - name: Build the Swift project (with temporary UniFFI ASan workaround) diff --git a/ACTIVITY_BACKPORT.md b/ACTIVITY_BACKPORT.md new file mode 100644 index 000000000..79dba908a --- /dev/null +++ b/ACTIVITY_BACKPORT.md @@ -0,0 +1,95 @@ +# Activity-only WalletKit candidate + +This local candidate starts at `v0.21.4` (`f0e3795`) and backports only the +credential-activity changes from #481 (`555497e`) and #506 (`f7c15ee`), plus the +native SQLite isolation fix and its regression tests. Imports are adapted to +the original `walletkit-db` crate; the SQLite crate split is not required. + +The Flamingo changes, WASM OPFS persistence, session-seed cache changes, +World Chain endpoint changes, and dependency updates in 0.22.0 are excluded. +`Cargo.lock` remains the 0.21.4 lockfile. This is a local development candidate, +not the published 0.21.4 artifact: assign a distinct reviewed version before +publishing any package or binary. + +## Reproduced failure + +The unchanged 0.22.0 SQLite crate passes its ten unit tests in isolation. A +small native host linked with Apple's `-lsqlite3` before the WalletKit static +archive reproduces the reported error exactly: + +```text +WalletKit SQLite version: 3.51.0; cipher: None +vault db error: sqlite error 101: query returned no rows +``` + +WalletKit's unprefixed `sqlite3_*` references resolve to the host's SQLite. +The cipher validation introduced by #493 discovers that `PRAGMA cipher` +returns no row. This happens before vault schema/leaf-index initialization. +The vault schema itself did not change between 0.21.4 and 0.22.0. + +Removing that check in the same disposable reproducer makes initialization +succeed, but creates a plaintext SQLite database and accepts the wrong key. +Therefore removing the check, or backporting activity alone, is insufficient. +These experiments use synthetic keys and records only; no real wallet stores +were inspected or modified. + +## Fix + +Compile the original, checksum-verified sqlite3mc amalgamation inside +`crates/walletkit-db/src/native_sqlite.c`, with its SQLite API given internal +C linkage. Expose only the 21 WalletKit-prefixed wrappers needed by Rust's +native FFI. Both allocations and operations on every SQLite handle remain +within the same engine, independent of the app's link order. + +The original SQLite version, compile settings, ChaCha20 cipher, key encoding, +vault/envelope formats, and content IDs are preserved. A cipher check fails +closed with a diagnostic if the required engine is unavailable. The activity +table and behavior are those of the two backported upstream PRs. + +## Validation + +Run with the Rust and Nargo versions pinned by the repository: + +```sh +cargo test -p walletkit-core --lib storage:: --locked +cargo test -p walletkit-db --locked +cargo clippy -p walletkit-db --all-targets --locked -- -D warnings +bash crates/walletkit-db/examples/test_native_linking.sh +bash crates/walletkit-db/examples/test_native_linking.sh release +``` + +The native host tests both library orders with dead stripping enabled. They +check that the host retains its own SQLite engine, encrypted records survive +reopening, wrong keys fail, failed opens preserve existing file bytes, and +plaintext stores are not silently accepted. The archive is also checked for +unprefixed SQLite API symbols. Both profiles are wired into the existing +macOS Swift CI job. + +Local results: all 64 core storage tests and all 20 database tests pass. Clippy, +Rust formatting, and ShellCheck pass. The native debug and optimized release +regressions pass in both link orders. Swift-package and full iOS application +validation are tracked separately; native test success is not a claim of a +working device verification flow. + +Build the local Swift package with `cargo xtask swift local`. Its output is +`swift/local_build/walletkit-swift`; the iOS dependency can point to that +package for testing without publishing a release. + +## Review and rollout constraints + +GUARD-01, GUARD-02, and GUARD-06 require explicit human review of this +cryptographic storage/dependency change before release. No cipher check is +disabled, and no vault deletion, identity reset, or automatic storage migration +is introduced. + +An existing database previously written through the wrong SQLite engine may +be plaintext. This candidate preserves that file and rejects it; it does not +invent a recovery or migration policy. Assess affected existing accounts and +any required data-preserving migration before distribution. Validate real +IDKit initialization/proof flows and credential activity in both app targets, +including existing encrypted stores, before a narrow internal rollout. + +The SDK binary is shared: an activity/UI feature flag does not undo this +dependency change. Keep the prior SDK artifact available for rollback, and +confirm old/new versions retain data on upgrade and downgrade. Publishing, +tagging, and shipping this candidate are outside the local experiment. diff --git a/crates/walletkit-core/src/storage/cache/activity.rs b/crates/walletkit-core/src/storage/cache/activity.rs index 33d900d28..16c780bc1 100644 --- a/crates/walletkit-core/src/storage/cache/activity.rs +++ b/crates/walletkit-core/src/storage/cache/activity.rs @@ -3,7 +3,7 @@ use crate::storage::types::{ ActivityEntry, ActivityMetadata, ActivityOutcome, ActivityQuery, ProtocolVersion, }; use crate::storage::ActivityFailureReason; -use walletkit_sqlite::{params, Connection, Row, StepResult}; +use walletkit_db::{params, Connection, Row, StepResult}; use super::util::{map_db_err, to_i64, to_u64}; diff --git a/crates/walletkit-core/src/storage/cache/mod.rs b/crates/walletkit-core/src/storage/cache/mod.rs index 563c32584..ef149d8c6 100644 --- a/crates/walletkit-core/src/storage/cache/mod.rs +++ b/crates/walletkit-core/src/storage/cache/mod.rs @@ -312,7 +312,7 @@ mod tests { drop(db); - let conn = walletkit_sqlite::cipher::open_encrypted(&path, &key, false) + let conn = walletkit_db::cipher::open_encrypted(&path, &key, false) .expect("open raw connection"); conn.execute( "UPDATE cache_meta SET schema_version = schema_version + 1", @@ -348,7 +348,7 @@ mod tests { let key = SecretBox::init_with(|| [0x88u8; 32]); let lock_path = temp_lock_path(); - let conn = walletkit_sqlite::cipher::open_encrypted(&path, &key, false) + let conn = walletkit_db::cipher::open_encrypted(&path, &key, false) .expect("create raw connection"); conn.execute_batch( "CREATE TABLE cache_meta ( @@ -384,7 +384,7 @@ mod tests { drop(db); - let conn = walletkit_sqlite::cipher::open_encrypted(&path, &key, false) + let conn = walletkit_db::cipher::open_encrypted(&path, &key, false) .expect("reopen raw connection"); let count = conn diff --git a/crates/walletkit-db/Cargo.toml b/crates/walletkit-db/Cargo.toml index b55d70f05..cbd29be48 100644 --- a/crates/walletkit-db/Cargo.toml +++ b/crates/walletkit-db/Cargo.toml @@ -41,3 +41,7 @@ tempfile = { workspace = true } [lints] workspace = true + +[[example]] +name = "native_link_probe" +crate-type = ["staticlib"] diff --git a/crates/walletkit-db/build.rs b/crates/walletkit-db/build.rs index 30cb517d7..8a3a6c0b1 100644 --- a/crates/walletkit-db/build.rs +++ b/crates/walletkit-db/build.rs @@ -24,6 +24,7 @@ const EXPECTED_SHA256: &str = fn main() { println!("cargo:rerun-if-env-changed=DOCS_RS"); + println!("cargo:rerun-if-changed=src/native_sqlite.c"); if std::env::var_os("DOCS_RS").is_some() { return; @@ -59,7 +60,7 @@ fn build_sqlite3mc() { ); } - compile(&amalgamation_c, &source_dir); + compile(&source_dir); } fn download(dest: &Path) { @@ -104,12 +105,12 @@ fn extract(zip_path: &Path, dest_dir: &Path) { } } -fn compile(amalgamation_c: &Path, include_dir: &Path) { +fn compile(include_dir: &Path) { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); let mut build = cc::Build::new(); build - .file(amalgamation_c) + .file("src/native_sqlite.c") .include(include_dir) // Core SQLite configuration .define("SQLITE_CORE", None) diff --git a/crates/walletkit-db/examples/native_link_host.c b/crates/walletkit-db/examples/native_link_host.c new file mode 100644 index 000000000..d13326e78 --- /dev/null +++ b/crates/walletkit-db/examples/native_link_host.c @@ -0,0 +1,33 @@ +#include +#include + +extern int walletkit_native_link_probe(void); + +int main(void) { + sqlite3 *db = NULL; + sqlite3_stmt *statement = NULL; + if (sqlite3_open(":memory:", &db) != SQLITE_OK) { + fprintf(stderr, "Host SQLite open failed\n"); + return 1; + } + if (sqlite3_prepare_v2(db, "PRAGMA cipher", -1, &statement, NULL) != SQLITE_OK || + sqlite3_step(statement) != SQLITE_DONE) { + fprintf(stderr, "WalletKit replaced the host's SQLite engine\n"); + sqlite3_finalize(statement); + sqlite3_close(db); + return 1; + } + sqlite3_finalize(statement); + printf("Host SQLite: %s; cipher unavailable as expected\n", sqlite3_libversion()); + int result = walletkit_native_link_probe(); + if (sqlite3_exec(db, "CREATE TABLE host (id INTEGER); INSERT INTO host VALUES (1);", + NULL, NULL, NULL) != SQLITE_OK || sqlite3_changes(db) != 1) { + fprintf(stderr, "Host SQLite failed after WalletKit used its own engine\n"); + result = 1; + } + if (sqlite3_close(db) != SQLITE_OK) { + fprintf(stderr, "Host SQLite close failed\n"); + result = 1; + } + return result; +} diff --git a/crates/walletkit-db/examples/native_link_probe.rs b/crates/walletkit-db/examples/native_link_probe.rs new file mode 100644 index 000000000..3358b0f83 --- /dev/null +++ b/crates/walletkit-db/examples/native_link_probe.rs @@ -0,0 +1,69 @@ +//! Native link-order regression probe using disposable, synthetic database data. + +use secrecy::SecretBox; +use walletkit_db::{cipher, Connection}; + +/// Exercises encrypted storage in a host that also links an unrelated `SQLite`. +/// Returns zero on success; reports an actionable error and returns one otherwise. +#[no_mangle] +pub extern "C" fn walletkit_native_link_probe() -> i32 { + match probe() { + Ok(()) => 0, + Err(error) => { + eprintln!("WalletKit SQLite link regression: {error}"); + 1 + } + } +} + +fn probe() -> Result<(), Box> { + let dir = tempfile::tempdir()?; + let path = dir.path().join("encrypted.sqlite"); + let key = SecretBox::init_with(|| [0xAB; 32]); + + { + let conn = cipher::open_encrypted(&path, &key, false)?; + let cipher = + conn.query_row("PRAGMA cipher", &[], |row| Ok(row.column_text(0)))?; + if cipher != "chacha20" { + return Err("WalletKit did not select its chacha20 cipher".into()); + } + conn.execute_batch("CREATE TABLE probe (value TEXT); INSERT INTO probe VALUES ('synthetic-test-record');")?; + } + + let original_bytes = std::fs::read(&path)?; + if original_bytes.starts_with(b"SQLite format 3\0") { + return Err("encrypted database has a plaintext SQLite header".into()); + } + let wrong_key = SecretBox::init_with(|| [0xCD; 32]); + if cipher::open_encrypted(&path, &wrong_key, false).is_ok() { + return Err("encrypted database accepted the wrong key".into()); + } + if std::fs::read(&path)? != original_bytes { + return Err("failed wrong-key open modified the database".into()); + } + { + let conn = cipher::open_encrypted(&path, &key, false)?; + let value = conn + .query_row("SELECT value FROM probe", &[], |row| Ok(row.column_text(0)))?; + if value != "synthetic-test-record" { + return Err("correct-key reopen did not preserve the record".into()); + } + } + + // Do not silently reset or reinterpret any pre-existing plaintext store. + let plaintext_path = dir.path().join("plaintext.sqlite"); + Connection::open(&plaintext_path, false)? + .execute_batch("CREATE TABLE existing (value TEXT); INSERT INTO existing VALUES ('preserve-me');")?; + let plaintext_bytes = std::fs::read(&plaintext_path)?; + if cipher::open_encrypted(&plaintext_path, &key, false).is_ok() { + return Err("plaintext store was silently accepted as encrypted".into()); + } + if std::fs::read(&plaintext_path)? != plaintext_bytes { + return Err("failed plaintext-store open modified existing data".into()); + } + println!( + "PASS: encrypted reopen, wrong-key rejection, and existing-data preservation" + ); + Ok(()) +} diff --git a/crates/walletkit-db/examples/test_native_linking.sh b/crates/walletkit-db/examples/test_native_linking.sh new file mode 100644 index 000000000..dfb293949 --- /dev/null +++ b/crates/walletkit-db/examples/test_native_linking.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Run on macOS: exercise the same static-library/system-SQLite collision as iOS. +set -euo pipefail + +if [[ "$(uname -s)" != Darwin ]]; then + echo "This regression test requires the Apple linker and system SQLite." >&2 + exit 1 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +cd "$repo_root" +profile="${1:-dev}" +case "$profile" in + dev) profile_dir=debug ;; + release) profile_dir=release ;; + *) echo "Usage: bash $0 [dev|release]" >&2; exit 1 ;; +esac +cargo build --locked -p walletkit-db --example native_link_probe --profile "$profile" + +target_dir="${CARGO_TARGET_DIR:-target}" +output_dir="$target_dir/native-link-probe/$profile_dir" +archive="$target_dir/$profile_dir/examples/libnative_link_probe.a" +host_source="crates/walletkit-db/examples/native_link_host.c" +mkdir -p "$output_dir" + +# Static archive visibility matters: a hidden global symbol can still collide +# during the final app link. The standard SQLite API must have local linkage. +nm -gU "$archive" > "$output_dir/symbols.txt" 2> "$output_dir/nm.log" +if grep -E ' [A-Za-z] _sqlite3_' "$output_dir/symbols.txt"; then + echo "WalletKit still exposes or references unprefixed SQLite API symbols." >&2 + exit 1 +fi + +clang "$host_source" -Wl,-dead_strip -lsqlite3 "$archive" \ + -framework Security -framework CoreFoundation -o "$output_dir/system-first" +"$output_dir/system-first" + +clang "$host_source" -Wl,-dead_strip "$archive" -lsqlite3 \ + -framework Security -framework CoreFoundation -o "$output_dir/walletkit-first" +"$output_dir/walletkit-first" + +echo "PASS: both link orders preserve separate host and WalletKit SQLite engines" diff --git a/crates/walletkit-db/src/native_sqlite.c b/crates/walletkit-db/src/native_sqlite.c new file mode 100644 index 000000000..c96172bfa --- /dev/null +++ b/crates/walletkit-db/src/native_sqlite.c @@ -0,0 +1,97 @@ +/* + * Keep SQLite's C API local to this translation unit. Visibility attributes alone + * do not prevent a static-library consumer from resolving sqlite3_* against a + * different SQLite implementation. Only the WalletKit-prefixed wrappers below + * cross the Rust FFI boundary; every handle stays with its creating engine. + * + * Compile the unchanged, checksum-verified amalgamation with the existing + * cipher/settings. This changes linkage only, not encryption or disk formats. + */ +#define SQLITE_API static +#define SQLITE_EXTERN +#include "sqlite3mc_amalgamation.c" + +int walletkit_sqlite3_open_v2(const char *filename, sqlite3 **db, int flags, const char *vfs) { + return sqlite3_open_v2(filename, db, flags, vfs); +} + +int walletkit_sqlite3_close_v2(sqlite3 *db) { + return sqlite3_close_v2(db); +} + +int walletkit_sqlite3_exec(sqlite3 *db, const char *sql, int (*callback)(void *, int, char **, char **), void *arg, char **error) { + return sqlite3_exec(db, sql, callback, arg, error); +} + +void walletkit_sqlite3_free(void *pointer) { + sqlite3_free(pointer); +} + +int walletkit_sqlite3_prepare_v2(sqlite3 *db, const char *sql, int length, sqlite3_stmt **statement, const char **tail) { + return sqlite3_prepare_v2(db, sql, length, statement, tail); +} + +int walletkit_sqlite3_step(sqlite3_stmt *statement) { + return sqlite3_step(statement); +} + +int walletkit_sqlite3_reset(sqlite3_stmt *statement) { + return sqlite3_reset(statement); +} + +int walletkit_sqlite3_finalize(sqlite3_stmt *statement) { + return sqlite3_finalize(statement); +} + +int walletkit_sqlite3_bind_int64(sqlite3_stmt *statement, int index, sqlite3_int64 value) { + return sqlite3_bind_int64(statement, index, value); +} + +int walletkit_sqlite3_bind_blob(sqlite3_stmt *statement, int index, const void *value, int length, sqlite3_destructor_type destructor) { + return sqlite3_bind_blob(statement, index, value, length, destructor); +} + +int walletkit_sqlite3_bind_text(sqlite3_stmt *statement, int index, const char *value, int length, sqlite3_destructor_type destructor) { + return sqlite3_bind_text(statement, index, value, length, destructor); +} + +int walletkit_sqlite3_bind_null(sqlite3_stmt *statement, int index) { + return sqlite3_bind_null(statement, index); +} + +sqlite3_int64 walletkit_sqlite3_column_int64(sqlite3_stmt *statement, int column) { + return sqlite3_column_int64(statement, column); +} + +const void * walletkit_sqlite3_column_blob(sqlite3_stmt *statement, int column) { + return sqlite3_column_blob(statement, column); +} + +int walletkit_sqlite3_column_bytes(sqlite3_stmt *statement, int column) { + return sqlite3_column_bytes(statement, column); +} + +const unsigned char * walletkit_sqlite3_column_text(sqlite3_stmt *statement, int column) { + return sqlite3_column_text(statement, column); +} + +int walletkit_sqlite3_column_type(sqlite3_stmt *statement, int column) { + return sqlite3_column_type(statement, column); +} + +int walletkit_sqlite3_column_count(sqlite3_stmt *statement) { + return sqlite3_column_count(statement); +} + +const char * walletkit_sqlite3_errmsg(sqlite3 *db) { + return sqlite3_errmsg(db); +} + +int walletkit_sqlite3_changes(sqlite3 *db) { + return sqlite3_changes(db); +} + +sqlite3_int64 walletkit_sqlite3_last_insert_rowid(sqlite3 *db) { + return sqlite3_last_insert_rowid(db); +} + diff --git a/crates/walletkit-db/src/sqlite/cipher.rs b/crates/walletkit-db/src/sqlite/cipher.rs index 98c796988..2a1204412 100644 --- a/crates/walletkit-db/src/sqlite/cipher.rs +++ b/crates/walletkit-db/src/sqlite/cipher.rs @@ -55,6 +55,20 @@ pub fn open_encrypted( read_only: bool, ) -> DbResult { let conn = Connection::open(path, read_only)?; + // A host application can link another SQLite implementation. Fail before + // applying a key or writing a schema if the encrypted engine is unavailable. + conn.execute_batch("PRAGMA cipher = 'chacha20';")?; + let cipher = + conn.query_row_optional("PRAGMA cipher;", &[], |row| Ok(row.column_text(0)))?; + if !cipher + .as_deref() + .is_some_and(|name| name.eq_ignore_ascii_case("chacha20")) + { + return Err(Error::new( + -1, + "required sqlite3mc chacha20 cipher is unavailable; verify WalletKit SQLite symbol isolation", + )); + } apply_key(&conn, k_intermediate)?; configure_connection(&conn)?; Ok(conn) diff --git a/crates/walletkit-db/src/sqlite/ffi.rs b/crates/walletkit-db/src/sqlite/ffi.rs index d1fce0b1f..7eff63287 100644 --- a/crates/walletkit-db/src/sqlite/ffi.rs +++ b/crates/walletkit-db/src/sqlite/ffi.rs @@ -436,13 +436,16 @@ mod raw { type sqlite3_stmt = c_void; extern "C" { + #[link_name = "walletkit_sqlite3_open_v2"] pub fn sqlite3_open_v2( filename: *const c_char, pp_db: *mut *mut sqlite3, flags: c_int, z_vfs: *const c_char, ) -> c_int; + #[link_name = "walletkit_sqlite3_close_v2"] pub fn sqlite3_close_v2(db: *mut sqlite3) -> c_int; + #[link_name = "walletkit_sqlite3_exec"] pub fn sqlite3_exec( db: *mut sqlite3, sql: *const c_char, @@ -450,7 +453,9 @@ mod raw { arg: *mut c_void, errmsg: *mut *mut c_char, ) -> c_int; + #[link_name = "walletkit_sqlite3_free"] pub fn sqlite3_free(ptr: *mut c_void); + #[link_name = "walletkit_sqlite3_prepare_v2"] pub fn sqlite3_prepare_v2( db: *mut sqlite3, z_sql: *const c_char, @@ -458,14 +463,19 @@ mod raw { pp_stmt: *mut *mut sqlite3_stmt, pz_tail: *mut *const c_char, ) -> c_int; + #[link_name = "walletkit_sqlite3_step"] pub fn sqlite3_step(stmt: *mut sqlite3_stmt) -> c_int; + #[link_name = "walletkit_sqlite3_reset"] pub fn sqlite3_reset(stmt: *mut sqlite3_stmt) -> c_int; + #[link_name = "walletkit_sqlite3_finalize"] pub fn sqlite3_finalize(stmt: *mut sqlite3_stmt) -> c_int; + #[link_name = "walletkit_sqlite3_bind_int64"] pub fn sqlite3_bind_int64( stmt: *mut sqlite3_stmt, index: c_int, value: i64, ) -> c_int; + #[link_name = "walletkit_sqlite3_bind_blob"] pub fn sqlite3_bind_blob( stmt: *mut sqlite3_stmt, index: c_int, @@ -473,6 +483,7 @@ mod raw { n: c_int, destructor: isize, ) -> c_int; + #[link_name = "walletkit_sqlite3_bind_text"] pub fn sqlite3_bind_text( stmt: *mut sqlite3_stmt, index: c_int, @@ -480,21 +491,31 @@ mod raw { n: c_int, destructor: isize, ) -> c_int; + #[link_name = "walletkit_sqlite3_bind_null"] pub fn sqlite3_bind_null(stmt: *mut sqlite3_stmt, index: c_int) -> c_int; + #[link_name = "walletkit_sqlite3_column_int64"] pub fn sqlite3_column_int64(stmt: *mut sqlite3_stmt, i_col: c_int) -> i64; + #[link_name = "walletkit_sqlite3_column_blob"] pub fn sqlite3_column_blob( stmt: *mut sqlite3_stmt, i_col: c_int, ) -> *const c_void; + #[link_name = "walletkit_sqlite3_column_bytes"] pub fn sqlite3_column_bytes(stmt: *mut sqlite3_stmt, i_col: c_int) -> c_int; + #[link_name = "walletkit_sqlite3_column_text"] pub fn sqlite3_column_text( stmt: *mut sqlite3_stmt, i_col: c_int, ) -> *const c_char; + #[link_name = "walletkit_sqlite3_column_type"] pub fn sqlite3_column_type(stmt: *mut sqlite3_stmt, i_col: c_int) -> c_int; + #[link_name = "walletkit_sqlite3_column_count"] pub fn sqlite3_column_count(stmt: *mut sqlite3_stmt) -> c_int; + #[link_name = "walletkit_sqlite3_errmsg"] pub fn sqlite3_errmsg(db: *mut sqlite3) -> *const c_char; + #[link_name = "walletkit_sqlite3_changes"] pub fn sqlite3_changes(db: *mut sqlite3) -> c_int; + #[link_name = "walletkit_sqlite3_last_insert_rowid"] pub fn sqlite3_last_insert_rowid(db: *mut sqlite3) -> i64; } } From 5a4b8353b0c854b0eb4cede848a05815f0dd4dfc Mon Sep 17 00:00:00 2001 From: vansh Date: Thu, 10 Sep 2026 14:46:54 -0700 Subject: [PATCH 4/7] docs: record activity candidate build validation --- ACTIVITY_BACKPORT.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/ACTIVITY_BACKPORT.md b/ACTIVITY_BACKPORT.md index 79dba908a..2f3c7befe 100644 --- a/ACTIVITY_BACKPORT.md +++ b/ACTIVITY_BACKPORT.md @@ -65,11 +65,23 @@ plaintext stores are not silently accepted. The archive is also checked for unprefixed SQLite API symbols. Both profiles are wired into the existing macOS Swift CI job. -Local results: all 64 core storage tests and all 20 database tests pass. Clippy, -Rust formatting, and ShellCheck pass. The native debug and optimized release -regressions pass in both link orders. Swift-package and full iOS application -validation are tracked separately; native test success is not a claim of a -working device verification flow. +Local results on 2026-09-10: + +- All 64 core storage tests and all 20 database tests pass. +- Clippy, Rust formatting, and ShellCheck pass. +- Native debug and optimized release regressions pass in both link orders. +- The optimized Swift package builds for iOS device, ARM simulator, and Intel + simulator with the normal `compress-zkeys,embed-zkeys,v3` features. +- A native host using the actual ARM simulator release archive passes the + cipher-isolation check in both link orders on the iPhone 17 / iOS 26.5 + simulator. This probe uses only in-memory databases. +- `timeout 600 make build-id` succeeds in `world-app-ios` with the local + package temporarily selected. Required SwiftLint autofix/check passes with + zero violations. The published dependency pin and lockfile are restored + afterward. + +This validates compilation and native database linkage, not an end-to-end +IDKit verification on a real account. No device account data was accessed. Build the local Swift package with `cargo xtask swift local`. Its output is `swift/local_build/walletkit-swift`; the iOS dependency can point to that From 699b8bf7f83c3a452175a50bc33b6a3263254adc Mon Sep 17 00:00:00 2001 From: vansh Date: Thu, 10 Sep 2026 14:47:52 -0700 Subject: [PATCH 5/7] chore: finish candidate validation notes and whitespace --- ACTIVITY_BACKPORT.md | 3 +++ crates/walletkit-db/src/native_sqlite.c | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ACTIVITY_BACKPORT.md b/ACTIVITY_BACKPORT.md index 2f3c7befe..4c47fca56 100644 --- a/ACTIVITY_BACKPORT.md +++ b/ACTIVITY_BACKPORT.md @@ -82,6 +82,9 @@ Local results on 2026-09-10: This validates compilation and native database linkage, not an end-to-end IDKit verification on a real account. No device account data was accessed. +The successful app build also emitted nonfatal `DecodingError.dataCorrupted` +diagnostics during SDUI compilation; those diagnostics were not investigated +as part of this SDK change. Build the local Swift package with `cargo xtask swift local`. Its output is `swift/local_build/walletkit-swift`; the iOS dependency can point to that diff --git a/crates/walletkit-db/src/native_sqlite.c b/crates/walletkit-db/src/native_sqlite.c index c96172bfa..5dab447f1 100644 --- a/crates/walletkit-db/src/native_sqlite.c +++ b/crates/walletkit-db/src/native_sqlite.c @@ -94,4 +94,3 @@ int walletkit_sqlite3_changes(sqlite3 *db) { sqlite3_int64 walletkit_sqlite3_last_insert_rowid(sqlite3 *db) { return sqlite3_last_insert_rowid(db); } - From e72ed0e1239dcb62b541e4b9a47d45bfd240aac4 Mon Sep 17 00:00:00 2001 From: vansh Date: Thu, 10 Sep 2026 15:39:37 -0700 Subject: [PATCH 6/7] docs: defer backport CI wiring pending workflow access --- .github/workflows/ci.yml | 5 ----- ACTIVITY_BACKPORT.md | 13 ++++++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c4a440a5..5aec8d38f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,11 +132,6 @@ jobs: # See https://github.com/worldfnd/provekit, a specific nargo toolchain version is required toolchain: v1.0.0-beta.11 - - name: Test native SQLite link isolation - run: | - bash crates/walletkit-db/examples/test_native_linking.sh - bash crates/walletkit-db/examples/test_native_linking.sh release - # Includes temporary downstream UniFFI callback vtable patch: # https://github.com/mozilla/uniffi-rs/pull/2821 - name: Build the Swift project (with temporary UniFFI ASan workaround) diff --git a/ACTIVITY_BACKPORT.md b/ACTIVITY_BACKPORT.md index 4c47fca56..115946235 100644 --- a/ACTIVITY_BACKPORT.md +++ b/ACTIVITY_BACKPORT.md @@ -1,13 +1,13 @@ # Activity-only WalletKit candidate -This local candidate starts at `v0.21.4` (`f0e3795`) and backports only the +This backport candidate starts at `v0.21.4` (`f0e3795`) and backports only the credential-activity changes from #481 (`555497e`) and #506 (`f7c15ee`), plus the native SQLite isolation fix and its regression tests. Imports are adapted to the original `walletkit-db` crate; the SQLite crate split is not required. The Flamingo changes, WASM OPFS persistence, session-seed cache changes, World Chain endpoint changes, and dependency updates in 0.22.0 are excluded. -`Cargo.lock` remains the 0.21.4 lockfile. This is a local development candidate, +`Cargo.lock` remains the 0.21.4 lockfile. This is a development candidate, not the published 0.21.4 artifact: assign a distinct reviewed version before publishing any package or binary. @@ -62,8 +62,11 @@ The native host tests both library orders with dead stripping enabled. They check that the host retains its own SQLite engine, encrypted records survive reopening, wrong keys fail, failed opens preserve existing file bytes, and plaintext stores are not silently accepted. The archive is also checked for -unprefixed SQLite API symbols. Both profiles are wired into the existing -macOS Swift CI job. +unprefixed SQLite API symbols. Run both profiles manually for now: GitHub +rejected the workflow update because the push credential lacks `workflow` +scope. CI wiring is not included in this backport PR. A maintainer with +workflow-write access should add both commands above to the existing macOS +Swift job, with a bounded step timeout, before release. Local results on 2026-09-10: @@ -107,4 +110,4 @@ including existing encrypted stores, before a narrow internal rollout. The SDK binary is shared: an activity/UI feature flag does not undo this dependency change. Keep the prior SDK artifact available for rollback, and confirm old/new versions retain data on upgrade and downgrade. Publishing, -tagging, and shipping this candidate are outside the local experiment. +tagging, and shipping this candidate are outside this backport PR. From afc510a4978db16f43d227978e152fe98b21757e Mon Sep 17 00:00:00 2001 From: vansh Date: Thu, 10 Sep 2026 15:50:11 -0700 Subject: [PATCH 7/7] refactor: separate SQLite fix from activity backport --- ACTIVITY_BACKPORT.md | 123 ++++-------------- crates/walletkit-db/Cargo.toml | 4 - crates/walletkit-db/build.rs | 7 +- .../walletkit-db/examples/native_link_host.c | 33 ----- .../examples/native_link_probe.rs | 69 ---------- .../examples/test_native_linking.sh | 42 ------ crates/walletkit-db/src/native_sqlite.c | 96 -------------- crates/walletkit-db/src/sqlite/cipher.rs | 14 -- crates/walletkit-db/src/sqlite/ffi.rs | 21 --- 9 files changed, 27 insertions(+), 382 deletions(-) delete mode 100644 crates/walletkit-db/examples/native_link_host.c delete mode 100644 crates/walletkit-db/examples/native_link_probe.rs delete mode 100644 crates/walletkit-db/examples/test_native_linking.sh delete mode 100644 crates/walletkit-db/src/native_sqlite.c diff --git a/ACTIVITY_BACKPORT.md b/ACTIVITY_BACKPORT.md index 115946235..636c6238c 100644 --- a/ACTIVITY_BACKPORT.md +++ b/ACTIVITY_BACKPORT.md @@ -1,113 +1,38 @@ -# Activity-only WalletKit candidate +# Credential activity backport to v0.21.4 -This backport candidate starts at `v0.21.4` (`f0e3795`) and backports only the -credential-activity changes from #481 (`555497e`) and #506 (`f7c15ee`), plus the -native SQLite isolation fix and its regression tests. Imports are adapted to -the original `walletkit-db` crate; the SQLite crate split is not required. +This candidate starts at `v0.21.4` (`f0e3795`) and backports the credential +activity changes from #481 and #506. PR #533 targets +`codex/backport-base-v0.21.4`, which is pinned to that release commit. -The Flamingo changes, WASM OPFS persistence, session-seed cache changes, -World Chain endpoint changes, and dependency updates in 0.22.0 are excluded. -`Cargo.lock` remains the 0.21.4 lockfile. This is a development candidate, -not the published 0.21.4 artifact: assign a distinct reviewed version before -publishing any package or binary. +The backport includes recording activity, paginated history, aggregate +metadata, clearing history, change listeners, and their schema and tests. +Imports use the original `walletkit-db` crate. The small +`Transaction::query_row_optional` helper is required by the activity code. -## Reproduced failure - -The unchanged 0.22.0 SQLite crate passes its ten unit tests in isolation. A -small native host linked with Apple's `-lsqlite3` before the WalletKit static -archive reproduces the reported error exactly: - -```text -WalletKit SQLite version: 3.51.0; cipher: None -vault db error: sqlite error 101: query returned no rows -``` - -WalletKit's unprefixed `sqlite3_*` references resolve to the host's SQLite. -The cipher validation introduced by #493 discovers that `PRAGMA cipher` -returns no row. This happens before vault schema/leaf-index initialization. -The vault schema itself did not change between 0.21.4 and 0.22.0. - -Removing that check in the same disposable reproducer makes initialization -succeed, but creates a plaintext SQLite database and accepts the wrong key. -Therefore removing the check, or backporting activity alone, is insufficient. -These experiments use synthetic keys and records only; no real wallet stores -were inspected or modified. - -## Fix - -Compile the original, checksum-verified sqlite3mc amalgamation inside -`crates/walletkit-db/src/native_sqlite.c`, with its SQLite API given internal -C linkage. Expose only the 21 WalletKit-prefixed wrappers needed by Rust's -native FFI. Both allocations and operations on every SQLite handle remain -within the same engine, independent of the app's link order. - -The original SQLite version, compile settings, ChaCha20 cipher, key encoding, -vault/envelope formats, and content IDs are preserved. A cipher check fails -closed with a diagnostic if the required engine is unavailable. The activity -table and behavior are those of the two backported upstream PRs. +The SQLite engine, linkage, encryption open sequence, dependency lockfile, +workspace manifest, and toolchain pins remain those of v0.21.4. Other 0.22.0 +changes are excluded. The native SQLite isolation fix is developed separately +on `codex/sqlite-isolation-v0.21.4` and is not part of this PR. ## Validation -Run with the Rust and Nargo versions pinned by the repository: +Run the storage suite with the repository's pinned toolchain: ```sh cargo test -p walletkit-core --lib storage:: --locked -cargo test -p walletkit-db --locked -cargo clippy -p walletkit-db --all-targets --locked -- -D warnings -bash crates/walletkit-db/examples/test_native_linking.sh -bash crates/walletkit-db/examples/test_native_linking.sh release +cargo fmt --all -- --check +git diff --check ``` -The native host tests both library orders with dead stripping enabled. They -check that the host retains its own SQLite engine, encrypted records survive -reopening, wrong keys fail, failed opens preserve existing file bytes, and -plaintext stores are not silently accepted. The archive is also checked for -unprefixed SQLite API symbols. Run both profiles manually for now: GitHub -rejected the workflow update because the push credential lacks `workflow` -scope. CI wiring is not included in this backport PR. A maintainer with -workflow-write access should add both commands above to the existing macOS -Swift job, with a bounded step timeout, before release. - -Local results on 2026-09-10: - -- All 64 core storage tests and all 20 database tests pass. -- Clippy, Rust formatting, and ShellCheck pass. -- Native debug and optimized release regressions pass in both link orders. -- The optimized Swift package builds for iOS device, ARM simulator, and Intel - simulator with the normal `compress-zkeys,embed-zkeys,v3` features. -- A native host using the actual ARM simulator release archive passes the - cipher-isolation check in both link orders on the iPhone 17 / iOS 26.5 - simulator. This probe uses only in-memory databases. -- `timeout 600 make build-id` succeeds in `world-app-ios` with the local - package temporarily selected. Required SwiftLint autofix/check passes with - zero violations. The published dependency pin and lockfile are restored - afterward. - -This validates compilation and native database linkage, not an end-to-end -IDKit verification on a real account. No device account data was accessed. -The successful app build also emitted nonfatal `DecodingError.dataCorrupted` -diagnostics during SDUI compilation; those diagnostics were not investigated -as part of this SDK change. - -Build the local Swift package with `cargo xtask swift local`. Its output is -`swift/local_build/walletkit-swift`; the iOS dependency can point to that -package for testing without publishing a release. - -## Review and rollout constraints +Build and test results recorded for the earlier combined activity/SQLite +candidate do not establish validation of this activity-only revision. +The PR description records the checks performed after splitting the changes. -GUARD-01, GUARD-02, and GUARD-06 require explicit human review of this -cryptographic storage/dependency change before release. No cipher check is -disabled, and no vault deletion, identity reset, or automatic storage migration -is introduced. +## Release scope -An existing database previously written through the wrong SQLite engine may -be plaintext. This candidate preserves that file and rejects it; it does not -invent a recovery or migration policy. Assess affected existing accounts and -any required data-preserving migration before distribution. Validate real -IDKit initialization/proof flows and credential activity in both app targets, -including existing encrypted stores, before a narrow internal rollout. +Activity history adds its own cache table; the credential vault and envelope +formats remain unchanged. The activity schema and behavior follow #481/#506. +Validate activity persistence and host-app integration before distribution. -The SDK binary is shared: an activity/UI feature flag does not undo this -dependency change. Keep the prior SDK artifact available for rollback, and -confirm old/new versions retain data on upgrade and downgrade. Publishing, -tagging, and shipping this candidate are outside this backport PR. +The source retains v0.21.4 version numbers. Assign a distinct reviewed +backport version before publishing any package or binary. diff --git a/crates/walletkit-db/Cargo.toml b/crates/walletkit-db/Cargo.toml index cbd29be48..b55d70f05 100644 --- a/crates/walletkit-db/Cargo.toml +++ b/crates/walletkit-db/Cargo.toml @@ -41,7 +41,3 @@ tempfile = { workspace = true } [lints] workspace = true - -[[example]] -name = "native_link_probe" -crate-type = ["staticlib"] diff --git a/crates/walletkit-db/build.rs b/crates/walletkit-db/build.rs index 8a3a6c0b1..30cb517d7 100644 --- a/crates/walletkit-db/build.rs +++ b/crates/walletkit-db/build.rs @@ -24,7 +24,6 @@ const EXPECTED_SHA256: &str = fn main() { println!("cargo:rerun-if-env-changed=DOCS_RS"); - println!("cargo:rerun-if-changed=src/native_sqlite.c"); if std::env::var_os("DOCS_RS").is_some() { return; @@ -60,7 +59,7 @@ fn build_sqlite3mc() { ); } - compile(&source_dir); + compile(&amalgamation_c, &source_dir); } fn download(dest: &Path) { @@ -105,12 +104,12 @@ fn extract(zip_path: &Path, dest_dir: &Path) { } } -fn compile(include_dir: &Path) { +fn compile(amalgamation_c: &Path, include_dir: &Path) { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); let mut build = cc::Build::new(); build - .file("src/native_sqlite.c") + .file(amalgamation_c) .include(include_dir) // Core SQLite configuration .define("SQLITE_CORE", None) diff --git a/crates/walletkit-db/examples/native_link_host.c b/crates/walletkit-db/examples/native_link_host.c deleted file mode 100644 index d13326e78..000000000 --- a/crates/walletkit-db/examples/native_link_host.c +++ /dev/null @@ -1,33 +0,0 @@ -#include -#include - -extern int walletkit_native_link_probe(void); - -int main(void) { - sqlite3 *db = NULL; - sqlite3_stmt *statement = NULL; - if (sqlite3_open(":memory:", &db) != SQLITE_OK) { - fprintf(stderr, "Host SQLite open failed\n"); - return 1; - } - if (sqlite3_prepare_v2(db, "PRAGMA cipher", -1, &statement, NULL) != SQLITE_OK || - sqlite3_step(statement) != SQLITE_DONE) { - fprintf(stderr, "WalletKit replaced the host's SQLite engine\n"); - sqlite3_finalize(statement); - sqlite3_close(db); - return 1; - } - sqlite3_finalize(statement); - printf("Host SQLite: %s; cipher unavailable as expected\n", sqlite3_libversion()); - int result = walletkit_native_link_probe(); - if (sqlite3_exec(db, "CREATE TABLE host (id INTEGER); INSERT INTO host VALUES (1);", - NULL, NULL, NULL) != SQLITE_OK || sqlite3_changes(db) != 1) { - fprintf(stderr, "Host SQLite failed after WalletKit used its own engine\n"); - result = 1; - } - if (sqlite3_close(db) != SQLITE_OK) { - fprintf(stderr, "Host SQLite close failed\n"); - result = 1; - } - return result; -} diff --git a/crates/walletkit-db/examples/native_link_probe.rs b/crates/walletkit-db/examples/native_link_probe.rs deleted file mode 100644 index 3358b0f83..000000000 --- a/crates/walletkit-db/examples/native_link_probe.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Native link-order regression probe using disposable, synthetic database data. - -use secrecy::SecretBox; -use walletkit_db::{cipher, Connection}; - -/// Exercises encrypted storage in a host that also links an unrelated `SQLite`. -/// Returns zero on success; reports an actionable error and returns one otherwise. -#[no_mangle] -pub extern "C" fn walletkit_native_link_probe() -> i32 { - match probe() { - Ok(()) => 0, - Err(error) => { - eprintln!("WalletKit SQLite link regression: {error}"); - 1 - } - } -} - -fn probe() -> Result<(), Box> { - let dir = tempfile::tempdir()?; - let path = dir.path().join("encrypted.sqlite"); - let key = SecretBox::init_with(|| [0xAB; 32]); - - { - let conn = cipher::open_encrypted(&path, &key, false)?; - let cipher = - conn.query_row("PRAGMA cipher", &[], |row| Ok(row.column_text(0)))?; - if cipher != "chacha20" { - return Err("WalletKit did not select its chacha20 cipher".into()); - } - conn.execute_batch("CREATE TABLE probe (value TEXT); INSERT INTO probe VALUES ('synthetic-test-record');")?; - } - - let original_bytes = std::fs::read(&path)?; - if original_bytes.starts_with(b"SQLite format 3\0") { - return Err("encrypted database has a plaintext SQLite header".into()); - } - let wrong_key = SecretBox::init_with(|| [0xCD; 32]); - if cipher::open_encrypted(&path, &wrong_key, false).is_ok() { - return Err("encrypted database accepted the wrong key".into()); - } - if std::fs::read(&path)? != original_bytes { - return Err("failed wrong-key open modified the database".into()); - } - { - let conn = cipher::open_encrypted(&path, &key, false)?; - let value = conn - .query_row("SELECT value FROM probe", &[], |row| Ok(row.column_text(0)))?; - if value != "synthetic-test-record" { - return Err("correct-key reopen did not preserve the record".into()); - } - } - - // Do not silently reset or reinterpret any pre-existing plaintext store. - let plaintext_path = dir.path().join("plaintext.sqlite"); - Connection::open(&plaintext_path, false)? - .execute_batch("CREATE TABLE existing (value TEXT); INSERT INTO existing VALUES ('preserve-me');")?; - let plaintext_bytes = std::fs::read(&plaintext_path)?; - if cipher::open_encrypted(&plaintext_path, &key, false).is_ok() { - return Err("plaintext store was silently accepted as encrypted".into()); - } - if std::fs::read(&plaintext_path)? != plaintext_bytes { - return Err("failed plaintext-store open modified existing data".into()); - } - println!( - "PASS: encrypted reopen, wrong-key rejection, and existing-data preservation" - ); - Ok(()) -} diff --git a/crates/walletkit-db/examples/test_native_linking.sh b/crates/walletkit-db/examples/test_native_linking.sh deleted file mode 100644 index dfb293949..000000000 --- a/crates/walletkit-db/examples/test_native_linking.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -# Run on macOS: exercise the same static-library/system-SQLite collision as iOS. -set -euo pipefail - -if [[ "$(uname -s)" != Darwin ]]; then - echo "This regression test requires the Apple linker and system SQLite." >&2 - exit 1 -fi - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" -cd "$repo_root" -profile="${1:-dev}" -case "$profile" in - dev) profile_dir=debug ;; - release) profile_dir=release ;; - *) echo "Usage: bash $0 [dev|release]" >&2; exit 1 ;; -esac -cargo build --locked -p walletkit-db --example native_link_probe --profile "$profile" - -target_dir="${CARGO_TARGET_DIR:-target}" -output_dir="$target_dir/native-link-probe/$profile_dir" -archive="$target_dir/$profile_dir/examples/libnative_link_probe.a" -host_source="crates/walletkit-db/examples/native_link_host.c" -mkdir -p "$output_dir" - -# Static archive visibility matters: a hidden global symbol can still collide -# during the final app link. The standard SQLite API must have local linkage. -nm -gU "$archive" > "$output_dir/symbols.txt" 2> "$output_dir/nm.log" -if grep -E ' [A-Za-z] _sqlite3_' "$output_dir/symbols.txt"; then - echo "WalletKit still exposes or references unprefixed SQLite API symbols." >&2 - exit 1 -fi - -clang "$host_source" -Wl,-dead_strip -lsqlite3 "$archive" \ - -framework Security -framework CoreFoundation -o "$output_dir/system-first" -"$output_dir/system-first" - -clang "$host_source" -Wl,-dead_strip "$archive" -lsqlite3 \ - -framework Security -framework CoreFoundation -o "$output_dir/walletkit-first" -"$output_dir/walletkit-first" - -echo "PASS: both link orders preserve separate host and WalletKit SQLite engines" diff --git a/crates/walletkit-db/src/native_sqlite.c b/crates/walletkit-db/src/native_sqlite.c deleted file mode 100644 index 5dab447f1..000000000 --- a/crates/walletkit-db/src/native_sqlite.c +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Keep SQLite's C API local to this translation unit. Visibility attributes alone - * do not prevent a static-library consumer from resolving sqlite3_* against a - * different SQLite implementation. Only the WalletKit-prefixed wrappers below - * cross the Rust FFI boundary; every handle stays with its creating engine. - * - * Compile the unchanged, checksum-verified amalgamation with the existing - * cipher/settings. This changes linkage only, not encryption or disk formats. - */ -#define SQLITE_API static -#define SQLITE_EXTERN -#include "sqlite3mc_amalgamation.c" - -int walletkit_sqlite3_open_v2(const char *filename, sqlite3 **db, int flags, const char *vfs) { - return sqlite3_open_v2(filename, db, flags, vfs); -} - -int walletkit_sqlite3_close_v2(sqlite3 *db) { - return sqlite3_close_v2(db); -} - -int walletkit_sqlite3_exec(sqlite3 *db, const char *sql, int (*callback)(void *, int, char **, char **), void *arg, char **error) { - return sqlite3_exec(db, sql, callback, arg, error); -} - -void walletkit_sqlite3_free(void *pointer) { - sqlite3_free(pointer); -} - -int walletkit_sqlite3_prepare_v2(sqlite3 *db, const char *sql, int length, sqlite3_stmt **statement, const char **tail) { - return sqlite3_prepare_v2(db, sql, length, statement, tail); -} - -int walletkit_sqlite3_step(sqlite3_stmt *statement) { - return sqlite3_step(statement); -} - -int walletkit_sqlite3_reset(sqlite3_stmt *statement) { - return sqlite3_reset(statement); -} - -int walletkit_sqlite3_finalize(sqlite3_stmt *statement) { - return sqlite3_finalize(statement); -} - -int walletkit_sqlite3_bind_int64(sqlite3_stmt *statement, int index, sqlite3_int64 value) { - return sqlite3_bind_int64(statement, index, value); -} - -int walletkit_sqlite3_bind_blob(sqlite3_stmt *statement, int index, const void *value, int length, sqlite3_destructor_type destructor) { - return sqlite3_bind_blob(statement, index, value, length, destructor); -} - -int walletkit_sqlite3_bind_text(sqlite3_stmt *statement, int index, const char *value, int length, sqlite3_destructor_type destructor) { - return sqlite3_bind_text(statement, index, value, length, destructor); -} - -int walletkit_sqlite3_bind_null(sqlite3_stmt *statement, int index) { - return sqlite3_bind_null(statement, index); -} - -sqlite3_int64 walletkit_sqlite3_column_int64(sqlite3_stmt *statement, int column) { - return sqlite3_column_int64(statement, column); -} - -const void * walletkit_sqlite3_column_blob(sqlite3_stmt *statement, int column) { - return sqlite3_column_blob(statement, column); -} - -int walletkit_sqlite3_column_bytes(sqlite3_stmt *statement, int column) { - return sqlite3_column_bytes(statement, column); -} - -const unsigned char * walletkit_sqlite3_column_text(sqlite3_stmt *statement, int column) { - return sqlite3_column_text(statement, column); -} - -int walletkit_sqlite3_column_type(sqlite3_stmt *statement, int column) { - return sqlite3_column_type(statement, column); -} - -int walletkit_sqlite3_column_count(sqlite3_stmt *statement) { - return sqlite3_column_count(statement); -} - -const char * walletkit_sqlite3_errmsg(sqlite3 *db) { - return sqlite3_errmsg(db); -} - -int walletkit_sqlite3_changes(sqlite3 *db) { - return sqlite3_changes(db); -} - -sqlite3_int64 walletkit_sqlite3_last_insert_rowid(sqlite3 *db) { - return sqlite3_last_insert_rowid(db); -} diff --git a/crates/walletkit-db/src/sqlite/cipher.rs b/crates/walletkit-db/src/sqlite/cipher.rs index 2a1204412..98c796988 100644 --- a/crates/walletkit-db/src/sqlite/cipher.rs +++ b/crates/walletkit-db/src/sqlite/cipher.rs @@ -55,20 +55,6 @@ pub fn open_encrypted( read_only: bool, ) -> DbResult { let conn = Connection::open(path, read_only)?; - // A host application can link another SQLite implementation. Fail before - // applying a key or writing a schema if the encrypted engine is unavailable. - conn.execute_batch("PRAGMA cipher = 'chacha20';")?; - let cipher = - conn.query_row_optional("PRAGMA cipher;", &[], |row| Ok(row.column_text(0)))?; - if !cipher - .as_deref() - .is_some_and(|name| name.eq_ignore_ascii_case("chacha20")) - { - return Err(Error::new( - -1, - "required sqlite3mc chacha20 cipher is unavailable; verify WalletKit SQLite symbol isolation", - )); - } apply_key(&conn, k_intermediate)?; configure_connection(&conn)?; Ok(conn) diff --git a/crates/walletkit-db/src/sqlite/ffi.rs b/crates/walletkit-db/src/sqlite/ffi.rs index 7eff63287..d1fce0b1f 100644 --- a/crates/walletkit-db/src/sqlite/ffi.rs +++ b/crates/walletkit-db/src/sqlite/ffi.rs @@ -436,16 +436,13 @@ mod raw { type sqlite3_stmt = c_void; extern "C" { - #[link_name = "walletkit_sqlite3_open_v2"] pub fn sqlite3_open_v2( filename: *const c_char, pp_db: *mut *mut sqlite3, flags: c_int, z_vfs: *const c_char, ) -> c_int; - #[link_name = "walletkit_sqlite3_close_v2"] pub fn sqlite3_close_v2(db: *mut sqlite3) -> c_int; - #[link_name = "walletkit_sqlite3_exec"] pub fn sqlite3_exec( db: *mut sqlite3, sql: *const c_char, @@ -453,9 +450,7 @@ mod raw { arg: *mut c_void, errmsg: *mut *mut c_char, ) -> c_int; - #[link_name = "walletkit_sqlite3_free"] pub fn sqlite3_free(ptr: *mut c_void); - #[link_name = "walletkit_sqlite3_prepare_v2"] pub fn sqlite3_prepare_v2( db: *mut sqlite3, z_sql: *const c_char, @@ -463,19 +458,14 @@ mod raw { pp_stmt: *mut *mut sqlite3_stmt, pz_tail: *mut *const c_char, ) -> c_int; - #[link_name = "walletkit_sqlite3_step"] pub fn sqlite3_step(stmt: *mut sqlite3_stmt) -> c_int; - #[link_name = "walletkit_sqlite3_reset"] pub fn sqlite3_reset(stmt: *mut sqlite3_stmt) -> c_int; - #[link_name = "walletkit_sqlite3_finalize"] pub fn sqlite3_finalize(stmt: *mut sqlite3_stmt) -> c_int; - #[link_name = "walletkit_sqlite3_bind_int64"] pub fn sqlite3_bind_int64( stmt: *mut sqlite3_stmt, index: c_int, value: i64, ) -> c_int; - #[link_name = "walletkit_sqlite3_bind_blob"] pub fn sqlite3_bind_blob( stmt: *mut sqlite3_stmt, index: c_int, @@ -483,7 +473,6 @@ mod raw { n: c_int, destructor: isize, ) -> c_int; - #[link_name = "walletkit_sqlite3_bind_text"] pub fn sqlite3_bind_text( stmt: *mut sqlite3_stmt, index: c_int, @@ -491,31 +480,21 @@ mod raw { n: c_int, destructor: isize, ) -> c_int; - #[link_name = "walletkit_sqlite3_bind_null"] pub fn sqlite3_bind_null(stmt: *mut sqlite3_stmt, index: c_int) -> c_int; - #[link_name = "walletkit_sqlite3_column_int64"] pub fn sqlite3_column_int64(stmt: *mut sqlite3_stmt, i_col: c_int) -> i64; - #[link_name = "walletkit_sqlite3_column_blob"] pub fn sqlite3_column_blob( stmt: *mut sqlite3_stmt, i_col: c_int, ) -> *const c_void; - #[link_name = "walletkit_sqlite3_column_bytes"] pub fn sqlite3_column_bytes(stmt: *mut sqlite3_stmt, i_col: c_int) -> c_int; - #[link_name = "walletkit_sqlite3_column_text"] pub fn sqlite3_column_text( stmt: *mut sqlite3_stmt, i_col: c_int, ) -> *const c_char; - #[link_name = "walletkit_sqlite3_column_type"] pub fn sqlite3_column_type(stmt: *mut sqlite3_stmt, i_col: c_int) -> c_int; - #[link_name = "walletkit_sqlite3_column_count"] pub fn sqlite3_column_count(stmt: *mut sqlite3_stmt) -> c_int; - #[link_name = "walletkit_sqlite3_errmsg"] pub fn sqlite3_errmsg(db: *mut sqlite3) -> *const c_char; - #[link_name = "walletkit_sqlite3_changes"] pub fn sqlite3_changes(db: *mut sqlite3) -> c_int; - #[link_name = "walletkit_sqlite3_last_insert_rowid"] pub fn sqlite3_last_insert_rowid(db: *mut sqlite3) -> i64; } }