soroban_cli/config/
secret.rs

1use clap::arg;
2use serde::{Deserialize, Serialize};
3use std::str::FromStr;
4
5use sep5::SeedPhrase;
6use stellar_strkey::ed25519::{PrivateKey, PublicKey};
7
8use crate::{
9    print::Print,
10    signer::{self, ledger, secure_store, LocalKey, SecureStoreEntry, Signer, SignerKind},
11    utils,
12};
13
14use super::key::Key;
15
16#[derive(thiserror::Error, Debug)]
17pub enum Error {
18    #[error(transparent)]
19    Secret(#[from] stellar_strkey::DecodeError),
20    #[error(transparent)]
21    SeedPhrase(#[from] sep5::error::Error),
22    #[error(transparent)]
23    Ed25519(#[from] ed25519_dalek::SignatureError),
24    #[error("cannot parse secret (S) or seed phrase (12 or 24 word)")]
25    InvalidSecretOrSeedPhrase,
26    #[error(transparent)]
27    Signer(#[from] signer::Error),
28    #[error("Ledger does not reveal secret key")]
29    LedgerDoesNotRevealSecretKey,
30    #[error(transparent)]
31    SecureStore(#[from] secure_store::Error),
32    #[error("Secure Store does not reveal secret key")]
33    SecureStoreDoesNotRevealSecretKey,
34    #[error(transparent)]
35    Ledger(#[from] signer::ledger::Error),
36}
37
38#[derive(Debug, clap::Args, Clone)]
39#[group(skip)]
40pub struct Args {
41    /// ⚠️ Deprecated, use `--secure-store`. Enter secret (S) key when prompted
42    #[arg(long)]
43    pub secret_key: bool,
44
45    /// ⚠️ Deprecated, use `--secure-store`. Enter key using 12-24 word seed phrase
46    #[arg(long)]
47    pub seed_phrase: bool,
48
49    /// Save the new key in your OS's credential secure store.
50    ///
51    /// On Mac this uses Keychain, on Windows it is Secure Store Service, and on *nix platforms it uses a combination of the kernel keyutils and DBus-based Secret Service.
52    ///
53    /// This only supports seed phrases for now.
54    #[arg(long)]
55    pub secure_store: bool,
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(untagged)]
60pub enum Secret {
61    SecretKey { secret_key: String },
62    SeedPhrase { seed_phrase: String },
63    Ledger,
64    SecureStore { entry_name: String },
65}
66
67impl FromStr for Secret {
68    type Err = Error;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        if PrivateKey::from_string(s).is_ok() {
72            Ok(Secret::SecretKey {
73                secret_key: s.to_string(),
74            })
75        } else if sep5::SeedPhrase::from_str(s).is_ok() {
76            Ok(Secret::SeedPhrase {
77                seed_phrase: s.to_string(),
78            })
79        } else if s == "ledger" {
80            Ok(Secret::Ledger)
81        } else if s.starts_with(secure_store::ENTRY_PREFIX) {
82            Ok(Secret::SecureStore {
83                entry_name: s.to_string(),
84            })
85        } else {
86            Err(Error::InvalidSecretOrSeedPhrase)
87        }
88    }
89}
90
91impl From<PrivateKey> for Secret {
92    fn from(value: PrivateKey) -> Self {
93        Secret::SecretKey {
94            secret_key: value.to_string(),
95        }
96    }
97}
98
99impl From<Secret> for Key {
100    fn from(value: Secret) -> Self {
101        Key::Secret(value)
102    }
103}
104
105impl From<SeedPhrase> for Secret {
106    fn from(value: SeedPhrase) -> Self {
107        Secret::SeedPhrase {
108            seed_phrase: value.seed_phrase.into_phrase(),
109        }
110    }
111}
112
113impl Secret {
114    pub fn private_key(&self, index: Option<usize>) -> Result<PrivateKey, Error> {
115        Ok(match self {
116            Secret::SecretKey { secret_key } => PrivateKey::from_string(secret_key)?,
117            Secret::SeedPhrase { seed_phrase } => PrivateKey::from_payload(
118                &sep5::SeedPhrase::from_str(seed_phrase)?
119                    .from_path_index(index.unwrap_or_default(), None)?
120                    .private()
121                    .0,
122            )?,
123            Secret::Ledger => panic!("Ledger does not reveal secret key"),
124            Secret::SecureStore { .. } => {
125                return Err(Error::SecureStoreDoesNotRevealSecretKey);
126            }
127        })
128    }
129
130    pub fn public_key(&self, index: Option<usize>) -> Result<PublicKey, Error> {
131        if let Secret::SecureStore { entry_name } = self {
132            Ok(secure_store::get_public_key(entry_name, index)?)
133        } else {
134            let key = self.key_pair(index)?;
135            Ok(stellar_strkey::ed25519::PublicKey::from_payload(
136                key.verifying_key().as_bytes(),
137            )?)
138        }
139    }
140
141    pub async fn signer(&self, hd_path: Option<usize>, print: Print) -> Result<Signer, Error> {
142        let kind = match self {
143            Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => {
144                let key = self.key_pair(hd_path)?;
145                SignerKind::Local(LocalKey { key })
146            }
147            Secret::Ledger => {
148                let hd_path: u32 = hd_path
149                    .unwrap_or_default()
150                    .try_into()
151                    .expect("uszie bigger than u32");
152                SignerKind::Ledger(ledger::new(hd_path).await?)
153            }
154            Secret::SecureStore { entry_name } => SignerKind::SecureStore(SecureStoreEntry {
155                name: entry_name.clone(),
156                hd_path,
157            }),
158        };
159        Ok(Signer { kind, print })
160    }
161
162    pub fn key_pair(&self, index: Option<usize>) -> Result<ed25519_dalek::SigningKey, Error> {
163        Ok(utils::into_signing_key(&self.private_key(index)?))
164    }
165
166    pub fn from_seed(seed: Option<&str>) -> Result<Self, Error> {
167        Ok(seed_phrase_from_seed(seed)?.into())
168    }
169}
170
171pub fn seed_phrase_from_seed(seed: Option<&str>) -> Result<SeedPhrase, Error> {
172    Ok(if let Some(seed) = seed.map(str::as_bytes) {
173        sep5::SeedPhrase::from_entropy(seed)?
174    } else {
175        sep5::SeedPhrase::random(sep5::MnemonicType::Words24)?
176    })
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    const TEST_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ";
184    const TEST_SECRET_KEY: &str = "SBF5HLRREHMS36XZNTUSKZ6FTXDZGNXOHF4EXKUL5UCWZLPBX3NGJ4BH";
185    const TEST_SEED_PHRASE: &str =
186        "depth decade power loud smile spatial sign movie judge february rate broccoli";
187
188    #[test]
189    fn test_from_str_for_secret_key() {
190        let secret = Secret::from_str(TEST_SECRET_KEY).unwrap();
191        let public_key = secret.public_key(None).unwrap();
192        let private_key = secret.private_key(None).unwrap();
193
194        assert!(matches!(secret, Secret::SecretKey { .. }));
195        assert_eq!(public_key.to_string(), TEST_PUBLIC_KEY);
196        assert_eq!(private_key.to_string(), TEST_SECRET_KEY);
197    }
198
199    #[test]
200    fn test_secret_from_seed_phrase() {
201        let secret = Secret::from_str(TEST_SEED_PHRASE).unwrap();
202        let public_key = secret.public_key(None).unwrap();
203        let private_key = secret.private_key(None).unwrap();
204
205        assert!(matches!(secret, Secret::SeedPhrase { .. }));
206        assert_eq!(public_key.to_string(), TEST_PUBLIC_KEY);
207        assert_eq!(private_key.to_string(), TEST_SECRET_KEY);
208    }
209
210    #[test]
211    fn test_secret_from_secure_store() {
212        //todo: add assertion for getting public key - will need to mock the keychain and add the keypair to the keychain
213        let secret = Secret::from_str("secure_store:org.stellar.cli-alice").unwrap();
214        assert!(matches!(secret, Secret::SecureStore { .. }));
215
216        let private_key_result = secret.private_key(None);
217        assert!(private_key_result.is_err());
218        assert!(matches!(
219            private_key_result.unwrap_err(),
220            Error::SecureStoreDoesNotRevealSecretKey
221        ));
222    }
223
224    #[test]
225    fn test_secret_from_invalid_string() {
226        let secret = Secret::from_str("invalid");
227        assert!(secret.is_err());
228    }
229}