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