use crate::time::sleep;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use rand::random;
use reqwest::IntoUrl;
use sia_core::seed::{self, Seed};
use sia_core::signing::PrivateKey;
use sia_core::types::Hash256;
use thiserror::Error;
use url::Url;
use crate::app_client::{self, Client};
use crate::object_encryption::derive;
use crate::time::Duration;
use crate::{AppID, AppKey, AppMetadata, Sdk};
pub struct DisconnectedState;
pub struct RequestingApprovalState {
response_url: Url,
register_url: Url,
status_url: Url,
expiration: DateTime<Utc>,
}
pub struct ApprovedState {
register_url: Url,
user_secret: Hash256,
}
pub struct Builder<S> {
ephemeral_key: PrivateKey,
state: S,
client: Client,
app_meta: AppMetadata,
}
#[derive(Error, Debug)]
pub enum BuilderError {
#[error("url error: {0}")]
Url(#[from] url::ParseError),
#[error("client error: {0}")]
Client(#[from] app_client::Error),
#[error("transport error: {0}")]
Transport(String),
#[error("mnemonic error: {0}")]
Mnemonic(#[from] seed::SeedError),
#[error("request expired")]
RequestExpired,
}
impl Builder<DisconnectedState> {
pub fn new<U: IntoUrl>(indexer_url: U, app_meta: AppMetadata) -> Result<Self, BuilderError> {
let client = Client::new(indexer_url)?;
Ok(Self {
ephemeral_key: PrivateKey::from_seed(&random::<[u8; 32]>()),
state: DisconnectedState,
client,
app_meta,
})
}
pub async fn connected(&self, app_key: &AppKey) -> Result<Option<Sdk>, BuilderError> {
let connected = self.client.check_app_authenticated(&app_key.0).await?;
if !connected {
return Ok(None);
}
let sdk = Sdk::new(self.client.clone(), Arc::new(app_key.clone())).await?;
Ok(Some(sdk))
}
pub async fn request_connection(
self,
) -> Result<Builder<RequestingApprovalState>, BuilderError> {
let resp = self
.client
.request_app_connection(&self.ephemeral_key, &self.app_meta)
.await?;
Ok(Builder {
ephemeral_key: self.ephemeral_key,
app_meta: self.app_meta,
state: RequestingApprovalState {
response_url: Url::parse(&resp.response_url)?,
register_url: Url::parse(&resp.register_url)?,
status_url: Url::parse(&resp.status_url)?,
expiration: resp.expiration,
},
client: self.client,
})
}
}
impl Builder<RequestingApprovalState> {
pub fn response_url(&self) -> &str {
self.state.response_url.as_str()
}
pub async fn wait_for_approval(self) -> Result<Builder<ApprovedState>, BuilderError> {
loop {
if Utc::now() >= self.state.expiration {
return Err(BuilderError::RequestExpired);
}
if let Some(user_secret) = self
.client
.check_request_status(&self.ephemeral_key, self.state.status_url.clone())
.await?
{
return Ok(Builder {
ephemeral_key: self.ephemeral_key,
state: ApprovedState {
register_url: self.state.register_url.clone(),
user_secret,
},
app_meta: self.app_meta,
client: self.client,
});
}
sleep(Duration::from_secs(5)).await;
}
}
}
impl Builder<ApprovedState> {
pub async fn register(self, mnemonic: &str) -> Result<Sdk, BuilderError> {
let private_key = derive_app_key(mnemonic, &self.app_meta.id, &self.state.user_secret)?;
self.client
.register_app(
&self.ephemeral_key,
&private_key,
self.state.register_url.clone(),
)
.await?;
Sdk::new(self.client, Arc::new(AppKey(private_key))).await
}
}
fn derive_app_key(
mnemonic: &str,
app_id: &AppID,
shared_secret: &Hash256,
) -> Result<PrivateKey, BuilderError> {
const KEY_DOMAIN: &[u8] = b"indexd app key derivation";
let seed = Seed::new(mnemonic)?;
let mut key = [0u8; 64];
key[..32].copy_from_slice(seed.entropy());
key[32..].copy_from_slice(shared_secret.as_ref());
let mut okm = [0u8; 32];
derive(&key, app_id.as_ref(), KEY_DOMAIN, &mut okm);
Ok(PrivateKey::from_seed(&okm))
}
#[cfg(test)]
mod test {
use crate::app_id;
use super::*;
use sia_core::hash_256;
use sia_core::types::Hash256;
#[cfg(target_arch = "wasm32")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[sia_core_derive::cross_target_test]
fn test_app_key_derivation_golden() {
const MNEMONIC: &str =
"glare own entire dish exact open theme family harsh room scrap rose";
const APP_ID: AppID =
app_id!("0e90d697f5045a6593f1c43ebf79a369e2bc72cc5c7b6282f3b5aeb0de6e4005");
const SHARED_SECRET: Hash256 =
hash_256!("cf02d945fe4bfe614d823dc13c19aa8501699e656d0f7915490c3056d5c97dc6");
const EXPECTED_APP_KEY: &str =
"b75061f34bb3aeab232b0671da2d0347c547343a0026bb5535c291d964fd09a1";
let mut seed = [0u8; 32];
hex::decode_to_slice(EXPECTED_APP_KEY, &mut seed).expect("decoding failed");
let expected_app_key = PrivateKey::from_seed(&seed);
let derived_app_key =
derive_app_key(MNEMONIC, &APP_ID, &SHARED_SECRET).expect("derivation failed");
assert_eq!(derived_app_key, expected_app_key);
}
}