mod display; use crate::commands::cesium::compute_g1v1_public_key; use crate::entities::vault_account; use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId}; use crate::*; use age::secrecy::Secret; use sea_orm::ActiveValue::Set; use sea_orm::{ConnectionTrait, TransactionTrait}; use sea_orm::{DbErr, ModelTrait}; use sp_core::crypto::AddressUri; use std::cell::RefCell; use std::io::{Read, Write}; use std::path::PathBuf; use std::rc::Rc; /// vault subcommands #[derive(Clone, Debug, clap::Parser)] pub enum Subcommand { /// List available SS58 Addresses in the vault #[clap(subcommand)] List(ListChoice), /// Use specific SS58 Address (changes the config Address) Use { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, }, /// Generate a mnemonic Generate, /// Import key from (substrate uri) or other format with interactive prompt #[clap( long_about = "Import key from (substrate uri) or other format with interactive prompt.\n\ \n\ This will create a <Base> account in the vault for the provided/computed Substrate URI \n\ and associated SS58 Address.\n\ \n\ If using default format (or specifically \"substrate\") a derivation path is supported\n\ in the substrate uri value." )] Import { /// Secret key format (substrate, seed, g1v1) #[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)] secret_format: SecretFormat, }, /// Add a derivation to an existing SS58 Address #[clap(long_about = "Add a derivation to an existing SS58 Address.\n\ \n\ Only \"sr25519\" crypto scheme is supported for derivations.\n\ \n\ Use command `vault list base` to see available <Base> account and their crypto scheme\n\ And then use command 'vault list for' to find all accounts linked to that <Base> account.")] #[clap(alias = "deriv")] #[clap(alias = "derivation")] Derive { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, }, /// Give a meaningful name to an SS58 Address in the vault Rename { /// SS58 Address address: AccountId, }, /// Remove an SS58 Address from the vault together with its linked derivations #[clap(long_about = "Remove an SS58 Address from the vault\n\ \n\ If a <Base> Address is given it will also remove the saved key")] Remove { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, }, /// Inspect a vault entry, retrieving its Substrate URI (will provide more data in a future version) Inspect { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, }, /// (deprecated) List available key files (needs to be migrated with command `vault migrate` in order to use them) #[deprecated( note = "Should be removed in a future version when db persistence of vault is present for a while" )] ListFiles, /// (deprecated) Migrate old key files into db (will have to provide password for each key) #[deprecated( note = "Should be removed in a future version when db persistence of vault is present for a while" )] Migrate, /// Show where vault db (or old keys) is stored Where, } #[derive(Clone, Default, Debug, clap::Parser)] pub enum ListChoice { /// List all <Base> SS58 Addresses and their linked derivations in the vault #[default] All, /// List <Base> and Derivation SS58 Addresses linked to the selected one For { #[clap(flatten)] address_or_vault_name: AddressOrVaultNameGroup, }, /// List all <Base> SS58 Addresses in the vault Base, } #[derive(Debug, clap::Args, Clone)] #[group(required = true, multiple = false)] pub struct AddressOrVaultNameGroup { /// SS58 Address #[clap(short)] address: Option<AccountId>, /// Name of an SS58 Address in the vault #[clap(short = 'v')] name: Option<String>, } pub struct VaultDataToImport { secret_format: SecretFormat, secret_suri: String, key_pair: KeyPair, } // encrypt input with passphrase pub 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 pub 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 vault commands pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { let db = data.connect_db(); // match subcommand match command { Subcommand::List(choice) => match choice { ListChoice::All => { let all_account_tree_node_hierarchies = vault_account::fetch_all_base_account_tree_node_hierarchies(db).await?; let table = display::compute_vault_accounts_table(&all_account_tree_node_hierarchies)?; println!("available SS58 Addresses:"); println!("{table}"); } ListChoice::Base => { let base_account_tree_nodes = vault_account::fetch_only_base_account_tree_nodes(db).await?; let table = display::compute_vault_accounts_table(&base_account_tree_nodes)?; println!("available <Base> SS58 Addresses:"); println!("{table}"); } ListChoice::For { address_or_vault_name, } => { let account_tree_node = retrieve_account_tree_node(db, address_or_vault_name).await?; let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node); let table = display::compute_vault_accounts_table(&[base_account_tree_node])?; println!( "available SS58 Addresses linked to {}:", account_tree_node.borrow().account ); println!("{table}"); } }, Subcommand::ListFiles => { let vault_key_addresses = fetch_vault_key_addresses(&data).await?; let table = display::compute_vault_key_files_table(&vault_key_addresses)?; println!("available key files (needs to be migrated with command `vault migrate` in order to use them):"); println!("{table}"); } Subcommand::Use { address_or_vault_name, } => { let account = retrieve_vault_account(db, address_or_vault_name).await?; println!("Using: {}", account); let updated_cfg = conf::Config { address: Some(account.address.0), ..data.cfg }; //This updated configuration will be picked up with next GCli execution conf::save(&updated_cfg); } Subcommand::Generate => { // TODO allow custom word count let mnemonic = bip39::Mnemonic::generate(12).unwrap(); println!("{mnemonic}"); } Subcommand::Import { secret_format } => { let vault_data_for_import = prompt_secret_and_compute_vault_data_to_import(secret_format)?; //Extra check for SecretFormat::G1v1 (old cesium) - showing the G1v1 cesium public key for confirmation if secret_format == SecretFormat::G1v1 { println!( "The G1v1 public key for the provided secret is: '{}'", compute_g1v1_public_key(&vault_data_for_import.key_pair)? ); let confirmed = inputs::confirm_action("Is it the correct one (if not, you should try again to input G1v1 id/password) ?".to_string())?; if !confirmed { return Ok(()); } } let txn = db.begin().await?; println!(); let _account = create_base_account_for_vault_data_to_import(&txn, &vault_data_for_import, None) .await?; txn.commit().await?; println!("Change done"); } Subcommand::Derive { address_or_vault_name, } => { let account_tree_node_to_derive = retrieve_account_tree_node(db, address_or_vault_name).await?; let account_to_derive = account_tree_node_to_derive.borrow().account.clone(); let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node_to_derive); let base_account = &base_account_tree_node.borrow().account.clone(); if base_account.crypto_scheme.is_none() { return Err(GcliError::DatabaseError(DbErr::Custom(format!("Crypto scheme is not set for the base account:{base_account} - should never happen")))); } if let Some(crypto_scheme) = base_account.crypto_scheme { if CryptoScheme::from(crypto_scheme) == CryptoScheme::Ed25519 { println!( "Only \"{}\" crypto scheme is supported for derivations.", Into::<&str>::into(CryptoScheme::Sr25519), ); println!(); println!( "Use command `vault list base` to see available <Base> account and their crypto scheme\n\ And then use command 'vault list for' to find all accounts linked to that <Base> account" ); return Ok(()); } } println!("Adding derivation to: {account_to_derive}"); let base_parent_hierarchy_account_tree_node_to_derive = vault_account::get_base_parent_hierarchy_account_tree_node( &account_tree_node_to_derive, ); let parent_hierarchy_table_account_to_derive = display::compute_vault_accounts_table(&[ base_parent_hierarchy_account_tree_node_to_derive, ])?; println!(); println!("Its parent hierarchy is this:"); println!("{parent_hierarchy_table_account_to_derive}"); println!(); println!("The linked <Base> account is {base_account}"); println!("Enter password to decrypt the <Base> account key"); let password = inputs::prompt_password()?; let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node( &account_tree_node_to_derive, password, )?; println!(); let derivation_path = inputs::prompt_vault_derivation_path()?; let derivation_secret_suri = format!("{account_to_derive_secret_suri}{derivation_path}"); let derivation_keypair = compute_keypair(CryptoScheme::Sr25519, &derivation_secret_suri)?; let derivation_address: String = derivation_keypair.address().to_string(); let txn = db.begin().await?; println!(); let _derivation = create_derivation_account( &txn, &derivation_address, &derivation_path, &account_to_derive.address.to_string(), ) .await?; txn.commit().await?; println!("Change done"); } Subcommand::Rename { address } => { let account = vault_account::find_by_id(db, &DbAccountId::from(address.clone())).await?; if account.is_none() { println!("No vault entry found for address:'{address}'"); println!("You might want to import it first with 'vault import'"); return Ok(()); } let account = account.unwrap(); println!( "Current name for address:'{address}' is {:?}", &account.name ); println!("Enter new name for address (leave empty to remove the name)"); let name = inputs::prompt_vault_name_and_check_availability(db, account.name.as_ref()).await?; let _account = vault_account::update_account_name(db, account, name.as_ref()).await?; println!("Rename done"); } Subcommand::Remove { address_or_vault_name, } => { let account_tree_node_to_delete = retrieve_account_tree_node(db, address_or_vault_name).await?; let txn = db.begin().await?; let account_to_delete = account_tree_node_to_delete.borrow().account.clone(); let address_to_delete = account_tree_node_to_delete.borrow().account.address.clone(); //If account to delete has children; also delete all linked derivations if !account_tree_node_to_delete.borrow().children.is_empty() { let table = display::compute_vault_accounts_table(&[account_tree_node_to_delete.clone()])?; println!("All addresses linked to: {account_to_delete}"); println!("{table}"); println!( "This {} account has {} addresses in total", account_to_delete.account_type(), vault_account::count_accounts_in_account_tree_node_and_children( &account_tree_node_to_delete ) ); let confirmation_message = if account_to_delete.is_base_account() { "Are you sure you want to delete it along with the saved key?" } else { "Are you sure you want to delete it?" }; let confirmed = inputs::confirm_action(confirmation_message.to_string())?; if !confirmed { return Ok(()); } for account_to_delete in vault_account::extract_accounts_depth_first_from_account_tree_node( &account_tree_node_to_delete, )? { let delete_result = account_to_delete.delete(&txn).await?; println!("Deleting {} address", delete_result.rows_affected); } } else { let delete_result = account_to_delete.delete(&txn).await?; println!("Deleting {} address", delete_result.rows_affected); } txn.commit().await?; println!("Done removing address:'{address_to_delete}'"); } Subcommand::Inspect { address_or_vault_name, } => { let account_tree_node_to_derive = retrieve_account_tree_node(db, address_or_vault_name).await?; println!("Enter password to decrypt the <Base> account key"); let password = inputs::prompt_password()?; let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node( &account_tree_node_to_derive, password, )?; println!("Substrate URI: '{account_to_derive_secret_suri}'") } Subcommand::Migrate => { println!("Migrating existing key files to db"); let vault_key_addresses = fetch_vault_key_addresses(&data).await?; let table = display::compute_vault_key_files_table(&vault_key_addresses)?; println!("available key files to possibly migrate:"); println!("{table}"); for address in vault_key_addresses { //Check if we already have a vault_derivation for that address let existing_account = vault_account::find_by_id(db, &DbAccountId::from_str(&address)?).await?; if existing_account.is_some() { //Already migrated continue; } println!(); println!("Trying to migrate key {address}"); let vault_data_from_file = match try_fetch_vault_data_from_file(&data, &address) { Ok(Some(vault_data)) => vault_data, Ok(None) => { println!("No vault entry file found for address {address}"); continue; } Err(e) => { println!("Error while fetching vault data for address {address}: {e}"); println!("Continuing to next one"); continue; } }; let vault_data_to_import = VaultDataToImport { secret_format: vault_data_from_file.secret_format, secret_suri: vault_data_from_file.secret, key_pair: vault_data_from_file.key_pair, }; let txn = db.begin().await?; let account = create_base_account_for_vault_data_to_import( &txn, &vault_data_to_import, Some(&vault_data_from_file.password), ) .await; match account { Ok(_account) => { txn.commit().await?; println!("Change done"); } Err(error) => { println!("Error occurred: {error}"); println!("Continuing to next key"); } } } println!("Migration done"); } Subcommand::Where => { println!("{}", data.project_dir.data_dir().to_str().unwrap()); } }; Ok(()) } /// Method used to separate vault `name` part from optional `derivation` part in computed names that can be provided by users in the different `vault` commands using `AddressOrVaultNameGroup` fn parse_vault_name_and_derivation_path_from_user_input( user_input_name: String, ) -> Result<(String, Option<String>), GcliError> { if user_input_name.contains("/") { user_input_name.find("/").map_or( Err(GcliError::Input("Invalid format".to_string())), |idx| { let (prefix, derivation_path) = user_input_name.split_at(idx); Ok((prefix.to_string(), Some(derivation_path.to_string()))) }, ) } else { Ok((user_input_name, None)) } } /// Method that can be used to parse a Substrate URI (which can also be only a derivation path) /// /// Does some internal verification (done by sp_core::address_uri::AddressUri) /// /// It extracts the (optional) `phrase` and the (optional) recomposed full `derivation path` /// /// It also checks if a derivation `password` was provided and returns an error if one was found pub fn parse_prefix_and_derivation_path_from_suri( raw_string: String, ) -> Result<(Option<String>, Option<String>), GcliError> { let address_uri = AddressUri::parse(&raw_string).map_err(|e| GcliError::Input(e.to_string()))?; if let Some(pass) = address_uri.pass { return Err(GcliError::Input(format!( "Having a password in the derivation path is not supported (password:'{}')", pass ))); } let full_path = if address_uri.paths.is_empty() { None } else { Some("/".to_owned() + &address_uri.paths.into_iter().collect::<Vec<_>>().join("/")) }; Ok((address_uri.phrase.map(|s| s.to_string()), full_path)) } fn map_secret_format_to_crypto_scheme(secret_format: SecretFormat) -> CryptoScheme { match secret_format { SecretFormat::Seed => CryptoScheme::Sr25519, SecretFormat::Substrate => CryptoScheme::Sr25519, SecretFormat::Predefined => CryptoScheme::Sr25519, SecretFormat::G1v1 => CryptoScheme::Ed25519, } } /// This method will scan files in the data directory and return the addresses of the vault keys found #[deprecated( note = "Should be removed in a future version when db persistence of vault is present for a while" )] async fn fetch_vault_key_addresses(data: &Data) -> Result<Vec<String>, GcliError> { let mut entries = std::fs::read_dir(data.project_dir.data_dir())? .map(|res| res.map(|e| e.path())) .collect::<Result<Vec<_>, std::io::Error>>()?; // To have consistent ordering entries.sort(); let mut vault_key_addresses: Vec<String> = vec![]; entries.iter().for_each(|dir_path| { let filename = dir_path.file_name().unwrap().to_str().unwrap(); // If potential_address is a valid AccountId if AccountId::from_str(filename).is_ok() { vault_key_addresses.push(filename.to_string()); } }); Ok(vault_key_addresses) } pub async fn retrieve_vault_account_for_name<C>( db: &C, name_input: &String, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, { let account_tree_node = retrieve_account_tree_node_for_name(db, name_input).await?; //Need this extra step to avoid borrowing issues let account = account_tree_node.borrow().account.clone(); Ok(account) } pub async fn retrieve_account_tree_node<C>( db: &C, address_or_vault_name: AddressOrVaultNameGroup, ) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError> where C: ConnectionTrait, { let account_tree_node = if let Some(name_input) = &address_or_vault_name.name { retrieve_account_tree_node_for_name(db, name_input).await? } else if let Some(address) = &address_or_vault_name.address { let base_account_tree_node = vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( db, &address.to_string(), ) .await?; let account_tree_node_for_address = vault_account::get_account_tree_node_for_address( &base_account_tree_node, &address.to_string(), ); Rc::clone(&account_tree_node_for_address) } else { //Should never happen since clap enforces exactly one of the 2 options return Err(GcliError::Input("No address or name provided".to_string())); }; Ok(account_tree_node) } pub async fn retrieve_account_tree_node_for_name<C>( db: &C, name_input: &String, ) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError> where C: ConnectionTrait, { let (name, derivation_path_opt) = parse_vault_name_and_derivation_path_from_user_input(name_input.to_string())?; let account_for_name = vault_account::find_by_name(db, &name).await?; let account_for_name = account_for_name.ok_or(GcliError::Input(format!( "No account found with name:'{name}'" )))?; let base_account_tree_node = vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( db, &account_for_name.address.to_string(), ) .await?; let account_tree_node_for_name = vault_account::get_account_tree_node_for_address( &base_account_tree_node, &account_for_name.address.to_string(), ); Ok(match derivation_path_opt { None => Rc::clone(&account_tree_node_for_name), Some(path) => { let account_tree_node_for_name_input = vault_account::compute_name_map_for_account_tree_node(&account_tree_node_for_name)? .get(name_input) .cloned() .ok_or(GcliError::Input(format!( "No account found with name:'{name}' and path:'{path}'" )))?; Rc::clone(&account_tree_node_for_name_input) } }) } pub async fn retrieve_vault_account<C>( db: &C, address_or_vault_name: AddressOrVaultNameGroup, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, { let account_tree_node = retrieve_account_tree_node(db, address_or_vault_name).await?; //Need this extra step to avoid borrowing issues let account = account_tree_node.borrow().account.clone(); Ok(account) } fn create_vault_data_to_import<F, P>( secret_format: SecretFormat, prompt_fn: F, ) -> Result<VaultDataToImport, GcliError> where F: Fn() -> (String, P), P: Into<KeyPair>, { let (secret, pair) = prompt_fn(); let key_pair = pair.into(); Ok(VaultDataToImport { secret_format, secret_suri: secret, key_pair, }) } fn prompt_secret_and_compute_vault_data_to_import( secret_format: SecretFormat, ) -> Result<VaultDataToImport, GcliError> { match secret_format { SecretFormat::Substrate => { create_vault_data_to_import(secret_format, prompt_secret_substrate_and_compute_keypair) } SecretFormat::Seed => { create_vault_data_to_import(secret_format, prompt_seed_and_compute_keypair) } SecretFormat::G1v1 => { create_vault_data_to_import(secret_format, prompt_secret_cesium_and_compute_keypair) } SecretFormat::Predefined => { create_vault_data_to_import(secret_format, prompt_predefined_and_compute_keypair) } } } /// Creates a `base` vault account for vault_data provided and returns it /// /// Does extra checks and asks for user input in case the address is already present in the vault. /// /// Can request password and (optional) name to the user at the proper time /// /// Typically used for `vault import|migrate` commands pub async fn create_base_account_for_vault_data_to_import<C>( db_tx: &C, vault_data: &VaultDataToImport, password: Option<&String>, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, { let address_to_import = vault_data.key_pair.address().to_string(); println!("Trying to import for SS58 address :'{}'", address_to_import); println!(); let vault_account = if let Some(existing_vault_account) = vault_account::find_by_id(db_tx, &DbAccountId::from(address_to_import.clone())).await? { if existing_vault_account.is_base_account() { println!("You are trying to add {address_to_import} as a <Base> account while it already exists as a <Base> account."); println!(); println!("Do you want to:"); println!("1. keep the existing <Base> account and cancel import"); println!("2. overwrite existing <Base> account with the new encrypted key (children will be re-parented)"); } else { // Existing derivation account let account_tree_node_hierarchy = vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( db_tx, &address_to_import, ) .await?; let account_tree_node_for_address = vault_account::get_account_tree_node_for_address( &account_tree_node_hierarchy, &address_to_import, ); let base_parent_hierarchy_account_tree_node = vault_account::get_base_parent_hierarchy_account_tree_node( &account_tree_node_for_address, ); let parent_hierarchy_table = display::compute_vault_accounts_table(&[base_parent_hierarchy_account_tree_node])?; println!("You are trying to add {address_to_import} as a <Base> account"); println!( "but it is already present as `{}` derivation of {} account.", existing_vault_account.path.clone().unwrap(), existing_vault_account.parent.clone().unwrap() ); println!(); println!("Its parent hierarchy is this:"); println!("{parent_hierarchy_table}"); println!(); println!("Do you want to:"); println!("1. keep the existing derivation and cancel import"); println!("2. delete the derivation account and replace it with the new <Base> account (children will be re-parented)"); } let result = inputs::select_action("Your choice?", vec!["1", "2"])?; match result { "2" => { let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?; println!( "(Optional) Enter a name for the vault entry (leave empty to remove the name)" ); let name = inputs::prompt_vault_name_and_check_availability( db_tx, existing_vault_account.name.as_ref(), ) .await?; // Since links are made based on address / parent(address) we can just edit the existing entry and it should be fine let mut vault_account: ActiveModel = existing_vault_account.into(); vault_account.path = Set(None); vault_account.parent = Set(None); vault_account.crypto_scheme = Set(Some( map_secret_format_to_crypto_scheme(vault_data.secret_format).into(), )); vault_account.encrypted_suri = Set(Some(encrypted_suri)); vault_account.name = Set(name.clone()); let updated_vault_account = vault_account::update_account(db_tx, vault_account).await?; println!("Updating vault account {updated_vault_account}"); updated_vault_account } _ => { return Err(GcliError::Input("import canceled".into())); } } } else { //New entry let secret_format = vault_data.secret_format; let encrypted_suri = compute_encrypted_suri(password, vault_data.secret_suri.clone())?; println!("(Optional) Enter a name for the vault entry"); let name = inputs::prompt_vault_name_and_check_availability(db_tx, None).await?; let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format); let base_account = vault_account::create_base_account( db_tx, &address_to_import, name.as_ref(), crypto_scheme, encrypted_suri, ) .await?; println!("Creating <Base> account {base_account}"); base_account }; Ok(vault_account) } /// Creates a `derivation` vault account for data provided and returns it /// /// Does extra checks and asks for user input in case the address is already present in the vault. /// /// Can request (optional) name to the user at the proper time /// /// Typically used for `vault derive` command pub async fn create_derivation_account<C>( db_tx: &C, derivation_address: &String, derivation_path: &String, parent_address: &String, ) -> Result<vault_account::Model, GcliError> where C: ConnectionTrait, { println!("Trying to create derivation with address '{derivation_address}'"); println!(); let vault_account = if let Some(existing_vault_account) = vault_account::find_by_id(db_tx, &DbAccountId::from(derivation_address.clone())).await? { // Existing account println!("You are trying to derive '{derivation_path}' from parent '{parent_address}'"); if existing_vault_account.is_base_account() { println!( "but it is already present as a direct <Base> account '{}'", existing_vault_account.address ); println!("Do you want to:"); println!("1. keep the existing <Base> account and cancel import"); println!("2. delete the existing <Base> account and associated key and replace it with the new derivation account (children will be re-parented)"); } else { //Existing derivation let existing_account_tree_node_hierarchy = vault_account::fetch_base_account_tree_node_hierarchy_unwrapped( db_tx, derivation_address, ) .await?; let existing_account_tree_node_for_address = vault_account::get_account_tree_node_for_address( &existing_account_tree_node_hierarchy, derivation_address, ); let base_parent_hierarchy_existing_account_tree_node = vault_account::get_base_parent_hierarchy_account_tree_node( &existing_account_tree_node_for_address, ); let parent_hierarchy_table_existing_account = display::compute_vault_accounts_table(&[ base_parent_hierarchy_existing_account_tree_node, ])?; println!( "but it is already present as `{}` derivation of '{}' account.", existing_vault_account.path.clone().unwrap(), existing_vault_account.parent.clone().unwrap() ); println!(); println!("Its parent hierarchy is this:"); println!("{parent_hierarchy_table_existing_account}"); println!(); println!("Do you want to:"); println!("1. keep the existing derivation and cancel import"); println!("2. delete the derivation account and replace it with this new derivation (children will be re-parented)"); } let result = inputs::select_action("Your choice?", vec!["1", "2"])?; match result { "2" => { println!( "(Optional) Enter a name for the vault entry (leave empty to remove the name)" ); let name = inputs::prompt_vault_name_and_check_availability( db_tx, existing_vault_account.name.as_ref(), ) .await?; // Since links are made based on address / parent(address) we can just edit the existing entry and it should be fine let mut vault_account: ActiveModel = existing_vault_account.into(); vault_account.path = Set(Some(derivation_path.clone())); vault_account.parent = Set(Some(DbAccountId::from(parent_address.clone()))); vault_account.crypto_scheme = Set(None); vault_account.encrypted_suri = Set(None); vault_account.name = Set(name.clone()); let updated_vault_account = vault_account::update_account(db_tx, vault_account).await?; println!("Updating vault account {updated_vault_account}"); updated_vault_account } _ => { return Err(GcliError::Input("derive canceled".into())); } } } else { println!("(Optional) Enter a name for the vault entry"); let name = inputs::prompt_vault_name_and_check_availability(db_tx, None).await?; let derivation = vault_account::create_derivation_account( db_tx, derivation_address, name.as_ref(), derivation_path, parent_address, ) .await?; println!("Creating derivation account {derivation}"); derivation }; Ok(vault_account) } /// Function will ask for password if not present and compute the encrypted suri fn compute_encrypted_suri( password: Option<&String>, secret_suri: String, ) -> Result<Vec<u8>, GcliError> { let password = match password.cloned() { Some(password) => password, _ => { println!("Enter password to protect the key"); inputs::prompt_password_confirm()? } }; Ok(encrypt(secret_suri.as_bytes(), password).map_err(|e| anyhow!(e))?) } fn get_vault_key_path(data: &Data, vault_filename: &str) -> PathBuf { data.project_dir.data_dir().join(vault_filename) } /// look for different possible paths for vault keys and return both format and path fn find_substrate_vault_key_file(data: &Data, address: &str) -> Result<Option<PathBuf>, GcliError> { let path = get_vault_key_path(data, address); if path.exists() { return Ok(Some(path)); } Ok(None) } /// try to get secret in keystore, prompt for the password and compute the keypair pub async fn try_fetch_key_pair( data: &Data, address: AccountId, ) -> Result<Option<KeyPair>, GcliError> { if let Some(account_tree_node_hierarchy) = vault_account::fetch_base_account_tree_node_hierarchy( data.connect_db(), &address.to_string(), ) .await? { let account_tree_node = vault_account::get_account_tree_node_for_address( &account_tree_node_hierarchy, &address.to_string(), ); println!("(Vault: {})", account_tree_node.borrow().account); let password = inputs::prompt_password()?; let secret_suri = vault_account::compute_suri_account_tree_node(&account_tree_node, password)?; let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node); let base_account = &base_account_tree_node.borrow().account.clone(); let key_pair = compute_keypair(base_account.crypto_scheme.unwrap().into(), &secret_suri)?; //To be safe if address != key_pair.address() { return Err(GcliError::Input(format!( "Computed address {} does not match the expected address {}", key_pair.address(), address ))); } Ok(Some(key_pair)) } else { Ok(None) } } pub fn compute_keypair( crypto_scheme: CryptoScheme, secret_suri: &str, ) -> Result<KeyPair, GcliError> { let key_pair = match crypto_scheme { CryptoScheme::Sr25519 => pair_from_sr25519_str(secret_suri)?.into(), CryptoScheme::Ed25519 => pair_from_ed25519_str(secret_suri)?.into(), }; Ok(key_pair) } pub struct VaultDataFromFile { secret_format: SecretFormat, secret: String, #[allow(dead_code)] path: PathBuf, password: String, key_pair: KeyPair, } /// try to get secret in keystore, prompt for the password and compute the keypair #[deprecated( note = "Should be removed in a future version when db persistence of vault is present for a while" )] pub fn try_fetch_vault_data_from_file( data: &Data, address: &str, ) -> Result<Option<VaultDataFromFile>, GcliError> { if let Some(path) = find_substrate_vault_key_file(data, address)? { println!("Enter password to unlock account {address}"); let password = inputs::prompt_password()?; let mut file = std::fs::OpenOptions::new().read(true).open(path.clone())?; let mut cypher = vec![]; file.read_to_end(&mut cypher)?; let secret_vec = decrypt(&cypher, password.clone()).map_err(|e| GcliError::Input(e.to_string()))?; let secret = String::from_utf8(secret_vec).map_err(|e| anyhow!(e))?; let key_pair = pair_from_sr25519_str(&secret)?.into(); Ok(Some(VaultDataFromFile { secret_format: SecretFormat::Substrate, secret, path, password, key_pair, })) } else { Ok(None) } } #[cfg(test)] mod tests { use super::*; use rstest::rstest; /// 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); } #[rstest] #[case( String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"), Some(String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk" )), Some(String::from("//0")) )] #[case( String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"), Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")), Some(String::from("//0")) )] #[case( String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice" ), Some(String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk" )), Some(String::from("//Alice")) )] #[case( String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Bob/soft1/soft2" ), Some(String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk")), Some(String::from("//Alice//Bob/soft1/soft2")) )] #[case( String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"), Some(String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk" )), None )] #[case( String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")), None )] #[case( String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), Some(String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")), None )] #[case( String::from("someVaultName//Alice"), Some(String::from("someVaultName")), Some(String::from("//Alice")) )] #[case( String::from("someVaultName"), Some(String::from("someVaultName")), None )] fn test_parse_prefix_and_derivation_path_from_suri( #[case] raw_string: String, #[case] expected_prefix: Option<String>, #[case] expected_derivation_path: Option<String>, ) { let (root_secret, derivation_path) = parse_prefix_and_derivation_path_from_suri(raw_string).unwrap(); assert_eq!(expected_prefix, root_secret); assert_eq!(expected_derivation_path, derivation_path); } #[rstest] #[case( String::from("//Alice//Bob/soft1/soft2"), None, Some(String::from("//Alice//Bob/soft1/soft2")) )] #[case(String::from(""), None, None)] #[case(String::from("//0"), None, Some(String::from("//0")))] fn test_parse_prefix_and_derivation_path_from_suri_works_with_empty_prefix_phrase( #[case] raw_string: String, #[case] expected_prefix: Option<String>, #[case] expected_derivation_path: Option<String>, ) { let (root_secret, derivation_path) = parse_prefix_and_derivation_path_from_suri(raw_string).unwrap(); assert_eq!(expected_prefix, root_secret); assert_eq!(expected_derivation_path, derivation_path); } #[rstest] #[case( String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Bob/soft1/soft2///password" ), )] #[case(String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk///password" ))] #[case(String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk///" ))] #[case( String::from( "bottom drive obey lake curtain smoke basket hold race lonely fit walk///password//NotDerivations//Still/password/part" ), )] fn test_parse_prefix_and_derivation_path_from_suri_does_not_allow_password( #[case] raw_string: String, ) { let result = parse_prefix_and_derivation_path_from_suri(raw_string); match result.unwrap_err() { GcliError::Input(err) => { println!("Error message: {}", err); assert!( err.starts_with("Having a password in the derivation path is not supported") ); } other => panic!("Should have been an Input error; got: {:?}", other), } } }