diff --git a/hierarchical_deterministic/Cargo.toml b/hierarchical_deterministic/Cargo.toml index 82749852..627a1d08 100644 --- a/hierarchical_deterministic/Cargo.toml +++ b/hierarchical_deterministic/Cargo.toml @@ -3,9 +3,6 @@ name = "hierarchical_deterministic" version = "0.1.0" edition = "2021" -[lib] -doctest = false - [dependencies] serde = { version = "1.0.192", features = ["derive"] } serde_json = { version = "1.0.108", features = ["preserve_order"] } @@ -21,8 +18,6 @@ memoize = "0.4.1" radix-engine-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "038ddee8b0f57aa90e36375c69946c4eb634efeb", features = [ "serde", ] } -transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "038ddee8b0f57aa90e36375c69946c4eb634efeb", features = [ - "serde", -] } bip32 = "0.5.1" # slip10 crate does not support `secp256k1`. hex = "0.4.3" +enum-as-inner = "0.6.0" diff --git a/hierarchical_deterministic/src/bip32/hd_path.rs b/hierarchical_deterministic/src/bip32/hd_path.rs index e79aed21..50ac86b7 100644 --- a/hierarchical_deterministic/src/bip32/hd_path.rs +++ b/hierarchical_deterministic/src/bip32/hd_path.rs @@ -3,8 +3,7 @@ use std::str::FromStr; use itertools::Itertools; use serde::{de, Deserializer, Serialize, Serializer}; use slip10::path::BIP32Path; - -use crate::hdpath_error::HDPathError; +use wallet_kit_common::error::hdpath_error::HDPathError; use super::hd_path_component::{HDPathComponent, HDPathValue}; @@ -79,12 +78,11 @@ impl HDPath { Ok(got) } - pub(crate) fn try_parse_base( - s: &str, + pub(crate) fn try_parse_base_hdpath( + path: &HDPath, depth_error: HDPathError, ) -> Result<(HDPath, Vec), HDPathError> { use HDPathError::*; - let path = HDPath::from_str(s).map_err(|_| HDPathError::InvalidBIP32Path(s.to_string()))?; if path.depth() < 2 { return Err(depth_error); } @@ -105,6 +103,14 @@ impl HDPath { )?; return Ok((path.clone(), components.clone())); } + + pub(crate) fn try_parse_base( + s: &str, + depth_error: HDPathError, + ) -> Result<(HDPath, Vec), HDPathError> { + let path = HDPath::from_str(s).map_err(|_| HDPathError::InvalidBIP32Path(s.to_string()))?; + return Self::try_parse_base_hdpath(&path, depth_error); + } } impl ToString for HDPath { @@ -119,7 +125,8 @@ impl ToString for HDPath { } impl Serialize for HDPath { - /// Serializes this `AccountAddress` into its bech32 address string as JSON. + /// Serializes this `HDPath` into its bech32 address string as JSON. + #[cfg(not(tarpaulin_include))] // false negative fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where S: Serializer, @@ -129,7 +136,8 @@ impl Serialize for HDPath { } impl<'de> serde::Deserialize<'de> for HDPath { - /// Tries to deserializes a JSON string as a bech32 address into an `AccountAddress`. + /// Tries to deserializes a JSON string as a bech32 address into an `HDPath`. + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; HDPath::from_str(&s).map_err(de::Error::custom) @@ -140,7 +148,8 @@ impl<'de> serde::Deserialize<'de> for HDPath { mod tests { use serde_json::json; use wallet_kit_common::json::{ - assert_json_value_eq_after_roundtrip, assert_json_value_ne_after_roundtrip, + assert_json_value_eq_after_roundtrip, assert_json_value_fails, + assert_json_value_ne_after_roundtrip, }; use super::HDPath; @@ -151,5 +160,6 @@ mod tests { let parsed = HDPath::from_str(str).unwrap(); assert_json_value_eq_after_roundtrip(&parsed, json!(str)); assert_json_value_ne_after_roundtrip(&parsed, json!("m/44H/33H")); + assert_json_value_fails::(json!("super invalid path")); } } diff --git a/hierarchical_deterministic/src/bip39/bip39_word/bip39_word.rs b/hierarchical_deterministic/src/bip39/bip39_word/bip39_word.rs index b7568efa..9c31e208 100644 --- a/hierarchical_deterministic/src/bip39/bip39_word/bip39_word.rs +++ b/hierarchical_deterministic/src/bip39/bip39_word/bip39_word.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use bip39::Language; use memoize::memoize; -use wallet_kit_common::error::Error; +use wallet_kit_common::error::hdpath_error::HDPathError as Error; use super::u11::U11; @@ -53,10 +53,9 @@ fn index_of_word_in_bip39_wordlist_of_language( #[cfg(test)] mod tests { - use bip39::Language; - use wallet_kit_common::error::Error; - use super::BIP39Word; + use bip39::Language; + use wallet_kit_common::error::hdpath_error::HDPathError as Error; #[test] fn equality() { diff --git a/hierarchical_deterministic/src/bip39/bip39_word_count.rs b/hierarchical_deterministic/src/bip39/bip39_word_count.rs index a81e90dc..900c4622 100644 --- a/hierarchical_deterministic/src/bip39/bip39_word_count.rs +++ b/hierarchical_deterministic/src/bip39/bip39_word_count.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use serde_repr::{Deserialize_repr, Serialize_repr}; use strum::FromRepr; -use wallet_kit_common::error::Error; +use wallet_kit_common::error::hdpath_error::HDPathError as Error; /// The number of words in the mnemonic of a DeviceFactorSource, according to the BIP39 /// standard, a multiple of 3, from 12 to 24 words. All "Babylon" `DeviceFactorSource`s diff --git a/hierarchical_deterministic/src/bip39/mnemonic.rs b/hierarchical_deterministic/src/bip39/mnemonic.rs index dc66bac2..95104bce 100644 --- a/hierarchical_deterministic/src/bip39/mnemonic.rs +++ b/hierarchical_deterministic/src/bip39/mnemonic.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use bip39::Language; use itertools::Itertools; use serde::{de, Deserializer, Serialize, Serializer}; -use wallet_kit_common::error::Error; +use wallet_kit_common::error::hdpath_error::HDPathError as Error; use super::{bip39_word::bip39_word::BIP39Word, bip39_word_count::BIP39WordCount}; @@ -48,7 +48,7 @@ impl Mnemonic { pub type Seed = [u8; 64]; impl Serialize for Mnemonic { - /// Serializes this `AccountAddress` into its bech32 address string as JSON. + /// Serializes this `Mnemonic` into a phrase, all words separated by a space. fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where S: Serializer, @@ -58,7 +58,8 @@ impl Serialize for Mnemonic { } impl<'de> serde::Deserialize<'de> for Mnemonic { - /// Tries to deserializes a JSON string as a bech32 address into an `AccountAddress`. + /// Tries to deserializes a JSON string as a Mnemonic + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; Mnemonic::from_phrase(&s).map_err(de::Error::custom) @@ -66,7 +67,7 @@ impl<'de> serde::Deserialize<'de> for Mnemonic { } impl TryInto for &str { - type Error = wallet_kit_common::error::Error; + type Error = wallet_kit_common::error::hdpath_error::HDPathError; /// Tries to deserializes a bech32 address into an `AccountAddress`. fn try_into(self) -> Result { @@ -74,6 +75,13 @@ impl TryInto for &str { } } +impl Mnemonic { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::from_phrase("bright club bacon dinner achieve pull grid save ramp cereal blush woman humble limb repeat video sudden possible story mask neutral prize goose mandate").expect("Valid mnemonic") + } +} + #[cfg(test)] mod tests { use bip39::Language; @@ -111,10 +119,7 @@ mod tests { #[test] fn words() { - let mnemonic: Mnemonic = - "bright club bacon dinner achieve pull grid save ramp cereal blush woman humble limb repeat video sudden possible story mask neutral prize goose mandate" - .try_into() - .unwrap(); + let mnemonic = Mnemonic::placeholder(); assert_eq!(mnemonic.words[0].word, "bright"); assert_eq!(mnemonic.words[1].word, "club"); assert_eq!(mnemonic.words[2].word, "bacon"); diff --git a/hierarchical_deterministic/src/bip44/bip44_like_path.rs b/hierarchical_deterministic/src/bip44/bip44_like_path.rs index 22b4103c..2abc9fbc 100644 --- a/hierarchical_deterministic/src/bip44/bip44_like_path.rs +++ b/hierarchical_deterministic/src/bip44/bip44_like_path.rs @@ -1,20 +1,26 @@ use serde::{de, Deserializer, Serialize, Serializer}; +use wallet_kit_common::error::hdpath_error::HDPathError; use crate::{ bip32::{ hd_path::HDPath, hd_path_component::{HDPathComponent, HDPathValue}, }, - derivation::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}, - hdpath_error::HDPathError, + derivation::{ + derivation::Derivation, derivation_path::DerivationPath, + derivation_path_scheme::DerivationPathScheme, + }, }; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct BIP44LikePath(HDPath); -impl BIP44LikePath { - pub fn from_str(s: &str) -> Result { - let (path, components) = HDPath::try_parse_base(s, HDPathError::InvalidDepthOfBIP44Path)?; +impl TryFrom<&HDPath> for BIP44LikePath { + type Error = HDPathError; + + fn try_from(value: &HDPath) -> Result { + let (path, components) = + HDPath::try_parse_base_hdpath(value, HDPathError::InvalidDepthOfBIP44Path)?; if path.depth() != 5 { return Err(HDPathError::InvalidDepthOfBIP44Path); } @@ -33,6 +39,13 @@ impl BIP44LikePath { } return Ok(Self(path)); } +} + +impl BIP44LikePath { + pub fn from_str(s: &str) -> Result { + let (path, _) = HDPath::try_parse_base(s, HDPathError::InvalidDepthOfBIP44Path)?; + return Self::try_from(&path); + } fn with_account_and_index(account: HDPathValue, index: HDPathValue) -> Self { let c0 = HDPathComponent::bip44_purpose(); // purpose @@ -51,6 +64,9 @@ impl BIP44LikePath { } impl Derivation for BIP44LikePath { + fn derivation_path(&self) -> DerivationPath { + DerivationPath::BIP44Like(self.clone()) + } fn hd_path(&self) -> &HDPath { &self.0 } @@ -61,7 +77,7 @@ impl Derivation for BIP44LikePath { } impl Serialize for BIP44LikePath { - /// Serializes this `AccountAddress` into its bech32 address string as JSON. + /// Serializes this `BIP44LikePath` into JSON as a string on: "m/44H/1022H/0H/0/0H" format fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where S: Serializer, @@ -71,7 +87,8 @@ impl Serialize for BIP44LikePath { } impl<'de> serde::Deserialize<'de> for BIP44LikePath { - /// Tries to deserializes a JSON string as a bech32 address into an `AccountAddress`. + /// Tries to deserializes a JSON string as a derivation path string into a `BIP44LikePath` + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; BIP44LikePath::from_str(&s).map_err(de::Error::custom) @@ -86,14 +103,22 @@ impl TryInto for &str { } } +impl BIP44LikePath { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::from_str("m/44H/1022H/0H/0/0H").expect("Valid placeholder") + } +} + #[cfg(test)] mod tests { use serde_json::json; - use wallet_kit_common::json::{ - assert_json_value_eq_after_roundtrip, assert_json_value_ne_after_roundtrip, + use wallet_kit_common::{ + error::hdpath_error::HDPathError, + json::{assert_json_value_eq_after_roundtrip, assert_json_value_ne_after_roundtrip}, }; - use crate::{derivation::derivation::Derivation, hdpath_error::HDPathError}; + use crate::derivation::derivation::Derivation; use super::BIP44LikePath; @@ -104,6 +129,14 @@ mod tests { assert_eq!(a.to_string(), str); } + #[test] + fn placeholder() { + assert_eq!( + BIP44LikePath::placeholder().to_string(), + "m/44H/1022H/0H/0/0H" + ); + } + #[test] fn invalid_depth_1() { assert_eq!( diff --git a/hierarchical_deterministic/src/cap26/cap26_entity_kind.rs b/hierarchical_deterministic/src/cap26/cap26_entity_kind.rs index 9215a212..3aa78af3 100644 --- a/hierarchical_deterministic/src/cap26/cap26_entity_kind.rs +++ b/hierarchical_deterministic/src/cap26/cap26_entity_kind.rs @@ -4,6 +4,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use strum::FromRepr; use crate::bip32::hd_path_component::HDPathValue; +use enum_as_inner::EnumAsInner; /// Account or Identity (used by Personas) part of a CAP26 derivation /// path. @@ -14,6 +15,7 @@ use crate::bip32::hd_path_component::HDPathValue; Clone, Copy, Debug, + EnumAsInner, PartialEq, Eq, Hash, diff --git a/hierarchical_deterministic/src/cap26/cap26_key_kind.rs b/hierarchical_deterministic/src/cap26/cap26_key_kind.rs index 85eba04b..3abb07e9 100644 --- a/hierarchical_deterministic/src/cap26/cap26_key_kind.rs +++ b/hierarchical_deterministic/src/cap26/cap26_key_kind.rs @@ -4,6 +4,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; use strum::FromRepr; use crate::bip32::hd_path_component::HDPathValue; +use enum_as_inner::EnumAsInner; #[derive( Serialize_repr, @@ -13,6 +14,7 @@ use crate::bip32::hd_path_component::HDPathValue; Copy, Debug, PartialEq, + EnumAsInner, Eq, Hash, PartialOrd, diff --git a/hierarchical_deterministic/src/cap26/cap26_path/cap26_path.rs b/hierarchical_deterministic/src/cap26/cap26_path/cap26_path.rs index 39b11b67..af941a49 100644 --- a/hierarchical_deterministic/src/cap26/cap26_path/cap26_path.rs +++ b/hierarchical_deterministic/src/cap26/cap26_path/cap26_path.rs @@ -1,35 +1,99 @@ -use serde::{Deserialize, Serialize}; - use crate::{ bip32::hd_path::HDPath, + cap26::cap26_repr::CAP26Repr, + derivation::derivation_path::DerivationPath, derivation::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}, }; +use enum_as_inner::EnumAsInner; +use serde::{de, Deserializer, Serialize, Serializer}; +use wallet_kit_common::error::hdpath_error::HDPathError; -use super::paths::{account_path::AccountPath, getid_path::GetIDPath}; +use super::paths::{account_path::AccountPath, getid_path::GetIDPath, identity_path::IdentityPath}; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "camelCase")] -#[serde(tag = "discriminator", content = "value")] +/// A derivation path design specifically for Radix Babylon wallets used by Accounts and Personas +/// to be unique per network with separate key spaces for Accounts/Identities (Personas) and key +/// kind: sign transaction or sign auth. +#[derive(Clone, Debug, PartialEq, EnumAsInner, Eq, PartialOrd, Ord)] pub enum CAP26Path { GetID(GetIDPath), AccountPath(AccountPath), + IdentityPath(IdentityPath), +} + +impl TryFrom<&HDPath> for CAP26Path { + type Error = HDPathError; + + fn try_from(value: &HDPath) -> Result { + if let Ok(get_id) = GetIDPath::try_from(value) { + return Ok(get_id.into()); + } + if let Ok(identity_path) = IdentityPath::try_from(value) { + return Ok(identity_path.into()); + } + return AccountPath::try_from(value).map(|p| p.into()); + } +} + +impl Serialize for CAP26Path { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> serde::Deserialize<'de> for CAP26Path { + fn deserialize>(d: D) -> Result { + let s = String::deserialize(d)?; + if let Ok(getid) = GetIDPath::from_str(&s) { + return Ok(CAP26Path::GetID(getid)); + } else { + AccountPath::from_str(&s) + .map(Self::AccountPath) + .map_err(de::Error::custom) + } + } } impl Derivation for CAP26Path { fn hd_path(&self) -> &HDPath { match self { CAP26Path::AccountPath(path) => path.hd_path(), + CAP26Path::IdentityPath(path) => path.hd_path(), CAP26Path::GetID(path) => path.hd_path(), } } + + fn derivation_path(&self) -> DerivationPath { + DerivationPath::CAP26(self.clone()) + } + fn scheme(&self) -> DerivationPathScheme { match self { CAP26Path::AccountPath(p) => p.scheme(), + CAP26Path::IdentityPath(p) => p.scheme(), CAP26Path::GetID(p) => p.scheme(), } } } +impl From for CAP26Path { + fn from(value: AccountPath) -> Self { + Self::AccountPath(value) + } +} +impl From for CAP26Path { + fn from(value: IdentityPath) -> Self { + Self::IdentityPath(value) + } +} +impl From for CAP26Path { + fn from(value: GetIDPath) -> Self { + Self::GetID(value) + } +} + impl CAP26Path { pub fn placeholder_account() -> Self { Self::AccountPath(AccountPath::placeholder()) @@ -38,6 +102,9 @@ impl CAP26Path { #[cfg(test)] mod tests { + use serde_json::json; + use wallet_kit_common::json::assert_json_value_eq_after_roundtrip; + use crate::{ cap26::cap26_path::paths::{account_path::AccountPath, getid_path::GetIDPath}, derivation::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}, @@ -76,4 +143,32 @@ mod tests { GetIDPath::default().hd_path() ); } + + #[test] + fn into_from_account_path() { + assert_eq!( + CAP26Path::AccountPath(AccountPath::placeholder()), + AccountPath::placeholder().into() + ); + } + + #[test] + fn into_from_getid_path() { + assert_eq!( + CAP26Path::GetID(GetIDPath::default()), + GetIDPath::default().into() + ); + } + + #[test] + fn json_roundtrip_getid() { + let model: CAP26Path = GetIDPath::default().into(); + assert_json_value_eq_after_roundtrip(&model, json!("m/44H/1022H/365H")); + } + + #[test] + fn json_roundtrip_account() { + let model: CAP26Path = AccountPath::placeholder().into(); + assert_json_value_eq_after_roundtrip(&model, json!("m/44H/1022H/1H/525H/1460H/0H")); + } } diff --git a/hierarchical_deterministic/src/cap26/cap26_path/paths/account_path.rs b/hierarchical_deterministic/src/cap26/cap26_path/paths/account_path.rs index 3042c79d..9f54fce6 100644 --- a/hierarchical_deterministic/src/cap26/cap26_path/paths/account_path.rs +++ b/hierarchical_deterministic/src/cap26/cap26_path/paths/account_path.rs @@ -1,22 +1,49 @@ use serde::{de, Deserializer, Serialize, Serializer}; -use wallet_kit_common::network_id::NetworkID; +use wallet_kit_common::{error::hdpath_error::HDPathError, network_id::NetworkID}; use crate::{ bip32::{hd_path::HDPath, hd_path_component::HDPathValue}, cap26::{ - cap26_entity_kind::CAP26EntityKind, cap26_key_kind::CAP26KeyKind, cap26_repr::CAP26Repr, + cap26_entity_kind::CAP26EntityKind, cap26_key_kind::CAP26KeyKind, + cap26_path::cap26_path::CAP26Path, cap26_repr::CAP26Repr, + }, + derivation::{ + derivation::Derivation, derivation_path::DerivationPath, + derivation_path_scheme::DerivationPathScheme, }, - derivation::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}, - hdpath_error::HDPathError, }; +use super::is_entity_path::IsEntityPath; + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct AccountPath { pub path: HDPath, pub network_id: NetworkID, - pub entity_kind: CAP26EntityKind, - pub key_kind: CAP26KeyKind, - pub index: HDPathValue, + entity_kind: CAP26EntityKind, + key_kind: CAP26KeyKind, + index: HDPathValue, +} + +impl IsEntityPath for AccountPath { + fn network_id(&self) -> NetworkID { + self.network_id + } + + fn key_kind(&self) -> CAP26KeyKind { + self.key_kind + } + + fn index(&self) -> HDPathValue { + self.index + } +} + +impl TryFrom<&HDPath> for AccountPath { + type Error = HDPathError; + + fn try_from(value: &HDPath) -> Result { + Self::try_from_hdpath(value) + } } impl CAP26Repr for AccountPath { @@ -42,13 +69,14 @@ impl CAP26Repr for AccountPath { } impl AccountPath { + /// A placeholder used to facilitate unit tests. pub fn placeholder() -> Self { Self::from_str("m/44H/1022H/1H/525H/1460H/0H").unwrap() } } impl Serialize for AccountPath { - /// Serializes this `AccountAddress` into its bech32 address string as JSON. + /// Serializes this `AccountPath` into JSON as a string on: "m/44H/1022H/1H/525H/1460H/0H" format fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where S: Serializer, @@ -58,7 +86,8 @@ impl Serialize for AccountPath { } impl<'de> serde::Deserialize<'de> for AccountPath { - /// Tries to deserializes a JSON string as a bech32 address into an `AccountAddress`. + /// Tries to deserializes a JSON string as a derivation path string into a `AccountPath` + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; AccountPath::from_str(&s).map_err(de::Error::custom) @@ -77,7 +106,9 @@ impl Derivation for AccountPath { fn hd_path(&self) -> &HDPath { &self.path } - + fn derivation_path(&self) -> DerivationPath { + DerivationPath::CAP26(CAP26Path::AccountPath(self.clone())) + } fn scheme(&self) -> DerivationPathScheme { DerivationPathScheme::Cap26 } @@ -87,16 +118,18 @@ impl Derivation for AccountPath { mod tests { use serde_json::json; use wallet_kit_common::{ + error::hdpath_error::HDPathError, json::{assert_json_value_eq_after_roundtrip, assert_json_value_ne_after_roundtrip}, network_id::NetworkID, }; use crate::{ + bip32::hd_path::HDPath, cap26::{ - cap26_entity_kind::CAP26EntityKind, cap26_key_kind::CAP26KeyKind, cap26_repr::CAP26Repr, + cap26_entity_kind::CAP26EntityKind, cap26_key_kind::CAP26KeyKind, + cap26_path::paths::is_entity_path::IsEntityPath, cap26_repr::CAP26Repr, }, derivation::derivation::Derivation, - hdpath_error::HDPathError, }; use super::AccountPath; @@ -126,6 +159,14 @@ mod tests { assert_eq!(built, parsed) } + #[test] + fn new_tx_sign() { + assert_eq!( + AccountPath::new_mainnet_transaction_signing(77).to_string(), + "m/44H/1022H/1H/525H/1460H/77H" + ); + } + #[test] fn invalid_depth() { assert_eq!( @@ -155,8 +196,8 @@ mod tests { assert_eq!( AccountPath::from_str("m/44H/1022H/1H/618H/1460H/0H"), Err(HDPathError::WrongEntityKind( - CAP26EntityKind::Identity, - CAP26EntityKind::Account + CAP26EntityKind::Identity.discriminant(), + CAP26EntityKind::Account.discriminant() )) ) } @@ -235,4 +276,28 @@ mod tests { assert_json_value_eq_after_roundtrip(&parsed, json!(str)); assert_json_value_ne_after_roundtrip(&parsed, json!("m/44H/1022H/1H/525H/1460H/1H")); } + + #[test] + fn is_entity_path_index() { + let sut = AccountPath::placeholder(); + assert_eq!(sut.index(), 0); + assert_eq!(sut.network_id(), NetworkID::Mainnet); + assert_eq!(sut.key_kind(), CAP26KeyKind::TransactionSigning); + } + + #[test] + fn try_from_hdpath_valid() { + let hdpath = HDPath::from_str("m/44H/1022H/1H/525H/1460H/0H").unwrap(); + let account_path = AccountPath::try_from(&hdpath); + assert!(account_path.is_ok()); + } + + #[test] + fn try_from_hdpath_invalid() { + let hdpath = HDPath::from_str("m/44H/1022H/1H/618H/1460H/0H").unwrap(); + assert_eq!( + AccountPath::try_from(&hdpath), + Err(HDPathError::WrongEntityKind(618, 525)) + ); + } } diff --git a/hierarchical_deterministic/src/cap26/cap26_path/paths/getid_path.rs b/hierarchical_deterministic/src/cap26/cap26_path/paths/getid_path.rs index 24361f6c..3de24eb0 100644 --- a/hierarchical_deterministic/src/cap26/cap26_path/paths/getid_path.rs +++ b/hierarchical_deterministic/src/cap26/cap26_path/paths/getid_path.rs @@ -1,9 +1,12 @@ use serde::{de, Deserializer, Serialize, Serializer}; +use wallet_kit_common::error::hdpath_error::HDPathError; use crate::{ bip32::{hd_path::HDPath, hd_path_component::HDPathValue}, - derivation::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}, - hdpath_error::HDPathError, + derivation::{ + derivation::Derivation, derivation_path::DerivationPath, + derivation_path_scheme::DerivationPathScheme, + }, }; /// Use it with `GetIDPath::default()` to create the path `m/44'/1022'/365'` @@ -13,6 +16,9 @@ use crate::{ pub struct GetIDPath(HDPath); impl Derivation for GetIDPath { + fn derivation_path(&self) -> DerivationPath { + DerivationPath::CAP26(self.clone().into()) + } fn hd_path(&self) -> &HDPath { &self.0 } @@ -27,12 +33,13 @@ impl Default for GetIDPath { } } -impl GetIDPath { - pub const LAST_COMPONENT_VALUE: HDPathValue = 365; +impl TryFrom<&HDPath> for GetIDPath { + type Error = HDPathError; - pub fn from_str(s: &str) -> Result { + fn try_from(value: &HDPath) -> Result { use HDPathError::*; - let (path, components) = HDPath::try_parse_base(s, HDPathError::InvalidDepthOfCAP26Path)?; + let (path, components) = + HDPath::try_parse_base_hdpath(value, HDPathError::InvalidDepthOfCAP26Path)?; if path.depth() != 3 { return Err(InvalidDepthOfCAP26Path); } @@ -46,8 +53,17 @@ impl GetIDPath { } } +impl GetIDPath { + pub const LAST_COMPONENT_VALUE: HDPathValue = 365; + + pub fn from_str(s: &str) -> Result { + let (path, _) = HDPath::try_parse_base(s, HDPathError::InvalidDepthOfCAP26Path)?; + return Self::try_from(&path); + } +} + impl Serialize for GetIDPath { - /// Serializes this `AccountAddress` into its bech32 address string as JSON. + /// Serializes this `GetIDPath` into JSON as a derivation path string on format `m/1022H/365H` fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where S: Serializer, @@ -57,7 +73,8 @@ impl Serialize for GetIDPath { } impl<'de> serde::Deserialize<'de> for GetIDPath { - /// Tries to deserializes a JSON string as a bech32 address into an `AccountAddress`. + /// Tries to deserializes a JSON string as derivation path string into a `GetIDPath` + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; GetIDPath::from_str(&s).map_err(de::Error::custom) @@ -75,9 +92,12 @@ impl TryInto for &str { #[cfg(test)] mod tests { use serde_json::json; - use wallet_kit_common::json::assert_json_value_eq_after_roundtrip; + use wallet_kit_common::{ + error::hdpath_error::HDPathError, + json::{assert_json_value_eq_after_roundtrip, assert_json_value_fails}, + }; - use crate::{derivation::derivation::Derivation, hdpath_error::HDPathError}; + use crate::derivation::derivation::Derivation; use super::GetIDPath; @@ -114,4 +134,9 @@ mod tests { let parsed: GetIDPath = str.try_into().unwrap(); assert_json_value_eq_after_roundtrip(&parsed, json!(str)); } + + #[test] + fn json_roundtrip_invalid() { + assert_json_value_fails::(json!("m/44H/1022H/99H")); + } } diff --git a/hierarchical_deterministic/src/cap26/cap26_path/paths/identity_path.rs b/hierarchical_deterministic/src/cap26/cap26_path/paths/identity_path.rs new file mode 100644 index 00000000..75a05c06 --- /dev/null +++ b/hierarchical_deterministic/src/cap26/cap26_path/paths/identity_path.rs @@ -0,0 +1,320 @@ +use serde::{de, Deserializer, Serialize, Serializer}; +use wallet_kit_common::{error::hdpath_error::HDPathError, network_id::NetworkID}; + +use crate::{ + bip32::{hd_path::HDPath, hd_path_component::HDPathValue}, + cap26::{ + cap26_entity_kind::CAP26EntityKind, cap26_key_kind::CAP26KeyKind, + cap26_path::cap26_path::CAP26Path, cap26_repr::CAP26Repr, + }, + derivation::{ + derivation::Derivation, derivation_path::DerivationPath, + derivation_path_scheme::DerivationPathScheme, + }, +}; + +use super::is_entity_path::IsEntityPath; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct IdentityPath { + pub path: HDPath, + pub network_id: NetworkID, + entity_kind: CAP26EntityKind, + key_kind: CAP26KeyKind, + index: HDPathValue, +} + +impl IsEntityPath for IdentityPath { + fn network_id(&self) -> NetworkID { + self.network_id + } + fn key_kind(&self) -> CAP26KeyKind { + self.key_kind + } + fn index(&self) -> HDPathValue { + self.index + } +} + +impl TryFrom<&HDPath> for IdentityPath { + type Error = HDPathError; + + fn try_from(value: &HDPath) -> Result { + Self::try_from_hdpath(value) + } +} + +impl CAP26Repr for IdentityPath { + fn entity_kind() -> Option { + Some(CAP26EntityKind::Identity) + } + + fn __with_path_and_components( + path: HDPath, + network_id: NetworkID, + entity_kind: CAP26EntityKind, + key_kind: CAP26KeyKind, + index: HDPathValue, + ) -> Self { + Self { + path, + network_id, + entity_kind, + key_kind, + index, + } + } +} + +impl IdentityPath { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::from_str("m/44H/1022H/1H/618H/1460H/0H").unwrap() + } +} + +impl Serialize for IdentityPath { + /// Serializes this `IdentityPath` into its bech32 address string as JSON. + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> serde::Deserialize<'de> for IdentityPath { + /// Tries to deserializes a JSON string as a bech32 address into an `IdentityPath`. + #[cfg(not(tarpaulin_include))] // false negative + fn deserialize>(d: D) -> Result { + let s = String::deserialize(d)?; + IdentityPath::from_str(&s).map_err(de::Error::custom) + } +} + +impl TryInto for &str { + type Error = HDPathError; + + fn try_into(self) -> Result { + IdentityPath::from_str(self) + } +} + +impl Derivation for IdentityPath { + fn hd_path(&self) -> &HDPath { + &self.path + } + + fn derivation_path(&self) -> DerivationPath { + DerivationPath::CAP26(CAP26Path::IdentityPath(self.clone())) + } + + fn scheme(&self) -> DerivationPathScheme { + DerivationPathScheme::Cap26 + } +} + +#[cfg(test)] +mod tests { + use crate::{ + bip32::hd_path::HDPath, + cap26::{ + cap26_entity_kind::CAP26EntityKind, cap26_key_kind::CAP26KeyKind, + cap26_path::paths::is_entity_path::IsEntityPath, cap26_repr::CAP26Repr, + }, + derivation::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}, + }; + use serde_json::json; + use wallet_kit_common::{ + error::hdpath_error::HDPathError, + json::{assert_json_value_eq_after_roundtrip, assert_json_value_ne_after_roundtrip}, + network_id::NetworkID, + }; + + use super::IdentityPath; + + #[test] + fn entity_kind() { + assert_eq!(IdentityPath::entity_kind(), Some(CAP26EntityKind::Identity)); + } + + #[test] + fn hd_path() { + let str = "m/44H/1022H/1H/618H/1460H/0H"; + let parsed: IdentityPath = str.try_into().unwrap(); + assert_eq!(parsed.hd_path().depth(), 6); + } + + #[test] + fn string_roundtrip() { + let str = "m/44H/1022H/1H/618H/1460H/0H"; + let parsed: IdentityPath = str.try_into().unwrap(); + assert_eq!(parsed.network_id, NetworkID::Mainnet); + assert_eq!(parsed.entity_kind, CAP26EntityKind::Identity); + assert_eq!(parsed.key_kind, CAP26KeyKind::TransactionSigning); + assert_eq!(parsed.index, 0); + assert_eq!(parsed.to_string(), str); + let built = IdentityPath::new(NetworkID::Mainnet, CAP26KeyKind::TransactionSigning, 0); + assert_eq!(built, parsed) + } + + #[test] + fn new_tx_sign() { + assert_eq!( + IdentityPath::new_mainnet_transaction_signing(77).to_string(), + "m/44H/1022H/1H/618H/1460H/77H" + ); + } + + #[test] + fn invalid_depth() { + assert_eq!( + IdentityPath::from_str("m/44H/1022H"), + Err(HDPathError::InvalidDepthOfCAP26Path) + ) + } + + #[test] + fn not_all_hardened() { + assert_eq!( + IdentityPath::from_str("m/44H/1022H/1H/618H/1460H/0"), // last not hardened + Err(HDPathError::NotAllComponentsAreHardened) + ) + } + + #[test] + fn cointype_not_found() { + assert_eq!( + IdentityPath::from_str("m/44H/33H/1H/618H/1460H/0"), // `33` instead of 1022 + Err(HDPathError::CoinTypeNotFound(33)) + ) + } + + #[test] + fn fails_when_entity_type_identity() { + assert_eq!( + IdentityPath::from_str("m/44H/1022H/1H/525H/1460H/0H"), + Err(HDPathError::WrongEntityKind( + CAP26EntityKind::Account.discriminant(), + CAP26EntityKind::Identity.discriminant() + )) + ) + } + + #[test] + fn fails_when_entity_type_does_not_exist() { + assert_eq!( + IdentityPath::from_str("m/44H/1022H/1H/99999H/1460H/0H"), + Err(HDPathError::InvalidEntityKind(99999)) + ) + } + + #[test] + fn fails_when_key_kind_does_not_exist() { + assert_eq!( + IdentityPath::from_str("m/44H/1022H/1H/618H/22222H/0H"), + Err(HDPathError::InvalidKeyKind(22222)) + ) + } + + #[test] + fn fails_when_network_id_is_out_of_bounds() { + assert_eq!( + IdentityPath::from_str("m/44H/1022H/4444H/618H/1460H/0H"), + Err(HDPathError::InvalidNetworkIDExceedsLimit(4444)) + ) + } + + #[test] + fn fails_when_not_bip44() { + assert_eq!( + IdentityPath::from_str("m/777H/1022H/1H/618H/1460H/0H"), + Err(HDPathError::BIP44PurposeNotFound(777)) + ) + } + + #[test] + fn missing_leading_m_is_ok() { + assert!(IdentityPath::from_str("44H/1022H/1H/618H/1460H/0H").is_ok()) + } + + #[test] + fn fails_when_index_is_too_large() { + assert_eq!( + IdentityPath::from_str("m/44H/1022H/1H/618H/1460H/4294967296H"), + Err(HDPathError::InvalidBIP32Path( + "m/44H/1022H/1H/618H/1460H/4294967296H".to_string() + )) + ) + } + + #[test] + fn inequality_different_index() { + let a: IdentityPath = "m/44H/1022H/1H/618H/1460H/0H".try_into().unwrap(); + let b: IdentityPath = "m/44H/1022H/1H/618H/1460H/1H".try_into().unwrap(); + assert!(a != b); + } + #[test] + fn inequality_different_network_id() { + let a: IdentityPath = "m/44H/1022H/1H/618H/1460H/0H".try_into().unwrap(); + let b: IdentityPath = "m/44H/1022H/2H/618H/1460H/0H".try_into().unwrap(); + assert!(a != b); + } + + #[test] + fn inequality_different_key_kind() { + let a: IdentityPath = "m/44H/1022H/1H/618H/1460H/0H".try_into().unwrap(); + let b: IdentityPath = "m/44H/1022H/1H/618H/1678H/0H".try_into().unwrap(); + assert!(a != b); + } + + #[test] + fn json_roundtrip() { + let str = "m/44H/1022H/1H/618H/1460H/0H"; + let parsed: IdentityPath = str.try_into().unwrap(); + assert_json_value_eq_after_roundtrip(&parsed, json!(str)); + assert_json_value_ne_after_roundtrip(&parsed, json!("m/44H/1022H/1H/618H/1460H/1H")); + } + + #[test] + fn identity_path_scheme() { + assert_eq!( + IdentityPath::placeholder().scheme(), + DerivationPathScheme::Cap26 + ); + } + + #[test] + fn identity_path_derivation_path() { + assert_eq!( + IdentityPath::placeholder() + .derivation_path() + .hd_path() + .to_string(), + "m/44H/1022H/1H/618H/1460H/0H" + ); + } + + #[test] + fn try_from_hdpath_valid() { + let hdpath = HDPath::from_str("m/44H/1022H/1H/618H/1460H/0H").unwrap(); + assert!(IdentityPath::try_from(&hdpath).is_ok()); + } + + #[test] + fn try_from_hdpath_invalid() { + let hdpath = HDPath::from_str("m/44H/1022H/1H/525H/1460H/0H").unwrap(); + assert_eq!( + IdentityPath::try_from(&hdpath), + Err(HDPathError::WrongEntityKind(525, 618)) + ); + } + + #[test] + fn is_entity_path_index() { + let sut = IdentityPath::placeholder(); + assert_eq!(sut.index(), 0); + assert_eq!(sut.network_id(), NetworkID::Mainnet); + assert_eq!(sut.key_kind(), CAP26KeyKind::TransactionSigning); + } +} diff --git a/hierarchical_deterministic/src/cap26/cap26_path/paths/is_entity_path.rs b/hierarchical_deterministic/src/cap26/cap26_path/paths/is_entity_path.rs new file mode 100644 index 00000000..3359c280 --- /dev/null +++ b/hierarchical_deterministic/src/cap26/cap26_path/paths/is_entity_path.rs @@ -0,0 +1,31 @@ +use wallet_kit_common::network_id::NetworkID; + +use crate::{ + bip32::hd_path_component::HDPathValue, + cap26::{cap26_key_kind::CAP26KeyKind, cap26_repr::CAP26Repr}, +}; + +pub trait IsEntityPath: CAP26Repr { + fn network_id(&self) -> NetworkID; + fn key_kind(&self) -> CAP26KeyKind; + fn index(&self) -> HDPathValue; +} + +pub trait HasEntityPath { + fn path(&self) -> Path; + + #[cfg(not(tarpaulin_include))] // false negative + fn network_id(&self) -> NetworkID { + self.path().network_id() + } + + #[cfg(not(tarpaulin_include))] // false negative + fn key_kind(&self) -> CAP26KeyKind { + self.path().key_kind() + } + + #[cfg(not(tarpaulin_include))] // false negative + fn index(&self) -> HDPathValue { + self.path().index() + } +} diff --git a/hierarchical_deterministic/src/cap26/cap26_path/paths/mod.rs b/hierarchical_deterministic/src/cap26/cap26_path/paths/mod.rs index 3dc6db78..d95d20cc 100644 --- a/hierarchical_deterministic/src/cap26/cap26_path/paths/mod.rs +++ b/hierarchical_deterministic/src/cap26/cap26_path/paths/mod.rs @@ -1,2 +1,4 @@ pub mod account_path; pub mod getid_path; +pub mod identity_path; +pub mod is_entity_path; diff --git a/hierarchical_deterministic/src/cap26/cap26_repr.rs b/hierarchical_deterministic/src/cap26/cap26_repr.rs index c8f23b74..7fdc7909 100644 --- a/hierarchical_deterministic/src/cap26/cap26_repr.rs +++ b/hierarchical_deterministic/src/cap26/cap26_repr.rs @@ -1,4 +1,4 @@ -use wallet_kit_common::network_id::NetworkID; +use wallet_kit_common::{error::hdpath_error::HDPathError, network_id::NetworkID}; use crate::{ bip32::{ @@ -6,7 +6,6 @@ use crate::{ hd_path_component::{HDPathComponent, HDPathValue}, }, derivation::derivation::Derivation, - hdpath_error::HDPathError, }; use super::{cap26_entity_kind::CAP26EntityKind, cap26_key_kind::CAP26KeyKind}; @@ -24,9 +23,11 @@ pub trait CAP26Repr: Derivation { index: HDPathValue, ) -> Self; - fn from_str(s: &str) -> Result { + #[cfg(not(tarpaulin_include))] // false negative, this is in fact heavily tested. + fn try_from_hdpath(hdpath: &HDPath) -> Result { use HDPathError::*; - let (path, components) = HDPath::try_parse_base(s, HDPathError::InvalidDepthOfCAP26Path)?; + let (path, components) = + HDPath::try_parse_base_hdpath(hdpath, HDPathError::InvalidDepthOfCAP26Path)?; if !components.clone().iter().all(|c| c.is_hardened()) { return Err(NotAllComponentsAreHardened); } @@ -53,7 +54,10 @@ pub trait CAP26Repr: Derivation { if let Some(expected_entity_kind) = Self::entity_kind() { if entity_kind != expected_entity_kind { - return Err(WrongEntityKind(entity_kind, expected_entity_kind)); + return Err(WrongEntityKind( + entity_kind.discriminant(), + expected_entity_kind.discriminant(), + )); } } @@ -74,6 +78,12 @@ pub trait CAP26Repr: Derivation { )); } + #[cfg(not(tarpaulin_include))] // false negative, this is in fact heavily tested. + fn from_str(s: &str) -> Result { + let (path, _) = HDPath::try_parse_base(s, HDPathError::InvalidDepthOfCAP26Path)?; + Self::try_from_hdpath(&path) + } + fn new(network_id: NetworkID, key_kind: CAP26KeyKind, index: HDPathValue) -> Self { let entity_kind = Self::entity_kind().expect("GetID cannot be used with this constructor"); let c0 = HDPathComponent::bip44_purpose(); @@ -87,4 +97,8 @@ pub trait CAP26Repr: Derivation { let path = HDPath::from_components(components); return Self::__with_path_and_components(path, network_id, entity_kind, key_kind, index); } + + fn new_mainnet_transaction_signing(index: HDPathValue) -> Self { + Self::new(NetworkID::Mainnet, CAP26KeyKind::TransactionSigning, index) + } } diff --git a/hierarchical_deterministic/src/derivation/derivation.rs b/hierarchical_deterministic/src/derivation/derivation.rs index ca957c33..b0595007 100644 --- a/hierarchical_deterministic/src/derivation/derivation.rs +++ b/hierarchical_deterministic/src/derivation/derivation.rs @@ -1,12 +1,19 @@ -use crate::bip32::hd_path::HDPath; +use crate::bip32::{hd_path::HDPath, hd_path_component::HDPathComponent}; -use super::derivation_path_scheme::DerivationPathScheme; +use super::{derivation_path::DerivationPath, derivation_path_scheme::DerivationPathScheme}; pub trait Derivation: Sized { + fn derivation_path(&self) -> DerivationPath; fn hd_path(&self) -> &HDPath; fn to_string(&self) -> String { self.hd_path().to_string() } + fn scheme(&self) -> DerivationPathScheme; + + #[cfg(not(tarpaulin_include))] // false negative + fn last_component(&self) -> &HDPathComponent { + self.hd_path().components().last().unwrap() + } } diff --git a/hierarchical_deterministic/src/derivation/derivation_path.rs b/hierarchical_deterministic/src/derivation/derivation_path.rs index 6ff5bfba..e9d8c38c 100644 --- a/hierarchical_deterministic/src/derivation/derivation_path.rs +++ b/hierarchical_deterministic/src/derivation/derivation_path.rs @@ -1,35 +1,99 @@ -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; +use super::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}; use crate::{ bip32::hd_path::HDPath, bip44::bip44_like_path::BIP44LikePath, - cap26::cap26_path::{cap26_path::CAP26Path, paths::account_path::AccountPath}, + cap26::cap26_path::{ + cap26_path::CAP26Path, + paths::{account_path::AccountPath, getid_path::GetIDPath, identity_path::IdentityPath}, + }, }; - -use super::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}; +use enum_as_inner::EnumAsInner; +use std::fmt::{Debug, Formatter}; /// A derivation path on either supported schemes, either Babylon (CAP26) or Olympia (BIP44Like). -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "camelCase")] -#[serde(tag = "discriminator", content = "value")] +#[derive(Clone, PartialEq, Eq, EnumAsInner, PartialOrd, Ord)] pub enum DerivationPath { CAP26(CAP26Path), BIP44Like(BIP44LikePath), } +impl TryFrom<&HDPath> for DerivationPath { + type Error = wallet_kit_common::error::common_error::CommonError; + + fn try_from(value: &HDPath) -> Result { + if let Ok(bip44) = BIP44LikePath::try_from(value) { + return Ok(bip44.into()); + }; + return CAP26Path::try_from(value) + .map(|p| p.derivation_path()) + .map_err(|e| e.into()); + } +} + +impl Debug for DerivationPath { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_string()) + } +} + +impl<'de> serde::Deserialize<'de> for DerivationPath { + /// Tries to deserializes a JSON string as a bech32 address into an `AccountAddress`. + #[cfg(not(tarpaulin_include))] // false negative + fn deserialize>(d: D) -> Result { + #[derive(Deserialize)] + pub struct Inner { + pub scheme: DerivationPathScheme, + pub path: serde_json::Value, + } + + let inner = Inner::deserialize(d)?; + + let derivation_path = match inner.scheme { + DerivationPathScheme::Cap26 => DerivationPath::CAP26( + CAP26Path::deserialize(inner.path).map_err(serde::de::Error::custom)?, + ), + DerivationPathScheme::Bip44Olympia => DerivationPath::BIP44Like( + BIP44LikePath::deserialize(inner.path).map_err(serde::de::Error::custom)?, + ), + }; + Ok(derivation_path) + } +} + +impl Serialize for DerivationPath { + #[cfg(not(tarpaulin_include))] // false negative + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("DerivationPath", 2)?; + state.serialize_field("scheme", &self.scheme())?; + state.serialize_field("path", &self.hd_path().to_string())?; + state.end() + } +} + impl DerivationPath { + /// A placeholder used to facilitate unit tests. pub fn placeholder() -> Self { Self::CAP26(CAP26Path::AccountPath(AccountPath::placeholder())) } } impl Derivation for DerivationPath { + fn derivation_path(&self) -> DerivationPath { + self.clone() + } + fn hd_path(&self) -> &HDPath { match self { DerivationPath::CAP26(path) => path.hd_path(), DerivationPath::BIP44Like(path) => path.hd_path(), } } + fn scheme(&self) -> DerivationPathScheme { match self { DerivationPath::CAP26(p) => p.scheme(), @@ -44,11 +108,48 @@ impl DerivationPath { } } +impl From for DerivationPath { + fn from(value: AccountPath) -> Self { + Self::CAP26(value.into()) + } +} + +impl From for DerivationPath { + fn from(value: IdentityPath) -> Self { + Self::CAP26(value.into()) + } +} + +impl From for DerivationPath { + fn from(value: GetIDPath) -> Self { + Self::CAP26(value.into()) + } +} + +impl From for DerivationPath { + fn from(value: BIP44LikePath) -> Self { + Self::BIP44Like(value) + } +} + +impl From for DerivationPath { + fn from(value: CAP26Path) -> Self { + Self::CAP26(value) + } +} + #[cfg(test)] mod tests { + use wallet_kit_common::json::assert_eq_after_json_roundtrip; + use crate::{ bip44::bip44_like_path::BIP44LikePath, - cap26::cap26_path::paths::account_path::AccountPath, + cap26::cap26_path::{ + cap26_path::CAP26Path, + paths::{ + account_path::AccountPath, getid_path::GetIDPath, identity_path::IdentityPath, + }, + }, derivation::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}, }; @@ -84,4 +185,145 @@ mod tests { BIP44LikePath::new(0).hd_path() ); } + + #[test] + fn into_from_account_bip44_path() { + assert_eq!( + DerivationPath::BIP44Like(BIP44LikePath::placeholder()), + BIP44LikePath::placeholder().into() + ); + } + + #[test] + fn as_bip44_path() { + let path: DerivationPath = BIP44LikePath::placeholder().into(); + assert_eq!(path.as_bip44_like().unwrap(), &BIP44LikePath::placeholder()); + } + + #[test] + fn into_from_account_cap26_path() { + assert_eq!( + DerivationPath::CAP26(AccountPath::placeholder().into()), + AccountPath::placeholder().into() + ); + } + + #[test] + fn into_from_identity_cap26_path() { + assert_eq!( + DerivationPath::CAP26(IdentityPath::placeholder().into()), + IdentityPath::placeholder().into() + ); + } + + #[test] + fn derivation_path_identity() { + let derivation_path: DerivationPath = IdentityPath::placeholder().into(); + assert_eq!(derivation_path, derivation_path.derivation_path()); + } + + #[test] + fn try_from_hdpath_account() { + let derivation_path: DerivationPath = AccountPath::placeholder().into(); + let hd_path = derivation_path.hd_path(); + assert_eq!(DerivationPath::try_from(hd_path), Ok(derivation_path)); + } + + #[test] + fn try_from_hdpath_identity() { + let derivation_path: DerivationPath = IdentityPath::placeholder().into(); + let hd_path = derivation_path.hd_path(); + assert_eq!(DerivationPath::try_from(hd_path), Ok(derivation_path)); + } + + #[test] + fn try_from_hdpath_bip44() { + let derivation_path: DerivationPath = BIP44LikePath::placeholder().into(); + let hd_path = derivation_path.hd_path(); + assert_eq!(DerivationPath::try_from(hd_path), Ok(derivation_path)); + } + + #[test] + fn try_from_hdpath_getid() { + let derivation_path: DerivationPath = GetIDPath::default().into(); + let hd_path = derivation_path.hd_path(); + assert_eq!(DerivationPath::try_from(hd_path), Ok(derivation_path)); + } + + #[test] + fn from_cap26() { + let derivation_path: DerivationPath = + CAP26Path::AccountPath(AccountPath::placeholder()).into(); + assert_eq!( + derivation_path.derivation_path(), + AccountPath::placeholder().derivation_path() + ) + } + + #[test] + fn as_cap26_path() { + let path: DerivationPath = AccountPath::placeholder().into(); + assert_eq!( + path.as_cap26().unwrap(), + &CAP26Path::AccountPath(AccountPath::placeholder()) + ); + } + + #[test] + fn into_from_getid_path() { + assert_eq!( + DerivationPath::CAP26(GetIDPath::default().into()), + GetIDPath::default().into() + ); + } + + #[test] + fn json_cap26_account() { + let model = DerivationPath::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + "#, + ); + } + + #[test] + fn debug() { + let model = DerivationPath::placeholder(); + assert_eq!(format!("{:?}", model), "m/44H/1022H/1H/525H/1460H/0H") + } + + #[test] + fn json_cap26_getid() { + let path = GetIDPath::default(); + let model: DerivationPath = path.into(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "scheme": "cap26", + "path": "m/44H/1022H/365H" + } + "#, + ); + } + + #[test] + fn json_bip44like_account() { + let path = BIP44LikePath::placeholder(); + let model: DerivationPath = path.into(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "scheme": "bip44Olympia", + "path": "m/44H/1022H/0H/0/0H" + } + "#, + ); + } } diff --git a/hierarchical_deterministic/src/derivation/derivation_path_scheme.rs b/hierarchical_deterministic/src/derivation/derivation_path_scheme.rs index 80549374..e7a8c2bc 100644 --- a/hierarchical_deterministic/src/derivation/derivation_path_scheme.rs +++ b/hierarchical_deterministic/src/derivation/derivation_path_scheme.rs @@ -1,20 +1,20 @@ use serde::{Deserialize, Serialize}; - -use super::slip10_curve::SLIP10Curve; +use wallet_kit_common::types::keys::slip10_curve::SLIP10Curve; /// Which derivation path to used for some particular HD operations /// such as signing or public key derivation. Radix Babylon introduces /// a new scheme call Cap26 but we also need to support BIP44-like used /// by Olympia. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -#[serde(rename_all = "camelCase")] pub enum DerivationPathScheme { /// A BIP32 based derivation path scheme, using SLIP10. + #[serde(rename = "cap26")] Cap26, /// A BIP32 based similar to BIP44, but not strict BIP44 since the /// last path component is hardened (a mistake made during Olympia), /// used to support legacy accounts imported from Olympia wallet. + #[serde(rename = "bip44Olympia")] Bip44Olympia, } @@ -38,14 +38,15 @@ impl DerivationPathScheme { #[cfg(test)] mod tests { use serde_json::json; - use wallet_kit_common::json::{ - assert_json_roundtrip, assert_json_value_eq_after_roundtrip, - assert_json_value_ne_after_roundtrip, + use wallet_kit_common::{ + json::{ + assert_json_roundtrip, assert_json_value_eq_after_roundtrip, + assert_json_value_ne_after_roundtrip, + }, + types::keys::slip10_curve::SLIP10Curve, }; - use crate::derivation::{ - derivation_path_scheme::DerivationPathScheme, slip10_curve::SLIP10Curve, - }; + use crate::derivation::derivation_path_scheme::DerivationPathScheme; #[test] fn curve_from_scheme() { diff --git a/hierarchical_deterministic/src/derivation/hierarchical_deterministic_private_key.rs b/hierarchical_deterministic/src/derivation/hierarchical_deterministic_private_key.rs new file mode 100644 index 00000000..d3390704 --- /dev/null +++ b/hierarchical_deterministic/src/derivation/hierarchical_deterministic_private_key.rs @@ -0,0 +1,75 @@ +use wallet_kit_common::types::keys::{ + ed25519::private_key::Ed25519PrivateKey, private_key::PrivateKey, +}; + +use crate::cap26::{cap26_path::paths::account_path::AccountPath, cap26_repr::CAP26Repr}; + +use super::{ + derivation_path::DerivationPath, + hierarchical_deterministic_public_key::HierarchicalDeterministicPublicKey, +}; + +/// An ephemeral (never persisted) HD PrivateKey which contains +/// the derivation path used to derive it. +pub struct HierarchicalDeterministicPrivateKey { + /// The PrivateKey derived from some HD FactorSource using `derivation_path`. + pub private_key: PrivateKey, + /// Derivation path used to derive the `PrivateKey` from some HD FactorSource. + pub derivation_path: DerivationPath, +} + +impl HierarchicalDeterministicPrivateKey { + /// Instantiates a new `HierarchicalDeterministicPrivateKey` from a PrivateKey and + /// the derivation path used to derive it. + pub fn new(private_key: PrivateKey, derivation_path: DerivationPath) -> Self { + Self { + private_key, + derivation_path, + } + } +} + +impl HierarchicalDeterministicPrivateKey { + /// Returns the public key of the private key with the derivation path intact. + pub fn public_key(&self) -> HierarchicalDeterministicPublicKey { + HierarchicalDeterministicPublicKey::new( + self.private_key.public_key(), + self.derivation_path.clone(), + ) + } + + /// The PrivateKey as hex string. + pub fn to_hex(&self) -> String { + self.private_key.to_hex() + } +} + +impl HierarchicalDeterministicPrivateKey { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::new( + Ed25519PrivateKey::from_str( + "cf52dbc7bb2663223e99fb31799281b813b939440a372d0aa92eb5f5b8516003", + ) + .unwrap() + .into(), + AccountPath::from_str("m/44H/1022H/1H/525H/1460H/0H") + .unwrap() + .into(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::HierarchicalDeterministicPrivateKey; + + #[test] + fn publickey_of_placeholder() { + let sut = HierarchicalDeterministicPrivateKey::placeholder(); + assert_eq!( + sut.public_key().to_hex(), + "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + ); + } +} diff --git a/hierarchical_deterministic/src/derivation/hierarchical_deterministic_public_key.rs b/hierarchical_deterministic/src/derivation/hierarchical_deterministic_public_key.rs new file mode 100644 index 00000000..2b109d80 --- /dev/null +++ b/hierarchical_deterministic/src/derivation/hierarchical_deterministic_public_key.rs @@ -0,0 +1,104 @@ +use serde::{Deserialize, Serialize}; +use wallet_kit_common::{network_id::NetworkID, types::keys::public_key::PublicKey}; + +use crate::{ + cap26::{ + cap26_key_kind::CAP26KeyKind, cap26_path::paths::account_path::AccountPath, + cap26_repr::CAP26Repr, + }, + derivation::{derivation::Derivation, derivation_path::DerivationPath}, +}; + +use super::mnemonic_with_passphrase::MnemonicWithPassphrase; + +/// The **source** of a virtual hierarchical deterministic badge, contains a +/// derivation path and public key, from which a private key is derived which +/// produces virtual badges (signatures). +/// +/// The `.device` `FactorSource` produces `FactorInstance`s with this kind if badge source. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct HierarchicalDeterministicPublicKey { + /// The expected public key of the private key derived at `derivationPath` + pub public_key: PublicKey, + + /// The HD derivation path for the key pair which produces virtual badges (signatures). + pub derivation_path: DerivationPath, +} + +impl HierarchicalDeterministicPublicKey { + pub fn new(public_key: PublicKey, derivation_path: DerivationPath) -> Self { + Self { + public_key, + derivation_path, + } + } +} + +impl HierarchicalDeterministicPublicKey { + pub fn to_hex(&self) -> String { + self.public_key.to_hex() + } + + pub fn to_bytes(&self) -> Vec { + self.public_key.to_bytes() + } +} + +impl HierarchicalDeterministicPublicKey { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + let mwp = MnemonicWithPassphrase::placeholder(); + let path = AccountPath::new(NetworkID::Mainnet, CAP26KeyKind::TransactionSigning, 0); + let private_key = mwp.derive_private_key(path.clone()); + + assert_eq!(path.to_string(), "m/44H/1022H/1H/525H/1460H/0H"); + + assert_eq!( + "cf52dbc7bb2663223e99fb31799281b813b939440a372d0aa92eb5f5b8516003", + private_key.to_hex() + ); + let public_key = private_key.public_key(); + assert_eq!( + "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b", + public_key.to_hex() + ); + // Self::new(public_key, path.into()) + return public_key; + } +} + +#[cfg(test)] +mod tests { + use wallet_kit_common::json::assert_eq_after_json_roundtrip; + + use super::HierarchicalDeterministicPublicKey; + + #[test] + fn to_hex() { + assert_eq!( + HierarchicalDeterministicPublicKey::placeholder().to_hex(), + "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + ); + } + + #[test] + fn json() { + let model = HierarchicalDeterministicPublicKey::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "publicKey": { + "curve": "curve25519", + "compressedData": "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + } + "#, + ); + } +} diff --git a/hierarchical_deterministic/src/derivation/mnemonic_with_passphrase.rs b/hierarchical_deterministic/src/derivation/mnemonic_with_passphrase.rs index d3abb6a8..df962417 100644 --- a/hierarchical_deterministic/src/derivation/mnemonic_with_passphrase.rs +++ b/hierarchical_deterministic/src/derivation/mnemonic_with_passphrase.rs @@ -1,18 +1,17 @@ +use super::hierarchical_deterministic_private_key::HierarchicalDeterministicPrivateKey; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use transaction::signing::{ - ed25519::Ed25519PrivateKey, secp256k1::Secp256k1PrivateKey, PrivateKey, +use wallet_kit_common::types::keys::{ + ed25519::private_key::Ed25519PrivateKey, secp256k1::private_key::Secp256k1PrivateKey, + slip10_curve::SLIP10Curve, }; -use wallet_kit_common::error::Error; +use super::{derivation::Derivation, derivation_path_scheme::DerivationPathScheme}; use crate::{ bip32::hd_path::HDPath, bip39::mnemonic::{Mnemonic, Seed}, }; - -use super::{ - derivation::Derivation, derivation_path_scheme::DerivationPathScheme, slip10_curve::SLIP10Curve, -}; +use wallet_kit_common::error::hdpath_error::HDPathError as Error; /// A BIP39 Mnemonic and BIP39 passphrase - aka "25th word" tuple, /// from which we can derive a HD Root used for derivation. @@ -46,6 +45,13 @@ impl MnemonicWithPassphrase { } } +impl MnemonicWithPassphrase { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::with_passphrase(Mnemonic::placeholder(), "radix".to_string()) + } +} + pub type PrivateKeyBytes = [u8; 32]; impl MnemonicWithPassphrase { @@ -82,22 +88,23 @@ impl MnemonicWithPassphrase { .expect("Valid Secp256k1PrivateKey bytes") } - pub fn derive_private_key(&self, derivation: D) -> PrivateKey + #[cfg(not(tarpaulin_include))] // false negative + pub fn derive_private_key(&self, derivation: D) -> HierarchicalDeterministicPrivateKey where D: Derivation, { let seed = self.to_seed(); - let path = derivation.hd_path(); + let path = derivation.derivation_path(); match derivation.scheme() { DerivationPathScheme::Cap26 => { assert_eq!(derivation.scheme().curve(), SLIP10Curve::Curve25519); - let key = Self::derive_ed25519_private_key(&seed, path); - PrivateKey::Ed25519(key) + let key = Self::derive_ed25519_private_key(&seed, path.hd_path()); + HierarchicalDeterministicPrivateKey::new(key.into(), path) } DerivationPathScheme::Bip44Olympia => { assert_eq!(derivation.scheme().curve(), SLIP10Curve::Secp256k1); - let key = Self::derive_secp256k1_private_key(&seed, path); - PrivateKey::Secp256k1(key) + let key = Self::derive_secp256k1_private_key(&seed, path.hd_path()); + HierarchicalDeterministicPrivateKey::new(key.into(), path) } } } @@ -109,10 +116,13 @@ mod tests { use crate::{ bip39::mnemonic::Mnemonic, bip44::bip44_like_path::BIP44LikePath, - cap26::{cap26_path::paths::account_path::AccountPath, cap26_repr::CAP26Repr}, - keys::key_extensions::{private_key_hex, public_key_hex_from_private}, + cap26::{ + cap26_key_kind::CAP26KeyKind, cap26_path::paths::account_path::AccountPath, + cap26_repr::CAP26Repr, + }, + derivation::derivation::Derivation, }; - use wallet_kit_common::json::assert_eq_after_json_roundtrip; + use wallet_kit_common::{json::assert_eq_after_json_roundtrip, network_id::NetworkID}; use super::MnemonicWithPassphrase; @@ -148,16 +158,16 @@ mod tests { "".to_string(), ); - let private_key: transaction::prelude::PrivateKey = + let private_key = mwp.derive_private_key(AccountPath::from_str("m/44H/1022H/12H/525H/1460H/0H").unwrap()); assert_eq!( "13e971fb16cb2c816d6b9f12176e9b8ab9af1831d006114d344d119ab2715506", - private_key_hex(&private_key) + private_key.to_hex() ); assert_eq!( "451152a1cef7be603205086d4ebac0a0b78fda2ff4684b9dea5ca9ef003d4e7d", - public_key_hex_from_private(&private_key) + private_key.public_key().to_hex() ); } @@ -172,17 +182,17 @@ mod tests { "".to_string(), ); - let private_key: transaction::prelude::PrivateKey = + let private_key = mwp.derive_private_key(BIP44LikePath::from_str("m/44H/1022H/0H/0/5H").unwrap()); assert_eq!( "111323d507d9d690836798e3ef2e5292cfd31092b75b9b59fa584ff593a3d7e4", - private_key_hex(&private_key) + private_key.to_hex() ); assert_eq!( "03e78cdb2e0b7ea6e55e121a58560ccf841a913d3a4a9b8349e0ef00c2102f48d8", - public_key_hex_from_private(&private_key) + private_key.public_key().to_hex() ); } @@ -206,4 +216,22 @@ mod tests { "#, ); } + + #[test] + fn keys_for_placeholder() { + let mwp = MnemonicWithPassphrase::placeholder(); + let path = AccountPath::new(NetworkID::Mainnet, CAP26KeyKind::TransactionSigning, 0); + let private_key = mwp.derive_private_key(path.clone()); + + assert_eq!(path.to_string(), "m/44H/1022H/1H/525H/1460H/0H"); + + assert_eq!( + "cf52dbc7bb2663223e99fb31799281b813b939440a372d0aa92eb5f5b8516003", + private_key.to_hex() + ); + assert_eq!( + "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b", + private_key.public_key().to_hex() + ); + } } diff --git a/hierarchical_deterministic/src/derivation/mod.rs b/hierarchical_deterministic/src/derivation/mod.rs index 2c931b0f..334e57ae 100644 --- a/hierarchical_deterministic/src/derivation/mod.rs +++ b/hierarchical_deterministic/src/derivation/mod.rs @@ -1,5 +1,6 @@ pub mod derivation; pub mod derivation_path; pub mod derivation_path_scheme; +pub mod hierarchical_deterministic_private_key; +pub mod hierarchical_deterministic_public_key; pub mod mnemonic_with_passphrase; -pub mod slip10_curve; diff --git a/hierarchical_deterministic/src/keys/key_extensions.rs b/hierarchical_deterministic/src/keys/key_extensions.rs deleted file mode 100644 index d04bb093..00000000 --- a/hierarchical_deterministic/src/keys/key_extensions.rs +++ /dev/null @@ -1,28 +0,0 @@ -use radix_engine_common::crypto::PublicKey; -use transaction::signing::PrivateKey; - -pub fn private_key_bytes(private_key: &PrivateKey) -> Vec { - match private_key { - PrivateKey::Ed25519(key) => key.to_bytes(), - PrivateKey::Secp256k1(key) => key.to_bytes(), - } -} - -pub fn public_key_bytes(public_key: &PublicKey) -> Vec { - match public_key { - PublicKey::Ed25519(key) => key.to_vec(), - PublicKey::Secp256k1(key) => key.to_vec(), - } -} - -pub fn private_key_hex(private_key: &PrivateKey) -> String { - hex::encode(private_key_bytes(private_key)) -} - -pub fn public_key_hex(public_key: &PublicKey) -> String { - hex::encode(public_key_bytes(public_key)) -} - -pub fn public_key_hex_from_private(private_key: &PrivateKey) -> String { - public_key_hex(&private_key.public_key()) -} diff --git a/hierarchical_deterministic/src/keys/mod.rs b/hierarchical_deterministic/src/keys/mod.rs deleted file mode 100644 index 90db8841..00000000 --- a/hierarchical_deterministic/src/keys/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod key_extensions; diff --git a/hierarchical_deterministic/src/lib.rs b/hierarchical_deterministic/src/lib.rs index a6b4e6e7..0a3a6d08 100644 --- a/hierarchical_deterministic/src/lib.rs +++ b/hierarchical_deterministic/src/lib.rs @@ -3,5 +3,3 @@ pub mod bip39; pub mod bip44; pub mod cap26; pub mod derivation; -pub mod hdpath_error; -pub mod keys; diff --git a/hierarchical_deterministic/tests/slip10_vectors_test.rs b/hierarchical_deterministic/tests/slip10_vectors_test.rs index 592e9fd6..12796645 100644 --- a/hierarchical_deterministic/tests/slip10_vectors_test.rs +++ b/hierarchical_deterministic/tests/slip10_vectors_test.rs @@ -1,8 +1,6 @@ use hierarchical_deterministic::{ - bip39::mnemonic::Mnemonic, - bip44::bip44_like_path::BIP44LikePath, + bip39::mnemonic::Mnemonic, bip44::bip44_like_path::BIP44LikePath, derivation::mnemonic_with_passphrase::MnemonicWithPassphrase, - keys::key_extensions::{private_key_hex, public_key_hex_from_private}, }; #[test] @@ -15,16 +13,16 @@ fn derive_a_secp256k1_key_with_bip44_olympia() { "".to_string(), ); - let private_key: transaction::prelude::PrivateKey = + let private_key = mwp.derive_private_key(BIP44LikePath::from_str("m/44H/1022H/0H/0/5H").unwrap()); assert_eq!( "111323d507d9d690836798e3ef2e5292cfd31092b75b9b59fa584ff593a3d7e4", - private_key_hex(&private_key) + private_key.to_hex() ); assert_eq!( "03e78cdb2e0b7ea6e55e121a58560ccf841a913d3a4a9b8349e0ef00c2102f48d8", - public_key_hex_from_private(&private_key) + private_key.public_key().to_hex() ); } diff --git a/profile/Cargo.toml b/profile/Cargo.toml index e50f5dfb..bc334740 100644 --- a/profile/Cargo.toml +++ b/profile/Cargo.toml @@ -23,3 +23,4 @@ nutype = { version = "0.4.0", features = ["serde"] } transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "038ddee8b0f57aa90e36375c69946c4eb634efeb", features = [ "serde", ] } +enum-as-inner = "0.6.0" diff --git a/profile/src/v100/address/account_address.rs b/profile/src/v100/address/account_address.rs index a198d698..88a6d22e 100644 --- a/profile/src/v100/address/account_address.rs +++ b/profile/src/v100/address/account_address.rs @@ -42,6 +42,7 @@ impl Serialize for AccountAddress { impl<'de> serde::Deserialize<'de> for AccountAddress { /// Tries to deserializes a JSON string as a bech32 address into an `AccountAddress`. + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; AccountAddress::try_from_bech32(&s).map_err(de::Error::custom) @@ -88,7 +89,7 @@ impl EntityAddress for AccountAddress { } impl TryInto for &str { - type Error = wallet_kit_common::error::Error; + type Error = wallet_kit_common::error::common_error::CommonError; /// Tries to deserializes a bech32 address into an `AccountAddress`. fn try_into(self) -> Result { @@ -104,6 +105,7 @@ impl Display for AccountAddress { } impl AccountAddress { + /// A placeholder used to facilitate unit tests. pub fn placeholder() -> Self { AccountAddress::try_from_bech32( "account_rdx16xlfcpp0vf7e3gqnswv8j9k58n6rjccu58vvspmdva22kf3aplease", @@ -114,12 +116,11 @@ impl AccountAddress { #[cfg(test)] mod tests { - use std::str::FromStr; - use radix_engine_common::crypto::{Ed25519PublicKey, PublicKey}; use serde_json::json; + use std::str::FromStr; + use wallet_kit_common::error::common_error::CommonError as Error; use wallet_kit_common::{ - error::Error, json::{ assert_json_roundtrip, assert_json_value_eq_after_roundtrip, assert_json_value_ne_after_roundtrip, @@ -137,6 +138,16 @@ mod tests { .is_ok()); } + #[test] + fn from_bech32_invalid_entity_type() { + assert_eq!( + AccountAddress::try_from_bech32( + "identity_tdx_21_12tljxea3s0mse52jmpvsphr0haqs86sung8d3qlhr763nxttj59650", + ), + Err(Error::MismatchingEntityTypeWhileDecodingAddress) + ); + } + #[test] fn format() { let a = AccountAddress::try_from_bech32( diff --git a/profile/src/v100/address/decode_address_helper.rs b/profile/src/v100/address/decode_address_helper.rs index e492ac97..429074dc 100644 --- a/profile/src/v100/address/decode_address_helper.rs +++ b/profile/src/v100/address/decode_address_helper.rs @@ -1,8 +1,9 @@ use radix_engine_common::types::EntityType as EngineEntityType; use radix_engine_toolkit::functions::address::decode; -use wallet_kit_common::{error::Error, network_id::NetworkID}; +use wallet_kit_common::network_id::NetworkID; use crate::v100::entity::abstract_entity_type::AbstractEntityType; +use wallet_kit_common::error::common_error::CommonError as Error; type EngineDecodeAddressOutput = (u8, EngineEntityType, String, [u8; 30]); pub type DecodeAddressOutput = (NetworkID, AbstractEntityType, String, [u8; 30]); @@ -23,9 +24,9 @@ pub fn decode_address(s: &str) -> Result { #[cfg(test)] mod tests { - use wallet_kit_common::error::Error; use super::decode_address; + use wallet_kit_common::error::common_error::CommonError as Error; #[test] fn decode_unsupported_entity() { diff --git a/profile/src/v100/address/entity_address.rs b/profile/src/v100/address/entity_address.rs index f8bbee6c..3423751f 100644 --- a/profile/src/v100/address/entity_address.rs +++ b/profile/src/v100/address/entity_address.rs @@ -1,11 +1,16 @@ -use radix_engine_common::crypto::PublicKey; +use hierarchical_deterministic::cap26::cap26_path::paths::is_entity_path::IsEntityPath; +use radix_engine_common::crypto::PublicKey as EnginePublicKey; use radix_engine_toolkit::functions::derive::{ virtual_account_address_from_public_key, virtual_identity_address_from_public_key, }; use radix_engine_toolkit_json::models::scrypto::node_id::SerializableNodeIdInternal; -use wallet_kit_common::{error::Error, network_id::NetworkID}; +use wallet_kit_common::network_id::NetworkID; -use crate::v100::entity::abstract_entity_type::AbstractEntityType; +use crate::v100::{ + entity::abstract_entity_type::AbstractEntityType, + factors::hd_transaction_signing_factor_instance::HDFactorInstanceTransactionSigning, +}; +use wallet_kit_common::error::common_error::CommonError as Error; use super::decode_address_helper::decode_address; @@ -21,7 +26,11 @@ pub trait EntityAddress: Sized { /// Creates a new address from `public_key` and `network_id` by bech32 encoding /// it. - fn from_public_key(public_key: PublicKey, network_id: NetworkID) -> Self { + #[cfg(not(tarpaulin_include))] // false negative + fn from_public_key

