use std::convert::TryFrom; use std::fmt; use std::time::Duration; use crate::unix_config::TpmPolicy; use kanidm_lib_crypto::CryptoPolicy; use kanidm_lib_crypto::DbPasswordV1; use kanidm_lib_crypto::Password; use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; use libc::umask; use rusqlite::Connection; use tokio::sync::{Mutex, MutexGuard}; use crate::cache::Id; pub struct Db { conn: Mutex, crypto_policy: CryptoPolicy, require_tpm: Option, } pub struct DbTxn<'a> { conn: MutexGuard<'a, Connection>, committed: bool, crypto_policy: &'a CryptoPolicy, require_tpm: Option<&'a tpm::TpmConfig>, } impl Db { pub fn new(path: &str, tpm_policy: &TpmPolicy) -> Result { let before = unsafe { umask(0o0027) }; let conn = Connection::open(path).map_err(|e| { error!(err = ?e, "rusqulite error"); })?; let _ = unsafe { umask(before) }; // We only build a single thread. If we need more than one, we'll // need to re-do this to account for path = "" for debug. let crypto_policy = CryptoPolicy::time_target(Duration::from_millis(250)); debug!("Configured {:?}", crypto_policy); // Test if we have a tpm context. let require_tpm = match tpm_policy { TpmPolicy::Ignore => None, TpmPolicy::IfPossible(tcti_str) => Db::tpm_setup_context(tcti_str, &conn).ok(), TpmPolicy::Required(tcti_str) => Some(Db::tpm_setup_context(tcti_str, &conn)?), }; Ok(Db { conn: Mutex::new(conn), crypto_policy, require_tpm, }) } #[allow(clippy::expect_used)] pub async fn write(&self) -> DbTxn<'_> { let conn = self.conn.lock().await; DbTxn::new(conn, &self.crypto_policy, self.require_tpm.as_ref()) } } impl fmt::Debug for Db { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Db {{}}") } } impl<'a> DbTxn<'a> { pub fn new( conn: MutexGuard<'a, Connection>, crypto_policy: &'a CryptoPolicy, require_tpm: Option<&'a tpm::TpmConfig>, ) -> Self { // Start the transaction // debug!("Starting db WR txn ..."); #[allow(clippy::expect_used)] conn.execute("BEGIN TRANSACTION", []) .expect("Unable to begin transaction!"); DbTxn { committed: false, conn, crypto_policy, require_tpm, } } /// This handles an error coming back from an sqlite event and dumps more information from it fn sqlite_error(&self, msg: &str, error: &rusqlite::Error) { error!( "sqlite {} error: {:?} db_path={:?}", msg, error, &self.conn.path() ); } /// This handles an error coming back from an sqlite transaction and dumps a load of information from it fn sqlite_transaction_error(&self, error: &rusqlite::Error, _stmt: &rusqlite::Statement) { error!( "sqlite transaction error={:?} db_path={:?}", error, &self.conn.path(), ); // TODO: one day figure out if there's an easy way to dump the transaction without the token... } pub fn migrate(&self) -> Result<(), ()> { self.conn.set_prepared_statement_cache_capacity(16); self.conn .prepare("PRAGMA journal_mode=WAL;") .and_then(|mut wal_stmt| wal_stmt.query([]).map(|_| ())) .map_err(|e| { self.sqlite_error("account_t create", &e); })?; // Setup two tables - one for accounts, one for groups. // correctly index the columns. // Optional pw hash field self.conn .execute( "CREATE TABLE IF NOT EXISTS account_t ( uuid TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, spn TEXT NOT NULL UNIQUE, gidnumber INTEGER NOT NULL UNIQUE, password BLOB, token BLOB NOT NULL, expiry NUMERIC NOT NULL ) ", [], ) .map_err(|e| { self.sqlite_error("account_t create", &e); })?; self.conn .execute( "CREATE TABLE IF NOT EXISTS group_t ( uuid TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, spn TEXT NOT NULL UNIQUE, gidnumber INTEGER NOT NULL UNIQUE, token BLOB NOT NULL, expiry NUMERIC NOT NULL ) ", [], ) .map_err(|e| { self.sqlite_error("group_t create", &e); })?; self.conn .execute( "CREATE TABLE IF NOT EXISTS memberof_t ( g_uuid TEXT, a_uuid TEXT, FOREIGN KEY(g_uuid) REFERENCES group_t(uuid) ON DELETE CASCADE, FOREIGN KEY(a_uuid) REFERENCES account_t(uuid) ON DELETE CASCADE ) ", [], ) .map_err(|e| { self.sqlite_error("memberof_t create error", &e); })?; Ok(()) } pub fn commit(mut self) -> Result<(), ()> { // debug!("Committing BE txn"); if self.committed { error!("Invalid state, SQL transaction was already committed!"); return Err(()); } self.committed = true; self.conn .execute("COMMIT TRANSACTION", []) .map(|_| ()) .map_err(|e| { self.sqlite_error("commit", &e); }) } pub fn invalidate(&self) -> Result<(), ()> { self.conn .execute("UPDATE group_t SET expiry = 0", []) .map_err(|e| { self.sqlite_error("update group_t", &e); })?; self.conn .execute("UPDATE account_t SET expiry = 0", []) .map_err(|e| { self.sqlite_error("update account_t", &e); })?; Ok(()) } pub fn clear_cache(&self) -> Result<(), ()> { self.conn.execute("DELETE FROM group_t", []).map_err(|e| { self.sqlite_error("delete group_t", &e); })?; self.conn .execute("DELETE FROM account_t", []) .map_err(|e| { self.sqlite_error("delete group_t", &e); })?; Ok(()) } fn get_account_data_name(&self, account_id: &str) -> Result, i64)>, ()> { let mut stmt = self.conn .prepare( "SELECT token, expiry FROM account_t WHERE uuid = :account_id OR name = :account_id OR spn = :account_id" ) .map_err(|e| { self.sqlite_error("select prepare", &e); })?; // Makes tuple (token, expiry) let data_iter = stmt .query_map([account_id], |row| Ok((row.get(0)?, row.get(1)?))) .map_err(|e| { self.sqlite_error("query_map failure", &e); })?; let data: Result, i64)>, _> = data_iter .map(|v| { v.map_err(|e| { self.sqlite_error("map failure", &e); }) }) .collect(); data } fn get_account_data_gid(&self, gid: u32) -> Result, i64)>, ()> { let mut stmt = self .conn .prepare("SELECT token, expiry FROM account_t WHERE gidnumber = :gid") .map_err(|e| { self.sqlite_error("select prepare", &e); })?; // Makes tuple (token, expiry) let data_iter = stmt .query_map(params![gid], |row| Ok((row.get(0)?, row.get(1)?))) .map_err(|e| { self.sqlite_error("query_map", &e); })?; let data: Result, i64)>, _> = data_iter .map(|v| { v.map_err(|e| { self.sqlite_error("map", &e); }) }) .collect(); data } pub fn get_account(&self, account_id: &Id) -> Result, ()> { let data = match account_id { Id::Name(n) => self.get_account_data_name(n.as_str()), Id::Gid(g) => self.get_account_data_gid(*g), }?; // Assert only one result? if data.len() >= 2 { error!("invalid db state, multiple entries matched query?"); return Err(()); } if let Some((token, expiry)) = data.first() { // token convert with json. // If this errors, we specifically return Ok(None) because that triggers // the cache to refetch the token. match serde_json::from_slice(token.as_slice()) { Ok(t) => { let e = u64::try_from(*expiry).map_err(|e| { error!("u64 convert error -> {:?}", e); })?; Ok(Some((t, e))) } Err(e) => { warn!("recoverable - json error -> {:?}", e); Ok(None) } } } else { Ok(None) } } pub fn get_accounts(&self) -> Result, ()> { let mut stmt = self .conn .prepare("SELECT token FROM account_t") .map_err(|e| { self.sqlite_error("select prepare", &e); })?; let data_iter = stmt.query_map([], |row| row.get(0)).map_err(|e| { self.sqlite_error("query_map", &e); })?; let data: Result>, _> = data_iter .map(|v| { v.map_err(|e| { self.sqlite_error("map", &e); }) }) .collect(); let data = data?; Ok(data .iter() // We filter map here so that anything invalid is skipped. .filter_map(|token| { // token convert with json. serde_json::from_slice(token.as_slice()) .map_err(|e| { warn!("get_accounts json error -> {:?}", e); }) .ok() }) .collect()) } pub fn update_account(&self, account: &UnixUserToken, expire: u64) -> Result<(), ()> { let data = serde_json::to_vec(account).map_err(|e| { error!("update_account json error -> {:?}", e); })?; let expire = i64::try_from(expire).map_err(|e| { error!("update_account i64 conversion error -> {:?}", e); })?; // This is needed because sqlites 'insert or replace into', will null the password field // if present, and upsert MUST match the exact conflicting column, so that means we have // to manually manage the update or insert :( :( // Find anything conflicting and purge it. self.conn.execute("DELETE FROM account_t WHERE NOT uuid = :uuid AND (name = :name OR spn = :spn OR gidnumber = :gidnumber)", named_params!{ ":uuid": &account.uuid, ":name": &account.name, ":spn": &account.spn, ":gidnumber": &account.gidnumber, } ) .map_err(|e| { self.sqlite_error("delete account_t duplicate", &e); }) .map(|_| ())?; let updated = self.conn.execute( "UPDATE account_t SET name=:name, spn=:spn, gidnumber=:gidnumber, token=:token, expiry=:expiry WHERE uuid = :uuid", named_params!{ ":uuid": &account.uuid, ":name": &account.name, ":spn": &account.spn, ":gidnumber": &account.gidnumber, ":token": &data, ":expiry": &expire, } ) .map_err(|e| { self.sqlite_error("delete account_t duplicate", &e); })?; if updated == 0 { let mut stmt = self.conn .prepare("INSERT INTO account_t (uuid, name, spn, gidnumber, token, expiry) VALUES (:uuid, :name, :spn, :gidnumber, :token, :expiry) ON CONFLICT(uuid) DO UPDATE SET name=excluded.name, spn=excluded.name, gidnumber=excluded.gidnumber, token=excluded.token, expiry=excluded.expiry") .map_err(|e| { self.sqlite_error("prepare", &e); })?; stmt.execute(named_params! { ":uuid": &account.uuid, ":name": &account.name, ":spn": &account.spn, ":gidnumber": &account.gidnumber, ":token": &data, ":expiry": &expire, }) .map(|r| { debug!("insert -> {:?}", r); }) .map_err(|error| { self.sqlite_transaction_error(&error, &stmt); })?; } // Now, we have to update the group memberships. // First remove everything that already exists: let mut stmt = self .conn .prepare("DELETE FROM memberof_t WHERE a_uuid = :a_uuid") .map_err(|e| { self.sqlite_error("prepare", &e); })?; stmt.execute([&account.uuid]) .map(|r| { debug!("delete memberships -> {:?}", r); }) .map_err(|error| { self.sqlite_transaction_error(&error, &stmt); })?; let mut stmt = self .conn .prepare("INSERT INTO memberof_t (a_uuid, g_uuid) VALUES (:a_uuid, :g_uuid)") .map_err(|e| { self.sqlite_error("prepare", &e); })?; // Now for each group, add the relation. account.groups.iter().try_for_each(|g| { stmt.execute(named_params! { ":a_uuid": &account.uuid, ":g_uuid": &g.uuid, }) .map(|r| { debug!("insert membership -> {:?}", r); }) .map_err(|error| { self.sqlite_transaction_error(&error, &stmt); }) }) } pub fn delete_account(&self, a_uuid: &str) -> Result<(), ()> { self.conn .execute( "DELETE FROM account_t WHERE uuid = :a_uuid", params![a_uuid], ) .map(|_| ()) .map_err(|e| { self.sqlite_error("memberof_t create", &e); }) } pub fn update_account_password(&self, a_uuid: &str, cred: &str) -> Result<(), ()> { #[allow(unused_variables)] let pw = if let Some(tcti_str) = self.require_tpm { // Do nothing. #[cfg(not(feature = "tpm"))] return Ok(()); #[cfg(feature = "tpm")] let pw = Db::tpm_new(self.crypto_policy, cred, tcti_str)?; #[cfg(feature = "tpm")] pw } else { Password::new(self.crypto_policy, cred).map_err(|e| { error!("password error -> {:?}", e); })? }; let dbpw = pw.to_dbpasswordv1(); let data = serde_json::to_vec(&dbpw).map_err(|e| { error!("json error -> {:?}", e); })?; self.conn .execute( "UPDATE account_t SET password = :data WHERE uuid = :a_uuid", named_params! { ":a_uuid": &a_uuid, ":data": &data, }, ) .map_err(|e| { self.sqlite_error("update account_t password", &e); }) .map(|_| ()) } pub fn check_account_password(&self, a_uuid: &str, cred: &str) -> Result { #[cfg(not(feature = "tpm"))] if self.require_tpm.is_some() { return Ok(false); } let mut stmt = self .conn .prepare("SELECT password FROM account_t WHERE uuid = :a_uuid AND password IS NOT NULL") .map_err(|e| { self.sqlite_error("select prepare", &e); })?; // Makes tuple (token, expiry) let data_iter = stmt.query_map([a_uuid], |row| row.get(0)).map_err(|e| { self.sqlite_error("query_map", &e); })?; let data: Result>, _> = data_iter .map(|v| { v.map_err(|e| { self.sqlite_error("map", &e); }) }) .collect(); let data = data?; if data.is_empty() { info!("No cached password, failing authentication"); return Ok(false); } if data.len() >= 2 { error!("invalid db state, multiple entries matched query?"); return Err(()); } let pw = data.first().map(|raw| { // Map the option from data.first. let dbpw: DbPasswordV1 = serde_json::from_slice(raw.as_slice()).map_err(|e| { error!("json error -> {:?}", e); })?; Password::try_from(dbpw) }); let pw = match pw { Some(Ok(p)) => p, _ => return Ok(false), }; #[allow(unused_variables)] if let Some(tcti_str) = self.require_tpm { #[cfg(feature = "tpm")] let r = Db::tpm_verify(pw, cred, tcti_str); // Do nothing. #[cfg(not(feature = "tpm"))] let r = Ok(false); r } else { pw.verify(cred).map_err(|e| { error!("password error -> {:?}", e); }) } } fn get_group_data_name(&self, grp_id: &str) -> Result, i64)>, ()> { let mut stmt = self.conn .prepare( "SELECT token, expiry FROM group_t WHERE uuid = :grp_id OR name = :grp_id OR spn = :grp_id" ) .map_err(|e| { self.sqlite_error("select prepare", &e); })?; // Makes tuple (token, expiry) let data_iter = stmt .query_map([grp_id], |row| Ok((row.get(0)?, row.get(1)?))) .map_err(|e| { self.sqlite_error("query_map", &e); })?; let data: Result, i64)>, _> = data_iter .map(|v| { v.map_err(|e| { self.sqlite_error("map", &e); }) }) .collect(); data } fn get_group_data_gid(&self, gid: u32) -> Result, i64)>, ()> { let mut stmt = self .conn .prepare("SELECT token, expiry FROM group_t WHERE gidnumber = :gid") .map_err(|e| { self.sqlite_error("select prepare", &e); })?; // Makes tuple (token, expiry) let data_iter = stmt .query_map(params![gid], |row| Ok((row.get(0)?, row.get(1)?))) .map_err(|e| { self.sqlite_error("query_map", &e); })?; let data: Result, i64)>, _> = data_iter .map(|v| { v.map_err(|e| { self.sqlite_error("map", &e); }) }) .collect(); data } pub fn get_group(&self, grp_id: &Id) -> Result, ()> { let data = match grp_id { Id::Name(n) => self.get_group_data_name(n.as_str()), Id::Gid(g) => self.get_group_data_gid(*g), }?; // Assert only one result? if data.len() >= 2 { error!("invalid db state, multiple entries matched query?"); return Err(()); } if let Some((token, expiry)) = data.first() { // token convert with json. // If this errors, we specifically return Ok(None) because that triggers // the cache to refetch the token. match serde_json::from_slice(token.as_slice()) { Ok(t) => { let e = u64::try_from(*expiry).map_err(|e| { error!("u64 convert error -> {:?}", e); })?; Ok(Some((t, e))) } Err(e) => { warn!("recoverable - json error -> {:?}", e); Ok(None) } } } else { Ok(None) } } pub fn get_group_members(&self, g_uuid: &str) -> Result, ()> { let mut stmt = self .conn .prepare("SELECT account_t.token FROM (account_t, memberof_t) WHERE account_t.uuid = memberof_t.a_uuid AND memberof_t.g_uuid = :g_uuid") .map_err(|e| { self.sqlite_error("select prepare", &e); })?; let data_iter = stmt.query_map([g_uuid], |row| row.get(0)).map_err(|e| { self.sqlite_error("query_map", &e); })?; let data: Result>, _> = data_iter .map(|v| { v.map_err(|e| { self.sqlite_error("map", &e); }) }) .collect(); let data = data?; data.iter() .map(|token| { // token convert with json. // debug!("{:?}", token); serde_json::from_slice(token.as_slice()).map_err(|e| { error!("json error -> {:?}", e); }) }) .collect() } pub fn get_groups(&self) -> Result, ()> { let mut stmt = self .conn .prepare("SELECT token FROM group_t") .map_err(|e| { self.sqlite_error("select prepare", &e); })?; let data_iter = stmt.query_map([], |row| row.get(0)).map_err(|e| { self.sqlite_error("query_map", &e); })?; let data: Result>, _> = data_iter .map(|v| { v.map_err(|e| { self.sqlite_error("map", &e); }) }) .collect(); let data = data?; Ok(data .iter() .filter_map(|token| { // token convert with json. // debug!("{:?}", token); serde_json::from_slice(token.as_slice()) .map_err(|e| { error!("json error -> {:?}", e); }) .ok() }) .collect()) } pub fn update_group(&self, grp: &UnixGroupToken, expire: u64) -> Result<(), ()> { let data = serde_json::to_vec(grp).map_err(|e| { error!("json error -> {:?}", e); })?; let expire = i64::try_from(expire).map_err(|e| { error!("i64 convert error -> {:?}", e); })?; let mut stmt = self.conn .prepare("INSERT OR REPLACE INTO group_t (uuid, name, spn, gidnumber, token, expiry) VALUES (:uuid, :name, :spn, :gidnumber, :token, :expiry)") .map_err(|e| { self.sqlite_error("prepare", &e); })?; stmt.execute(named_params! { ":uuid": &grp.uuid, ":name": &grp.name, ":spn": &grp.spn, ":gidnumber": &grp.gidnumber, ":token": &data, ":expiry": &expire, }) .map(|r| { debug!("insert -> {:?}", r); }) .map_err(|e| { self.sqlite_error("execute", &e); }) } pub fn delete_group(&self, g_uuid: &str) -> Result<(), ()> { self.conn .execute("DELETE FROM group_t WHERE uuid = :g_uuid", [g_uuid]) .map(|_| ()) .map_err(|e| { self.sqlite_error("memberof_t create", &e); }) } } impl<'a> fmt::Debug for DbTxn<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "DbTxn {{}}") } } impl<'a> Drop for DbTxn<'a> { // Abort fn drop(&mut self) { if !self.committed { // debug!("Aborting BE WR txn"); #[allow(clippy::expect_used)] self.conn .execute("ROLLBACK TRANSACTION", []) .expect("Unable to rollback transaction! Can not proceed!!!"); } } } #[cfg(not(feature = "tpm"))] pub(crate) mod tpm { use super::Db; use rusqlite::Connection; pub struct TpmConfig {} impl Db { pub fn tpm_setup_context(_tcti_str: &str, _conn: &Connection) -> Result { warn!("tpm feature is not available in this build"); Err(()) } } } #[cfg(feature = "tpm")] pub(crate) mod tpm { use super::Db; use rusqlite::{Connection, OptionalExtension}; use kanidm_lib_crypto::{CryptoError, CryptoPolicy, Password, TpmError}; use tss_esapi::{Context, TctiNameConf}; use tss_esapi::{ attributes::ObjectAttributesBuilder, handles::KeyHandle, interface_types::{ algorithm::{HashingAlgorithm, PublicAlgorithm}, resource_handles::Hierarchy, }, structures::{ Digest, KeyedHashScheme, Private, Public, PublicBuilder, PublicKeyedHashParameters, SymmetricCipherParameters, SymmetricDefinitionObject, }, traits::{Marshall, UnMarshall}, }; use std::str::FromStr; use base64urlsafedata::Base64UrlSafeData; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] struct BinaryLoadableKey { private: Base64UrlSafeData, public: Base64UrlSafeData, } impl Into for LoadableKey { fn into(self) -> BinaryLoadableKey { BinaryLoadableKey { private: self.private.as_slice().to_owned().into(), public: self.public.marshall().unwrap().into(), } } } impl TryFrom for LoadableKey { type Error = &'static str; fn try_from(value: BinaryLoadableKey) -> Result { let private = Private::try_from(value.private.0).map_err(|e| { error!(tpm_err = ?e, "Failed to restore tpm hmac key"); "private try from" })?; let public = Public::unmarshall(value.public.0.as_slice()).map_err(|e| { error!(tpm_err = ?e, "Failed to restore tpm hmac key"); "public unmarshall" })?; Ok(LoadableKey { public, private }) } } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(try_from = "BinaryLoadableKey", into = "BinaryLoadableKey")] struct LoadableKey { private: Private, public: Public, } pub struct TpmConfig { tcti: TctiNameConf, private: Private, public: Public, } // First, setup the primary key we will be using. Remember tpm Primary keys // are the same provided the *same* public parameters are used and the tpm // seed hasn't been reset. fn get_primary_key_public() -> Result { let object_attributes = ObjectAttributesBuilder::new() .with_fixed_tpm(true) .with_fixed_parent(true) .with_st_clear(false) .with_sensitive_data_origin(true) .with_user_with_auth(true) .with_decrypt(true) // .with_sign_encrypt(true) .with_restricted(true) .build() .map_err(|e| { error!(tpm_err = ?e, "Failed to create tpm primary key attributes"); })?; PublicBuilder::new() .with_public_algorithm(PublicAlgorithm::SymCipher) .with_name_hashing_algorithm(HashingAlgorithm::Sha256) .with_object_attributes(object_attributes) .with_symmetric_cipher_parameters(SymmetricCipherParameters::new( SymmetricDefinitionObject::AES_128_CFB, )) .with_symmetric_cipher_unique_identifier(Digest::default()) .build() .map_err(|e| { error!(tpm_err = ?e, "Failed to create tpm primary key public"); }) } fn new_hmac_public() -> Result { let object_attributes = ObjectAttributesBuilder::new() .with_fixed_tpm(true) .with_fixed_parent(true) .with_st_clear(false) .with_sensitive_data_origin(true) .with_user_with_auth(true) .with_sign_encrypt(true) .build() .map_err(|e| { error!(tpm_err = ?e, "Failed to create tpm hmac key attributes"); })?; PublicBuilder::new() .with_public_algorithm(PublicAlgorithm::KeyedHash) .with_name_hashing_algorithm(HashingAlgorithm::Sha256) .with_object_attributes(object_attributes) .with_keyed_hash_parameters(PublicKeyedHashParameters::new( KeyedHashScheme::HMAC_SHA_256, )) .with_keyed_hash_unique_identifier(Digest::default()) .build() .map_err(|e| { error!(tpm_err = ?e, "Failed to create tpm hmac key public"); }) } fn setup_keys(ctx: &mut Context, tpm_conf: &TpmConfig) -> Result { // Given our public and private parts, setup our keys for usage. let primary_pub = get_primary_key_public().map_err(|_| CryptoError::Tpm2PublicBuilder)?; trace!(?primary_pub); let primary_key = ctx .create_primary(Hierarchy::Owner, primary_pub, None, None, None, None) .map_err(|e| { error!(tpm_err = ?e, "Failed to load tpm context"); >::into(e) })?; /* let hmac_pub = new_hmac_public() .map_err(|_| CryptoError::Tpm2PublicBuilder ) ?; trace!(?hmac_pub); */ ctx.load( primary_key.key_handle.clone(), tpm_conf.private.clone(), tpm_conf.public.clone(), ) .map_err(|e| { error!(tpm_err = ?e, "Failed to load tpm context"); >::into(e) }) } impl Db { pub fn tpm_setup_context(tcti_str: &str, conn: &Connection) -> Result { let tcti = TctiNameConf::from_str(tcti_str).map_err(|e| { error!(tpm_err = ?e, "Failed to parse tcti name"); })?; let mut context = Context::new(tcti.clone()).map_err(|e| { error!(tpm_err = ?e, "Failed to create tpm context"); })?; // Create the primary object that will contain our key. let primary_pub = get_primary_key_public()?; let primary_key = context .execute_with_nullauth_session(|ctx| { ctx.create_primary(Hierarchy::Owner, primary_pub, None, None, None, None) }) .map_err(|e| { error!(tpm_err = ?e, "Failed to create tpm primary key"); })?; // Now we know we can establish a correct primary key, lets get any previously saved // hmac keys. conn.execute( "CREATE TABLE IF NOT EXISTS config_t ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) ", [], ) .map_err(|e| { error!(sqlite_err = ?e, "update config_t tpm_ctx"); })?; // Try and get the db context. let ctx_data: Option> = conn .query_row( "SELECT value FROM config_t WHERE key='tpm2_ctx'", [], |row| row.get(0), ) .optional() .map_err(|e| { error!(sqlite_err = ?e, "Failed to load tpm2_ctx"); }) .unwrap_or(None); trace!(ctx_data_present = %ctx_data.is_some()); let ex_private = if let Some(ctx_data) = ctx_data { // Test loading, blank it out if it fails. serde_json::from_slice(ctx_data.as_slice()) .map_err(|e| { warn!("json error -> {:?}", e); }) // On error, becomes none and we do nothing else. .ok() .and_then(|loadable: LoadableKey| { // test if it can load? context .execute_with_nullauth_session(|ctx| { ctx.load( primary_key.key_handle.clone(), loadable.private.clone(), loadable.public.clone(), ) // We have to flush the handle here to not overfill tpm context memory. // We'll be reloading the key later anyway. .and_then(|kh| ctx.flush_context(kh.into())) }) .map_err(|e| { error!(tpm_err = ?e, "Failed to load tpm hmac key"); }) .ok() // It loaded, so sub in our Private data. .map(|_hmac_handle| loadable) }) } else { None }; let loadable = if let Some(existing) = ex_private { existing } else { // Need to regenerate the private key for some reason info!("Creating new hmac key"); let hmac_pub = new_hmac_public()?; context .execute_with_nullauth_session(|ctx| { ctx.create( primary_key.key_handle.clone(), hmac_pub, None, None, None, None, ) .map(|key| LoadableKey { public: key.out_public, private: key.out_private, }) }) .map_err(|e| { error!(tpm_err = ?e, "Failed to create tpm hmac key"); })? }; // Serialise it out. let data = serde_json::to_vec(&loadable).map_err(|e| { error!("json error -> {:?}", e); })?; // Update the tpm ctx str conn.execute( "INSERT OR REPLACE INTO config_t (key, value) VALUES ('tpm2_ctx', :data)", named_params! { ":data": &data, }, ) .map_err(|e| { error!(sqlite_err = ?e, "update config_t tpm_ctx"); }) .map(|_| ())?; // info!("tpm binding configured"); let LoadableKey { private, public } = loadable; Ok(TpmConfig { tcti, private, public, }) } pub fn tpm_new( policy: &CryptoPolicy, cred: &str, tpm_conf: &TpmConfig, ) -> Result { let mut context = Context::new(tpm_conf.tcti.clone()).map_err(|e| { error!(tpm_err = ?e, "Failed to create tpm context"); })?; context .execute_with_nullauth_session(|ctx| { let key = setup_keys(ctx, tpm_conf)?; Password::new_argon2id_tpm(policy, cred, ctx, key.into()) }) .map_err(|e: CryptoError| { error!(tpm_err = ?e, "Failed to create tpm bound password"); }) } pub fn tpm_verify(pw: Password, cred: &str, tpm_conf: &TpmConfig) -> Result { let mut context = Context::new(tpm_conf.tcti.clone()).map_err(|e| { error!(tpm_err = ?e, "Failed to create tpm context"); })?; context .execute_with_nullauth_session(|ctx| { let key = setup_keys(ctx, tpm_conf)?; pw.verify_ctx(cred, Some((ctx, key.into()))) }) .map_err(|e: CryptoError| { error!(tpm_err = ?e, "Failed to create tpm bound password"); }) } } } #[cfg(test)] mod tests { use kanidm_proto::v1::{UnixGroupToken, UnixUserToken}; use super::Db; use crate::cache::Id; use crate::unix_config::TpmPolicy; const TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test"; const TESTACCOUNT1_PASSWORD_B: &str = "password b for account1 test"; #[tokio::test] async fn test_cache_db_account_basic() { sketching::test_init(); let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); let mut ut1 = UnixUserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2000, uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), shell: None, groups: Vec::new(), sshkeys: vec!["key-a".to_string()], valid: true, }; let id_name = Id::Name("testuser".to_string()); let id_name2 = Id::Name("testuser2".to_string()); let id_spn = Id::Name("testuser@example.com".to_string()); let id_spn2 = Id::Name("testuser2@example.com".to_string()); let id_uuid = Id::Name("0302b99c-f0f6-41ab-9492-852692b0fd16".to_string()); let id_gid = Id::Gid(2000); // test finding no account let r1 = dbtxn.get_account(&id_name).unwrap(); assert!(r1.is_none()); let r2 = dbtxn.get_account(&id_spn).unwrap(); assert!(r2.is_none()); let r3 = dbtxn.get_account(&id_uuid).unwrap(); assert!(r3.is_none()); let r4 = dbtxn.get_account(&id_gid).unwrap(); assert!(r4.is_none()); // test adding an account dbtxn.update_account(&ut1, 0).unwrap(); // test we can get it. let r1 = dbtxn.get_account(&id_name).unwrap(); assert!(r1.is_some()); let r2 = dbtxn.get_account(&id_spn).unwrap(); assert!(r2.is_some()); let r3 = dbtxn.get_account(&id_uuid).unwrap(); assert!(r3.is_some()); let r4 = dbtxn.get_account(&id_gid).unwrap(); assert!(r4.is_some()); // test adding an account that was renamed ut1.name = "testuser2".to_string(); ut1.spn = "testuser2@example.com".to_string(); dbtxn.update_account(&ut1, 0).unwrap(); // get the account let r1 = dbtxn.get_account(&id_name).unwrap(); assert!(r1.is_none()); let r2 = dbtxn.get_account(&id_spn).unwrap(); assert!(r2.is_none()); let r1 = dbtxn.get_account(&id_name2).unwrap(); assert!(r1.is_some()); let r2 = dbtxn.get_account(&id_spn2).unwrap(); assert!(r2.is_some()); let r3 = dbtxn.get_account(&id_uuid).unwrap(); assert!(r3.is_some()); let r4 = dbtxn.get_account(&id_gid).unwrap(); assert!(r4.is_some()); // Clear cache assert!(dbtxn.clear_cache().is_ok()); // should be nothing let r1 = dbtxn.get_account(&id_name2).unwrap(); assert!(r1.is_none()); let r2 = dbtxn.get_account(&id_spn2).unwrap(); assert!(r2.is_none()); let r3 = dbtxn.get_account(&id_uuid).unwrap(); assert!(r3.is_none()); let r4 = dbtxn.get_account(&id_gid).unwrap(); assert!(r4.is_none()); assert!(dbtxn.commit().is_ok()); } #[tokio::test] async fn test_cache_db_group_basic() { sketching::test_init(); let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); let mut gt1 = UnixGroupToken { name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2000, uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), }; let id_name = Id::Name("testgroup".to_string()); let id_name2 = Id::Name("testgroup2".to_string()); let id_spn = Id::Name("testgroup@example.com".to_string()); let id_spn2 = Id::Name("testgroup2@example.com".to_string()); let id_uuid = Id::Name("0302b99c-f0f6-41ab-9492-852692b0fd16".to_string()); let id_gid = Id::Gid(2000); // test finding no group let r1 = dbtxn.get_group(&id_name).unwrap(); assert!(r1.is_none()); let r2 = dbtxn.get_group(&id_spn).unwrap(); assert!(r2.is_none()); let r3 = dbtxn.get_group(&id_uuid).unwrap(); assert!(r3.is_none()); let r4 = dbtxn.get_group(&id_gid).unwrap(); assert!(r4.is_none()); // test adding a group dbtxn.update_group(>1, 0).unwrap(); let r1 = dbtxn.get_group(&id_name).unwrap(); assert!(r1.is_some()); let r2 = dbtxn.get_group(&id_spn).unwrap(); assert!(r2.is_some()); let r3 = dbtxn.get_group(&id_uuid).unwrap(); assert!(r3.is_some()); let r4 = dbtxn.get_group(&id_gid).unwrap(); assert!(r4.is_some()); // add a group via update gt1.name = "testgroup2".to_string(); gt1.spn = "testgroup2@example.com".to_string(); dbtxn.update_group(>1, 0).unwrap(); let r1 = dbtxn.get_group(&id_name).unwrap(); assert!(r1.is_none()); let r2 = dbtxn.get_group(&id_spn).unwrap(); assert!(r2.is_none()); let r1 = dbtxn.get_group(&id_name2).unwrap(); assert!(r1.is_some()); let r2 = dbtxn.get_group(&id_spn2).unwrap(); assert!(r2.is_some()); let r3 = dbtxn.get_group(&id_uuid).unwrap(); assert!(r3.is_some()); let r4 = dbtxn.get_group(&id_gid).unwrap(); assert!(r4.is_some()); // clear cache assert!(dbtxn.clear_cache().is_ok()); // should be nothing. let r1 = dbtxn.get_group(&id_name2).unwrap(); assert!(r1.is_none()); let r2 = dbtxn.get_group(&id_spn2).unwrap(); assert!(r2.is_none()); let r3 = dbtxn.get_group(&id_uuid).unwrap(); assert!(r3.is_none()); let r4 = dbtxn.get_group(&id_gid).unwrap(); assert!(r4.is_none()); assert!(dbtxn.commit().is_ok()); } #[tokio::test] async fn test_cache_db_account_group_update() { sketching::test_init(); let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); let gt1 = UnixGroupToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), gidnumber: 2000, uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), }; let gt2 = UnixGroupToken { name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2001, uuid: "b500be97-8552-42a5-aca0-668bc5625705".to_string(), }; let mut ut1 = UnixUserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2000, uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), shell: None, groups: vec![gt1.clone(), gt2], sshkeys: vec!["key-a".to_string()], valid: true, }; // First, add the groups. ut1.groups.iter().for_each(|g| { dbtxn.update_group(g, 0).unwrap(); }); // The add the account dbtxn.update_account(&ut1, 0).unwrap(); // Now, get the memberships of the two groups. let m1 = dbtxn .get_group_members("0302b99c-f0f6-41ab-9492-852692b0fd16") .unwrap(); let m2 = dbtxn .get_group_members("b500be97-8552-42a5-aca0-668bc5625705") .unwrap(); assert!(m1[0].name == "testuser"); assert!(m2[0].name == "testuser"); // Now alter testuser, remove gt2, update. ut1.groups = vec![gt1]; dbtxn.update_account(&ut1, 0).unwrap(); // Check that the memberships have updated correctly. let m1 = dbtxn .get_group_members("0302b99c-f0f6-41ab-9492-852692b0fd16") .unwrap(); let m2 = dbtxn .get_group_members("b500be97-8552-42a5-aca0-668bc5625705") .unwrap(); assert!(m1[0].name == "testuser"); assert!(m2.is_empty()); assert!(dbtxn.commit().is_ok()); } #[tokio::test] async fn test_cache_db_account_password() { sketching::test_init(); #[cfg(feature = "tpm")] let tpm_policy = TpmPolicy::Required("device:/dev/tpmrm0".to_string()); #[cfg(not(feature = "tpm"))] let tpm_policy = TpmPolicy::default(); let db = Db::new("", &tpm_policy).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); let uuid1 = "0302b99c-f0f6-41ab-9492-852692b0fd16"; let mut ut1 = UnixUserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2000, uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), shell: None, groups: Vec::new(), sshkeys: vec!["key-a".to_string()], valid: true, }; // Test that with no account, is false assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) == Ok(false)); // test adding an account dbtxn.update_account(&ut1, 0).unwrap(); // check with no password is false. assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) == Ok(false)); // update the pw assert!(dbtxn .update_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) .is_ok()); // Check it now works. assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) == Ok(true)); assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B) == Ok(false)); // Update the pw assert!(dbtxn .update_account_password(uuid1, TESTACCOUNT1_PASSWORD_B) .is_ok()); // Check it matches. assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_A) == Ok(false)); assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B) == Ok(true)); // Check that updating the account does not break the password. ut1.displayname = "Test User Update".to_string(); dbtxn.update_account(&ut1, 0).unwrap(); assert!(dbtxn.check_account_password(uuid1, TESTACCOUNT1_PASSWORD_B) == Ok(true)); assert!(dbtxn.commit().is_ok()); } #[tokio::test] async fn test_cache_db_group_rename_duplicate() { sketching::test_init(); let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); let mut gt1 = UnixGroupToken { name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2000, uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), }; let gt2 = UnixGroupToken { name: "testgroup".to_string(), spn: "testgroup@example.com".to_string(), gidnumber: 2001, uuid: "799123b2-3802-4b19-b0b8-1ffae2aa9a4b".to_string(), }; let id_name = Id::Name("testgroup".to_string()); let id_name2 = Id::Name("testgroup2".to_string()); // test finding no group let r1 = dbtxn.get_group(&id_name).unwrap(); assert!(r1.is_none()); // test adding a group dbtxn.update_group(>1, 0).unwrap(); let r0 = dbtxn.get_group(&id_name).unwrap(); assert!(r0.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16"); // Do the "rename" of gt1 which is what would allow gt2 to be valid. gt1.name = "testgroup2".to_string(); gt1.spn = "testgroup2@example.com".to_string(); // Now, add gt2 which dups on gt1 name/spn. dbtxn.update_group(>2, 0).unwrap(); let r2 = dbtxn.get_group(&id_name).unwrap(); assert!(r2.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b"); let r3 = dbtxn.get_group(&id_name2).unwrap(); assert!(r3.is_none()); // Now finally update gt1 dbtxn.update_group(>1, 0).unwrap(); // Both now coexist let r4 = dbtxn.get_group(&id_name).unwrap(); assert!(r4.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b"); let r5 = dbtxn.get_group(&id_name2).unwrap(); assert!(r5.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16"); assert!(dbtxn.commit().is_ok()); } #[tokio::test] async fn test_cache_db_account_rename_duplicate() { sketching::test_init(); let db = Db::new("", &TpmPolicy::default()).expect("failed to create."); let dbtxn = db.write().await; assert!(dbtxn.migrate().is_ok()); let mut ut1 = UnixUserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2000, uuid: "0302b99c-f0f6-41ab-9492-852692b0fd16".to_string(), shell: None, groups: Vec::new(), sshkeys: vec!["key-a".to_string()], valid: true, }; let ut2 = UnixUserToken { name: "testuser".to_string(), spn: "testuser@example.com".to_string(), displayname: "Test User".to_string(), gidnumber: 2001, uuid: "799123b2-3802-4b19-b0b8-1ffae2aa9a4b".to_string(), shell: None, groups: Vec::new(), sshkeys: vec!["key-a".to_string()], valid: true, }; let id_name = Id::Name("testuser".to_string()); let id_name2 = Id::Name("testuser2".to_string()); // test finding no account let r1 = dbtxn.get_account(&id_name).unwrap(); assert!(r1.is_none()); // test adding an account dbtxn.update_account(&ut1, 0).unwrap(); let r0 = dbtxn.get_account(&id_name).unwrap(); assert!(r0.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16"); // Do the "rename" of gt1 which is what would allow gt2 to be valid. ut1.name = "testuser2".to_string(); ut1.spn = "testuser2@example.com".to_string(); // Now, add gt2 which dups on gt1 name/spn. dbtxn.update_account(&ut2, 0).unwrap(); let r2 = dbtxn.get_account(&id_name).unwrap(); assert!(r2.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b"); let r3 = dbtxn.get_account(&id_name2).unwrap(); assert!(r3.is_none()); // Now finally update gt1 dbtxn.update_account(&ut1, 0).unwrap(); // Both now coexist let r4 = dbtxn.get_account(&id_name).unwrap(); assert!(r4.unwrap().0.uuid == "799123b2-3802-4b19-b0b8-1ffae2aa9a4b"); let r5 = dbtxn.get_account(&id_name2).unwrap(); assert!(r5.unwrap().0.uuid == "0302b99c-f0f6-41ab-9492-852692b0fd16"); assert!(dbtxn.commit().is_ok()); } }