Skip to content
Snippets Groups Projects
vault.rs 3.73 KiB
Newer Older
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);
}