use handle_trait::Handle;
use super::{
User,
crypto::{current_timestamp, derive_encryption_key, encrypt_private_key, hash_password},
errors::UserError,
key_manager::UserKeyManager,
types::{KeyEncryption, UserInfo, UserKey, UserStatus},
};
use crate::{
Database, Instance, Result,
auth::{
crypto::generate_keypair,
settings::AuthSettings,
types::{AuthKey, Permission},
},
constants::{DATABASES, INSTANCE, USERS},
crdt::Doc,
store::Table,
};
pub fn create_instance_database(
instance: &Instance,
device_signing_key: &ed25519_dalek::SigningKey,
device_pubkey: &str,
) -> Result<Database> {
let mut settings = Doc::new();
settings.set_string("name", INSTANCE);
settings.set_string("type", "system");
settings.set_string("description", "Instance configuration and management");
let mut auth_settings = AuthSettings::new();
auth_settings.add_key(
"_device_key",
AuthKey::active(device_pubkey, Permission::Admin(0))?,
)?;
settings.set_doc("auth", auth_settings.as_doc().clone());
let database = Database::create(
settings,
instance,
device_signing_key.clone(),
"_device_key".to_string(),
)?;
Ok(database)
}
pub fn create_users_database(
instance: &Instance,
device_signing_key: &ed25519_dalek::SigningKey,
device_pubkey: &str,
) -> Result<Database> {
let mut settings = Doc::new();
settings.set_string("name", USERS);
settings.set_string("type", "system");
settings.set_string("description", "User directory database");
let mut auth_settings = AuthSettings::new();
auth_settings.add_key(
"_device_key",
AuthKey::active(device_pubkey, Permission::Admin(0))?,
)?;
settings.set_doc("auth", auth_settings.as_doc().clone());
let database = Database::create(
settings,
instance,
device_signing_key.clone(),
"_device_key".to_string(),
)?;
Ok(database)
}
pub fn create_databases_tracking(
instance: &Instance,
device_signing_key: &ed25519_dalek::SigningKey,
device_pubkey: &str,
) -> Result<Database> {
let mut settings = Doc::new();
settings.set_string("name", DATABASES);
settings.set_string("type", "system");
settings.set_string("description", "Database tracking and registry");
let mut auth_settings = AuthSettings::new();
auth_settings.add_key(
"_device_key",
AuthKey::active(device_pubkey, Permission::Admin(0))?,
)?;
settings.set_doc("auth", auth_settings.as_doc().clone());
let database = Database::create(
settings,
instance,
device_signing_key.clone(),
"_device_key".to_string(),
)?;
Ok(database)
}
pub fn create_user(
users_db: &Database,
instance: &Instance,
username: impl AsRef<str>,
password: Option<&str>,
) -> Result<(String, UserInfo)> {
let username = username.as_ref();
let users_table = users_db.get_store_viewer::<Table<UserInfo>>("users")?;
let existing = users_table.search(|u| u.username == username)?;
if !existing.is_empty() {
return Err(UserError::UsernameAlreadyExists {
username: username.to_string(),
}
.into());
}
let (password_hash, password_salt) = match password {
Some(pwd) => {
let (hash, salt) = hash_password(pwd)?;
(Some(hash), Some(salt))
}
None => (None, None),
};
let (user_private_key, user_public_key) = generate_keypair();
let user_public_key_str = crate::auth::crypto::format_public_key(&user_public_key);
let mut user_db_settings = Doc::new();
user_db_settings.set_string("name", format!("_user_{username}"));
user_db_settings.set_string("type", "user");
user_db_settings.set_string("description", format!("User database for {username}"));
let device_private_key = instance
.backend()
.get_private_key("_device_key")?
.ok_or_else(|| UserError::KeyNotFound {
key_id: "_device_key".to_string(),
})?;
let device_pubkey = device_private_key.verifying_key();
let device_pubkey_str = crate::auth::crypto::format_public_key(&device_pubkey);
let mut auth_settings = AuthSettings::new();
auth_settings.add_key(
"_device_key",
AuthKey::active(&device_pubkey_str, Permission::Admin(0))?,
)?;
auth_settings.add_key(
&user_public_key_str,
AuthKey::active(&user_public_key_str, Permission::Admin(0))?,
)?;
user_db_settings.set_doc("auth", auth_settings.as_doc().clone());
let user_database = Database::create(
user_db_settings,
instance,
device_private_key,
"_device_key".to_string(),
)?;
let user_database_id = user_database.root_id().clone();
let user_key = match (password, &password_salt) {
(Some(pwd), Some(salt)) => {
let encryption_key = derive_encryption_key(pwd, salt)?;
let (encrypted_key, nonce) = encrypt_private_key(&user_private_key, &encryption_key)?;
UserKey {
key_id: user_public_key_str.clone(),
private_key_bytes: encrypted_key,
encryption: KeyEncryption::Encrypted { nonce },
display_name: Some("Default Key".to_string()),
created_at: current_timestamp()?,
last_used: None,
is_default: true, database_sigkeys: std::collections::HashMap::new(),
}
}
_ => {
UserKey {
key_id: user_public_key_str.clone(),
private_key_bytes: user_private_key.to_bytes().to_vec(),
encryption: KeyEncryption::Unencrypted,
display_name: Some("Default Key".to_string()),
created_at: current_timestamp()?,
last_used: None,
is_default: true, database_sigkeys: std::collections::HashMap::new(),
}
}
};
let tx = user_database.new_transaction()?;
let keys_table = tx.get_store::<Table<UserKey>>("keys")?;
keys_table.insert(user_key)?;
tx.commit()?;
let user_info = UserInfo {
username: username.to_string(),
user_database_id,
password_hash,
password_salt,
created_at: current_timestamp()?,
status: UserStatus::Active,
};
let tx = users_db.new_transaction()?;
let users_table = tx.get_store::<Table<UserInfo>>("users")?;
let user_uuid = users_table.insert(user_info.clone())?; tx.commit()?;
Ok((user_uuid, user_info))
}
pub fn login_user(
users_db: &Database,
instance: &Instance,
username: impl AsRef<str>,
password: Option<&str>,
) -> Result<super::User> {
let username = username.as_ref();
let users_table = users_db.get_store_viewer::<Table<UserInfo>>("users")?;
let results = users_table.search(|u| u.username == username)?;
let (user_uuid, user_info) = match results.len() {
0 => {
return Err(UserError::UserNotFound {
username: username.to_string(),
}
.into());
}
1 => results.into_iter().next().unwrap(),
count => {
return Err(UserError::DuplicateUsersDetected {
username: username.to_string(),
count,
}
.into());
}
};
if user_info.status != UserStatus::Active {
return Err(UserError::UserDisabled {
username: username.to_string(),
}
.into());
}
let is_passwordless = user_info.password_hash.is_none();
match (password, is_passwordless) {
(Some(pwd), false) => {
let password_hash = user_info.password_hash.as_ref().unwrap();
super::crypto::verify_password(pwd, password_hash)?;
}
(None, true) => {
}
(Some(_), true) => {
return Err(UserError::InvalidPassword.into());
}
(None, false) => {
return Err(UserError::PasswordRequired {
operation: "login for password-protected user".to_string(),
}
.into());
}
}
let temp_user_database = Database::open_readonly(user_info.user_database_id.clone(), instance)?;
let keys_table = temp_user_database.get_store_viewer::<Table<UserKey>>("keys")?;
let keys: Vec<UserKey> = keys_table
.search(|_| true)? .into_iter()
.map(|(_, key)| key)
.collect();
let key_manager = if let Some(pwd) = password {
let password_salt =
user_info
.password_salt
.as_ref()
.ok_or_else(|| UserError::PasswordRequired {
operation: "decrypt keys for password-protected user".to_string(),
})?;
UserKeyManager::new(pwd, password_salt, keys)?
} else {
UserKeyManager::new_passwordless(keys)?
};
let default_key_id = key_manager
.get_default_key_id()
.ok_or(UserError::NoKeysAvailable)?;
let default_signing_key = key_manager
.get_signing_key(&default_key_id)
.ok_or_else(|| UserError::KeyNotFound {
key_id: default_key_id.clone(),
})?
.clone();
let user_database = Database::open(
instance.handle(),
&user_info.user_database_id,
default_signing_key,
default_key_id,
)?;
let tx = users_db.new_transaction()?;
let last_login_table = tx.get_store::<Table<i64>>("last_login")?;
last_login_table.set(&user_uuid, current_timestamp()?)?;
tx.commit()?;
Ok(User::new(
user_uuid,
user_info,
user_database,
instance.handle(),
key_manager,
))
}
pub fn list_users(users_db: &Database) -> Result<Vec<String>> {
let users_table = users_db.get_store_viewer::<Table<UserInfo>>("users")?;
let users: Vec<UserInfo> = users_table
.search(|_| true)? .into_iter()
.map(|(_, user)| user)
.collect();
Ok(users.into_iter().map(|u| u.username).collect())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Instance;
use crate::auth::crypto::{format_public_key, generate_keypair};
use crate::backend::BackendImpl;
use crate::backend::database::InMemory;
use crate::store::DocStore;
use crate::store::SettingsStore;
use std::sync::Arc;
fn setup_instance() -> (Instance, ed25519_dalek::SigningKey, String) {
let backend = Arc::new(InMemory::new());
let (device_key, device_pubkey) = generate_keypair();
let pubkey_str = format_public_key(&device_pubkey);
backend
.store_private_key("_device_key", device_key.clone())
.unwrap();
let instance = Instance::create_internal(backend).unwrap();
(instance, device_key, pubkey_str)
}
#[test]
fn test_create_instance_database() {
let (instance, device_key, pubkey_str) = setup_instance();
let instance_db = create_instance_database(&instance, &device_key, &pubkey_str).unwrap();
assert!(!instance_db.root_id().to_string().is_empty());
let transaction = instance_db.new_transaction().unwrap();
let doc_store = transaction.get_store::<DocStore>("_settings").unwrap();
let name = doc_store.get_string("name").unwrap();
assert_eq!(name, INSTANCE);
let settings_store = SettingsStore::new(&transaction).unwrap();
let auth_settings = settings_store.get_auth_settings().unwrap();
let device_key = auth_settings.get_key("_device_key").unwrap();
assert_eq!(device_key.permissions(), &Permission::Admin(0));
assert_eq!(device_key.pubkey(), &pubkey_str);
}
#[test]
fn test_create_users_database() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
assert!(!users_db.root_id().to_string().is_empty());
let transaction = users_db.new_transaction().unwrap();
let doc_store = transaction.get_store::<DocStore>("_settings").unwrap();
let name = doc_store.get_string("name").unwrap();
assert_eq!(name, USERS);
}
#[test]
fn test_create_databases_tracking() {
let (instance, device_key, pubkey_str) = setup_instance();
let databases_db = create_databases_tracking(&instance, &device_key, &pubkey_str).unwrap();
assert!(!databases_db.root_id().to_string().is_empty());
let transaction = databases_db.new_transaction().unwrap();
let doc_store = transaction.get_store::<DocStore>("_settings").unwrap();
let name = doc_store.get_string("name").unwrap();
assert_eq!(name, DATABASES);
}
#[test]
fn test_system_databases_have_device_key_auth() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
let transaction = users_db.new_transaction().unwrap();
let settings_store = SettingsStore::new(&transaction).unwrap();
let auth_settings = settings_store.get_auth_settings().unwrap();
let device_key = auth_settings.get_key("_device_key").unwrap();
assert_eq!(device_key.permissions(), &Permission::Admin(0));
assert_eq!(device_key.pubkey(), &pubkey_str);
}
#[test]
fn test_create_user() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
let (user_uuid, user_info) =
create_user(&users_db, &instance, "alice", Some("password123")).unwrap();
assert_eq!(user_info.username, "alice");
assert_eq!(user_info.status, UserStatus::Active);
assert!(user_info.password_hash.is_some());
assert!(user_info.password_salt.is_some());
assert!(!user_uuid.is_empty());
let users_table = users_db
.get_store_viewer::<Table<UserInfo>>("users")
.unwrap();
let stored_user = users_table.get(&user_uuid).unwrap();
assert_eq!(stored_user.username, "alice");
}
#[test]
fn test_create_user_passwordless() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
let (user_uuid, user_info) = create_user(&users_db, &instance, "bob", None).unwrap();
assert_eq!(user_info.username, "bob");
assert_eq!(user_info.status, UserStatus::Active);
assert!(user_info.password_hash.is_none());
assert!(user_info.password_salt.is_none());
assert!(!user_uuid.is_empty());
let users_table = users_db
.get_store_viewer::<Table<UserInfo>>("users")
.unwrap();
let stored_user = users_table.get(&user_uuid).unwrap();
assert_eq!(stored_user.username, "bob");
}
#[test]
fn test_create_duplicate_user() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
create_user(&users_db, &instance, "alice", Some("password123")).unwrap();
let result = create_user(&users_db, &instance, "alice", Some("password456"));
assert!(result.is_err());
}
#[test]
fn test_login_user() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
create_user(&users_db, &instance, "bob", Some("bobpassword")).unwrap();
let user = login_user(&users_db, &instance, "bob", Some("bobpassword")).unwrap();
assert_eq!(user.username(), "bob");
let last_login_table = users_db
.get_store_viewer::<Table<i64>>("last_login")
.unwrap();
let last_login = last_login_table.get(user.user_uuid()).unwrap();
assert!(last_login > 0);
}
#[test]
fn test_login_user_passwordless() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
create_user(&users_db, &instance, "charlie", None).unwrap();
let user = login_user(&users_db, &instance, "charlie", None).unwrap();
assert_eq!(user.username(), "charlie");
let last_login_table = users_db
.get_store_viewer::<Table<i64>>("last_login")
.unwrap();
let last_login = last_login_table.get(user.user_uuid()).unwrap();
assert!(last_login > 0);
}
#[test]
fn test_login_wrong_password() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
create_user(&users_db, &instance, "dave", Some("correct_password")).unwrap();
let result = login_user(&users_db, &instance, "dave", Some("wrong_password"));
assert!(result.is_err());
}
#[test]
fn test_login_password_mismatch() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
create_user(&users_db, &instance, "eve", None).unwrap();
let result = login_user(&users_db, &instance, "eve", Some("password"));
assert!(result.is_err());
create_user(&users_db, &instance, "frank", Some("password")).unwrap();
let result = login_user(&users_db, &instance, "frank", None);
assert!(result.is_err());
}
#[test]
fn test_login_nonexistent_user() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
let result = login_user(&users_db, &instance, "nonexistent", Some("password"));
assert!(result.is_err());
}
#[test]
fn test_list_users() {
let (instance, device_key, pubkey_str) = setup_instance();
let users_db = create_users_database(&instance, &device_key, &pubkey_str).unwrap();
let users = list_users(&users_db).unwrap();
assert_eq!(users.len(), 0);
create_user(&users_db, &instance, "alice", Some("pass1")).unwrap();
create_user(&users_db, &instance, "bob", None).unwrap();
create_user(&users_db, &instance, "charlie", Some("pass3")).unwrap();
let users = list_users(&users_db).unwrap();
assert_eq!(users.len(), 3);
assert!(users.contains(&"alice".into()));
assert!(users.contains(&"bob".into()));
assert!(users.contains(&"charlie".into()));
}
}