soroban_cli/config/
secret.rs1use 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, keyring, ledger, 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
29 #[error("Ledger does not reveal secret key")]
30 LedgerDoesNotRevealSecretKey,
31
32 #[error(transparent)]
33 Keyring(#[from] keyring::Error),
34 #[error("Secure Store does not reveal secret key")]
35 SecureStoreDoesNotRevealSecretKey,
36}
37
38#[derive(Debug, clap::Args, Clone)]
39#[group(skip)]
40pub struct Args {
41 #[arg(long)]
43 pub secret_key: bool,
44 #[arg(long)]
46 pub seed_phrase: bool,
47 #[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(keyring::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 let entry = keyring::StellarEntry::new(entry_name)?;
127 Ok(entry.get_public_key(index)?)
128 } else {
129 let key = self.key_pair(index)?;
130 Ok(stellar_strkey::ed25519::PublicKey::from_payload(
131 key.verifying_key().as_bytes(),
132 )?)
133 }
134 }
135
136 pub async fn signer(&self, hd_path: Option<usize>, print: Print) -> Result<Signer, Error> {
137 let kind = match self {
138 Secret::SecretKey { .. } | Secret::SeedPhrase { .. } => {
139 let key = self.key_pair(hd_path)?;
140 SignerKind::Local(LocalKey { key })
141 }
142 Secret::Ledger => {
143 let hd_path: u32 = hd_path
144 .unwrap_or_default()
145 .try_into()
146 .expect("uszie bigger than u32");
147 SignerKind::Ledger(ledger(hd_path).await?)
148 }
149 Secret::SecureStore { entry_name } => SignerKind::SecureStore(SecureStoreEntry {
150 name: entry_name.to_string(),
151 hd_path,
152 }),
153 };
154 Ok(Signer { kind, print })
155 }
156
157 pub fn key_pair(&self, index: Option<usize>) -> Result<ed25519_dalek::SigningKey, Error> {
158 Ok(utils::into_signing_key(&self.private_key(index)?))
159 }
160
161 pub fn from_seed(seed: Option<&str>) -> Result<Self, Error> {
162 Ok(seed_phrase_from_seed(seed)?.into())
163 }
164}
165
166pub fn seed_phrase_from_seed(seed: Option<&str>) -> Result<SeedPhrase, Error> {
167 Ok(if let Some(seed) = seed.map(str::as_bytes) {
168 sep5::SeedPhrase::from_entropy(seed)?
169 } else {
170 sep5::SeedPhrase::random(sep5::MnemonicType::Words24)?
171 })
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 const TEST_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ";
179 const TEST_SECRET_KEY: &str = "SBF5HLRREHMS36XZNTUSKZ6FTXDZGNXOHF4EXKUL5UCWZLPBX3NGJ4BH";
180 const TEST_SEED_PHRASE: &str =
181 "depth decade power loud smile spatial sign movie judge february rate broccoli";
182
183 #[test]
184 fn test_from_str_for_secret_key() {
185 let secret = Secret::from_str(TEST_SECRET_KEY).unwrap();
186 let public_key = secret.public_key(None).unwrap();
187 let private_key = secret.private_key(None).unwrap();
188
189 assert!(matches!(secret, Secret::SecretKey { .. }));
190 assert_eq!(public_key.to_string(), TEST_PUBLIC_KEY);
191 assert_eq!(private_key.to_string(), TEST_SECRET_KEY);
192 }
193
194 #[test]
195 fn test_secret_from_seed_phrase() {
196 let secret = Secret::from_str(TEST_SEED_PHRASE).unwrap();
197 let public_key = secret.public_key(None).unwrap();
198 let private_key = secret.private_key(None).unwrap();
199
200 assert!(matches!(secret, Secret::SeedPhrase { .. }));
201 assert_eq!(public_key.to_string(), TEST_PUBLIC_KEY);
202 assert_eq!(private_key.to_string(), TEST_SECRET_KEY);
203 }
204
205 #[test]
206 fn test_secret_from_secure_store() {
207 let secret = Secret::from_str("secure_store:org.stellar.cli-alice").unwrap();
209 assert!(matches!(secret, Secret::SecureStore { .. }));
210
211 let private_key_result = secret.private_key(None);
212 assert!(private_key_result.is_err());
213 assert!(matches!(
214 private_key_result.unwrap_err(),
215 Error::SecureStoreDoesNotRevealSecretKey
216 ));
217 }
218
219 #[test]
220 fn test_secret_from_invalid_string() {
221 let secret = Secret::from_str("invalid");
222 assert!(secret.is_err());
223 }
224}