diff --git a/libwallet/Cargo.toml b/libwallet/Cargo.toml index 4f1e9ec..da3cc32 100644 --- a/libwallet/Cargo.toml +++ b/libwallet/Cargo.toml @@ -35,8 +35,7 @@ serde_json = {version = "1.0", default-features = false, features = ["alloc"]} dirs = "4.0" [features] -# The library has no default features but this can be uncommented during development -# default = [ "std", "substrate", "vault_simple", "vault_os", "vault_pass", "mnemonic", "util_pin", "rand", ] +# default = ["std", "substrate", "vault_simple", "mnemonic", "rand", "vault_pass", "vault_os", "util_pin"] rand = ["rand_core", "schnorrkel?/getrandom"] sr25519 = ["dep:schnorrkel"] std = [ @@ -46,6 +45,7 @@ substrate = ["sr25519"] util_pin = ["pbkdf2", "hmac", "sha2"] vault_os = ["keyring"] vault_pass = ["prs-lib"] +vault_simple = [] [workspace] members = [ diff --git a/libwallet/examples/account_generation.rs b/libwallet/examples/account_generation.rs index 6eda8e6..c34b8c4 100644 --- a/libwallet/examples/account_generation.rs +++ b/libwallet/examples/account_generation.rs @@ -1,7 +1,7 @@ -use libwallet::{self, vault}; +use libwallet::{self, vault, Account}; use std::env; -type Wallet = libwallet::Wallet; +type Wallet = libwallet::Wallet>; #[async_std::main] async fn main() -> Result<(), Box> { @@ -15,10 +15,10 @@ async fn main() -> Result<(), Box> { }; let mut wallet = Wallet::new(vault); - wallet.unlock(None).await?; + wallet.unlock(None, None).await?; let account = wallet.default_account(); println!("Secret phrase: \"{phrase}\""); - println!("Default Account: 0x{account}"); + println!("Default Account: 0x{:?}", account.unwrap()); Ok(()) } diff --git a/libwallet/examples/persisted_in_keyring.rs b/libwallet/examples/persisted_in_keyring.rs index e7e01d6..d3a1840 100644 --- a/libwallet/examples/persisted_in_keyring.rs +++ b/libwallet/examples/persisted_in_keyring.rs @@ -2,7 +2,7 @@ use libwallet::{self, vault, Language}; use std::{env, error::Error}; -type Wallet = libwallet::Wallet; +type Wallet = libwallet::Wallet>; const TEST_USER: &str = "test_user"; @@ -11,12 +11,15 @@ async fn main() -> Result<(), Box> { let pin = env::args().nth(1); let pin = pin.as_ref().map(String::as_str); - let vault = vault::OSKeyring::new(TEST_USER, Language::default()); + let vault = vault::OSKeyring::::new(TEST_USER, Language::default()); + let mut wallet = Wallet::new(vault); - wallet.unlock(pin).await?; + + + wallet.unlock(None, pin).await?; let account = wallet.default_account(); - println!("Default account: {}", account); + println!("Default account: {}", account.unwrap()); Ok(()) } diff --git a/libwallet/examples/persisted_in_pass.rs b/libwallet/examples/persisted_in_pass.rs index ba88db8..9c1b173 100644 --- a/libwallet/examples/persisted_in_pass.rs +++ b/libwallet/examples/persisted_in_pass.rs @@ -1,7 +1,8 @@ use dirs::home_dir; use libwallet::{self, vault::Pass, Language}; use std::error::Error; -type Wallet = libwallet::Wallet; +type PassVault = Pass; +type Wallet = libwallet::Wallet; #[async_std::main] async fn main() -> Result<(), Box> { @@ -13,10 +14,10 @@ async fn main() -> Result<(), Box> { let vault = Pass::new(store_path.to_str().unwrap(), Language::default()); let mut wallet = Wallet::new(vault); - wallet.unlock(account).await?; + wallet.unlock(None, account).await?; let account = wallet.default_account(); - println!("Default account: {}", account); + println!("Default account: {}", account.unwrap()); Ok(()) } diff --git a/libwallet/src/account.rs b/libwallet/src/account.rs index 197cec5..f6d3d9a 100644 --- a/libwallet/src/account.rs +++ b/libwallet/src/account.rs @@ -1,103 +1,9 @@ +use core::fmt::{Debug, Display}; + use crate::{ - any::{self, AnySignature}, - Derive, Network, Pair, Public, RootAccount, + Public, Signer, }; -use arrayvec::ArrayString; -// use regex::Regex; -// use sp_core::crypto::DeriveJunction; - -const MAX_PATH_LEN: usize = 16; - -/// Account is an abstration around public/private key pairs that are more convenient to use and -/// can hold extra metadata. Accounts are constructed by the wallet and are used to sign messages. -#[derive(Debug)] -pub struct Account { - pair: Option, - network: Network, - path: ArrayString, - name: ArrayString<{ MAX_PATH_LEN - 2 }>, -} - -impl Account { - pub(crate) fn new<'a>(name: impl Into>) -> Self { - let n = name.into().unwrap_or_else(|| "default"); - let mut path = ArrayString::from("//").unwrap(); - path.push_str(&n); - Account { - pair: None, - network: Network::default(), - name: ArrayString::from(&n).expect("short name"), - path, - } - } - - pub fn switch_network(self, net: impl Into) -> Self { - Account { - network: net.into(), - ..self - } - } - - pub fn name(&self) -> &str { - &self.name - } - - pub fn public(&self) -> impl Public { - self.pair.as_ref().expect("account unlocked").public() - } - - pub fn network(&self) -> &Network { - &self.network - } - - pub fn is_locked(&self) -> bool { - self.pair.is_none() - } - - pub(crate) fn unlock(&mut self, root: &RootAccount) -> &Self { - if self.is_locked() { - self.pair = Some(root.derive(&self.path)); - } - self - } -} - -impl crate::Signer for Account { - type Signature = AnySignature; - - fn sign_msg>(&self, msg: M) -> Self::Signature { - self.pair.as_ref().expect("account unlocked").sign_msg(msg) - } - - fn verify>(&self, msg: M, sig: &[u8]) -> bool { - self.pair - .as_ref() - .expect("account unlocked") - .verify(msg, sig) - } -} - -#[cfg(feature = "serde")] -impl serde::Serialize for Account { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - - let mut state = serializer.serialize_struct("Account", 1)?; - state.serialize_field("network", &self.network)?; - state.serialize_field("path", self.path.as_str())?; - state.serialize_field("name", self.name.as_str())?; - state.end() - } -} -impl core::fmt::Display for Account { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - for byte in self.public().as_ref() { - write!(f, "{:02x}", byte)?; - } - Ok(()) - } +pub trait Account: Signer + Display { + fn public(&self) -> impl Public; } diff --git a/libwallet/src/key_pair.rs b/libwallet/src/key_pair.rs index aeaf0c2..9004e82 100644 --- a/libwallet/src/key_pair.rs +++ b/libwallet/src/key_pair.rs @@ -1,7 +1,9 @@ -use core::fmt::Debug; +use core::{convert::TryFrom, fmt::Debug}; pub use derive::Derive; +use self::any::AnySignature; + type Bytes = [u8; N]; /// A key pair with a public key @@ -24,12 +26,13 @@ impl Signature for Bytes {} /// Something that can sign messages pub trait Signer { type Signature: Signature; - fn sign_msg>(&self, msg: M) -> Self::Signature; - fn verify>(&self, msg: M, sig: &[u8]) -> bool; + async fn sign_msg(&self, data: impl AsRef<[u8]>) -> Result; + async fn verify(&self, msg: impl AsRef<[u8]>, sig: impl AsRef<[u8]>) -> bool; } - /// Wrappers to represent any supported key pair. pub mod any { + use crate::Signer; + use super::{Public, Signature}; use core::fmt; @@ -91,19 +94,17 @@ pub mod any { impl super::Signer for Pair { type Signature = AnySignature; - fn sign_msg>(&self, msg: M) -> Self::Signature { + async fn sign_msg(&self, msg: impl AsRef<[u8]>) -> Result { match self { #[cfg(feature = "sr25519")] - Pair::Sr25519(p) => p.sign_msg(msg).into(), + Pair::Sr25519(p) => Ok(p.sign_msg(msg).await?.into()), } } - fn verify>(&self, msg: M, sig: &[u8]) -> bool { + async fn verify(&self, msg: impl AsRef<[u8]>, sig: impl AsRef<[u8]>) -> bool { match self { #[cfg(feature = "sr25519")] - Pair::Sr25519(p) => super::Signer::verify(p, msg, sig), - #[cfg(not(feature = "sr25519"))] - Pair::_None => unreachable!(), + Pair::Sr25519(p) => super::Signer::verify(p, msg, sig).await, } } } @@ -145,6 +146,7 @@ pub mod any { #[cfg(not(feature = "sr25519"))] _None, } + impl AsRef<[u8]> for AnySignature { fn as_ref(&self) -> &[u8] { match self { @@ -198,13 +200,13 @@ pub mod sr25519 { impl Signer for Pair { type Signature = Signature; - fn sign_msg>(&self, msg: M) -> Self::Signature { + async fn sign_msg(&self, msg: impl AsRef<[u8]>) -> Result { let context = signing_context(SIGNING_CTX); - self.sign(context.bytes(msg.as_ref())).to_bytes() + Ok(self.sign(context.bytes(msg.as_ref())).to_bytes()) } - fn verify>(&self, msg: M, sig: &[u8]) -> bool { - let sig = match schnorrkel::Signature::from_bytes(sig) { + async fn verify(&self, msg: impl AsRef<[u8]>, sig: impl AsRef<[u8]>) -> bool { + let sig = match schnorrkel::Signature::from_bytes(sig.as_ref()) { Ok(s) => s, Err(_) => return false, }; @@ -310,6 +312,7 @@ pub mod sr25519 { ] { let phrase = Mnemonic::from_phrase(phrase).unwrap(); let seed = Pin::from("").protect::<64>(&phrase.entropy()); + let root: super::Pair = Pair::from_bytes(&seed); let derived = root.derive(path); assert_eq!(&derived.public(), pubkey); diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 2f89e4c..54a38a1 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -9,23 +9,26 @@ compile_error!("Enable at least one type of signature algorithm"); mod account; mod key_pair; +pub mod util; + #[cfg(feature = "substrate")] mod substrate_ext; +pub use account::Account; use arrayvec::ArrayVec; -use core::{convert::TryInto, fmt}; +use core::{cell::RefCell, convert::TryInto, fmt}; use key_pair::any::AnySignature; #[cfg(feature = "mnemonic")] use mnemonic; -pub use account::Account; pub use key_pair::*; #[cfg(feature = "mnemonic")] pub use mnemonic::{Language, Mnemonic}; -pub use vault::{RootAccount, Vault}; +pub use vault::Vault; pub mod vault; + const MSG_MAX_SIZE: usize = u8::MAX as usize; type Message = ArrayVec; @@ -42,8 +45,8 @@ type Message = ArrayVec; pub struct Wallet { vault: V, is_locked: bool, - default_account: Account, - accounts: ArrayVec, + default_account: Option, + accounts: ArrayVec, pending_sign: ArrayVec<(Message, Option), M>, // message -> account index or default } @@ -55,7 +58,7 @@ where pub fn new(vault: V) -> Self { Wallet { vault, - default_account: Account::new(None), + default_account: None, accounts: ArrayVec::new_const(), pending_sign: ArrayVec::new(), is_locked: true, @@ -63,8 +66,8 @@ where } /// Get the account currently set as default - pub fn default_account(&self) -> &Account { - &self.default_account + pub fn default_account(&self) -> Option<&V::Account> { + self.default_account.map(|x| &self.accounts[x as usize]) } /// Use credentials to unlock the vault. @@ -72,29 +75,38 @@ where /// ``` /// # use libwallet::{Wallet, Error, vault, Vault}; /// # use std::convert::TryInto; - /// # type Result = std::result::Result<(), Error<::Error>>; + /// # type SimpleVault = vault::Simple; + /// # type Result = std::result::Result<(), Error<::Error>>; /// # #[async_std::main] async fn main() -> Result { - /// # let vault = vault::Simple::generate(&mut rand_core::OsRng); + /// # let vault = SimpleVault::generate(&mut rand_core::OsRng); /// let mut wallet: Wallet<_> = Wallet::new(vault); /// if wallet.is_locked() { - /// wallet.unlock(None).await?; + /// wallet.unlock(None, None).await?; /// } /// /// assert!(!wallet.is_locked()); /// # Ok(()) /// # } /// ``` - pub async fn unlock(&mut self, cred: impl Into) -> Result<(), Error> { + pub async fn unlock( + &mut self, + account: V::Id, + cred: impl Into, + ) -> Result<(), Error> { if self.is_locked() { let vault = &mut self.vault; - let def = &mut self.default_account; - vault - .unlock(cred, |root| { - def.unlock(root); - }) + let signer = vault + .unlock(account, cred) .await - .map_err(Error::Vault)?; + .map_err(|e| Error::Vault(e))?; + + if self.default_account.is_none() { + self.default_account = Some(0); + } + + self.accounts.push(signer); + self.is_locked = false; } Ok(()) @@ -110,30 +122,37 @@ where /// /// ``` /// # use libwallet::{Wallet, vault, Error, Signer, Vault}; - /// # type Result = std::result::Result<(), Error<::Error>>; + /// # type SimpleVault = vault::Simple; + /// # type Result = std::result::Result<(), Error<::Error>>; /// # #[async_std::main] async fn main() -> Result { - /// # let vault = vault::Simple::generate(&mut rand_core::OsRng); + /// # let vault = SimpleVault::generate(&mut rand_core::OsRng); /// let mut wallet: Wallet<_> = Wallet::new(vault); - /// wallet.unlock(None).await?; + /// wallet.unlock(None, None).await?; /// /// let msg = &[0x12, 0x34, 0x56]; - /// let signature = wallet.sign(msg); + /// let signature = wallet.sign(msg).await.expect("it must sign"); /// - /// assert!(wallet.default_account().verify(msg, signature.as_ref())); + /// assert!(wallet.default_account().expect("it must have a default signer").verify(msg, signature.as_ref()).await); /// # Ok(()) } /// ``` - pub fn sign(&self, message: &[u8]) -> impl Signature { + pub async fn sign(&self, message: &[u8]) -> Result { assert!(!self.is_locked()); - self.default_account().sign_msg(message) + + let Some(signer) = self.default_account() else { + return Err(()); + }; + + signer.sign_msg(message).await } /// Save data to be signed some time later. /// /// ``` /// # use libwallet::{Wallet, vault, Error, Vault}; - /// # type Result = std::result::Result<(), Error<::Error>>; + /// # type SimpleVault = vault::Simple; + /// # type Result = std::result::Result<(), Error<::Error>>; /// # #[async_std::main] async fn main() -> Result { - /// # let vault = vault::Simple::generate(&mut rand_core::OsRng); + /// # let vault = SimpleVault::generate(&mut rand_core::OsRng); /// let mut wallet: Wallet<_> = Wallet::new(vault); /// wallet.sign_later(&[0x01, 0x02, 0x03]); /// @@ -155,38 +174,42 @@ where /// /// ``` /// # use libwallet::{Wallet, vault, Error, Vault}; - /// # type Result = std::result::Result<(), Error<::Error>>; + /// # type SimpleVault = vault::Simple; + /// # type Result = std::result::Result<(), Error<::Error>>; /// # #[async_std::main] async fn main() -> Result { - /// # let vault = vault::Simple::generate(&mut rand_core::OsRng); + /// # let vault = SimpleVault::generate(&mut rand_core::OsRng); /// let mut wallet: Wallet<_> = Wallet::new(vault); - /// wallet.unlock(None).await?; + /// wallet.unlock(None, None).await?; /// /// wallet.sign_later(&[0x01, 0x02, 0x03]); /// wallet.sign_later(&[0x04, 0x05, 0x06]); - /// let signatures = wallet.sign_pending(); + /// let signatures = wallet.sign_pending().await.expect("it must sign"); /// /// assert_eq!(signatures.len(), 2); /// assert_eq!(wallet.pending().count(), 0); /// # Ok(()) } /// ``` - pub fn sign_pending(&mut self) -> ArrayVec { + pub async fn sign_pending(&mut self) -> Result, M>, ()> { let mut signatures = ArrayVec::new(); for (msg, a) in self.pending_sign.take() { - let account = a + let signer = a .map(|idx| self.account(idx)) - .unwrap_or_else(|| self.default_account()); - signatures.push(account.sign_msg(&msg)); + .unwrap_or_else(|| self.default_account().expect("Signer not set")); + + let message = signer.sign_msg(&msg).await?; + signatures.push(message); } - signatures + Ok(signatures) } /// Iteratate over the messages pending for signature for all the accounts. /// /// ``` /// # use libwallet::{Wallet, vault, Error, Vault}; - /// # type Result = std::result::Result<(), Error<::Error>>; + /// # type SimpleVault = vault::Simple; + /// # type Result = std::result::Result<(), Error<::Error>>; /// # #[async_std::main] async fn main() -> Result { - /// # let vault = vault::Simple::generate(&mut rand_core::OsRng); + /// # let vault = SimpleVault::generate(&mut rand_core::OsRng); /// let mut wallet: Wallet<_> = Wallet::new(vault); /// wallet.sign_later(&[0x01]); /// wallet.sign_later(&[0x02]); @@ -199,17 +222,7 @@ where self.pending_sign.iter().map(|(msg, _)| msg.as_ref()) } - #[cfg(feature = "subaccounts")] - pub fn new_account(&mut self, name: &str, derivation_path: &str) -> Result<&Account> { - let root = self.root_account()?; - let subaccount = root - .derive_subaccount(name, derivation_path) - .ok_or(Error::DeriveError)?; - self.subaccounts.insert(name.to_string(), subaccount); - Ok(self.subaccounts.get(name).unwrap()) - } - - fn account(&self, idx: u8) -> &Account { + fn account(&self, idx: u8) -> &V::Account { &self.accounts[idx as usize] } } @@ -280,123 +293,3 @@ impl From for Error { } } -mod util { - use core::{iter, ops}; - - #[cfg(feature = "rand")] - pub fn random_bytes(rng: &mut R) -> [u8; S] - where - R: rand_core::CryptoRng + rand_core::RngCore, - { - let mut bytes = [0u8; S]; - rng.fill_bytes(&mut bytes); - bytes - } - - #[cfg(feature = "rand")] - pub fn gen_phrase(rng: &mut R, lang: mnemonic::Language) -> mnemonic::Mnemonic - where - R: rand_core::CryptoRng + rand_core::RngCore, - { - let seed = random_bytes::<_, 32>(rng); - let phrase = mnemonic::Mnemonic::from_entropy_in(lang, seed.as_ref()).expect("seed valid"); - phrase - } - - /// A simple pin credential that can be used to add some - /// extra level of protection to seeds stored in vaults - #[derive(Default, Copy, Clone)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct Pin(u16); - - impl Pin { - const LEN: usize = 4; - - #[cfg(feature = "util_pin")] - pub fn protect(&self, data: &[u8]) -> [u8; S] { - use hmac::Hmac; - use pbkdf2::pbkdf2; - use sha2::Sha512; - - let salt = { - let mut s = [0; 10]; - s.copy_from_slice(b"mnemonic\0\0"); - let [b1, b2] = self.to_le_bytes(); - s[8] = b1; - s[9] = b2; - s - }; - let mut seed = [0; S]; - // using same hashing strategy as Substrate to have some compatibility - // when pin is 0(no pin) we produce the same addresses - let len = self.eq(&0).then_some(salt.len() - 2).unwrap_or(salt.len()); - pbkdf2::>(data, &salt[..len], 2048, &mut seed); - seed - } - } - - // Use 4 chars long hex string as pin. i.e. "ABCD", "1234" - impl<'a> From<&'a str> for Pin { - fn from(s: &str) -> Self { - let l = s.len().min(Pin::LEN); - let chars = s - .chars() - .take(l) - .chain(iter::repeat('0').take(Pin::LEN - l)); - Pin(chars - .map(|c| c.to_digit(16).unwrap_or(0)) - .enumerate() - .fold(0, |pin, (i, d)| { - pin | ((d as u16) << (Pin::LEN - 1 - i) * Pin::LEN) - })) - } - } - - impl<'a> From> for Pin { - fn from(p: Option<&'a str>) -> Self { - p.unwrap_or("").into() - } - } - - impl From<()> for Pin { - fn from(_: ()) -> Self { - Self(0) - } - } - - impl From for Pin { - fn from(n: u16) -> Self { - Self(n) - } - } - - impl ops::Deref for Pin { - type Target = u16; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - #[test] - fn pin_parsing() { - for (s, expected) in [ - ("0000", 0), - // we only take the first 4 characters and ignore the rest - ("0000001", 0), - // non hex chars are ignored and defaulted to 0, here a,d are kept - ("zasdasjgkadg", 0x0A0D), - ("ABCD", 0xABCD), - ("1000", 0x1000), - ("000F", 0x000F), - ("FFFF", 0xFFFF), - ] { - let pin = Pin::from(s); - assert_eq!( - *pin, expected, - "(input:\"{}\", l:{:X} == r:{:X})", - s, *pin, expected - ); - } - } -} diff --git a/libwallet/src/util.rs b/libwallet/src/util.rs new file mode 100644 index 0000000..6d7b931 --- /dev/null +++ b/libwallet/src/util.rs @@ -0,0 +1,135 @@ +use core::{iter, ops}; +use crate::Pair; + +#[cfg(feature = "rand")] +pub fn random_bytes(rng: &mut R) -> [u8; S] +where + R: rand_core::CryptoRng + rand_core::RngCore, +{ + let mut bytes = [0u8; S]; + rng.fill_bytes(&mut bytes); + bytes +} + +#[cfg(feature = "rand")] +pub fn gen_phrase(rng: &mut R, lang: mnemonic::Language) -> mnemonic::Mnemonic +where + R: rand_core::CryptoRng + rand_core::RngCore, +{ + let seed = random_bytes::<_, 32>(rng); + let phrase = mnemonic::Mnemonic::from_entropy_in(lang, seed.as_ref()).expect("seed valid"); + phrase +} + +/// A simple pin credential that can be used to add some +/// extra level of protection to seeds stored in vaults +#[derive(Default, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Pin(u16); + +macro_rules! seed_from_entropy { + ($seed: ident, $pin: expr) => { + #[cfg(feature = "util_pin")] + let protected_seed = $pin.protect::<64>($seed); + #[cfg(feature = "util_pin")] + let $seed = &protected_seed; + #[cfg(not(feature = "util_pin"))] + let _ = &$pin; // use the variable to avoid warning + }; +} + +use arrayvec::ArrayString; +pub(crate) use seed_from_entropy; + +use crate::{any::{self, AnySignature}, Network, Public }; + +impl Pin { + const LEN: usize = 4; + + #[cfg(feature = "util_pin")] + pub fn protect(&self, data: &[u8]) -> [u8; S] { + use hmac::Hmac; + use pbkdf2::pbkdf2; + use sha2::Sha512; + + let salt = { + let mut s = [0; 10]; + s.copy_from_slice(b"mnemonic\0\0"); + let [b1, b2] = self.to_le_bytes(); + s[8] = b1; + s[9] = b2; + s + }; + let mut seed = [0; S]; + // using same hashing strategy as Substrate to have some compatibility + // when pin is 0(no pin) we produce the same addresses + let len = self.eq(&0).then_some(salt.len() - 2).unwrap_or(salt.len()); + pbkdf2::>(data, &salt[..len], 2048, &mut seed); + seed + } +} + +// Use 4 chars long hex string as pin. i.e. "ABCD", "1234" +impl<'a> From<&'a str> for Pin { + fn from(s: &str) -> Self { + let l = s.len().min(Pin::LEN); + let chars = s + .chars() + .take(l) + .chain(iter::repeat('0').take(Pin::LEN - l)); + Pin(chars + .map(|c| c.to_digit(16).unwrap_or(0)) + .enumerate() + .fold(0, |pin, (i, d)| { + pin | ((d as u16) << (Pin::LEN - 1 - i) * Pin::LEN) + })) + } +} + +impl<'a> From> for Pin { + fn from(p: Option<&'a str>) -> Self { + p.unwrap_or("").into() + } +} + +impl From<()> for Pin { + fn from(_: ()) -> Self { + Self(0) + } +} + +impl From for Pin { + fn from(n: u16) -> Self { + Self(n) + } +} + +impl ops::Deref for Pin { + type Target = u16; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[test] +fn pin_parsing() { + for (s, expected) in [ + ("0000", 0), + // we only take the first 4 characters and ignore the rest + ("0000001", 0), + // non hex chars are ignored and defaulted to 0, here a,d are kept + ("zasdasjgkadg", 0x0A0D), + ("ABCD", 0xABCD), + ("1000", 0x1000), + ("000F", 0x000F), + ("FFFF", 0xFFFF), + ] { + let pin = Pin::from(s); + assert_eq!( + *pin, expected, + "(input:\"{}\", l:{:X} == r:{:X})", + s, *pin, expected + ); + } +} diff --git a/libwallet/src/vault.rs b/libwallet/src/vault.rs index 97f9f92..e320904 100644 --- a/libwallet/src/vault.rs +++ b/libwallet/src/vault.rs @@ -11,70 +11,167 @@ pub use os::*; pub use pass::*; pub use simple::*; -use crate::{any, Derive}; +use crate::account::Account; -/// Abstration for storage of private keys that are protected by some credentials. +/// Abstraction for storage of private keys that are protected by some credentials. pub trait Vault { type Credentials; type Error; + type Id; + type Account: Account; - /// Use a set of credentials to make the guarded keys available to the user. - /// It returns a `Future` to allow for vaults that might take an arbitrary amount - /// of time getting the secret ready like waiting for some user physical interaction. - async fn unlock( + async fn unlock( &mut self, + account: Self::Id, cred: impl Into, - cb: impl FnMut(&RootAccount) -> T, - ) -> Result; + ) -> Result; } -/// The root account is a container of the key pairs stored in the vault and cannot be -/// used to sign messages directly, we always derive new key pairs from it to create -/// and use accounts with the wallet. -#[derive(Debug)] -pub struct RootAccount { - #[cfg(feature = "substrate")] - sub: crate::key_pair::sr25519::Pair, -} +mod utils { + const MAX_PATH_LEN: usize = 16; + use arrayvec::ArrayString; -impl RootAccount { - fn from_bytes(seed: &[u8]) -> Self { - #[cfg(not(feature = "substrate"))] - let _ = seed; - RootAccount { - #[cfg(feature = "substrate")] - sub: ::from_bytes(seed), - } + use crate::{account::Account, any::AnySignature, any, Derive, Network, Pair, Public}; + + + /// The root account is a container of the key pairs stored in the vault and cannot be + /// used to sign messages directly, we always derive new key pairs from it to create + /// and use accounts with the wallet. + #[derive(Debug)] + pub struct RootAccount { + #[cfg(feature = "substrate")] + sub: crate::key_pair::sr25519::Pair, } -} -impl<'a> Derive for &'a RootAccount { - type Pair = any::Pair; - - fn derive(&self, path: &str) -> Self::Pair - where - Self: Sized, - { - match &path[..2] { - #[cfg(feature = "substrate")] - "//" => self.sub.derive(path).into(), - "m/" => unimplemented!(), - #[cfg(feature = "substrate")] - _ => self.sub.derive("//default").into(), + impl RootAccount { + pub fn from_bytes(seed: &[u8]) -> Self { #[cfg(not(feature = "substrate"))] - _ => unreachable!(), + let _ = seed; + RootAccount { + #[cfg(feature = "substrate")] + sub: ::from_bytes(seed), + } } } -} -macro_rules! seed_from_entropy { - ($seed: ident, $pin: expr) => { - #[cfg(feature = "util_pin")] - let protected_seed = $pin.protect::<64>($seed); - #[cfg(feature = "util_pin")] - let $seed = &protected_seed; - #[cfg(not(feature = "util_pin"))] - let _ = &$pin; // use the variable to avoid warning - }; + impl<'a> Derive for &'a RootAccount { + type Pair = any::Pair; + + fn derive(&self, path: &str) -> Self::Pair + where + Self: Sized, + { + match &path[..2] { + #[cfg(feature = "substrate")] + "//" => self.sub.derive(path).into(), + "m/" => unimplemented!(), + #[cfg(feature = "substrate")] + _ => self.sub.derive("//default").into(), + #[cfg(not(feature = "substrate"))] + _ => unreachable!(), + } + } + } + + /// Account is an abstration around public/private key pairs that are more convenient to use and + /// can hold extra metadata. Accounts are constructed by the wallet and are used to sign messages. + #[derive(Debug)] + pub struct AccountSigner { + pair: Option, + network: Network, + path: ArrayString, + name: ArrayString<{ MAX_PATH_LEN - 2 }>, + } + + impl Account for AccountSigner { + fn public(&self) -> impl Public { + self.pair.as_ref().expect("account unlocked").public() + } + } + + impl AccountSigner { + pub(crate) fn new<'a>(name: impl Into>) -> Self { + let n = name.into().unwrap_or_else(|| "default"); + let mut path = ArrayString::from("//").unwrap(); + path.push_str(&n); + AccountSigner { + pair: None, + network: Network::default(), + name: ArrayString::from(&n).expect("short name"), + path, + } + } + + pub fn switch_network(self, net: impl Into) -> Self { + AccountSigner { + network: net.into(), + ..self + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn network(&self) -> &Network { + &self.network + } + + pub fn is_locked(&self) -> bool { + self.pair.is_none() + } + + pub(crate) fn unlock(mut self, root: &RootAccount) -> Self { + if self.is_locked() { + self.pair = Some(root.derive(&self.path)); + } + self + } + } + + impl crate::Signer for AccountSigner { + type Signature = AnySignature; + + async fn sign_msg(&self, msg: impl AsRef<[u8]>) -> Result { + Ok(self + .pair + .as_ref() + .expect("account unlocked") + .sign_msg(msg) + .await?) + } + + async fn verify(&self, msg: impl AsRef<[u8]>, sig: impl AsRef<[u8]>) -> bool { + self.pair + .as_ref() + .expect("account unlocked") + .verify(msg, sig) + .await + } + } + + #[cfg(feature = "serde")] + impl serde::Serialize for AccountSigner { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + let mut state = serializer.serialize_struct("Account", 1)?; + state.serialize_field("network", &self.network)?; + state.serialize_field("path", self.path.as_str())?; + state.serialize_field("name", self.name.as_str())?; + state.end() + } + } + + impl core::fmt::Display for AccountSigner { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + for byte in self.public().as_ref() { + write!(f, "{:02x}", byte)?; + } + Ok(()) + } + } } -pub(crate) use seed_from_entropy; diff --git a/libwallet/src/vault/os.rs b/libwallet/src/vault/os.rs index 614ef1d..c647103 100644 --- a/libwallet/src/vault/os.rs +++ b/libwallet/src/vault/os.rs @@ -1,20 +1,25 @@ +use core::marker::PhantomData; + use crate::{ mnemonic::{Language, Mnemonic}, util::{seed_from_entropy, Pin}, - RootAccount, Vault, + Vault, + vault::utils::{ RootAccount, AccountSigner } }; +use arrayvec::ArrayVec; use keyring; const SERVICE: &str = "libwallet_account"; /// A vault that stores keys in the default OS secure store -pub struct OSKeyring { +pub struct OSKeyring { entry: keyring::Entry, root: Option, auto_generate: Option, + _phantom: PhantomData } -impl OSKeyring { +impl OSKeyring { /// Create a new OSKeyring vault for the given user. /// The optional `lang` instructs the vault to generarte a backup phrase /// in the given language in case one does not exist. @@ -23,6 +28,7 @@ impl OSKeyring { entry: keyring::Entry::new(SERVICE, &uname), root: None, auto_generate: lang.into(), + _phantom: PhantomData::default() } } @@ -94,15 +100,17 @@ impl core::fmt::Display for Error { #[cfg(feature = "std")] impl std::error::Error for Error {} -impl Vault for OSKeyring { +impl> Vault for OSKeyring { type Credentials = Pin; type Error = Error; + type Id = Option; + type Account = AccountSigner; - async fn unlock( + async fn unlock( &mut self, + account: Self::Id, cred: impl Into, - mut cb: impl FnMut(&RootAccount) -> T, - ) -> Result { + ) -> Result { let pin = cred.into(); self.get_key(pin) .or_else(|err| { @@ -110,9 +118,10 @@ impl Vault for OSKeyring { .ok_or(err) .and_then(|l| self.generate(pin, l)) }) - .and_then(|r| { + .map(|r| { + let acc = AccountSigner::new(account.as_ref().map(|x| x.as_ref())).unlock(&r); self.root = Some(r); - Ok(cb(self.root.as_ref().unwrap())) + acc }) } } diff --git a/libwallet/src/vault/pass.rs b/libwallet/src/vault/pass.rs index de03ad6..a62ae56 100644 --- a/libwallet/src/vault/pass.rs +++ b/libwallet/src/vault/pass.rs @@ -1,3 +1,6 @@ +use core::marker::PhantomData; + +use arrayvec::ArrayVec; use mnemonic::Language; use prs_lib::{ crypto::{self, IsContext, Proto}, @@ -6,20 +9,23 @@ use prs_lib::{ }; use crate::{ + account, util::{seed_from_entropy, Pin}, - RootAccount, Vault, + vault::utils::{AccountSigner, RootAccount}, + Vault, }; /// A vault that stores secrets in a `pass` compatible repository -pub struct Pass { +pub struct Pass { store: Store, root: Option, auto_generate: Option, + _phantom_data: PhantomData, } const DEFAULT_DIR: &str = "libwallet_accounts/"; -impl Pass { +impl Pass { /// Create a new `Pass` vault in the given location. /// The optional `lang` instructs the vault to generarte a backup phrase /// in the given language in case one does not exist. @@ -30,6 +36,7 @@ impl Pass { store, root: None, auto_generate: lang.into(), + _phantom_data: Default::default(), } } @@ -132,15 +139,17 @@ impl From for PassCreds { } } -impl Vault for Pass { +impl> Vault for Pass { + type Id = Option; type Credentials = PassCreds; + type Account = AccountSigner; type Error = Error; - async fn unlock( + async fn unlock( &mut self, + path: Self::Id, creds: impl Into, - mut cb: impl FnMut(&RootAccount) -> T, - ) -> Result { + ) -> Result { let credentials = creds.into(); self.get_key(&credentials) @@ -149,9 +158,10 @@ impl Vault for Pass { .ok_or(err) .and_then(|l| self.generate(&credentials, l)) }) - .and_then(|r| { + .map(|r| { + let acc = AccountSigner::new(path.as_ref().map(|x| x.as_ref())).unlock(&r); self.root = Some(r); - Ok(cb(self.root.as_ref().unwrap())) + acc }) } } diff --git a/libwallet/src/vault/simple.rs b/libwallet/src/vault/simple.rs index 029d94c..ae003e5 100644 --- a/libwallet/src/vault/simple.rs +++ b/libwallet/src/vault/simple.rs @@ -1,25 +1,33 @@ -use super::seed_from_entropy; -use crate::util::Pin; -use crate::{RootAccount, Vault}; +use core::convert::TryInto; +use core::marker::PhantomData; + +use arrayvec::ArrayVec; + +use crate::util::{seed_from_entropy, Pin}; +use crate::{ + vault::utils::{AccountSigner, RootAccount}, + Derive, Vault, +}; /// A vault that holds secrets in memory -pub struct Simple { +pub struct Simple { locked: Option<[u8; 32]>, unlocked: Option<[u8; 32]>, + _phantom: PhantomData } -impl Simple { +impl Simple { /// A vault with a random seed, once dropped the the vault can't be restored /// /// ``` /// # use libwallet::{vault, Error, Derive, Pair, Vault}; - /// # type Result = std::result::Result<(), ::Error>; + /// # type SimpleVault = vault::Simple; + /// # type Result = std::result::Result<(), ::Error>; /// # #[async_std::main] async fn main() -> Result { - /// let mut vault = vault::Simple::generate(&mut rand_core::OsRng); - /// let root = vault.unlock(None, |root| { - /// println!("{}", root.derive("//default").public()); - /// }).await?; - /// # Ok(()) } + /// let mut vault = SimpleVault::generate(&mut rand_core::OsRng); + /// let root = vault.unlock(None, None).await?; + /// # Ok(()) + /// } /// ``` #[cfg(feature = "rand")] pub fn generate(rng: &mut R) -> Self @@ -29,6 +37,7 @@ impl Simple { Simple { locked: Some(crate::util::random_bytes::<_, 32>(rng)), unlocked: None, + _phantom: Default::default() } } @@ -57,6 +66,7 @@ impl Simple { Simple { locked: Some(entropy), unlocked: None, + _phantom: Default::default(), } } @@ -81,18 +91,21 @@ impl core::fmt::Display for Error { #[cfg(feature = "std")] impl std::error::Error for Error {} -impl Vault for Simple { +impl> Vault for Simple { type Credentials = Option; type Error = Error; + type Id = Option; + type Account = AccountSigner; - async fn unlock( + async fn unlock( &mut self, - credentials: impl Into, - mut cb: impl FnMut(&RootAccount) -> T, - ) -> Result { + path: Self::Id, + creds: impl Into, + ) -> Result { self.unlocked = self.locked.take(); - let pin = credentials.into(); - let root_account = &self.get_key(pin.unwrap_or_default())?; - Ok(cb(root_account)) + let pin = creds.into(); + let root_account = self.get_key(pin.unwrap_or_default())?; + let path = path.as_ref().map(|x| x.as_ref()); + Ok(AccountSigner::new(path).unlock(&root_account)) } }