Newer
Older
Nicolas80
committed
use crate::entities::vault_account::CryptoType;
use crate::entities::{vault_account, vault_derivation};
use crate::*;
use age::secrecy::Secret;
Nicolas80
committed
use comfy_table::{Cell, Table};
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, EntityTrait, ModelTrait};
use sea_orm::{ColumnTrait, QueryFilter};
use sea_orm::{ConnectionTrait, TransactionTrait};
Nicolas80
committed
use std::path::PathBuf;
/// define universal dividends subcommands
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,
Nicolas80
committed
/// Import key from (substrate)mnemonic or other format with interactive prompt
#[clap(
long_about = "Import key from (substrate)mnemonic or other format with interactive prompt\n\
\n\
Nicolas80
committed
If a (substrate)mnemonic is provided with a derivation path, it will ensure the base <Account>\n\
and associated SS58 Address exists before creating the derivation; but please use command \n\
`vault derivation|derive|deriv` to add a derivation to an existing <Account> instead."
)]
Nicolas80
committed
Import {
/// Secret key format (substrate, seed, cesium)
#[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)]
secret_format: SecretFormat,
},
Nicolas80
committed
/// 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\
Nicolas80
committed
Use command `vault list account` to see available <Account> and their format")]
Nicolas80
committed
#[clap(alias = "deriv")]
#[clap(alias = "derive")]
Derivation {
#[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,
},
Nicolas80
committed
/// Remove an SS58 Address from the vault
#[clap(long_about = "Remove an SS58 Address from the vault\n\
\n\
If an <Account> Address is given it will also remove all linked derivations")]
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 {
Nicolas80
committed
/// List all <Account> and their linked derivations SS58 Addresses in the vault
Nicolas80
committed
#[default]
All,
Nicolas80
committed
/// List <Account> and derivations SS58 Addresses linked to the selected one
For {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
Nicolas80
committed
/// List all <Account> SS58 Addresses in the vault
Account,
Nicolas80
committed
}
pub struct VaultDataToImport {
secret_format: SecretFormat,
secret: keys::Secret,
address: AccountId,
key_pair: KeyPair,
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
}
// 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
Nicolas80
committed
pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
// match subcommand
match command {
Nicolas80
committed
Subcommand::List(choice) => match choice {
ListChoice::All => {
let derivations = vault_derivation::list_all_derivations_in_order(
data.connection.as_ref().unwrap(),
)
.await?;
let table = compute_vault_derivations_table(
data.connection.as_ref().unwrap(),
&derivations,
)
.await?;
Nicolas80
committed
Nicolas80
committed
println!("available SS58 Addresses:");
Nicolas80
committed
println!("{table}");
}
Nicolas80
committed
ListChoice::Account => {
Nicolas80
committed
let derivations = vault_derivation::list_all_root_derivations_in_order(
data.connection.as_ref().unwrap(),
)
.await?;
let table = compute_vault_derivations_table(
data.connection.as_ref().unwrap(),
&derivations,
)
.await?;
Nicolas80
committed
Nicolas80
committed
println!("available <Account> SS58 Addresses:");
println!("{table}");
}
ListChoice::For {
address_or_vault_name,
} => {
Nicolas80
committed
let selected_derivation =
retrieve_vault_derivation(&data, address_or_vault_name).await?;
let linked_derivations = vault_derivation::fetch_all_linked_derivations_in_order(
data.connection.as_ref().unwrap(),
Nicolas80
committed
&selected_derivation.root_address,
)
.await?;
let table = compute_vault_derivations_table(
data.connection.as_ref().unwrap(),
&linked_derivations,
)
.await?;
Nicolas80
committed
println!("available SS58 Addresses linked to {selected_derivation}:");
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):");
println!("{table}");
Nicolas80
committed
Subcommand::Use {
address_or_vault_name,
} => {
let derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?;
//FIXME not sure if this is ok (but since it's a CLI; this data instance won't be used afterwards)
let mut data = data;
data.cfg.address =
Some(AccountId::from_str(&derivation.address).expect("invalid address"));
println!("Using key {}", derivation);
conf::save_config(&data);
}
Subcommand::Generate => {
// TODO allow custom word count
let mnemonic = bip39::Mnemonic::generate(12).unwrap();
println!("{mnemonic}");
}
Nicolas80
committed
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
Subcommand::Import { secret_format } => {
let vault_data_for_import =
prompt_secret_and_compute_vault_data_to_import(secret_format)?;
println!(
"Trying to import for address :'{}'",
vault_data_for_import.address
);
if let Some(derivation) =
vault_derivation::Entity::find_by_id(vault_data_for_import.address.to_string())
.one(data.connection.as_ref().unwrap())
.await?
{
println!(
"Vault entry already exists for address:'{}'",
vault_data_for_import.address
);
let linked_derivations = vault_derivation::fetch_all_linked_derivations_in_order(
data.connection.as_ref().unwrap(),
&derivation.root_address.clone(),
)
.await?;
println!("Here are all the linked derivations already present in the vault:");
let table = compute_vault_derivations_table(
data.connection.as_ref().unwrap(),
&linked_derivations,
)
.await?;
Nicolas80
committed
println!("{table}");
return Ok(());
}
println!("Enter password to protect the key");
Nicolas80
committed
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
let password = inputs::prompt_password_confirm()?;
println!("(Optional) Enter a name for the vault entry");
let name = inputs::prompt_vault_name()?;
let txn = data.connection.as_ref().unwrap().begin().await?;
let _derivation = create_derivation_for_vault_data_to_import(
&txn,
&vault_data_for_import,
&password,
name.as_ref(),
)
.await?;
txn.commit().await?;
println!("Import done");
}
Subcommand::Derivation {
address_or_vault_name,
} => {
let root_derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?;
if root_derivation.path.is_some() {
println!("Can only add derivation on a ROOT key.");
println!(
"The selected key with address:'{}' already has a ROOT with address:'{}'",
root_derivation.address, root_derivation.root_address
);
println!("You can check for available ROOT keys with command 'vault list root'");
return Ok(());
}
let vault_account = vault_account::Entity::find_by_id(&root_derivation.address)
.one(data.connection.as_ref().unwrap())
.await?
.ok_or(GcliError::Input(format!(
"Could not find (root) vault account for address:'{}'",
root_derivation.address
)))?;
if vault_account.crypto_type == CryptoType::Ed25519Seed {
println!(
"Only \"{}\" and \"{}\" format are supported for derivations",
Into::<&str>::into(SecretFormat::Substrate),
Into::<&str>::into(SecretFormat::Seed)
);
Nicolas80
committed
println!("Use command `vault list root` to see available <Account> and their format");
return Ok(());
}
Nicolas80
committed
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
println!("Adding derivation to root key: {root_derivation}");
println!("Enter password to decrypt the root key");
let root_secret_suri = retrieve_suri_from_vault_account(&vault_account)?;
let derivation_path = inputs::prompt_vault_derivation_path()?;
let derivation_secret_suri = format!("{root_secret_suri}{derivation_path}");
let derivation_keypair =
compute_keypair(vault_account.crypto_type, &derivation_secret_suri)?;
let derivation_address: String = derivation_keypair.address().to_string();
let check_derivation = vault_derivation::Entity::find_by_id(&derivation_address)
.one(data.connection.as_ref().unwrap())
.await?;
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_derivation::ActiveModel {
address: Set(derivation_address),
name: Set(name),
path: Set(Some(derivation_path)),
root_address: Set(root_derivation.root_address.clone()),
};
let derivation = derivation.insert(data.connection.as_ref().unwrap()).await?;
println!("Created derivation {}", derivation);
}
Subcommand::Rename { address } => {
let derivation = vault_derivation::Entity::find_by_id(address.to_string())
.one(data.connection.as_ref().unwrap())
.await?;
if derivation.is_none() {
println!("No vault entry found for address:'{address}'");
println!("You might want to import it first with 'vault import'");
return Ok(());
}
let derivation = derivation.unwrap();
println!(
"Current name for address:'{address}' is {:?}",
derivation.name
);
println!("Enter new vault name for the key (leave empty to remove the name)");
let name = inputs::prompt_vault_name()?;
let old_name = derivation.name.clone();
let mut derivation: vault_derivation::ActiveModel = derivation.into();
derivation.name = Set(name.clone());
let _derivation = derivation.update(data.connection.as_ref().unwrap()).await?;
println!(
"Renamed address:'{address}' from {:?} to {:?}",
old_name, name
);
}
Subcommand::Remove {
address_or_vault_name,
} => {
let derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?;
let address_to_delete = derivation.address.clone();
let txn = data.connection.as_ref().unwrap().begin().await?;
//If deleting a root derivation; also delete the vault account and all linked derivations
if derivation.path.is_none() {
let all_derivations_to_delete =
vault_derivation::fetch_all_linked_derivations_in_order(
&txn,
&address_to_delete,
)
.await?;
let table =
compute_vault_derivations_table(&txn, &all_derivations_to_delete).await?;
Nicolas80
committed
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
println!("All derivations linked to the root derivation:");
println!("{table}");
println!(
"This root derivation has {} derivations in total",
all_derivations_to_delete.len()
);
let confirmed = inputs::confirm_action(
"Are you sure you want to delete it along with the vault account (and saved key)?".to_string()
)?;
if !confirmed {
return Ok(());
}
for derivation_to_delete in all_derivations_to_delete {
let delete_result = derivation_to_delete.delete(&txn).await?;
println!("Deleted {} derivation", delete_result.rows_affected);
}
let delete_result = vault_account::Entity::delete_by_id(&address_to_delete)
.exec(&txn)
.await?;
println!("Deleted {} vault account", delete_result.rows_affected);
} else {
let delete_result = derivation.delete(&txn).await?;
println!("Deleted {} derivation", delete_result.rows_affected);
}
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 derivation = vault_derivation::Entity::find_by_id(&address)
.one(data.connection.as_ref().unwrap())
.await?;
if derivation.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: SecretFormat::Substrate,
secret: keys::Secret::SimpleSecret(vault_data_from_file.secret),
address: AccountId::from_str(&address).expect("invalid address"),
key_pair: vault_data_from_file.key_pair,
};
let txn = data.connection.as_ref().unwrap().begin().await?;
let derivation = create_derivation_for_vault_data_to_import(
&txn,
&vault_data_to_import,
&vault_data_from_file.password,
None,
)
.await?;
txn.commit().await?;
println!("Import done: {}", derivation);
}
println!("Migration done");
}
Subcommand::Where => {
println!("{}", data.project_dir.data_dir().to_str().unwrap());
}
};
Ok(())
}
Nicolas80
committed
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
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
528
529
530
531
532
533
fn parse_prefix_and_derivation_path_from_string(
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))
}
}
fn map_secret_format_to_crypto_type(secret_format: SecretFormat) -> CryptoType {
match secret_format {
SecretFormat::Seed => vault_account::CryptoType::Sr25519Seed,
SecretFormat::Substrate => vault_account::CryptoType::Sr25519Mnemonic,
SecretFormat::Predefined => vault_account::CryptoType::Sr25519Mnemonic,
SecretFormat::Cesium => vault_account::CryptoType::Ed25519Seed,
}
}
/// 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)
}
async fn compute_vault_derivations_table<C>(
db: &C,
Nicolas80
committed
derivations_ordered: &[vault_derivation::Model],
) -> Result<Table, GcliError>
where
C: ConnectionTrait,
{
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"]);
let empty_string = "".to_string();
Nicolas80
committed
let root_path = "<Account>".to_string();
Nicolas80
committed
let mut current_root_address = "".to_string();
let mut current_root_name: Option<String> = None;
let mut current_vault_format: Option<&str> = None;
Nicolas80
committed
for derivation in derivations_ordered {
if derivation.root_address != current_root_address {
// First entry should be a root derivation
if derivation.path.is_some() {
return Err(GcliError::Input(
"Order of derivations parameter is wrong".to_string(),
));
}
current_root_address = derivation.root_address.clone();
current_root_name = derivation.name.clone();
let vault_account = vault_account::Entity::find_by_id(current_root_address.clone())
.one(db)
.await?
.ok_or(GcliError::Input(format!(
Nicolas80
committed
"No vault <Account> found with address:'{current_root_address}'"
)))?;
current_vault_format = match vault_account.crypto_type {
CryptoType::Sr25519Mnemonic => Some(SecretFormat::Substrate.into()),
CryptoType::Sr25519Seed => Some(SecretFormat::Seed.into()),
CryptoType::Ed25519Seed => Some(SecretFormat::Cesium.into()),
};
Nicolas80
committed
} else {
// Validate that the RootAddress is the same as current_root_address
if derivation.root_address != current_root_address {
return Err(GcliError::Input(
"Order of derivations parameter is wrong".to_string(),
));
}
}
let address = if derivation.path.is_none() {
derivation.address.clone()
} else {
" ".to_string() + &derivation.address
};
let (path, format) = if derivation.path.is_none() {
(root_path.clone(), current_vault_format.unwrap())
Nicolas80
committed
} else {
(derivation.path.clone().unwrap(), empty_string.as_str())
Nicolas80
committed
};
let name = if derivation.name.is_none() {
if derivation.path.is_none() {
empty_string.clone()
Nicolas80
committed
} else if let Some(current_root_name) = ¤t_root_name {
format!(
"<{}{}>",
current_root_name,
derivation.path.clone().unwrap()
)
} else {
empty_string.clone()
Nicolas80
committed
}
} else {
derivation.name.clone().unwrap()
};
table.add_row(vec![
Cell::new(&address),
Cell::new(format),
Nicolas80
committed
Cell::new(&path),
Cell::new(&name),
]);
}
Ok(table)
}
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 derivation = retrieve_vault_derivation(data, address_or_vault_name).await?;
Ok(derivation.address)
}
pub async fn retrieve_vault_derivation<T: AddressOrVaultName>(
data: &Data,
address_or_vault_name: T,
) -> Result<vault_derivation::Model, GcliError> {
let derivation = if let Some(name) = address_or_vault_name.name() {
let (name, derivation_path_opt) =
parse_prefix_and_derivation_path_from_string(name.to_string())?;
let derivation = vault_derivation::Entity::find()
.filter(vault_derivation::Column::Name.eq(Some(name.clone())))
.one(data.connection.as_ref().unwrap())
.await?;
let derivation = derivation.ok_or(GcliError::Input(format!(
Nicolas80
committed
"No vault SS58 Address found with name:'{name}'"
Nicolas80
committed
)))?;
match derivation_path_opt {
None => derivation,
Some(path) => {
let sub_derivation = vault_derivation::Entity::find()
.filter(
vault_derivation::Column::RootAddress.eq(derivation.root_address.clone()),
)
.filter(vault_derivation::Column::Path.eq(Some(path.clone())))
.one(data.connection.as_ref().unwrap())
.await?;
sub_derivation.ok_or(GcliError::Input(format!(
Nicolas80
committed
"No vault derivation found with <Account> name:'{name}' and path:'{path}'"
Nicolas80
committed
)))?
}
}
} else if let Some(address) = address_or_vault_name.address() {
let derivation = vault_derivation::Entity::find_by_id(address.to_string())
.one(data.connection.as_ref().unwrap())
.await?;
derivation.ok_or(GcliError::Input(format!(
Nicolas80
committed
"No vault entry found with Address:'{address}'"
Nicolas80
committed
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
)))?
} 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(derivation)
}
fn create_vault_data_to_import<F, P>(
secret_format: SecretFormat,
prompt_fn: F,
) -> Result<VaultDataToImport, GcliError>
where
F: Fn() -> (keys::Secret, P),
P: Into<KeyPair>,
{
let (secret, pair) = prompt_fn();
let key_pair = pair.into();
Ok(VaultDataToImport {
secret_format,
secret,
address: key_pair.address(),
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 derivation and if necessary root vault account and root derivation
///
/// Does it all using "db" parameter that should better be a transaction since multiple operations can be done
pub async fn create_derivation_for_vault_data_to_import<C>(
db: &C,
vault_data: &VaultDataToImport,
password: &str,
name: Option<&String>,
) -> Result<vault_derivation::Model, GcliError>
where
C: ConnectionTrait,
{
//To be safe
if vault_derivation::Entity::find_by_id(vault_data.address.to_string())
.one(db)
.await?
.is_some()
{
return Err(GcliError::Input(format!(
"Vault entry already exists for address {}",
vault_data.address
)));
}
let secret_suri: String = match &vault_data.secret_format {
SecretFormat::Cesium => {
if let KeyPair::Nacl(keypair) = &vault_data.key_pair {
// In case of cesium key, we will store the seed suri instead of id/password (so it supports derivations)
let seed: [u8; 32] = keypair.skey[0..32]
.try_into()
.expect("slice with incorrect length");
format!("0x{}", hex::encode(seed))
} else {
return Err(GcliError::Input("Expected KeyPair::Nacl".to_string()));
}
}
SecretFormat::Seed => {
if let keys::Secret::SimpleSecret(seed_str) = &vault_data.secret {
format!("0x{seed_str}")
} else {
return Err(GcliError::Input("Expected SimpleSecret".to_string()));
}
}
SecretFormat::Substrate | SecretFormat::Predefined => {
if let keys::Secret::SimpleSecret(secret_suri) = &vault_data.secret {
secret_suri.clone()
} else {
return Err(GcliError::Input("Expected SimpleSecret".to_string()));
}
}
};
let secret_format = vault_data.secret_format;
let (root_secret_suri, derivation_path_opt, root_address, derivation_address) =
compute_root_and_derivation_data(&secret_format, secret_suri)?;
// Making sure the computed address is the same as the address to import
let address_to_import = vault_data.address.to_string();
if let Some(derivation_address) = &derivation_address {
if *derivation_address != address_to_import {
return Err(GcliError::Input(format!(
"Derivation address {} does not match the expected address {}",
derivation_address, address_to_import
)));
}
} else if root_address != address_to_import {
return Err(GcliError::Input(format!(
"Derivation address {} does not match the expected address {}",
root_address, address_to_import
)));
}
let encrypted_private_key =
encrypt(root_secret_suri.as_bytes(), password.to_string()).map_err(|e| anyhow!(e))?;
let _root_account = vault_account::create_vault_account(
db,
&root_address,
map_secret_format_to_crypto_type(secret_format),
encrypted_private_key,
)
.await?;
let derivation = if let Some(derivation_path) = derivation_path_opt {
// Root derivation
let _root_derivation =
vault_derivation::create_root_vault_derivation(db, &root_address, None).await?;
// Compute derivation !
let derivation = vault_derivation::ActiveModel {
address: Set(derivation_address.unwrap().clone()),
name: Set(name.cloned()),
path: Set(Some(derivation_path)),
root_address: Set(root_address.clone()),
};
let derivation = derivation.insert(db).await?;
println!("Created derivation {}", derivation);
derivation
} else {
let derivation =
vault_derivation::create_root_vault_derivation(db, &root_address, name).await?;
println!("Created derivation {}", derivation);
derivation
};
Ok(derivation)
}
fn compute_root_and_derivation_data(
secret_format: &SecretFormat,
secret_suri: String,
) -> Result<(String, Option<String>, String, Option<String>), GcliError> {
let (root_secret_suri, derivation_path_opt) =
parse_prefix_and_derivation_path_from_string(secret_suri)?;
let (root_address, derivation_address_opt) = match &secret_format {
SecretFormat::Cesium => match &derivation_path_opt {
None => {
let root_suri = &root_secret_suri;
let root_pair = pair_from_ed25519_str(root_suri)?;
let root_address: AccountId = root_pair.public().into();
(root_address.to_string(), None)
}
Some(derivation_path) => {
let root_suri = &root_secret_suri;
let root_pair = pair_from_ed25519_str(root_suri)?;
let root_address: AccountId = root_pair.public().into();
let derivation_suri = root_suri.clone() + derivation_path;
let derivation_pair = pair_from_ed25519_str(&derivation_suri)?;
let derivation_address: AccountId = derivation_pair.public().into();
(
root_address.to_string(),
Some(derivation_address.to_string()),
)
}
},
SecretFormat::Substrate | SecretFormat::Seed | SecretFormat::Predefined => {
match &derivation_path_opt {
None => {
let root_suri = &root_secret_suri;
let root_pair = pair_from_sr25519_str(root_suri)?;
let root_address: AccountId = root_pair.public().into();
(root_address.to_string(), None)
}
Some(derivation_path) => {
let root_suri = &root_secret_suri;
let root_pair = pair_from_sr25519_str(root_suri)?;
let root_address: AccountId = root_pair.public().into();
let derivation_suri = root_suri.clone() + derivation_path;
let derivation_pair = pair_from_sr25519_str(&derivation_suri)?;
let derivation_address: AccountId = derivation_pair.public().into();
(
root_address.to_string(),
Some(derivation_address.to_string()),
)
}
}
}
};
Ok((
root_secret_suri,
derivation_path_opt,
root_address,
derivation_address_opt,
))
}
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
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
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(derivation) = vault_derivation::Entity::find_by_id(address.to_string())
.one(data.connection.as_ref().unwrap())
.await?
{
if let Some(vault_account) =
vault_account::Entity::find_by_id(derivation.root_address.clone())
.one(data.connection.as_ref().unwrap())
.await?
{
let root_secret_suri = retrieve_suri_from_vault_account(&vault_account)?;
let secret_suri = if let Some(derivation_path) = derivation.path {
format!("{root_secret_suri}{derivation_path}")
} else {
root_secret_suri
};
let key_pair = compute_keypair(vault_account.crypto_type, &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)
}
} else {
Ok(None)
}
}
pub fn retrieve_suri_from_vault_account(
vault_account: &vault_account::Model,
) -> Result<String, GcliError> {
let password = inputs::prompt_password()?;
let cypher = &vault_account.encrypted_private_key;
let secret_vec =
decrypt(cypher, password.clone()).map_err(|e| GcliError::Input(e.to_string()))?;
let secret_suri = String::from_utf8(secret_vec).map_err(|e| anyhow!(e))?;
Ok(secret_suri)
}
pub fn compute_keypair(crypto_type: CryptoType, secret_suri: &str) -> Result<KeyPair, GcliError> {
let key_pair = match crypto_type {
CryptoType::Sr25519Mnemonic | CryptoType::Sr25519Seed => {
pair_from_sr25519_str(secret_suri)?.into()
}
CryptoType::Ed25519Seed => pair_from_ed25519_str(secret_suri)?.into(),
};
Ok(key_pair)
}
pub struct VaultDataFromFile {
address: String,
secret_format: SecretFormat,
secret: String,
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 = rpassword::prompt_password("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 {
address: address.to_string(),