subxt_signer/crypto/secret_uri.rs
1// Copyright 2019-2025 Parity Technologies (UK) Ltd.
2// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3// see LICENSE for license details.
4
5use super::DeriveJunction;
6use alloc::{string::ToString, vec::Vec};
7use regex::Regex;
8use secrecy::SecretString;
9
10use thiserror::Error as DeriveError;
11
12// This code is taken from sp_core::crypto::DeriveJunction. The logic should be identical,
13// though the code is tweaked a touch!
14
15/// A secret uri (`SURI`) that can be used to generate a key pair.
16///
17/// The `SURI` can be parsed from a string. The string takes this form:
18///
19/// ```text
20/// phrase/path0/path1///password
21/// 111111 22222 22222 33333333
22/// ```
23///
24/// Where:
25/// - 1 denotes a phrase or hex string. If this is not provided, the [`DEV_PHRASE`] is used
26/// instead.
27/// - 2's denote optional "derivation junctions" which are used to derive keys. Each of these is
28/// separated by "/". A derivation junction beginning with "/" (ie "//" in the original string)
29/// is a "hard" path.
30/// - 3 denotes an optional password which is used in conjunction with the phrase provided in 1
31/// to generate an initial key. If hex is provided for 1, it's ignored.
32///
33/// Notes:
34/// - If 1 is a `0x` prefixed 64-digit hex string, then we'll interpret it as hex, and treat the hex bytes
35/// as a seed/MiniSecretKey directly, ignoring any password.
36/// - Else if the phrase part is a valid BIP-39 phrase, we'll use the phrase (and password, if provided)
37/// to generate a seed/MiniSecretKey.
38/// - Uris like "//Alice" correspond to keys derived from a DEV_PHRASE, since no phrase part is given.
39///
40/// There is no correspondence mapping between `SURI` strings and the keys they represent.
41/// Two different non-identical strings can actually lead to the same secret being derived.
42/// Notably, integer junction indices may be legally prefixed with arbitrary number of zeros.
43/// Similarly an empty password (ending the `SURI` with `///`) is perfectly valid and will
44/// generally be equivalent to no password at all.
45///
46/// # Examples
47///
48/// Parse [`DEV_PHRASE`] secret URI with junction:
49///
50/// ```
51/// # use subxt_signer::{SecretUri, DeriveJunction, DEV_PHRASE, ExposeSecret};
52/// # use std::str::FromStr;
53/// let suri = SecretUri::from_str("//Alice").expect("Parse SURI");
54///
55/// assert_eq!(vec![DeriveJunction::from("Alice").harden()], suri.junctions);
56/// assert_eq!(DEV_PHRASE, suri.phrase.expose_secret());
57/// assert!(suri.password.is_none());
58/// ```
59///
60/// Parse [`DEV_PHRASE`] secret URI with junction and password:
61///
62/// ```
63/// # use subxt_signer::{SecretUri, DeriveJunction, DEV_PHRASE, ExposeSecret};
64/// # use std::str::FromStr;
65/// let suri = SecretUri::from_str("//Alice///SECRET_PASSWORD").expect("Parse SURI");
66///
67/// assert_eq!(vec![DeriveJunction::from("Alice").harden()], suri.junctions);
68/// assert_eq!(DEV_PHRASE, suri.phrase.expose_secret());
69/// assert_eq!("SECRET_PASSWORD", suri.password.unwrap().expose_secret());
70/// ```
71///
72/// Parse [`DEV_PHRASE`] secret URI with hex phrase and junction:
73///
74/// ```
75/// # use subxt_signer::{SecretUri, DeriveJunction, DEV_PHRASE, ExposeSecret};
76/// # use std::str::FromStr;
77/// let suri = SecretUri::from_str("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a//Alice").expect("Parse SURI");
78///
79/// assert_eq!(vec![DeriveJunction::from("Alice").harden()], suri.junctions);
80/// assert_eq!("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a", suri.phrase.expose_secret());
81/// assert!(suri.password.is_none());
82/// ```
83pub struct SecretUri {
84 /// The phrase to derive the private key.
85 ///
86 /// This can either be a 64-bit hex string or a BIP-39 key phrase.
87 pub phrase: SecretString,
88 /// Optional password as given as part of the uri.
89 pub password: Option<SecretString>,
90 /// The junctions as part of the uri.
91 pub junctions: Vec<DeriveJunction>,
92}
93
94impl core::str::FromStr for SecretUri {
95 type Err = SecretUriError;
96
97 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 let cap = secret_phrase_regex()
99 .captures(s)
100 .ok_or(SecretUriError::InvalidFormat)?;
101
102 let junctions = junction_regex()
103 .captures_iter(&cap["path"])
104 .map(|f| DeriveJunction::from(&f[1]))
105 .collect::<Vec<_>>();
106
107 let phrase = cap.name("phrase").map(|r| r.as_str()).unwrap_or(DEV_PHRASE);
108 let password = cap.name("password");
109
110 Ok(Self {
111 phrase: SecretString::from(phrase.to_string()),
112 password: password.map(|v| SecretString::from(v.as_str().to_string())),
113 junctions,
114 })
115 }
116}
117
118/// This is returned if `FromStr` cannot parse a string into a `SecretUri`.
119#[derive(Debug, Copy, Clone, PartialEq, DeriveError)]
120pub enum SecretUriError {
121 /// Parsing the secret URI from a string failed; wrong format.
122 #[error("Invalid secret phrase format")]
123 InvalidFormat,
124}
125
126once_static_cloned! {
127 /// Interpret a phrase like:
128 ///
129 /// ```text
130 /// foo bar wibble /path0/path1///password
131 /// 11111111111111 222222222222 33333333
132 /// ```
133 /// Where 1 is the phrase, 2 the path and 3 the password.
134 /// Taken from `sp_core::crypto::SECRET_PHRASE_REGEX`.
135 fn secret_phrase_regex() -> regex::Regex {
136 Regex::new(r"^(?P<phrase>[\d\w ]+)?(?P<path>(//?[^/]+)*)(///(?P<password>.*))?$").unwrap()
137 }
138
139 /// Interpret a part of a path into a "junction":
140 ///
141 /// ```text
142 /// //foo/bar/wibble
143 /// 1111 222 333333
144 /// ```
145 /// Where the numbers denote matching junctions.
146 ///
147 /// The leading "/" deliminates each part, and then a "/" beginning
148 /// a path piece denotes that it's a "hard" path. Taken from
149 /// `sp_core::crypto::JUNCTION_REGEX`.
150 fn junction_regex() -> regex::Regex {
151 Regex::new(r"/(/?[^/]+)").unwrap()
152 }
153}
154
155/// The root phrase for our publicly known keys.
156pub const DEV_PHRASE: &str =
157 "bottom drive obey lake curtain smoke basket hold race lonely fit walk";