From 5eb4fa6d3bd95e4bb0b26294279919cf932fd2f7 Mon Sep 17 00:00:00 2001 From: Ericson Soares Date: Mon, 19 Aug 2024 20:38:07 -0300 Subject: [PATCH] Device register route and integrate key manager on bootstrap --- .../crates/cloud-services/src/cloud_client.rs | 21 +- core/crates/cloud-services/src/error.rs | 2 + .../src/key_manager/key_store.rs | 4 +- .../cloud-services/src/key_manager/mod.rs | 64 ++++-- core/crates/cloud-services/src/lib.rs | 1 + core/src/api/cloud/devices.rs | 184 +++++++++++++++--- core/src/api/cloud/mod.rs | 145 +++++++++----- 7 files changed, 320 insertions(+), 101 deletions(-) diff --git a/core/crates/cloud-services/src/cloud_client.rs b/core/crates/cloud-services/src/cloud_client.rs index 41baefd2b..b9154f979 100644 --- a/core/crates/cloud-services/src/cloud_client.rs +++ b/core/crates/cloud-services/src/cloud_client.rs @@ -34,7 +34,7 @@ pub struct CloudServices { http_client: ClientWithMiddleware, domain_name: String, pub token_refresher: TokenRefresher, - pub key_manager: Option>, + key_manager: Arc>>>, } impl CloudServices { @@ -90,7 +90,7 @@ impl CloudServices { get_cloud_api_address, http_client, domain_name, - key_manager: None, + key_manager: Arc::default(), }) } @@ -180,6 +180,23 @@ impl CloudServices { Ok(client) } + + pub async fn set_key_manager(&self, key_manager: KeyManager) { + self.key_manager + .write() + .await + .replace(Arc::new(key_manager)); + } + + pub async fn key_manager(&self) -> Result, Error> { + self.key_manager + .read() + .await + .as_ref() + .map_or(Err(Error::KeyManagerNotInitialized), |key_manager| { + Ok(Arc::clone(key_manager)) + }) + } } #[cfg(test)] diff --git a/core/crates/cloud-services/src/error.rs b/core/crates/cloud-services/src/error.rs index 48b45ac07..d76d3f349 100644 --- a/core/crates/cloud-services/src/error.rs +++ b/core/crates/cloud-services/src/error.rs @@ -46,6 +46,8 @@ pub enum Error { source: sd_crypto::Error, context: &'static str, }, + #[error("Key manager not initialized")] + KeyManagerNotInitialized, } #[derive(thiserror::Error, Debug)] diff --git a/core/crates/cloud-services/src/key_manager/key_store.rs b/core/crates/cloud-services/src/key_manager/key_store.rs index 6d96b8a6b..1d80f42c5 100644 --- a/core/crates/cloud-services/src/key_manager/key_store.rs +++ b/core/crates/cloud-services/src/key_manager/key_store.rs @@ -26,9 +26,9 @@ pub struct KeyStore { } impl KeyStore { - pub fn new(rng: &mut CryptoRng) -> Self { + pub fn new(iroh_secret_key: IrohSecretKey) -> Self { Self { - iroh_secret_key: IrohSecretKey::generate_with_rng(rng), + iroh_secret_key, keys_by_hash: HashMap::new(), } } diff --git a/core/crates/cloud-services/src/key_manager/mod.rs b/core/crates/cloud-services/src/key_manager/mod.rs index 763ee07bd..74386e0c8 100644 --- a/core/crates/cloud-services/src/key_manager/mod.rs +++ b/core/crates/cloud-services/src/key_manager/mod.rs @@ -10,7 +10,7 @@ use std::{ }; use iroh_base::key::{NodeId, SecretKey as IrohSecretKey}; -use tokio::{fs, io, sync::RwLock}; +use tokio::{fs, sync::RwLock}; mod key_store; @@ -27,33 +27,18 @@ pub struct KeyManager { impl KeyManager { pub async fn new( master_key: SecretKey, + iroh_secret_key: IrohSecretKey, data_directory: impl AsRef + Send, rng: &mut CryptoRng, ) -> Result { async fn inner( master_key: SecretKey, + iroh_secret_key: IrohSecretKey, keys_file_path: PathBuf, rng: &mut CryptoRng, ) -> Result { - let store = match fs::metadata(&keys_file_path).await { - Ok(metadata) => KeyStore::decrypt(&master_key, metadata, &keys_file_path).await?, - - Err(e) if e.kind() == io::ErrorKind::NotFound => { - // File not found, so we create a new one - let store = KeyStore::new(rng); - store.encrypt(&master_key, rng, &keys_file_path).await?; - store - } - - Err(e) => { - return Err(FileIOError::from(( - keys_file_path, - e, - "Failed to read space keys file", - )) - .into()); - } - }; + let store = KeyStore::new(iroh_secret_key); + store.encrypt(&master_key, rng, &keys_file_path).await?; Ok(KeyManager { master_key, @@ -62,7 +47,44 @@ impl KeyManager { }) } - inner(master_key, data_directory.as_ref().join(KEY_FILE_NAME), rng).await + inner( + master_key, + iroh_secret_key, + data_directory.as_ref().join(KEY_FILE_NAME), + rng, + ) + .await + } + + pub async fn load( + master_key: SecretKey, + data_directory: impl AsRef + Send, + ) -> Result { + async fn inner( + master_key: SecretKey, + keys_file_path: PathBuf, + ) -> Result { + Ok(KeyManager { + store: RwLock::new( + KeyStore::decrypt( + &master_key, + fs::metadata(&keys_file_path).await.map_err(|e| { + FileIOError::from(( + &keys_file_path, + e, + "Failed to read space keys file", + )) + })?, + &keys_file_path, + ) + .await?, + ), + master_key, + keys_file_path, + }) + } + + inner(master_key, data_directory.as_ref().join(KEY_FILE_NAME)).await } pub async fn iroh_secret_key(&self) -> IrohSecretKey { diff --git a/core/crates/cloud-services/src/lib.rs b/core/crates/cloud-services/src/lib.rs index 66fb7f973..470550715 100644 --- a/core/crates/cloud-services/src/lib.rs +++ b/core/crates/cloud-services/src/lib.rs @@ -37,6 +37,7 @@ mod token_refresher; pub use cloud_client::CloudServices; pub use error::{Error, GetTokenError}; +pub use key_manager::KeyManager; // Re-exports pub use iroh_base::key::{NodeId, SecretKey as IrohSecretKey}; diff --git a/core/src/api/cloud/devices.rs b/core/src/api/cloud/devices.rs index ab3d6de6f..ee619d933 100644 --- a/core/src/api/cloud/devices.rs +++ b/core/src/api/cloud/devices.rs @@ -1,16 +1,21 @@ -use crate::{api::{Ctx, R}, node::HardwareModel}; +use crate::{ + api::{Ctx, R}, + node::HardwareModel, +}; use futures::{SinkExt, StreamExt}; use sd_cloud_schema::{ auth::AccessToken, devices::{self, DeviceOS, PubId}, opaque_ke::{ - rand::rngs::OsRng, ClientLogin, ClientLoginFinishParameters, ClientLoginFinishResult, - ClientLoginStartResult, + ClientLogin, ClientLoginFinishParameters, ClientLoginFinishResult, ClientLoginStartResult, + ClientRegistration, ClientRegistrationFinishParameters, ClientRegistrationFinishResult, + ClientRegistrationStartResult, }, Client, Service, SpacedriveCipherSuite, }; -use sd_core_cloud_services::QuinnConnection; +use sd_core_cloud_services::{NodeId, QuinnConnection}; +use sd_crypto::{cloud::secret_key::SecretKey, CryptoRng}; use blake3::Hash; use chrono::DateTime; @@ -198,20 +203,19 @@ pub async fn hello( access_token: AccessToken, device_pub_id: PubId, hashed_pub_id: Hash, -) -> Result<(), rspc::Error> { + rng: &mut CryptoRng, +) -> Result { use devices::hello::{Request, RequestUpdate, Response, State}; - let ClientLoginStartResult { message, state } = ClientLogin::::start( - &mut OsRng, - hashed_pub_id.as_bytes().as_slice(), - ) - .map_err(|e| { - error!(?e, "OPAQUE error initializing device hello request;"); - rspc::Error::new( - rspc::ErrorCode::InternalServerError, - "Failed to initialize device login".into(), - ) - })?; + let ClientLoginStartResult { message, state } = + ClientLogin::::start(rng, hashed_pub_id.as_bytes().as_slice()) + .map_err(|e| { + error!(?e, "OPAQUE error initializing device hello request;"); + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Failed to initialize device login".into(), + ) + })?; let (mut hello_continuation, mut res_stream) = handle_comm_error( client @@ -234,9 +238,9 @@ pub async fn hello( )); }; - let login_response = + let credential_response = match handle_comm_error(res, "Communication error on device hello response;")? { - Ok(Response(State::LoginResponse(login_response))) => login_response, + Ok(Response(State::LoginResponse(credential_response))) => credential_response, Ok(Response(State::End)) => { unreachable!("Device hello response MUST not be End here, this is a serious bug and should crash;"); } @@ -253,7 +257,7 @@ pub async fn hello( } = state .finish( hashed_pub_id.as_bytes().as_slice(), - *login_response, + *credential_response, ClientLoginFinishParameters::default(), ) .map_err(|e| { @@ -290,16 +294,146 @@ pub async fn hello( Ok(Response(State::LoginResponse(_))) => { unreachable!("Device hello final response MUST be End here, this is a serious bug and should crash;"); } - Ok(Response(State::End)) => {} + Ok(Response(State::End)) => { + // Protocol completed successfully + Ok(SecretKey::new(export_key.as_slice().try_into().expect( + "Key mismatch between OPAQUE and crypto crate; this is a serious bug and should crash;", + ))) + } Err(e) => { error!(?e, "Device hello final response error;"); - return Err(e.into()); + Err(e.into()) } + } +} + +pub struct DeviceRegisterData { + pub pub_id: PubId, + pub name: String, + pub os: DeviceOS, + pub storage_size: u64, + pub connection_id: NodeId, +} + +pub async fn register( + client: &Client, Service>, + access_token: AccessToken, + DeviceRegisterData { + pub_id, + name, + os, + storage_size, + connection_id, + }: DeviceRegisterData, + hashed_pub_id: Hash, + rng: &mut CryptoRng, +) -> Result { + use devices::register::{Request, RequestUpdate, Response, State}; + + let ClientRegistrationStartResult { message, state } = + ClientRegistration::::start( + rng, + hashed_pub_id.as_bytes().as_slice(), + ) + .map_err(|e| { + error!(?e, "OPAQUE error initializing device register request;"); + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Failed to initialize device register".into(), + ) + })?; + + let (mut register_continuation, mut res_stream) = handle_comm_error( + client + .devices() + .register(Request { + access_token, + pub_id, + name, + os, + storage_size, + connection_id, + opaque_register_message: Box::new(message), + }) + .await, + "Failed to send device register request;", + )?; + + let Some(res) = res_stream.next().await else { + let message = "Server did not send a device register response;"; + error!("{message}"); + return Err(rspc::Error::new( + rspc::ErrorCode::InternalServerError, + message.to_string(), + )); }; - Ok(()) -} + let registration_response = + match handle_comm_error(res, "Communication error on device register response;")? { + Ok(Response(State::RegistrationResponse(res))) => res, + Ok(Response(State::End)) => { + unreachable!("Device hello response MUST not be End here, this is a serious bug and should crash;"); + } + Err(e) => { + error!(?e, "Device hello response error;"); + return Err(e.into()); + } + }; -pub async fn device_registration() -> Result<(), rspc::Error> { - Ok(()) + let ClientRegistrationFinishResult { + message, + export_key, + .. + } = state + .finish( + rng, + hashed_pub_id.as_bytes().as_slice(), + *registration_response, + ClientRegistrationFinishParameters::default(), + ) + .map_err(|e| { + error!(?e, "Device register finish error;"); + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Failed to finish device register".into(), + ) + })?; + + register_continuation + .send(RequestUpdate { + opaque_registration_finish: Box::new(message), + }) + .await + .map_err(|e| { + error!(?e, "Failed to send device register request continuation;"); + rspc::Error::new( + rspc::ErrorCode::InternalServerError, + "Failed to finish device register procedure;".into(), + ) + })?; + + let Some(res) = res_stream.next().await else { + let message = "Server did not send a device register END response;"; + error!("{message}"); + return Err(rspc::Error::new( + rspc::ErrorCode::InternalServerError, + message.to_string(), + )); + }; + + match handle_comm_error(res, "Communication error on device register response;")? { + Ok(Response(State::RegistrationResponse(_))) => { + unreachable!("Device register final response MUST be End here, this is a serious bug and should crash;"); + } + Ok(Response(State::End)) => { + // Protocol completed successfully + Ok(SecretKey::new(export_key.as_slice().try_into().expect( + "Key mismatch between OPAQUE and crypto crate; this is a serious bug and should crash;", + ))) + } + Err(e) => { + error!(?e, "Device register final response error;"); + Err(e.into()) + } + } } diff --git a/core/src/api/cloud/mod.rs b/core/src/api/cloud/mod.rs index d676e7323..2fd6e31b6 100644 --- a/core/src/api/cloud/mod.rs +++ b/core/src/api/cloud/mod.rs @@ -1,11 +1,12 @@ -use crate::Node; +use crate::{node::config::NodeConfig, volume::get_volumes, Node}; use sd_cloud_schema::{ auth, error::{ClientSideError, Error}, users, Client, Service, }; -use sd_core_cloud_services::QuinnConnection; +use sd_core_cloud_services::{IrohSecretKey, KeyManager, QuinnConnection}; +use sd_crypto::{CryptoRng, SeedableRng}; use rspc::alpha::AlphaRouter; use tracing::error; @@ -35,64 +36,106 @@ pub(crate) fn mount() -> AlphaRouter { .merge("libraries.", libraries::mount()) .merge("locations.", locations::mount()) .merge("devices.", devices::mount()) - // .procedure("bootstrap", { - // R.mutation(|node, access_token: auth::AccessToken| async move { - // use sd_cloud_schema::devices; + .procedure("bootstrap", { + R.mutation( + |node, (access_token, refresh_token): (auth::AccessToken, auth::RefreshToken)| async move { + use sd_cloud_schema::devices; - // let client = try_get_cloud_services_client(&node).await?; + node.cloud_services + .token_refresher + .init(access_token.clone(), refresh_token) + .await?; - // // create user route is idempotent, so we can safely keep creating the same user over and over - // handle_comm_error( - // client - // .users() - // .create(users::create::Request { - // access_token: access_token.clone(), - // }) - // .await, - // "Failed to create user;", - // )??; + let client = try_get_cloud_services_client(&node).await?; + let data_directory = node.config.data_directory(); - // let device_pub_id = devices::PubId(node.config.get().await.id); - // let mut hasher = blake3::Hasher::new(); - // hasher.update(device_pub_id.0.as_bytes().as_slice()); - // let hashed_pub_id = hasher.finalize(); + let mut rng = + CryptoRng::from_seed(node.master_rng.lock().await.generate_fixed()); - // match handle_comm_error( - // client - // .devices() - // .get(devices::get::Request { - // access_token: access_token.clone(), - // pub_id: device_pub_id, - // }) - // .await, - // "Failed to get device on cloud bootstrap;", - // )? { - // Ok(_) => { - // // Device registered, we execute a device hello flow - // self::devices::hello(&client, access_token, device_pub_id, hashed_pub_id) - // .await - // } - // Err(Error::Client(ClientSideError::NotFound(_))) => { - // // Device not registered, we execute a device register flow - // todo!() - // } - // Err(e) => return Err(e.into()), - // } + // create user route is idempotent, so we can safely keep creating the same user over and over + handle_comm_error( + client + .users() + .create(users::create::Request { + access_token: access_token.clone(), + }) + .await, + "Failed to create user;", + )??; - // // TODO: figure out a way to know if we need to register the device or send a device hello request + let (device_pub_id, name, os) = { + let NodeConfig { id, name, os, .. } = node.config.get().await; + (devices::PubId(id), name, os) + }; + let mut hasher = blake3::Hasher::new(); + hasher.update(device_pub_id.0.as_bytes().as_slice()); + let hashed_pub_id = hasher.finalize(); - // // TODO: in case of a device register request, we use the OPAQUE key to encrypt iroh's secret key (NodeId) - // // and save on data directory + let key_manager = match handle_comm_error( + client + .devices() + .get(devices::get::Request { + access_token: access_token.clone(), + pub_id: device_pub_id, + }) + .await, + "Failed to get device on cloud bootstrap;", + )? { + Ok(_) => { + // Device registered, we execute a device hello flow + let master_key = self::devices::hello( + &client, + access_token, + device_pub_id, + hashed_pub_id, + &mut rng, + ) + .await?; - // // TODO: in case of a device hello request, we use the OPAQUE key to decrypt iroh's secret key (NodeId) - // // and keep it in memory + KeyManager::load(master_key, data_directory).await? + } + Err(Error::Client(ClientSideError::NotFound(_))) => { + // Device not registered, we execute a device register flow + let iroh_secret_key = IrohSecretKey::generate_with_rng(&mut rng); - // // TODO: With this device iroh's secret key (NodeId) now known and we can start the iroh - // // node for cloud p2p + let master_key = self::devices::register( + &client, + access_token, + self::devices::DeviceRegisterData { + pub_id: device_pub_id, + name, + os, + // TODO(@fogodev): We should use storage statistics from sqlite db + storage_size: get_volumes() + .await + .into_iter() + .map(|volume| volume.total_capacity) + .sum(), + connection_id: iroh_secret_key.public(), + }, + hashed_pub_id, + &mut rng, + ) + .await?; - // Ok(()) - // }) - // }) + KeyManager::new(master_key, iroh_secret_key, data_directory, &mut rng) + .await? + } + Err(e) => return Err(e.into()), + }; + + let iroh_secret_key = key_manager.iroh_secret_key().await; + + node.cloud_services.set_key_manager(key_manager).await; + + // TODO: With this device iroh's secret key (NodeId) now known and we can start the iroh + // node for cloud p2p + todo!("Start iroh node for cloud p2p"); + + Ok(()) + }, + ) + }) } fn handle_comm_error(