use base64::Engine; use base64::prelude::BASE64_STANDARD; use chrono::{DateTime, Utc}; use ed25519_dalek::{Signer, SigningKey}; use rand::TryRng; use rand::distr::{Alphanumeric, SampleString}; use rand::rngs::SysRng; use std::fs::File; use std::io; use std::io::{BufReader, Read, Write}; use ureq::Error::StatusCode; use ureq::SendBody; use crate::config::{read_config, ClientConfig}; struct AuthExchange { nonce: String, timestamp: i64, signature: String, } fn generate_nonce() -> String { Alphanumeric.sample_string(&mut rand::rng(), 32) } fn create_auth(config: &ClientConfig) -> AuthExchange { let mut buffer = [0u8; 44]; File::open(config.private_key.clone()) .expect("Failed to open private key file") .read_exact(&mut buffer) .expect("Failed to read private key file"); let decoded = BASE64_STANDARD .decode(buffer) .expect("Malformed private key"); let signing_key = SigningKey::from_bytes(&decoded.try_into().expect("Malformed private key")); let nonce = generate_nonce(); let timestamp = Utc::now().timestamp(); let signature = BASE64_STANDARD.encode( signing_key .sign(format!("{}{}", nonce, timestamp).as_bytes()) .to_bytes(), ); AuthExchange { nonce, timestamp, signature, } } pub fn gen_pubkey() { let mut buffer = [0u8; 44]; io::stdin().read_exact(&mut buffer).unwrap(); let decoded = BASE64_STANDARD .decode(buffer) .expect("Malformed private key"); let signing_key = SigningKey::from_bytes(&decoded.try_into().expect("Malformed private key")); let verifying_key = signing_key.verifying_key(); let encoded = BASE64_STANDARD.encode(verifying_key); println!("{}", encoded); } pub fn gen_privkey() { let mut seed = [0u8; 32]; SysRng .try_fill_bytes(&mut seed) .expect("Failed to generate random seed"); let signing_key = SigningKey::from_bytes(&seed); let encoded = BASE64_STANDARD.encode(signing_key.to_bytes()); println!("{}", encoded); } pub fn upload_vault() { let config: ClientConfig = read_config().expect("Failed to read config"); let server_url = &config.server_url; let file = File::open(&config.db_path).expect("Failed to open database file"); let reader = BufReader::new(file); let auth = create_auth(&config); let code = ureq::post(format!("{}/vaults", server_url)) .content_type("application/x-keepass2") .header("X-Signature", auth.signature) .header("X-Nonce", auth.nonce) .header("X-Timestamp", auth.timestamp) .send(SendBody::from_owned_reader(reader)) .expect("Failed to send file") .status(); if code == 200 { println!("Successfully uploaded vault file"); } else { println!("Failed to upload vault file: {}", code); } } pub fn ls_vaults(long: bool) { let config: ClientConfig = read_config().expect("Failed to read config"); let server_url = &config.server_url; let auth = create_auth(&config); let result = ureq::get(format!("{}/vaults", server_url)) .header("X-Signature", auth.signature) .header("X-Nonce", auth.nonce) .header("X-Timestamp", auth.timestamp) .call() .expect("Failed to get from server") .body_mut() .read_to_string() .expect("Failed to read server response"); let files = result.split("\n"); for file in files { if file.is_empty() { break; } let mut parts = file.split("_"); let vault_id = parts.next().unwrap(); let timestamp: i64 = parts .next() .expect("Malformed response") .parse() .expect("Malformed response"); let dt = DateTime::from_timestamp(timestamp, 0).expect("Failed to parse timestamp"); if long { println!( "{} \t [Created: {}]", vault_id, dt.format("%m-%d-%Y %H:%M:%S") ); } else { println!( "{} \t [Created: {}]", &vault_id[0..8], dt.format("%m-%d-%Y %H:%M:%S") ); } } } pub fn download_vault(id: String, install: bool, file: String) { let config: ClientConfig = read_config().expect("Failed to read config"); let server_url = &config.server_url; let auth = create_auth(&config); let response = ureq::get(format!("{}/vaults/{}", server_url, id)) .header("X-Signature", auth.signature) .header("X-Nonce", auth.nonce) .header("X-Timestamp", auth.timestamp) .call(); match response { Ok(mut response) => { let result = response .body_mut() .read_to_vec() .expect("Failed to read server response"); let mut file = File::create(if install { config.db_path } else { file }) .expect("Failed to open database file"); file.write_all(&result) .expect("Failed to write to database file"); } Err(error) => match error { StatusCode(stat) => { if stat == 300 { println!("There are multiple candidates available for download."); println!("** Rerun the command with a long ID.") } else { println!("An internal server error occurred!"); } } _ => eprintln!("An error occurred: {}", error), }, } }