/__w/smoldot/smoldot/repo/lib/src/identity/seed_phrase.rs
Line | Count | Source (jump to first uncovered line) |
1 | | // Smoldot |
2 | | // Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. |
3 | | // SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 |
4 | | |
5 | | // This program is free software: you can redistribute it and/or modify |
6 | | // it under the terms of the GNU General Public License as published by |
7 | | // the Free Software Foundation, either version 3 of the License, or |
8 | | // (at your option) any later version. |
9 | | |
10 | | // This program is distributed in the hope that it will be useful, |
11 | | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | | // GNU General Public License for more details. |
14 | | |
15 | | // You should have received a copy of the GNU General Public License |
16 | | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | | |
18 | | use alloc::{boxed::Box, string::String, vec::Vec}; |
19 | | use zeroize::Zeroize as _; |
20 | | |
21 | | // TODO: unclear what purpose soft derivations serve |
22 | | |
23 | | /// Default seed phrase used when decoding a private key in case no seed is provided. |
24 | | /// |
25 | | /// This seed phrase is publicly-known and is meant to be used to create keys for testing purposes |
26 | | /// only. |
27 | | pub const DEFAULT_SEED_PHRASE: &str = |
28 | | "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; |
29 | | |
30 | | /// Decodes a human-readable private key (a.k.a. a seed phrase) using the Sr25519 curve. |
31 | | /// |
32 | | /// > **Note**: The key is returned within a `Box` in order to guarantee that no trace of the |
33 | | /// > secret key is accidentally left in memory due to automatic copies of stack data. |
34 | 10 | pub fn decode_sr25519_private_key(phrase: &str) -> Result<Box<[u8; 64]>, ParsePrivateKeyError> { |
35 | 10 | let parsed = parse_private_key(phrase)?0 ; |
36 | | |
37 | | // Note: `from_bytes` can only panic if the slice is of the wrong length, which we know can |
38 | | // never happen. |
39 | 10 | let mini_key = |
40 | 10 | zeroize::Zeroizing::new(schnorrkel::MiniSecretKey::from_bytes(&*parsed.seed).unwrap()); |
41 | 10 | |
42 | 10 | let mut secret_key = mini_key |
43 | 10 | .expand_to_keypair(schnorrkel::ExpansionMode::Ed25519) |
44 | 10 | .secret |
45 | 10 | .clone(); |
46 | | |
47 | 18 | for junction8 in parsed.path { |
48 | 8 | secret_key = match junction { |
49 | 0 | DeriveJunction::Soft(_) => todo!(), // TODO: return error |
50 | 8 | DeriveJunction::Hard(cc) => secret_key |
51 | 8 | .hard_derive_mini_secret_key(Some(schnorrkel::derive::ChainCode(cc)), b"") |
52 | 8 | .0 |
53 | 8 | .expand(schnorrkel::ExpansionMode::Ed25519), |
54 | | }; |
55 | | } |
56 | | |
57 | | // TODO: unclear if the zeroizing works, we probably need some tweaks in the schnorrkel library |
58 | 10 | let bytes = zeroize::Zeroizing::new(secret_key.to_bytes()); |
59 | 10 | let mut out = Box::new([0; 64]); |
60 | 10 | out.copy_from_slice(bytes.as_ref()); |
61 | 10 | Ok(out) |
62 | 10 | } _RNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase26decode_sr25519_private_key Line | Count | Source | 34 | 10 | pub fn decode_sr25519_private_key(phrase: &str) -> Result<Box<[u8; 64]>, ParsePrivateKeyError> { | 35 | 10 | let parsed = parse_private_key(phrase)?0 ; | 36 | | | 37 | | // Note: `from_bytes` can only panic if the slice is of the wrong length, which we know can | 38 | | // never happen. | 39 | 10 | let mini_key = | 40 | 10 | zeroize::Zeroizing::new(schnorrkel::MiniSecretKey::from_bytes(&*parsed.seed).unwrap()); | 41 | 10 | | 42 | 10 | let mut secret_key = mini_key | 43 | 10 | .expand_to_keypair(schnorrkel::ExpansionMode::Ed25519) | 44 | 10 | .secret | 45 | 10 | .clone(); | 46 | | | 47 | 18 | for junction8 in parsed.path { | 48 | 8 | secret_key = match junction { | 49 | 0 | DeriveJunction::Soft(_) => todo!(), // TODO: return error | 50 | 8 | DeriveJunction::Hard(cc) => secret_key | 51 | 8 | .hard_derive_mini_secret_key(Some(schnorrkel::derive::ChainCode(cc)), b"") | 52 | 8 | .0 | 53 | 8 | .expand(schnorrkel::ExpansionMode::Ed25519), | 54 | | }; | 55 | | } | 56 | | | 57 | | // TODO: unclear if the zeroizing works, we probably need some tweaks in the schnorrkel library | 58 | 10 | let bytes = zeroize::Zeroizing::new(secret_key.to_bytes()); | 59 | 10 | let mut out = Box::new([0; 64]); | 60 | 10 | out.copy_from_slice(bytes.as_ref()); | 61 | 10 | Ok(out) | 62 | 10 | } |
Unexecuted instantiation: _RNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase26decode_sr25519_private_key |
63 | | |
64 | | /// Decodes a human-readable private key (a.k.a. a seed phrase) using the Ed25519 curve. |
65 | | /// |
66 | | /// > **Note**: The key is returned within a `Box` in order to guarantee that no trace of the |
67 | | /// > secret key is accidentally left in memory due to automatic copies of stack data. |
68 | 10 | pub fn decode_ed25519_private_key(phrase: &str) -> Result<Box<[u8; 32]>, ParsePrivateKeyError> { |
69 | 10 | let parsed = parse_private_key(phrase)?0 ; |
70 | | |
71 | 10 | let mut secret_key = parsed.seed; |
72 | 18 | for junction8 in parsed.path { |
73 | 8 | secret_key = match junction { |
74 | 0 | DeriveJunction::Soft(_) => todo!(), // TODO: return error |
75 | 8 | DeriveJunction::Hard(cc) => { |
76 | 8 | let mut hash = blake2_rfc::blake2b::Blake2b::new(32); |
77 | 8 | hash.update(crate::util::encode_scale_compact_usize(11).as_ref()); // Length of `"Ed25519HDKD"` |
78 | 8 | hash.update(b"Ed25519HDKD"); |
79 | 8 | hash.update(&*secret_key); |
80 | 8 | hash.update(&cc); |
81 | 8 | |
82 | 8 | let mut out = Box::new([0; 32]); |
83 | 8 | out.copy_from_slice(hash.finalize().as_ref()); |
84 | 8 | // TODO: `hash` should be zero'ed on drop :-/ |
85 | 8 | out |
86 | | } |
87 | | }; |
88 | | } |
89 | | |
90 | 10 | Ok(secret_key) |
91 | 10 | } _RNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase26decode_ed25519_private_key Line | Count | Source | 68 | 10 | pub fn decode_ed25519_private_key(phrase: &str) -> Result<Box<[u8; 32]>, ParsePrivateKeyError> { | 69 | 10 | let parsed = parse_private_key(phrase)?0 ; | 70 | | | 71 | 10 | let mut secret_key = parsed.seed; | 72 | 18 | for junction8 in parsed.path { | 73 | 8 | secret_key = match junction { | 74 | 0 | DeriveJunction::Soft(_) => todo!(), // TODO: return error | 75 | 8 | DeriveJunction::Hard(cc) => { | 76 | 8 | let mut hash = blake2_rfc::blake2b::Blake2b::new(32); | 77 | 8 | hash.update(crate::util::encode_scale_compact_usize(11).as_ref()); // Length of `"Ed25519HDKD"` | 78 | 8 | hash.update(b"Ed25519HDKD"); | 79 | 8 | hash.update(&*secret_key); | 80 | 8 | hash.update(&cc); | 81 | 8 | | 82 | 8 | let mut out = Box::new([0; 32]); | 83 | 8 | out.copy_from_slice(hash.finalize().as_ref()); | 84 | 8 | // TODO: `hash` should be zero'ed on drop :-/ | 85 | 8 | out | 86 | | } | 87 | | }; | 88 | | } | 89 | | | 90 | 10 | Ok(secret_key) | 91 | 10 | } |
Unexecuted instantiation: _RNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase26decode_ed25519_private_key |
92 | | |
93 | | /// Turns a human-readable private key (a.k.a. a seed phrase) into a seed and a derivation path. |
94 | 20 | pub fn parse_private_key(phrase: &str) -> Result<ParsedPrivateKey, ParsePrivateKeyError> { |
95 | 20 | let parse_result: Result<_, nom::Err<nom::error::Error<&str>>> = |
96 | 20 | nom::combinator::all_consuming(nom::sequence::tuple(( |
97 | 20 | // Either BIP39 words or some hexadecimal |
98 | 20 | nom::branch::alt(( |
99 | 20 | // Hexadecimal. Wrapped in `either::Left` |
100 | 20 | nom::combinator::complete(nom::combinator::map( |
101 | 20 | nom::combinator::map_opt( |
102 | 20 | nom::sequence::preceded( |
103 | 20 | nom::bytes::streaming::tag("0x"), |
104 | 20 | nom::character::complete::hex_digit0, |
105 | 20 | ), |
106 | 20 | |hex| { |
107 | 6 | let mut out = Box::new([0; 32]); |
108 | 6 | hex::decode_to_slice(hex, &mut *out).ok()?0 ; |
109 | 6 | Some(out) |
110 | 20 | }6 , _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase17parse_private_key0B7_ Line | Count | Source | 106 | 6 | |hex| { | 107 | 6 | let mut out = Box::new([0; 32]); | 108 | 6 | hex::decode_to_slice(hex, &mut *out).ok()?0 ; | 109 | 6 | Some(out) | 110 | 6 | }, |
Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase17parse_private_key0B7_ |
111 | 20 | ), |
112 | 20 | either::Left, |
113 | 20 | )), |
114 | 20 | // BIP39. Wrapped in `either::Right` |
115 | 20 | nom::combinator::complete(nom::combinator::map( |
116 | 432 | nom::bytes::complete::take_till(|c| c == '/'), _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase17parse_private_keys_0B7_ Line | Count | Source | 116 | 432 | nom::bytes::complete::take_till(|c| c == '/'), |
Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase17parse_private_keys_0B7_ |
117 | 20 | either::Right, |
118 | 20 | )), |
119 | 20 | )), |
120 | 20 | // Derivation path |
121 | 20 | nom::multi::many0(nom::branch::alt(( |
122 | 20 | // Soft |
123 | 20 | nom::combinator::complete(nom::combinator::map( |
124 | 20 | nom::sequence::preceded( |
125 | 20 | nom::bytes::streaming::tag("/"), |
126 | 20 | nom::bytes::complete::take_till1(|c| c == '/'18 ), _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase17parse_private_keys0_0B7_ Line | Count | Source | 126 | 18 | nom::bytes::complete::take_till1(|c| c == '/'), |
Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase17parse_private_keys0_0B7_ |
127 | 20 | ), |
128 | 20 | |code| DeriveJunction::from_components(false, code)0 , Unexecuted instantiation: _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase17parse_private_keys1_0B7_ Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase17parse_private_keys1_0B7_ |
129 | 20 | )), |
130 | 20 | // Hard |
131 | 20 | nom::combinator::complete(nom::combinator::map( |
132 | 20 | nom::sequence::preceded( |
133 | 20 | nom::bytes::streaming::tag("//"), |
134 | 100 | nom::bytes::complete::take_till1(|c| c == '/'), _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase17parse_private_keys2_0B7_ Line | Count | Source | 134 | 100 | nom::bytes::complete::take_till1(|c| c == '/'), |
Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase17parse_private_keys2_0B7_ |
135 | 20 | ), |
136 | 20 | |code| DeriveJunction::from_components(true, code)16 , _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase17parse_private_keys3_0B7_ Line | Count | Source | 136 | 16 | |code| DeriveJunction::from_components(true, code), |
Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase17parse_private_keys3_0B7_ |
137 | 20 | )), |
138 | 20 | ))), |
139 | 20 | // Optional password |
140 | 20 | nom::combinator::opt(nom::combinator::complete(nom::sequence::preceded( |
141 | 20 | nom::bytes::streaming::tag("///"), |
142 | 20 | |s| Ok(("", s))2 , // Take the rest of the input after the `///` _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase17parse_private_keys4_0B7_ Line | Count | Source | 142 | 2 | |s| Ok(("", s)), // Take the rest of the input after the `///` |
Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase17parse_private_keys4_0B7_ |
143 | 20 | ))), |
144 | 20 | )))(phrase); |
145 | | |
146 | 20 | match parse_result { |
147 | 6 | Ok((_, (either::Left(seed), path, _password))) => { |
148 | 6 | // Hexadecimal seed |
149 | 6 | // TODO: what if there's a password? do we just ignore it? |
150 | 6 | Ok(ParsedPrivateKey { seed, path }) |
151 | | } |
152 | 14 | Ok((_, (either::Right(phrase), path, password))) => { |
153 | | // BIP39 words |
154 | 14 | let phrase = if phrase.is_empty() { |
155 | 8 | DEFAULT_SEED_PHRASE |
156 | | } else { |
157 | 6 | phrase |
158 | | }; |
159 | | |
160 | | Ok(ParsedPrivateKey { |
161 | 14 | seed: bip39_to_seed(phrase, password.unwrap_or("")) |
162 | 14 | .map_err(ParsePrivateKeyError::Bip39Decode)?0 , |
163 | 14 | path, |
164 | | }) |
165 | | } |
166 | 0 | Err(_) => Err(ParsePrivateKeyError::InvalidFormat), |
167 | | } |
168 | 20 | } _RNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase17parse_private_key Line | Count | Source | 94 | 20 | pub fn parse_private_key(phrase: &str) -> Result<ParsedPrivateKey, ParsePrivateKeyError> { | 95 | 20 | let parse_result: Result<_, nom::Err<nom::error::Error<&str>>> = | 96 | 20 | nom::combinator::all_consuming(nom::sequence::tuple(( | 97 | 20 | // Either BIP39 words or some hexadecimal | 98 | 20 | nom::branch::alt(( | 99 | 20 | // Hexadecimal. Wrapped in `either::Left` | 100 | 20 | nom::combinator::complete(nom::combinator::map( | 101 | 20 | nom::combinator::map_opt( | 102 | 20 | nom::sequence::preceded( | 103 | 20 | nom::bytes::streaming::tag("0x"), | 104 | 20 | nom::character::complete::hex_digit0, | 105 | 20 | ), | 106 | 20 | |hex| { | 107 | | let mut out = Box::new([0; 32]); | 108 | | hex::decode_to_slice(hex, &mut *out).ok()?; | 109 | | Some(out) | 110 | 20 | }, | 111 | 20 | ), | 112 | 20 | either::Left, | 113 | 20 | )), | 114 | 20 | // BIP39. Wrapped in `either::Right` | 115 | 20 | nom::combinator::complete(nom::combinator::map( | 116 | 20 | nom::bytes::complete::take_till(|c| c == '/'), | 117 | 20 | either::Right, | 118 | 20 | )), | 119 | 20 | )), | 120 | 20 | // Derivation path | 121 | 20 | nom::multi::many0(nom::branch::alt(( | 122 | 20 | // Soft | 123 | 20 | nom::combinator::complete(nom::combinator::map( | 124 | 20 | nom::sequence::preceded( | 125 | 20 | nom::bytes::streaming::tag("/"), | 126 | 20 | nom::bytes::complete::take_till1(|c| c == '/'), | 127 | 20 | ), | 128 | 20 | |code| DeriveJunction::from_components(false, code), | 129 | 20 | )), | 130 | 20 | // Hard | 131 | 20 | nom::combinator::complete(nom::combinator::map( | 132 | 20 | nom::sequence::preceded( | 133 | 20 | nom::bytes::streaming::tag("//"), | 134 | 20 | nom::bytes::complete::take_till1(|c| c == '/'), | 135 | 20 | ), | 136 | 20 | |code| DeriveJunction::from_components(true, code), | 137 | 20 | )), | 138 | 20 | ))), | 139 | 20 | // Optional password | 140 | 20 | nom::combinator::opt(nom::combinator::complete(nom::sequence::preceded( | 141 | 20 | nom::bytes::streaming::tag("///"), | 142 | 20 | |s| Ok(("", s)), // Take the rest of the input after the `///` | 143 | 20 | ))), | 144 | 20 | )))(phrase); | 145 | | | 146 | 20 | match parse_result { | 147 | 6 | Ok((_, (either::Left(seed), path, _password))) => { | 148 | 6 | // Hexadecimal seed | 149 | 6 | // TODO: what if there's a password? do we just ignore it? | 150 | 6 | Ok(ParsedPrivateKey { seed, path }) | 151 | | } | 152 | 14 | Ok((_, (either::Right(phrase), path, password))) => { | 153 | | // BIP39 words | 154 | 14 | let phrase = if phrase.is_empty() { | 155 | 8 | DEFAULT_SEED_PHRASE | 156 | | } else { | 157 | 6 | phrase | 158 | | }; | 159 | | | 160 | | Ok(ParsedPrivateKey { | 161 | 14 | seed: bip39_to_seed(phrase, password.unwrap_or("")) | 162 | 14 | .map_err(ParsePrivateKeyError::Bip39Decode)?0 , | 163 | 14 | path, | 164 | | }) | 165 | | } | 166 | 0 | Err(_) => Err(ParsePrivateKeyError::InvalidFormat), | 167 | | } | 168 | 20 | } |
Unexecuted instantiation: _RNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase17parse_private_key |
169 | | |
170 | | /// Successful outcome of [`parse_private_key`]. |
171 | | pub struct ParsedPrivateKey { |
172 | | /// Base seed phrase. Must be derived through [`ParsedPrivateKey::path`] to obtain the final |
173 | | /// result. |
174 | | /// |
175 | | /// > **Note**: The key is embedded within a `Box` in order to guarantee that no trace of the |
176 | | /// > secret key is accidentally left in memory due to automatic copies of stack |
177 | | /// > data. |
178 | | pub seed: Box<[u8; 32]>, |
179 | | |
180 | | /// Derivation path found in the secret phrase. |
181 | | pub path: Vec<DeriveJunction>, |
182 | | } |
183 | | |
184 | | /// Error in [`parse_private_key`]. |
185 | 0 | #[derive(Debug, derive_more::Display)] Unexecuted instantiation: _RNvXs0_NtNtCsN16ciHI6Qf_7smoldot8identity11seed_phraseNtB5_20ParsePrivateKeyErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt Unexecuted instantiation: _RNvXs0_NtNtCseuYC0Zibziv_7smoldot8identity11seed_phraseNtB5_20ParsePrivateKeyErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt |
186 | | pub enum ParsePrivateKeyError { |
187 | | /// Couldn't parse the string in any meaningful way. |
188 | | InvalidFormat, |
189 | | /// Failed to decode the provided BIP39 seed phrase. |
190 | | Bip39Decode(Bip39ToSeedError), |
191 | | } |
192 | | |
193 | | #[derive(Debug, Copy, Clone, PartialEq, Eq)] |
194 | | pub enum DeriveJunction { |
195 | | Soft([u8; 32]), |
196 | | Hard([u8; 32]), |
197 | | } |
198 | | |
199 | | impl DeriveJunction { |
200 | 16 | fn from_components(hard: bool, code: &str) -> DeriveJunction { |
201 | 16 | // The algorithm here is the same as in Substrate, but way more readable. |
202 | 16 | let mut chain_code = [0; 32]; |
203 | 16 | if let Ok(n6 ) = str::parse::<u64>(code) { |
204 | 6 | chain_code[..8].copy_from_slice(&n.to_le_bytes()); |
205 | 6 | } else { |
206 | | // A SCALE-compact-encoded length prefix is added in front of the path. |
207 | 10 | let code = code.as_bytes(); |
208 | 10 | let code_len_prefix = crate::util::encode_scale_compact_usize(code.len()); |
209 | 10 | let code_len_prefix = code_len_prefix.as_ref(); |
210 | 10 | |
211 | 10 | if code_len_prefix.len() + code.len() > 32 { |
212 | 0 | let mut hash = blake2_rfc::blake2b::Blake2b::new(32); |
213 | 0 | hash.update(code_len_prefix); |
214 | 0 | hash.update(code); |
215 | 0 | chain_code.copy_from_slice(hash.finalize().as_bytes()); |
216 | 10 | } else { |
217 | 10 | chain_code[..code_len_prefix.len()].copy_from_slice(code_len_prefix); |
218 | 10 | chain_code[code_len_prefix.len()..][..code.len()].copy_from_slice(code); |
219 | 10 | } |
220 | | } |
221 | | |
222 | 16 | if hard { |
223 | 16 | DeriveJunction::Hard(chain_code) |
224 | | } else { |
225 | 0 | DeriveJunction::Soft(chain_code) |
226 | | } |
227 | 16 | } _RNvMNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phraseNtB2_14DeriveJunction15from_components Line | Count | Source | 200 | 16 | fn from_components(hard: bool, code: &str) -> DeriveJunction { | 201 | 16 | // The algorithm here is the same as in Substrate, but way more readable. | 202 | 16 | let mut chain_code = [0; 32]; | 203 | 16 | if let Ok(n6 ) = str::parse::<u64>(code) { | 204 | 6 | chain_code[..8].copy_from_slice(&n.to_le_bytes()); | 205 | 6 | } else { | 206 | | // A SCALE-compact-encoded length prefix is added in front of the path. | 207 | 10 | let code = code.as_bytes(); | 208 | 10 | let code_len_prefix = crate::util::encode_scale_compact_usize(code.len()); | 209 | 10 | let code_len_prefix = code_len_prefix.as_ref(); | 210 | 10 | | 211 | 10 | if code_len_prefix.len() + code.len() > 32 { | 212 | 0 | let mut hash = blake2_rfc::blake2b::Blake2b::new(32); | 213 | 0 | hash.update(code_len_prefix); | 214 | 0 | hash.update(code); | 215 | 0 | chain_code.copy_from_slice(hash.finalize().as_bytes()); | 216 | 10 | } else { | 217 | 10 | chain_code[..code_len_prefix.len()].copy_from_slice(code_len_prefix); | 218 | 10 | chain_code[code_len_prefix.len()..][..code.len()].copy_from_slice(code); | 219 | 10 | } | 220 | | } | 221 | | | 222 | 16 | if hard { | 223 | 16 | DeriveJunction::Hard(chain_code) | 224 | | } else { | 225 | 0 | DeriveJunction::Soft(chain_code) | 226 | | } | 227 | 16 | } |
Unexecuted instantiation: _RNvMNtNtCseuYC0Zibziv_7smoldot8identity11seed_phraseNtB2_14DeriveJunction15from_components |
228 | | } |
229 | | |
230 | | /// Turns a BIP39 seed phrase into a 32 bytes cryptographic seed. |
231 | | /// |
232 | | /// > **Note**: The key is returned within a `Box` in order to guarantee that no trace of the |
233 | | /// > secret key is accidentally left in memory due to automatic copies of stack data. |
234 | 14 | pub fn bip39_to_seed(phrase: &str, password: &str) -> Result<Box<[u8; 32]>, Bip39ToSeedError> { |
235 | 14 | let parsed = bip39::Mnemonic::parse_in_normalized(bip39::Language::English, phrase) |
236 | 14 | .map_err(|err| Bip39ToSeedError::WrongMnemonic(Bip39DecodeError(err))0 )?0 ; Unexecuted instantiation: _RNCNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase13bip39_to_seed0B7_ Unexecuted instantiation: _RNCNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase13bip39_to_seed0B7_ |
237 | | |
238 | | // Note that the `bip39` library implementation that turns the mnemonic to a seed isn't |
239 | | // conformant to the BIP39 specification. Instead, we do it manually. |
240 | | |
241 | | // `to_entropy_array()` returns the entropy as an array where only the first `entropy_len` |
242 | | // bytes are meaningful. `entropy_len` depends on the number of words provided. |
243 | 14 | let (entropy, entropy_len) = parsed.to_entropy_array(); |
244 | 14 | |
245 | 14 | // These rules are part of the seed phrase format "specification" and have been copy-pasted |
246 | 14 | // from the Substrate code base. |
247 | 14 | if !(16..=32).contains(&entropy_len) || entropy_len % 4 != 0 { |
248 | 0 | return Err(Bip39ToSeedError::BadWordsCount); |
249 | 14 | } |
250 | 14 | |
251 | 14 | let mut salt = zeroize::Zeroizing::new(String::with_capacity(8 + password.len())); |
252 | 14 | salt.push_str("mnemonic"); |
253 | 14 | salt.push_str(password); |
254 | 14 | |
255 | 14 | // This function returns an error only in case of wrong buffer length, making it safe to |
256 | 14 | // unwrap. |
257 | 14 | let mut seed_too_long = Box::new([0u8; 64]); |
258 | 14 | pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha512>>( |
259 | 14 | &entropy[..entropy_len], |
260 | 14 | salt.as_bytes(), |
261 | 14 | 2048, |
262 | 14 | &mut *seed_too_long, |
263 | 14 | ) |
264 | 14 | .unwrap(); |
265 | 14 | |
266 | 14 | // The seed is truncated to 32 bytes. |
267 | 14 | let mut seed = Box::new([0u8; 32]); |
268 | 14 | seed.copy_from_slice(&seed_too_long[..32]); |
269 | 14 | seed_too_long.zeroize(); |
270 | 14 | |
271 | 14 | Ok(seed) |
272 | 14 | } _RNvNtNtCsN16ciHI6Qf_7smoldot8identity11seed_phrase13bip39_to_seed Line | Count | Source | 234 | 14 | pub fn bip39_to_seed(phrase: &str, password: &str) -> Result<Box<[u8; 32]>, Bip39ToSeedError> { | 235 | 14 | let parsed = bip39::Mnemonic::parse_in_normalized(bip39::Language::English, phrase) | 236 | 14 | .map_err(|err| Bip39ToSeedError::WrongMnemonic(Bip39DecodeError(err)))?0 ; | 237 | | | 238 | | // Note that the `bip39` library implementation that turns the mnemonic to a seed isn't | 239 | | // conformant to the BIP39 specification. Instead, we do it manually. | 240 | | | 241 | | // `to_entropy_array()` returns the entropy as an array where only the first `entropy_len` | 242 | | // bytes are meaningful. `entropy_len` depends on the number of words provided. | 243 | 14 | let (entropy, entropy_len) = parsed.to_entropy_array(); | 244 | 14 | | 245 | 14 | // These rules are part of the seed phrase format "specification" and have been copy-pasted | 246 | 14 | // from the Substrate code base. | 247 | 14 | if !(16..=32).contains(&entropy_len) || entropy_len % 4 != 0 { | 248 | 0 | return Err(Bip39ToSeedError::BadWordsCount); | 249 | 14 | } | 250 | 14 | | 251 | 14 | let mut salt = zeroize::Zeroizing::new(String::with_capacity(8 + password.len())); | 252 | 14 | salt.push_str("mnemonic"); | 253 | 14 | salt.push_str(password); | 254 | 14 | | 255 | 14 | // This function returns an error only in case of wrong buffer length, making it safe to | 256 | 14 | // unwrap. | 257 | 14 | let mut seed_too_long = Box::new([0u8; 64]); | 258 | 14 | pbkdf2::pbkdf2::<hmac::Hmac<sha2::Sha512>>( | 259 | 14 | &entropy[..entropy_len], | 260 | 14 | salt.as_bytes(), | 261 | 14 | 2048, | 262 | 14 | &mut *seed_too_long, | 263 | 14 | ) | 264 | 14 | .unwrap(); | 265 | 14 | | 266 | 14 | // The seed is truncated to 32 bytes. | 267 | 14 | let mut seed = Box::new([0u8; 32]); | 268 | 14 | seed.copy_from_slice(&seed_too_long[..32]); | 269 | 14 | seed_too_long.zeroize(); | 270 | 14 | | 271 | 14 | Ok(seed) | 272 | 14 | } |
Unexecuted instantiation: _RNvNtNtCseuYC0Zibziv_7smoldot8identity11seed_phrase13bip39_to_seed |
273 | | |
274 | | /// Failed to decode BIP39 mnemonic phrase. |
275 | 0 | #[derive(Debug, derive_more::Display)] Unexecuted instantiation: _RNvXs8_NtNtCsN16ciHI6Qf_7smoldot8identity11seed_phraseNtB5_16Bip39ToSeedErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt Unexecuted instantiation: _RNvXs8_NtNtCseuYC0Zibziv_7smoldot8identity11seed_phraseNtB5_16Bip39ToSeedErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt |
276 | | pub enum Bip39ToSeedError { |
277 | | /// Invalid BIP39 mnemonic phrase. |
278 | | WrongMnemonic(Bip39DecodeError), |
279 | | /// Number of mnemonic phrase words isn't supported by the SS58 format. |
280 | | BadWordsCount, |
281 | | } |
282 | | |
283 | | /// Invalid BIP39 mnemonic phrase. |
284 | 0 | #[derive(Debug, derive_more::Display)] Unexecuted instantiation: _RNvXsa_NtNtCsN16ciHI6Qf_7smoldot8identity11seed_phraseNtB5_16Bip39DecodeErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt Unexecuted instantiation: _RNvXsa_NtNtCseuYC0Zibziv_7smoldot8identity11seed_phraseNtB5_16Bip39DecodeErrorNtNtCsaYZPK01V26L_4core3fmt7Display3fmt |
285 | | pub struct Bip39DecodeError(bip39::Error); |
286 | | |
287 | | #[cfg(test)] |
288 | | mod tests { |
289 | | #[test] |
290 | 1 | fn empty_matches_sr25519() { |
291 | 1 | assert_eq!( |
292 | 1 | *super::decode_sr25519_private_key("").unwrap(), |
293 | 1 | [ |
294 | 1 | 5, 214, 85, 132, 99, 13, 22, 205, 74, 246, 208, 190, 193, 15, 52, 187, 80, 74, 93, |
295 | 1 | 203, 98, 219, 162, 18, 45, 73, 245, 166, 99, 118, 61, 10, 253, 25, 12, 206, 116, |
296 | 1 | 223, 53, 100, 50, 180, 16, 189, 100, 104, 35, 9, 214, 222, 219, 39, 199, 104, 69, |
297 | 1 | 218, 243, 136, 85, 124, 186, 195, 202, 52 |
298 | 1 | ] |
299 | 1 | ); |
300 | 1 | } |
301 | | |
302 | | #[test] |
303 | 1 | fn empty_matches_ed25519() { |
304 | 1 | assert_eq!( |
305 | 1 | *super::decode_ed25519_private_key("").unwrap(), |
306 | 1 | [ |
307 | 1 | 250, 199, 149, 157, 191, 231, 47, 5, 46, 90, 12, 60, 141, 101, 48, 242, 2, 176, 47, |
308 | 1 | 216, 249, 245, 202, 53, 128, 236, 141, 235, 119, 151, 71, 158 |
309 | 1 | ] |
310 | 1 | ); |
311 | 1 | } |
312 | | |
313 | | #[test] |
314 | 1 | fn default_seed_is_correct_sr25519() { |
315 | 1 | assert_eq!( |
316 | 1 | super::decode_sr25519_private_key( |
317 | 1 | "bottom drive obey lake curtain smoke basket hold race lonely fit walk" |
318 | 1 | ) |
319 | 1 | .unwrap(), |
320 | 1 | super::decode_sr25519_private_key("").unwrap(), |
321 | 1 | ); |
322 | | |
323 | 1 | assert_eq!( |
324 | 1 | super::decode_sr25519_private_key( |
325 | 1 | "bottom drive obey lake curtain smoke basket hold race lonely fit walk//smoldot rules//125" |
326 | 1 | ) |
327 | 1 | .unwrap(), |
328 | 1 | super::decode_sr25519_private_key("//smoldot rules//125").unwrap(), |
329 | 1 | ); |
330 | 1 | } |
331 | | |
332 | | #[test] |
333 | 1 | fn default_seed_is_correct_ed25519() { |
334 | 1 | assert_eq!( |
335 | 1 | super::decode_ed25519_private_key( |
336 | 1 | "bottom drive obey lake curtain smoke basket hold race lonely fit walk" |
337 | 1 | ) |
338 | 1 | .unwrap(), |
339 | 1 | super::decode_ed25519_private_key("").unwrap(), |
340 | 1 | ); |
341 | | |
342 | 1 | assert_eq!( |
343 | 1 | super::decode_ed25519_private_key( |
344 | 1 | "bottom drive obey lake curtain smoke basket hold race lonely fit walk//smoldot rules//125" |
345 | 1 | ) |
346 | 1 | .unwrap(), |
347 | 1 | super::decode_ed25519_private_key("//smoldot rules//125").unwrap(), |
348 | 1 | ); |
349 | 1 | } |
350 | | |
351 | | #[test] |
352 | 1 | fn alice_matches_sr25519() { |
353 | 1 | assert_eq!( |
354 | 1 | *super::decode_sr25519_private_key("//Alice").unwrap(), |
355 | 1 | [ |
356 | 1 | 51, 166, 243, 9, 63, 21, 138, 113, 9, 246, 121, 65, 11, 239, 26, 12, 84, 22, 129, |
357 | 1 | 69, 224, 206, 203, 77, 240, 6, 193, 194, 255, 251, 31, 9, 146, 90, 34, 93, 151, |
358 | 1 | 170, 0, 104, 45, 106, 89, 185, 91, 24, 120, 12, 16, 215, 3, 35, 54, 232, 143, 52, |
359 | 1 | 66, 180, 35, 97, 244, 166, 96, 17, |
360 | 1 | ] |
361 | 1 | ); |
362 | 1 | } |
363 | | |
364 | | #[test] |
365 | 1 | fn alice_matches_ed25519() { |
366 | 1 | assert_eq!( |
367 | 1 | *super::decode_ed25519_private_key("//Alice").unwrap(), |
368 | 1 | [ |
369 | 1 | 171, 248, 229, 189, 190, 48, 198, 86, 86, 192, 163, 203, 209, 129, 255, 138, 86, |
370 | 1 | 41, 74, 105, 223, 237, 210, 121, 130, 170, 206, 74, 118, 144, 145, 21 |
371 | 1 | ] |
372 | 1 | ); |
373 | 1 | } |
374 | | |
375 | | #[test] |
376 | 1 | fn hex_seed_matches_sr25519() { |
377 | 1 | assert_eq!( |
378 | 1 | *super::decode_sr25519_private_key( |
379 | 1 | "0x0000000000000000000000000000000000000000000000000000000000000000" |
380 | 1 | ) |
381 | 1 | .unwrap(), |
382 | 1 | [ |
383 | 1 | 202, 168, 53, 120, 27, 21, 199, 112, 111, 101, 183, 31, 122, 88, 200, 7, 171, 54, |
384 | 1 | 15, 174, 214, 68, 15, 178, 62, 15, 76, 82, 233, 48, 222, 10, 10, 106, 133, 234, |
385 | 1 | 166, 66, 218, 200, 53, 66, 75, 93, 124, 141, 99, 124, 0, 64, 140, 122, 115, 218, |
386 | 1 | 103, 43, 127, 73, 133, 33, 66, 11, 109, 211 |
387 | 1 | ] |
388 | 1 | ); |
389 | 1 | } |
390 | | |
391 | | #[test] |
392 | 1 | fn hex_seed_matches_ed25519() { |
393 | 1 | assert_eq!( |
394 | 1 | *super::decode_ed25519_private_key( |
395 | 1 | "0x0000000000000000000000000000000000000000000000000000000000000000" |
396 | 1 | ) |
397 | 1 | .unwrap(), |
398 | 1 | [ |
399 | 1 | 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, |
400 | 1 | 0, 0, 0, 0 |
401 | 1 | ] |
402 | 1 | ); |
403 | 1 | } |
404 | | |
405 | | #[test] |
406 | 1 | fn multi_derivation_and_password_sr25519() { |
407 | 1 | assert_eq!( |
408 | 1 | *super::decode_sr25519_private_key("strong isolate job basic auto frozen want garlic autumn height riot desert//foo//2//baz///my_password").unwrap(), |
409 | 1 | [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] |
410 | 1 | ); |
411 | 1 | } |
412 | | |
413 | | #[test] |
414 | 1 | fn multi_derivation_and_password_ed25519() { |
415 | 1 | assert_eq!( |
416 | 1 | *super::decode_ed25519_private_key("strong isolate job basic auto frozen want garlic autumn height riot desert//foo//2//baz///my_password").unwrap(), |
417 | 1 | [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] |
418 | 1 | ); |
419 | 1 | } |
420 | | } |