use alloc::{boxed::Box, string::String, vec::Vec};
use zeroize::Zeroize as _;
pub const DEFAULT_SEED_PHRASE: &str =
"bottom drive obey lake curtain smoke basket hold race lonely fit walk";
pub fn decode_sr25519_private_key(phrase: &str) -> Result<Box<[u8; 64]>, ParsePrivateKeyError> {
let parsed = parse_private_key(phrase)?;
let mini_key =
zeroize::Zeroizing::new(schnorrkel::MiniSecretKey::from_bytes(&*parsed.seed).unwrap());
let mut secret_key = mini_key
.expand_to_keypair(schnorrkel::ExpansionMode::Ed25519)
.secret
.clone();
for junction in parsed.path {
secret_key = match junction {
DeriveJunction::Soft(_) => todo!(), DeriveJunction::Hard(cc) => secret_key
.hard_derive_mini_secret_key(Some(schnorrkel::derive::ChainCode(cc)), b"")
.0
.expand(schnorrkel::ExpansionMode::Ed25519),
};
}
let bytes = zeroize::Zeroizing::new(secret_key.to_bytes());
let mut out = Box::new([0; 64]);
out.copy_from_slice(bytes.as_ref());
Ok(out)
}
pub fn decode_ed25519_private_key(phrase: &str) -> Result<Box<[u8; 32]>, ParsePrivateKeyError> {
let parsed = parse_private_key(phrase)?;
let mut secret_key = parsed.seed;
for junction in parsed.path {
secret_key = match junction {
DeriveJunction::Soft(_) => todo!(), DeriveJunction::Hard(cc) => {
let mut hash = blake2_rfc::blake2b::Blake2b::new(32);
hash.update(crate::util::encode_scale_compact_usize(11).as_ref()); hash.update(b"Ed25519HDKD");
hash.update(&*secret_key);
hash.update(&cc);
let mut out = Box::new([0; 32]);
out.copy_from_slice(hash.finalize().as_ref());
out
}
};
}
Ok(secret_key)
}
pub fn parse_private_key(phrase: &str) -> Result<ParsedPrivateKey, ParsePrivateKeyError> {
let parse_result: Result<_, nom::Err<nom::error::Error<&str>>> = nom::Parser::parse(
&mut nom::combinator::all_consuming((
nom::branch::alt((
nom::combinator::complete(nom::combinator::map(
nom::combinator::map_opt(
nom::sequence::preceded(
nom::bytes::streaming::tag("0x"),
nom::character::complete::hex_digit0,
),
|hex| {
let mut out = Box::new([0; 32]);
hex::decode_to_slice(hex, &mut *out).ok()?;
Some(out)
},
),
either::Left,
)),
nom::combinator::complete(nom::combinator::map(
nom::bytes::complete::take_till(|c| c == '/'),
either::Right,
)),
)),
nom::multi::many0(nom::branch::alt((
nom::combinator::complete(nom::combinator::map(
nom::sequence::preceded(
nom::bytes::streaming::tag("/"),
nom::bytes::complete::take_till1(|c| c == '/'),
),
|code| DeriveJunction::from_components(false, code),
)),
nom::combinator::complete(nom::combinator::map(
nom::sequence::preceded(
nom::bytes::streaming::tag("//"),
nom::bytes::complete::take_till1(|c| c == '/'),
),
|code| DeriveJunction::from_components(true, code),
)),
))),
nom::combinator::opt(nom::combinator::complete(nom::sequence::preceded(
nom::bytes::streaming::tag("///"),
|s| Ok(("", s)), ))),
)),
phrase,
);
match parse_result {
Ok((_, (either::Left(seed), path, _password))) => {
Ok(ParsedPrivateKey { seed, path })
}
Ok((_, (either::Right(phrase), path, password))) => {
let phrase = if phrase.is_empty() {
DEFAULT_SEED_PHRASE
} else {
phrase
};
Ok(ParsedPrivateKey {
seed: bip39_to_seed(phrase, password.unwrap_or(""))
.map_err(ParsePrivateKeyError::Bip39Decode)?,
path,
})
}
Err(_) => Err(ParsePrivateKeyError::InvalidFormat),
}
}
pub struct ParsedPrivateKey {
pub seed: Box<[u8; 32]>,
pub path: Vec<DeriveJunction>,
}
#[derive(Debug, derive_more::Display, derive_more::Error)]
pub enum ParsePrivateKeyError {
InvalidFormat,
Bip39Decode(Bip39ToSeedError),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum DeriveJunction {
Soft([u8; 32]),
Hard([u8; 32]),
}
impl DeriveJunction {
fn from_components(hard: bool, code: &str) -> DeriveJunction {
let mut chain_code = [0; 32];
if let Ok(n) = str::parse::<u64>(code) {
chain_code[..8].copy_from_slice(&n.to_le_bytes());
} else {
let code = code.as_bytes();
let code_len_prefix = crate::util::encode_scale_compact_usize(code.len());
let code_len_prefix = code_len_prefix.as_ref();
if code_len_prefix.len() + code.len() > 32 {
let mut hash = blake2_rfc::blake2b::Blake2b::new(32);
hash.update(code_len_prefix);
hash.update(code);
chain_code.copy_from_slice(hash.finalize().as_bytes());
} else {
chain_code[..code_len_prefix.len()].copy_from_slice(code_len_prefix);
chain_code[code_len_prefix.len()..][..code.len()].copy_from_slice(code);
}
}
if hard {
DeriveJunction::Hard(chain_code)
} else {
DeriveJunction::Soft(chain_code)
}
}
}
pub fn bip39_to_seed(phrase: &str, password: &str) -> Result<Box<[u8; 32]>, Bip39ToSeedError> {
let parsed = bip39::Mnemonic::parse_in_normalized(bip39::Language::English, phrase)
.map_err(|err| Bip39ToSeedError::WrongMnemonic(Bip39DecodeError(err)))?;
let (entropy, entropy_len) = parsed.to_entropy_array();
if !(16..=32).contains(&entropy_len) || entropy_len % 4 != 0 {
return Err(Bip39ToSeedError::BadWordsCount);
}
let mut salt = zeroize::Zeroizing::new(String::with_capacity(8 + password.len()));
salt.push_str("mnemonic");
salt.push_str(password);
let mut seed_too_long = Box::new([0u8; 64]);
pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha512>>(
&entropy[..entropy_len],
salt.as_bytes(),
2048,
&mut *seed_too_long,
)
.unwrap();
let mut seed = Box::new([0u8; 32]);
seed.copy_from_slice(&seed_too_long[..32]);
seed_too_long.zeroize();
Ok(seed)
}
#[derive(Debug, derive_more::Display, derive_more::Error)]
pub enum Bip39ToSeedError {
WrongMnemonic(Bip39DecodeError),
BadWordsCount,
}
#[derive(Debug, derive_more::Display, derive_more::Error)]
pub struct Bip39DecodeError(#[error(not(source))] bip39::Error);
#[cfg(test)]
mod tests {
#[test]
fn empty_matches_sr25519() {
assert_eq!(
*super::decode_sr25519_private_key("").unwrap(),
[
5, 214, 85, 132, 99, 13, 22, 205, 74, 246, 208, 190, 193, 15, 52, 187, 80, 74, 93,
203, 98, 219, 162, 18, 45, 73, 245, 166, 99, 118, 61, 10, 253, 25, 12, 206, 116,
223, 53, 100, 50, 180, 16, 189, 100, 104, 35, 9, 214, 222, 219, 39, 199, 104, 69,
218, 243, 136, 85, 124, 186, 195, 202, 52
]
);
}
#[test]
fn empty_matches_ed25519() {
assert_eq!(
*super::decode_ed25519_private_key("").unwrap(),
[
250, 199, 149, 157, 191, 231, 47, 5, 46, 90, 12, 60, 141, 101, 48, 242, 2, 176, 47,
216, 249, 245, 202, 53, 128, 236, 141, 235, 119, 151, 71, 158
]
);
}
#[test]
fn default_seed_is_correct_sr25519() {
assert_eq!(
super::decode_sr25519_private_key(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk"
)
.unwrap(),
super::decode_sr25519_private_key("").unwrap(),
);
assert_eq!(
super::decode_sr25519_private_key(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//smoldot rules//125"
)
.unwrap(),
super::decode_sr25519_private_key("//smoldot rules//125").unwrap(),
);
}
#[test]
fn default_seed_is_correct_ed25519() {
assert_eq!(
super::decode_ed25519_private_key(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk"
)
.unwrap(),
super::decode_ed25519_private_key("").unwrap(),
);
assert_eq!(
super::decode_ed25519_private_key(
"bottom drive obey lake curtain smoke basket hold race lonely fit walk//smoldot rules//125"
)
.unwrap(),
super::decode_ed25519_private_key("//smoldot rules//125").unwrap(),
);
}
#[test]
fn alice_matches_sr25519() {
assert_eq!(
*super::decode_sr25519_private_key("//Alice").unwrap(),
[
51, 166, 243, 9, 63, 21, 138, 113, 9, 246, 121, 65, 11, 239, 26, 12, 84, 22, 129,
69, 224, 206, 203, 77, 240, 6, 193, 194, 255, 251, 31, 9, 146, 90, 34, 93, 151,
170, 0, 104, 45, 106, 89, 185, 91, 24, 120, 12, 16, 215, 3, 35, 54, 232, 143, 52,
66, 180, 35, 97, 244, 166, 96, 17,
]
);
}
#[test]
fn alice_matches_ed25519() {
assert_eq!(
*super::decode_ed25519_private_key("//Alice").unwrap(),
[
171, 248, 229, 189, 190, 48, 198, 86, 86, 192, 163, 203, 209, 129, 255, 138, 86,
41, 74, 105, 223, 237, 210, 121, 130, 170, 206, 74, 118, 144, 145, 21
]
);
}
#[test]
fn hex_seed_matches_sr25519() {
assert_eq!(
*super::decode_sr25519_private_key(
"0x0000000000000000000000000000000000000000000000000000000000000000"
)
.unwrap(),
[
202, 168, 53, 120, 27, 21, 199, 112, 111, 101, 183, 31, 122, 88, 200, 7, 171, 54,
15, 174, 214, 68, 15, 178, 62, 15, 76, 82, 233, 48, 222, 10, 10, 106, 133, 234,
166, 66, 218, 200, 53, 66, 75, 93, 124, 141, 99, 124, 0, 64, 140, 122, 115, 218,
103, 43, 127, 73, 133, 33, 66, 11, 109, 211
]
);
}
#[test]
fn hex_seed_matches_ed25519() {
assert_eq!(
*super::decode_ed25519_private_key(
"0x0000000000000000000000000000000000000000000000000000000000000000"
)
.unwrap(),
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0
]
);
}
#[test]
fn multi_derivation_and_password_sr25519() {
assert_eq!(
*super::decode_sr25519_private_key("strong isolate job basic auto frozen want garlic autumn height riot desert//foo//2//baz///my_password").unwrap(),
[144, 209, 243, 24, 75, 220, 185, 255, 47, 39, 160, 1, 179, 74, 230, 178, 26, 1, 64, 139, 194, 14, 123, 204, 213, 105, 88, 17, 142, 68, 198, 10, 101, 57, 5, 124, 59, 208, 57, 242, 223, 43, 140, 191, 21, 56, 88, 79, 192, 241, 237, 195, 169, 103, 244, 249, 36, 90, 106, 10, 109, 40, 29, 73]
);
}
#[test]
fn multi_derivation_and_password_ed25519() {
assert_eq!(
*super::decode_ed25519_private_key("strong isolate job basic auto frozen want garlic autumn height riot desert//foo//2//baz///my_password").unwrap(),
[95, 205, 122, 218, 56, 195, 127, 158, 30, 205, 82, 84, 159, 120, 105, 63, 210, 155, 217, 74, 40, 142, 70, 179, 11, 75, 82, 143, 219, 208, 86, 245]
);
}
}