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, 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 #[arg(long)]
43 pub secret_key: bool,
44
45 #[arg(long)]
47 pub seed_phrase: bool,
48
49 #[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 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}