112 lines
3.7 KiB
Rust
Raw Normal View History

2026-02-27 12:53:13 +08:00
//! Chat-specific storage implementation.
use storage::{RusqliteError, SqliteDb, StorageConfig, StorageError, params};
use zeroize::Zeroize;
2026-02-27 12:53:13 +08:00
2026-02-27 14:23:13 +08:00
use super::migrations;
use super::types::IdentityRecord;
2026-02-27 12:53:13 +08:00
use crate::identity::Identity;
/// Chat-specific storage operations.
///
/// This struct wraps a SqliteDb and provides domain-specific
/// storage operations for chat state (identity, inbox keys, chat metadata).
///
/// Note: Ratchet state persistence is delegated to double_ratchets::RatchetStorage.
pub struct ChatStorage {
db: SqliteDb,
}
impl ChatStorage {
/// Creates a new ChatStorage with the given configuration.
pub fn new(config: StorageConfig) -> Result<Self, StorageError> {
let db = SqliteDb::new(config)?;
2026-02-27 14:23:13 +08:00
Self::run_migrations(db)
2026-02-27 12:53:13 +08:00
}
2026-02-27 14:23:13 +08:00
/// Applies all migrations and returns the storage instance.
2026-03-02 11:42:21 +08:00
fn run_migrations(mut db: SqliteDb) -> Result<Self, StorageError> {
migrations::apply_migrations(db.connection_mut())?;
2026-02-27 12:53:13 +08:00
Ok(Self { db })
}
// ==================== Identity Operations ====================
/// Saves the identity (secret key).
///
/// Note: The secret key bytes are explicitly zeroized after use to minimize
/// the time sensitive data remains in stack memory.
2026-02-27 12:53:13 +08:00
pub fn save_identity(&mut self, identity: &Identity) -> Result<(), StorageError> {
let mut secret_bytes = identity.secret().DANGER_to_bytes();
let result = self.db.connection().execute(
2026-02-27 14:09:18 +08:00
"INSERT OR REPLACE INTO identity (id, name, secret_key) VALUES (1, ?1, ?2)",
params![identity.get_name(), secret_bytes.as_slice()],
);
secret_bytes.zeroize();
result?;
2026-02-27 12:53:13 +08:00
Ok(())
}
/// Loads the identity if it exists.
///
/// Note: Secret key bytes are zeroized after being copied into IdentityRecord,
/// which handles its own zeroization via ZeroizeOnDrop.
2026-02-27 12:53:13 +08:00
pub fn load_identity(&self) -> Result<Option<Identity>, StorageError> {
let mut stmt = self
.db
.connection()
2026-02-27 14:09:18 +08:00
.prepare("SELECT name, secret_key FROM identity WHERE id = 1")?;
2026-02-27 12:53:13 +08:00
let result = stmt.query_row([], |row| {
2026-02-27 14:09:18 +08:00
let name: String = row.get(0)?;
let secret_key: Vec<u8> = row.get(1)?;
Ok((name, secret_key))
2026-02-27 12:53:13 +08:00
});
match result {
Ok((name, mut secret_key_vec)) => {
let bytes: Result<[u8; 32], _> = secret_key_vec.as_slice().try_into();
let bytes = match bytes {
Ok(b) => b,
Err(_) => {
secret_key_vec.zeroize();
return Err(StorageError::InvalidData(
"Invalid secret key length".into(),
));
}
};
secret_key_vec.zeroize();
2026-02-27 14:09:18 +08:00
let record = IdentityRecord {
name,
secret_key: bytes,
};
2026-02-27 12:53:13 +08:00
Ok(Some(Identity::from(record)))
}
Err(RusqliteError::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identity_roundtrip() {
let mut storage = ChatStorage::new(StorageConfig::InMemory).unwrap();
// Initially no identity
assert!(storage.load_identity().unwrap().is_none());
// Save identity
2026-02-27 14:09:18 +08:00
let identity = Identity::new("default");
let pubkey = identity.public_key();
2026-02-27 12:53:13 +08:00
storage.save_identity(&identity).unwrap();
// Load identity
let loaded = storage.load_identity().unwrap().unwrap();
2026-02-27 14:09:18 +08:00
assert_eq!(loaded.public_key(), pubkey);
2026-02-27 12:53:13 +08:00
}
}