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