A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Added encryption at rest for the DBs

madclaws 4035eecb 120d1b2e

+76 -29
+11
Cargo.lock
··· 3744 3744 checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" 3745 3745 dependencies = [ 3746 3746 "cc", 3747 + "openssl-sys", 3747 3748 "pkg-config", 3748 3749 "vcpkg", 3749 3750 ] ··· 4630 4631 checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" 4631 4632 4632 4633 [[package]] 4634 + name = "openssl-src" 4635 + version = "300.5.5+3.5.5" 4636 + source = "registry+https://github.com/rust-lang/crates.io-index" 4637 + checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" 4638 + dependencies = [ 4639 + "cc", 4640 + ] 4641 + 4642 + [[package]] 4633 4643 name = "openssl-sys" 4634 4644 version = "0.9.112" 4635 4645 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4637 4647 dependencies = [ 4638 4648 "cc", 4639 4649 "libc", 4650 + "openssl-src", 4640 4651 "pkg-config", 4641 4652 "vcpkg", 4642 4653 ]
+18
tilekit/src/accounts.rs
··· 46 46 Ok(signing_key.to_bytes()) 47 47 } 48 48 49 + pub fn create_and_save_passkey(app: &str, key: &str) -> Result<String> { 50 + let rand_bytes = get_random_bytes_32(); 51 + let rand_hex: String = rand_bytes.iter().map(|b| format!("{:02x}", b)).collect(); 52 + let entry = Entry::new(app, key)?; 53 + entry.set_secret(rand_bytes.as_slice())?; 54 + Ok(rand_hex) 55 + } 56 + 57 + pub fn get_passkey(app: &str, key: &str) -> Result<String> { 58 + let entry = Entry::new(app, key)?; 59 + let secret = entry.get_secret()?; 60 + Ok(to_hex(secret.as_slice())) 61 + } 62 + 63 + fn to_hex(bytes: &[u8]) -> String { 64 + bytes.iter().map(|b| format!("{:02x}", b)).collect() 65 + } 66 + 49 67 pub fn get_public_key_from_did(did: &str) -> Result<[u8; 32]> { 50 68 let ed_did = Ed25519Did::from_str(did)?; 51 69 Ok(ed_did.0.to_bytes())
+1 -1
tiles/Cargo.toml
··· 17 17 rustyline = "17.0" 18 18 toml = "1.0.3" 19 19 semver = "1.0" 20 - rusqlite = { version = "0.38.0", features = ["bundled"] } 20 + rusqlite = { version = "0.38.0", features = ["bundled-sqlcipher-vendored-openssl"] } 21 21 rusqlite_migration = "2.4.1" 22 22 uuid = {version = "1.21.0", features = ["v7"]} 23 23 axum = "0.8.8"
+2 -2
tiles/src/commands/mod.rs
··· 339 339 } 340 340 341 341 pub fn show_peers() -> Result<()> { 342 - let db_conn = get_db_conn(core::storage::db::DBTYPE::COMMON)?; 342 + let db_conn = get_db_conn(&core::storage::db::DBTYPE::COMMON)?; 343 343 344 344 let peers = get_peer_list(&db_conn)?; 345 345 ··· 351 351 } 352 352 353 353 pub fn unlink_peer(user_id: &str) -> Result<()> { 354 - let db_conn = get_db_conn(core::storage::db::DBTYPE::COMMON)?; 354 + let db_conn = get_db_conn(&core::storage::db::DBTYPE::COMMON)?; 355 355 356 356 if let Err(err) = unlink(&db_conn, user_id) { 357 357 println!("{:?}", err)
+6 -14
tiles/src/core/accounts.rs
··· 13 13 14 14 use crate::{ 15 15 core::storage::db::{DBTYPE, get_db_conn}, 16 - utils::config::{get_or_create_config, save_config}, 16 + utils::config::{get_app_name, get_or_create_config, save_config}, 17 17 }; 18 18 const ROOT_USER_CONFIG_KEY: &str = "root-user"; 19 19 ··· 250 250 } 251 251 252 252 pub fn save_root_account_db() -> Result<()> { 253 - let conn = get_db_conn(DBTYPE::COMMON)?; 253 + let conn = get_db_conn(&DBTYPE::COMMON)?; 254 254 let config = get_or_create_config()?; 255 255 let root_user = get_root_user_details(&config)?; 256 256 let user = User { ··· 320 320 321 321 fn create_root_user(root_user_config: &Table, nickname: Option<String>) -> Result<Table> { 322 322 let mut root_user_table = root_user_config.clone(); 323 - let app_name = if cfg!(debug_assertions) { 324 - "tiles_dev" 325 - } else { 326 - "tiles" 327 - }; 328 - match create_identity(app_name) { 323 + let app_name = get_app_name(); 324 + match create_identity(&app_name) { 329 325 Ok(did) => { 330 326 root_user_table.insert("id".to_owned(), toml::Value::String(did)); 331 327 if let Some(nickname) = nickname { ··· 400 396 } 401 397 402 398 pub fn get_app_secret_key(did: &str) -> Result<SecretKey> { 403 - let app_name = if cfg!(debug_assertions) { 404 - "tiles_dev" 405 - } else { 406 - "tiles" 407 - }; 408 - let signing_key = get_secret_key(app_name, did)?; 399 + let app_name = get_app_name(); 400 + let signing_key = get_secret_key(&app_name, did)?; 409 401 Ok(SecretKey::from_bytes(&signing_key)) 410 402 } 411 403
+1 -1
tiles/src/core/chats.rs
··· 227 227 let (tx, mut rx) = mpsc::channel::<SyncOp>(32); 228 228 229 229 tokio::spawn(async move { 230 - let mut chat_db_conn = get_db_conn(super::storage::db::DBTYPE::CHAT)?; 230 + let mut chat_db_conn = get_db_conn(&super::storage::db::DBTYPE::CHAT)?; 231 231 info!("DB sync channel ready.."); 232 232 while let Some(msg) = rx.recv().await { 233 233 match msg {
+2 -2
tiles/src/core/network/mod.rs
··· 100 100 } 101 101 102 102 pub async fn link(ticket: Option<String>) -> Result<()> { 103 - let user_db_conn = get_db_conn(DBTYPE::COMMON)?; 103 + let user_db_conn = get_db_conn(&DBTYPE::COMMON)?; 104 104 let user = get_current_user(&user_db_conn)?; 105 105 let endpoint = create_endpoint(&user).await?; 106 106 let is_online = is_online(&endpoint).await; ··· 419 419 } 420 420 421 421 pub async fn sync(did: Option<String>) -> Result<()> { 422 - let user_db_conn = get_db_conn(DBTYPE::COMMON)?; 422 + let user_db_conn = get_db_conn(&DBTYPE::COMMON)?; 423 423 let user = get_current_user(&user_db_conn)?; 424 424 let endpoint = create_endpoint(&user).await?; 425 425 let is_online = is_online(&endpoint).await;
+25 -7
tiles/src/core/storage/db.rs
··· 6 6 use std::path::PathBuf; 7 7 8 8 use anyhow::{Result, anyhow}; 9 + use log::info; 9 10 use rusqlite::Connection; 11 + use tilekit::accounts::{create_and_save_passkey, get_passkey}; 10 12 11 - use crate::utils::config::{ConfigProvider, DefaultProvider}; 13 + use crate::utils::config::{ConfigProvider, DefaultProvider, get_app_name}; 12 14 use rusqlite_migration::{M, Migrations}; 15 + 16 + #[derive(Debug)] 13 17 pub enum DBTYPE { 14 18 COMMON, 15 19 CHAT, ··· 68 72 const CHATS_MIGRATIONS: Migrations = Migrations::from_slice(CHATS_MIGRATION_ARRAY); 69 73 70 74 pub fn init_db() -> Result<()> { 71 - let mut chat_conn = get_db_conn(DBTYPE::CHAT)?; 72 - let mut common_conn = get_db_conn(DBTYPE::COMMON)?; 75 + let mut chat_conn = get_db_conn(&DBTYPE::CHAT)?; 76 + let mut common_conn = get_db_conn(&DBTYPE::COMMON)?; 73 77 74 78 apply_migrations(&mut common_conn, &mut chat_conn) 75 79 } 76 80 77 - pub fn get_db_conn(db_type: DBTYPE) -> Result<Connection> { 81 + pub fn get_db_conn(db_type: &DBTYPE) -> Result<Connection> { 78 82 let db_path = get_db_path(db_type)?; 79 83 let conn = Connection::open(db_path) 80 84 .map_err(|e| anyhow!("Failed to create db connection due to {:?}", e))?; 81 85 86 + let passkey = fetch_passkey()?; 87 + let cipher_format = format!("x'{}'", passkey); 88 + conn.pragma_update(None, "KEY", cipher_format)?; 82 89 conn.pragma_update(None, "journal_mode", "WAL")?; 90 + info!("DB {:?} Opened", db_type); 83 91 Ok(conn) 84 92 } 85 93 ··· 89 97 .map_err(<rusqlite_migration::Error as Into<anyhow::Error>>::into)?; 90 98 CHATS_MIGRATIONS.to_latest(chat_conn).map_err(|e| e.into()) 91 99 } 92 - fn get_db_path(db_type: DBTYPE) -> Result<PathBuf> { 100 + fn get_db_path(db_type: &DBTYPE) -> Result<PathBuf> { 93 101 let user_data_dir = DefaultProvider.get_user_data_dir()?; 94 102 match db_type { 95 - DBTYPE::COMMON => Ok(user_data_dir.join("common.db")), 96 - DBTYPE::CHAT => Ok(user_data_dir.join("chats.db")), 103 + DBTYPE::COMMON => Ok(user_data_dir.join("common_v2.db")), 104 + DBTYPE::CHAT => Ok(user_data_dir.join("chats_v2.db")), 105 + } 106 + } 107 + 108 + fn fetch_passkey() -> Result<String> { 109 + let app_name = get_app_name(); 110 + if let Ok(passkey) = get_passkey(&app_name, "db_passkey") { 111 + Ok(passkey) 112 + } else { 113 + info!("DB passkey not found, creating one.."); 114 + create_and_save_passkey(&app_name, "db_passkey") 97 115 } 98 116 } 99 117
+2 -2
tiles/src/runtime/mlx.rs
··· 255 255 .ok_or_else(|| anyhow!("Error getting FROM from modelfile due to"))?; 256 256 257 257 println!("Running {} in interactive mode", modelname); 258 - let common_db_conn = get_db_conn(crate::core::storage::db::DBTYPE::COMMON)?; 259 - let chat_db_conn = get_db_conn(crate::core::storage::db::DBTYPE::CHAT)?; 258 + let common_db_conn = get_db_conn(&crate::core::storage::db::DBTYPE::COMMON)?; 259 + let chat_db_conn = get_db_conn(&crate::core::storage::db::DBTYPE::CHAT)?; 260 260 let current_user = get_current_user(&common_db_conn)?; 261 261 262 262 let config = Config::builder().auto_add_history(true).build();
+8
tiles/src/utils/config.rs
··· 298 298 Ok(model_dir) 299 299 } 300 300 301 + pub fn get_app_name() -> String { 302 + if cfg!(debug_assertions) { 303 + "tiles_dev".to_owned() 304 + } else { 305 + "tiles".to_owned() 306 + } 307 + } 308 + 301 309 //TODO: Add more tests for config.toml 302 310 #[cfg(test)] 303 311 mod tests {