(public_key: P, network_id: NetworkID) -> Self + where + P: Into + Clone, + { let component = match Self::entity_type() { AbstractEntityType::Account => virtual_account_address_from_public_key(&public_key), AbstractEntityType::Identity => virtual_identity_address_from_public_key(&public_key), @@ -37,6 +46,17 @@ pub trait EntityAddress: Sized { return Self::__with_address_and_network_id(&address, network_id); } + fn from_hd_factor_instance_virtual_entity_creation( + hd_factor_instance_virtual_entity_creation: HDFactorInstanceTransactionSigning, + ) -> Self { + Self::from_public_key( + hd_factor_instance_virtual_entity_creation + .public_key() + .public_key, + hd_factor_instance_virtual_entity_creation.path.network_id(), + ) + } + fn try_from_bech32(s: &str) -> Result { let (network_id, entity_type, hrp, _) = decode_address(s)?; if entity_type != Self::entity_type() { diff --git a/profile/src/v100/address/identity_address.rs b/profile/src/v100/address/identity_address.rs index ad6e775a..f3485343 100644 --- a/profile/src/v100/address/identity_address.rs +++ b/profile/src/v100/address/identity_address.rs @@ -59,6 +59,7 @@ impl Serialize for IdentityAddress { impl<'de> serde::Deserialize<'de> for IdentityAddress { /// Tries to deserializes a JSON string as a bech32 address into an `IdentityAddress`. + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; IdentityAddress::try_from_bech32(&s).map_err(de::Error::custom) @@ -66,7 +67,7 @@ impl<'de> serde::Deserialize<'de> for IdentityAddress { } impl TryInto for &str { - type Error = wallet_kit_common::error::Error; + type Error = wallet_kit_common::error::common_error::CommonError; /// Tries to deserializes a bech32 address into an `IdentityAddress`. fn try_into(self) -> Result { @@ -85,16 +86,17 @@ impl Display for IdentityAddress { mod tests { use std::str::FromStr; - use radix_engine_common::crypto::{Ed25519PublicKey, PublicKey}; + use radix_engine_common::crypto::{ + Ed25519PublicKey as EngineEd25519PublicKey, PublicKey as EnginePublicKey, + }; use serde_json::json; use wallet_kit_common::json::{ assert_json_roundtrip, assert_json_value_eq_after_roundtrip, assert_json_value_ne_after_roundtrip, }; - use wallet_kit_common::error::Error; - use super::*; + use wallet_kit_common::error::common_error::CommonError as Error; #[test] fn from_bech32() { @@ -118,13 +120,16 @@ mod tests { #[test] fn from_public_key_bytes_and_network_id() { - let public_key = Ed25519PublicKey::from_str( + let public_key = EngineEd25519PublicKey::from_str( "6c28952be5cdade99c7dd5d003b6b692714b6b74c5fdb5fdc9a8e4ee1d297838", ) .unwrap(); assert_eq!( - IdentityAddress::from_public_key(PublicKey::Ed25519(public_key), NetworkID::Mainnet) - .address, + IdentityAddress::from_public_key( + EnginePublicKey::Ed25519(public_key), + NetworkID::Mainnet + ) + .address, "identity_rdx12tgzjrz9u0xz4l28vf04hz87eguclmfaq4d2p8f8lv7zg9ssnzku8j" ) } diff --git a/profile/src/v100/address/non_fungible_global_id.rs b/profile/src/v100/address/non_fungible_global_id.rs index 4c71c5d3..aabeff42 100644 --- a/profile/src/v100/address/non_fungible_global_id.rs +++ b/profile/src/v100/address/non_fungible_global_id.rs @@ -11,7 +11,9 @@ use std::{ hash::{Hash, Hasher}, str::FromStr, }; -use wallet_kit_common::{error::Error, network_id::NetworkID}; + +use wallet_kit_common::error::common_error::CommonError as Error; +use wallet_kit_common::network_id::NetworkID; use super::resource_address::ResourceAddress; @@ -52,12 +54,12 @@ impl NonFungibleGlobalId { pub fn try_from_str(s: &str) -> Result { EngineSerializableNonFungibleGlobalIdInternal::from_str(s) .map(|i| Self(EngineSerializableNonFungibleGlobalId(i))) - .map_err(|_| wallet_kit_common::error::Error::InvalidNonFungibleGlobalID) + .map_err(|_| Error::InvalidNonFungibleGlobalID) } } impl TryInto for &str { - type Error = wallet_kit_common::error::Error; + type Error = wallet_kit_common::error::common_error::CommonError; /// Tries to deserializes a bech32 address into an `AccountAddress`. fn try_into(self) -> Result { diff --git a/profile/src/v100/address/resource_address.rs b/profile/src/v100/address/resource_address.rs index 0b549814..37706d99 100644 --- a/profile/src/v100/address/resource_address.rs +++ b/profile/src/v100/address/resource_address.rs @@ -16,7 +16,7 @@ pub struct ResourceAddress { } impl Serialize for ResourceAddress { - /// Serializes this `AccountAddress` into its bech32 address string as JSON. + /// Serializes this `ResourceAddress` into its bech32 address string as JSON. fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where S: Serializer, @@ -26,7 +26,8 @@ impl Serialize for ResourceAddress { } impl<'de> serde::Deserialize<'de> for ResourceAddress { - /// Tries to deserializes a JSON string as a bech32 address into an `AccountAddress`. + /// Tries to deserializes a JSON string as a bech32 address into an `ResourceAddress`. + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; ResourceAddress::try_from_bech32(&s).map_err(de::Error::custom) @@ -51,7 +52,7 @@ impl EntityAddress for ResourceAddress { } impl TryInto for &str { - type Error = wallet_kit_common::error::Error; + type Error = wallet_kit_common::error::common_error::CommonError; fn try_into(self) -> Result { ResourceAddress::try_from_bech32(self) diff --git a/profile/src/v100/entity/abstract_entity_type.rs b/profile/src/v100/entity/abstract_entity_type.rs index 84267be7..b8f7feae 100644 --- a/profile/src/v100/entity/abstract_entity_type.rs +++ b/profile/src/v100/entity/abstract_entity_type.rs @@ -2,7 +2,7 @@ use radix_engine_common::types::EntityType as EngineEntityType; use serde::{Deserialize, Serialize}; use strum::FromRepr; -use wallet_kit_common::error::Error; +use wallet_kit_common::error::common_error::CommonError as Error; /// Type of a wallet Radix Entity - Account or Identity (used by Personas). /// diff --git a/profile/src/v100/entity/account/account.rs b/profile/src/v100/entity/account/account.rs index 71b32f3e..6e2a6f10 100644 --- a/profile/src/v100/entity/account/account.rs +++ b/profile/src/v100/entity/account/account.rs @@ -1,20 +1,25 @@ -use hierarchical_deterministic::derivation::derivation_path::DerivationPath; -use radix_engine_common::crypto::PublicKey; +use hierarchical_deterministic::{ + bip32::hd_path_component::HDPathValue, + cap26::cap26_path::paths::is_entity_path::HasEntityPath, + derivation::{derivation::Derivation, mnemonic_with_passphrase::MnemonicWithPassphrase}, +}; use serde::{Deserialize, Serialize}; use std::{cell::RefCell, cmp::Ordering, fmt::Display}; -use transaction::signing::ed25519::Ed25519PrivateKey; use wallet_kit_common::network_id::NetworkID; use crate::v100::{ - address::account_address::AccountAddress, + address::{account_address::AccountAddress, entity_address::EntityAddress}, entity::{display_name::DisplayName, entity_flags::EntityFlags}, entity_security_state::{ entity_security_state::EntitySecurityState, unsecured_entity_control::UnsecuredEntityControl, }, factors::{ - factor_source_id_from_hash::FactorSourceIDFromHash, - hierarchical_deterministic_factor_instance::HierarchicalDeterministicFactorInstance, + factor_sources::{ + device_factor_source::device_factor_source::DeviceFactorSource, + private_hierarchical_deterministic_factor_source::PrivateHierarchicalDeterministicFactorSource, + }, + hd_transaction_signing_factor_instance::HDFactorInstanceAccountCreation, }, }; @@ -43,6 +48,7 @@ use super::{ #[serde(rename_all = "camelCase")] pub struct Account { /// The ID of the network this account can be used with. + #[serde(rename = "networkID")] pub network_id: NetworkID, /// A globally unique identifier of this account, being a human readable @@ -69,6 +75,7 @@ pub struct Account { /// The visual cue user learns to associated this account with, typically /// a beautiful colorful gradient. + #[serde(rename = "appearanceID")] appearance_id: RefCell, /// An order set of `EntityFlag`s used to describe certain Off-ledger @@ -76,44 +83,35 @@ pub struct Account { /// marked as hidden or not. flags: RefCell, - /// The on ledger synced settings for this account + /// The on ledger synced settings for this account, contains e.g. + /// ThirdPartyDeposit settings, with deposit rules for assets. on_ledger_settings: RefCell, } impl Account { - /// Instantiates an account with a display name, address and appearance id. - pub fn with_values( - address: AccountAddress, + pub fn new( + account_creating_factor_instance: HDFactorInstanceAccountCreation, display_name: DisplayName, appearance_id: AppearanceID, ) -> Self { + let address = AccountAddress::from_hd_factor_instance_virtual_entity_creation( + account_creating_factor_instance.clone(), + ); Self { - network_id: address.network_id, + network_id: account_creating_factor_instance.network_id(), address, display_name: RefCell::new(display_name), + security_state: UnsecuredEntityControl::with_account_creating_factor_instance( + account_creating_factor_instance, + ) + .into(), appearance_id: RefCell::new(appearance_id), flags: RefCell::new(EntityFlags::default()), on_ledger_settings: RefCell::new(OnLedgerSettings::default()), - security_state: EntitySecurityState::Unsecured(UnsecuredEntityControl::new( - 0, - HierarchicalDeterministicFactorInstance::placeholder(), - )), } } } -impl HierarchicalDeterministicFactorInstance { - pub fn placeholder() -> Self { - let private_key = Ed25519PrivateKey::from_u64(1337).unwrap(); - let public_key = private_key.public_key(); - Self::new( - FactorSourceIDFromHash::placeholder(), - PublicKey::Ed25519(public_key), - DerivationPath::placeholder(), - ) - } -} - // Getters impl Account { /// Returns this accounts `display_name` as **a clone**. @@ -165,9 +163,11 @@ impl Account { impl Ord for Account { fn cmp(&self, other: &Self) -> Ordering { match (&self.security_state, &other.security_state) { - (EntitySecurityState::Unsecured(l), EntitySecurityState::Unsecured(r)) => { - l.entity_index.cmp(&r.entity_index) - } + (EntitySecurityState::Unsecured(l), EntitySecurityState::Unsecured(r)) => l + .transaction_signing + .derivation_path() + .last_component() + .cmp(r.transaction_signing.derivation_path().last_component()), } } } @@ -184,15 +184,61 @@ impl Display for Account { } } +impl Account { + /// Instantiates an account with a display name, address and appearance id. + pub fn placeholder_with_values( + address: AccountAddress, + display_name: DisplayName, + appearance_id: AppearanceID, + ) -> Self { + Self { + network_id: address.network_id, + address, + display_name: RefCell::new(display_name), + appearance_id: RefCell::new(appearance_id), + flags: RefCell::new(EntityFlags::default()), + on_ledger_settings: RefCell::new(OnLedgerSettings::default()), + security_state: EntitySecurityState::placeholder(), + } + } + + fn placeholder_at_index_name(index: HDPathValue, name: &str) -> Self { + let mwp = MnemonicWithPassphrase::placeholder(); + let bdfs = DeviceFactorSource::babylon(true, mwp.clone(), "iPhone"); + let private_hd_factor_source = PrivateHierarchicalDeterministicFactorSource::new(mwp, bdfs); + let account_creating_factor_instance = private_hd_factor_source + .derive_account_creation_factor_instance(NetworkID::Mainnet, index); + + Self::new( + account_creating_factor_instance, + DisplayName::new(name).unwrap(), + AppearanceID::try_from(index as u8).unwrap(), + ) + } + + /// A `Mainnet` account named "Alice", a placeholder used to facilitate unit tests, with + /// derivation index 0, + pub fn placeholder_alice() -> Self { + Self::placeholder_at_index_name(0, "Alice") + } + + /// A `Mainnet` account named "Bob", a placeholder used to facilitate unit tests, with + /// derivation index 1. + pub fn placeholder_bob() -> Self { + Self::placeholder_at_index_name(1, "Bob") + } +} + // CFG test #[cfg(test)] impl Account { + /// A placeholder used to facilitate unit tests. pub fn placeholder() -> Self { Self::placeholder_mainnet() } pub fn placeholder_mainnet() -> Self { - Self::with_values( + Self::placeholder_with_values( "account_rdx16xlfcpp0vf7e3gqnswv8j9k58n6rjccu58vvspmdva22kf3aplease" .try_into() .unwrap(), @@ -202,7 +248,7 @@ impl Account { } pub fn placeholder_stokenet() -> Self { - Self::with_values( + Self::placeholder_with_values( "account_tdx_2_12ygsf87pma439ezvdyervjfq2nhqme6reau6kcxf6jtaysaxl7sqvd" .try_into() .unwrap(), @@ -212,7 +258,7 @@ impl Account { } pub fn placeholder_nebunet() -> Self { - Self::with_values( + Self::placeholder_with_values( "account_tdx_b_1p8ahenyznrqy2w0tyg00r82rwuxys6z8kmrhh37c7maqpydx7p" .try_into() .unwrap(), @@ -222,7 +268,7 @@ impl Account { } pub fn placeholder_kisharnet() -> Self { - Self::with_values( + Self::placeholder_with_values( "account_tdx_c_1px26p5tyqq65809em2h4yjczxcxj776kaun6sv3dw66sc3wrm6" .try_into() .unwrap(), @@ -232,7 +278,7 @@ impl Account { } pub fn placeholder_adapanet() -> Self { - Self::with_values( + Self::placeholder_with_values( "account_tdx_a_1qwv0unmwmxschqj8sntg6n9eejkrr6yr6fa4ekxazdzqhm6wy5" .try_into() .unwrap(), @@ -244,9 +290,9 @@ impl Account { #[cfg(test)] mod tests { - use std::{cell::RefCell, collections::BTreeSet}; + use std::collections::BTreeSet; - use hierarchical_deterministic::bip32::hd_path_component::HDPathValue; + use wallet_kit_common::json::assert_eq_after_json_roundtrip; use crate::v100::{ address::account_address::AccountAddress, @@ -267,11 +313,6 @@ mod tests { entity_flag::EntityFlag, entity_flags::EntityFlags, }, - entity_security_state::{ - entity_security_state::EntitySecurityState, - unsecured_entity_control::UnsecuredEntityControl, - }, - factors::hierarchical_deterministic_factor_instance::HierarchicalDeterministicFactorInstance, }; use super::Account; @@ -282,7 +323,7 @@ mod tests { "account_rdx16xlfcpp0vf7e3gqnswv8j9k58n6rjccu58vvspmdva22kf3aplease" .try_into() .unwrap(); - let account = Account::with_values( + let account = Account::placeholder_with_values( address.clone(), DisplayName::default(), AppearanceID::default(), @@ -308,9 +349,14 @@ mod tests { ); } + #[test] + fn compare() { + assert!(Account::placeholder_alice() < Account::placeholder_bob()); + } + #[test] fn display_name_get_set() { - let account = Account::with_values( + let account = Account::placeholder_with_values( "account_rdx16xlfcpp0vf7e3gqnswv8j9k58n6rjccu58vvspmdva22kf3aplease" .try_into() .unwrap(), @@ -325,7 +371,7 @@ mod tests { #[test] fn flags_get_set() { - let account = Account::with_values( + let account = Account::placeholder_with_values( "account_rdx16xlfcpp0vf7e3gqnswv8j9k58n6rjccu58vvspmdva22kf3aplease" .try_into() .unwrap(), @@ -340,7 +386,7 @@ mod tests { #[test] fn on_ledger_settings_get_set() { - let account = Account::with_values( + let account = Account::placeholder_with_values( "account_rdx16xlfcpp0vf7e3gqnswv8j9k58n6rjccu58vvspmdva22kf3aplease" .try_into() .unwrap(), @@ -399,43 +445,112 @@ mod tests { } #[test] - fn compare() { - let make = |index: HDPathValue| -> Account { - let address: AccountAddress = - "account_rdx16xlfcpp0vf7e3gqnswv8j9k58n6rjccu58vvspmdva22kf3aplease" - .try_into() - .unwrap(); - - let account = Account { - address: address.clone(), - network_id: address.network_id, - display_name: RefCell::new(DisplayName::new("Test").unwrap()), - appearance_id: RefCell::new(AppearanceID::default()), - flags: RefCell::new(EntityFlags::default()), - on_ledger_settings: RefCell::new(OnLedgerSettings::default()), - security_state: EntitySecurityState::Unsecured(UnsecuredEntityControl::new( - index, - HierarchicalDeterministicFactorInstance::placeholder(), - )), - }; - account - }; - let a = make(0); - let b = make(1); - assert!(a < b); + fn json_roundtrip_alice() { + let model = Account::placeholder_alice(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "securityState": { + "unsecuredEntityControl": { + "transactionSigning": { + "badge": { + "virtualSource": { + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + }, + "discriminator": "hierarchicalDeterministicPublicKey" + }, + "discriminator": "virtualSource" + }, + "factorSourceID": { + "fromHash": { + "kind": "device", + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" + }, + "discriminator": "fromHash" + } + } + }, + "discriminator": "unsecured" + }, + "networkID": 1, + "appearanceID": 0, + "flags": [], + "displayName": "Alice", + "onLedgerSettings": { + "thirdPartyDeposits": { + "depositRule": "acceptAll", + "assetsExceptionList": [], + "depositorsAllowList": [] + } + }, + "flags": [], + "address": "account_rdx12yy8n09a0w907vrjyj4hws2yptrm3rdjv84l9sr24e3w7pk7nuxst8" + } + "#, + ); } #[test] - fn json_roundtrip() { - // let model = assert_eq_after_json_roundtrip( - // &model, - // r#" - // { - // "id": "66f07ca2-a9d9-49e5-8152-77aca3d1dd74", - // "date": "2023-09-11T16:05:56", - // "description": "iPhone" - // } - // "#, - // ); + fn json_roundtrip_bob() { + let model = Account::placeholder_bob(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "securityState": { + "unsecuredEntityControl": { + "transactionSigning": { + "badge": { + "virtualSource": { + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "08740a2fd178c40ce71966a6537f780978f7f00548cfb59196344b5d7d67e9cf" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/1H" + } + }, + "discriminator": "hierarchicalDeterministicPublicKey" + }, + "discriminator": "virtualSource" + }, + "factorSourceID": { + "fromHash": { + "kind": "device", + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" + }, + "discriminator": "fromHash" + } + } + }, + "discriminator": "unsecured" + }, + "networkID": 1, + "appearanceID": 1, + "flags": [], + "displayName": "Bob", + "onLedgerSettings": { + "thirdPartyDeposits": { + "depositRule": "acceptAll", + "assetsExceptionList": [], + "depositorsAllowList": [] + } + }, + "flags": [], + "address": "account_rdx129a9wuey40lducsf6yu232zmzk5kscpvnl6fv472r0ja39f3hced69" + } + "#, + ); } } diff --git a/profile/src/v100/entity/account/appearance_id.rs b/profile/src/v100/entity/account/appearance_id.rs index f47f6974..a5ac4f6e 100644 --- a/profile/src/v100/entity/account/appearance_id.rs +++ b/profile/src/v100/entity/account/appearance_id.rs @@ -1,5 +1,7 @@ use nutype::nutype; +use wallet_kit_common::error::common_error::CommonError as Error; + #[nutype( validate(less_or_equal = 11), derive( @@ -23,12 +25,21 @@ impl Default for AppearanceID { } } +impl TryFrom for AppearanceID { + type Error = wallet_kit_common::error::common_error::CommonError; + + fn try_from(value: u8) -> Result { + AppearanceID::new(value).map_err(|_| Error::InvalidAppearanceID) + } +} + #[cfg(test)] mod tests { use serde_json::json; use wallet_kit_common::json::{assert_json_value_eq_after_roundtrip, assert_json_value_fails}; use crate::v100::entity::account::appearance_id::{AppearanceID, AppearanceIDError}; + use wallet_kit_common::error::common_error::CommonError as Error; #[test] fn lowest() { @@ -48,6 +59,12 @@ mod tests { ); } + #[test] + fn try_from() { + assert_eq!(AppearanceID::try_from(250), Err(Error::InvalidAppearanceID)); + assert_eq!(AppearanceID::try_from(1), Ok(AppearanceID::new(1).unwrap())); + } + #[test] fn json() { assert_json_value_eq_after_roundtrip(&AppearanceID::new(3).unwrap(), json!(3)); diff --git a/profile/src/v100/entity/display_name.rs b/profile/src/v100/entity/display_name.rs index 666a6c99..ae15ff8e 100644 --- a/profile/src/v100/entity/display_name.rs +++ b/profile/src/v100/entity/display_name.rs @@ -1,5 +1,7 @@ use nutype::nutype; +use wallet_kit_common::error::common_error::CommonError as Error; + #[nutype( sanitize(trim), validate(not_empty, len_char_max = 20), @@ -23,3 +25,56 @@ impl Default for DisplayName { Self::new("Unnamed").expect("Default display name") } } + +impl TryFrom<&str> for DisplayName { + type Error = wallet_kit_common::error::common_error::CommonError; + + fn try_from(value: &str) -> Result { + DisplayName::new(value.to_string()).map_err(|_| Error::InvalidDisplayName) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use wallet_kit_common::json::{ + assert_json_roundtrip, assert_json_value_eq_after_roundtrip, + assert_json_value_ne_after_roundtrip, + }; + + use super::DisplayName; + use wallet_kit_common::error::common_error::CommonError as Error; + + #[test] + fn invalid() { + assert_eq!( + DisplayName::try_from("this is a much much too long display name"), + Err(Error::InvalidDisplayName) + ); + } + + #[test] + fn valid_try_from() { + assert_eq!( + DisplayName::try_from("Main"), + Ok(DisplayName::new("Main").unwrap()) + ); + } + + #[test] + fn inner() { + assert_eq!( + DisplayName::new("Main account").unwrap().into_inner(), + "Main account" + ); + } + + #[test] + fn json_roundtrip() { + let a: DisplayName = "Cool persona".try_into().unwrap(); + + assert_json_value_eq_after_roundtrip(&a, json!("Cool persona")); + assert_json_roundtrip(&a); + assert_json_value_ne_after_roundtrip(&a, json!("Main account")); + } +} diff --git a/profile/src/v100/entity_security_state/entity_security_state.rs b/profile/src/v100/entity_security_state/entity_security_state.rs index 8b95e3a9..1f55e14a 100644 --- a/profile/src/v100/entity_security_state/entity_security_state.rs +++ b/profile/src/v100/entity_security_state/entity_security_state.rs @@ -1,8 +1,106 @@ -use serde::{Deserialize, Serialize}; +use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; use super::unsecured_entity_control::UnsecuredEntityControl; +/// Describes the state an entity - Account or Persona - is in in regards to how +/// the user controls it, i.e. if it is controlled by a single factor (private key) +/// or an `AccessController` with a potential Multi-Factor setup. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(remote = "Self")] pub enum EntitySecurityState { + /// The account is controlled by a single factor (private key) + #[serde(rename = "unsecuredEntityControl")] Unsecured(UnsecuredEntityControl), } + +impl<'de> Deserialize<'de> for EntitySecurityState { + #[cfg(not(tarpaulin_include))] // false negative + fn deserialize>(deserializer: D) -> Result { + // https://github.com/serde-rs/serde/issues/1343#issuecomment-409698470 + #[derive(Deserialize, Serialize)] + struct Wrapper { + #[serde(rename = "discriminator")] + _ignore: String, + #[serde(flatten, with = "EntitySecurityState")] + inner: EntitySecurityState, + } + Wrapper::deserialize(deserializer).map(|w| w.inner) + } +} + +impl Serialize for EntitySecurityState { + #[cfg(not(tarpaulin_include))] // false negative + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("EntitySecurityState", 2)?; + match self { + EntitySecurityState::Unsecured(control) => { + state.serialize_field("discriminator", "unsecured")?; + state.serialize_field("unsecuredEntityControl", control)?; + } + } + state.end() + } +} + +impl From for EntitySecurityState { + fn from(value: UnsecuredEntityControl) -> Self { + Self::Unsecured(value) + } +} + +impl EntitySecurityState { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::Unsecured(UnsecuredEntityControl::placeholder()) + } +} + +#[cfg(test)] +mod tests { + use wallet_kit_common::json::assert_eq_after_json_roundtrip; + + use super::EntitySecurityState; + + #[test] + fn json_roundtrip() { + let model = EntitySecurityState::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "unsecuredEntityControl": { + "transactionSigning": { + "badge": { + "virtualSource": { + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + }, + "discriminator": "hierarchicalDeterministicPublicKey" + }, + "discriminator": "virtualSource" + }, + "factorSourceID": { + "fromHash": { + "kind": "device", + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" + }, + "discriminator": "fromHash" + } + } + }, + "discriminator": "unsecured" + } + "#, + ); + } +} diff --git a/profile/src/v100/entity_security_state/unsecured_entity_control.rs b/profile/src/v100/entity_security_state/unsecured_entity_control.rs index 175e63f9..a2133bd8 100644 --- a/profile/src/v100/entity_security_state/unsecured_entity_control.rs +++ b/profile/src/v100/entity_security_state/unsecured_entity_control.rs @@ -1,7 +1,11 @@ -use hierarchical_deterministic::bip32::hd_path_component::HDPathValue; +use hierarchical_deterministic::cap26::cap26_key_kind::CAP26KeyKind; use serde::{Deserialize, Serialize}; -use crate::v100::factors::hierarchical_deterministic_factor_instance::HierarchicalDeterministicFactorInstance; +use crate::v100::factors::{ + hd_transaction_signing_factor_instance::HDFactorInstanceAccountCreation, + hierarchical_deterministic_factor_instance::HierarchicalDeterministicFactorInstance, +}; +use wallet_kit_common::error::common_error::CommonError as Error; /// Basic security control of an unsecured entity. When said entity /// is "securified" it will no longer be controlled by this `UnsecuredEntityControl` @@ -10,26 +14,113 @@ use crate::v100::factors::hierarchical_deterministic_factor_instance::Hierarchic #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct UnsecuredEntityControl { - /// The last path component of the SLIP10 derivation path. - pub entity_index: HDPathValue, - // /// The factor instance which was used to create this unsecured entity, which // /// also controls this entity and is used for signing transactions. pub transaction_signing: HierarchicalDeterministicFactorInstance, /// The factor instance which can be used for ROLA. + #[serde(skip_serializing_if = "Option::is_none")] pub authentication_signing: Option, } impl UnsecuredEntityControl { - pub fn new( - entity_index: HDPathValue, - transaction_signing: HierarchicalDeterministicFactorInstance, + pub fn with_account_creating_factor_instance( + account_creating_factor_instance: HDFactorInstanceAccountCreation, ) -> Self { Self { - entity_index, - transaction_signing, - authentication_signing: Option::None, + transaction_signing: account_creating_factor_instance.into(), + authentication_signing: None, } } + + #[cfg(not(tarpaulin_include))] // false negative + pub fn new( + transaction_signing: HierarchicalDeterministicFactorInstance, + authentication_signing: Option, + ) -> Result { + if let Some(auth) = &authentication_signing { + if let Some(key_kind) = auth.key_kind() { + if key_kind != CAP26KeyKind::AuthenticationSigning { + return Err(Error::WrongKeyKindOfAuthenticationSigningFactorInstance); + } + } + } + if let Some(key_kind) = transaction_signing.key_kind() { + if key_kind != CAP26KeyKind::TransactionSigning { + return Err(Error::WrongKeyKindOfTransactionSigningFactorInstance); + } + } + Ok(Self { + transaction_signing, + authentication_signing, + }) + } + + pub fn with_transaction_signing_only( + transaction_signing: HierarchicalDeterministicFactorInstance, + ) -> Result { + Self::new(transaction_signing, None) + } +} + +impl UnsecuredEntityControl { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::with_transaction_signing_only(HierarchicalDeterministicFactorInstance::placeholder()) + .expect("Valid placeholder") + } +} + +#[cfg(test)] +mod tests { + use wallet_kit_common::json::assert_eq_after_json_roundtrip; + + use crate::v100::factors::hierarchical_deterministic_factor_instance::HierarchicalDeterministicFactorInstance; + + use super::UnsecuredEntityControl; + + #[test] + fn with_auth_signing() { + let tx_sign = HierarchicalDeterministicFactorInstance::placeholder_transaction_signing(); + let auth_sign = HierarchicalDeterministicFactorInstance::placeholder_auth_signing(); + let control = UnsecuredEntityControl::new(tx_sign, Some(auth_sign.clone())).unwrap(); + assert_eq!(control.authentication_signing, Some(auth_sign)); + } + + #[test] + fn json_roundtrip() { + let model = UnsecuredEntityControl::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "transactionSigning": { + "badge": { + "virtualSource": { + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + }, + "discriminator": "hierarchicalDeterministicPublicKey" + }, + "discriminator": "virtualSource" + }, + "factorSourceID": { + "fromHash": { + "kind": "device", + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" + }, + "discriminator": "fromHash" + } + } + } + "#, + ); + } } diff --git a/profile/src/v100/factors/factor_instance/badge_virtual_source.rs b/profile/src/v100/factors/factor_instance/badge_virtual_source.rs new file mode 100644 index 00000000..3ab0061a --- /dev/null +++ b/profile/src/v100/factors/factor_instance/badge_virtual_source.rs @@ -0,0 +1,92 @@ +use hierarchical_deterministic::derivation::hierarchical_deterministic_public_key::HierarchicalDeterministicPublicKey; +use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(remote = "Self")] +pub enum FactorInstanceBadgeVirtualSource { + #[serde(rename = "hierarchicalDeterministicPublicKey")] + HierarchicalDeterministic(HierarchicalDeterministicPublicKey), +} + +impl FactorInstanceBadgeVirtualSource { + pub fn as_hierarchical_deterministic(&self) -> &HierarchicalDeterministicPublicKey { + match self { + FactorInstanceBadgeVirtualSource::HierarchicalDeterministic(hd) => hd, + } + } +} + +impl From for FactorInstanceBadgeVirtualSource { + fn from(value: HierarchicalDeterministicPublicKey) -> Self { + Self::HierarchicalDeterministic(value) + } +} + +impl<'de> Deserialize<'de> for FactorInstanceBadgeVirtualSource { + #[cfg(not(tarpaulin_include))] // false negative + fn deserialize>(deserializer: D) -> Result { + // https://github.com/serde-rs/serde/issues/1343#issuecomment-409698470 + #[derive(Deserialize, Serialize)] + struct Wrapper { + #[serde(rename = "discriminator")] + _ignore: String, + #[serde(flatten, with = "FactorInstanceBadgeVirtualSource")] + inner: FactorInstanceBadgeVirtualSource, + } + Wrapper::deserialize(deserializer).map(|w| w.inner) + } +} + +impl Serialize for FactorInstanceBadgeVirtualSource { + #[cfg(not(tarpaulin_include))] // false negative + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("FactorInstanceBadgeVirtualSource", 2)?; + match self { + FactorInstanceBadgeVirtualSource::HierarchicalDeterministic(hd) => { + let discriminant = "hierarchicalDeterministicPublicKey"; + state.serialize_field("discriminator", discriminant)?; + state.serialize_field(discriminant, hd)?; + } + } + state.end() + } +} + +impl FactorInstanceBadgeVirtualSource { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::HierarchicalDeterministic(HierarchicalDeterministicPublicKey::placeholder()) + } +} + +#[cfg(test)] +mod tests { + use wallet_kit_common::json::assert_eq_after_json_roundtrip; + + use super::FactorInstanceBadgeVirtualSource; + #[test] + fn json_roundtrip() { + let model = FactorInstanceBadgeVirtualSource::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + }, + "discriminator": "hierarchicalDeterministicPublicKey" + } + + "#, + ); + } +} diff --git a/profile/src/v100/factors/factor_instance/factor_instance.rs b/profile/src/v100/factors/factor_instance/factor_instance.rs new file mode 100644 index 00000000..852839c8 --- /dev/null +++ b/profile/src/v100/factors/factor_instance/factor_instance.rs @@ -0,0 +1,96 @@ +use hierarchical_deterministic::derivation::hierarchical_deterministic_public_key::HierarchicalDeterministicPublicKey; +use serde::{Deserialize, Serialize}; + +use super::{ + badge_virtual_source::FactorInstanceBadgeVirtualSource, + factor_instance_badge::FactorInstanceBadge, +}; +use crate::v100::factors::factor_source_id::FactorSourceID; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct FactorInstance { + /// The ID of the `FactorSource` that was used to produce this + /// factor instance. We will lookup the `FactorSource` in the + /// `Profile` and can present user with instruction to re-access + /// this factor source in order control the `badge`. + #[serde(rename = "factorSourceID")] + pub factor_source_id: FactorSourceID, + + /// Either a "physical" badge (NFT) or some source for recreation of a producer + /// of a virtual badge (signature), e.g. a HD derivation path, from which a private key + /// is derived which produces virtual badges (signatures). + pub badge: FactorInstanceBadge, +} + +impl FactorInstance { + pub fn new(factor_source_id: FactorSourceID, badge: FactorInstanceBadge) -> Self { + Self { + factor_source_id, + badge, + } + } + + pub fn with_hierarchical_deterministic_public_key( + factor_source_id: FactorSourceID, + hierarchical_deterministic_public_key: HierarchicalDeterministicPublicKey, + ) -> Self { + Self::new( + factor_source_id, + FactorInstanceBadge::Virtual( + FactorInstanceBadgeVirtualSource::HierarchicalDeterministic( + hierarchical_deterministic_public_key, + ), + ), + ) + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::new( + FactorSourceID::placeholder(), + FactorInstanceBadge::placeholder(), + ) + } +} + +#[cfg(test)] +mod tests { + use wallet_kit_common::json::assert_eq_after_json_roundtrip; + + use super::FactorInstance; + + #[test] + fn json_roundtrip() { + let model = FactorInstance::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "badge": { + "virtualSource": { + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + }, + "discriminator": "hierarchicalDeterministicPublicKey" + }, + "discriminator": "virtualSource" + }, + "factorSourceID": { + "fromHash": { + "kind": "device", + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" + }, + "discriminator": "fromHash" + } + } + "#, + ); + } +} diff --git a/profile/src/v100/factors/factor_instance/factor_instance_badge.rs b/profile/src/v100/factors/factor_instance/factor_instance_badge.rs new file mode 100644 index 00000000..d47c4a23 --- /dev/null +++ b/profile/src/v100/factors/factor_instance/factor_instance_badge.rs @@ -0,0 +1,128 @@ +use hierarchical_deterministic::derivation::hierarchical_deterministic_public_key::HierarchicalDeterministicPublicKey; +use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; + +use super::badge_virtual_source::FactorInstanceBadgeVirtualSource; +use enum_as_inner::EnumAsInner; + +/// Either a "physical" badge (NFT) or some source for recreation of a producer +/// of a virtual badge (signature), e.g. a HD derivation path, from which a private key +/// is derived which produces virtual badges (signatures). +#[derive(Serialize, Deserialize, EnumAsInner, Clone, Debug, PartialEq, Eq)] +#[serde(remote = "Self")] +pub enum FactorInstanceBadge { + #[serde(rename = "virtualSource")] + Virtual(FactorInstanceBadgeVirtualSource), +} + +impl FactorInstanceBadge { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + FactorInstanceBadge::Virtual(FactorInstanceBadgeVirtualSource::placeholder()) + } +} + +impl From for FactorInstanceBadge { + fn from(value: FactorInstanceBadgeVirtualSource) -> Self { + Self::Virtual(value) + } +} + +impl From for FactorInstanceBadge { + fn from(value: HierarchicalDeterministicPublicKey) -> Self { + Self::Virtual(value.into()) + } +} + +impl<'de> Deserialize<'de> for FactorInstanceBadge { + #[cfg(not(tarpaulin_include))] // false negative + fn deserialize>(deserializer: D) -> Result { + // https://github.com/serde-rs/serde/issues/1343#issuecomment-409698470 + #[derive(Deserialize, Serialize)] + struct Wrapper { + #[serde(rename = "discriminator")] + _ignore: String, + #[serde(flatten, with = "FactorInstanceBadge")] + inner: FactorInstanceBadge, + } + Wrapper::deserialize(deserializer).map(|w| w.inner) + } +} + +impl Serialize for FactorInstanceBadge { + #[cfg(not(tarpaulin_include))] // false negative + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("FactorInstanceBadge", 2)?; + match self { + FactorInstanceBadge::Virtual(virtual_source) => { + let discriminant = "virtualSource"; + state.serialize_field("discriminator", discriminant)?; + state.serialize_field(discriminant, virtual_source)?; + } + } + state.end() + } +} + +#[cfg(test)] +mod tests { + use hierarchical_deterministic::derivation::hierarchical_deterministic_public_key::HierarchicalDeterministicPublicKey; + use wallet_kit_common::json::assert_eq_after_json_roundtrip; + + use crate::v100::factors::factor_instance::badge_virtual_source::FactorInstanceBadgeVirtualSource; + + use super::FactorInstanceBadge; + #[test] + fn json_roundtrip() { + let model = FactorInstanceBadge::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "virtualSource": { + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + }, + "discriminator": "hierarchicalDeterministicPublicKey" + }, + "discriminator": "virtualSource" + } + "#, + ); + } + + #[test] + fn into_from_hd_pubkey() { + let sut: FactorInstanceBadge = HierarchicalDeterministicPublicKey::placeholder().into(); + assert_eq!( + sut, + FactorInstanceBadge::Virtual( + FactorInstanceBadgeVirtualSource::HierarchicalDeterministic( + HierarchicalDeterministicPublicKey::placeholder() + ) + ) + ) + } + + #[test] + fn into_from_virtual_source() { + let sut: FactorInstanceBadge = FactorInstanceBadgeVirtualSource::placeholder().into(); + assert_eq!( + sut, + FactorInstanceBadge::Virtual( + FactorInstanceBadgeVirtualSource::HierarchicalDeterministic( + HierarchicalDeterministicPublicKey::placeholder() + ) + ) + ) + } +} diff --git a/profile/src/v100/factors/factor_instance/mod.rs b/profile/src/v100/factors/factor_instance/mod.rs new file mode 100644 index 00000000..fcbe9df9 --- /dev/null +++ b/profile/src/v100/factors/factor_instance/mod.rs @@ -0,0 +1,4 @@ +pub mod badge_virtual_source; +pub mod factor_instance; +pub mod factor_instance_badge; +pub mod private_hierarchical_deterministic_factor_instance; diff --git a/profile/src/v100/factors/factor_instance/private_hierarchical_deterministic_factor_instance.rs b/profile/src/v100/factors/factor_instance/private_hierarchical_deterministic_factor_instance.rs new file mode 100644 index 00000000..07908c2a --- /dev/null +++ b/profile/src/v100/factors/factor_instance/private_hierarchical_deterministic_factor_instance.rs @@ -0,0 +1,98 @@ +use hierarchical_deterministic::derivation::hierarchical_deterministic_private_key::HierarchicalDeterministicPrivateKey; + +use crate::v100::factors::factor_source_id::FactorSourceID; + +use super::factor_instance::FactorInstance; + +/// An ephemeral (never persisted) HD FactorInstance which contains +/// the private key, with the ID of its creating FactorSource. +pub struct PrivateHierarchicalDeterministicFactorInstance { + /// The HD Private Key. + pub private_key: HierarchicalDeterministicPrivateKey, + /// The ID of the FactorSource creating the `PrivateKey`. + pub factor_source_id: FactorSourceID, +} + +impl From for HierarchicalDeterministicPrivateKey { + fn from(value: PrivateHierarchicalDeterministicFactorInstance) -> Self { + value.private_key + } +} + +impl From for FactorInstance { + fn from(value: PrivateHierarchicalDeterministicFactorInstance) -> Self { + FactorInstance::with_hierarchical_deterministic_public_key( + value.factor_source_id, + value.private_key.public_key(), + ) + } +} + +impl PrivateHierarchicalDeterministicFactorInstance { + /// Instantiates a new `PrivateHierarchicalDeterministicFactorInstance` from the HD PrivateKey + /// with a FactorSourceID. + pub fn new( + private_key: HierarchicalDeterministicPrivateKey, + factor_source_id: FactorSourceID, + ) -> Self { + Self { + private_key, + factor_source_id, + } + } +} + +impl PrivateHierarchicalDeterministicFactorInstance { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::new( + HierarchicalDeterministicPrivateKey::placeholder(), + FactorSourceID::placeholder(), + ) + } +} + +#[cfg(test)] +mod tests { + use hierarchical_deterministic::derivation::{ + derivation::Derivation, + hierarchical_deterministic_private_key::HierarchicalDeterministicPrivateKey, + }; + + use crate::v100::factors::factor_instance::factor_instance::FactorInstance; + + use super::PrivateHierarchicalDeterministicFactorInstance; + + #[test] + fn new() { + let sut = PrivateHierarchicalDeterministicFactorInstance::placeholder(); + assert_eq!( + sut.private_key.derivation_path.to_string(), + "m/44H/1022H/1H/525H/1460H/0H" + ); + assert_eq!( + sut.private_key.private_key.to_hex(), + "cf52dbc7bb2663223e99fb31799281b813b939440a372d0aa92eb5f5b8516003" + ); + } + + #[test] + fn into_hierarchical_deterministic_private_key() { + let sut = PrivateHierarchicalDeterministicFactorInstance::placeholder(); + let key: HierarchicalDeterministicPrivateKey = sut.into(); + assert_eq!( + key.public_key(), + HierarchicalDeterministicPrivateKey::placeholder().public_key() + ); + } + + #[test] + fn into_factor_instance() { + let sut = PrivateHierarchicalDeterministicFactorInstance::placeholder(); + let key: FactorInstance = sut.into(); + assert_eq!( + key.factor_source_id, + PrivateHierarchicalDeterministicFactorInstance::placeholder().factor_source_id + ); + } +} diff --git a/profile/src/v100/factors/factor_source.rs b/profile/src/v100/factors/factor_source.rs index d41e0670..b3c47eb8 100644 --- a/profile/src/v100/factors/factor_source.rs +++ b/profile/src/v100/factors/factor_source.rs @@ -1,15 +1,35 @@ use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; -use super::factor_sources::device_factor_source::device_factor_source::DeviceFactorSource; +use enum_as_inner::EnumAsInner; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +use super::factor_sources::{ + device_factor_source::device_factor_source::DeviceFactorSource, + ledger_hardware_wallet_factor_source::ledger_hardware_wallet_factor_source::LedgerHardwareWalletFactorSource, +}; +#[derive(Serialize, Deserialize, Clone, EnumAsInner, Debug, PartialEq, Eq)] #[serde(remote = "Self")] pub enum FactorSource { #[serde(rename = "device")] Device(DeviceFactorSource), + + #[serde(rename = "ledgerHQHardwareWallet")] + Ledger(LedgerHardwareWalletFactorSource), +} + +impl From for FactorSource { + fn from(value: DeviceFactorSource) -> Self { + FactorSource::Device(value) + } +} + +impl From for FactorSource { + fn from(value: LedgerHardwareWalletFactorSource) -> Self { + FactorSource::Ledger(value) + } } impl<'de> Deserialize<'de> for FactorSource { + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(deserializer: D) -> Result { // https://github.com/serde-rs/serde/issues/1343#issuecomment-409698470 #[derive(Deserialize, Serialize)] @@ -24,18 +44,24 @@ impl<'de> Deserialize<'de> for FactorSource { } impl Serialize for FactorSource { + #[cfg(not(tarpaulin_include))] // false negative fn serialize(&self, serializer: S) -> Result where S: Serializer, { - // 3 is the number of fields in the struct. let mut state = serializer.serialize_struct("FactorSource", 2)?; + let discriminator_key = "discriminator"; match self { FactorSource::Device(device) => { let discriminant = "device"; - state.serialize_field("discriminator", discriminant)?; + state.serialize_field(discriminator_key, discriminant)?; state.serialize_field(discriminant, device)?; } + FactorSource::Ledger(ledger) => { + let discriminant = "ledgerHQHardwareWallet"; + state.serialize_field(discriminator_key, discriminant)?; + state.serialize_field(discriminant, ledger)?; + } } state.end() } @@ -59,7 +85,7 @@ mod tests { "device": { "id": { "kind": "device", - "body": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" }, "common": { "flags": ["main"], diff --git a/profile/src/v100/factors/factor_source_common.rs b/profile/src/v100/factors/factor_source_common.rs index e017b302..664b97e2 100644 --- a/profile/src/v100/factors/factor_source_common.rs +++ b/profile/src/v100/factors/factor_source_common.rs @@ -65,6 +65,18 @@ impl FactorSourceCommon { let date: NaiveDateTime = now(); Self::with_values(crypto_parameters, date, date, flags) } + + pub fn new_bdfs(is_main: bool) -> Self { + Self::new( + FactorSourceCryptoParameters::babylon(), + if is_main { + vec![FactorSourceFlag::Main] + } else { + Vec::new() + } + .into_iter(), + ) + } } impl Default for FactorSourceCommon { @@ -74,6 +86,7 @@ impl Default for FactorSourceCommon { } impl FactorSourceCommon { + /// A placeholder used to facilitate unit tests. pub fn placeholder() -> Self { let date = NaiveDateTime::parse_from_str("2023-09-11T16:05:56", "%Y-%m-%dT%H:%M:%S").unwrap(); @@ -111,7 +124,11 @@ mod tests { fn new_uses_now_as_date() { let date0 = now(); let model = FactorSourceCommon::new(FactorSourceCryptoParameters::default(), []); - let date1 = now(); + let mut date1 = now(); + for _ in 0..10 { + // rust is too fast... lol. + date1 = now(); + } let do_test = |d: NaiveDateTime| { assert!(d > date0); assert!(d < date1); @@ -146,4 +163,20 @@ mod tests { "#, ); } + + #[test] + fn main_flag_present_if_main() { + assert!(FactorSourceCommon::new_bdfs(true) + .flags + .get_mut() + .contains(&FactorSourceFlag::Main)); + } + + #[test] + fn main_flag_not_present_if_not_main() { + assert!(FactorSourceCommon::new_bdfs(false) + .flags + .get_mut() + .is_empty()); + } } diff --git a/profile/src/v100/factors/factor_source_crypto_parameters.rs b/profile/src/v100/factors/factor_source_crypto_parameters.rs index d42f3a33..3e19292d 100644 --- a/profile/src/v100/factors/factor_source_crypto_parameters.rs +++ b/profile/src/v100/factors/factor_source_crypto_parameters.rs @@ -1,8 +1,7 @@ -use hierarchical_deterministic::derivation::{ - derivation_path_scheme::DerivationPathScheme, slip10_curve::SLIP10Curve, -}; +use hierarchical_deterministic::derivation::derivation_path_scheme::DerivationPathScheme; use serde::{Deserialize, Serialize}; -use wallet_kit_common::error::Error; +use wallet_kit_common::error::common_error::CommonError as Error; +use wallet_kit_common::types::keys::slip10_curve::SLIP10Curve; /// Cryptographic parameters a certain FactorSource supports, e.g. which Elliptic Curves /// it supports and which Hierarchical Deterministic (HD) derivations schemes it supports, @@ -73,12 +72,13 @@ impl Default for FactorSourceCryptoParameters { #[cfg(test)] mod tests { - use hierarchical_deterministic::derivation::{ - derivation_path_scheme::DerivationPathScheme, slip10_curve::SLIP10Curve, + use hierarchical_deterministic::derivation::derivation_path_scheme::DerivationPathScheme; + use wallet_kit_common::{ + json::assert_eq_after_json_roundtrip, types::keys::slip10_curve::SLIP10Curve, }; - use wallet_kit_common::{error::Error, json::assert_eq_after_json_roundtrip}; use super::FactorSourceCryptoParameters; + use wallet_kit_common::error::common_error::CommonError as Error; #[test] fn babylon_has_curve25519_as_first_curve() { diff --git a/profile/src/v100/factors/factor_source_id.rs b/profile/src/v100/factors/factor_source_id.rs index 1a3abc32..b3ff8779 100644 --- a/profile/src/v100/factors/factor_source_id.rs +++ b/profile/src/v100/factors/factor_source_id.rs @@ -1,24 +1,41 @@ -use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; - use super::{ factor_source_id_from_address::FactorSourceIDFromAddress, factor_source_id_from_hash::FactorSourceIDFromHash, }; +use enum_as_inner::EnumAsInner; +use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; /// A unique and stable identifier of a FactorSource, e.g. a /// DeviceFactorSource being a mnemonic securely stored in a /// device (phone), where the ID of it is the hash of a special /// key derived near the root of it. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Serialize, Deserialize, EnumAsInner, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[serde(remote = "Self")] pub enum FactorSourceID { + /// FactorSourceID from the blake2b hash of the special HD public key derived at `CAP26::GetID`, + /// for a certain `FactorSourceKind` #[serde(rename = "fromHash")] Hash(FactorSourceIDFromHash), + + /// FactorSourceID from an AccountAddress, typically used by `trustedContact` FactorSource. #[serde(rename = "fromAddress")] Address(FactorSourceIDFromAddress), } +impl From for FactorSourceID { + fn from(value: FactorSourceIDFromHash) -> Self { + Self::Hash(value) + } +} + +impl From for FactorSourceID { + fn from(value: FactorSourceIDFromAddress) -> Self { + Self::Address(value) + } +} + impl<'de> Deserialize<'de> for FactorSourceID { + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(deserializer: D) -> Result { // https://github.com/serde-rs/serde/issues/1343#issuecomment-409698470 #[derive(Deserialize, Serialize)] @@ -33,11 +50,11 @@ impl<'de> Deserialize<'de> for FactorSourceID { } impl Serialize for FactorSourceID { + #[cfg(not(tarpaulin_include))] // false negative fn serialize(&self, serializer: S) -> Result where S: Serializer, { - // 3 is the number of fields in the struct. let mut state = serializer.serialize_struct("FactorSourceID", 2)?; match self { FactorSourceID::Hash(from_hash) => { @@ -55,6 +72,13 @@ impl Serialize for FactorSourceID { } } +impl FactorSourceID { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + FactorSourceID::Hash(FactorSourceIDFromHash::placeholder()) + } +} + #[cfg(test)] mod tests { use wallet_kit_common::json::assert_eq_after_json_roundtrip; @@ -68,14 +92,14 @@ mod tests { #[test] fn json_roundtrip_from_hash() { - let model = FactorSourceID::Hash(FactorSourceIDFromHash::placeholder()); + let model = FactorSourceID::placeholder(); assert_eq_after_json_roundtrip( &model, r#" { "fromHash": { "kind": "device", - "body": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" }, "discriminator" : "fromHash" } @@ -99,4 +123,32 @@ mod tests { "#, ) } + + #[test] + fn hash_into_as_roundtrip() { + let from_hash = FactorSourceIDFromHash::placeholder(); + let id: FactorSourceID = from_hash.clone().into(); // test `into()` + assert_eq!(id.as_hash().unwrap(), &from_hash); + } + + #[test] + fn hash_into_as_wrong_fails() { + let from_hash = FactorSourceIDFromHash::placeholder(); + let id: FactorSourceID = from_hash.into(); // test `into()` + assert!(id.as_address().is_none()); + } + + #[test] + fn address_into_as_roundtrip() { + let from_address = FactorSourceIDFromAddress::placeholder(); + let id: FactorSourceID = from_address.clone().into(); // test `into()` + assert_eq!(id.as_address().unwrap(), &from_address); + } + + #[test] + fn address_into_as_wrong_fails() { + let from_address = FactorSourceIDFromAddress::placeholder(); + let id: FactorSourceID = from_address.into(); // test `into()` + assert!(id.as_hash().is_none()); + } } diff --git a/profile/src/v100/factors/factor_source_id_from_address.rs b/profile/src/v100/factors/factor_source_id_from_address.rs index 610fa5d0..2075d72f 100644 --- a/profile/src/v100/factors/factor_source_id_from_address.rs +++ b/profile/src/v100/factors/factor_source_id_from_address.rs @@ -4,10 +4,13 @@ use crate::v100::address::account_address::AccountAddress; use super::factor_source_kind::FactorSourceKind; -/// FactorSourceID from a hash. +/// FactorSourceID from an AccountAddress, typically used by `trustedContact` FactorSource. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct FactorSourceIDFromAddress { + /// The kind of the FactorSource this ID refers to, typically `trustedContact`. pub kind: FactorSourceKind, + + /// An account address which the FactorSource this ID refers uses/needs. pub body: AccountAddress, } @@ -19,6 +22,7 @@ impl FactorSourceIDFromAddress { } impl FactorSourceIDFromAddress { + /// A placeholder used to facilitate unit tests. pub fn placeholder() -> Self { Self::new( FactorSourceKind::TrustedContact, diff --git a/profile/src/v100/factors/factor_source_id_from_hash.rs b/profile/src/v100/factors/factor_source_id_from_hash.rs index fe43aab7..d679f7b7 100644 --- a/profile/src/v100/factors/factor_source_id_from_hash.rs +++ b/profile/src/v100/factors/factor_source_id_from_hash.rs @@ -1,7 +1,6 @@ use hierarchical_deterministic::{ cap26::cap26_path::paths::getid_path::GetIDPath, derivation::mnemonic_with_passphrase::MnemonicWithPassphrase, - keys::key_extensions::public_key_bytes, }; use radix_engine_common::crypto::{blake2b_256_hash, Hash}; use serde::{Deserialize, Serialize}; @@ -9,10 +8,14 @@ use wallet_kit_common::types::hex_32bytes::Hex32Bytes; use super::factor_source_kind::FactorSourceKind; -/// FactorSourceID from a hash. +/// FactorSourceID from the blake2b hash of the special HD public key derived at `CAP26::GetID`, +/// for a certain `FactorSourceKind` #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct FactorSourceIDFromHash { + /// The kind of the FactorSource this ID refers to, typically `device` or `ledger`. pub kind: FactorSourceKind, + + /// The blake2b hash of the special HD public key derived at `CAP26::GetID`. pub body: Hex32Bytes, } @@ -23,6 +26,7 @@ impl ToString for FactorSourceIDFromHash { } impl FactorSourceIDFromHash { + /// Instantiates a new `FactorSourceIDFromHash` from the `kind` and `body`. pub fn new(kind: FactorSourceKind, body: Hex32Bytes) -> Self { Self { kind, body } } @@ -32,7 +36,7 @@ impl FactorSourceIDFromHash { mnemonic_with_passphrase: MnemonicWithPassphrase, ) -> Self { let private_key = mnemonic_with_passphrase.derive_private_key(GetIDPath::default()); - let public_key_bytes = public_key_bytes(&private_key.public_key()); + let public_key_bytes = private_key.public_key().to_bytes(); let hash: Hash = blake2b_256_hash(public_key_bytes); let body = Hex32Bytes::from(hash); Self::new(factor_source_kind, body) @@ -44,11 +48,23 @@ impl FactorSourceIDFromHash { } impl FactorSourceIDFromHash { + /// A placeholder used to facilitate unit tests, just an alias + /// for `placeholder_device` pub fn placeholder() -> Self { - Self { - kind: FactorSourceKind::Device, - body: Hex32Bytes::placeholder(), - } + Self::placeholder_device() + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_device() -> Self { + Self::new_for_device(MnemonicWithPassphrase::placeholder()) + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_ledger() -> Self { + Self::from_mnemonic_with_passphrase( + FactorSourceKind::LedgerHQHardwareWallet, + MnemonicWithPassphrase::placeholder(), + ) } } @@ -62,7 +78,7 @@ mod tests { use super::FactorSourceIDFromHash; #[test] - fn json_roundtrip() { + fn json_roundtrip_placeholder() { let model = FactorSourceIDFromHash::placeholder(); assert_eq_after_json_roundtrip( @@ -70,7 +86,22 @@ mod tests { r#" { "kind": "device", - "body": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" + } + "#, + ); + } + + #[test] + fn json_from_placeholder_mnemonic() { + let mwp = MnemonicWithPassphrase::placeholder(); + let model = FactorSourceIDFromHash::new_for_device(mwp); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "kind": "device", + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" } "#, ); diff --git a/profile/src/v100/factors/factor_source_kind.rs b/profile/src/v100/factors/factor_source_kind.rs index 73ee7bfb..292b5670 100644 --- a/profile/src/v100/factors/factor_source_kind.rs +++ b/profile/src/v100/factors/factor_source_kind.rs @@ -75,6 +75,7 @@ impl FactorSourceKind { } impl Display for FactorSourceKind { + #[cfg(not(tarpaulin_include))] // false negative fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.discriminant()) } @@ -147,6 +148,25 @@ mod tests { format!("{}", FactorSourceKind::Device.discriminant()), "device" ); + assert_eq!( + format!( + "{}", + FactorSourceKind::LedgerHQHardwareWallet.discriminant() + ), + "ledgerHQHardwareWallet" + ); + assert_eq!( + format!("{}", FactorSourceKind::SecurityQuestions.discriminant()), + "securityQuestions" + ); + assert_eq!( + format!("{}", FactorSourceKind::OffDeviceMnemonic.discriminant()), + "offDeviceMnemonic" + ); + assert_eq!( + format!("{}", FactorSourceKind::TrustedContact.discriminant()), + "trustedContact" + ); } #[test] diff --git a/profile/src/v100/factors/factor_sources/device_factor_source/device_factor_source.rs b/profile/src/v100/factors/factor_sources/device_factor_source/device_factor_source.rs index b281a313..8020cf1c 100644 --- a/profile/src/v100/factors/factor_sources/device_factor_source/device_factor_source.rs +++ b/profile/src/v100/factors/factor_sources/device_factor_source/device_factor_source.rs @@ -1,7 +1,12 @@ +use std::cell::RefCell; + +use hierarchical_deterministic::derivation::mnemonic_with_passphrase::MnemonicWithPassphrase; use serde::{Deserialize, Serialize}; use crate::v100::factors::{ - factor_source_common::FactorSourceCommon, factor_source_id_from_hash::FactorSourceIDFromHash, + factor_source::FactorSource, factor_source_common::FactorSourceCommon, + factor_source_id::FactorSourceID, factor_source_id_from_hash::FactorSourceIDFromHash, + factor_source_kind::FactorSourceKind, is_factor_source::IsFactorSource, }; use super::device_factor_source_hint::DeviceFactorSourceHint; @@ -20,18 +25,142 @@ pub struct DeviceFactorSource { /// Common properties shared between FactorSources of different kinds, /// describing its state, when added, and supported cryptographic parameters. - pub common: FactorSourceCommon, + /// + /// Has interior mutability since we must be able to update the + /// last used date. + pub common: RefCell, /// Properties describing a DeviceFactorSource to help user disambiguate between it and another one. pub hint: DeviceFactorSourceHint, } +impl TryFrom for DeviceFactorSource { + type Error = wallet_kit_common::error::common_error::CommonError; + + fn try_from(value: FactorSource) -> Result { + value + .into_device() + .map_err(|_| Self::Error::ExpectedDeviceFactorSourceGotSomethingElse) + } +} + +impl IsFactorSource for DeviceFactorSource { + fn factor_source_kind(&self) -> FactorSourceKind { + self.id.kind + } + + fn factor_source_id(&self) -> FactorSourceID { + self.clone().id.into() + } +} + impl DeviceFactorSource { - pub fn placeholder() -> Self { + /// Instantiates a new `DeviceFactorSource` + pub fn new( + id: FactorSourceIDFromHash, + common: FactorSourceCommon, + hint: DeviceFactorSourceHint, + ) -> Self { Self { - id: FactorSourceIDFromHash::placeholder(), - common: FactorSourceCommon::placeholder(), - hint: DeviceFactorSourceHint::placeholder(), + id, + common: RefCell::new(common), + hint, } } + + pub fn babylon( + is_main: bool, + mnemonic_with_passphrase: MnemonicWithPassphrase, + device_model: &str, + ) -> Self { + let id = FactorSourceIDFromHash::from_mnemonic_with_passphrase( + FactorSourceKind::Device, + mnemonic_with_passphrase.clone(), + ); + + Self::new( + id, + FactorSourceCommon::new_bdfs(is_main), + DeviceFactorSourceHint::unknown_model_and_name_with_word_count( + mnemonic_with_passphrase.mnemonic.word_count, + device_model, + ), + ) + } +} + +impl DeviceFactorSource { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::new( + FactorSourceIDFromHash::placeholder(), + FactorSourceCommon::placeholder(), + DeviceFactorSourceHint::placeholder(), + ) + } +} + +#[cfg(test)] +mod tests { + use wallet_kit_common::json::assert_eq_after_json_roundtrip; + + use crate::v100::factors::{ + factor_source_id::FactorSourceID, is_factor_source::IsFactorSource, factor_source::FactorSource, factor_sources::ledger_hardware_wallet_factor_source::ledger_hardware_wallet_factor_source::LedgerHardwareWalletFactorSource, + }; +use wallet_kit_common::error::common_error::CommonError as Error; + use super::DeviceFactorSource; + + #[test] + fn json() { + let model = DeviceFactorSource::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "common": { + "addedOn": "2023-09-11T16:05:56", + "cryptoParameters": { + "supportedCurves": ["curve25519"], + "supportedDerivationPathSchemes": ["cap26"] + }, + "flags": ["main"], + "lastUsedOn": "2023-09-11T16:05:56" + }, + "hint": { + "mnemonicWordCount": 24, + "model": "iPhone", + "name": "Unknown Name" + }, + "id": { + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240", + "kind": "device" + } + } + "#, + ); + } + + #[test] + fn factor_source_id() { + let sut = DeviceFactorSource::placeholder(); + let factor_source_id: FactorSourceID = sut.clone().id.into(); + assert_eq!(factor_source_id, sut.factor_source_id()); + } + + #[test] + fn from_factor_source() { + let sut = DeviceFactorSource::placeholder(); + let factor_source: FactorSource = sut.clone().into(); + assert_eq!(DeviceFactorSource::try_from(factor_source), Ok(sut)); + } + + #[test] + fn from_factor_source_invalid_got_ledger() { + let ledger = LedgerHardwareWalletFactorSource::placeholder(); + let factor_source: FactorSource = ledger.clone().into(); + assert_eq!( + DeviceFactorSource::try_from(factor_source), + Err(Error::ExpectedDeviceFactorSourceGotSomethingElse) + ); + } } diff --git a/profile/src/v100/factors/factor_sources/device_factor_source/device_factor_source_hint.rs b/profile/src/v100/factors/factor_sources/device_factor_source/device_factor_source_hint.rs index 4b5de737..0790ddc7 100644 --- a/profile/src/v100/factors/factor_sources/device_factor_source/device_factor_source_hint.rs +++ b/profile/src/v100/factors/factor_sources/device_factor_source/device_factor_source_hint.rs @@ -29,24 +29,29 @@ impl DeviceFactorSourceHint { } } - pub fn iphone_unknown_model_and_name_with_word_count(word_count: BIP39WordCount) -> Self { - Self::new("Unknown Name".to_string(), "iPhone".to_string(), word_count) + pub fn unknown_model_and_name_with_word_count(word_count: BIP39WordCount, model: &str) -> Self { + Self::new("Unknown Name".to_string(), model.to_string(), word_count) } - - pub fn iphone_unknown() -> Self { - Self::iphone_unknown_model_and_name_with_word_count(BIP39WordCount::TwentyFour) + pub fn iphone_unknown_model_and_name_with_word_count(word_count: BIP39WordCount) -> Self { + Self::unknown_model_and_name_with_word_count(word_count, "iPhone") } } impl Default for DeviceFactorSourceHint { fn default() -> Self { - Self::iphone_unknown() + Self::placeholder_iphone_unknown() } } impl DeviceFactorSourceHint { + /// A placeholder used to facilitate unit tests. pub fn placeholder() -> Self { - Self::iphone_unknown() + Self::placeholder_iphone_unknown() + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_iphone_unknown() -> Self { + Self::iphone_unknown_model_and_name_with_word_count(BIP39WordCount::TwentyFour) } } @@ -60,7 +65,7 @@ mod tests { fn default_is_iphone_unknown() { assert_eq!( DeviceFactorSourceHint::default(), - DeviceFactorSourceHint::iphone_unknown() + DeviceFactorSourceHint::placeholder_iphone_unknown() ); } diff --git a/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/ledger_hardware_wallet_factor_source.rs b/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/ledger_hardware_wallet_factor_source.rs new file mode 100644 index 00000000..a2a8218b --- /dev/null +++ b/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/ledger_hardware_wallet_factor_source.rs @@ -0,0 +1,155 @@ +use std::cell::RefCell; + +use serde::{Deserialize, Serialize}; + +use crate::v100::factors::{ + factor_source::FactorSource, factor_source_common::FactorSourceCommon, + factor_source_id::FactorSourceID, factor_source_id_from_hash::FactorSourceIDFromHash, + factor_source_kind::FactorSourceKind, is_factor_source::IsFactorSource, +}; + +use super::ledger_hardware_wallet_hint::LedgerHardwareWalletHint; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct LedgerHardwareWalletFactorSource { + /// Unique and stable identifier of this factor source, stemming from the + /// hash of a special child key of the HD root of the mnemonic, + /// that is secured by the Ledger Hardware Wallet device. + pub id: FactorSourceIDFromHash, + + /// Common properties shared between FactorSources of different kinds, + /// describing its state, when added, and supported cryptographic parameters. + /// + /// Has interior mutability since we must be able to update the + /// last used date. + pub common: RefCell, + + /// Properties describing a LedgerHardwareWalletFactorSource to help user disambiguate between it and another one. + pub hint: LedgerHardwareWalletHint, +} + +impl LedgerHardwareWalletFactorSource { + /// Instantiates a new `LedgerHardwareWalletFactorSource` + pub fn new( + id: FactorSourceIDFromHash, + common: FactorSourceCommon, + hint: LedgerHardwareWalletHint, + ) -> Self { + Self { + id, + common: RefCell::new(common), + hint, + } + } +} + +impl LedgerHardwareWalletFactorSource { + pub fn placeholder() -> Self { + Self::new( + FactorSourceIDFromHash::placeholder_ledger(), + FactorSourceCommon::placeholder(), + LedgerHardwareWalletHint::placeholder(), + ) + } +} + +impl TryFrom for LedgerHardwareWalletFactorSource { + type Error = wallet_kit_common::error::common_error::CommonError; + + fn try_from(value: FactorSource) -> Result { + value + .into_ledger() + .map_err(|_| Self::Error::ExpectedLedgerHardwareWalletFactorSourceGotSomethingElse) + } +} + +impl IsFactorSource for LedgerHardwareWalletFactorSource { + fn factor_source_kind(&self) -> FactorSourceKind { + self.id.kind + } + + fn factor_source_id(&self) -> FactorSourceID { + self.clone().id.into() + } +} + +#[cfg(test)] +mod tests { + use wallet_kit_common::{ + error::common_error::CommonError as Error, json::assert_eq_after_json_roundtrip, + }; + + use crate::v100::factors::{ + factor_source::FactorSource, + factor_sources::device_factor_source::device_factor_source::DeviceFactorSource, + is_factor_source::IsFactorSource, + }; + + use super::LedgerHardwareWalletFactorSource; + + #[test] + fn json_roundtrip() { + let model: LedgerHardwareWalletFactorSource = + LedgerHardwareWalletFactorSource::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "id": { + "kind": "ledgerHQHardwareWallet", + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" + }, + "common": { + "addedOn": "2023-09-11T16:05:56", + "cryptoParameters": { + "supportedCurves": ["curve25519"], + "supportedDerivationPathSchemes": ["cap26"] + }, + "flags": ["main"], + "lastUsedOn": "2023-09-11T16:05:56" + }, + "hint": { + "name": "Orange, scratched", + "model": "nanoS+" + } + }"#, + ); + } + + #[test] + fn from_factor_source() { + let sut = LedgerHardwareWalletFactorSource::placeholder(); + let factor_source: FactorSource = sut.clone().into(); + assert_eq!( + LedgerHardwareWalletFactorSource::try_from(factor_source), + Ok(sut) + ); + } + + #[test] + fn from_factor_source_invalid_got_device() { + let wrong = DeviceFactorSource::placeholder(); + let factor_source: FactorSource = wrong.clone().into(); + assert_eq!( + LedgerHardwareWalletFactorSource::try_from(factor_source), + Err(Error::ExpectedLedgerHardwareWalletFactorSourceGotSomethingElse) + ); + } + + #[test] + fn factor_source_id() { + assert_eq!( + LedgerHardwareWalletFactorSource::placeholder().factor_source_id(), + LedgerHardwareWalletFactorSource::placeholder().id.into() + ); + } + + #[test] + fn factor_source_kind() { + assert_eq!( + LedgerHardwareWalletFactorSource::placeholder().factor_source_kind(), + LedgerHardwareWalletFactorSource::placeholder().id.kind + ); + } +} diff --git a/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/ledger_hardware_wallet_hint.rs b/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/ledger_hardware_wallet_hint.rs new file mode 100644 index 00000000..b3f2cf05 --- /dev/null +++ b/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/ledger_hardware_wallet_hint.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; + +use super::ledger_hardware_wallet_model::LedgerHardwareWalletModel; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct LedgerHardwareWalletHint { + /// "Orange, scratched" + pub name: String, + + /// E.g. `nanoS+` + pub model: LedgerHardwareWalletModel, +} + +impl LedgerHardwareWalletHint { + pub fn new(name: &str, model: LedgerHardwareWalletModel) -> Self { + Self { + name: name.to_string(), + model, + } + } +} + +impl LedgerHardwareWalletHint { + pub fn placeholder() -> Self { + Self::new("Orange, scratched", LedgerHardwareWalletModel::NanoSPlus) + } +} + +#[cfg(test)] +mod tests { + use wallet_kit_common::json::assert_eq_after_json_roundtrip; + + use super::LedgerHardwareWalletHint; + #[test] + fn json_roundtrip() { + let model = LedgerHardwareWalletHint::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "name": "Orange, scratched", + "model": "nanoS+" + } + "#, + ); + } +} diff --git a/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/ledger_hardware_wallet_model.rs b/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/ledger_hardware_wallet_model.rs new file mode 100644 index 00000000..02661344 --- /dev/null +++ b/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/ledger_hardware_wallet_model.rs @@ -0,0 +1,72 @@ +use serde::{Deserialize, Serialize}; + +/// The model of a Ledger HQ hardware wallet NanoS, e.g. +/// *Ledger Nano S+*. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[serde(rename_all = "camelCase")] +pub enum LedgerHardwareWalletModel { + NanoS, + + #[serde(rename = "nanoS+")] + NanoSPlus, + NanoX, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use serde_json::json; + use wallet_kit_common::json::assert_json_value_eq_after_roundtrip; + + use crate::v100::factors::factor_sources::ledger_hardware_wallet_factor_source::ledger_hardware_wallet_model::LedgerHardwareWalletModel; + + #[test] + fn equality() { + assert_eq!( + LedgerHardwareWalletModel::NanoS, + LedgerHardwareWalletModel::NanoS + ); + assert_eq!( + LedgerHardwareWalletModel::NanoX, + LedgerHardwareWalletModel::NanoX + ); + } + #[test] + fn inequality() { + assert_ne!( + LedgerHardwareWalletModel::NanoS, + LedgerHardwareWalletModel::NanoX + ); + } + + #[test] + fn hash() { + assert_eq!( + BTreeSet::from_iter( + [ + LedgerHardwareWalletModel::NanoS, + LedgerHardwareWalletModel::NanoS + ] + .into_iter() + ) + .len(), + 1 + ); + } + + #[test] + fn ord() { + assert!(LedgerHardwareWalletModel::NanoS < LedgerHardwareWalletModel::NanoX); + } + + #[test] + fn json_roundtrip() { + assert_json_value_eq_after_roundtrip(&LedgerHardwareWalletModel::NanoS, json!("nanoS")); + assert_json_value_eq_after_roundtrip( + &LedgerHardwareWalletModel::NanoSPlus, + json!("nanoS+"), + ); + assert_json_value_eq_after_roundtrip(&LedgerHardwareWalletModel::NanoX, json!("nanoX")); + } +} diff --git a/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/mod.rs b/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/mod.rs new file mode 100644 index 00000000..3c155b24 --- /dev/null +++ b/profile/src/v100/factors/factor_sources/ledger_hardware_wallet_factor_source/mod.rs @@ -0,0 +1,3 @@ +pub mod ledger_hardware_wallet_factor_source; +pub mod ledger_hardware_wallet_hint; +pub mod ledger_hardware_wallet_model; diff --git a/profile/src/v100/factors/factor_sources/mod.rs b/profile/src/v100/factors/factor_sources/mod.rs index 8ca920a2..689964e8 100644 --- a/profile/src/v100/factors/factor_sources/mod.rs +++ b/profile/src/v100/factors/factor_sources/mod.rs @@ -1 +1,3 @@ pub mod device_factor_source; +pub mod ledger_hardware_wallet_factor_source; +pub mod private_hierarchical_deterministic_factor_source; diff --git a/profile/src/v100/factors/factor_sources/private_hierarchical_deterministic_factor_source.rs b/profile/src/v100/factors/factor_sources/private_hierarchical_deterministic_factor_source.rs new file mode 100644 index 00000000..c0581257 --- /dev/null +++ b/profile/src/v100/factors/factor_sources/private_hierarchical_deterministic_factor_source.rs @@ -0,0 +1,59 @@ +use hierarchical_deterministic::{ + bip32::hd_path_component::HDPathValue, + cap26::{ + cap26_key_kind::CAP26KeyKind, cap26_path::paths::account_path::AccountPath, + cap26_repr::CAP26Repr, + }, + derivation::mnemonic_with_passphrase::MnemonicWithPassphrase, +}; +use wallet_kit_common::network_id::NetworkID; + +use crate::v100::factors::{ + factor_source_id_from_hash::FactorSourceIDFromHash, + hd_transaction_signing_factor_instance::HDFactorInstanceAccountCreation, + hierarchical_deterministic_factor_instance::HierarchicalDeterministicFactorInstance, + is_factor_source::IsFactorSource, +}; + +use super::device_factor_source::device_factor_source::DeviceFactorSource; + +pub struct PrivateHierarchicalDeterministicFactorSource { + pub mnemonic_with_passphrase: MnemonicWithPassphrase, + pub factor_source: DeviceFactorSource, +} + +impl PrivateHierarchicalDeterministicFactorSource { + pub fn new( + mnemonic_with_passphrase: MnemonicWithPassphrase, + factor_source: DeviceFactorSource, + ) -> Self { + assert_eq!( + factor_source.id, + FactorSourceIDFromHash::from_mnemonic_with_passphrase( + factor_source.factor_source_kind(), + mnemonic_with_passphrase.clone() + ) + .into() + ); + Self { + mnemonic_with_passphrase, + factor_source, + } + } +} + +impl PrivateHierarchicalDeterministicFactorSource { + pub fn derive_account_creation_factor_instance( + &self, + network_id: NetworkID, + index: HDPathValue, + ) -> HDFactorInstanceAccountCreation { + let path = AccountPath::new(network_id, CAP26KeyKind::TransactionSigning, index); + let hd_private_key = self.mnemonic_with_passphrase.derive_private_key(path); + let hd_factor_instance = HierarchicalDeterministicFactorInstance::new( + self.factor_source.id.clone(), + hd_private_key.public_key(), + ); + HDFactorInstanceAccountCreation::new(hd_factor_instance).unwrap() + } +} diff --git a/profile/src/v100/factors/hd_transaction_signing_factor_instance.rs b/profile/src/v100/factors/hd_transaction_signing_factor_instance.rs new file mode 100644 index 00000000..c0492fe6 --- /dev/null +++ b/profile/src/v100/factors/hd_transaction_signing_factor_instance.rs @@ -0,0 +1,224 @@ +use super::{ + factor_source_id_from_hash::FactorSourceIDFromHash, + hierarchical_deterministic_factor_instance::HierarchicalDeterministicFactorInstance, +}; +use hierarchical_deterministic::{ + cap26::cap26_path::{ + cap26_path::CAP26Path, + paths::{ + account_path::AccountPath, + identity_path::IdentityPath, + is_entity_path::{HasEntityPath, IsEntityPath}, + }, + }, + derivation::hierarchical_deterministic_public_key::HierarchicalDeterministicPublicKey, +}; +use wallet_kit_common::{ + error::common_error::CommonError as Error, types::keys::public_key::PublicKey, +}; + +/// A specialized Hierarchical Deterministic FactorInstance used for transaction signing +/// and creation of virtual Accounts and Identities (Personas). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HDFactorInstanceTransactionSigning { + pub factor_source_id: FactorSourceIDFromHash, + public_key: PublicKey, + pub path: E, +} +impl HDFactorInstanceTransactionSigning { + pub fn try_from( + hd_factor_instance: HierarchicalDeterministicFactorInstance, + extract: F, + ) -> Result + where + F: Fn(&CAP26Path) -> Option<&E>, + { + if let Some(path) = hd_factor_instance + .derivation_path() + .as_cap26() + .and_then(|p| extract(p)) + { + if !path.key_kind().is_transaction_signing() { + return Err(Error::WrongKeyKindOfTransactionSigningFactorInstance); + } + + Ok(Self { + factor_source_id: hd_factor_instance.factor_source_id, + public_key: hd_factor_instance.public_key.public_key, + path: path.clone(), + }) + } else { + return Err(Error::WrongEntityKindOfInFactorInstancesPath); + } + } +} + +impl HasEntityPath for HDFactorInstanceTransactionSigning { + fn path(&self) -> E { + self.path.clone() + } +} + +impl HDFactorInstanceTransactionSigning { + pub fn public_key(&self) -> HierarchicalDeterministicPublicKey { + HierarchicalDeterministicPublicKey::new(self.public_key, self.path.derivation_path()) + } +} + +/// Just an alias for when `HDFactorInstanceTransactionSigning` is used to create a new Account. +pub type HDFactorInstanceAccountCreation = HDFactorInstanceTransactionSigning; + +/// Just an alias for when `HDFactorInstanceTransactionSigning` is used to create a new Account. +pub type HDFactorInstanceIdentityCreation = HDFactorInstanceTransactionSigning; + +impl HDFactorInstanceAccountCreation { + pub fn new(hd_factor_instance: HierarchicalDeterministicFactorInstance) -> Result { + Self::try_from(hd_factor_instance, |p| p.as_account_path()) + } +} + +impl From> + for HierarchicalDeterministicFactorInstance +{ + fn from(value: HDFactorInstanceTransactionSigning) -> Self { + HierarchicalDeterministicFactorInstance::new( + value.clone().factor_source_id, + value.public_key(), + ) + } +} + +impl HDFactorInstanceIdentityCreation { + pub fn new(hd_factor_instance: HierarchicalDeterministicFactorInstance) -> Result { + Self::try_from(hd_factor_instance, |p| p.as_identity_path()) + } +} + +#[cfg(test)] +mod tests { + use hierarchical_deterministic::{ + cap26::{ + cap26_key_kind::CAP26KeyKind, + cap26_path::paths::{ + account_path::AccountPath, identity_path::IdentityPath, + is_entity_path::IsEntityPath, + }, + cap26_repr::CAP26Repr, + }, + derivation::hierarchical_deterministic_public_key::HierarchicalDeterministicPublicKey, + }; + use wallet_kit_common::{ + error::common_error::CommonError as Error, network_id::NetworkID, + types::keys::public_key::PublicKey, + }; + + use crate::v100::factors::{ + factor_source_id_from_hash::FactorSourceIDFromHash, + hd_transaction_signing_factor_instance::{ + HDFactorInstanceAccountCreation, HDFactorInstanceIdentityCreation, + }, + hierarchical_deterministic_factor_instance::HierarchicalDeterministicFactorInstance, + }; + + #[test] + fn account_creation_valid() { + let hd_key = HierarchicalDeterministicPublicKey::new( + PublicKey::placeholder_ed25519(), + AccountPath::placeholder().into(), + ); + let hd_fi = HierarchicalDeterministicFactorInstance::new( + FactorSourceIDFromHash::placeholder(), + hd_key, + ); + assert_eq!( + HDFactorInstanceAccountCreation::new(hd_fi) + .unwrap() + .path + .key_kind(), + CAP26KeyKind::TransactionSigning + ); + } + + #[test] + fn account_creation_wrong_entity_kind() { + let hd_key = HierarchicalDeterministicPublicKey::new( + PublicKey::placeholder_ed25519(), + IdentityPath::placeholder().into(), + ); + let hd_fi = HierarchicalDeterministicFactorInstance::new( + FactorSourceIDFromHash::placeholder(), + hd_key, + ); + assert_eq!( + HDFactorInstanceAccountCreation::new(hd_fi), + Err(Error::WrongEntityKindOfInFactorInstancesPath) + ); + } + + #[test] + fn account_creation_wrong_key_kind() { + let hd_key = HierarchicalDeterministicPublicKey::new( + PublicKey::placeholder_ed25519(), + AccountPath::new(NetworkID::Mainnet, CAP26KeyKind::AuthenticationSigning, 0).into(), + ); + let hd_fi = HierarchicalDeterministicFactorInstance::new( + FactorSourceIDFromHash::placeholder(), + hd_key, + ); + assert_eq!( + HDFactorInstanceAccountCreation::new(hd_fi), + Err(Error::WrongKeyKindOfTransactionSigningFactorInstance) + ); + } + + #[test] + fn identity_creation_valid() { + let hd_key = HierarchicalDeterministicPublicKey::new( + PublicKey::placeholder_ed25519(), + IdentityPath::placeholder().into(), + ); + let hd_fi = HierarchicalDeterministicFactorInstance::new( + FactorSourceIDFromHash::placeholder(), + hd_key, + ); + assert_eq!( + HDFactorInstanceIdentityCreation::new(hd_fi) + .unwrap() + .path + .key_kind(), + CAP26KeyKind::TransactionSigning + ); + } + + #[test] + fn identity_creation_wrong_entity_kind() { + let hd_key = HierarchicalDeterministicPublicKey::new( + PublicKey::placeholder_ed25519(), + AccountPath::placeholder().into(), + ); + let hd_fi = HierarchicalDeterministicFactorInstance::new( + FactorSourceIDFromHash::placeholder(), + hd_key, + ); + assert_eq!( + HDFactorInstanceIdentityCreation::new(hd_fi), + Err(Error::WrongEntityKindOfInFactorInstancesPath) + ); + } + + #[test] + fn identity_creation_wrong_key_kind() { + let hd_key = HierarchicalDeterministicPublicKey::new( + PublicKey::placeholder_ed25519(), + IdentityPath::new(NetworkID::Mainnet, CAP26KeyKind::AuthenticationSigning, 0).into(), + ); + let hd_fi = HierarchicalDeterministicFactorInstance::new( + FactorSourceIDFromHash::placeholder(), + hd_key, + ); + assert_eq!( + HDFactorInstanceIdentityCreation::new(hd_fi), + Err(Error::WrongKeyKindOfTransactionSigningFactorInstance) + ); + } +} diff --git a/profile/src/v100/factors/hierarchical_deterministic_factor_instance.rs b/profile/src/v100/factors/hierarchical_deterministic_factor_instance.rs index 06908df5..53e5a020 100644 --- a/profile/src/v100/factors/hierarchical_deterministic_factor_instance.rs +++ b/profile/src/v100/factors/hierarchical_deterministic_factor_instance.rs @@ -1,27 +1,267 @@ -use hierarchical_deterministic::derivation::derivation_path::DerivationPath; -use radix_engine_common::crypto::PublicKey; -use serde::{Deserialize, Serialize}; +use hierarchical_deterministic::{ + bip32::hd_path_component::HDPathValue, + cap26::{ + cap26_key_kind::CAP26KeyKind, + cap26_path::{ + cap26_path::CAP26Path, + paths::{account_path::AccountPath, is_entity_path::IsEntityPath}, + }, + cap26_repr::CAP26Repr, + }, + derivation::{ + derivation::Derivation, derivation_path::DerivationPath, + hierarchical_deterministic_public_key::HierarchicalDeterministicPublicKey, + mnemonic_with_passphrase::MnemonicWithPassphrase, + }, +}; +use serde::{de, Deserializer, Serialize, Serializer}; +use wallet_kit_common::{network_id::NetworkID, types::keys::public_key::PublicKey}; -use super::factor_source_id_from_hash::FactorSourceIDFromHash; +use crate::v100::factors::factor_source_kind::FactorSourceKind; +use wallet_kit_common::error::common_error::CommonError as Error; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] +use super::{ + factor_instance::{ + factor_instance::FactorInstance, factor_instance_badge::FactorInstanceBadge, + }, + factor_source_id::FactorSourceID, + factor_source_id_from_hash::FactorSourceIDFromHash, +}; + +/// A virtual hierarchical deterministic `FactorInstance` +#[derive(Clone, Debug, PartialEq, Eq)] pub struct HierarchicalDeterministicFactorInstance { pub factor_source_id: FactorSourceIDFromHash, - pub public_key: PublicKey, - pub derivation_path: DerivationPath, + pub public_key: HierarchicalDeterministicPublicKey, } impl HierarchicalDeterministicFactorInstance { + pub fn derivation_path(&self) -> DerivationPath { + self.public_key.derivation_path.clone() + } + pub fn new( + factor_source_id: FactorSourceIDFromHash, + public_key: HierarchicalDeterministicPublicKey, + ) -> Self { + Self { + factor_source_id, + public_key, + } + } + + pub fn with_key_and_path( factor_source_id: FactorSourceIDFromHash, public_key: PublicKey, derivation_path: DerivationPath, ) -> Self { - Self { + Self::new( factor_source_id, + HierarchicalDeterministicPublicKey::new(public_key, derivation_path), + ) + } + + pub fn try_from( + factor_source_id: FactorSourceID, + public_key: PublicKey, + derivation_path: DerivationPath, + ) -> Result { + let factor_source_id = factor_source_id + .as_hash() + .ok_or(Error::FactorSourceIDNotFromHash)?; + Ok(Self::with_key_and_path( + factor_source_id.clone(), public_key, derivation_path, + )) + } + + pub fn try_from_factor_instance(factor_instance: FactorInstance) -> Result { + let virtual_source = factor_instance + .badge + .as_virtual() + .ok_or(Error::BadgeIsNotVirtualHierarchicalDeterministic)?; + + let badge = virtual_source.as_hierarchical_deterministic(); + + Self::try_from( + factor_instance.factor_source_id, + badge.public_key, + badge.derivation_path.clone(), + ) + } + + pub fn factor_instance(&self) -> FactorInstance { + FactorInstance::new( + self.factor_source_id.clone().into(), + FactorInstanceBadge::Virtual(self.public_key.clone().into()), + ) + } + + pub fn key_kind(&self) -> Option { + match &self.derivation_path() { + DerivationPath::CAP26(cap26) => match cap26 { + CAP26Path::GetID(_) => None, + CAP26Path::IdentityPath(identity_path) => Some(identity_path.key_kind()), + CAP26Path::AccountPath(account_path) => Some(account_path.key_kind()), + }, + DerivationPath::BIP44Like(_) => None, } } } + +impl Serialize for HierarchicalDeterministicFactorInstance { + #[cfg(not(tarpaulin_include))] // false negative + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + self.factor_instance().serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for HierarchicalDeterministicFactorInstance { + #[cfg(not(tarpaulin_include))] // false negative + fn deserialize>( + d: D, + ) -> Result { + FactorInstance::deserialize(d).and_then(|fi| { + HierarchicalDeterministicFactorInstance::try_from_factor_instance(fi) + .map_err(de::Error::custom) + }) + } +} + +impl HierarchicalDeterministicFactorInstance { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::placeholder_transaction_signing() + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_transaction_signing() -> Self { + Self::placeholder_with_key_kind(CAP26KeyKind::TransactionSigning, 0) + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_auth_signing() -> Self { + Self::placeholder_with_key_kind(CAP26KeyKind::AuthenticationSigning, 0) + } + + /// A placeholder used to facilitate unit tests. + fn placeholder_with_key_kind(key_kind: CAP26KeyKind, index: HDPathValue) -> Self { + let mwp = MnemonicWithPassphrase::placeholder(); + let path = AccountPath::new(NetworkID::Mainnet, key_kind, index); + let private_key = mwp.derive_private_key(path.clone()); + let public_key = private_key.public_key(); + let id = + FactorSourceIDFromHash::from_mnemonic_with_passphrase(FactorSourceKind::Device, mwp); + Self::new(id.into(), public_key) + } +} + +#[cfg(test)] +mod tests { + use hierarchical_deterministic::{ + bip44::bip44_like_path::BIP44LikePath, + cap26::{ + cap26_key_kind::CAP26KeyKind, + cap26_path::paths::{getid_path::GetIDPath, identity_path::IdentityPath}, + }, + derivation::{ + derivation::Derivation, derivation_path::DerivationPath, + hierarchical_deterministic_public_key::HierarchicalDeterministicPublicKey, + }, + }; + use wallet_kit_common::{ + json::assert_eq_after_json_roundtrip, types::keys::public_key::PublicKey, + }; + + use crate::v100::factors::factor_source_id_from_hash::FactorSourceIDFromHash; + + use super::HierarchicalDeterministicFactorInstance; + + #[test] + fn json_roundtrip() { + let model = HierarchicalDeterministicFactorInstance::placeholder(); + assert_eq_after_json_roundtrip( + &model, + r#" + { + "badge": { + "virtualSource": { + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "d24cc6af91c3f103d7f46e5691ce2af9fea7d90cfb89a89d5bba4b513b34be3b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0H" + } + }, + "discriminator": "hierarchicalDeterministicPublicKey" + }, + "discriminator": "virtualSource" + }, + "factorSourceID": { + "fromHash": { + "kind": "device", + "body": "3c986ebf9dcd9167a97036d3b2c997433e85e6cc4e4422ad89269dac7bfea240" + }, + "discriminator": "fromHash" + } + } + "#, + ); + } + + #[test] + fn key_kind_bip44_is_none() { + let derivation_path: DerivationPath = BIP44LikePath::placeholder().into(); + let sut = HierarchicalDeterministicFactorInstance::new( + FactorSourceIDFromHash::placeholder(), + HierarchicalDeterministicPublicKey::new( + PublicKey::placeholder_ed25519(), + derivation_path, + ), + ); + assert_eq!(sut.key_kind(), None); + } + + #[test] + fn key_kind_identity() { + let derivation_path: DerivationPath = IdentityPath::placeholder().into(); + let sut = HierarchicalDeterministicFactorInstance::new( + FactorSourceIDFromHash::placeholder(), + HierarchicalDeterministicPublicKey::new( + PublicKey::placeholder_ed25519(), + derivation_path, + ), + ); + assert_eq!(sut.key_kind(), Some(CAP26KeyKind::TransactionSigning)); + } + + #[test] + fn key_kind_cap26_getid_is_none() { + let derivation_path: DerivationPath = GetIDPath::default().into(); + let sut = HierarchicalDeterministicFactorInstance::new( + FactorSourceIDFromHash::placeholder(), + HierarchicalDeterministicPublicKey::new( + PublicKey::placeholder_ed25519(), + derivation_path, + ), + ); + assert_eq!(sut.key_kind(), None); + } + + #[test] + fn placeholder_auth() { + assert_eq!( + HierarchicalDeterministicFactorInstance::placeholder_auth_signing() + .derivation_path() + .to_string(), + "m/44H/1022H/1H/525H/1678H/0H" + ); + } +} diff --git a/profile/src/v100/factors/is_factor_source.rs b/profile/src/v100/factors/is_factor_source.rs new file mode 100644 index 00000000..1d53194e --- /dev/null +++ b/profile/src/v100/factors/is_factor_source.rs @@ -0,0 +1,9 @@ +use super::{ + factor_source::FactorSource, factor_source_id::FactorSourceID, + factor_source_kind::FactorSourceKind, +}; + +pub trait IsFactorSource: Into + TryFrom { + fn factor_source_kind(&self) -> FactorSourceKind; + fn factor_source_id(&self) -> FactorSourceID; +} diff --git a/profile/src/v100/factors/mod.rs b/profile/src/v100/factors/mod.rs index 4d3baedd..2d349458 100644 --- a/profile/src/v100/factors/mod.rs +++ b/profile/src/v100/factors/mod.rs @@ -1,3 +1,4 @@ +pub mod factor_instance; pub mod factor_source; pub mod factor_source_common; pub mod factor_source_crypto_parameters; @@ -7,4 +8,6 @@ pub mod factor_source_id_from_address; pub mod factor_source_id_from_hash; pub mod factor_source_kind; pub mod factor_sources; +pub mod hd_transaction_signing_factor_instance; pub mod hierarchical_deterministic_factor_instance; +pub mod is_factor_source; diff --git a/profile/src/v100/networks/network/accounts.rs b/profile/src/v100/networks/network/accounts.rs index 22f678d3..09540fda 100644 --- a/profile/src/v100/networks/network/accounts.rs +++ b/profile/src/v100/networks/network/accounts.rs @@ -86,7 +86,7 @@ mod tests { "account_rdx16xlfcpp0vf7e3gqnswv8j9k58n6rjccu58vvspmdva22kf3aplease" .try_into() .unwrap(); - let account = Account::with_values( + let account = Account::placeholder_with_values( address.clone(), DisplayName::default(), AppearanceID::default(), diff --git a/wallet_kit_common/Cargo.toml b/wallet_kit_common/Cargo.toml index 396b804b..1b4634ea 100644 --- a/wallet_kit_common/Cargo.toml +++ b/wallet_kit_common/Cargo.toml @@ -3,9 +3,6 @@ name = "wallet_kit_common" version = "0.1.0" edition = "2021" -[lib] -doctest = false - [dependencies] serde = { version = "1.0.192", features = ["derive"] } serde_json = { version = "1.0.108", features = ["preserve_order"] } @@ -19,3 +16,11 @@ radix-engine-common = { git = "https://github.com/radixdlt/radixdlt-scrypto", re ] } enum-iterator = "1.4.1" hex = "0.4.3" +transaction = { git = "https://github.com/radixdlt/radixdlt-scrypto", rev = "038ddee8b0f57aa90e36375c69946c4eb634efeb", features = [ + "serde", +] } +bip32 = "0.5.1" # only need Secp256k1, to do validation of PublicKey +ed25519-dalek = "1.0.1" +rand = "0.8.5" +enum-as-inner = "0.6.0" +pretty_assertions = "1.4.0" diff --git a/wallet_kit_common/src/error.rs b/wallet_kit_common/src/error.rs deleted file mode 100644 index 99ed1798..00000000 --- a/wallet_kit_common/src/error.rs +++ /dev/null @@ -1,43 +0,0 @@ -use thiserror::Error; - -#[derive(Debug, Error, PartialEq)] -pub enum Error { - #[error("String not hex")] - StringNotHex, - - #[error("Invalid byte count, expected 32.")] - InvalidByteCountExpected32, - - #[error("Invalid Account Address '{0}'.")] - InvalidAccountAddress(String), - - #[error("Unsupported engine entity type.")] - UnsupportedEntityType, - - #[error("Failed to decode address from bech32.")] - FailedToDecodeAddressFromBech32, - - #[error("Failed to decode address mismatching entity type")] - MismatchingEntityTypeWhileDecodingAddress, - - #[error("Failed to decode address mismatching HRP")] - MismatchingHRPWhileDecodingAddress, - - #[error("Unknown network ID '{0}'")] - UnknownNetworkID(u8), - - #[error("Failed to parse InvalidNonFungibleGlobalID from str.")] - InvalidNonFungibleGlobalID, - - #[error("Supported SLIP10 curves in FactorSource crypto parameters is either empty or contains more elements than allowed.")] - FactorSourceCryptoParametersSupportedCurvesInvalidSize, - - #[error("Unknown BIP39 word.")] - UnknownBIP39Word, - - #[error("Invalid mnemonic phrase.")] - InvalidMnemonicPhrase, - - #[error("Invalid bip39 word count : '{0}'")] - InvalidBIP39WordCount(usize), -} diff --git a/wallet_kit_common/src/error/bytes_error.rs b/wallet_kit_common/src/error/bytes_error.rs new file mode 100644 index 00000000..be18bc78 --- /dev/null +++ b/wallet_kit_common/src/error/bytes_error.rs @@ -0,0 +1,10 @@ +use thiserror::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum BytesError { + #[error("String not hex")] + StringNotHex, + + #[error("Invalid byte count, expected 32.")] + InvalidByteCountExpected32, +} diff --git a/wallet_kit_common/src/error/common_error.rs b/wallet_kit_common/src/error/common_error.rs new file mode 100644 index 00000000..97836415 --- /dev/null +++ b/wallet_kit_common/src/error/common_error.rs @@ -0,0 +1,76 @@ +use thiserror::Error; + +use super::{bytes_error::BytesError, hdpath_error::HDPathError, key_error::KeyError}; + +#[derive(Debug, Error, PartialEq)] +pub enum CommonError { + /// + /// NESTED + /// + #[error("Hierarchical Deterministic Path error")] + HDPath(#[from] HDPathError), + + #[error("EC key error")] + Key(#[from] KeyError), + + #[error("Bytes error")] + Bytes(#[from] BytesError), + + /// + /// UN-NESTED + /// + + #[error("Appearance id not recognized.")] + InvalidAppearanceID, + + #[error("String not not a valid display name, did not pass validation.")] + InvalidDisplayName, + + #[error("Invalid Account Address '{0}'.")] + InvalidAccountAddress(String), + + #[error("Unsupported engine entity type.")] + UnsupportedEntityType, + + #[error("Failed to decode address from bech32.")] + FailedToDecodeAddressFromBech32, + + #[error("Failed to decode address mismatching entity type")] + MismatchingEntityTypeWhileDecodingAddress, + + #[error("Failed to decode address mismatching HRP")] + MismatchingHRPWhileDecodingAddress, + + #[error("Unknown network ID '{0}'")] + UnknownNetworkID(u8), + + #[error("Failed to parse InvalidNonFungibleGlobalID from str.")] + InvalidNonFungibleGlobalID, + + #[error("Supported SLIP10 curves in FactorSource crypto parameters is either empty or contains more elements than allowed.")] + FactorSourceCryptoParametersSupportedCurvesInvalidSize, + + #[error("Failed to convert FactorInstance into HierarchicalDeterministicFactorInstance, badge is not virtual HD.")] + BadgeIsNotVirtualHierarchicalDeterministic, + + #[error("Failed to create FactorSourceIDFromHash from FactorSourceID")] + FactorSourceIDNotFromHash, + + #[error("Expected AccountPath but got something else.")] + ExpectedAccountPathButGotSomethingElse, + + #[error("Wrong entity kind in path of FactorInstance")] + WrongEntityKindOfInFactorInstancesPath, + + #[error("Wrong key kind of FactorInstance - expected transaction signing")] + WrongKeyKindOfTransactionSigningFactorInstance, + + #[error("Wrong key kind of FactorInstance - expected authentication signing")] + WrongKeyKindOfAuthenticationSigningFactorInstance, + + #[error("Expected DeviceFactorSource")] + ExpectedDeviceFactorSourceGotSomethingElse, + + #[error("Expected LedgerHardwareWalletFactorSource")] + ExpectedLedgerHardwareWalletFactorSourceGotSomethingElse, +} diff --git a/hierarchical_deterministic/src/hdpath_error.rs b/wallet_kit_common/src/error/hdpath_error.rs similarity index 79% rename from hierarchical_deterministic/src/hdpath_error.rs rename to wallet_kit_common/src/error/hdpath_error.rs index 084b2b54..90976fae 100644 --- a/hierarchical_deterministic/src/hdpath_error.rs +++ b/wallet_kit_common/src/error/hdpath_error.rs @@ -1,7 +1,5 @@ use thiserror::Error; -use crate::{bip32::hd_path_component::HDPathValue, cap26::cap26_entity_kind::CAP26EntityKind}; - #[derive(Debug, Error, PartialEq)] pub enum HDPathError { #[error("Invalid BIP32 path '{0}'.")] @@ -28,26 +26,35 @@ pub enum HDPathError { NotAllComponentsAreHardened, #[error("Did not find 44H, found value: '{0}'")] - BIP44PurposeNotFound(HDPathValue), + BIP44PurposeNotFound(u32), #[error("Did not find cointype 1022H, found value: '{0}'")] - CoinTypeNotFound(HDPathValue), + CoinTypeNotFound(u32), #[error("Network ID exceeds limit of 255, will never be valid, at index 3, found value: '{0}', known network IDs: [1 (mainnet), 2 (stokenet)]")] - InvalidNetworkIDExceedsLimit(HDPathValue), + InvalidNetworkIDExceedsLimit(u32), #[error("InvalidEntityKind, got: '{0}', expected any of: [525H, 618H].")] - InvalidEntityKind(HDPathValue), + InvalidEntityKind(u32), #[error("Wrong entity kind, got: '{0}', but expected: '{1}'")] - WrongEntityKind(CAP26EntityKind, CAP26EntityKind), + WrongEntityKind(u32, u32), #[error("InvalidKeyKind, got: '{0}', expected any of: [1460H, 1678H, 1391H].")] - InvalidKeyKind(HDPathValue), + InvalidKeyKind(u32), #[error("Unsupported NetworkID, got: '{0}', found value: '{0}', known network IDs: [1 (mainnet), 2 (stokenet)]")] UnsupportedNetworkID(u8), #[error("Invalid GetID path, last component was not 365' but {0}'")] - InvalidGetIDPath(HDPathValue), + InvalidGetIDPath(u32), + + #[error("Unknown BIP39 word.")] + UnknownBIP39Word, + + #[error("Invalid mnemonic phrase.")] + InvalidMnemonicPhrase, + + #[error("Invalid bip39 word count : '{0}'")] + InvalidBIP39WordCount(usize), } diff --git a/wallet_kit_common/src/error/key_error.rs b/wallet_kit_common/src/error/key_error.rs new file mode 100644 index 00000000..6e9c7545 --- /dev/null +++ b/wallet_kit_common/src/error/key_error.rs @@ -0,0 +1,34 @@ +use thiserror::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum KeyError { + #[error("Failed to create Ed25519 Private key from bytes.")] + InvalidEd25519PrivateKeyFromBytes, + + #[error("Failed to create Ed25519 Private key from String.")] + InvalidEd25519PrivateKeyFromString, + + #[error("Failed to create Secp256k1 Private key from bytes.")] + InvalidSecp256k1PrivateKeyFromBytes, + + #[error("Failed to create Secp256k1 Private key from String.")] + InvalidSecp256k1PrivateKeyFromString, + + #[error("Failed to create Ed25519 Public key from bytes.")] + InvalidEd25519PublicKeyFromBytes, + + #[error("Failed to create Ed25519 Public key from String.")] + InvalidEd25519PublicKeyFromString, + + #[error("Failed to create Secp256k1 Public key from bytes.")] + InvalidSecp256k1PublicKeyFromBytes, + + #[error("Failed to create Secp256k1 Public key from String.")] + InvalidSecp256k1PublicKeyFromString, + + #[error("Failed to create Secp256k1 Public key, invalid point, not on curve.")] + InvalidSecp256k1PublicKeyPointNotOnCurve, + + #[error("Failed to create Ed25519 Public key, invalid point, not on curve.")] + InvalidEd25519PublicKeyPointNotOnCurve, +} diff --git a/wallet_kit_common/src/error/mod.rs b/wallet_kit_common/src/error/mod.rs new file mode 100644 index 00000000..66925710 --- /dev/null +++ b/wallet_kit_common/src/error/mod.rs @@ -0,0 +1,4 @@ +pub mod bytes_error; +pub mod common_error; +pub mod hdpath_error; +pub mod key_error; diff --git a/wallet_kit_common/src/hash.rs b/wallet_kit_common/src/hash.rs new file mode 100644 index 00000000..2d707da4 --- /dev/null +++ b/wallet_kit_common/src/hash.rs @@ -0,0 +1,6 @@ +use radix_engine_common::crypto::{blake2b_256_hash, Hash}; + +/// Computes the hash digest of a message. +pub fn hash>(data: T) -> Hash { + blake2b_256_hash(data) +} diff --git a/wallet_kit_common/src/json.rs b/wallet_kit_common/src/json.rs index 497babc6..32f56af2 100644 --- a/wallet_kit_common/src/json.rs +++ b/wallet_kit_common/src/json.rs @@ -1,4 +1,5 @@ use core::fmt::Debug; +use pretty_assertions::{assert_eq, assert_ne}; use serde::{de::DeserializeOwned, ser::Serialize}; use serde_json::Value; @@ -9,9 +10,11 @@ where let serialized = serde_json::to_value(&model).unwrap(); let deserialized: T = serde_json::from_value(json.clone()).unwrap(); if expect_eq { + assert_eq!(model, &deserialized); assert_eq!(&deserialized, model, "Expected `model: T` and `T` deserialized from `json_string`, to be equal, but they were not."); assert_eq!(serialized, json, "Expected `json` (string) and json serialized from `model to be equal`, but they were not."); } else { + assert_ne!(model, &deserialized); assert_ne!(&deserialized, model, "Expected difference between `model: T` and `T` deserialized from `json_string`, but they were unexpectedly equal."); assert_ne!(serialized, json, "Expected difference between `json` (string) and json serialized from `model`, but they were unexpectedly equal."); } diff --git a/wallet_kit_common/src/lib.rs b/wallet_kit_common/src/lib.rs index 62befde7..06cded68 100644 --- a/wallet_kit_common/src/lib.rs +++ b/wallet_kit_common/src/lib.rs @@ -1,5 +1,7 @@ pub mod error; +pub mod hash; pub mod json; pub mod network_id; +pub mod secure_random_bytes; pub mod types; pub mod utils; diff --git a/wallet_kit_common/src/network_id.rs b/wallet_kit_common/src/network_id.rs index e9d8f5fe..b631f569 100644 --- a/wallet_kit_common/src/network_id.rs +++ b/wallet_kit_common/src/network_id.rs @@ -58,6 +58,11 @@ pub enum NetworkID { /// The third release candidate of Babylon (RCnet v3) Zabanet = 0x0e, + /// Enkinet (0x21 / 0d33) + /// + /// https://github.com/radixdlt/babylon-node/blob/main/common/src/main/java/com/radixdlt/networks/Network.java#L94 + Enkinet = 0x21, + /// Simulator (0xf2 / 0d242) Simulator = 242, } @@ -82,7 +87,7 @@ impl NetworkID { } impl TryFrom for NetworkID { - type Error = crate::error::Error; + type Error = crate::error::common_error::CommonError; /// Tries to instantiate a NetworkID from its raw representation `u8`. fn try_from(value: u8) -> Result { @@ -108,6 +113,11 @@ impl NetworkID { NetworkID::Kisharnet => NetworkDefinition::kisharnet(), NetworkID::Ansharnet => NetworkDefinition::ansharnet(), NetworkID::Zabanet => NetworkDefinition::zabanet(), + NetworkID::Enkinet => NetworkDefinition { + id: NetworkID::Enkinet.discriminant(), + logical_name: String::from("enkinet"), + hrp_suffix: String::from("tdx_21_"), + }, NetworkID::Simulator => NetworkDefinition::simulator(), } } @@ -170,6 +180,11 @@ mod tests { assert_eq!(NetworkID::Adapanet.discriminant(), 0x0a); } + #[test] + fn discriminant_enkinet() { + assert_eq!(NetworkID::Enkinet.discriminant(), 0x21); + } + #[test] fn discriminant_nebunet() { assert_eq!(NetworkID::Nebunet.discriminant(), 0x0b); @@ -207,6 +222,14 @@ mod tests { ) } + #[test] + fn lookup_network_definition_enkinet() { + assert_eq!( + NetworkID::Enkinet.network_definition().id, + NetworkID::Enkinet.discriminant() + ) + } + #[test] fn logical_name() { assert_eq!(NetworkID::Mainnet.logical_name(), "mainnet") diff --git a/wallet_kit_common/src/secure_random_bytes.rs b/wallet_kit_common/src/secure_random_bytes.rs new file mode 100644 index 00000000..d4ec3c8f --- /dev/null +++ b/wallet_kit_common/src/secure_random_bytes.rs @@ -0,0 +1,37 @@ +use rand::{rngs::OsRng, RngCore}; + +/// Generates `N` random bytes using a cryptographically +/// secure random generator and returns these bytes as +/// a Vec. +pub fn generate_bytes() -> Vec { + let mut csprng = OsRng; + let mut bytes: [u8; N] = [0u8; N]; + csprng.fill_bytes(&mut bytes); + Vec::from(bytes) +} + +/// Generates `32` random bytes using a cryptographically +/// secure random generator and returns these bytes as +/// a Vec. +pub fn generate_32_bytes() -> Vec { + generate_bytes::<32>() +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use super::generate_32_bytes; + + #[test] + fn random() { + let mut set: HashSet> = HashSet::new(); + let n = 100; + for _ in 0..n { + let bytes = generate_32_bytes(); + assert_eq!(bytes.len(), 32); + set.insert(bytes); + } + assert_eq!(set.len(), n); + } +} diff --git a/wallet_kit_common/src/types/hex_32bytes.rs b/wallet_kit_common/src/types/hex_32bytes.rs index d10f03e8..914acf14 100644 --- a/wallet_kit_common/src/types/hex_32bytes.rs +++ b/wallet_kit_common/src/types/hex_32bytes.rs @@ -1,20 +1,47 @@ -use std::str::FromStr; +use std::{ + fmt::{Debug, Display, Formatter}, + str::FromStr, +}; use radix_engine_common::crypto::{Hash, IsHash}; use serde::{de, Deserializer, Serialize, Serializer}; -use crate::error::Error; +use crate::{error::bytes_error::BytesError as Error, secure_random_bytes::generate_32_bytes}; -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +/// Serializable 32 bytes which **always** serializes as a **hex** string, this is useful +/// since in Radix Wallet Kit we almost always want to serialize bytes into hex and this +/// allows us to skip using +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Hex32Bytes([u8; 32]); -impl ToString for Hex32Bytes { - fn to_string(&self) -> String { - hex::encode(self.0) +impl Hex32Bytes { + /// Instantiates a new `Hex32Bytes` from bytes generated by + /// a CSPRNG. + pub fn generate() -> Self { + Hex32Bytes::from_vec(generate_32_bytes()).expect("Should be able to generate 32 bytes.") + } + + /// Just an alias for `Self::generate()` + pub fn new() -> Self { + Self::generate() + } +} + +impl Display for Hex32Bytes { + /// Formats the `Hex32Bytes` as a hex string. + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} +impl Debug for Hex32Bytes { + /// Formats the `Hex32Bytes` as a hex string. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&hex::encode(self.0)) } } impl From for Hex32Bytes { + /// Instantiates a new `Hex32Bytes` from the `Hash` (32 bytes). fn from(value: Hash) -> Self { Self::from_bytes(&value.into_bytes()) } @@ -23,6 +50,9 @@ impl From for Hex32Bytes { impl FromStr for Hex32Bytes { type Err = Error; + /// Tries to decode the string `s` into a `Hex32Bytes`. Will fail + /// if the string is not valid hex or if the decoded bytes does + /// not have length 32. fn from_str(s: &str) -> Result { hex::decode(s) .map_err(|_| Error::StringNotHex) @@ -31,23 +61,34 @@ impl FromStr for Hex32Bytes { } impl Hex32Bytes { + /// Just some placeholder Hex32Bytes + /// A placeholder used to facilitate unit tests. pub fn placeholder() -> Self { Self::from_str("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") .expect("Deadbeef") } +} +impl Hex32Bytes { + /// Returns a clone of the inner bytes as a `Vec`. pub fn to_vec(&self) -> Vec { Vec::from(self.bytes().clone()) } + /// Returns a references to the inner array slice. pub fn bytes(&self) -> &[u8; 32] { &self.0 } +} +impl Hex32Bytes { + /// Instantiates a new `Hex32Bytes` from the 32 bytes, by cloning them. pub fn from_bytes(bytes: &[u8; 32]) -> Self { Self(bytes.clone()) } + /// Tries to turn the `Vec` into a `Hex32Bytes`. Will fail + /// if `bytes` does not have length 32. pub fn from_vec(bytes: Vec) -> Result { bytes .try_into() @@ -55,13 +96,16 @@ impl Hex32Bytes { .map_err(|_| Error::InvalidByteCountExpected32) } + /// Tries to decode the string `s` into a `Hex32Bytes`. Will fail + /// if the string is not valid hex or if the decoded bytes does + /// not have length 32. pub fn from_hex(s: &str) -> Result { Self::from_str(s) } } impl Serialize for Hex32Bytes { - /// Serializes this `AccountAddress` into its bech32 address string as JSON. + /// Serializes this `Hex32Bytes` into a hex string as JSON. fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> where S: Serializer, @@ -71,7 +115,8 @@ impl Serialize for Hex32Bytes { } impl<'de> serde::Deserialize<'de> for Hex32Bytes { - /// Tries to deserializes a JSON string as a bech32 address into an `AccountAddress`. + /// Tries to deserializes a JSON string as a hex string into an `Hex32Bytes`. + #[cfg(not(tarpaulin_include))] // false negative fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; Hex32Bytes::from_hex(&s).map_err(de::Error::custom) @@ -80,9 +125,13 @@ impl<'de> serde::Deserialize<'de> for Hex32Bytes { #[cfg(test)] mod tests { - use std::str::FromStr; + use std::{collections::HashSet, str::FromStr}; - use crate::error::Error; + use serde_json::json; + + use crate::{ + error::bytes_error::BytesError as Error, json::assert_json_value_eq_after_roundtrip, + }; use super::Hex32Bytes; @@ -92,6 +141,22 @@ mod tests { assert_eq!(Hex32Bytes::from_hex(str).unwrap().to_string(), str); } + #[test] + fn debug() { + let str = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + let hex_bytes = Hex32Bytes::placeholder(); + assert_eq!(format!("{:?}", hex_bytes), str); + } + + #[test] + fn json_roundtrip() { + let model = Hex32Bytes::placeholder(); + assert_json_value_eq_after_roundtrip( + &model, + json!("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), + ); + } + #[test] fn from_bytes_roundtrip() { let bytes = [0u8; 32]; @@ -119,4 +184,15 @@ mod tests { Err(Error::InvalidByteCountExpected32) ) } + + #[test] + fn random() { + let mut set: HashSet> = HashSet::new(); + let n = 100; + for _ in 0..n { + let bytes = Hex32Bytes::new(); + set.insert(bytes.to_vec()); + } + assert_eq!(set.len(), n); + } } diff --git a/wallet_kit_common/src/types/keys/ed25519/mod.rs b/wallet_kit_common/src/types/keys/ed25519/mod.rs new file mode 100644 index 00000000..a37c90a7 --- /dev/null +++ b/wallet_kit_common/src/types/keys/ed25519/mod.rs @@ -0,0 +1,2 @@ +pub mod private_key; +pub mod public_key; diff --git a/wallet_kit_common/src/types/keys/ed25519/private_key.rs b/wallet_kit_common/src/types/keys/ed25519/private_key.rs new file mode 100644 index 00000000..c5625215 --- /dev/null +++ b/wallet_kit_common/src/types/keys/ed25519/private_key.rs @@ -0,0 +1,279 @@ +use radix_engine_common::crypto::IsHash; +use transaction::signing::ed25519::{ + Ed25519PrivateKey as EngineEd25519PrivateKey, Ed25519Signature, +}; + +use super::public_key::Ed25519PublicKey; +use crate::{error::key_error::KeyError as Error, types::hex_32bytes::Hex32Bytes}; +use std::fmt::{Debug, Formatter}; + +/// An Ed25519 private key used to create cryptographic signatures, using +/// EdDSA scheme. +pub struct Ed25519PrivateKey(EngineEd25519PrivateKey); + +impl Ed25519PrivateKey { + /// Generates a new `Ed25519PrivateKey` from random bytes + /// generated by a CSRNG, note that this is typically never + /// used by wallets, which tend to rather use a Mnemonic and + /// derive hierarchical deterministic keys. + pub fn generate() -> Self { + Self::from_hex32_bytes(Hex32Bytes::generate()).expect("Should be able to generate 32 bytes") + } + + /// Just an alias for `Self::generate()`, generating a new + /// key from random bytes. + pub fn new() -> Self { + Self::generate() + } +} + +impl PartialEq for Ed25519PrivateKey { + fn eq(&self, other: &Self) -> bool { + self.to_bytes() == other.to_bytes() + } +} + +impl Debug for Ed25519PrivateKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_hex()) + } +} + +impl Ed25519PrivateKey { + pub fn from_engine(engine: EngineEd25519PrivateKey) -> Self { + Self(engine) + } + + pub fn public_key(&self) -> Ed25519PublicKey { + Ed25519PublicKey::from_engine(self.0.public_key()) + .expect("Public Key from EC scalar multiplication should always be valid.") + } + + pub fn sign(&self, msg_hash: &impl IsHash) -> Ed25519Signature { + self.0.sign(msg_hash) + } + + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes().to_vec() + } + + pub fn to_hex(&self) -> String { + hex::encode(self.to_bytes()) + } + + pub fn from_bytes(slice: &[u8]) -> Result { + EngineEd25519PrivateKey::from_bytes(slice) + .map_err(|_| Error::InvalidEd25519PrivateKeyFromBytes) + .map(Self::from_engine) + } + + pub fn from_str(hex: &str) -> Result { + Hex32Bytes::from_hex(hex) + .map_err(|_| Error::InvalidEd25519PrivateKeyFromString) + .and_then(|b| Self::from_bytes(&b.to_vec())) + } + + pub fn from_vec(bytes: Vec) -> Result { + Self::from_bytes(bytes.as_slice()) + } + + pub fn from_hex32_bytes(bytes: Hex32Bytes) -> Result { + Self::from_vec(bytes.to_vec()) + } +} + +impl TryFrom<&[u8]> for Ed25519PrivateKey { + type Error = crate::error::key_error::KeyError; + + fn try_from(slice: &[u8]) -> Result { + Ed25519PrivateKey::from_bytes(slice) + } +} + +impl TryInto for &str { + type Error = crate::error::key_error::KeyError; + + fn try_into(self) -> Result { + Ed25519PrivateKey::from_str(self) + } +} + +impl Ed25519PrivateKey { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::placeholder_alice() + } + + /// `833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42` + /// + /// expected public key: + /// `ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf` + /// + /// https://github.com/dalek-cryptography/ed25519-dalek/blob/main/tests/ed25519.rs#L103 + pub fn placeholder_alice() -> Self { + Self::from_str("833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42").unwrap() + } + + /// `1498b5467a63dffa2dc9d9e069caf075d16fc33fdd4c3b01bfadae6433767d93`` + + /// expected public key: + /// `b7a3c12dc0c8c748ab07525b701122b88bd78f600c76342d27f25e5f92444cde` + /// + /// https://cryptobook.nakov.com/digital-signatures/eddsa-sign-verify-examples + pub fn placeholder_bob() -> Self { + Self::from_str("1498b5467a63dffa2dc9d9e069caf075d16fc33fdd4c3b01bfadae6433767d93").unwrap() + } +} + +#[cfg(test)] +mod tests { + use std::{collections::HashSet, str::FromStr}; + + use transaction::signing::ed25519::Ed25519Signature; + + use crate::{error::key_error::KeyError as Error, hash::hash, types::hex_32bytes::Hex32Bytes}; + + use super::Ed25519PrivateKey; + + #[test] + fn sign_and_verify() { + let msg = hash("Test"); + let sk: Ed25519PrivateKey = + "0000000000000000000000000000000000000000000000000000000000000001" + .try_into() + .unwrap(); + let pk = sk.public_key(); + assert_eq!( + pk.to_hex(), + "4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29" + ); + let sig = Ed25519Signature::from_str("cf0ca64435609b85ab170da339d415bbac87d678dfd505969be20adc6b5971f4ee4b4620c602bcbc34fd347596546675099d696265f4a42a16df343da1af980e").unwrap(); + + assert_eq!(sk.sign(&msg), sig); + assert!(pk.is_valid(&sig, &msg)) + } + + #[test] + fn bytes_roundtrip() { + let bytes = hex::decode("0000000000000000000000000000000000000000000000000000000000000001") + .unwrap(); + assert_eq!( + Ed25519PrivateKey::from_bytes(bytes.as_slice()) + .unwrap() + .to_bytes(), + bytes.as_slice() + ); + } + + #[test] + fn hex_roundtrip() { + let hex = "0000000000000000000000000000000000000000000000000000000000000001"; + assert_eq!(Ed25519PrivateKey::from_str(hex).unwrap().to_hex(), hex); + } + + #[test] + fn invalid_hex() { + assert_eq!( + Ed25519PrivateKey::from_str("not hex"), + Err(Error::InvalidEd25519PrivateKeyFromString) + ); + } + + #[test] + fn invalid_hex_too_short() { + assert_eq!( + Ed25519PrivateKey::from_str("dead"), + Err(Error::InvalidEd25519PrivateKeyFromString) + ); + } + + #[test] + fn invalid_bytes() { + assert_eq!( + Ed25519PrivateKey::from_bytes(&[0u8] as &[u8]), + Err(Error::InvalidEd25519PrivateKeyFromBytes) + ); + } + + #[test] + fn equality() { + assert_eq!( + Ed25519PrivateKey::from_str( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + .unwrap(), + Ed25519PrivateKey::from_str( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + .unwrap() + ); + } + + #[test] + fn debug() { + let hex = "0000000000000000000000000000000000000000000000000000000000000001"; + assert_eq!( + format!("{:?}", Ed25519PrivateKey::from_str(hex).unwrap()), + hex + ); + } + + #[test] + fn from_vec() { + let hex = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + assert_eq!( + Ed25519PrivateKey::from_vec(Vec::from(hex::decode(hex).unwrap())) + .unwrap() + .to_hex(), + hex + ); + } + + #[test] + fn from_hex32() { + let hex = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + assert_eq!( + Ed25519PrivateKey::from_hex32_bytes(Hex32Bytes::from_hex(hex).unwrap()) + .unwrap() + .to_hex(), + hex + ); + } + + #[test] + fn generate_new() { + let mut set: HashSet> = HashSet::new(); + let n = 100; + for _ in 0..n { + let key = Ed25519PrivateKey::new(); + let bytes = key.to_bytes(); + assert_eq!(bytes.len(), 32); + set.insert(bytes); + } + assert_eq!(set.len(), n); + } + + #[test] + fn from_hex32_bytes() { + let str = "0000000000000000000000000000000000000000000000000000000000000001"; + let hex32 = Hex32Bytes::from_hex(str).unwrap(); + let key = Ed25519PrivateKey::from_hex32_bytes(hex32).unwrap(); + assert_eq!(key.to_hex(), str); + } + + #[test] + fn try_from_bytes() { + let str = "0000000000000000000000000000000000000000000000000000000000000001"; + let vec = hex::decode(str).unwrap(); + let key = Ed25519PrivateKey::try_from(vec.as_slice()).unwrap(); + assert_eq!(key.to_hex(), str); + } + + #[test] + fn placeholder() { + assert_eq!( + Ed25519PrivateKey::placeholder().public_key().to_hex(), + "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf" + ); + } +} diff --git a/wallet_kit_common/src/types/keys/ed25519/public_key.rs b/wallet_kit_common/src/types/keys/ed25519/public_key.rs new file mode 100644 index 00000000..5c63637e --- /dev/null +++ b/wallet_kit_common/src/types/keys/ed25519/public_key.rs @@ -0,0 +1,217 @@ +use crate::{ + error::key_error::KeyError as Error, + types::{hex_32bytes::Hex32Bytes, keys::ed25519::private_key::Ed25519PrivateKey}, +}; +use radix_engine_common::crypto::{Ed25519PublicKey as EngineEd25519PublicKey, Hash}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{Debug, Formatter}, + str::FromStr, +}; +use transaction::{signing::ed25519::Ed25519Signature, validation::verify_ed25519}; + +/// An Ed25519 public key used to verify cryptographic signatures (EdDSA signatures). +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Ed25519PublicKey(EngineEd25519PublicKey); + +impl Ed25519PublicKey { + pub(crate) fn from_engine(engine: EngineEd25519PublicKey) -> Result { + ed25519_dalek::PublicKey::from_bytes(engine.to_vec().as_slice()) + .map(|_| Self(engine)) + .map_err(|_| Error::InvalidEd25519PublicKeyPointNotOnCurve) + } + + pub fn to_bytes(&self) -> Vec { + self.0.to_vec() + } + + pub fn to_hex(&self) -> String { + hex::encode(self.to_bytes()) + } + + /// Verifies an EdDSA signature over Curve25519. + pub fn is_valid(&self, signature: &Ed25519Signature, for_hash: &Hash) -> bool { + verify_ed25519(for_hash, &self.0, signature) + } +} + +impl TryFrom<&[u8]> for Ed25519PublicKey { + type Error = crate::error::key_error::KeyError; + + fn try_from(slice: &[u8]) -> Result { + EngineEd25519PublicKey::try_from(slice) + .map_err(|_| Error::InvalidEd25519PublicKeyFromBytes) + .and_then(|pk| Self::from_engine(pk)) + } +} + +impl TryInto for &str { + type Error = crate::error::key_error::KeyError; + + fn try_into(self) -> Result { + Ed25519PublicKey::from_str(self) + } +} + +impl Ed25519PublicKey { + pub fn from_str(hex: &str) -> Result { + Hex32Bytes::from_str(hex) + .map_err(|_| Error::InvalidEd25519PublicKeyFromString) + .and_then(|b| Ed25519PublicKey::try_from(b.to_vec().as_slice())) + } +} + +impl Debug for Ed25519PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_hex()) + } +} + +impl Ed25519PublicKey { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::placeholder_alice() + } + + pub fn placeholder_alice() -> Self { + Ed25519PrivateKey::placeholder_alice().public_key() + } + + pub fn placeholder_bob() -> Self { + Ed25519PrivateKey::placeholder_bob().public_key() + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use crate::{error::key_error::KeyError as Error, json::assert_json_value_eq_after_roundtrip}; + use serde_json::json; + + use super::Ed25519PublicKey; + #[test] + fn json() { + let model = Ed25519PublicKey::placeholder_alice(); + assert_json_value_eq_after_roundtrip( + &model, + json!("ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf"), + ) + } + + #[test] + fn from_str() { + assert!(Ed25519PublicKey::from_str( + "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf" + ) + .is_ok()); + } + + #[test] + fn bytes_roundtrip() { + let bytes: &[u8] = &[ + 0xec, 0x17, 0x2b, 0x93, 0xad, 0x5e, 0x56, 0x3b, 0xf4, 0x93, 0x2c, 0x70, 0xe1, 0x24, + 0x50, 0x34, 0xc3, 0x54, 0x67, 0xef, 0x2e, 0xfd, 0x4d, 0x64, 0xeb, 0xf8, 0x19, 0x68, + 0x34, 0x67, 0xe2, 0xbf, + ]; + let key = Ed25519PublicKey::try_from(bytes).unwrap(); + assert_eq!( + key.to_hex(), + "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf" + ); + assert_eq!(key.to_bytes(), bytes); + } + + #[test] + fn placeholder_alice() { + assert_eq!( + Ed25519PublicKey::placeholder_alice().to_hex(), + "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf" + ); + } + + #[test] + fn placeholder_bob() { + assert_eq!( + Ed25519PublicKey::placeholder_bob().to_hex(), + "b7a3c12dc0c8c748ab07525b701122b88bd78f600c76342d27f25e5f92444cde" + ); + } + + #[test] + fn invalid_bytes() { + assert_eq!( + Ed25519PublicKey::try_from(&[0u8] as &[u8]), + Err(Error::InvalidEd25519PublicKeyFromBytes) + ); + } + + #[test] + fn invalid_hex_str() { + assert_eq!( + Ed25519PublicKey::from_str("not a valid hex string"), + Err(Error::InvalidEd25519PublicKeyFromString) + ); + } + + #[test] + fn invalid_str_too_short() { + assert_eq!( + Ed25519PublicKey::from_str("dead"), + Err(Error::InvalidEd25519PublicKeyFromString) + ); + } + + #[test] + fn try_into_from_str() { + let str = "b7a3c12dc0c8c748ab07525b701122b88bd78f600c76342d27f25e5f92444cde"; + let key: Ed25519PublicKey = str.try_into().unwrap(); + assert_eq!(key.to_hex(), str); + } + + #[test] + fn invalid_key_not_on_curve() { + assert_eq!( + Ed25519PublicKey::from_str( + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ), + Err(Error::InvalidEd25519PublicKeyPointNotOnCurve) + ); + } + + #[test] + fn debug() { + assert_eq!( + format!("{:?}", Ed25519PublicKey::placeholder_alice()), + "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf" + ); + } + + #[test] + fn inequality() { + assert_ne!( + Ed25519PublicKey::placeholder_alice(), + Ed25519PublicKey::placeholder_bob() + ); + } + + #[test] + fn equality() { + assert_eq!( + Ed25519PublicKey::placeholder_alice(), + Ed25519PublicKey::placeholder_alice() + ); + } + + #[test] + fn hash() { + assert_eq!( + BTreeSet::from_iter([ + Ed25519PublicKey::placeholder_alice(), + Ed25519PublicKey::placeholder_alice() + ]) + .len(), + 1 + ); + } +} diff --git a/wallet_kit_common/src/types/keys/mod.rs b/wallet_kit_common/src/types/keys/mod.rs new file mode 100644 index 00000000..55224e96 --- /dev/null +++ b/wallet_kit_common/src/types/keys/mod.rs @@ -0,0 +1,5 @@ +pub mod ed25519; +pub mod private_key; +pub mod public_key; +pub mod secp256k1; +pub mod slip10_curve; diff --git a/wallet_kit_common/src/types/keys/private_key.rs b/wallet_kit_common/src/types/keys/private_key.rs new file mode 100644 index 00000000..5eb2d5b7 --- /dev/null +++ b/wallet_kit_common/src/types/keys/private_key.rs @@ -0,0 +1,157 @@ +use super::{ + ed25519::private_key::Ed25519PrivateKey, public_key::PublicKey, + secp256k1::private_key::Secp256k1PrivateKey, +}; +use enum_as_inner::EnumAsInner; + +/// A tagged union of supported private keys on different curves, supported +/// curves are `secp256k1` and `Curve25519` +#[derive(EnumAsInner)] +pub enum PrivateKey { + /// An Ed25519 private key used to create cryptographic signatures, using EdDSA scheme. + Ed25519(Ed25519PrivateKey), + + /// A secp256k1 private key used to create cryptographic signatures, + /// more specifically ECDSA signatures, that offer recovery of the public key + Secp256k1(Secp256k1PrivateKey), +} + +impl From for PrivateKey { + /// Enables: + /// + /// ``` + /// extern crate wallet_kit_common; + /// use wallet_kit_common::types::keys::ed25519::private_key::Ed25519PrivateKey; + /// use wallet_kit_common::types::keys::public_key::PublicKey; + /// + /// let key: PublicKey = Ed25519PrivateKey::new().public_key().into(); + /// ``` + fn from(value: Ed25519PrivateKey) -> Self { + Self::Ed25519(value) + } +} + +impl From for PrivateKey { + /// Enables: + /// + /// ``` + /// extern crate wallet_kit_common; + /// use wallet_kit_common::types::keys::secp256k1::private_key::Secp256k1PrivateKey; + /// use wallet_kit_common::types::keys::public_key::PublicKey; + /// + /// let key: PublicKey = Secp256k1PrivateKey::new().public_key().into(); + /// ``` + fn from(value: Secp256k1PrivateKey) -> Self { + Self::Secp256k1(value) + } +} + +impl PrivateKey { + /// Generates a new `PrivateKey` over Curve25519. + pub fn new() -> Self { + Ed25519PrivateKey::generate().into() + } + + /// Calculates the public key of the inner `PrivateKey` and wraps it + /// in the `PublicKey` tagged union. + pub fn public_key(&self) -> PublicKey { + match self { + PrivateKey::Ed25519(key) => PublicKey::Ed25519(key.public_key()), + PrivateKey::Secp256k1(key) => PublicKey::Secp256k1(key.public_key()), + } + } + + /// Returns the hex representation of the inner private key's bytes as a `Vec`. + pub fn to_bytes(&self) -> Vec { + match self { + PrivateKey::Ed25519(key) => key.to_bytes(), + PrivateKey::Secp256k1(key) => key.to_bytes(), + } + } + + /// Returns the hex representation of the inner private key as a `String`. + pub fn to_hex(&self) -> String { + match self { + PrivateKey::Ed25519(key) => key.to_hex(), + PrivateKey::Secp256k1(key) => key.to_hex(), + } + } +} + +#[cfg(test)] +mod tests { + + use std::collections::HashSet; + + use crate::{ + secure_random_bytes::generate_32_bytes, + types::keys::{ + ed25519::private_key::Ed25519PrivateKey, secp256k1::private_key::Secp256k1PrivateKey, + }, + }; + + use super::PrivateKey; + + #[test] + fn private_key_ed25519_into_as_roundtrip() { + let bytes = generate_32_bytes(); + // test `into`` + let private_key: PrivateKey = Ed25519PrivateKey::from_vec(bytes.clone()).unwrap().into(); + // test `as` + assert_eq!( + private_key.as_ed25519().unwrap(), + &Ed25519PrivateKey::from_vec(bytes).unwrap() + ); + } + + #[test] + fn private_key_ed25519_into_as_wrong_fails() { + let bytes = generate_32_bytes(); + // test `into`` + let private_key: PrivateKey = Ed25519PrivateKey::from_vec(bytes.clone()).unwrap().into(); + // test `as` + assert!(private_key.as_secp256k1().is_none()); + } + + #[test] + fn private_key_secp256k1_into_as_roundtrip() { + let bytes = generate_32_bytes(); + // test `into`` + let private_key: PrivateKey = Secp256k1PrivateKey::from_vec(bytes.clone()).unwrap().into(); + // test `as` + assert_eq!( + private_key.as_secp256k1().unwrap(), + &Secp256k1PrivateKey::from_vec(bytes).unwrap() + ); + } + + #[test] + fn private_key_secp256k1_into_as_wrong_fails() { + let bytes = generate_32_bytes(); + // test `into`` + let private_key: PrivateKey = Secp256k1PrivateKey::from_vec(bytes.clone()).unwrap().into(); + // test `as` + assert!(private_key.as_ed25519().is_none()); + } + + #[test] + fn generate_new() { + let mut set: HashSet> = HashSet::new(); + let n = 100; + for _ in 0..n { + let key = PrivateKey::new(); + let bytes = key.to_bytes(); + assert_eq!(bytes.len(), 32); + set.insert(bytes); + } + assert_eq!(set.len(), n); + } + + #[test] + fn secp256k1_to_bytes() { + let bytes = generate_32_bytes(); + let key = Secp256k1PrivateKey::from_bytes(&bytes).unwrap(); + let private_key: PrivateKey = key.into(); + assert_eq!(private_key.to_bytes(), bytes); + } +} diff --git a/wallet_kit_common/src/types/keys/public_key.rs b/wallet_kit_common/src/types/keys/public_key.rs new file mode 100644 index 00000000..a1b49373 --- /dev/null +++ b/wallet_kit_common/src/types/keys/public_key.rs @@ -0,0 +1,407 @@ +use serde::{de, ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::error::key_error::KeyError as Error; + +use radix_engine_common::crypto::{ + Ed25519PublicKey as EngineEd25519PublicKey, PublicKey as EnginePublicKey, + Secp256k1PublicKey as EngineSecp256k1PublicKey, +}; + +use super::{ + ed25519::public_key::Ed25519PublicKey, secp256k1::public_key::Secp256k1PublicKey, + slip10_curve::SLIP10Curve, +}; +use enum_as_inner::EnumAsInner; + +/// A tagged union of supported public keys on different curves, supported +/// curves are `secp256k1` and `Curve25519` +#[derive(Clone, Copy, Debug, PartialEq, EnumAsInner, Eq, Hash, PartialOrd, Ord)] +pub enum PublicKey { + /// An Ed25519 public key used to verify cryptographic signatures. + Ed25519(Ed25519PublicKey), + + /// A secp256k1 public key used to verify cryptographic signatures (ECDSA signatures). + Secp256k1(Secp256k1PublicKey), +} + +impl From for PublicKey { + /// Enables: + /// + /// ``` + /// extern crate wallet_kit_common; + /// use wallet_kit_common::types::keys::ed25519::private_key::Ed25519PrivateKey; + /// use wallet_kit_common::types::keys::public_key::PublicKey; + /// + /// let key: PublicKey = Ed25519PrivateKey::new().public_key().into(); + /// ``` + fn from(value: Ed25519PublicKey) -> Self { + Self::Ed25519(value) + } +} + +impl From for PublicKey { + /// Enables: + /// + /// ``` + /// extern crate wallet_kit_common; + /// use wallet_kit_common::types::keys::secp256k1::private_key::Secp256k1PrivateKey; + /// use wallet_kit_common::types::keys::public_key::PublicKey; + /// + /// let key: PublicKey = Secp256k1PrivateKey::new().public_key().into(); + /// ``` + fn from(value: Secp256k1PublicKey) -> Self { + Self::Secp256k1(value) + } +} + +impl PublicKey { + /// Try to instantiate a `PublicKey` from bytes as a `Secp256k1PublicKey`. + pub fn secp256k1_from_bytes(slice: &[u8]) -> Result { + Secp256k1PublicKey::try_from(slice).map(Self::Secp256k1) + } + + /// Try to instantiate a `PublicKey` from bytes as a `Ed25519PublicKey`. + pub fn ed25519_from_bytes(slice: &[u8]) -> Result { + Ed25519PublicKey::try_from(slice).map(Self::Ed25519) + } + + /// Try to instantiate a `PublicKey` from hex string as a `Secp256k1PublicKey`. + pub fn secp256k1_from_str(hex: &str) -> Result { + Secp256k1PublicKey::from_str(hex).map(Self::Secp256k1) + } + + /// Try to instantiate a `PublicKey` from hex string as a `Ed25519PublicKey`. + pub fn ed25519_from_str(hex: &str) -> Result { + Ed25519PublicKey::from_str(hex).map(Self::Ed25519) + } +} + +impl PublicKey { + /// Returns a `SLIP10Curve`, being the curve of the `PublicKey`. + pub fn curve(&self) -> SLIP10Curve { + match self { + PublicKey::Ed25519(_) => SLIP10Curve::Curve25519, + PublicKey::Secp256k1(_) => SLIP10Curve::Secp256k1, + } + } + + /// Returns a hex encoding of the inner public key. + pub fn to_hex(&self) -> String { + match self { + PublicKey::Ed25519(key) => key.to_hex(), + PublicKey::Secp256k1(key) => key.to_hex(), + } + } + + /// Returns a clone of the bytes of the inner public key as a `Vec`. + pub fn to_bytes(&self) -> Vec { + match self { + PublicKey::Ed25519(key) => key.to_bytes(), + PublicKey::Secp256k1(key) => key.to_bytes(), + } + } +} + +impl PublicKey { + /// A placeholder used to facilitate unit tests. + pub fn placeholder_secp256k1() -> Self { + Self::placeholder_secp256k1_alice() + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_secp256k1_alice() -> Self { + Self::Secp256k1(Secp256k1PublicKey::placeholder_alice()) + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_secp256k1_bob() -> Self { + Self::Secp256k1(Secp256k1PublicKey::placeholder_bob()) + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_ed25519() -> Self { + Self::placeholder_ed25519_alice() + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_ed25519_alice() -> Self { + Self::Ed25519(Ed25519PublicKey::placeholder_alice()) + } + + /// A placeholder used to facilitate unit tests. + pub fn placeholder_ed25519_bob() -> Self { + Self::Ed25519(Ed25519PublicKey::placeholder_bob()) + } +} + +impl<'de> Deserialize<'de> for PublicKey { + #[cfg(not(tarpaulin_include))] // false negative + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize, Serialize)] + struct Wrapper { + #[serde(rename = "compressedData")] + hex: String, + curve: SLIP10Curve, + } + let wrapper = Wrapper::deserialize(deserializer)?; + match wrapper.curve { + SLIP10Curve::Curve25519 => Ed25519PublicKey::from_str(&wrapper.hex) + .map(|pk| PublicKey::Ed25519(pk)) + .map_err(de::Error::custom), + SLIP10Curve::Secp256k1 => Secp256k1PublicKey::from_str(&wrapper.hex) + .map(|pk| PublicKey::Secp256k1(pk)) + .map_err(de::Error::custom), + } + } +} + +impl Serialize for PublicKey { + #[cfg(not(tarpaulin_include))] // false negative + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("PublicKey", 2)?; + state.serialize_field("curve", &self.curve())?; + state.serialize_field("compressedData", &self.to_hex())?; + state.end() + } +} + +impl Into for PublicKey { + fn into(self) -> EnginePublicKey { + match self { + PublicKey::Ed25519(key) => EnginePublicKey::Ed25519(key.into()), + PublicKey::Secp256k1(key) => EnginePublicKey::Secp256k1(key.into()), + } + } +} + +impl Into for Secp256k1PublicKey { + fn into(self) -> EngineSecp256k1PublicKey { + EngineSecp256k1PublicKey::try_from(self.to_bytes().as_slice()).unwrap() + } +} + +impl Into for Ed25519PublicKey { + fn into(self) -> EngineEd25519PublicKey { + EngineEd25519PublicKey::try_from(self.to_bytes().as_slice()).unwrap() + } +} + +#[cfg(test)] +mod tests { + + use std::collections::BTreeSet; + + use crate::{ + json::{assert_eq_after_json_roundtrip, assert_json_fails}, + types::keys::{ + ed25519::public_key::Ed25519PublicKey, secp256k1::public_key::Secp256k1PublicKey, + }, + }; + + use super::PublicKey; + + use radix_engine_common::crypto::PublicKey as EnginePublicKey; + + #[test] + fn engine_roundtrip_secp256k1() { + let public_key_secp256k1: PublicKey = Secp256k1PublicKey::placeholder().into(); + let engine_key_secp256k1: EnginePublicKey = public_key_secp256k1.clone().into(); + match engine_key_secp256k1 { + EnginePublicKey::Secp256k1(k) => { + assert_eq!(k.to_vec(), public_key_secp256k1.to_bytes()) + } + EnginePublicKey::Ed25519(_) => panic!("wrong kind"), + } + } + + #[test] + fn engine_roundtrip_ed25519() { + let public_key_ed25519: PublicKey = Ed25519PublicKey::placeholder().into(); + let engine_key_ed25519: EnginePublicKey = public_key_ed25519.clone().into(); + match engine_key_ed25519 { + EnginePublicKey::Ed25519(k) => { + assert_eq!(k.to_vec(), public_key_ed25519.to_bytes()) + } + EnginePublicKey::Secp256k1(_) => panic!("wrong kind"), + } + } + + #[test] + fn json_roundtrip_ed25519() { + let model = PublicKey::placeholder_ed25519_alice(); + + assert_eq_after_json_roundtrip( + &model, + r#" + { + "curve": "curve25519", + "compressedData": "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf" + } + "#, + ); + } + + #[test] + fn json_invalid_curve() { + assert_json_fails::( + r#" + { + "curve": "invalid curve", + "compressedData": "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf" + } + "#, + ); + } + + #[test] + fn json_invalid_public_key_not_on_curve() { + assert_json_fails::( + r#" + { + "curve": "curve25519", + "compressedData": "abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba" + } + "#, + ); + } + + #[test] + fn json_roundtrip_secp256k1() { + let model = PublicKey::placeholder_secp256k1_alice(); + + assert_eq_after_json_roundtrip( + &model, + r#" + { + "curve": "secp256k1", + "compressedData": "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" + } + "#, + ); + } + + #[test] + fn inequality_secp256k1() { + assert_ne!( + PublicKey::placeholder_secp256k1_alice(), + PublicKey::placeholder_secp256k1_bob(), + ); + } + + #[test] + fn equality_secp256k1() { + assert_eq!( + PublicKey::placeholder_secp256k1(), + PublicKey::placeholder_secp256k1_alice() + ); + } + + #[test] + fn hash_secp256k1() { + assert_eq!( + BTreeSet::from_iter([ + PublicKey::placeholder_secp256k1_alice(), + PublicKey::placeholder_secp256k1_alice() + ]) + .len(), + 1 + ); + } + + #[test] + fn inequality_ed25519() { + assert_ne!( + PublicKey::placeholder_ed25519_alice(), + PublicKey::placeholder_ed25519_bob(), + ); + } + + #[test] + fn equality_ed25519() { + assert_eq!( + PublicKey::placeholder_ed25519(), + PublicKey::placeholder_ed25519_alice() + ); + } + + #[test] + fn hash_ed25519() { + assert_eq!( + BTreeSet::from_iter([ + PublicKey::placeholder_ed25519_alice(), + PublicKey::placeholder_ed25519_alice() + ]) + .len(), + 1 + ); + } + + #[test] + fn inequality_different_curves() { + assert_ne!( + PublicKey::placeholder_ed25519_alice(), + PublicKey::placeholder_secp256k1_alice(), + ); + } + + #[test] + fn secp256k1_bytes_roundtrip() { + let bytes: &[u8] = &[ + 0x02, 0x51, 0x7b, 0x88, 0x91, 0x6e, 0x7f, 0x31, 0x5b, 0xb6, 0x82, 0xf9, 0x92, 0x6b, + 0x14, 0xbc, 0x67, 0xa0, 0xe4, 0x24, 0x6f, 0x8a, 0x41, 0x9b, 0x98, 0x62, 0x69, 0xe1, + 0xa7, 0xe6, 0x1f, 0xff, 0xa7, + ]; + let key = PublicKey::secp256k1_from_bytes(bytes).unwrap(); + assert_eq!( + key.to_hex(), + "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" + ); + assert_eq!(key.to_bytes(), bytes); + } + + #[test] + fn secp256k1_hex_roundtrip() { + let hex = "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7"; + let key = PublicKey::secp256k1_from_str(hex).unwrap(); + assert_eq!(key.to_hex(), hex); + } + + #[test] + fn ed25519_bytes_roundtrip() { + let bytes: &[u8] = &[ + 0xec, 0x17, 0x2b, 0x93, 0xad, 0x5e, 0x56, 0x3b, 0xf4, 0x93, 0x2c, 0x70, 0xe1, 0x24, + 0x50, 0x34, 0xc3, 0x54, 0x67, 0xef, 0x2e, 0xfd, 0x4d, 0x64, 0xeb, 0xf8, 0x19, 0x68, + 0x34, 0x67, 0xe2, 0xbf, + ]; + let key = PublicKey::ed25519_from_bytes(bytes).unwrap(); + assert_eq!( + key.to_hex(), + "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf" + ); + assert_eq!(key.to_bytes(), bytes); + } + + #[test] + fn ed25519_hex_roundtrip() { + let hex = "ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf"; + let key = PublicKey::ed25519_from_str(hex).unwrap(); + assert_eq!(key.to_hex(), hex); + } + + #[test] + fn ed25519_into_as_roundtrip() { + let ed25519 = Ed25519PublicKey::placeholder(); + let key: PublicKey = ed25519.clone().into(); + assert_eq!(key.as_ed25519().unwrap(), &ed25519); + } + + #[test] + fn secp256k1_into_as_roundtrip() { + let secp256k1 = Secp256k1PublicKey::placeholder(); + let key: PublicKey = secp256k1.clone().into(); + assert_eq!(key.as_secp256k1().unwrap(), &secp256k1); + } +} diff --git a/wallet_kit_common/src/types/keys/secp256k1/mod.rs b/wallet_kit_common/src/types/keys/secp256k1/mod.rs new file mode 100644 index 00000000..a37c90a7 --- /dev/null +++ b/wallet_kit_common/src/types/keys/secp256k1/mod.rs @@ -0,0 +1,2 @@ +pub mod private_key; +pub mod public_key; diff --git a/wallet_kit_common/src/types/keys/secp256k1/private_key.rs b/wallet_kit_common/src/types/keys/secp256k1/private_key.rs new file mode 100644 index 00000000..85c8d9fb --- /dev/null +++ b/wallet_kit_common/src/types/keys/secp256k1/private_key.rs @@ -0,0 +1,276 @@ +use crate::{error::key_error::KeyError as Error, types::hex_32bytes::Hex32Bytes}; +use radix_engine_common::crypto::IsHash; +use transaction::signing::secp256k1::{ + Secp256k1PrivateKey as EngineSecp256k1PrivateKey, Secp256k1Signature, +}; + +use super::public_key::Secp256k1PublicKey; +use std::fmt::{Debug, Formatter}; + +/// A secp256k1 private key used to create cryptographic signatures, more specifically +/// ECDSA signatures, that offer recovery of the public key. +pub struct Secp256k1PrivateKey(EngineSecp256k1PrivateKey); + +impl Secp256k1PrivateKey { + /// Generates a new `Secp256k1PrivateKey` from random bytes + /// generated by a CSRNG, note that this is typically never + /// used by wallets, which tend to rather use a Mnemonic and + /// derive hierarchical deterministic keys. + pub fn generate() -> Self { + Self::from_hex32_bytes(Hex32Bytes::generate()).expect("Should be able to generate 32 bytes") + } + + /// Just an alias for `Self::generate()`, generating a new + /// key from random bytes. + pub fn new() -> Self { + Self::generate() + } +} + +impl PartialEq for Secp256k1PrivateKey { + fn eq(&self, other: &Self) -> bool { + self.to_bytes() == other.to_bytes() + } +} + +impl Debug for Secp256k1PrivateKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_hex()) + } +} + +impl Secp256k1PrivateKey { + pub fn from_engine(engine: EngineSecp256k1PrivateKey) -> Self { + Self(engine) + } + + pub fn public_key(&self) -> Secp256k1PublicKey { + Secp256k1PublicKey::from_engine(self.0.public_key()) + .expect("Public Key from EC scalar multiplication should always be valid.") + } + + pub fn sign(&self, msg_hash: &impl IsHash) -> Secp256k1Signature { + self.0.sign(msg_hash) + } + + pub fn to_bytes(&self) -> Vec { + self.0.to_bytes() + } + + pub fn to_hex(&self) -> String { + hex::encode(self.to_bytes()) + } + + pub fn from_bytes(slice: &[u8]) -> Result { + EngineSecp256k1PrivateKey::from_bytes(slice) + .map_err(|_| Error::InvalidSecp256k1PrivateKeyFromBytes) + .map(Self::from_engine) + } + + pub fn from_vec(bytes: Vec) -> Result { + Self::from_bytes(bytes.as_slice()) + } + + pub fn from_hex32_bytes(bytes: Hex32Bytes) -> Result { + Self::from_vec(bytes.to_vec()) + } + + pub fn from_str(hex: &str) -> Result { + Hex32Bytes::from_hex(hex) + .map_err(|_| Error::InvalidSecp256k1PrivateKeyFromString) + .and_then(|b| Self::from_bytes(&b.to_vec())) + } +} + +impl TryInto for &str { + type Error = crate::error::key_error::KeyError; + + fn try_into(self) -> Result { + Secp256k1PrivateKey::from_str(self) + } +} + +impl TryFrom<&[u8]> for Secp256k1PrivateKey { + type Error = crate::error::key_error::KeyError; + + fn try_from(slice: &[u8]) -> Result { + Secp256k1PrivateKey::from_bytes(slice) + } +} + +impl Secp256k1PrivateKey { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::placeholder_alice() + } + + /// `d78b6578b33f3446bdd9d09d057d6598bc915fec4008a54c509dc3b8cdc7dbe5` + /// expected public key uncompressed: + /// `04517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa71159e5614fb40739f4d22004380670cbc99ee4a2a73899d084098f3a139130c4` + /// expected public key compressed: + /// `02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7` + /// + /// https://github.com/Sajjon/K1/blob/main/Tests/K1Tests/TestVectors/cyon_ecdh_two_variants_with_kdf.json#L10 + pub fn placeholder_alice() -> Self { + Self::from_str("d78b6578b33f3446bdd9d09d057d6598bc915fec4008a54c509dc3b8cdc7dbe5").unwrap() + } + + /// `871761c9921a467059e090a0422ae76af87fa8eb905da91c9b554bd6a028c760`` + /// expected public key uncompressed: + /// `043083620d1596d3f8988ff3270e42970dd2a031e2b9b6488052a4170ff999f3e8ab3efd3320b8f893cb421ed7ff0aa9ff43b43cad4e00e194f89845c6ac8233a7` + /// expected public key compressed: + /// `033083620d1596d3f8988ff3270e42970dd2a031e2b9b6488052a4170ff999f3e8` + /// + /// https://github.com/Sajjon/K1/blob/main/Tests/K1Tests/TestVectors/cyon_ecdh_two_variants_with_kdf.json#L12 + pub fn placeholder_bob() -> Self { + Self::from_str("871761c9921a467059e090a0422ae76af87fa8eb905da91c9b554bd6a028c760").unwrap() + } +} + +#[cfg(test)] +mod tests { + + use std::{collections::HashSet, str::FromStr}; + + use transaction::signing::secp256k1::Secp256k1Signature; + + use crate::{error::key_error::KeyError as Error, hash::hash, types::hex_32bytes::Hex32Bytes}; + + use super::Secp256k1PrivateKey; + + #[test] + fn sign_and_verify() { + let msg = hash("Test"); + let sk: Secp256k1PrivateKey = + "0000000000000000000000000000000000000000000000000000000000000001" + .try_into() + .unwrap(); + let pk = sk.public_key(); + assert_eq!( + pk.to_hex(), + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + ); + let sig = Secp256k1Signature::from_str("00eb8dcd5bb841430dd0a6f45565a1b8bdb4a204eb868832cd006f963a89a662813ab844a542fcdbfda4086a83fbbde516214113051b9c8e42a206c98d564d7122").unwrap(); + + assert_eq!(sk.sign(&msg), sig); + assert!(pk.is_valid(&sig, &msg)) + } + + #[test] + fn bytes_roundtrip() { + let bytes = hex::decode("0000000000000000000000000000000000000000000000000000000000000001") + .unwrap(); + assert_eq!( + Secp256k1PrivateKey::from_bytes(bytes.as_slice()) + .unwrap() + .to_bytes(), + bytes.as_slice() + ); + } + + #[test] + fn hex_roundtrip() { + let hex = "0000000000000000000000000000000000000000000000000000000000000001"; + assert_eq!(Secp256k1PrivateKey::from_str(hex).unwrap().to_hex(), hex); + } + + #[test] + fn invalid_hex() { + assert_eq!( + Secp256k1PrivateKey::from_str("not hex"), + Err(Error::InvalidSecp256k1PrivateKeyFromString) + ); + } + + #[test] + fn invalid_hex_too_short() { + assert_eq!( + Secp256k1PrivateKey::from_str("dead"), + Err(Error::InvalidSecp256k1PrivateKeyFromString) + ); + } + + #[test] + fn invalid_bytes() { + assert_eq!( + Secp256k1PrivateKey::from_bytes(&[0u8] as &[u8]), + Err(Error::InvalidSecp256k1PrivateKeyFromBytes) + ); + } + + #[test] + fn invalid_too_large() { + assert_eq!( + Secp256k1PrivateKey::from_bytes(&[0xFFu8; 32]), + Err(Error::InvalidSecp256k1PrivateKeyFromBytes) + ); + } + + #[test] + fn invalid_zero() { + assert_eq!( + Secp256k1PrivateKey::from_bytes(&[0u8; 32]), + Err(Error::InvalidSecp256k1PrivateKeyFromBytes) + ); + } + + #[test] + fn equality() { + assert_eq!( + Secp256k1PrivateKey::from_str( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + .unwrap(), + Secp256k1PrivateKey::from_str( + "0000000000000000000000000000000000000000000000000000000000000001" + ) + .unwrap() + ); + } + + #[test] + fn debug() { + let hex = "0000000000000000000000000000000000000000000000000000000000000001"; + assert_eq!( + format!("{:?}", Secp256k1PrivateKey::from_str(hex).unwrap()), + hex + ); + } + + #[test] + fn from_hex32_bytes() { + let str = "0000000000000000000000000000000000000000000000000000000000000001"; + let hex32 = Hex32Bytes::from_hex(str).unwrap(); + let key = Secp256k1PrivateKey::from_hex32_bytes(hex32).unwrap(); + assert_eq!(key.to_hex(), str); + } + + #[test] + fn try_from_bytes() { + let str = "0000000000000000000000000000000000000000000000000000000000000001"; + let vec = hex::decode(str).unwrap(); + let key = Secp256k1PrivateKey::try_from(vec.as_slice()).unwrap(); + assert_eq!(key.to_hex(), str); + } + + #[test] + fn generate_new() { + let mut set: HashSet> = HashSet::new(); + let n = 100; + for _ in 0..n { + let key = Secp256k1PrivateKey::new(); + let bytes = key.to_bytes(); + assert_eq!(bytes.len(), 32); + set.insert(bytes); + } + assert_eq!(set.len(), n); + } + + #[test] + fn placeholder() { + assert_eq!( + Secp256k1PrivateKey::placeholder().public_key().to_hex(), + "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" + ); + } +} diff --git a/wallet_kit_common/src/types/keys/secp256k1/public_key.rs b/wallet_kit_common/src/types/keys/secp256k1/public_key.rs new file mode 100644 index 00000000..ceaa281b --- /dev/null +++ b/wallet_kit_common/src/types/keys/secp256k1/public_key.rs @@ -0,0 +1,214 @@ +use crate::{ + error::key_error::KeyError as Error, types::keys::secp256k1::private_key::Secp256k1PrivateKey, +}; +use bip32::secp256k1::PublicKey as BIP32Secp256k1PublicKey; +use radix_engine_common::crypto::{Hash, Secp256k1PublicKey as EngineSecp256k1PublicKey}; +use serde::{Deserialize, Serialize}; +use std::fmt::{Debug, Formatter}; +use transaction::{signing::secp256k1::Secp256k1Signature, validation::verify_secp256k1}; + +/// A `secp256k1` public key used to verify cryptographic signatures (ECDSA signatures). +#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Secp256k1PublicKey(EngineSecp256k1PublicKey); + +impl Secp256k1PublicKey { + pub(crate) fn from_engine(engine: EngineSecp256k1PublicKey) -> Result { + BIP32Secp256k1PublicKey::from_sec1_bytes(engine.to_vec().as_slice()) + .map(|_| Self(engine)) + .map_err(|_| Error::InvalidSecp256k1PublicKeyPointNotOnCurve) + } + + pub fn to_bytes(&self) -> Vec { + self.0.to_vec() + } + + pub fn to_hex(&self) -> String { + hex::encode(self.to_bytes()) + } + + /// Verifies an ECDSA signature over Secp256k1. + pub fn is_valid(&self, signature: &Secp256k1Signature, for_hash: &Hash) -> bool { + verify_secp256k1(for_hash, &self.0, signature) + } +} + +impl TryFrom<&[u8]> for Secp256k1PublicKey { + type Error = crate::error::key_error::KeyError; + + fn try_from(slice: &[u8]) -> Result { + EngineSecp256k1PublicKey::try_from(slice) + .map_err(|_| Error::InvalidSecp256k1PublicKeyFromBytes) + .and_then(|pk| Self::from_engine(pk)) + } +} + +impl TryInto for &str { + type Error = crate::error::key_error::KeyError; + + fn try_into(self) -> Result { + Secp256k1PublicKey::from_str(self) + } +} + +impl Secp256k1PublicKey { + pub fn from_str(hex: &str) -> Result { + hex::decode(hex) + .map_err(|_| Error::InvalidSecp256k1PublicKeyFromString) + .and_then(|b| Secp256k1PublicKey::try_from(b.as_slice())) + } +} + +impl Debug for Secp256k1PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.to_hex()) + } +} + +impl Secp256k1PublicKey { + /// A placeholder used to facilitate unit tests. + pub fn placeholder() -> Self { + Self::placeholder_alice() + } + + pub fn placeholder_alice() -> Self { + Secp256k1PrivateKey::placeholder_alice().public_key() + } + + pub fn placeholder_bob() -> Self { + Secp256k1PrivateKey::placeholder_bob().public_key() + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use super::Secp256k1PublicKey; + use crate::{error::key_error::KeyError as Error, json::assert_json_value_eq_after_roundtrip}; + use serde_json::json; + + #[test] + fn from_str() { + assert!(Secp256k1PublicKey::from_str( + "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" + ) + .is_ok()); + } + + #[test] + fn bytes_roundtrip() { + let bytes: &[u8] = &[ + 0x02, 0x51, 0x7b, 0x88, 0x91, 0x6e, 0x7f, 0x31, 0x5b, 0xb6, 0x82, 0xf9, 0x92, 0x6b, + 0x14, 0xbc, 0x67, 0xa0, 0xe4, 0x24, 0x6f, 0x8a, 0x41, 0x9b, 0x98, 0x62, 0x69, 0xe1, + 0xa7, 0xe6, 0x1f, 0xff, 0xa7, + ]; + let key = Secp256k1PublicKey::try_from(bytes).unwrap(); + assert_eq!( + key.to_hex(), + "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" + ); + assert_eq!(key.to_bytes(), bytes); + } + + #[test] + fn placeholder_alice() { + assert_eq!( + Secp256k1PublicKey::placeholder_alice().to_hex(), + "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" + ); + } + + #[test] + fn placeholder_bob() { + assert_eq!( + Secp256k1PublicKey::placeholder_bob().to_hex(), + "033083620d1596d3f8988ff3270e42970dd2a031e2b9b6488052a4170ff999f3e8" + ); + } + + #[test] + fn invalid_hex_str() { + assert_eq!( + Secp256k1PublicKey::from_str("not a valid hex string"), + Err(Error::InvalidSecp256k1PublicKeyFromString) + ); + } + + #[test] + fn invalid_str_too_short() { + assert_eq!( + Secp256k1PublicKey::from_str("dead"), + Err(Error::InvalidSecp256k1PublicKeyFromBytes) + ); + } + + #[test] + fn invalid_bytes() { + assert_eq!( + Secp256k1PublicKey::try_from(&[0u8] as &[u8]), + Err(Error::InvalidSecp256k1PublicKeyFromBytes) + ); + } + + #[test] + fn invalid_key_not_on_curve() { + assert_eq!( + Secp256k1PublicKey::from_str( + "99deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ), + Err(Error::InvalidSecp256k1PublicKeyPointNotOnCurve) + ); + } + + #[test] + fn debug() { + assert_eq!( + format!("{:?}", Secp256k1PublicKey::placeholder_alice()), + "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7" + ); + } + + #[test] + fn json() { + let model = Secp256k1PublicKey::placeholder(); + assert_json_value_eq_after_roundtrip( + &model, + json!("02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7"), + ) + } + + #[test] + fn try_into_from_str() { + let str = "02517b88916e7f315bb682f9926b14bc67a0e4246f8a419b986269e1a7e61fffa7"; + let key: Secp256k1PublicKey = str.try_into().unwrap(); + assert_eq!(key.to_hex(), str); + } + + #[test] + fn inequality() { + assert_ne!( + Secp256k1PublicKey::placeholder_alice(), + Secp256k1PublicKey::placeholder_bob() + ); + } + + #[test] + fn equality() { + assert_eq!( + Secp256k1PublicKey::placeholder_alice(), + Secp256k1PublicKey::placeholder_alice() + ); + } + + #[test] + fn hash() { + assert_eq!( + BTreeSet::from_iter([ + Secp256k1PublicKey::placeholder_alice(), + Secp256k1PublicKey::placeholder_alice() + ]) + .len(), + 1 + ); + } +} diff --git a/hierarchical_deterministic/src/derivation/slip10_curve.rs b/wallet_kit_common/src/types/keys/slip10_curve.rs similarity index 72% rename from hierarchical_deterministic/src/derivation/slip10_curve.rs rename to wallet_kit_common/src/types/keys/slip10_curve.rs index 1cf41930..0c7f9f37 100644 --- a/hierarchical_deterministic/src/derivation/slip10_curve.rs +++ b/wallet_kit_common/src/types/keys/slip10_curve.rs @@ -1,9 +1,15 @@ use serde::{Deserialize, Serialize}; +/// Elliptic Curves which the SLIP10 derivation algorithm supports. +/// +/// We use SLIP10 for hierarchical deterministic derivation since we +/// prefer using Curve25519 - which is incompatible with BIP32 (BIP44). +/// +/// For for information see [SLIP10 reference](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "camelCase")] pub enum SLIP10Curve { - /// Curve25519 or Ed25519 + /// Curve25519 which we use for Ed25519 for EdDSA signatures. Curve25519, /// The bitcoin curve, used by Radix Olympia and still valid @@ -14,7 +20,8 @@ pub enum SLIP10Curve { #[cfg(test)] mod tests { use serde_json::json; - use wallet_kit_common::json::{ + + use crate::json::{ assert_json_roundtrip, assert_json_value_eq_after_roundtrip, assert_json_value_ne_after_roundtrip, }; diff --git a/wallet_kit_common/src/types/mod.rs b/wallet_kit_common/src/types/mod.rs index ffb4b228..a672e5b0 100644 --- a/wallet_kit_common/src/types/mod.rs +++ b/wallet_kit_common/src/types/mod.rs @@ -1 +1,2 @@ pub mod hex_32bytes; +pub mod keys;