1use core::borrow::Borrow;
7use std::ops::Range;
8
9use async_trait::async_trait;
10use crypto::{
11 hashes::{blake2b::Blake2b256, Digest},
12 keys::{
13 bip39::{Mnemonic, MnemonicRef, Passphrase},
14 bip44::Bip44,
15 slip10::Segment,
16 },
17 signatures::{
18 ed25519,
19 secp256k1_ecdsa::{self, EvmAddress},
20 },
21};
22use instant::Duration;
23use iota_stronghold::{
24 procedures::{self, Curve, KeyType, Slip10DeriveInput},
25 Location,
26};
27
28use super::{
29 common::{DERIVE_OUTPUT_RECORD_PATH, PRIVATE_DATA_CLIENT_PATH, SECRET_VAULT_PATH, SEED_RECORD_PATH},
30 StrongholdAdapter,
31};
32use crate::{
33 client::{
34 api::PreparedTransactionData,
35 secret::{types::StrongholdDto, GenerateAddressOptions, SecretManage, SecretManagerConfig},
36 stronghold::Error,
37 },
38 types::block::{
39 address::Ed25519Address, payload::transaction::TransactionPayload, signature::Ed25519Signature, unlock::Unlocks,
40 },
41};
42
43#[async_trait]
44impl SecretManage for StrongholdAdapter {
45 type Error = crate::client::Error;
46
47 async fn generate_ed25519_addresses(
48 &self,
49 coin_type: u32,
50 account_index: u32,
51 address_indexes: Range<u32>,
52 options: impl Into<Option<GenerateAddressOptions>> + Send,
53 ) -> Result<Vec<Ed25519Address>, Self::Error> {
54 if !self.is_key_available().await {
60 return Err(Error::KeyCleared.into());
61 }
62
63 let seed_location = Slip10DeriveInput::Seed(Location::generic(SECRET_VAULT_PATH, SEED_RECORD_PATH));
65
66 let mut addresses = Vec::new();
68 let internal = options.into().map(|o| o.internal).unwrap_or_default();
69
70 for address_index in address_indexes {
71 let chain = Bip44::new(coin_type)
72 .with_account(account_index)
73 .with_change(internal as _)
74 .with_address_index(address_index);
75
76 let derive_location = Location::generic(
77 SECRET_VAULT_PATH,
78 [
79 DERIVE_OUTPUT_RECORD_PATH,
80 &chain
81 .to_chain::<ed25519::SecretKey>()
82 .into_iter()
83 .flat_map(|seg| seg.ser32())
84 .collect::<Vec<u8>>(),
85 ]
86 .concat(),
87 );
88
89 self.slip10_derive(Curve::Ed25519, chain, seed_location.clone(), derive_location.clone())
91 .await?;
92
93 let public_key = self.ed25519_public_key(derive_location.clone()).await?;
95
96 self.stronghold
98 .lock()
99 .await
100 .get_client(PRIVATE_DATA_CLIENT_PATH)
101 .map_err(Error::from)?
102 .vault(SECRET_VAULT_PATH)
103 .delete_secret(derive_location.record_path())
104 .map_err(Error::from)?;
105
106 let hash = Blake2b256::digest(public_key);
108
109 let address = Ed25519Address::new(hash.into());
111
112 addresses.push(address);
114 }
115
116 Ok(addresses)
117 }
118
119 async fn generate_evm_addresses(
120 &self,
121 coin_type: u32,
122 account_index: u32,
123 address_indexes: Range<u32>,
124 options: impl Into<Option<GenerateAddressOptions>> + Send,
125 ) -> Result<Vec<EvmAddress>, Self::Error> {
126 if !self.is_key_available().await {
132 return Err(Error::KeyCleared.into());
133 }
134
135 let seed_location = Slip10DeriveInput::Seed(Location::generic(SECRET_VAULT_PATH, SEED_RECORD_PATH));
137
138 let mut addresses = Vec::new();
140 let internal = options.into().map(|o| o.internal).unwrap_or_default();
141
142 for address_index in address_indexes {
143 let chain = Bip44::new(coin_type)
144 .with_account(account_index)
145 .with_change(internal as _)
146 .with_address_index(address_index);
147
148 let derive_location = Location::generic(
149 SECRET_VAULT_PATH,
150 [
151 DERIVE_OUTPUT_RECORD_PATH,
152 &chain
153 .to_chain::<secp256k1_ecdsa::SecretKey>()
154 .into_iter()
155 .flat_map(|seg| seg.ser32())
156 .collect::<Vec<u8>>(),
157 ]
158 .concat(),
159 );
160
161 self.slip10_derive(Curve::Secp256k1, chain, seed_location.clone(), derive_location.clone())
163 .await?;
164
165 let public_key = self.secp256k1_ecdsa_public_key(derive_location.clone()).await?;
167
168 self.stronghold
170 .lock()
171 .await
172 .get_client(PRIVATE_DATA_CLIENT_PATH)
173 .map_err(Error::from)?
174 .vault(SECRET_VAULT_PATH)
175 .delete_secret(derive_location.record_path())
176 .map_err(Error::from)?;
177
178 addresses.push(public_key.evm_address());
180 }
181
182 Ok(addresses)
183 }
184
185 async fn sign_ed25519(&self, msg: &[u8], chain: Bip44) -> Result<Ed25519Signature, Self::Error> {
186 if !self.is_key_available().await {
192 return Err(Error::KeyCleared.into());
193 }
194
195 let seed_location = Slip10DeriveInput::Seed(Location::generic(SECRET_VAULT_PATH, SEED_RECORD_PATH));
197
198 let derive_location = Location::generic(
199 SECRET_VAULT_PATH,
200 [
201 DERIVE_OUTPUT_RECORD_PATH,
202 &chain
203 .to_chain::<ed25519::SecretKey>()
204 .into_iter()
205 .flat_map(|seg| seg.ser32())
206 .collect::<Vec<u8>>(),
207 ]
208 .concat(),
209 );
210
211 self.slip10_derive(Curve::Ed25519, chain, seed_location, derive_location.clone())
213 .await?;
214
215 let public_key = self.ed25519_public_key(derive_location.clone()).await?;
217 let signature = self.ed25519_sign(derive_location.clone(), msg).await?;
218
219 self.stronghold
221 .lock()
222 .await
223 .get_client(PRIVATE_DATA_CLIENT_PATH)
224 .map_err(Error::from)?
225 .vault(SECRET_VAULT_PATH)
226 .delete_secret(derive_location.record_path())
227 .map_err(Error::from)?;
228
229 Ok(Ed25519Signature::new(public_key, signature))
230 }
231
232 async fn sign_secp256k1_ecdsa(
233 &self,
234 msg: &[u8],
235 chain: Bip44,
236 ) -> Result<(secp256k1_ecdsa::PublicKey, secp256k1_ecdsa::RecoverableSignature), Self::Error> {
237 if !self.is_key_available().await {
243 return Err(Error::KeyCleared.into());
244 }
245
246 let seed_location = Slip10DeriveInput::Seed(Location::generic(SECRET_VAULT_PATH, SEED_RECORD_PATH));
248
249 let derive_location = Location::generic(
250 SECRET_VAULT_PATH,
251 [
252 DERIVE_OUTPUT_RECORD_PATH,
253 &chain
254 .to_chain::<secp256k1_ecdsa::SecretKey>()
255 .into_iter()
256 .flat_map(|seg| seg.ser32())
257 .collect::<Vec<u8>>(),
258 ]
259 .concat(),
260 );
261
262 self.slip10_derive(Curve::Secp256k1, chain, seed_location, derive_location.clone())
264 .await?;
265
266 let public_key = self.secp256k1_ecdsa_public_key(derive_location.clone()).await?;
268 let signature = self.secp256k1_ecdsa_sign(derive_location.clone(), msg).await?;
269
270 self.stronghold
272 .lock()
273 .await
274 .get_client(PRIVATE_DATA_CLIENT_PATH)
275 .map_err(Error::from)?
276 .vault(SECRET_VAULT_PATH)
277 .delete_secret(derive_location.record_path())
278 .map_err(Error::from)?;
279
280 Ok((public_key, signature))
281 }
282
283 async fn sign_transaction_essence(
284 &self,
285 prepared_transaction_data: &PreparedTransactionData,
286 time: Option<u32>,
287 ) -> Result<Unlocks, Self::Error> {
288 crate::client::secret::default_sign_transaction_essence(self, prepared_transaction_data, time).await
289 }
290
291 async fn sign_transaction(
292 &self,
293 prepared_transaction_data: PreparedTransactionData,
294 ) -> Result<TransactionPayload, Self::Error> {
295 crate::client::secret::default_sign_transaction(self, prepared_transaction_data).await
296 }
297}
298
299impl SecretManagerConfig for StrongholdAdapter {
300 type Config = StrongholdDto;
301
302 fn to_config(&self) -> Option<Self::Config> {
303 Some(Self::Config {
304 password: None,
305 timeout: self.get_timeout().map(|duration| duration.as_secs()),
306 snapshot_path: self.snapshot_path.clone().into_os_string().to_string_lossy().into(),
307 })
308 }
309
310 fn from_config(config: &Self::Config) -> Result<Self, Self::Error> {
311 let mut builder = Self::builder();
312
313 if let Some(password) = &config.password {
314 builder = builder.password(password.clone());
315 }
316
317 if let Some(timeout) = &config.timeout {
318 builder = builder.timeout(Duration::from_secs(*timeout));
319 }
320
321 Ok(builder.build(&config.snapshot_path)?)
322 }
323}
324
325impl StrongholdAdapter {
327 async fn bip39_recover(&self, mnemonic: Mnemonic, passphrase: Passphrase, output: Location) -> Result<(), Error> {
330 self.stronghold
331 .lock()
332 .await
333 .get_client(PRIVATE_DATA_CLIENT_PATH)?
334 .execute_procedure(procedures::BIP39Recover {
335 mnemonic,
336 passphrase,
337 output,
338 })?;
339
340 Ok(())
341 }
342
343 async fn slip10_derive(
346 &self,
347 curve: Curve,
348 chain: Bip44,
349 input: Slip10DeriveInput,
350 output: Location,
351 ) -> Result<(), Error> {
352 let chain = match curve {
353 Curve::Ed25519 => chain
354 .to_chain::<ed25519::SecretKey>()
355 .into_iter()
356 .map(Into::into)
357 .collect(),
358 Curve::Secp256k1 => chain.to_chain::<secp256k1_ecdsa::SecretKey>().to_vec(),
359 };
360 if let Err(err) = self
361 .stronghold
362 .lock()
363 .await
364 .get_client(PRIVATE_DATA_CLIENT_PATH)?
365 .execute_procedure(procedures::Slip10Derive {
366 curve,
367 chain,
368 input,
369 output,
370 })
371 {
372 match err {
373 iota_stronghold::procedures::ProcedureError::Engine(ref e) => {
374 if e.to_string().contains("does not exist") {
376 return Err(Error::MnemonicMissing);
378 } else {
379 return Err(err.into());
380 }
381 }
382 _ => {
383 return Err(err.into());
384 }
385 }
386 };
387
388 Ok(())
389 }
390
391 async fn ed25519_public_key(&self, private_key: Location) -> Result<ed25519::PublicKey, Error> {
394 Ok(ed25519::PublicKey::try_from_bytes(
395 self.stronghold
396 .lock()
397 .await
398 .get_client(PRIVATE_DATA_CLIENT_PATH)?
399 .execute_procedure(procedures::PublicKey {
400 ty: KeyType::Ed25519,
401 private_key,
402 })?
403 .try_into()
404 .unwrap(),
405 )?)
406 }
407
408 async fn ed25519_sign(&self, private_key: Location, msg: &[u8]) -> Result<ed25519::Signature, Error> {
411 Ok(ed25519::Signature::from_bytes(
412 self.stronghold
413 .lock()
414 .await
415 .get_client(PRIVATE_DATA_CLIENT_PATH)?
416 .execute_procedure(procedures::Ed25519Sign {
417 private_key,
418 msg: msg.to_vec(),
419 })?,
420 ))
421 }
422
423 async fn secp256k1_ecdsa_sign(
426 &self,
427 private_key: Location,
428 msg: &[u8],
429 ) -> Result<secp256k1_ecdsa::RecoverableSignature, Error> {
430 Ok(secp256k1_ecdsa::RecoverableSignature::try_from_bytes(
431 &self
432 .stronghold
433 .lock()
434 .await
435 .get_client(PRIVATE_DATA_CLIENT_PATH)?
436 .execute_procedure(procedures::Secp256k1EcdsaSign {
437 private_key,
438 msg: msg.to_vec(),
439 flavor: procedures::Secp256k1EcdsaFlavor::Keccak256,
440 })?,
441 )?)
442 }
443
444 async fn secp256k1_ecdsa_public_key(&self, private_key: Location) -> Result<secp256k1_ecdsa::PublicKey, Error> {
447 let bytes = self
448 .stronghold
449 .lock()
450 .await
451 .get_client(PRIVATE_DATA_CLIENT_PATH)?
452 .execute_procedure(procedures::PublicKey {
453 ty: KeyType::Secp256k1Ecdsa,
454 private_key,
455 })?;
456 Ok(secp256k1_ecdsa::PublicKey::try_from_slice(&bytes)?)
457 }
458
459 pub async fn store_mnemonic(&self, mnemonic: impl Borrow<MnemonicRef> + Send) -> Result<(), Error> {
461 if self.key_provider.lock().await.is_none() {
463 return Err(Error::KeyCleared);
464 };
465
466 let output = Location::generic(SECRET_VAULT_PATH, SEED_RECORD_PATH);
468
469 let trimmed_mnemonic = Mnemonic::from(mnemonic.borrow().trim().to_owned());
471
472 crypto::keys::bip39::wordlist::verify(&trimmed_mnemonic, &crypto::keys::bip39::wordlist::ENGLISH)
474 .map_err(|e| Error::InvalidMnemonic(format!("{e:?}")))?;
475
476 if self
478 .stronghold
479 .lock()
480 .await
481 .get_client(PRIVATE_DATA_CLIENT_PATH)?
482 .record_exists(&output)?
483 {
484 return Err(Error::MnemonicAlreadyStored);
485 }
486
487 self.bip39_recover(trimmed_mnemonic, Passphrase::default(), output)
489 .await?;
490
491 self.write_stronghold_snapshot(None).await?;
493
494 Ok(())
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use std::path::Path;
501
502 use pretty_assertions::assert_eq;
503
504 use super::*;
505 use crate::{
506 client::constants::{ETHER_COIN_TYPE, IOTA_COIN_TYPE},
507 types::block::address::ToBech32Ext,
508 };
509
510 #[tokio::test]
511 async fn test_ed25519_address_generation() {
512 let stronghold_path = "test_ed25519_address_generation.stronghold";
513 std::fs::remove_file(stronghold_path).ok();
515 let mnemonic = Mnemonic::from(
516 "giant dynamic museum toddler six deny defense ostrich bomb access mercy blood explain muscle shoot shallow glad autumn author calm heavy hawk abuse rally".to_owned(),
517 );
518 let stronghold_adapter = StrongholdAdapter::builder()
519 .password("drowssap".to_owned())
520 .build(stronghold_path)
521 .unwrap();
522
523 stronghold_adapter.store_mnemonic(mnemonic).await.unwrap();
524
525 assert!(Path::new(stronghold_path).exists());
527
528 let addresses = stronghold_adapter
529 .generate_ed25519_addresses(IOTA_COIN_TYPE, 0, 0..1, None)
530 .await
531 .unwrap();
532
533 assert_eq!(
534 addresses[0].to_bech32_unchecked("atoi"),
535 "atoi1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluehe53e"
536 );
537
538 std::fs::remove_file(stronghold_path).ok();
540 }
541
542 #[tokio::test]
543 async fn test_evm_address_generation() {
544 let stronghold_path = "test_evm_address_generation.stronghold";
545 std::fs::remove_file(stronghold_path).ok();
547 let mnemonic = Mnemonic::from(
548 "endorse answer radar about source reunion marriage tag sausage weekend frost daring base attack because joke dream slender leisure group reason prepare broken river".to_owned(),
549 );
550 let stronghold_adapter = StrongholdAdapter::builder()
551 .password("drowssap".to_owned())
552 .build(stronghold_path)
553 .unwrap();
554
555 stronghold_adapter.store_mnemonic(mnemonic).await.unwrap();
556
557 assert!(Path::new(stronghold_path).exists());
559
560 let addresses = stronghold_adapter
561 .generate_evm_addresses(ETHER_COIN_TYPE, 0, 0..1, None)
562 .await
563 .unwrap();
564
565 assert_eq!(
566 prefix_hex::encode(addresses[0].as_ref()),
567 "0xcaefde2b487ded55688765964320ff390cd87828"
568 );
569
570 std::fs::remove_file(stronghold_path).ok();
572 }
573
574 #[tokio::test]
575 async fn test_key_cleared() {
576 let stronghold_path = "test_key_cleared.stronghold";
577 std::fs::remove_file(stronghold_path).ok();
579 let mnemonic = Mnemonic::from(
580 "giant dynamic museum toddler six deny defense ostrich bomb access mercy blood explain muscle shoot shallow glad autumn author calm heavy hawk abuse rally".to_owned(),
581 );
582 let stronghold_adapter = StrongholdAdapter::builder()
583 .password("drowssap".to_owned())
584 .build(stronghold_path)
585 .unwrap();
586
587 stronghold_adapter.store_mnemonic(mnemonic).await.unwrap();
588
589 assert!(Path::new(stronghold_path).exists());
591
592 stronghold_adapter.clear_key().await;
593
594 assert!(
596 stronghold_adapter
597 .generate_ed25519_addresses(IOTA_COIN_TYPE, 0, 0..1, None,)
598 .await
599 .is_err()
600 );
601
602 stronghold_adapter.set_password("drowssap".to_owned()).await.unwrap();
603
604 let addresses = stronghold_adapter
606 .generate_ed25519_addresses(IOTA_COIN_TYPE, 0, 0..1, None)
607 .await
608 .unwrap();
609
610 assert_eq!(
611 addresses[0].to_bech32_unchecked("atoi"),
612 "atoi1qpszqzadsym6wpppd6z037dvlejmjuke7s24hm95s9fg9vpua7vluehe53e"
613 );
614
615 std::fs::remove_file(stronghold_path).ok();
617 }
618}