soroban_cli/config/
secret.rs

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