crypto: lazy_static removed, light parser for address URI added (#2250)

The `lazy_static` package does not work well in `no-std`: it requires
`spin_no_std` feature, which also will propagate into `std` if enabled.
This is not what we want.

This PR provides simple address uri parser which allows to get rid of
_regex_ which was used to parse the address uri, what in turns allows to
remove lazy_static.

Three regular expressions
(`SS58_REGEX`,`SECRET_PHRASE_REGEX`,`JUNCTION_REGEX`) were replaced with
the parser which unifies all of them.

The new parser does not support Unicode, it is ASCII only.

Related to: #2044

---------

Co-authored-by: Bastian Köcher <git@kchr.de>
Co-authored-by: Koute <koute@users.noreply.github.com>
Co-authored-by: command-bot <>
This commit is contained in:
Michal Kucharczyk
2023-11-17 14:43:37 +01:00
committed by GitHub
parent 3ab2bc9ff3
commit 5007e2dd5c
10 changed files with 601 additions and 67 deletions
@@ -0,0 +1,432 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Little util for parsing an address URI. Replaces regular expressions.
#[cfg(all(not(feature = "std"), any(feature = "serde", feature = "full_crypto")))]
use sp_std::{
alloc::string::{String, ToString},
vec::Vec,
};
/// A container for results of parsing the address uri string.
///
/// Intended to be equivalent of:
/// `Regex::new(r"^(?P<phrase>[a-zA-Z0-9 ]+)?(?P<path>(//?[^/]+)*)(///(?P<password>.*))?$")`
/// which also handles soft and hard derivation paths:
/// `Regex::new(r"/(/?[^/]+)")`
///
/// Example:
/// ```
/// use sp_core::crypto::AddressUri;
/// let manual_result = AddressUri::parse("hello world/s//h///pass");
/// assert_eq!(
/// manual_result.unwrap(),
/// AddressUri { phrase: Some("hello world"), paths: vec!["s", "/h"], pass: Some("pass") }
/// );
/// ```
#[derive(Debug, PartialEq)]
pub struct AddressUri<'a> {
/// Phrase, hexadecimal string, or ss58-compatible string.
pub phrase: Option<&'a str>,
/// Key derivation paths, ordered as in input string,
pub paths: Vec<&'a str>,
/// Password.
pub pass: Option<&'a str>,
}
/// Errors that are possible during parsing the address URI.
#[allow(missing_docs)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Error {
#[cfg_attr(feature = "std", error("Invalid character in phrase:\n{0}"))]
InvalidCharacterInPhrase(InvalidCharacterInfo),
#[cfg_attr(feature = "std", error("Invalid character in password:\n{0}"))]
InvalidCharacterInPass(InvalidCharacterInfo),
#[cfg_attr(feature = "std", error("Missing character in hard path:\n{0}"))]
MissingCharacterInHardPath(InvalidCharacterInfo),
#[cfg_attr(feature = "std", error("Missing character in soft path:\n{0}"))]
MissingCharacterInSoftPath(InvalidCharacterInfo),
}
impl Error {
/// Creates an instance of `Error::InvalidCharacterInPhrase` using given parameters.
pub fn in_phrase(input: &str, pos: usize) -> Self {
Self::InvalidCharacterInPhrase(InvalidCharacterInfo::new(input, pos))
}
/// Creates an instance of `Error::InvalidCharacterInPass` using given parameters.
pub fn in_pass(input: &str, pos: usize) -> Self {
Self::InvalidCharacterInPass(InvalidCharacterInfo::new(input, pos))
}
/// Creates an instance of `Error::MissingCharacterInHardPath` using given parameters.
pub fn in_hard_path(input: &str, pos: usize) -> Self {
Self::MissingCharacterInHardPath(InvalidCharacterInfo::new(input, pos))
}
/// Creates an instance of `Error::MissingCharacterInSoftPath` using given parameters.
pub fn in_soft_path(input: &str, pos: usize) -> Self {
Self::MissingCharacterInSoftPath(InvalidCharacterInfo::new(input, pos))
}
}
/// Complementary error information.
///
/// Strucutre contains complementary information about parsing address URI string.
/// String contains a copy of an original URI string, 0-based integer indicates position of invalid
/// character.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct InvalidCharacterInfo(String, usize);
impl InvalidCharacterInfo {
fn new(info: &str, pos: usize) -> Self {
Self(info.to_string(), pos)
}
}
impl sp_std::fmt::Display for InvalidCharacterInfo {
fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result {
let (s, pos) = escape_string(&self.0, self.1);
write!(f, "{s}\n{i}^", i = sp_std::iter::repeat(" ").take(pos).collect::<String>())
}
}
/// Escapes the control characters in given string, and recomputes the position if some characters
/// were actually escaped.
fn escape_string(input: &str, pos: usize) -> (String, usize) {
let mut out = String::with_capacity(2 * input.len());
let mut out_pos = 0;
input
.chars()
.enumerate()
.map(|(i, c)| {
let esc = |c| (i, Some('\\'), c, 2);
match c {
'\t' => esc('t'),
'\n' => esc('n'),
'\r' => esc('r'),
'\x07' => esc('a'),
'\x08' => esc('b'),
'\x0b' => esc('v'),
'\x0c' => esc('f'),
_ => (i, None, c, 1),
}
})
.for_each(|(i, maybe_escape, c, increment)| {
maybe_escape.map(|e| out.push(e));
out.push(c);
if i < pos {
out_pos += increment;
}
});
(out, out_pos)
}
fn extract_prefix<'a>(input: &mut &'a str, is_allowed: &dyn Fn(char) -> bool) -> Option<&'a str> {
let output = input.trim_start_matches(is_allowed);
let prefix_len = input.len() - output.len();
let prefix = if prefix_len > 0 { Some(&input[..prefix_len]) } else { None };
*input = output;
prefix
}
fn strip_prefix(input: &mut &str, prefix: &str) -> bool {
if let Some(stripped_input) = input.strip_prefix(prefix) {
*input = stripped_input;
true
} else {
false
}
}
impl<'a> AddressUri<'a> {
/// Parses the given string.
pub fn parse(mut input: &'a str) -> Result<Self, Error> {
let initial_input = input;
let initial_input_len = input.len();
let phrase = extract_prefix(&mut input, &|ch: char| {
ch.is_ascii_digit() || ch.is_ascii_alphabetic() || ch == ' '
});
let mut pass = None;
let mut paths = Vec::new();
while !input.is_empty() {
let unstripped_input = input;
if strip_prefix(&mut input, "///") {
pass = Some(extract_prefix(&mut input, &|ch: char| ch != '\n').unwrap_or(""));
} else if strip_prefix(&mut input, "//") {
let path = extract_prefix(&mut input, &|ch: char| ch != '/')
.ok_or(Error::in_hard_path(initial_input, initial_input_len - input.len()))?;
assert!(path.len() > 0);
// hard path shall contain leading '/', so take it from unstripped input.
paths.push(&unstripped_input[1..path.len() + 2]);
} else if strip_prefix(&mut input, "/") {
paths.push(
extract_prefix(&mut input, &|ch: char| ch != '/').ok_or(
Error::in_soft_path(initial_input, initial_input_len - input.len()),
)?,
);
} else {
return Err(if pass.is_some() {
Error::in_pass(initial_input, initial_input_len - input.len())
} else {
Error::in_phrase(initial_input, initial_input_len - input.len())
});
}
}
Ok(Self { phrase, paths, pass })
}
}
#[cfg(test)]
mod tests {
use super::*;
use regex::Regex;
lazy_static::lazy_static! {
static ref SECRET_PHRASE_REGEX: Regex = Regex::new(r"^(?P<phrase>[a-zA-Z0-9 ]+)?(?P<path>(//?[^/]+)*)(///(?P<password>.*))?$")
.expect("constructed from known-good static value; qed");
}
fn check_with_regex(input: &str) {
let regex_result = SECRET_PHRASE_REGEX.captures(input);
let manual_result = AddressUri::parse(input);
assert_eq!(regex_result.is_some(), manual_result.is_ok());
if let (Some(regex_result), Ok(manual_result)) = (regex_result, manual_result) {
assert_eq!(
regex_result.name("phrase").map(|phrase| phrase.as_str()),
manual_result.phrase
);
let manual_paths = manual_result
.paths
.iter()
.map(|s| "/".to_string() + s)
.collect::<Vec<_>>()
.join("");
assert_eq!(regex_result.name("path").unwrap().as_str().to_string(), manual_paths);
assert_eq!(
regex_result.name("password").map(|phrase| phrase.as_str()),
manual_result.pass
);
}
}
fn check(input: &str, result: Result<AddressUri, Error>) {
let manual_result = AddressUri::parse(input);
assert_eq!(manual_result, result);
check_with_regex(input);
}
#[test]
fn test00() {
check("///", Ok(AddressUri { phrase: None, pass: Some(""), paths: vec![] }));
}
#[test]
fn test01() {
check("////////", Ok(AddressUri { phrase: None, pass: Some("/////"), paths: vec![] }))
}
#[test]
fn test02() {
check(
"sdasd///asda",
Ok(AddressUri { phrase: Some("sdasd"), pass: Some("asda"), paths: vec![] }),
);
}
#[test]
fn test03() {
check(
"sdasd//asda",
Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["/asda"] }),
);
}
#[test]
fn test04() {
check("sdasd//a", Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["/a"] }));
}
#[test]
fn test05() {
let input = "sdasd//";
check(input, Err(Error::in_hard_path(input, 7)));
}
#[test]
fn test06() {
check(
"sdasd/xx//asda",
Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["xx", "/asda"] }),
);
}
#[test]
fn test07() {
check(
"sdasd/xx//a/b//c///pass",
Ok(AddressUri {
phrase: Some("sdasd"),
pass: Some("pass"),
paths: vec!["xx", "/a", "b", "/c"],
}),
);
}
#[test]
fn test08() {
check(
"sdasd/xx//a",
Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["xx", "/a"] }),
);
}
#[test]
fn test09() {
let input = "sdasd/xx//";
check(input, Err(Error::in_hard_path(input, 10)));
}
#[test]
fn test10() {
check(
"sdasd/asda",
Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["asda"] }),
);
}
#[test]
fn test11() {
check(
"sdasd/asda//x",
Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["asda", "/x"] }),
);
}
#[test]
fn test12() {
check("sdasd/a", Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["a"] }));
}
#[test]
fn test13() {
let input = "sdasd/";
check(input, Err(Error::in_soft_path(input, 6)));
}
#[test]
fn test14() {
check("sdasd", Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec![] }));
}
#[test]
fn test15() {
let input = "sdasd.";
check(input, Err(Error::in_phrase(input, 5)));
}
#[test]
fn test16() {
let input = "sd.asd/asd.a";
check(input, Err(Error::in_phrase(input, 2)));
}
#[test]
fn test17() {
let input = "sd.asd//asd.a";
check(input, Err(Error::in_phrase(input, 2)));
}
#[test]
fn test18() {
check(
"sdasd/asd.a",
Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["asd.a"] }),
);
}
#[test]
fn test19() {
check(
"sdasd//asd.a",
Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["/asd.a"] }),
);
}
#[test]
fn test20() {
let input = "///\n";
check(input, Err(Error::in_pass(input, 3)));
}
#[test]
fn test21() {
let input = "///a\n";
check(input, Err(Error::in_pass(input, 4)));
}
#[test]
fn test22() {
let input = "sd asd///asd.a\n";
check(input, Err(Error::in_pass(input, 14)));
}
#[test]
fn test_invalid_char_info_1() {
let expected = "01234\n^";
let f = format!("{}", InvalidCharacterInfo::new("01234", 0));
assert_eq!(expected, f);
}
#[test]
fn test_invalid_char_info_2() {
let expected = "01\n ^";
let f = format!("{}", InvalidCharacterInfo::new("01", 1));
assert_eq!(expected, f);
}
#[test]
fn test_invalid_char_info_3() {
let expected = "01234\n ^";
let f = format!("{}", InvalidCharacterInfo::new("01234", 2));
assert_eq!(expected, f);
}
#[test]
fn test_invalid_char_info_4() {
let expected = "012\\n456\n ^";
let f = format!("{}", InvalidCharacterInfo::new("012\n456", 3));
assert_eq!(expected, f);
}
#[test]
fn test_invalid_char_info_5() {
let expected = "012\\n456\n ^";
let f = format!("{}", InvalidCharacterInfo::new("012\n456", 5));
assert_eq!(expected, f);
}
#[test]
fn test_invalid_char_info_6() {
let expected = "012\\f456\\t89\n ^";
let f = format!("{}", InvalidCharacterInfo::new("012\x0c456\t89", 9));
assert_eq!(expected, f);
}
}
+43 -42
View File
@@ -25,8 +25,6 @@ use codec::{Decode, Encode, MaxEncodedLen};
use itertools::Itertools;
#[cfg(feature = "std")]
use rand::{rngs::OsRng, RngCore};
#[cfg(feature = "std")]
use regex::Regex;
use scale_info::TypeInfo;
#[cfg(feature = "std")]
pub use secrecy::{ExposeSecret, SecretString};
@@ -43,6 +41,11 @@ pub use ss58_registry::{from_known_address_format, Ss58AddressFormat, Ss58Addres
/// Trait to zeroize a memory buffer.
pub use zeroize::Zeroize;
#[cfg(feature = "std")]
pub use crate::address_uri::AddressUri;
#[cfg(any(feature = "std", feature = "full_crypto"))]
pub use crate::address_uri::Error as AddressUriError;
/// 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";
@@ -82,8 +85,8 @@ impl<S, T: UncheckedFrom<S>> UncheckedInto<T> for S {
#[cfg(feature = "full_crypto")]
pub enum SecretStringError {
/// The overall format was invalid (e.g. the seed phrase contained symbols).
#[cfg_attr(feature = "std", error("Invalid format"))]
InvalidFormat,
#[cfg_attr(feature = "std", error("Invalid format {0}"))]
InvalidFormat(AddressUriError),
/// The seed phrase provided is not a valid BIP39 phrase.
#[cfg_attr(feature = "std", error("Invalid phrase"))]
InvalidPhrase,
@@ -101,6 +104,13 @@ pub enum SecretStringError {
InvalidPath,
}
#[cfg(any(feature = "std", feature = "full_crypto"))]
impl From<AddressUriError> for SecretStringError {
fn from(e: AddressUriError) -> Self {
Self::InvalidFormat(e)
}
}
/// An error when deriving a key.
#[cfg_attr(feature = "std", derive(thiserror::Error))]
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -208,7 +218,7 @@ impl<T: AsRef<str>> From<T> for DeriveJunction {
/// An error type for SS58 decoding.
#[cfg_attr(feature = "std", derive(thiserror::Error))]
#[cfg_attr(not(feature = "std"), derive(Debug))]
#[derive(Clone, Copy, Eq, PartialEq)]
#[derive(Clone, Eq, PartialEq)]
#[allow(missing_docs)]
#[cfg(any(feature = "full_crypto", feature = "serde"))]
pub enum PublicError {
@@ -235,6 +245,11 @@ pub enum PublicError {
InvalidPath,
#[cfg_attr(feature = "std", error("Disallowed SS58 Address Format for this datatype."))]
FormatNotAllowed,
#[cfg_attr(feature = "std", error("Password not allowed."))]
PasswordNotAllowed,
#[cfg(feature = "std")]
#[cfg_attr(feature = "std", error("Incorrect URI syntax {0}."))]
MalformedUri(#[from] AddressUriError),
}
#[cfg(feature = "std")]
@@ -414,47 +429,40 @@ pub fn set_default_ss58_version(new_default: Ss58AddressFormat) {
DEFAULT_VERSION.store(new_default.into(), core::sync::atomic::Ordering::Relaxed);
}
#[cfg(feature = "std")]
lazy_static::lazy_static! {
static ref SS58_REGEX: Regex = Regex::new(r"^(?P<ss58>[\w\d ]+)?(?P<path>(//?[^/]+)*)$")
.expect("constructed from known-good static value; qed");
static ref SECRET_PHRASE_REGEX: Regex = Regex::new(r"^(?P<phrase>[\d\w ]+)?(?P<path>(//?[^/]+)*)(///(?P<password>.*))?$")
.expect("constructed from known-good static value; qed");
static ref JUNCTION_REGEX: Regex = Regex::new(r"/(/?[^/]+)")
.expect("constructed from known-good static value; qed");
}
#[cfg(feature = "std")]
impl<T: Sized + AsMut<[u8]> + AsRef<[u8]> + Public + Derive> Ss58Codec for T {
fn from_string(s: &str) -> Result<Self, PublicError> {
let cap = SS58_REGEX.captures(s).ok_or(PublicError::InvalidFormat)?;
let s = cap.name("ss58").map(|r| r.as_str()).unwrap_or(DEV_ADDRESS);
let cap = AddressUri::parse(s)?;
if cap.pass.is_some() {
return Err(PublicError::PasswordNotAllowed);
}
let s = cap.phrase.unwrap_or(DEV_ADDRESS);
let addr = if let Some(stripped) = s.strip_prefix("0x") {
let d = array_bytes::hex2bytes(stripped).map_err(|_| PublicError::InvalidFormat)?;
Self::from_slice(&d).map_err(|()| PublicError::BadLength)?
} else {
Self::from_ss58check(s)?
};
if cap["path"].is_empty() {
if cap.paths.is_empty() {
Ok(addr)
} else {
let path =
JUNCTION_REGEX.captures_iter(&cap["path"]).map(|f| DeriveJunction::from(&f[1]));
addr.derive(path).ok_or(PublicError::InvalidPath)
addr.derive(cap.paths.iter().map(DeriveJunction::from))
.ok_or(PublicError::InvalidPath)
}
}
fn from_string_with_version(s: &str) -> Result<(Self, Ss58AddressFormat), PublicError> {
let cap = SS58_REGEX.captures(s).ok_or(PublicError::InvalidFormat)?;
let (addr, v) = Self::from_ss58check_with_version(
cap.name("ss58").map(|r| r.as_str()).unwrap_or(DEV_ADDRESS),
)?;
if cap["path"].is_empty() {
let cap = AddressUri::parse(s)?;
if cap.pass.is_some() {
return Err(PublicError::PasswordNotAllowed);
}
let (addr, v) = Self::from_ss58check_with_version(cap.phrase.unwrap_or(DEV_ADDRESS))?;
if cap.paths.is_empty() {
Ok((addr, v))
} else {
let path =
JUNCTION_REGEX.captures_iter(&cap["path"]).map(|f| DeriveJunction::from(&f[1]));
addr.derive(path).ok_or(PublicError::InvalidPath).map(|a| (a, v))
addr.derive(cap.paths.iter().map(DeriveJunction::from))
.ok_or(PublicError::InvalidPath)
.map(|a| (a, v))
}
}
}
@@ -817,22 +825,15 @@ impl sp_std::str::FromStr for SecretUri {
type Err = SecretStringError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let cap = SECRET_PHRASE_REGEX.captures(s).ok_or(SecretStringError::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");
let cap = AddressUri::parse(s)?;
let phrase = cap.phrase.unwrap_or(DEV_PHRASE);
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,
password: cap
.pass
.map(|v| SecretString::from_str(v).expect("Returns infallible error; qed")),
junctions: cap.paths.iter().map(DeriveJunction::from).collect::<Vec<_>>(),
})
}
}
+2
View File
@@ -55,6 +55,8 @@ pub mod crypto;
pub mod hexdisplay;
pub use paste;
#[cfg(any(feature = "full_crypto", feature = "std"))]
mod address_uri;
#[cfg(feature = "bandersnatch-experimental")]
pub mod bandersnatch;
#[cfg(feature = "bls-experimental")]