mod contract; mod crypto; pub mod error; mod exchange_client; pub use contract::{ add_participant, init_meeting, is_meet_participant, view_meet_participants, view_moderator_account, }; use common_api::api::{ApiResponse, Data, ExchangeMessage}; use crypto::{decrypt, encrypt, secret}; use error::ApiError; use exchange_client::{exchange, public_keys, receive}; use futures::{select, FutureExt}; use gloo_timers::future::TimeoutFuture; use itertools::Itertools; use js_sys::Promise; use log::{info, warn}; use near_client::prelude::*; use near_primitives_core::{account::id::AccountId, hash::CryptoHash, types::Nonce}; use serde::{Deserialize, Serialize}; use std::{collections::HashSet, str::FromStr, sync::Arc}; use url::Url; use uuid::Uuid; use wasm_bindgen::prelude::*; type Result = std::result::Result; #[wasm_bindgen(start)] pub fn start() -> Result<()> { console_error_panic_hook::set_once(); console_log::init().unwrap(); Ok(()) } fn to_value(value: &T) -> JsValue { match serde_wasm_bindgen::to_value(value) { Ok(value) => value, Err(err) => err.into(), } } pub struct Handler { contract_id: AccountId, secret: [u8; 32], exchange_url: url::Url, rpc_url: url::Url, } impl Handler { pub fn add_path(&self, path: &str) -> url::Url { let mut url = self.exchange_url.clone(); url.set_path(path); url } } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Meeting { pub meet_id: Uuid, pub transaction_id: CryptoHash, } #[wasm_bindgen] #[derive(Debug, Clone)] pub struct ProvisionerConfig { contract_id: AccountId, rpc_url: Url, exchange_url: Url, } #[wasm_bindgen] impl ProvisionerConfig { #[wasm_bindgen(constructor)] pub fn new( contract_id: String, rpc_url: &str, exchange_url: &str, ) -> Result { let rpc_url = Url::from_str(rpc_url) .map_err(|err| ApiError::Other(format!("Bad rpc url, cause {err}")))?; let exchange_url = Url::from_str(exchange_url) .map_err(|err| ApiError::Other(format!("Bad exchange url, cause {err}")))?; let contract_id = AccountId::from_str(&contract_id).map_err(ApiError::from)?; Ok(Self { contract_id, rpc_url, exchange_url, }) } } #[wasm_bindgen] pub struct KeyProvisioner { signer: Arc, handler: Arc, } impl KeyProvisioner { fn handler(&self) -> Arc { Arc::clone(&self.handler) } fn signer(&self) -> Arc { Arc::clone(&self.signer) } } #[wasm_bindgen] impl KeyProvisioner { #[wasm_bindgen(constructor)] pub fn new( keypair_str: String, nonce: Nonce, account_id: String, ProvisionerConfig { contract_id, rpc_url, exchange_url, }: ProvisionerConfig, ) -> Result { let signer = Arc::new(Signer::from_secret_str( &keypair_str, AccountId::from_str(&account_id)?, nonce, )?); let handler = Arc::new(Handler { contract_id, secret: secret(), exchange_url, rpc_url, }); Ok(Self { signer, handler }) } /// Initializes meeting by calling the contract and providing there a set of participants' keys /// /// Arguments /// /// - participants_set - The [`js_sys::Set`] represents hash set of participants' keys /// - timeout_ms - The [`u32`] that represents milliseconds that were given not to be exceeded pub fn init_meeting(&self, participants_set: js_sys::Set, timeout_ms: u32) -> Promise { let handler = self.handler(); let signer = self.signer(); let init_meeting = async move { let participants: HashSet = participants_set .values() .into_iter() .map_ok(|it| { let Some(acc_id) = it.as_string() else { return Err(ApiError::Other("Set item type isn't a string".to_owned())); }; AccountId::from_str(&acc_id).map_err(ApiError::from) }) .flatten_ok() .try_collect()?; let (meet_id, transaction_id) = init_meeting(&handler, &signer, participants).await?; Ok(to_value(&Meeting { meet_id, transaction_id, })) }; wasm_bindgen_futures::future_to_promise(async move { select! { meet = init_meeting.fuse() => meet, _ = TimeoutFuture::new(timeout_ms).fuse() => { Err(ApiError::CallTimeout("The initialization has been timed out".to_owned()).into()) } } }) } /// Sends participants' keys to the keys exchange server /// /// Arguments /// /// - meeting_id - The [`String`] that indicates ID of the meeting room /// - timeout_ms - The [`u32`] that represents milliseconds that were given not to be exceeded pub fn send_keys(&self, meeting_id: String, timeout_ms: u32) -> Promise { let handler = self.handler(); let signer = self.signer(); let send_keys = async move { let meet_id = Uuid::from_str(&meeting_id).map_err(ApiError::from)?; let mut participants = view_meet_participants(&handler, meet_id) .await? .ok_or_else(|| { ApiError::InvalidSessionUuid(format!("Wrong Session ID: {meet_id}")) })?; info!("Get a meeting participants {participants:?}"); while !participants.is_empty() { let infos = match public_keys(&handler, &signer, meet_id, participants.clone()).await { Ok(ApiResponse::Success(infos)) => infos, Ok(ApiResponse::Timeout) => { warn!("Timeout happens on a server-side, subscribed one more time"); continue; } Err(err) => { return Err(ApiError::KeyExchange(format!( "The send keys operation has been failed, cause {err:?}" )) .into()); } }; // remove infos that is already processed for key in &infos { participants.remove(&key.account_id); } let messages = infos .into_iter() .map(|info| { let sk = signer.secret_key(); let other_pk = info.public_key; let msg = encrypt(sk, other_pk, meet_id, &handler.secret)?; Ok::(ExchangeMessage { account_id: info.account_id, message: Data { data: msg, moderator_pk: signer.public_key().to_owned(), }, }) }) .try_collect()?; exchange(&handler, &signer, meet_id, messages).await?; } Ok(JsValue::from_str(&base64::encode(handler.secret))) }; wasm_bindgen_futures::future_to_promise(async move { select! { aes_secret = send_keys.fuse() => aes_secret, _ = TimeoutFuture::new(timeout_ms).fuse() => { Err(ApiError::CallTimeout("The send keys operation has been timed out".to_owned()).into()) } } }) } /// Get participant's key from a server /// /// Arguments /// /// - meeting_id - The [`String`] that indicates ID of the meeting room /// - timeout_ms - The [`u32`] that represents milliseconds that were given not to be exceeded pub fn get_key(&self, meeting_id: String, timeout_ms: u32) -> Promise { let handler = self.handler(); let signer = self.signer(); let get_key = async move { let meet_id = Uuid::from_str(&meeting_id).map_err(ApiError::from)?; loop { if let ApiResponse::Success(data) = receive(&handler, &signer, meet_id).await? { let secret = decrypt(signer.secret_key(), data.moderator_pk, meet_id, data.data)?; return Ok(JsValue::from_str(&base64::encode(secret))); } } }; wasm_bindgen_futures::future_to_promise(async move { select! { key = get_key.fuse() => key, _ = TimeoutFuture::new(timeout_ms).fuse() => { Err(ApiError::CallTimeout("The getting key operation has been timed out".to_owned()).into()) } } }) } }