Skip to content
Snippets Groups Projects
Commit 8d6df397 authored by Nicolas80's avatar Nicolas80
Browse files

* Added extra data in `vault inspect` command

  * Secret seed/mini-secret (if no soft derivation is used)
  * Public key (hex)
  * SS58 Address
  * (potential G1v1 public key if inspecting a <Base> account with ed25519 crypto-scheme)
* Does most changes mentioned in Issue #28 (except the network id)
parent 3e96d537
No related branches found
No related tags found
1 merge request!44feat: Can choose between ed25519 ans sr25519
Pipeline #40298 passed
mod display;
use crate::commands::cesium::compute_g1v1_public_key;
use crate::commands::cesium;
use crate::entities::vault_account;
use crate::entities::vault_account::{AccountTreeNode, ActiveModel, DbAccountId};
use crate::inputs::{trim_and_reduce_empty_as_none, validate_derivation_path, validate_vault_name};
......@@ -114,7 +114,7 @@ pub enum Subcommand {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
},
/// Inspect a vault entry, retrieving its Substrate URI (will provide more data in a future version)
/// Inspect a vault entry, retrieving its Substrate URI, Crypto-Scheme, Secret seed/mini-secret(if possible), Public key (hex), SS58 Address and potential G1v1 public key if inspecting a <Base> account with ed25519 crypto-scheme
Inspect {
#[clap(flatten)]
address_or_vault_name: AddressOrVaultNameGroup,
......@@ -348,7 +348,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
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)?
cesium::compute_g1v1_public_key(&vault_data_for_import.key_pair)?
);
// Skip confirmation in non-interactive mode
......@@ -560,7 +560,9 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
let base_account_tree_node =
vault_account::get_base_account_tree_node(&account_tree_node_to_inspect);
if !Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node) {
let is_base_account =
Rc::ptr_eq(&account_tree_node_to_inspect, &base_account_tree_node);
if !is_base_account {
let base_account = base_account_tree_node.borrow().account.clone();
println!("The linked <Base> account is {base_account}");
}
......@@ -583,6 +585,36 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE
))?
.into();
println!("Crypto scheme: {}", <&'static str>::from(crypto_scheme));
match compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&account_to_derive_secret_suri,
crypto_scheme,
) {
Err(e) => {
println!("Secret seed/mini-secret: cannot be computed: {}", e)
}
Ok((_computed_pair, seed)) => {
println!("Secret seed/mini-secret: '0x{}'", hex::encode(seed));
}
}
let account_address: AccountId = account_tree_node_to_inspect
.borrow()
.account
.address
.clone()
.into();
println!("Public key (hex): '0x{}'", hex::encode(account_address.0));
println!("SS58 Address: '{account_address}'");
if CryptoScheme::Ed25519 == crypto_scheme && is_base_account {
println!(
"(potential G1v1 public key: '{}')",
cesium::compute_g1v1_public_key_from_ed25519_account_id(&account_address)
);
}
}
Subcommand::Migrate => {
println!("Migrating existing key files to db");
......
use crate::commands::vault;
use crate::*;
use sp_core::ed25519;
use sp_core::sr25519;
use sp_core::crypto::AddressUri;
use sp_core::crypto::Pair as PairTrait;
use sp_core::DeriveJunction;
use sp_core::{ed25519, sr25519};
pub const SUBSTRATE_MNEMONIC: &str =
"bottom drive obey lake curtain smoke basket hold race lonely fit walk";
......@@ -235,6 +237,130 @@ pub fn pair_from_ed25519_seed(secret: &str) -> Result<ed25519::Pair, GcliError>
Ok(pair)
}
/// Check [compute_pair_and_mini_secret_seed_from_suri] method for details
pub fn compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
suri: &str,
crypto_scheme: CryptoScheme,
) -> Result<(KeyPair, [u8; 32]), GcliError> {
match crypto_scheme {
CryptoScheme::Ed25519 => {
let (pair, seed) =
compute_pair_and_mini_secret_seed_from_suri::<sp_core::ed25519::Pair>(suri)?;
Ok((pair.into(), seed))
}
CryptoScheme::Sr25519 => {
let (pair, seed) =
compute_pair_and_mini_secret_seed_from_suri::<sp_core::sr25519::Pair>(suri)?;
Ok((pair.into(), seed))
}
}
}
/// Computes the pair and mini-secret/seed from a Substrate URI for either sr25519 or ed25519.
///
/// # Arguments
/// * `suri` - The Substrate URI (e.g., mnemonic, hex seed, possibly with HARD derivation(s) like "//0")
///
/// # Returns
/// A tuple `(P, [u8; 32])` where `P` is the computed `Pair` and `[u8; 32]` is the mini-secret.
///
/// # Errors
/// Returns `GcliError` if:
/// - The Substrate URI lacks a phrase part
/// - Passwords are present in the derivation
/// - Soft derivation is used (not supported for seed retrieval)
/// - The computed pair's address is different from the one directly retrieved from the `suri` (should not happen)
pub fn compute_pair_and_mini_secret_seed_from_suri<P: PairTrait<Seed = [u8; 32]>>(
suri: &str,
) -> Result<(P, [u8; 32]), GcliError>
where
subxt::utils::AccountId32: std::convert::From<<P as sp_core::Pair>::Public>,
{
let address_uri = AddressUri::parse(suri).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 base_uri = address_uri.phrase.ok_or(GcliError::Input(
"The `suri` need to contain the 'phrase' part".into(),
))?;
let pair_from_suri_for_validation =
P::from_string(suri, None).map_err(|_| GcliError::Input("Invalid secret".to_string()))?;
// Get the <Base> pair and seed
// If using mini-secret / seed
let (base_pair, base_seed) = if let Some(base_uri_seed) = base_uri.strip_prefix("0x") {
let seed_bytes = hex::decode(base_uri_seed)
.map_err(|e| GcliError::Input(format!("Invalid hex seed: {e}")))?;
let seed: [u8; 32] = seed_bytes
.try_into()
.map_err(|_e| GcliError::Input("Incomplete seed".into()))?;
(P::from_seed(&seed), seed)
} else {
// If using mnemonic / passphrase
let (pair, seed_vec) = P::from_phrase(base_uri, None)
.map_err(|e| GcliError::Input(format!("Invalid mnemonic or passphrase: {e}")))?;
let seed: [u8; 32] = seed_vec
.as_slice()
.try_into()
.map_err(|_e| GcliError::Input("Seed should be 32 bytes".into()))?;
(pair, seed)
};
let derivation_paths = address_uri.paths;
// Apply derivation if present
let (result_pair, result_seed) = if !derivation_paths.is_empty() {
// AddressUri paths have one less '/' everywhere, so matching for no '/' at the start to exclude SOFT derivations
if derivation_paths.iter().any(|path| !path.starts_with("/")) {
return Err(GcliError::Input(
"Soft derivation is not supported when trying to retrieve mini-secret/seed"
.to_string(),
));
}
let derive_junctions_from_path = derivation_paths.iter().map(|path| {
if let Some(hard_derivation_no_prefix) = path.strip_prefix("/") {
// AddressUri paths have one less '/' everywhere so this is for HARD derivation
// (and we excluded passwords before; so there won't be any '//')
if let Ok(num) = hard_derivation_no_prefix.parse::<u64>() {
DeriveJunction::hard(num) // Numeric hard derivation
} else {
DeriveJunction::hard(hard_derivation_no_prefix) // String hard derivation
}
} else {
unreachable!("Should not have SOFT derivation detected here");
}
});
let (derived_pair, derived_seed_opt) = base_pair
.derive(derive_junctions_from_path.into_iter(), Some(base_seed))
.map_err(|e| GcliError::Input(format!("Failed to derive key: {e}")))?;
let derived_seed = derived_seed_opt.ok_or(GcliError::Input(format!("Derived seed should be present when base seed is provided (and no soft derivation is used) - base seed was:'0x{}'",hex::encode(base_seed))))?;
let seed_array: [u8; 32] = derived_seed
.as_slice()
.try_into()
.map_err(|_e| GcliError::Input("Derived seed should be 32 bytes".into()))?;
(derived_pair, seed_array)
} else {
(base_pair, base_seed)
};
let result_pair_account_id: AccountId = result_pair.public().into();
let pair_from_suri_account_id: AccountId = pair_from_suri_for_validation.public().into();
if result_pair_account_id != pair_from_suri_account_id {
return Err(GcliError::Input(format!("Computed pair has a different address: '{result_pair_account_id}' != '{pair_from_suri_account_id}'")));
}
Ok((result_pair, result_seed))
}
/// get mnemonic from predefined derivation path
pub fn predefined_suri(deriv: &str) -> String {
format!("{SUBSTRATE_MNEMONIC}//{deriv}")
......@@ -534,6 +660,7 @@ mod tests {
mod substrate {
use super::*;
use crate::keys::SUBSTRATE_MNEMONIC;
/// Testing sr25519 mnemonic derivations
///
......@@ -772,7 +899,7 @@ mod tests {
}
}
mod cesium {
mod g1v1 {
use super::*;
/// Test which verifies that it's possible to derive a key coming from a cesium v1 id & password
......@@ -806,7 +933,7 @@ mod tests {
/// SS58 Address: 5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf
/// ```
#[test]
fn test_cesium_v1_key_derivation() {
fn test_g1v1_key_derivation() {
let cesium_id = "test_cesium_id".to_string();
let cesium_pwd = "test_cesium_pwd".to_string();
......@@ -919,7 +1046,7 @@ mod tests {
/// SS58 Address: 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4
/// ```
#[test]
fn test_cesium_v1_seed_using_scrypt() {
fn test_g1v1_seed_using_scrypt() {
let cesium_id = "test_cesium_id".to_string();
let cesium_pwd = "test_cesium_pwd".to_string();
......@@ -963,4 +1090,239 @@ mod tests {
);
}
}
mod parameterized_tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"),
String::from("5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV"),
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
String::from("5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV"),
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"
),
String::from("5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH"),
String::from("0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393")
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"),
String::from("5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH"),
String::from("0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0//1"
),
String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"),
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e")
)]
#[case(
String::from(
"0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0//1"
),
String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"),
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e")
)]
#[case(
String::from("0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393//1"),
String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"),
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e")
)]
#[case(
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e"),
String::from("5Cr4pXhwGbmjQpWw86zYcym5QiixA9XEyMujtiFmHLMNE1wB"),
String::from("0x73fdf8bcfde4b4b014c84624f1e6f44ebd77e68c971780d31d09fa7699892c2e")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice"
),
String::from("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"),
String::from("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Alan"
),
String::from("5HHCrQYbfr1n9me9LoB4REHZZ9BQZAcw2dnbUKdzdXkFLEr6"),
String::from("0x06ace9363c66d855542d55d8b233c697db3bd7fe2fbdc6c34cefb94ad43eccf0")
)]
/// Expected data was retrieved using `subkey inspect` command
fn sr25519_compute_pair_and_mini_secret_seed_from_suri(
#[case] suri_string: String,
#[case] expected_address: String,
#[case] expected_seed: String,
) {
let (keypair, seed) = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&suri_string,
CryptoScheme::Sr25519,
)
.unwrap();
assert_eq!(format!("0x{}", hex::encode(seed)), expected_seed);
assert_eq!(keypair.address().to_string(), expected_address)
}
#[rstest]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk/0"),
String::from("Input(\"Soft derivation is not supported when trying to retrieve mini-secret/seed\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0/1"),
String::from("Input(\"Soft derivation is not supported when trying to retrieve mini-secret/seed\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass')\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass//0"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//0')\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0///pass//1"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//1')\")")
)]
fn sr25519_compute_pair_and_mini_secret_seed_from_suri_expecting_errors(
#[case] suri_string: String,
#[case] expected_error: String,
) {
let result = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&suri_string,
CryptoScheme::Sr25519,
);
match result {
Ok(_) => assert!(false, "expected error"),
Err(e) => assert_eq!(expected_error, e.to_string()),
}
}
#[rstest]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"),
String::from("5DFJF7tY4bpbpcKPJcBTQaKuCDEPCpiz8TRjpmLeTtweqmXL"),
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"),
String::from("5DFJF7tY4bpbpcKPJcBTQaKuCDEPCpiz8TRjpmLeTtweqmXL"),
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"
),
String::from("5HrCphkqYygSXWt9rHebqaqbfEYekhzjyjQNjZiPxpb3XsKY"),
String::from("0xf8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57")
)]
#[case(
String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"),
String::from("5HrCphkqYygSXWt9rHebqaqbfEYekhzjyjQNjZiPxpb3XsKY"),
String::from("0xf8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0//1"
),
String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"),
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab")
)]
#[case(
String::from(
"0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0//1"
),
String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"),
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab")
)]
#[case(
String::from("0xf8dfdb0f1103d9fb2905204ac32529d5f148761c4321b2865b0a40e15be75f57//1"),
String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"),
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab")
)]
#[case(
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab"),
String::from("5G4xo2TvB4Uv7MR1b35yWBW8g7WLaiuETWChtX1dHkyHrLEP"),
String::from("0xb91db3bc88f1302eff28cb05a3ca963d78b39cb8bef86837f80c1a5f8181c6ab")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice"
),
String::from("5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu"),
String::from("0xabf8e5bdbe30c65656c0a3cbd181ff8a56294a69dfedd27982aace4a76909115")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice//Alan"
),
String::from("5CvaztNtrHKyi4vPK4xHNRCyLdoWTFovSr9g2MekGUGEvBaS"),
String::from("0xb1f3996e8083bda16a43abfdbea6549bbfecdcc4b5c043a73645fff232765fca")
)]
/// Expected data was retrieved using `subkey inspect --scheme ed25519` command
fn ed25519_compute_pair_and_mini_secret_seed_from_suri(
#[case] suri_string: String,
#[case] expected_address: String,
#[case] expected_seed: String,
) {
let (keypair, seed) = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&suri_string,
CryptoScheme::Ed25519,
)
.unwrap();
assert_eq!(format!("0x{}", hex::encode(seed)), expected_seed);
assert_eq!(keypair.address().to_string(), expected_address)
}
#[rstest]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk/0"
),
String::from("Input(\"Invalid secret\")")
)]
#[case(
String::from(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//0/1"
),
String::from("Input(\"Invalid secret\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass')\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk///pass//0"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//0')\")")
)]
#[case(
String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0///pass//1"),
String::from("Input(\"Having a password in the derivation path is not supported (password:'pass//1')\")")
)]
fn ed25519_compute_pair_and_mini_secret_seed_from_suri_expecting_errors(
#[case] suri_string: String,
#[case] expected_error: String,
) {
let result = compute_pair_and_mini_secret_seed_from_suri_and_crypto_scheme(
&suri_string,
CryptoScheme::Ed25519,
);
match result {
Ok(_) => assert!(false, "expected error"),
Err(e) => assert_eq!(expected_error, e.to_string()),
}
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment