substrate_crypto_light/
common.rs1#[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
60pub 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 fn to_base58_string(&self, base58prefix: u16) -> String {
183 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 let first = ((ident & 0b0000_0000_1111_1100) as u8) >> 2;
190 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 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 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 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}