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";