Newer
Older
Nicolas80
committed
use crate::commands::cesium::compute_g1v1_public_key;
use crate::entities::vault_account;
use crate::entities::vault_account::{AccountTreeNode, DbAccountId};
use crate::*;
use age::secrecy::Secret;
Nicolas80
committed
use comfy_table::{Cell, Table};
use sea_orm::ModelTrait;
Nicolas80
committed
use sea_orm::{ConnectionTrait, TransactionTrait};
use sp_core::crypto::AddressUri;
use std::cell::RefCell;
Nicolas80
committed
use std::path::PathBuf;
use std::rc::Rc;
Nicolas80
committed
#[derive(Clone, Debug, clap::Parser)]
Nicolas80
committed
/// List available SS58 Addresses in the vault
Nicolas80
committed
#[clap(subcommand)]
List(ListChoice),
Nicolas80
committed
/// Use specific SS58 Address (changes the config Address)
Nicolas80
committed
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"
)]
Nicolas80
committed
Import {
/// Secret key format (substrate, seed, cesium)
#[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)]
secret_format: SecretFormat,
},
/// Add a derivation to an existing account
#[clap(long_about = "Add a derivation to an existing account\n\
\n\
Only \"substrate\" and \"seed\" format are supported for derivations\n\
\n\
Use command `vault list base` to see available <Base> account and their format\n\
And then use command 'vault list for' to find all accounts linked to that <Base> account")]
Nicolas80
committed
#[clap(alias = "deriv")]
#[clap(alias = "derivation")]
Derive {
Nicolas80
committed
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
Nicolas80
committed
/// Give a meaningful name to an SS58 Address in the vault
Nicolas80
committed
Rename {
/// SS58 Address
Nicolas80
committed
address: AccountId,
},
/// Remove an SS58 Address from the vault together with it's linked derivations
Nicolas80
committed
#[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")]
Nicolas80
committed
Remove {
#[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)
Nicolas80
committed
ListFiles,
/// (deprecated)Migrate old key files into db (will have to provide password for each key)
Migrate,
/// Show where vault db (or old keys) is stored
Where,
}
#[derive(Clone, Default, Debug, clap::Parser)]
pub enum ListChoice {
/// List all <Base> accounts and their linked derivations SS58 Addresses in the vault
Nicolas80
committed
#[default]
All,
/// List <Base> and Derivations SS58 Addresses linked to the selected one
For {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
/// List all <Base> SS58 Addresses in the vault
Base,
Nicolas80
committed
}
pub struct VaultDataToImport {
secret_format: SecretFormat,
Nicolas80
committed
secret_suri: String,
Nicolas80
committed
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)
}
Nicolas80
committed
pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
Nicolas80
committed
let db = data.connect_db();
// match subcommand
match command {
Nicolas80
committed
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 = compute_vault_accounts_table(&all_account_tree_node_hierarchies)?;
Nicolas80
committed
Nicolas80
committed
println!("available SS58 Addresses:");
Nicolas80
committed
println!("{table}");
}
ListChoice::Base => {
let base_account_tree_nodes =
vault_account::fetch_only_base_account_tree_nodes(db).await?;
Nicolas80
committed
let table = compute_vault_accounts_table(&base_account_tree_nodes)?;
Nicolas80
committed
println!("available <Base> SS58 Addresses:");
println!("{table}");
}
ListChoice::For {
address_or_vault_name,
} => {
let account = retrieve_vault_account(&data, address_or_vault_name).await?;
let account_tree_node_hierarchy =
vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
db,
&account.address.to_string(),
)
.await?;
let table = compute_vault_accounts_table(&[account_tree_node_hierarchy])?;
println!("available SS58 Addresses linked to {account}:");
Nicolas80
committed
println!("{table}");
Nicolas80
committed
},
Subcommand::ListFiles => {
let vault_key_addresses = fetch_vault_key_addresses(&data).await?;
let table = compute_vault_key_files_table(&vault_key_addresses).await?;
println!("available key files (needs to be migrated with command `vault migrate` in order to use them):");
Nicolas80
committed
println!("{table}");
Nicolas80
committed
Subcommand::Use {
address_or_vault_name,
} => {
let account = retrieve_vault_account(&data, address_or_vault_name).await?;
Nicolas80
committed
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}");
}
Nicolas80
committed
Subcommand::Import { secret_format } => {
let vault_data_for_import =
prompt_secret_and_compute_vault_data_to_import(secret_format)?;
Nicolas80
committed
//Extra check for SecretFormat::Cesium / G1v1Seed - showing the G1v1 cesium public key for confirmation
if secret_format == SecretFormat::Cesium {
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 Cesium id/password) ?".to_string())?;
if !confirmed {
return Ok(());
}
}
let address_to_import = vault_data_for_import.key_pair.address();
Nicolas80
committed
Nicolas80
committed
println!("Trying to import for SS58 address :'{}'", address_to_import);
Nicolas80
committed
if let Some(check_account) =
vault_account::find_by_id(db, &DbAccountId::from(address_to_import)).await?
Nicolas80
committed
{
println!(
"Vault entry already exists for that address: {}",
check_account
Nicolas80
committed
);
let account_tree_node_hierarchy =
vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
db,
&check_account.address.to_string(),
)
.await?;
println!("Here are all the SS58 Addresses linked to it in the vault:");
Nicolas80
committed
let table = compute_vault_accounts_table(&[account_tree_node_hierarchy])?;
Nicolas80
committed
println!("{table}");
return Ok(());
//TODO For later, possibly allow to replace the entry
Nicolas80
committed
}
println!("Enter password to protect the key");
Nicolas80
committed
let password = inputs::prompt_password_confirm()?;
println!("(Optional) Enter a name for the vault entry");
let name = inputs::prompt_vault_name()?;
Nicolas80
committed
let txn = db.begin().await?;
Nicolas80
committed
let _account = create_account_for_vault_data_to_import(
Nicolas80
committed
&txn,
&vault_data_for_import,
&password,
name.as_ref(),
)
.await?;
txn.commit().await?;
println!("Import done");
}
Subcommand::Derive {
Nicolas80
committed
address_or_vault_name,
} => {
let account_tree_node_to_derive =
retrieve_account_tree_node(&data, address_or_vault_name).await?;
Nicolas80
committed
let account_to_derive = account_tree_node_to_derive.borrow().account.clone();
Nicolas80
committed
let base_account_tree_node =
vault_account::get_base_account_tree_node(&account_tree_node_to_derive);
Nicolas80
committed
let base_account = &base_account_tree_node.borrow().account.clone();
if base_account.crypto_scheme.is_none() {
panic!("Crypto scheme is not set for the base account:{base_account} - should not happen");
}
if let Some(crypto_scheme) = base_account.crypto_scheme {
if CryptoScheme::from(crypto_scheme) == CryptoScheme::Ed25519 {
println!(
"Only \"{}\" and \"{}\" format are supported for derivations",
Into::<&str>::into(SecretFormat::Substrate),
Into::<&str>::into(SecretFormat::Seed)
);
println!();
println!(
"Use command `vault list base` to see available <Base> account and their format\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}");
Nicolas80
committed
println!("Enter password to decrypt the <Base> account key");
let password = inputs::prompt_password()?;
Nicolas80
committed
let account_to_derive_secret_suri = vault_account::compute_suri_account_tree_node(
&account_tree_node_to_derive,
password,
)?;
Nicolas80
committed
let derivation_path = inputs::prompt_vault_derivation_path()?;
let derivation_secret_suri =
format!("{account_to_derive_secret_suri}{derivation_path}");
Nicolas80
committed
let derivation_keypair =
compute_keypair(CryptoScheme::Sr25519, &derivation_secret_suri)?;
Nicolas80
committed
let derivation_address: String = derivation_keypair.address().to_string();
let check_derivation =
vault_account::find_by_id(db, &DbAccountId::from_str(&derivation_address)?).await?;
Nicolas80
committed
//TODO For later, possibly allow to replace the entry
Nicolas80
committed
if check_derivation.is_some() {
println!("Derivation already exists for address:'{derivation_address}'");
return Ok(());
}
println!("(Optional) Enter a name for the new derivation");
let name = inputs::prompt_vault_name()?;
let _derivation = vault_account::create_derivation_account(
db,
&derivation_address,
name.as_ref(),
&derivation_path,
&account_to_derive.address.to_string(),
)
.await?;
println!("Derive done");
Nicolas80
committed
}
Subcommand::Rename { address } => {
let account =
vault_account::find_by_id(db, &DbAccountId::from(address.clone())).await?;
Nicolas80
committed
if account.is_none() {
Nicolas80
committed
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();
Nicolas80
committed
println!(
"Current name for address:'{address}' is {:?}",
&account.name
Nicolas80
committed
);
println!("Enter new name for address (leave empty to remove the name)");
Nicolas80
committed
let name = inputs::prompt_vault_name()?;
let _account = vault_account::update_account_name(db, account, name.as_ref()).await?;
println!("Rename done");
Nicolas80
committed
}
Subcommand::Remove {
address_or_vault_name,
} => {
let account_tree_node_to_delete =
retrieve_account_tree_node(&data, address_or_vault_name).await?;
Nicolas80
committed
Nicolas80
committed
let txn = db.begin().await?;
Nicolas80
committed
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();
Nicolas80
committed
//If account to delete has children; also delete all linked derivations
if !account_tree_node_to_delete.borrow().children.is_empty() {
let table = compute_vault_accounts_table(&[account_tree_node_to_delete.clone()])?;
Nicolas80
committed
println!("All addresses linked to: {account_to_delete}");
Nicolas80
committed
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
)
Nicolas80
committed
);
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())?;
Nicolas80
committed
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!("Deleted {} address", delete_result.rows_affected);
Nicolas80
committed
}
} else {
let delete_result = account_to_delete.delete(&txn).await?;
println!("Deleted {} address", delete_result.rows_affected);
Nicolas80
committed
}
txn.commit().await?;
println!("Done removing address:'{address_to_delete}'");
}
Subcommand::Migrate => {
println!("Migrating existing key files to db");
let vault_key_addresses = fetch_vault_key_addresses(&data).await?;
let table = compute_vault_key_files_table(&vault_key_addresses).await?;
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?;
Nicolas80
committed
if existing_account.is_some() {
Nicolas80
committed
//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 {
Nicolas80
committed
secret_format: vault_data_from_file.secret_format,
secret_suri: vault_data_from_file.secret,
Nicolas80
committed
key_pair: vault_data_from_file.key_pair,
};
Nicolas80
committed
let txn = db.begin().await?;
Nicolas80
committed
let account = create_account_for_vault_data_to_import(
Nicolas80
committed
&txn,
&vault_data_to_import,
&vault_data_from_file.password,
None,
)
.await?;
txn.commit().await?;
println!("Import done: {}", account);
Nicolas80
committed
}
println!("Migration done");
}
Subcommand::Where => {
println!("{}", data.project_dir.data_dir().to_str().unwrap());
}
};
Ok(())
}
/// Method used to separate `name` part from optional `derivation` part in computed names
fn parse_prefix_and_derivation_path_from_string_for_vault_name(
Nicolas80
committed
raw_string: String,
) -> Result<(String, Option<String>), GcliError> {
if raw_string.contains("/") {
raw_string
.find("/")
.map_or(Err(GcliError::Input("Invalid format".to_string())), |idx| {
let (prefix, derivation_path) = raw_string.split_at(idx);
Ok((prefix.to_string(), Some(derivation_path.to_string())))
})
} else {
Ok((raw_string, None))
}
}
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
/// 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(
address_uri
.paths
.iter()
.map(|s| "/".to_string() + s)
.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 {
Nicolas80
committed
match secret_format {
SecretFormat::Seed => CryptoScheme::Sr25519,
SecretFormat::Substrate => CryptoScheme::Sr25519,
SecretFormat::Predefined => CryptoScheme::Sr25519,
SecretFormat::Cesium => CryptoScheme::Ed25519,
Nicolas80
committed
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
}
}
/// This method will scan files in the data directory and return the addresses of the vault keys found
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();
// To only keep the address part of the filename for names like "<ss58 address>-<secret_format>"
let potential_address = filename.split("-").next().unwrap();
// If potential_address is a valid AccountId
if AccountId::from_str(potential_address).is_ok() {
vault_key_addresses.push(potential_address.to_string());
}
});
Ok(vault_key_addresses)
}
async fn compute_vault_key_files_table(vault_key_addresses: &[String]) -> Result<Table, GcliError> {
let mut table = Table::new();
table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY);
table.set_header(vec!["Key file"]);
vault_key_addresses.iter().for_each(|address| {
table.add_row(vec![Cell::new(address)]);
});
Ok(table)
}
fn compute_vault_accounts_table(
account_tree_nodes: &[Rc<RefCell<AccountTreeNode>>],
) -> Result<Table, GcliError> {
Nicolas80
committed
let mut table = Table::new();
table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY);
Nicolas80
committed
table.set_header(vec!["SS58 Address", "Format", "Account/Path", "Name"]);
for account_tree_node in account_tree_nodes {
add_account_tree_node_to_table(&mut table, account_tree_node);
}
Ok(table)
}
fn add_account_tree_node_to_table(
table: &mut Table,
account_tree_node: &Rc<RefCell<AccountTreeNode>>,
) {
let row = compute_vault_accounts_row(account_tree_node);
table.add_row(row);
for child in &account_tree_node.borrow().children {
add_account_tree_node_to_table(table, child);
}
}
pub fn compute_vault_accounts_row(account_tree_node: &Rc<RefCell<AccountTreeNode>>) -> Vec<Cell> {
let empty_string = "".to_string();
let depth_account_tree_node = vault_account::count_depth_account_tree_node(account_tree_node);
let name = if let Some(name) = account_tree_node.borrow().account.name.clone() {
name
} else if let Some(computed_name) =
vault_account::compute_name_account_tree_node(account_tree_node)
{
format!("<{}>", computed_name)
} else {
empty_string.clone()
};
Nicolas80
committed
let account_tree_node = account_tree_node.borrow();
Nicolas80
committed
let address = if depth_account_tree_node > 0 {
let ancestors = "│ ".repeat(depth_account_tree_node - 1);
format!("{}├─{}", ancestors, account_tree_node.account.address)
} else {
account_tree_node.account.address.to_string()
};
Nicolas80
committed
let (path, format) = if let Some(path) = account_tree_node.account.path.clone() {
(path, empty_string.clone())
} else {
let secret_format = match account_tree_node.account.crypto_scheme.unwrap().into() {
CryptoScheme::Sr25519 => SecretFormat::Substrate,
CryptoScheme::Ed25519 => SecretFormat::Cesium,
Nicolas80
committed
};
let secret_format_str: &str = secret_format.into();
(
format!("<{}>", account_tree_node.account.account_type()),
secret_format_str.to_string(),
)
};
Nicolas80
committed
vec![
Cell::new(&address),
Cell::new(format),
Cell::new(&path),
Cell::new(&name),
]
Nicolas80
committed
}
pub async fn retrieve_address_string<T: AddressOrVaultName>(
Nicolas80
committed
address_or_vault_name: T,
) -> Result<String, GcliError> {
if let Some(address) = address_or_vault_name.address() {
return Ok(address.to_string());
}
let account = retrieve_vault_account(data, address_or_vault_name).await?;
Nicolas80
committed
Ok(account.address.to_string())
Nicolas80
committed
}
pub async fn retrieve_account_tree_node<T: AddressOrVaultName>(
Nicolas80
committed
data: &Data,
address_or_vault_name: T,
) -> Result<Rc<RefCell<AccountTreeNode>>, GcliError> {
//FIXME Should do the inverse as we do potentially several times same operation
let account = retrieve_vault_account(data, address_or_vault_name).await?;
let account_tree_node = vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
data.connect_db(),
&account.address.to_string(),
)
.await?;
Ok(vault_account::get_account_tree_node_for_address(
&account_tree_node,
&account.address.to_string(),
))
}
pub async fn retrieve_vault_account<T: AddressOrVaultName>(
data: &Data,
address_or_vault_name: T,
) -> Result<vault_account::Model, GcliError> {
let account = if let Some(name_input) = address_or_vault_name.name() {
Nicolas80
committed
let (name, derivation_path_opt) =
parse_prefix_and_derivation_path_from_string_for_vault_name(name_input.to_string())?;
Nicolas80
committed
let account = vault_account::find_by_name(data.connect_db(), &name).await?;
Nicolas80
committed
let account = account.ok_or(GcliError::Input(format!(
"No account found with name:'{name}'"
Nicolas80
committed
)))?;
match derivation_path_opt {
None => account,
Nicolas80
committed
Some(path) => {
let account_tree_node_hierarchy =
vault_account::fetch_base_account_tree_node_hierarchy_unwrapped(
data.connect_db(),
&account.address.to_string(),
Nicolas80
committed
)
.await?;
let account_tree_node_hierarchy = vault_account::get_account_tree_node_for_address(
&account_tree_node_hierarchy,
&account.address.to_string(),
);
let account_tree_node = vault_account::compute_name_map_for_account_tree_node(
&account_tree_node_hierarchy,
)?
.get(name_input)
.cloned()
.ok_or(GcliError::Input(format!(
"No account found with name:'{name}' and path:'{path}'"
)))?;
//Need this extra step to avoid borrowing issues
let account = account_tree_node.borrow().account.clone();
account
Nicolas80
committed
}
}
} else if let Some(address) = address_or_vault_name.address() {
let account =
vault_account::find_by_id(data.connect_db(), &DbAccountId::from(address.clone()))
.await?;
Nicolas80
committed
account.ok_or(GcliError::Input(format!(
Nicolas80
committed
"No vault entry found with Address:'{address}'"
Nicolas80
committed
)))?
} else {
//Should never happen since clap enforces exactly one of the 2 options
return Err(GcliError::Input("No address or name provided".to_string()));
};
Nicolas80
committed
}
fn create_vault_data_to_import<F, P>(
secret_format: SecretFormat,
prompt_fn: F,
) -> Result<VaultDataToImport, GcliError>
where
Nicolas80
committed
F: Fn() -> (String, P),
Nicolas80
committed
P: Into<KeyPair>,
{
let (secret, pair) = prompt_fn();
let key_pair = pair.into();
Ok(VaultDataToImport {
secret_format,
Nicolas80
committed
secret_suri: secret,
Nicolas80
committed
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::Cesium => {
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 an account for the vault data to import
Nicolas80
committed
///
/// Does it all using "db" parameter that should better be a transaction since multiple operations can be done
pub async fn create_account_for_vault_data_to_import<C>(
Nicolas80
committed
db: &C,
vault_data: &VaultDataToImport,
password: &str,
name: Option<&String>,
) -> Result<vault_account::Model, GcliError>
Nicolas80
committed
where
C: ConnectionTrait,
{
Nicolas80
committed
let address_to_import = vault_data.key_pair.address().to_string();
Nicolas80
committed
//To be safe
if vault_account::find_by_id(db, &DbAccountId::from_str(&address_to_import)?)
Nicolas80
committed
.await?
.is_some()
{
//TODO Later possibly allow to replace the entry
Nicolas80
committed
return Err(GcliError::Input(format!(
"Vault entry already exists for address {}",
Nicolas80
committed
&address_to_import
Nicolas80
committed
)));
}
let secret_format = vault_data.secret_format;
let encrypted_suri = encrypt(
vault_data.secret_suri.clone().as_bytes(),
password.to_string(),
)
.map_err(|e| anyhow!(e))?;
let crypto_scheme = map_secret_format_to_crypto_scheme(secret_format);
Nicolas80
committed
let base_account = vault_account::create_base_account(
db,
&address_to_import,
name,
crypto_scheme,
encrypted_suri,
)
.await?;
Nicolas80
committed
Ok(base_account)
Nicolas80
committed
}
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);
Nicolas80
committed
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(),
)
Nicolas80
committed
.await?
{
let account_tree_node = vault_account::get_account_tree_node_for_address(
&account_tree_node_hierarchy,
&address.to_string(),
);
Nicolas80
committed
let password = inputs::prompt_password()?;
let secret_suri =
vault_account::compute_suri_account_tree_node(&account_tree_node, password)?;
Nicolas80
committed
let base_account_tree_node = vault_account::get_base_account_tree_node(&account_tree_node);
Nicolas80
committed
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
)));
Nicolas80
committed
}
Ok(Some(key_pair))
Nicolas80
committed
} 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(),
Nicolas80
committed
};
Ok(key_pair)
}
pub struct VaultDataFromFile {
secret_format: SecretFormat,
secret: String,
Nicolas80
committed
#[allow(dead_code)]
Nicolas80
committed
path: PathBuf,
password: String,
key_pair: KeyPair,
}
/// try to get secret in keystore, prompt for the password and compute the keypair
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()?;
Nicolas80
committed
let mut file = std::fs::OpenOptions::new().read(true).open(path.clone())?;
let mut cypher = vec![];
file.read_to_end(&mut cypher)?;
Nicolas80
committed
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)
}
}
Nicolas80
committed
#[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"
)),
Nicolas80
committed
Some(String::from("//0"))
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"),
Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")),
Nicolas80
committed
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"
)),
Nicolas80
committed
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"))
)]
Nicolas80
committed
#[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"
)),
Nicolas80
committed
None
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
Some(String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")),
Nicolas80
committed
None
)]
#[case(
String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
Some(String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")),
Nicolas80
committed
None
)]
#[case(
String::from("someVaultName//Alice"),
Some(String::from("someVaultName")),
Nicolas80
committed
Some(String::from("//Alice"))
)]
#[case(
String::from("someVaultName"),
Some(String::from("someVaultName")),
None
)]
fn test_parse_prefix_and_derivation_path_from_suri(
Nicolas80
committed
#[case] raw_string: String,
#[case] expected_prefix: Option<String>,
Nicolas80
committed
#[case] expected_derivation_path: Option<String>,
) {
let (root_secret, derivation_path) =
parse_prefix_and_derivation_path_from_suri(raw_string).unwrap();
Nicolas80
committed
assert_eq!(expected_prefix, root_secret);