iota_sdk/client/stronghold/
secret.rs

1// Copyright 2022 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! The [SecretManage] implementation for [StrongholdAdapter].
5
6use 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        // Prevent the method from being invoked when the key has been cleared from the memory. Do note that Stronghold
55        // only asks for a key for reading / writing a snapshot, so without our cached key this method is invocable, but
56        // it doesn't make sense when it comes to our user (signing transactions / generating addresses without a key).
57        // Thus, we put an extra guard here to prevent this methods from being invoked when our cached key has
58        // been cleared.
59        if !self.is_key_available().await {
60            return Err(Error::KeyCleared.into());
61        }
62
63        // Stronghold arguments.
64        let seed_location = Slip10DeriveInput::Seed(Location::generic(SECRET_VAULT_PATH, SEED_RECORD_PATH));
65
66        // Addresses to return.
67        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            // Derive a SLIP-10 private key in the vault.
90            self.slip10_derive(Curve::Ed25519, chain, seed_location.clone(), derive_location.clone())
91                .await?;
92
93            // Get the Ed25519 public key from the derived SLIP-10 private key in the vault.
94            let public_key = self.ed25519_public_key(derive_location.clone()).await?;
95
96            // Cleanup location afterwards
97            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            // Hash the public key to get the address.
107            let hash = Blake2b256::digest(public_key);
108
109            // Convert the hash into [Address].
110            let address = Ed25519Address::new(hash.into());
111
112            // Collect it.
113            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        // Prevent the method from being invoked when the key has been cleared from the memory. Do note that Stronghold
127        // only asks for a key for reading / writing a snapshot, so without our cached key this method is invocable, but
128        // it doesn't make sense when it comes to our user (signing transactions / generating addresses without a key).
129        // Thus, we put an extra guard here to prevent this methods from being invoked when our cached key has
130        // been cleared.
131        if !self.is_key_available().await {
132            return Err(Error::KeyCleared.into());
133        }
134
135        // Stronghold arguments.
136        let seed_location = Slip10DeriveInput::Seed(Location::generic(SECRET_VAULT_PATH, SEED_RECORD_PATH));
137
138        // Addresses to return.
139        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            // Derive a SLIP-10 private key in the vault.
162            self.slip10_derive(Curve::Secp256k1, chain, seed_location.clone(), derive_location.clone())
163                .await?;
164
165            // Get the Secp256k1 public key from the derived SLIP-10 private key in the vault.
166            let public_key = self.secp256k1_ecdsa_public_key(derive_location.clone()).await?;
167
168            // Cleanup location afterwards
169            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            // Collect it.
179            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        // Prevent the method from being invoked when the key has been cleared from the memory. Do note that Stronghold
187        // only asks for a key for reading / writing a snapshot, so without our cached key this method is invocable, but
188        // it doesn't make sense when it comes to our user (signing transactions / generating addresses without a key).
189        // Thus, we put an extra guard here to prevent this methods from being invoked when our cached key has
190        // been cleared.
191        if !self.is_key_available().await {
192            return Err(Error::KeyCleared.into());
193        }
194
195        // Stronghold arguments.
196        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        // Derive a SLIP-10 private key in the vault.
212        self.slip10_derive(Curve::Ed25519, chain, seed_location, derive_location.clone())
213            .await?;
214
215        // Get the Ed25519 public key from the derived SLIP-10 private key in the vault.
216        let public_key = self.ed25519_public_key(derive_location.clone()).await?;
217        let signature = self.ed25519_sign(derive_location.clone(), msg).await?;
218
219        // Cleanup location afterwards
220        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        // Prevent the method from being invoked when the key has been cleared from the memory. Do note that Stronghold
238        // only asks for a key for reading / writing a snapshot, so without our cached key this method is invocable, but
239        // it doesn't make sense when it comes to our user (signing transactions / generating addresses without a key).
240        // Thus, we put an extra guard here to prevent this methods from being invoked when our cached key has
241        // been cleared.
242        if !self.is_key_available().await {
243            return Err(Error::KeyCleared.into());
244        }
245
246        // Stronghold arguments.
247        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        // Derive a SLIP-10 private key in the vault.
263        self.slip10_derive(Curve::Secp256k1, chain, seed_location, derive_location.clone())
264            .await?;
265
266        // Get the public key from the derived SLIP-10 private key in the vault.
267        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        // Cleanup location afterwards
271        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
325/// Private methods for the secret manager implementation.
326impl StrongholdAdapter {
327    /// Execute [BIP39Recover](procedures::BIP39Recover) procedure in Stronghold to put a mnemonic into the Stronghold
328    /// vault.
329    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    /// Execute [Slip10Derive](procedures::Slip10Derive) procedure in Stronghold to derive a SLIP-10 private key in the
344    /// Stronghold vault.
345    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                    // Custom error for missing vault error: https://github.com/iotaledger/stronghold.rs/blob/7f0a2e0637394595e953f9071fa74b1d160f51ec/client/src/types/error.rs#L170
375                    if e.to_string().contains("does not exist") {
376                        // Actually the seed, derived from the mnemonic, is not stored.
377                        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    /// Execute [PublicKey](procedures::PublicKey) procedure in Stronghold to get an Ed25519 public key from the SLIP-10
392    /// private key located in `private_key`.
393    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    /// Execute [Ed25519Sign](procedures::Ed25519Sign) procedure in Stronghold to sign `msg` with `private_key` stored
409    /// in the Stronghold vault.
410    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    /// Execute [Secp256k1EcdsaSign](procedures::Secp256k1EcdsaSign) procedure in Stronghold to sign `msg` with
424    /// `private_key` stored in the Stronghold vault.
425    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    /// Execute [PublicKey](procedures::PublicKey) procedure in Stronghold to get a Secp256k1Ecdsa public key from the
445    /// SLIP-10 private key located in `private_key`.
446    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    /// Store a mnemonic into the Stronghold vault.
460    pub async fn store_mnemonic(&self, mnemonic: impl Borrow<MnemonicRef> + Send) -> Result<(), Error> {
461        // The key needs to be supplied first.
462        if self.key_provider.lock().await.is_none() {
463            return Err(Error::KeyCleared);
464        };
465
466        // Stronghold arguments.
467        let output = Location::generic(SECRET_VAULT_PATH, SEED_RECORD_PATH);
468
469        // Trim the mnemonic, in case it hasn't been, as otherwise the restored seed would be wrong.
470        let trimmed_mnemonic = Mnemonic::from(mnemonic.borrow().trim().to_owned());
471
472        // Check if the mnemonic is valid.
473        crypto::keys::bip39::wordlist::verify(&trimmed_mnemonic, &crypto::keys::bip39::wordlist::ENGLISH)
474            .map_err(|e| Error::InvalidMnemonic(format!("{e:?}")))?;
475
476        // We need to check if there has been a mnemonic stored in Stronghold or not to prevent overwriting it.
477        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        // Execute the BIP-39 recovery procedure to put it into the vault (in memory).
488        self.bip39_recover(trimmed_mnemonic, Passphrase::default(), output)
489            .await?;
490
491        // Persist Stronghold to the disk
492        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        // Remove potential old stronghold file
514        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        // The snapshot should have been on the disk now.
526        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        // Remove garbage after test, but don't care about the result
539        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        // Remove potential old stronghold file
546        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        // The snapshot should have been on the disk now.
558        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        // Remove garbage after test, but don't care about the result
571        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        // Remove potential old stronghold file
578        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        // The snapshot should have been on the disk now.
590        assert!(Path::new(stronghold_path).exists());
591
592        stronghold_adapter.clear_key().await;
593
594        // Address generation returns an error when the key is cleared.
595        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        // After setting the correct password it works again.
605        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        // Remove garbage after test, but don't care about the result
616        std::fs::remove_file(stronghold_path).ok();
617    }
618}