diff --git a/cmd/crates/soroban-test/src/lib.rs b/cmd/crates/soroban-test/src/lib.rs index b1381be806..19478dd71d 100644 --- a/cmd/crates/soroban-test/src/lib.rs +++ b/cmd/crates/soroban-test/src/lib.rs @@ -27,6 +27,7 @@ use std::{ ffi::OsString, fmt::Display, path::{Path, PathBuf}, + sync::OnceLock, }; use assert_cmd::{assert::Assert, Command}; @@ -277,6 +278,7 @@ impl TestEnv { locator: config::locator::Args { global: false, config_dir, + cached_keys: OnceLock::new(), }, sign_with: config::sign_with::Args { sign_with_key: None, diff --git a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs index b0e667a3f0..db818e4b08 100644 --- a/cmd/crates/soroban-test/tests/it/integration/hello_world.rs +++ b/cmd/crates/soroban-test/tests/it/integration/hello_world.rs @@ -1,3 +1,5 @@ +use super::util::{deploy_hello, extend}; +use crate::integration::util::extend_contract; use soroban_cli::{ commands::{ contract::{self, fetch}, @@ -6,9 +8,7 @@ use soroban_cli::{ config::{locator, secret}, }; use soroban_test::{AssertExt, TestEnv, LOCAL_NETWORK_PASSPHRASE}; - -use super::util::{deploy_hello, extend}; -use crate::integration::util::extend_contract; +use std::sync::OnceLock; #[allow(clippy::too_many_lines)] #[tokio::test] @@ -101,6 +101,7 @@ async fn invoke_contract() { let config_locator = locator::Args { global: false, config_dir: Some(dir.to_path_buf()), + cached_keys: OnceLock::new(), }; config_locator diff --git a/cmd/soroban-cli/src/commands/keys/add.rs b/cmd/soroban-cli/src/commands/keys/add.rs index 8537d76779..6d99321e4c 100644 --- a/cmd/soroban-cli/src/commands/keys/add.rs +++ b/cmd/soroban-cli/src/commands/keys/add.rs @@ -10,7 +10,7 @@ use crate::{ secret::{self, Secret}, }, print::Print, - signer::secure_store, + signer::secure_store_entry::{self, SecureStoreEntry}, }; #[derive(thiserror::Error, Debug)] @@ -23,7 +23,7 @@ pub enum Error { Config(#[from] locator::Error), #[error(transparent)] - SecureStore(#[from] secure_store::Error), + SecureStoreEntry(#[from] secure_store_entry::Error), #[error(transparent)] SeedPhrase(#[from] sep5::error::Error), @@ -96,7 +96,7 @@ impl Cmd { let seed_phrase: SeedPhrase = secret_key.parse()?; - let secret = secure_store::save_secret(print, &self.name, &seed_phrase)?; + let secret = SecureStoreEntry::create_and_save(&self.name, &seed_phrase, print)?; Ok(secret.parse()?) } else { let prompt = "Type a secret key or 12/24 word seed phrase:"; diff --git a/cmd/soroban-cli/src/commands/keys/generate.rs b/cmd/soroban-cli/src/commands/keys/generate.rs index 37e41ff657..ccae9eb7fa 100644 --- a/cmd/soroban-cli/src/commands/keys/generate.rs +++ b/cmd/soroban-cli/src/commands/keys/generate.rs @@ -5,7 +5,12 @@ use super::super::config::{ secret::{self, Secret}, }; -use crate::{commands::global, config::address::KeyName, print::Print, signer::secure_store}; +use crate::{ + commands::global, + config::address::KeyName, + print::Print, + signer::secure_store_entry::{self, SecureStoreEntry}, +}; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -22,7 +27,7 @@ pub enum Error { IdentityAlreadyExists(String), #[error(transparent)] - SecureStore(#[from] secure_store::Error), + SecureStoreEntry(#[from] secure_store_entry::Error), } #[derive(Debug, clap::Parser, Clone)] @@ -115,7 +120,7 @@ impl Cmd { fn secret(&self, print: &Print) -> Result { let seed_phrase = self.seed_phrase()?; if self.secure_store { - let secret = secure_store::save_secret(print, &self.name, &seed_phrase)?; + let secret = SecureStoreEntry::create_and_save(&self.name, &seed_phrase, print)?; Ok(secret.parse()?) } else if self.as_secret { let secret: Secret = seed_phrase.into(); @@ -132,6 +137,8 @@ impl Cmd { #[cfg(test)] mod tests { + use std::sync::OnceLock; + use crate::config::{address::KeyName, key::Key, secret::Secret}; fn set_up_test() -> (super::locator::Args, super::Cmd) { @@ -139,6 +146,7 @@ mod tests { let locator = super::locator::Args { global: false, config_dir: Some(temp_dir.path().to_path_buf()), + cached_keys: OnceLock::new(), }; let cmd = super::Cmd { diff --git a/cmd/soroban-cli/src/config/key.rs b/cmd/soroban-cli/src/config/key.rs index 9d21ad0615..27ed9ab853 100644 --- a/cmd/soroban-cli/src/config/key.rs +++ b/cmd/soroban-cli/src/config/key.rs @@ -17,7 +17,7 @@ pub enum Error { Parse, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub enum Key { #[serde(rename = "public_key")] PublicKey(Public), @@ -85,7 +85,9 @@ impl From<&stellar_strkey::ed25519::PublicKey> for Key { } } -#[derive(Debug, PartialEq, Eq, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)] +#[derive( + Debug, Clone, PartialEq, Eq, serde_with::SerializeDisplay, serde_with::DeserializeFromStr, +)] pub struct Public(pub stellar_strkey::ed25519::PublicKey); impl FromStr for Public { @@ -111,7 +113,9 @@ impl From<&Public> for stellar_strkey::ed25519::MuxedAccount { } } -#[derive(Debug, PartialEq, Eq, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)] +#[derive( + Debug, Clone, PartialEq, Eq, serde_with::SerializeDisplay, serde_with::DeserializeFromStr, +)] pub struct MuxedAccount(pub stellar_strkey::ed25519::MuxedAccount); impl FromStr for MuxedAccount { @@ -143,13 +147,66 @@ impl TryFrom for Secret { #[cfg(test)] mod test { + use std::sync::Arc; + use super::*; fn round_trip(key: &Key) { let serialized = toml::to_string(&key).unwrap(); - println!("{serialized}"); let deserialized: Key = toml::from_str(&serialized).unwrap(); - assert_eq!(key, &deserialized); + + assert_key_equality(key, &deserialized); + } + + // using this fn instead of just doing assert_eq!(key, deserialized) because Secret::SecureStore keys contain a StellarEntry which contains a keyring::Entry + // keyring::Entry comes from the keyring crate which does not implement PartialEq + fn assert_key_equality(expected: &Key, actual: &Key) { + match (expected, actual) { + (Key::PublicKey(e), Key::PublicKey(a)) => { + assert_eq!(e, a); + } + (Key::MuxedAccount(e), Key::MuxedAccount(a)) => { + assert_eq!(e, a); + } + (Key::Secret(e), Key::Secret(a)) => match (e, a) { + ( + Secret::SecretKey { + secret_key: e_secret_key, + }, + Secret::SecretKey { + secret_key: a_secret_key, + }, + ) => { + assert_eq!(e_secret_key, a_secret_key); + } + ( + Secret::SeedPhrase { + seed_phrase: e_seed_phrase, + }, + Secret::SeedPhrase { + seed_phrase: a_seed_phrase, + }, + ) => { + assert_eq!(e_seed_phrase, a_seed_phrase); + } + (Secret::Ledger, Secret::Ledger) => todo!(), + ( + Secret::SecureStore { + entry_name: e_entry_name, + cached_entry: e_cached_entry, + }, + Secret::SecureStore { + entry_name: a_entry_name, + cached_entry: a_cached_entry, + }, + ) => { + assert_eq!(e_entry_name, a_entry_name); + assert!(Arc::ptr_eq(e_cached_entry, a_cached_entry)); + } + _ => panic!("keys are not equal {expected:?} != {actual:?}"), + }, + _ => panic!("keys are not equal {expected:?} != {actual:?}"), + } } #[test] diff --git a/cmd/soroban-cli/src/config/locator.rs b/cmd/soroban-cli/src/config/locator.rs index c09f69d750..2c1d27e58f 100644 --- a/cmd/soroban-cli/src/config/locator.rs +++ b/cmd/soroban-cli/src/config/locator.rs @@ -1,6 +1,8 @@ use directories::UserDirs; use itertools::Itertools; use serde::de::DeserializeOwned; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; use std::{ ffi::OsStr, fmt::Display, @@ -14,7 +16,7 @@ use stellar_strkey::{Contract, DecodeError}; use crate::{ commands::{global, HEADING_GLOBAL}, print::Print, - signer::secure_store, + signer::secure_store_entry::{self, SecureStoreEntry}, utils::find_config_dir, xdr, Pwd, }; @@ -96,7 +98,7 @@ pub enum Error { #[error("Key cannot {0} cannot overlap with contract alias")] KeyCannotOverlapWithContractAlias(String), #[error(transparent)] - SecureStore(#[from] secure_store::Error), + SecureStoreEntry(#[from] secure_store_entry::Error), #[error("Only private keys and seed phrases are supported for getting private keys {0}")] SecretKeyOnly(String), #[error(transparent)] @@ -105,6 +107,8 @@ pub enum Error { ProjectDirsError(), } +pub type CachedKeys = HashMap; + #[derive(Debug, clap::Args, Default, Clone)] #[group(skip)] pub struct Args { @@ -116,6 +120,10 @@ pub struct Args { /// Contains configuration files, aliases, and other persistent settings. #[arg(long, global = true, help_heading = HEADING_GLOBAL)] pub config_dir: Option, + + #[clap(skip)] + // This saves us from reading the same key from the file system more than once for one cmd + pub cached_keys: OnceLock>>, } pub enum Location { @@ -269,10 +277,30 @@ impl Args { KeyType::Identity.read_with_global(name, self) } + // read_key caches the Key after reading it from the config pub fn read_key(&self, key_or_name: &str) -> Result { - key_or_name + // check cache for key & return it if its there + if let Some(arc) = self.cached_keys.get() { + let map = arc.lock().unwrap(); + if let Some(k) = map.get(key_or_name) { + return Ok(k.clone()); + } + } + + // if its not in the cache, read it from config + let key = key_or_name .parse() - .or_else(|_| self.read_identity(key_or_name)) + .or_else(|_| self.read_identity(key_or_name))?; + + // get or initialize the cached keys + let arc = self + .cached_keys + .get_or_init(|| Arc::new(Mutex::new(HashMap::new()))); + let mut map = arc.lock().unwrap(); + // add the key to cached_keys + map.insert(key_or_name.to_string(), key.clone()); + + Ok(key) } pub fn get_secret_key(&self, key_or_name: &str) -> Result { @@ -305,8 +333,9 @@ impl Args { let print = Print::new(global_args.quiet); let identity = self.read_identity(name)?; - if let Key::Secret(Secret::SecureStore { entry_name }) = identity { - secure_store::delete_secret(&print, &entry_name)?; + if let Key::Secret(Secret::SecureStore { entry_name, .. }) = identity { + let secure_store_entry = SecureStoreEntry::new(entry_name, None)?; + secure_store_entry.delete_secret(&print)?; } print.infoln("Removing the key's cli config file"); diff --git a/cmd/soroban-cli/src/config/secret.rs b/cmd/soroban-cli/src/config/secret.rs index 6784f03fa1..805682a2e0 100644 --- a/cmd/soroban-cli/src/config/secret.rs +++ b/cmd/soroban-cli/src/config/secret.rs @@ -1,12 +1,19 @@ use serde::{Deserialize, Serialize}; -use std::str::FromStr; +use std::{ + str::FromStr, + sync::{Arc, OnceLock}, +}; use sep5::SeedPhrase; use stellar_strkey::ed25519::{PrivateKey, PublicKey}; use crate::{ print::Print, - signer::{self, ledger, secure_store, LocalKey, SecureStoreEntry, Signer, SignerKind}, + signer::{ + self, ledger, + secure_store_entry::{self, SecureStoreEntry}, + LocalKey, Signer, SignerKind, + }, utils, }; @@ -27,7 +34,7 @@ pub enum Error { #[error("Ledger does not reveal secret key")] LedgerDoesNotRevealSecretKey, #[error(transparent)] - SecureStore(#[from] secure_store::Error), + SecureStore(#[from] secure_store_entry::Error), #[error("Secure Store does not reveal secret key")] SecureStoreDoesNotRevealSecretKey, #[error(transparent)] @@ -54,13 +61,22 @@ pub struct Args { pub secure_store: bool, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum Secret { - SecretKey { secret_key: String }, - SeedPhrase { seed_phrase: String }, + SecretKey { + secret_key: String, + }, + SeedPhrase { + seed_phrase: String, + }, Ledger, - SecureStore { entry_name: String }, + SecureStore { + entry_name: String, + #[serde(skip)] + #[serde(default)] + cached_entry: Arc>, + }, } impl FromStr for Secret { @@ -77,9 +93,10 @@ impl FromStr for Secret { }) } else if s == "ledger" { Ok(Secret::Ledger) - } else if s.starts_with(secure_store::ENTRY_PREFIX) { + } else if s.starts_with(secure_store_entry::ENTRY_PREFIX) { Ok(Secret::SecureStore { entry_name: s.to_string(), + cached_entry: OnceLock::new().into(), }) } else { Err(Error::InvalidSecretOrSeedPhrase) @@ -127,8 +144,13 @@ impl Secret { } pub fn public_key(&self, index: Option) -> Result { - if let Secret::SecureStore { entry_name } = self { - Ok(secure_store::get_public_key(entry_name, index)?) + if let Secret::SecureStore { + entry_name, + cached_entry, + } = self + { + let entry = Self::cached_secure_store_entry(index, entry_name, cached_entry)?; + Ok(entry.get_public_key()?) } else { let key = self.key_pair(index)?; Ok(stellar_strkey::ed25519::PublicKey::from_payload( @@ -147,17 +169,36 @@ impl Secret { let hd_path: u32 = hd_path .unwrap_or_default() .try_into() - .expect("uszie bigger than u32"); + .expect("usize bigger than u32"); SignerKind::Ledger(ledger::new(hd_path).await?) } - Secret::SecureStore { entry_name } => SignerKind::SecureStore(SecureStoreEntry { - name: entry_name.clone(), - hd_path, - }), + Secret::SecureStore { + entry_name, + cached_entry, + } => { + let entry = Self::cached_secure_store_entry(hd_path, entry_name, cached_entry)?; + SignerKind::SecureStore(entry.clone()) + } }; Ok(Signer { kind, print }) } + fn cached_secure_store_entry( + hd_path: Option, + entry_name: &str, + cached_entry: &Arc>, + ) -> Result { + let entry = if let Some(e) = cached_entry.get() { + e.clone() + } else { + let e = SecureStoreEntry::new(entry_name.to_owned(), hd_path)?; + // It's fine if set fails because another thread initialized it concurrently. + let _ = cached_entry.set(e.clone()); + e + }; + Ok(entry) + } + pub fn key_pair(&self, index: Option) -> Result { Ok(utils::into_signing_key(&self.private_key(index)?)) } diff --git a/cmd/soroban-cli/src/signer/keyring.rs b/cmd/soroban-cli/src/signer/keyring.rs index 22b3d68843..9e34a05dea 100644 --- a/cmd/soroban-cli/src/signer/keyring.rs +++ b/cmd/soroban-cli/src/signer/keyring.rs @@ -1,3 +1,6 @@ +use std::sync::Arc; +use std::sync::Mutex; + use crate::print::Print; use ed25519_dalek::Signer; use keyring::Entry; @@ -15,19 +18,32 @@ pub enum Error { #[error("Secure Store keys are not allowed: additional-libs feature must be enabled")] FeatureNotEnabled, + + #[error("Mutex poisoned")] + MutexPoison, } +#[derive(Debug)] pub struct StellarEntry { + inner: Arc, +} + +#[derive(Debug)] +pub struct StellarEntryInner { name: String, #[cfg(feature = "additional-libs")] keyring: Entry, + cached_seed: Mutex>, } impl StellarEntry { pub fn new(name: &str) -> Result { Ok(StellarEntry { - name: name.to_string(), - keyring: Entry::new(name, &whoami::username())?, + inner: Arc::new(StellarEntryInner { + name: name.to_string(), + keyring: Entry::new(name, &whoami::username())?, + cached_seed: Mutex::new(None), + }), }) } @@ -35,12 +51,12 @@ impl StellarEntry { if let Ok(key) = self.get_public_key(None) { print.warnln(format!( "A key for {0} already exists in your operating system's secure store: {1}", - self.name, key + self.inner.name, key )); } else { print.infoln(format!( "Saving a new key to your operating system's secure store: {0}", - self.name + self.inner.name )); self.set_seed_phrase(seed_phrase)?; } @@ -50,14 +66,22 @@ impl StellarEntry { fn set_seed_phrase(&self, seed_phrase: SeedPhrase) -> Result<(), Error> { let mut data = seed_phrase.seed_phrase.into_phrase(); - self.keyring.set_password(&data)?; + self.inner.keyring.set_password(&data)?; data.zeroize(); Ok(()) } pub fn delete_seed_phrase(&self, print: &Print) -> Result<(), Error> { - match self.keyring.delete_credential() { - Ok(()) => Ok(()), + match self.inner.keyring.delete_credential() { + Ok(()) => { + // clear the cached seed + self.inner + .cached_seed + .lock() + .map_err(|_| Error::MutexPoison)? + .take(); + Ok(()) + } Err(e) => match e { keyring::Error::NoEntry => { print.infoln("This key was already removed from the secure store."); @@ -69,7 +93,15 @@ impl StellarEntry { } fn get_seed_phrase(&self) -> Result { - Ok(self.keyring.get_password()?.parse()?) + let mut guard = self.inner.cached_seed.lock().unwrap(); + + if let Some(seed_phrase) = &*guard { + return Ok(seed_phrase.clone()); + } + + let seed_phrase: SeedPhrase = self.inner.keyring.get_password()?.parse()?; + *guard = Some(seed_phrase.clone()); + Ok(seed_phrase) } fn use_key( diff --git a/cmd/soroban-cli/src/signer/mod.rs b/cmd/soroban-cli/src/signer/mod.rs index c699b30b33..a701f9ac46 100644 --- a/cmd/soroban-cli/src/signer/mod.rs +++ b/cmd/soroban-cli/src/signer/mod.rs @@ -1,9 +1,13 @@ -use crate::xdr::{ - self, AccountId, DecoratedSignature, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, - InvokeHostFunctionOp, Limits, Operation, OperationBody, PublicKey, ScAddress, ScMap, ScSymbol, - ScVal, Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry, - SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope, - TransactionV1Envelope, Uint256, VecM, WriteXdr, +use crate::{ + signer::secure_store_entry::SecureStoreEntry, + xdr::{ + self, AccountId, DecoratedSignature, Hash, HashIdPreimage, + HashIdPreimageSorobanAuthorization, InvokeHostFunctionOp, Limits, Operation, OperationBody, + PublicKey, ScAddress, ScMap, ScSymbol, ScVal, Signature, SignatureHint, + SorobanAddressCredentials, SorobanAuthorizationEntry, SorobanAuthorizedFunction, + SorobanCredentials, Transaction, TransactionEnvelope, TransactionV1Envelope, Uint256, VecM, + WriteXdr, + }, }; use ed25519_dalek::{ed25519::signature::Signer as _, Signature as Ed25519Signature}; use sha2::{Digest, Sha256}; @@ -13,8 +17,8 @@ use crate::{config::network::Network, print::Print, utils::transaction_hash}; pub mod ledger; #[cfg(feature = "additional-libs")] -mod keyring; -pub mod secure_store; +pub mod keyring; +pub mod secure_store_entry; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -39,7 +43,7 @@ pub enum Error { #[error("Returning a signature from Lab is not yet supported; Transaction can be found and submitted in lab")] ReturningSignatureFromLab, #[error(transparent)] - SecureStore(#[from] secure_store::Error), + SecureStoreEntry(#[from] secure_store_entry::Error), #[error(transparent)] Ledger(#[from] ledger::Error), #[error(transparent)] @@ -281,7 +285,9 @@ impl Signer { SignerKind::Local(local_key) => local_key.sign_payload(payload), SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"), SignerKind::Lab => Err(Error::ReturningSignatureFromLab), - SignerKind::SecureStore(secure_store_entry) => secure_store_entry.sign_payload(payload), + SignerKind::SecureStore(secure_store_entry) => { + Ok(secure_store_entry.sign_payload(payload)?) + } } } } @@ -326,31 +332,3 @@ impl Lab { Err(Error::ReturningSignatureFromLab) } } - -pub struct SecureStoreEntry { - pub name: String, - pub hd_path: Option, -} - -impl SecureStoreEntry { - pub fn get_public_key(&self) -> Result { - Ok(secure_store::get_public_key(&self.name, self.hd_path)?) - } - - pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { - let hint = SignatureHint( - secure_store::get_public_key(&self.name, self.hd_path)?.0[28..].try_into()?, - ); - - let signed_tx_hash = secure_store::sign_tx_data(&self.name, self.hd_path, &tx_hash)?; - - let signature = Signature(signed_tx_hash.clone().try_into()?); - Ok(DecoratedSignature { hint, signature }) - } - - pub fn sign_payload(&self, payload: [u8; 32]) -> Result { - let signed_bytes = secure_store::sign_tx_data(&self.name, self.hd_path, &payload)?; - let sig = Ed25519Signature::from_bytes(signed_bytes.as_slice().try_into()?); - Ok(sig) - } -} diff --git a/cmd/soroban-cli/src/signer/secure_store.rs b/cmd/soroban-cli/src/signer/secure_store.rs deleted file mode 100644 index d30cf3b501..0000000000 --- a/cmd/soroban-cli/src/signer/secure_store.rs +++ /dev/null @@ -1,95 +0,0 @@ -use sep5::SeedPhrase; -use stellar_strkey::ed25519::PublicKey; - -use crate::print::Print; - -#[cfg(feature = "additional-libs")] -use crate::signer::keyring::{self, StellarEntry}; - -pub(crate) const ENTRY_PREFIX: &str = "secure_store:"; - -pub use secure_store_impl::*; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[cfg(feature = "additional-libs")] - #[error(transparent)] - Keyring(#[from] keyring::Error), - - #[error("Storing an existing private key in Secure Store is not supported")] - DoesNotSupportPrivateKey, - - #[error(transparent)] - SeedPhrase(#[from] sep5::Error), - - #[error("Secure Store keys are not allowed: additional-libs feature must be enabled")] - FeatureNotEnabled, -} - -#[cfg(feature = "additional-libs")] -mod secure_store_impl { - use super::{Error, Print, PublicKey, SeedPhrase, StellarEntry, ENTRY_PREFIX}; - const ENTRY_SERVICE: &str = "org.stellar.cli"; - - pub fn get_public_key(entry_name: &str, index: Option) -> Result { - let entry = StellarEntry::new(entry_name)?; - Ok(entry.get_public_key(index)?) - } - - pub fn delete_secret(print: &Print, entry_name: &str) -> Result<(), Error> { - let entry = StellarEntry::new(entry_name)?; - Ok(entry.delete_seed_phrase(print)?) - } - - pub fn save_secret( - print: &Print, - entry_name: &str, - seed_phrase: &SeedPhrase, - ) -> Result { - // secure_store:org.stellar.cli: - let entry_name_with_prefix = format!("{ENTRY_PREFIX}{ENTRY_SERVICE}-{entry_name}"); - - let entry = StellarEntry::new(&entry_name_with_prefix)?; - entry.write(seed_phrase.clone(), print)?; - - Ok(entry_name_with_prefix) - } - - pub fn sign_tx_data( - entry_name: &str, - hd_path: Option, - data: &[u8], - ) -> Result, Error> { - let entry = StellarEntry::new(entry_name)?; - Ok(entry.sign_data(data, hd_path)?) - } -} - -#[cfg(not(feature = "additional-libs"))] -mod secure_store_impl { - use super::{Error, Print, PublicKey, SeedPhrase}; - - pub fn get_public_key(_entry_name: &str, _index: Option) -> Result { - Err(Error::FeatureNotEnabled) - } - - pub fn delete_secret(_print: &Print, _entry_name: &str) -> Result<(), Error> { - Err(Error::FeatureNotEnabled) - } - - pub fn save_secret( - _print: &Print, - _entry_name: &str, - _seed_phrase: &SeedPhrase, - ) -> Result { - Err(Error::FeatureNotEnabled) - } - - pub fn sign_tx_data( - _entry_name: &str, - _hd_path: Option, - _data: &[u8], - ) -> Result, Error> { - Err(Error::FeatureNotEnabled) - } -} diff --git a/cmd/soroban-cli/src/signer/secure_store_entry.rs b/cmd/soroban-cli/src/signer/secure_store_entry.rs new file mode 100644 index 0000000000..0cc4995295 --- /dev/null +++ b/cmd/soroban-cli/src/signer/secure_store_entry.rs @@ -0,0 +1,131 @@ +use stellar_strkey::ed25519::PublicKey; + +use crate::{ + print::Print, + xdr::{self, DecoratedSignature}, +}; + +#[cfg(feature = "additional-libs")] +use crate::{ + signer::keyring::{self, StellarEntry}, + xdr::{Signature, SignatureHint}, +}; +#[cfg(feature = "additional-libs")] +use std::sync::Arc; + +use ed25519_dalek::Signature as Ed25519Signature; + +use sep5::SeedPhrase; + +#[cfg(feature = "additional-libs")] +const ENTRY_SERVICE: &str = "org.stellar.cli"; +pub(crate) const ENTRY_PREFIX: &str = "secure_store:"; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[cfg(feature = "additional-libs")] + #[error(transparent)] + Keyring(#[from] keyring::Error), + + #[error(transparent)] + TryFromSlice(#[from] std::array::TryFromSliceError), + + #[error(transparent)] + Xdr(#[from] xdr::Error), + + #[error("Secure Store keys are not allowed: additional-libs feature must be enabled")] + FeatureNotEnabled, +} + +#[derive(Debug, Clone)] +pub struct SecureStoreEntry { + pub hd_path: Option, + #[cfg(feature = "additional-libs")] + pub entry: Arc, +} + +#[cfg(feature = "additional-libs")] +impl SecureStoreEntry { + pub fn new(name: String, hd_path: Option) -> Result { + Ok(Self { + hd_path, + entry: Arc::new(StellarEntry::new(&name)?), + }) + } + + pub fn get_public_key(&self) -> Result { + Ok(self.entry.get_public_key(self.hd_path)?) + } + + pub fn delete_secret(&self, print: &Print) -> Result<(), Error> { + Ok(self.entry.delete_seed_phrase(print)?) + } + + pub fn create_and_save( + entry_name: &str, + seed_phrase: &SeedPhrase, + print: &Print, + ) -> Result { + let entry_name_with_prefix = format!("{ENTRY_PREFIX}{ENTRY_SERVICE}-{entry_name}"); + + let s = Self::new(entry_name_with_prefix.clone(), None)?; + s.entry.write(seed_phrase.clone(), print)?; + + Ok(entry_name_with_prefix) + } + + pub fn sign_tx_data(&self, data: &[u8]) -> Result, Error> { + Ok(self.entry.sign_data(data, self.hd_path)?) + } + + pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result { + let hint = SignatureHint(self.get_public_key()?.0[28..].try_into()?); + + let signed_tx_hash = self.sign_tx_data(&tx_hash)?; + + let signature = Signature(signed_tx_hash.clone().try_into()?); + Ok(DecoratedSignature { hint, signature }) + } + + pub fn sign_payload(&self, payload: [u8; 32]) -> Result { + let signed_bytes = self.sign_tx_data(&payload)?; + + let sig = Ed25519Signature::from_bytes(signed_bytes.as_slice().try_into()?); + Ok(sig) + } +} + +#[cfg(not(feature = "additional-libs"))] +impl SecureStoreEntry { + pub fn new(_name: String, _hd_path: Option) -> Result { + Err(Error::FeatureNotEnabled) + } + + pub fn get_public_key(&self) -> Result { + Err(Error::FeatureNotEnabled) + } + + pub fn delete_secret(&self, _print: &Print) -> Result<(), Error> { + Err(Error::FeatureNotEnabled) + } + + pub fn create_and_save( + _entry_name: &str, + _seed_phrase: &SeedPhrase, + _print: &Print, + ) -> Result { + Err(Error::FeatureNotEnabled) + } + + pub fn sign_tx_data(_data: &[u8]) -> Result, Error> { + Err(Error::FeatureNotEnabled) + } + + pub fn sign_tx_hash(&self, _tx_hash: [u8; 32]) -> Result { + Err(Error::FeatureNotEnabled) + } + + pub fn sign_payload(&self, _payload: [u8; 32]) -> Result { + Err(Error::FeatureNotEnabled) + } +}