use anyhow::anyhow; use isahc::{ReadResponseExt, RequestExt}; use isahc::http::StatusCode; use rsa::pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, LineEnding}; use rsa::pkcs8::EncodePublicKey; use rsa::RsaPrivateKey; use serde::{Deserialize, Serialize}; use crate::{BASE_URL, BunqClient, deserialize_retarded_response, sign}; #[derive(Serialize, Deserialize, Default)] struct AppState { token: String, pem_private: String, } #[derive(Serialize, Deserialize, Default)] pub struct BunqConfig { api_key: String, state: Option, } impl BunqConfig { pub fn load() -> anyhow::Result { println!("Loading config file from {}", confy::get_configuration_file_path("bunq-rs", "bunq-rs")?.to_string_lossy()); Ok(confy::load("bunq-rs", "bunq-rs")?) } pub fn save(&self) -> anyhow::Result<()> { println!("Storing config file in {}", confy::get_configuration_file_path("bunq-rs", None)?.to_string_lossy()); confy::store("bunq-rs", "bunq-rs", self)?; Ok(()) } pub fn install(mut self) -> anyhow::Result { let api_key = &self.api_key; let keypair = if let Some(state) = &self.state { RsaPrivateKey::from_pkcs1_pem(&state.pem_private)? } else { let mut rng = rand::thread_rng(); let bits = 2048; let keypair = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key"); let pem_public = keypair.to_public_key().to_public_key_pem(LineEnding::CRLF)?; let body = Installation { client_public_key: &pem_public, }; let response = isahc::post( format!("{}/v1/installation", BASE_URL), serde_json::to_string(&body)?, )? .text()?; let response: InstallationResponse = deserialize_retarded_response(&response)?.response; let token = response.token.token; let body = DeviceServer { description: "JK Test Server", secret: api_key, permitted_ips: &["77.175.86.236", "*"], }; let body = serde_json::to_string(&body)?; let mut response = isahc::http::Request::post(format!("{}/v1/device-server", BASE_URL)) .header("X-Bunq-Client-Authentication", &token) .body(body)? .send()?; let response_text = response.text()?; println!("{}", response_text); if response.status() != StatusCode::OK { return Err(anyhow!(response_text)); } self.state = Some(AppState { pem_private: keypair.to_pkcs1_pem(LineEnding::CRLF)?.to_string(), token }); self.save()?; keypair }; let token = self.state.unwrap().token; let body = SessionServer { secret: api_key }; let body = serde_json::to_string(&body)?; let sig = sign(&body, &keypair)?; let response = isahc::http::Request::post(format!("{}/v1/session-server", BASE_URL)) .header("X-Bunq-Client-Authentication", &token) .header("X-Bunq-Client-Signature", &sig) .body(body)? .send()? .text()?; let r: SessionServerResponse = deserialize_retarded_response(&response)?.response; Ok(BunqClient { keypair, token: r.token.token, user_id: r.user_person.id, }) } } #[derive(Serialize)] pub(super) struct Installation<'a> { pub(super) client_public_key: &'a str, } #[derive(Deserialize)] pub(super) struct Token { pub(super) token: String, } #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] pub(super) struct InstallationResponse { pub(super) token: Token, } #[derive(Serialize)] pub(super) struct DeviceServer<'a> { pub(super) description: &'a str, pub(super) secret: &'a str, pub(super) permitted_ips: &'a [&'a str], } #[derive(Serialize)] pub(super) struct SessionServer<'a> { pub(super) secret: &'a str, } #[derive(Deserialize)] #[serde(rename_all = "PascalCase")] pub(super) struct SessionServerResponse { pub(super) token: Token, pub(super) user_person: UserPerson, } #[derive(Deserialize)] pub(super) struct UserPerson { pub(super) id: i64, }