use crate::util::db; use rusqlite::{OptionalExtension, params}; use std::sync::{Arc, LazyLock, Mutex}; pub type StorageError = String; #[derive(Debug, Clone, PartialEq, Eq)] pub struct StoredChatSecret { pub user_id: String, pub chat_id: String, pub secret_id: String, pub version: i64, pub encrypted_secret: Vec, pub kem_ciphertext: Vec, pub wrapping_scheme: String, pub created_at: i64, pub updated_at: i64, } #[derive(Debug, Clone, Default)] pub struct ChatSecretQuery { pub user_id: String, pub chat_id: String, pub secret_id: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PendingChatSecretForward { pub recipient_user_id: String, pub chat_id: String, pub sender_user_id: String, pub secret_id: String, pub version: i64, pub encrypted_secret: Vec, pub kem_ciphertext: Vec, pub wrapping_scheme: String, pub created_at: i64, } static E2EE_DB: LazyLock>> = LazyLock::new(|| { db::create_shared_connection( "e2ee", r#" PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; DROP TABLE IF EXISTS encrypted_messages; DROP TABLE IF EXISTS encrypted_device_secrets; CREATE TABLE IF NOT EXISTS chat_secrets ( user_id TEXT NOT NULL, chat_id TEXT NOT NULL, secret_id TEXT NOT NULL, version INTEGER NOT NULL, encrypted_secret BLOB NOT NULL, kem_ciphertext BLOB NOT NULL, wrapping_scheme TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (user_id, chat_id, secret_id) ); CREATE INDEX IF NOT EXISTS idx_chat_secrets_owner ON chat_secrets (user_id, chat_id, secret_id); CREATE TABLE IF NOT EXISTS pending_chat_secret_forwards ( recipient_user_id TEXT NOT NULL, chat_id TEXT NOT NULL, sender_user_id TEXT NOT NULL, secret_id TEXT NOT NULL, version INTEGER NOT NULL, encrypted_secret BLOB NOT NULL, kem_ciphertext BLOB NOT NULL, wrapping_scheme TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (recipient_user_id, chat_id, secret_id) ); CREATE INDEX IF NOT EXISTS idx_pending_chat_secret_forwards_recipient ON pending_chat_secret_forwards (recipient_user_id, created_at); "#, ) .expect("Failed to create or initialize E2EE DB") }); pub fn put_chat_secret(record: StoredChatSecret) -> Result<(), StorageError> { db::with_conn(&E2EE_DB, |conn| { conn.execute( r#" INSERT INTO chat_secrets ( user_id, chat_id, secret_id, version, encrypted_secret, kem_ciphertext, wrapping_scheme, created_at, updated_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) ON CONFLICT(user_id, chat_id, secret_id) DO UPDATE SET version = excluded.version, encrypted_secret = excluded.encrypted_secret, kem_ciphertext = excluded.kem_ciphertext, wrapping_scheme = excluded.wrapping_scheme, created_at = excluded.created_at, updated_at = excluded.updated_at "#, params![ record.user_id, record.chat_id, record.secret_id, record.version, record.encrypted_secret, record.kem_ciphertext, record.wrapping_scheme, record.created_at, record.updated_at, ], )?; Ok(()) }) } pub fn put_pending_chat_secret_forward( record: PendingChatSecretForward, ) -> Result<(), StorageError> { db::with_conn(&E2EE_DB, |conn| { conn.execute( r#" INSERT INTO pending_chat_secret_forwards ( recipient_user_id, chat_id, sender_user_id, secret_id, version, encrypted_secret, kem_ciphertext, wrapping_scheme, created_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) ON CONFLICT(recipient_user_id, chat_id, secret_id) DO UPDATE SET sender_user_id = excluded.sender_user_id, version = excluded.version, encrypted_secret = excluded.encrypted_secret, kem_ciphertext = excluded.kem_ciphertext, wrapping_scheme = excluded.wrapping_scheme, created_at = excluded.created_at "#, params![ record.recipient_user_id, record.chat_id, record.sender_user_id, record.secret_id, record.version, record.encrypted_secret, record.kem_ciphertext, record.wrapping_scheme, record.created_at, ], )?; Ok(()) }) } pub fn get_pending_chat_secret_forwards( limit: i64, ) -> Result, StorageError> { db::with_conn(&E2EE_DB, |conn| { let mut stmt = conn.prepare( r#" SELECT recipient_user_id, chat_id, sender_user_id, secret_id, version, encrypted_secret, kem_ciphertext, wrapping_scheme, created_at FROM pending_chat_secret_forwards ORDER BY created_at ASC LIMIT ?1 "#, )?; let rows = stmt.query_map(params![limit.clamp(1, 500)], pending_forward_from_row)?; let mut out = Vec::new(); for row in rows { out.push(row?); } Ok(out) }) } pub fn delete_pending_chat_secret_forward( recipient_user_id: &str, chat_id: &str, secret_id: &str, ) -> Result<(), StorageError> { db::with_conn(&E2EE_DB, |conn| { conn.execute( r#" DELETE FROM pending_chat_secret_forwards WHERE recipient_user_id = ?1 AND chat_id = ?2 AND secret_id = ?3 "#, params![recipient_user_id, chat_id, secret_id], )?; Ok(()) }) } /// Erase every E2EE record owned by, or queued for, a user. The operation is /// intentionally idempotent so it can be retried after an interrupted remote /// erasure request. pub fn purge_user(user_id: i64) -> Result<(), StorageError> { let user_id = user_id.to_string(); db::with_conn(&E2EE_DB, |conn| { let tx = conn.unchecked_transaction()?; tx.execute("DELETE FROM chat_secrets WHERE user_id = ?1", params![user_id])?; tx.execute( "DELETE FROM pending_chat_secret_forwards WHERE recipient_user_id = ?1 OR sender_user_id = ?1", params![user_id], )?; tx.commit()?; Ok(()) }) } pub fn get_chat_secret(query: ChatSecretQuery) -> Result, StorageError> { if query.user_id.is_empty() || query.chat_id.is_empty() { return Ok(None); } db::with_conn(&E2EE_DB, |conn| { conn.query_row( r#" SELECT user_id, chat_id, secret_id, version, encrypted_secret, kem_ciphertext, wrapping_scheme, created_at, updated_at FROM chat_secrets WHERE user_id = ?1 AND chat_id = ?2 AND (?3 IS NULL OR secret_id = ?3) ORDER BY updated_at DESC LIMIT 1 "#, params![query.user_id, query.chat_id, query.secret_id], chat_secret_from_row, ) .optional() }) } fn chat_secret_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(StoredChatSecret { user_id: row.get(0)?, chat_id: row.get(1)?, secret_id: row.get(2)?, version: row.get(3)?, encrypted_secret: row.get(4)?, kem_ciphertext: row.get(5)?, wrapping_scheme: row.get(6)?, created_at: row.get(7)?, updated_at: row.get(8)?, }) } fn pending_forward_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { Ok(PendingChatSecretForward { recipient_user_id: row.get(0)?, chat_id: row.get(1)?, sender_user_id: row.get(2)?, secret_id: row.get(3)?, version: row.get(4)?, encrypted_secret: row.get(5)?, kem_ciphertext: row.get(6)?, wrapping_scheme: row.get(7)?, created_at: row.get(8)?, }) }