Coverage Report

Created: 2024-05-16 12:16

/__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
}