use crate::{general, utils::block_on}; use ed25519_dalek::SigningKey; use log::{debug, info, warn}; use openssl::bn::BigNum; use openssl::hash::{Hasher, MessageDigest}; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; use serde::{Deserialize, Serialize}; use std::{fs::File, io::Write, path::Path}; #[derive(Serialize, Default)] pub struct AccountData { path: String, network: String, ssh_pubkey: String, account_balance: f64, locked_funds: f64, wallet_address: String, wallet_path: String, hratls_pubkey: String, hratls_key_path: String, mrsigner: String, mrsigner_key_path: String, } impl super::HumanOutput for AccountData { fn human_cli_print(&self) { if !self.network.is_empty() { println!("The network you are using is: {}", self.network); } if !self.ssh_pubkey.is_empty() { println!("The SSH key used for VMs is: {}", self.ssh_pubkey); } if !self.wallet_path.is_empty() { println!("The address of your DeTEE wallet is {}", self.wallet_address); println!("The balance of your account is {} LP", self.account_balance); if self.locked_funds != 0.0 { println!( "WARNING! {} LP is temporary locked, waiting for a Contract.", self.locked_funds ); } if self.account_balance == 0.0 { println!("Hop on discord to get an airdrop: https://discord.gg/DcfYczAMtD \n") } } if !self.mrsigner.is_empty() { println!("The MRSIGNER for apps is: {}", self.mrsigner); } } } #[derive(Serialize, Deserialize, Debug, Default)] pub struct Config { ssh_key_path: String, #[serde(default = "default_network")] network: String, } fn default_network() -> String { // default to testnet // TODO: remove instruction from docs to set brain_url, since it defaults now "testnet".to_string() } #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Failed to get access to disk: {0}")] DiskAccess(#[from] std::io::Error), #[error("Parsing of the yaml config file failed: {0}")] YamlFormat(#[from] serde_yaml::Error), #[error("The private key of the DeTEE account got corrupted.")] CorruptedWalletKey, #[error("The private ED25519 key used for HRATLS got corrupted: {0}")] CorruptedHratlsKey(String), #[error("The MRSIGNER key (used for signing apps) got corrupted: {0}")] CorruptedMrSigner(String), #[error("Failed to generate key using openssl: {0}")] Openssl(String), #[error{"Failed to retrive/download the artefact"}] ArtefactError, #[error{"SSH key not defined. Run `detee-cli account` for more info."}] SshKeyNoDefined, #[error{"RSA Error: {0}"}] RSAError(#[from] openssl::error::ErrorStack), #[error{"Internal CLI error: {0}"}] InternalError(String), #[error(transparent)] BrainConnection(#[from] tonic::transport::Error), } impl Config { fn save_to_disk(&self) -> Result<(), Error> { let path = Self::config_path()?; debug!("Writing config file to disk at {}", path); let mut file = File::create(path)?; file.write_all(serde_yaml::to_string(self)?.as_bytes())?; Ok(()) } pub fn home_dir() -> String { match std::env::var("HOME") { Ok(path) => path, Err(_) => { // TODO: considering lowering log priority from warn to debug. warn!( "Could not find HOME env variable. Config will get saved to /opt/.detee/cli/" ); "/opt".to_string() } } } pub fn logs_dir() -> Result { let dir = Self::home_dir() + ("/.detee/cli/logs"); if !Path::new(&dir).exists() { warn!("Could not config dir. Creating {dir}"); std::fs::create_dir_all(dir.clone())?; } Ok(dir) } pub fn verify_and_install_artefacts(file_hash: &str) -> Result<(), Error> { use std::fs; let artefacts_dir = Config::artefacts_dir()? + "/"; let file_path = Path::new(&artefacts_dir).join(file_hash); if file_path.exists() { log::debug!("Artefact '{}' already exists", file_hash); return Ok(()); } log::debug!("Artefact '{}' not found. Proceeding to download...", file_hash); let url = format!("https://registry.detee.ltd/{}", file_hash); let response = match reqwest::blocking::get(&url) { Ok(resp) => resp, Err(err) => { log::error!("Failed to download artifact: {}", err); return Err(Error::ArtefactError); } }; if !response.status().is_success() { log::error!("Failed to download artifact: server responded with {}", response.status()); return Err(Error::ArtefactError); } let content = match response.bytes() { Ok(bytes) => bytes, Err(err) => { log::error!("Error reading artifact data from response: {}", err); return Err(Error::ArtefactError); } }; if let Err(e) = fs::write(&file_path, &content) { log::error!("Error writing artifact to file: {}", e); return Err(Error::ArtefactError); } log::debug!("Downloaded atrifact {}", file_hash); Ok(()) } pub fn artefacts_dir() -> Result { let dir = Self::home_dir() + "/.detee/artefacts"; if !Path::new(&dir).exists() { info!("Creating {dir}"); warn!("Since {dir} does not exist, also assume meaurement will fail cause you need the artefacts."); std::fs::create_dir_all(dir.clone())?; } Ok(dir) } pub fn vm_uuid_list_path() -> Result { let dir = Self::home_dir() + ("/.detee/cli/vms"); if !Path::new(&dir).exists() { std::fs::create_dir_all(dir.clone())?; } Ok(dir + "/uuid_list") } pub fn cli_dir_path() -> Result { let dir = Self::home_dir() + ("/.detee/cli"); if !Path::new(&dir).exists() { warn!("Could not config dir. Creating {dir}"); std::fs::create_dir_all(dir.clone())?; } Ok(dir) } fn config_path() -> Result { let config_path = Self::cli_dir_path()? + ("/cli-config.yaml"); Ok(config_path) } fn detee_wallet_key_path() -> Result { let config_path = Self::cli_dir_path()? + ("/secret_detee_wallet_key"); Ok(config_path) } fn load_config_from_file() -> Result { Ok(serde_yaml::from_str(&std::fs::read_to_string(Self::config_path()?)?)?) } pub fn init_config() -> Self { // TODO: create if it does not exist let config = match Self::load_config_from_file() { Ok(config) => config, Err(e) => { log::error!("Could not load config due to error: {e}"); eprintln!("Config file not found. Creating new config file!"); let config = Self::default(); if let Err(e) = config.save_to_disk() { log::error!("Could not save config to disk: {e}"); panic!("Could not initialize config."); }; config } }; config } fn create_wallet_key() -> Result<(), Error> { use rand::rngs::OsRng; let mut csprng = OsRng; let key_path = Self::detee_wallet_key_path()?; if Path::new(&key_path).exists() { log::debug!("Found SNP admin key at {key_path}"); return Ok(()); } let account_key = SigningKey::generate(&mut csprng); let mut account_file = File::create(key_path)?; account_file.write_all(bs58::encode(account_key.to_bytes()).into_string().as_bytes())?; Ok(()) } fn load_wallet_key() -> Result { Self::create_wallet_key()?; Ok(SigningKey::from_bytes( &bs58::decode(std::fs::read_to_string(Self::detee_wallet_key_path()?)?.trim()) .into_vec() .map_err(|_| Error::CorruptedWalletKey)? .try_into() .map_err(|_| Error::CorruptedWalletKey)?, )) } pub fn get_detee_wallet() -> Result { Ok(bs58::encode(Self::load_wallet_key()?.verifying_key().to_bytes()).into_string()) } pub fn try_sign_message(message: &str) -> Result { use ed25519_dalek::Signer; let key = Self::load_wallet_key()?; Ok(bs58::encode(key.sign(message.as_bytes()).to_bytes()).into_string()) } fn try_sign_file(&self, path: &str) -> Result { use ed25519_dalek::Signer; let file_bytes = std::fs::read(path)?; let key = Self::load_wallet_key()?; Ok(bs58::encode(key.sign(&file_bytes).to_bytes()).into_string()) } pub fn sign_file(&self, path: &str) { match self.try_sign_file(path) { Ok(s) => println!("{s}"), Err(e) => { log::error!("coult not sign file due to error: {e}"); std::process::exit(1); } } } pub fn set_ssh_pubkey_path(ssh_pubkey_path: &str) { let mut config = Self::init_config(); match std::fs::read_to_string(ssh_pubkey_path) { Ok(content) => { info!("Found the file {ssh_pubkey_path}"); info!("The file contains the following data:\n{content}"); info!("This will be used as your public key."); } Err(e) => { log::error!("Opening {ssh_pubkey_path} returned an error: {e:?}"); warn!("Cancelling operation!"); return; } }; config.ssh_key_path = ssh_pubkey_path.to_string(); if let Err(e) = config.save_to_disk() { log::error!("Could not save new SSH key path to config: {e}"); } } pub fn get_ssh_pubkey(&self) -> Result { if self.ssh_key_path.is_empty() { return Err(Error::SshKeyNoDefined); } Ok(self.ssh_key_path.clone()) } pub fn get_root_ca_path() -> Result { Ok(Self::home_dir() + "/.detee/certs/root_ca.pem") } pub fn get_brain_info() -> (String, String) { match Self::init_config().network.as_str() { "staging" => ("https://159.65.58.38:31337".to_string(), "staging-brain".to_string()), _ => ("https://164.92.249.180:31337".to_string(), "testnet-brain".to_string()), } } pub async fn get_brain_channel() -> Result { let (brain_url, brain_san) = Self::get_brain_info(); use hyper_rustls::HttpsConnectorBuilder; use rustls::pki_types::pem::PemObject; use rustls::pki_types::CertificateDer; use rustls::{ClientConfig, RootCertStore}; let mut detee_root_ca_store = RootCertStore::empty(); detee_root_ca_store .add(CertificateDer::from_pem_file(Config::get_root_ca_path()?).map_err(|e| { Error::InternalError(format!("Could not parse PEM certificate: {e}")) })?) .unwrap(); let client_tls_config = ClientConfig::builder() .with_root_certificates(detee_root_ca_store) .with_no_client_auth(); let connector = HttpsConnectorBuilder::new() .with_tls_config(client_tls_config) .https_only() .with_server_name_resolver(hyper_rustls::FixedServerNameResolver::new( brain_san.clone().try_into().map_err(|e| { Error::InternalError(format!( "Could not parse {brain_san} into domain resolver: {e}" )) })?, )) .enable_http2() .build(); Ok(tonic::transport::Channel::from_shared(brain_url.to_string()) .map_err(|e| { Error::InternalError(format!( "Could not parse {brain_san} into domain resolver: {e}" )) })? .connect_with_connector(connector) .await?) } pub fn set_network(mut network: &str) { if network != "staging" && network != "testnet" { log::error!( "The network {network} is not officially supported. Defaulting to testnet!" ); network = "testnet"; } let mut config = Self::init_config(); info!("Setting the network to {network}"); config.network = network.to_string(); if let Err(e) = config.save_to_disk() { log::error!("Could not save new brain URL to config: {e}"); } } pub fn get_account_data() -> AccountData { let mut account_data = AccountData { ..Default::default() }; let config = Self::init_config(); match Self::config_path() { Ok(path) => account_data.path = path, Err(e) => { println!("Could not read config due to error: {e}"); std::process::exit(1); } } if config.network.is_empty() { log::error!("The network is not set! To configure it, run:"); eprintln!(" detee-cli account network testnet"); } else { account_data.network = config.network; } if config.ssh_key_path.is_empty() { log::error!("SSH public key path not set! To configure it, run:"); eprintln!(" detee-cli account ssh-pubkey-path /home/your_user/.ssh/id_ed25519.pub"); } else { account_data.ssh_pubkey = config.ssh_key_path; } match Self::get_detee_wallet() { Ok(key) => { account_data.wallet_address = key.to_string(); match block_on(general::grpc::get_balance(&key)) { Ok(account) => { account_data.account_balance = account.balance as f64 / 1_000_000_000.0; account_data.locked_funds = account.tmp_locked as f64 / 1_000_000_000.0; } Err(e) => log::error!("Could not get account balance due to error: {e}"), } } Err(e) => log::error!("Please report this bug. Could not load admin key: {e}"), } match Self::detee_wallet_key_path() { Ok(path) => account_data.wallet_path = path, Err(_) => log::error!("This error should never happen. Please report this bug."), } match Self::mrsigner_key_path() { Ok(path) => { account_data.mrsigner_key_path = path; match Self::get_mrsigner() { Ok(mrsigner) => account_data.mrsigner = mrsigner, Err(e) => { log::error!("Could not load MRSIGNER key: {e}") } } } Err(e) => log::error!("Please report this bug. Could not get MRSIGNER path: {e}"), } match Self::hratls_key_path() { Ok(path) => { account_data.hratls_key_path = path; match Self::get_hratls_pubkey_hex() { Ok(pubkey) => account_data.hratls_pubkey = pubkey, Err(e) => { log::error!("Could not load HRATLS key: {e}") } } } Err(e) => log::error!("Please report this bug. Could not get HRATLS key path: {e}"), } account_data } } impl Config { pub fn get_hratls_private_key() -> Result { let private_key_path = Self::hratls_key_path()?; if Path::new(&private_key_path).exists() { log::debug!("Found HRaTLS private key at {private_key_path}"); return std::fs::read_to_string(private_key_path) .map_err(|e| Error::CorruptedHratlsKey(e.to_string())); } let key = PKey::generate_ed25519()?; let mut key_file = File::create(private_key_path)?; let pem_pkcs8 = key.private_key_to_pem_pkcs8()?; key_file.write_all(&pem_pkcs8)?; String::from_utf8(pem_pkcs8).map_err(|e| Error::CorruptedHratlsKey(e.to_string())) } pub fn get_hratls_pubkey_hex() -> Result { let private_key_pem_str = Self::get_hratls_private_key()?; let private_key = PKey::private_key_from_pem(private_key_pem_str.as_ref())?; let pubkey = private_key.raw_public_key()?; Ok(pubkey.iter().fold(String::new(), |acc, x| acc + &format!("{:02X?}", x))) } pub fn hratls_key_path() -> Result { Ok(Self::cli_dir_path()? + ("/hratls_private_key.pem")) } } impl Config { pub fn get_mrsigner() -> Result { let mut signing_key_mod = Self::get_mrsigner_rsa_key()?.n().to_vec(); signing_key_mod.reverse(); // make it little endian // TODO: double check if hasher can actually fail let mut hasher = Hasher::new(MessageDigest::sha256()).unwrap(); hasher.update(&signing_key_mod).unwrap(); let mr_signer_raw = hasher.finish().unwrap(); let mut mr_signer = [0u8; 32]; mr_signer.copy_from_slice(&mr_signer_raw[..32]); Ok(mr_signer.iter().fold(String::new(), |acc, x| acc + &format!("{:02X?}", x))) } fn get_mrsigner_rsa_key() -> Result, Error> { let signing_key_pem_str = Self::create_mrsigner_rsa_key()?; Rsa::private_key_from_pem(signing_key_pem_str.as_ref()) .map_err(|e| Error::CorruptedMrSigner(e.to_string())) } fn create_mrsigner_rsa_key() -> Result { let signing_key_path = Self::mrsigner_key_path()?; if Path::new(&signing_key_path).exists() { log::debug!("Found signing_key at {signing_key_path}"); return std::fs::read_to_string(signing_key_path) .map_err(|e| Error::CorruptedMrSigner(e.to_string())); } let key = Rsa::generate_with_e(3072, BigNum::from_u32(3)?.as_ref())?; let mut key_file = File::create(signing_key_path)?; let pem_pkcs8 = key.private_key_to_pem()?; key_file.write_all(&pem_pkcs8)?; String::from_utf8(pem_pkcs8).map_err(|e| Error::CorruptedMrSigner(e.to_string())) } pub fn mrsigner_key_path() -> Result { Ok(Self::cli_dir_path()? + ("/app_signing_key.pem")) } } impl Config { pub fn app_uuid_list_path() -> Result { let dir = Self::home_dir() + ("/.detee/cli/apps"); if !Path::new(&dir).exists() { std::fs::create_dir_all(dir.clone())?; } Ok(dir + "/uuid_list") } }