substrate_crypto_light/
common.rs

1#[cfg(feature = "std")]
2use std::{string::String, vec, vec::Vec};
3
4#[cfg(not(feature = "std"))]
5use alloc::{string::String, vec, vec::Vec};
6
7use base58::{FromBase58, ToBase58};
8use hmac::Hmac;
9use lazy_static::lazy_static;
10use parity_scale_codec::Encode;
11use pbkdf2::pbkdf2;
12use regex::Regex;
13use sha2::Sha512;
14use zeroize::Zeroize;
15
16use crate::error::Error;
17
18#[cfg(feature = "ecdsa")]
19use crate::ecdsa::{Public as EcdsaPublic, PUBLIC_LEN as ECDSA_PUBLIC_LEN};
20
21#[cfg(feature = "ed25519")]
22use crate::ed25519::{Public as Ed25519Public, PUBLIC_LEN as ED25519_PUBLIC_LEN};
23
24#[cfg(feature = "sr25519")]
25use crate::sr25519::{Public as Sr25519Public, PUBLIC_LEN as SR25519_PUBLIC_LEN};
26
27pub const ALICE_WORDS: &str =
28    "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
29
30pub const BIG_SEED_LEN: usize = 64;
31
32pub const HASH_256_LEN: usize = 32;
33pub const HASH_512_LEN: usize = 64;
34
35pub const BASE58_ID: &[u8] = b"SS58PRE";
36pub const BASE58_CHECKSUM_LEN: usize = 2;
37
38pub fn blake2_256(bytes: &[u8]) -> [u8; HASH_256_LEN] {
39    blake2b_simd::Params::new()
40        .hash_length(HASH_256_LEN)
41        .hash(bytes)
42        .as_bytes()
43        .try_into()
44        .expect("static length, always fits")
45}
46
47fn ss58hash(data: &[u8]) -> [u8; HASH_512_LEN] {
48    let mut blake2b_state = blake2b_simd::Params::new()
49        .hash_length(HASH_512_LEN)
50        .to_state();
51    blake2b_state.update(BASE58_ID);
52    blake2b_state.update(data);
53    blake2b_state
54        .finalize()
55        .as_bytes()
56        .try_into()
57        .expect("static length, always fits")
58}
59
60/// Verbatim from `substrate-bip39`.
61pub fn entropy_to_big_seed(entropy: &[u8], password: &str) -> Result<[u8; BIG_SEED_LEN], Error> {
62    if entropy.len() < 16 || entropy.len() > 32 || entropy.len() % 4 != 0 {
63        return Err(Error::InvalidEntropy);
64    }
65
66    let mut salt = String::with_capacity(8 + password.len());
67    salt.push_str("mnemonic");
68    salt.push_str(password);
69
70    let mut seed = [0u8; BIG_SEED_LEN];
71
72    let result = pbkdf2::<Hmac<Sha512>>(entropy, salt.as_bytes(), 2048, &mut seed);
73
74    salt.zeroize();
75
76    if result.is_ok() {
77        Ok(seed)
78    } else {
79        Err(Error::Pbkdf2Internal)
80    }
81}
82
83lazy_static! {
84    static ref REG_PATH_PWD: Regex =
85        Regex::new(r"^(?P<path>(//?[^/]+)*)(///(?P<password>.+))?$").expect("checked value");
86    static ref REG_DERIVATION: Regex =
87        Regex::new(r"/(?P<derivation>/?[^/]+)").expect("checked value");
88}
89
90#[derive(Debug)]
91pub enum DeriveJunction {
92    Hard([u8; HASH_256_LEN]),
93    Soft([u8; HASH_256_LEN]),
94}
95
96fn derive_junction_inner<T: Encode>(input: T) -> [u8; HASH_256_LEN] {
97    input.using_encoded(|encoded_input| {
98        if encoded_input.len() > HASH_256_LEN {
99            blake2_256(encoded_input)
100        } else {
101            let mut out = [0u8; HASH_256_LEN];
102            out[0..encoded_input.len()].copy_from_slice(encoded_input);
103            out
104        }
105    })
106}
107
108impl DeriveJunction {
109    pub fn soft<T: Encode>(input: T) -> Self {
110        DeriveJunction::Soft(derive_junction_inner(input))
111    }
112    pub fn hard<T: Encode>(input: T) -> Self {
113        DeriveJunction::Hard(derive_junction_inner(input))
114    }
115    pub fn is_soft(&self) -> bool {
116        matches!(self, DeriveJunction::Soft(_))
117    }
118    pub fn is_hard(&self) -> bool {
119        matches!(self, DeriveJunction::Hard(_))
120    }
121    pub fn inner(&self) -> [u8; HASH_256_LEN] {
122        match self {
123            DeriveJunction::Hard(inner) | DeriveJunction::Soft(inner) => *inner,
124        }
125    }
126}
127
128#[derive(Debug)]
129pub struct FullDerivation<'a> {
130    pub junctions: Vec<DeriveJunction>,
131    pub password: Option<&'a str>,
132}
133
134pub fn cut_path(path_and_pwd: &str) -> Option<FullDerivation<'_>> {
135    match REG_PATH_PWD.captures(path_and_pwd) {
136        Some(caps) => {
137            let junctions = {
138                if let Some(path) = caps.name("path") {
139                    REG_DERIVATION
140                        .captures_iter(path.as_str())
141                        .map(|caps| {
142                            let derivation = caps.name("derivation").expect("");
143                            let (is_hard, derivation_body) = {
144                                if derivation.as_str().starts_with('/') {
145                                    (true, &derivation.as_str()[1..])
146                                } else {
147                                    (false, derivation.as_str())
148                                }
149                            };
150                            if let Ok(number) = str::parse::<u64>(derivation_body) {
151                                if is_hard {
152                                    DeriveJunction::hard(number)
153                                } else {
154                                    DeriveJunction::soft(number)
155                                }
156                            } else if is_hard {
157                                DeriveJunction::hard(derivation_body)
158                            } else {
159                                DeriveJunction::soft(derivation_body)
160                            }
161                        })
162                        .collect()
163                } else {
164                    Vec::new()
165                }
166            };
167            let password = caps.name("password").map(|a| a.as_str());
168            Some(FullDerivation {
169                junctions,
170                password,
171            })
172        }
173        None => None,
174    }
175}
176
177pub trait AsBase58<const LEN: usize>: Sized {
178    fn inner(&self) -> [u8; LEN];
179    fn from_inner(inner: [u8; LEN]) -> Self;
180
181    /// Same as `to_ss58check_with_version()` method for `Ss58Codec` from `sp_core`, comments from `sp_core`.
182    fn to_base58_string(&self, base58prefix: u16) -> String {
183        // We mask out the upper two bits of the ident - SS58 Prefix currently only supports 14-bits
184        let ident: u16 = base58prefix & 0b0011_1111_1111_1111;
185        let mut v = match ident {
186            0..=63 => vec![ident as u8],
187            64..=16_383 => {
188                // upper six bits of the lower byte(!)
189                let first = ((ident & 0b0000_0000_1111_1100) as u8) >> 2;
190                // lower two bits of the lower byte in the high pos,
191                // lower bits of the upper byte in the low pos
192                let second = ((ident >> 8) as u8) | ((ident & 0b0000_0000_0000_0011) as u8) << 6;
193                vec![first | 0b0100_0000, second]
194            }
195            _ => unreachable!("masked out the upper two bits; qed"),
196        };
197        v.extend(self.inner());
198        let r = ss58hash(&v);
199        v.extend(&r[0..2]);
200        v.to_base58()
201    }
202
203    /// Same as `from_ss58check_with_version()` method for `Ss58Codec` from `sp_core`, comments from `sp_core`.
204    fn from_base58_string(base58_string: &str) -> Result<(Self, u16), Error> {
205        let data = base58_string.from_base58().map_err(Error::Base58Decoding)?;
206        if data.len() < 2 {
207            return Err(Error::Base58Length);
208        }
209        let (prefix_len, prefix) = match data[0] {
210            0..=63 => (1, data[0] as u16),
211            64..=127 => {
212                // weird bit manipulation owing to the combination of LE encoding and missing two
213                // bits from the left.
214                // d[0] d[1] are: 01aaaaaa bbcccccc
215                // they make the LE-encoded 16-bit value: aaaaaabb 00cccccc
216                // so the lower byte is formed of aaaaaabb and the higher byte is 00cccccc
217                let lower = (data[0] << 2) | (data[1] >> 6);
218                let upper = data[1] & 0b0011_1111;
219                (2, (lower as u16) | ((upper as u16) << 8))
220            }
221            _ => return Err(Error::Base58Prefix),
222        };
223        if data.len() != prefix_len + LEN + BASE58_CHECKSUM_LEN {
224            return Err(Error::Base58Length);
225        }
226        let hash = ss58hash(&data[..prefix_len + LEN]);
227        if data[prefix_len + LEN..prefix_len + LEN + BASE58_CHECKSUM_LEN]
228            != hash[..BASE58_CHECKSUM_LEN]
229        {
230            return Err(Error::Base58Checksum);
231        }
232        let inner = data[prefix_len..prefix_len + LEN]
233            .try_into()
234            .expect("static length, always fit");
235        Ok((Self::from_inner(inner), prefix))
236    }
237}
238
239pub const ACCOUNT_ID_32_LEN: usize = 32;
240
241#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
242pub struct AccountId32(pub [u8; ACCOUNT_ID_32_LEN]);
243
244macro_rules! impl_as_base58 {
245    ($($ty: ty, $len: expr), *) => {
246        $(
247            impl AsBase58<$len> for $ty {
248                fn inner(&self) -> [u8; $len] {
249                    self.0
250                }
251                fn from_inner(inner: [u8; $len]) -> Self {
252                    Self(inner)
253                }
254            }
255        )*
256    }
257}
258
259impl_as_base58!(AccountId32, ACCOUNT_ID_32_LEN);
260
261#[cfg(feature = "ecdsa")]
262impl_as_base58!(EcdsaPublic, ECDSA_PUBLIC_LEN);
263
264#[cfg(feature = "ed25519")]
265impl_as_base58!(Ed25519Public, ED25519_PUBLIC_LEN);
266
267#[cfg(feature = "sr25519")]
268impl_as_base58!(Sr25519Public, SR25519_PUBLIC_LEN);
269
270#[cfg(any(feature = "std", test))]
271#[cfg(test)]
272mod tests {
273
274    use mnemonic_external::{regular::InternalWordList, WordSet};
275    use sp_core::crypto::DeriveJunction;
276
277    use crate::common::{cut_path, entropy_to_big_seed, AccountId32, AsBase58, ALICE_WORDS};
278
279    #[test]
280    fn cut_path_test() {
281        let path_and_pwd =
282        "//alice/soft//hard//some_extraordinarily_long_derivation_just_for_test///secret_password";
283        let cut = cut_path(path_and_pwd).unwrap();
284        assert_eq!(cut.junctions.len(), 4);
285        assert!(cut.junctions[0].is_hard());
286        assert_eq!(
287            cut.junctions[0].inner(),
288            DeriveJunction::hard("alice").unwrap_inner()
289        );
290        assert!(cut.junctions[1].is_soft());
291        assert_eq!(
292            cut.junctions[1].inner(),
293            DeriveJunction::soft("soft").unwrap_inner()
294        );
295        assert!(cut.junctions[2].is_hard());
296        assert_eq!(
297            cut.junctions[2].inner(),
298            DeriveJunction::hard("hard").unwrap_inner()
299        );
300        assert!(cut.junctions[3].is_hard());
301        assert_eq!(
302            cut.junctions[3].inner(),
303            DeriveJunction::hard("some_extraordinarily_long_derivation_just_for_test")
304                .unwrap_inner()
305        );
306        assert_eq!(cut.password.unwrap(), "secret_password");
307    }
308
309    #[test]
310    fn big_seed_works() {
311        // phrase-to-entropy, with `mnemonic-external`
312        let internal_word_list = InternalWordList;
313        let mut word_set = WordSet::new();
314        for word in ALICE_WORDS.split(' ') {
315            word_set.add_word(word, &internal_word_list).unwrap();
316        }
317        let entropy = word_set.to_entropy().unwrap();
318
319        assert!(entropy_to_big_seed(&entropy, "").is_ok());
320    }
321
322    #[test]
323    fn from_base58() {
324        let base58 = "5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg";
325        let (account_id32, prefix) = AccountId32::from_base58_string(base58).unwrap();
326        assert_eq!(
327            hex::encode(account_id32.inner()),
328            "b6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30"
329        );
330        assert_eq!(prefix, 42u16);
331    }
332
333    #[test]
334    fn to_base58() {
335        let account_id32 = AccountId32(
336            hex::decode("b6a8b4b6bf796991065035093d3265e314c3fe89e75ccb623985e57b0c2e0c30")
337                .unwrap()
338                .try_into()
339                .unwrap(),
340        );
341        let prefix = 42u16;
342        let base58 = account_id32.to_base58_string(prefix);
343        assert_eq!(base58, "5GCCgshTQCfGkXy6kAkFDW1TZXAdsbCNZJ9Uz2c7ViBnwcVg");
344    }
345}