use crate::*; use age::secrecy::Secret; use std::io::{Read, Write}; /// define universal dividends subcommands #[derive(Clone, Default, Debug, clap::Parser)] pub enum Subcommand { #[default] /// List available keys List, /// Show where vault stores secret Where, /// Generate a mnemonic Generate, /// Import mnemonic with interactive prompt Import, } // encrypt input with passphrase fn encrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::EncryptError> { let encryptor = age::Encryptor::with_user_passphrase(Secret::new(passphrase)); let mut encrypted = vec![]; let mut writer = encryptor.wrap_output(age::armor::ArmoredWriter::wrap_output( &mut encrypted, age::armor::Format::AsciiArmor, )?)?; writer.write_all(input)?; writer.finish().and_then(|armor| armor.finish())?; Ok(encrypted) } // decrypt cypher with passphrase fn decrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::DecryptError> { let age::Decryptor::Passphrase(decryptor) = age::Decryptor::new(age::armor::ArmoredReader::new(input))? else { unimplemented!() }; let mut decrypted = vec![]; let mut reader = decryptor.decrypt(&Secret::new(passphrase.to_owned()), None)?; reader.read_to_end(&mut decrypted)?; Ok(decrypted) } /// handle ud commands pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { // match subcommand match command { Subcommand::List => { if let Ok(entries) = std::fs::read_dir(data.project_dir.data_dir()) { println!("available keys:"); entries.for_each(|e| println!("{}", e.unwrap().file_name().to_str().unwrap())); } else { println!("could not read project dir"); } } Subcommand::Where => { println!("{}", data.project_dir.data_dir().to_str().unwrap()); } Subcommand::Generate => { // TODO allow custom word count let mnemonic = bip39::Mnemonic::generate(12).unwrap(); println!("{mnemonic}"); } Subcommand::Import => { let mnemonic = rpassword::prompt_password("Mnemonic: ")?; println!("Enter password to protect the key"); let password = rpassword::prompt_password("Password: ")?; let address = store_mnemonic(&data, &mnemonic, password)?; println!("Stored secret for {address}"); } }; Ok(()) } /// store mnemonic protected with password pub fn store_mnemonic( data: &Data, mnemonic: &str, password: String, ) -> Result<AccountId, GcliError> { // check validity by deriving keypair let keypair = pair_from_str(mnemonic)?; let address = keypair.public(); // write encrypted mnemonic in file identified by pubkey let path = data.project_dir.data_dir().join(address.to_string()); let mut file = std::fs::File::create(path)?; file.write_all(&encrypt(mnemonic.as_bytes(), password).map_err(|e| anyhow!(e))?[..])?; Ok(keypair.public().into()) } /// try get secret in keystore pub fn try_fetch_secret(data: &Data, address: AccountId) -> Result<Option<String>, GcliError> { let path = data.project_dir.data_dir().join(address.to_string()); if path.exists() { println!("Enter password to unlock account {address}"); let password = rpassword::prompt_password("Password: ")?; let mut file = std::fs::OpenOptions::new().read(true).open(path)?; let mut cypher = vec![]; file.read_to_end(&mut cypher)?; let secret = decrypt(&cypher, password).map_err(|e| GcliError::Input(e.to_string()))?; let secretstr = String::from_utf8(secret).map_err(|e| anyhow!(e))?; Ok(Some(secretstr)) } else { Ok(None) } } // test that armored encryption/decryption work as intended #[test] fn test_encrypt_decrypt() { let plaintext = b"Hello world!"; let passphrase = "this is not a good passphrase".to_string(); let encrypted = encrypt(plaintext, passphrase.clone()).unwrap(); let decrypted = decrypt(&encrypted, passphrase).unwrap(); assert_eq!(decrypted, plaintext); }