etopay_sdk/wallet/
share.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD};
2use blake2::Digest;
3use etopay_wallet::bip39::Mnemonic;
4use secrecy::{ExposeSecret, SecretBox, SecretSlice, SecretString};
5use std::str::FromStr;
6
7use crate::types::crypto::Blake2b256;
8
9/// A share that can be used with other [`Share`] to construct the secret.
10#[derive(Debug, Clone)] // for testing purposes we also derive PartialEq
11#[cfg_attr(test, derive(PartialEq))]
12pub struct Share {
13    /// The type of the secret payload stored by the shares.
14    payload_type: PayloadType,
15
16    /// The encoding used for the share parts.
17    encoding: Encoding,
18
19    /// The encryption method used to encrypt the data field.
20    encryption: Encryption,
21
22    /// The actual share data bytes, representing the `payload_type` content split into shares using
23    /// `encoding` and encrypted using `encryption`.
24    data: ShareData,
25}
26
27/// A type that has the immutable data from the share, and will zeroize it on Drop
28#[derive(zeroize::ZeroizeOnDrop, Clone)]
29#[cfg_attr(test, derive(PartialEq))] // for testing purposes we also derive PartialEq
30struct ShareData(Box<[u8]>);
31
32/// Debug implementation that does not print the content
33impl std::fmt::Debug for ShareData {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.write_str("[REDACTED ")?;
36        f.write_str(core::any::type_name::<Self>())?;
37        f.write_str("]")
38    }
39}
40
41impl FromStr for Share {
42    type Err = ShareError;
43
44    fn from_str(value: &str) -> Result<Self, Self::Err> {
45        let mut parts = value.split('-');
46        let version: PayloadType = parts.next().ok_or(ShareError::NotEnoughParts)?.parse()?;
47        let encoding: Encoding = parts.next().ok_or(ShareError::NotEnoughParts)?.parse()?;
48        let encryption: Encryption = parts.next().ok_or(ShareError::NotEnoughParts)?.parse()?;
49        let data = parts.next().ok_or(ShareError::NotEnoughParts)?;
50        let data = ShareData(STANDARD.decode(data)?.into());
51
52        Ok(Share {
53            payload_type: version,
54            encoding,
55            encryption,
56            data,
57        })
58    }
59}
60
61impl Share {
62    /// Format this [`Share`] to a string value, returned as a [`Secret`].
63    pub fn to_string(&self) -> SecretString {
64        let base64_data = STANDARD.encode(&self.data.0);
65        format!(
66            "{}-{}-{}-{}",
67            self.payload_type, self.encoding, self.encryption, base64_data
68        )
69        .into()
70    }
71
72    /// Checks if the share is encrypted.
73    pub fn is_encrypted(&self) -> bool {
74        self.encryption != Encryption::None
75    }
76
77    #[cfg(test)]
78    pub(crate) fn mock_share() -> Self {
79        Share {
80            payload_type: PayloadType::MnemonicEntropy,
81            encoding: Encoding::RustySecrets,
82            encryption: Encryption::None,
83            data: ShareData("test".to_string().into_bytes().into()),
84        }
85    }
86}
87
88/// Version of the Share, used for allowing different formats in the future
89#[derive(Debug, Clone, Copy, Eq, PartialEq)]
90enum PayloadType {
91    /// Payload contains the raw entropy bytes stored in the mnemonic.
92    MnemonicEntropy,
93}
94
95impl std::fmt::Display for PayloadType {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            Self::MnemonicEntropy => write!(f, "ME"),
99        }
100    }
101}
102
103impl FromStr for PayloadType {
104    type Err = ShareError;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        match s {
108            "ME" => Ok(Self::MnemonicEntropy),
109            other => Err(ShareError::InvalidShareFormat(format!(
110                "Unrecognized Payload type: `{}`",
111                other
112            ))),
113        }
114    }
115}
116
117/// The SSS scheme used to encode the shares
118#[derive(Debug, Clone, Copy, Eq, PartialEq)]
119enum Encoding {
120    RustySecrets,
121}
122
123impl std::fmt::Display for Encoding {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        match self {
126            Self::RustySecrets => write!(f, "RS"),
127        }
128    }
129}
130
131impl FromStr for Encoding {
132    type Err = ShareError;
133
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        match s {
136            "RS" => Ok(Self::RustySecrets),
137            other => Err(ShareError::InvalidShareFormat(format!(
138                "Unrecognized Encoding: `{}`",
139                other
140            ))),
141        }
142    }
143}
144
145/// The encryption used or none of the share payload
146#[derive(Debug, Clone, Copy, Eq, PartialEq)]
147enum Encryption {
148    None,
149    AesGcm,
150}
151
152impl std::fmt::Display for Encryption {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self {
155            Self::None => write!(f, "N"),
156            Self::AesGcm => write!(f, "AesGcm"),
157        }
158    }
159}
160
161impl FromStr for Encryption {
162    type Err = ShareError;
163
164    fn from_str(s: &str) -> Result<Self, Self::Err> {
165        match s {
166            "N" => Ok(Self::None),
167            "AesGcm" => Ok(Self::AesGcm),
168            other => Err(ShareError::InvalidShareFormat(format!(
169                "Unrecognized Encryption: `{}`",
170                other
171            ))),
172        }
173    }
174}
175
176#[derive(Debug, thiserror::Error)]
177#[allow(missing_docs)]
178/// Error produced when working with [`Share`] objects.
179pub enum ShareError {
180    #[error("InvalidShareFormat: {0}")]
181    InvalidShareFormat(String),
182
183    #[error("Not enough parts are available to parse share")]
184    NotEnoughParts,
185
186    #[error("No password was provided but is needed")]
187    PasswordNotProvided,
188
189    #[error("Provided {provided} shares but at least {required} are required")]
190    NotEnoughShares { provided: usize, required: usize },
191
192    #[error("Provided shares are incompatible: {0}")]
193    IncompatibleShares(String),
194
195    #[error("Error while decrypting / encrypting: {0}")]
196    EncryptionError(&'static str),
197
198    #[error("Unable to parse: {0:#?}")]
199    ParseError(#[from] std::num::ParseIntError),
200
201    #[error("Base64 decoding failed: {0:#?}")]
202    Base64Decode(#[from] base64::DecodeError),
203
204    #[error("Error in RustySecrets: {0:?}")]
205    RustySecretsError(#[from] rusty_secrets::errors::Error),
206}
207
208#[derive(Debug)]
209/// Contains all the shares generated by splitting a secret
210pub struct GeneratedShares {
211    /// recovery share that the user should download and store safely
212    pub recovery: Share,
213    /// share to store locally
214    pub local: Share,
215    /// backup share that is shared with etopay backend, encrypted
216    pub backup: Share,
217}
218
219/// Creates shares from a [`Mnemonic`] that can be resolved into a [`Mnemonic`] again when reconstructed.
220#[allow(clippy::result_large_err)]
221pub fn create_shares_from_mnemonic(
222    mnemonic: &Mnemonic,
223    password: &SecretSlice<u8>,
224) -> super::error::Result<GeneratedShares> {
225    // convert the mnemonic string into the raw entropy it encodes
226    let entropy = mnemonic.entropy();
227
228    create_shares_from_secret(PayloadType::MnemonicEntropy, &entropy.to_vec().into(), password).map_err(Into::into)
229}
230
231/// Reconstruct a [`Mnemonic`] from the shares. Can be used to initialize a wallet using the
232/// [`iota_sdk::client::secret::mnemonic::MnemonicSecretManager::try_from_mnemonic`] function.
233#[allow(clippy::result_large_err)]
234pub fn reconstruct_mnemonic(shares: &[&Share], password: Option<&SecretSlice<u8>>) -> super::error::Result<Mnemonic> {
235    let (payload_type, secret) = reconstruct_secret(shares, password)?;
236    match payload_type {
237        PayloadType::MnemonicEntropy => Ok(Mnemonic::from_entropy(
238            secret.expose_secret(),
239            etopay_wallet::bip39::Language::English,
240        )?),
241    }
242}
243
244/// Creates shares from any secret represented as a vector of bytes.
245#[allow(clippy::result_large_err)]
246fn create_shares_from_secret(
247    payload_type: PayloadType,
248    secret: &SecretSlice<u8>,
249    password: &SecretSlice<u8>,
250) -> Result<GeneratedShares, ShareError> {
251    let out = rusty_secrets::dss::ss1::split_secret(
252        2,
253        3,
254        secret.expose_secret(),
255        // we specify reproducibility since we want to be able to regenerate the local share from
256        // the others, and we need the signatures to match
257        rusty_secrets::dss::ss1::Reproducibility::seeded("etopay".to_owned().into_bytes()),
258        &None,
259    )?;
260
261    let mut share_data_iter = out.into_iter().map(|s| Share {
262        payload_type,
263        encoding: Encoding::RustySecrets,
264        encryption: Encryption::None,
265        data: ShareData(s.into_string().into_bytes().into()),
266    });
267
268    let recovery = share_data_iter.next().ok_or(ShareError::NotEnoughParts)?;
269    let local = share_data_iter.next().ok_or(ShareError::NotEnoughParts)?;
270    let mut backup = share_data_iter.next().ok_or(ShareError::NotEnoughParts)?;
271
272    // encrypt the backup / recovery share(s) with the password
273    backup.encryption = Encryption::AesGcm;
274    backup.data = encrypt_with_password(&backup.data, password)?;
275
276    Ok(GeneratedShares {
277        recovery,
278        local,
279        backup,
280    })
281}
282
283/// Reconstruct the secret from provided shares.
284#[allow(clippy::result_large_err)]
285fn reconstruct_secret(
286    shares: &[&Share],
287    password: Option<&SecretSlice<u8>>,
288) -> Result<(PayloadType, SecretSlice<u8>), ShareError> {
289    let Some(share) = shares.first() else {
290        return Err(ShareError::NotEnoughShares {
291            provided: shares.len(),
292            required: 2,
293        });
294    };
295
296    // make sure all shares have the same payload type
297    let payload_type = share.payload_type;
298    if !shares.iter().all(|s| s.payload_type == payload_type) {
299        return Err(ShareError::IncompatibleShares(format!(
300            "All shares must have the same PayloadType, first share is `{payload_type}`"
301        )));
302    }
303
304    // make sure all shares have the same encoding
305    let encoding = share.encoding;
306    if !shares.iter().all(|s| s.encoding == encoding) {
307        return Err(ShareError::IncompatibleShares(format!(
308            "All shares must use the same Encoding, first share uses `{encoding}`"
309        )));
310    }
311
312    // decrypt any encrypted shares with the password
313    let share_data = shares
314        .iter()
315        .map(|&s| match s.encryption {
316            Encryption::None => Ok(s.data.clone()),
317            Encryption::AesGcm => {
318                let Some(password) = password else {
319                    return Err(ShareError::PasswordNotProvided);
320                };
321                decrypt_with_password(&s.data, password)
322            }
323        })
324        .collect::<Result<Vec<ShareData>, ShareError>>()?;
325
326    match encoding {
327        Encoding::RustySecrets => {
328            // use the rusty_secrets to get the secret back
329            let rusty_secrets_shares = share_data
330                .iter()
331                .map(|s| rusty_secrets::dss::ss1::Share::from_string(&String::from_utf8_lossy(&s.0)))
332                .collect::<Result<Vec<rusty_secrets::dss::ss1::Share>, _>>()?;
333
334            let (secret, _access_structure, _metadata) =
335                rusty_secrets::dss::ss1::recover_secret(&rusty_secrets_shares)?;
336
337            Ok((payload_type, SecretBox::new(secret.into())))
338        }
339    }
340}
341
342#[allow(clippy::result_large_err)]
343fn encrypt_with_password(data: &ShareData, key: &SecretSlice<u8>) -> Result<ShareData, ShareError> {
344    use aes_gcm::{
345        Aes256Gcm, Key, Nonce,
346        aead::{Aead, KeyInit, consts::U12},
347    };
348    use rand::RngCore;
349
350    // create a random nonce value (96-bit for AesGcm256) since it needs to be unique for each
351    // encryption with the same key
352    let mut nonce = Nonce::<U12>::default();
353    let mut rng = rand::rng();
354    rng.fill_bytes(&mut nonce);
355
356    // hash the key string with the nonce to use as encryption key
357    let key = Blake2b256::new()
358        .chain_update(key.expose_secret())
359        .chain_update(nonce)
360        .finalize();
361    // panics if length is invalid but since we have provided exactly 256 bits it is fine
362    let key = Key::<aes_gcm::Aes256Gcm>::from_slice(key.as_slice());
363
364    let cipher = Aes256Gcm::new(key);
365
366    // encrypt the data and prepend the nonce value (to use while decrypting)
367    let encrypted = cipher
368        .encrypt(&nonce, data.0.as_ref())
369        .map_err(|_| ShareError::EncryptionError("Error encrypting share using password"))?;
370
371    let mut data = nonce.to_vec();
372    data.extend(encrypted);
373    Ok(ShareData(data.into()))
374}
375
376#[allow(clippy::result_large_err)]
377fn decrypt_with_password(data: &ShareData, key: &SecretSlice<u8>) -> Result<ShareData, ShareError> {
378    use aes_gcm::{
379        Aes256Gcm, Key, Nonce,
380        aead::{Aead, KeyInit},
381    };
382
383    let data = &data.0;
384
385    // split the nonce and the data
386    let nonce_bytes = 96 / 8;
387    if data.len() <= nonce_bytes {
388        return Err(ShareError::InvalidShareFormat(format!(
389            "not enough data for decryption, need at least {nonce_bytes} but {} bytes provided",
390            data.len()
391        )));
392    }
393    let (nonce, data) = data.split_at(nonce_bytes);
394
395    let nonce = Nonce::from_slice(nonce);
396
397    // hash the key string and nonce to use as encryption key
398    let key = Blake2b256::new()
399        .chain_update(key.expose_secret())
400        .chain_update(nonce)
401        .finalize();
402    // panics if length is invalid but since we have provided exactly 256 bits it is fine
403    let key = Key::<aes_gcm::Aes256Gcm>::from_slice(key.as_slice());
404
405    let cipher = Aes256Gcm::new(key);
406
407    Ok(ShareData(
408        cipher
409            .decrypt(nonce, data)
410            .map_err(|_| ShareError::EncryptionError("Error decrypting share using password"))?
411            .into(),
412    ))
413}
414
415#[cfg(test)]
416mod test {
417    use super::*;
418    use secrecy::SecretBox;
419
420    #[test]
421    fn test_string_serialization() {
422        let s = Share {
423            payload_type: super::PayloadType::MnemonicEntropy,
424            encoding: Encoding::RustySecrets,
425            encryption: super::Encryption::None,
426            data: ShareData("data".to_string().into_bytes().into()),
427        };
428        println!("{:?}, {}, {:?}", s.to_string(), s.to_string().expose_secret(), s);
429
430        let parsed = s.to_string().expose_secret().parse::<Share>().unwrap();
431        assert_eq!(parsed, s);
432    }
433
434    #[test]
435    fn test_split_recover_secret() {
436        let secret = SecretBox::new("secret".to_string().into_bytes().into());
437        let password = SecretBox::new("password".to_string().into_bytes().into());
438
439        let shares = create_shares_from_secret(PayloadType::MnemonicEntropy, &secret, &password).unwrap();
440
441        assert_eq!(
442            reconstruct_secret(&[&shares.backup, &shares.local], Some(&password))
443                .unwrap()
444                .1
445                .expose_secret(),
446            secret.expose_secret()
447        );
448        assert_eq!(
449            reconstruct_secret(&[&shares.backup, &shares.recovery], Some(&password))
450                .unwrap()
451                .1
452                .expose_secret(),
453            secret.expose_secret()
454        );
455        assert_eq!(
456            reconstruct_secret(&[&shares.recovery, &shares.local], None)
457                .unwrap()
458                .1
459                .expose_secret(),
460            secret.expose_secret()
461        );
462        assert_eq!(
463            reconstruct_secret(&[&shares.recovery, &shares.local, &shares.backup], Some(&password))
464                .unwrap()
465                .1
466                .expose_secret(),
467            secret.expose_secret()
468        );
469
470        assert!(reconstruct_secret(&[&shares.recovery], Some(&password)).is_err());
471        assert!(reconstruct_secret(&[&shares.local], Some(&password)).is_err());
472        assert!(reconstruct_secret(&[&shares.backup], Some(&password)).is_err());
473    }
474
475    #[test]
476    fn test_split_recover_local() {
477        // Arrange
478        let secret = SecretBox::new("my hex string".to_string().into_bytes().into());
479        let password = SecretBox::new("password".to_string().into_bytes().into());
480
481        let shares = create_shares_from_secret(PayloadType::MnemonicEntropy, &secret, &password).unwrap();
482
483        // reconstruct using backup and recovery
484        let (_, reconstructed_secret) =
485            reconstruct_secret(&[&shares.backup, &shares.recovery], Some(&password)).unwrap();
486
487        // now create shares again and make sure we
488        let new_shares =
489            create_shares_from_secret(PayloadType::MnemonicEntropy, &reconstructed_secret, &password).unwrap();
490
491        // reconstruct using a mix of old and "new" shares
492        let (_, final_secret) = reconstruct_secret(&[&shares.backup, &new_shares.local], Some(&password)).unwrap();
493
494        assert_eq!(final_secret.expose_secret(), secret.expose_secret());
495    }
496
497    #[test]
498    fn test_split_recover_mnemonic() {
499        // Arrange
500        let password = SecretBox::new("password".to_string().into_bytes().into());
501
502        let mnemonic = Mnemonic::new(
503            etopay_wallet::bip39::MnemonicType::Words24,
504            etopay_wallet::bip39::Language::English,
505        );
506
507        // Perform and check
508        let shares = create_shares_from_mnemonic(&mnemonic, &password).unwrap();
509
510        assert_eq!(
511            reconstruct_mnemonic(&[&shares.backup, &shares.local], Some(&password))
512                .unwrap()
513                .entropy(),
514            mnemonic.entropy(),
515        );
516        assert_eq!(
517            reconstruct_mnemonic(&[&shares.backup, &shares.recovery], Some(&password))
518                .unwrap()
519                .entropy(),
520            mnemonic.entropy(),
521        );
522        assert_eq!(
523            reconstruct_mnemonic(&[&shares.recovery, &shares.local], None)
524                .unwrap()
525                .entropy(),
526            mnemonic.entropy(),
527        );
528        assert_eq!(
529            reconstruct_mnemonic(&[&shares.recovery, &shares.local, &shares.backup], Some(&password))
530                .unwrap()
531                .entropy(),
532            mnemonic.entropy()
533        );
534
535        assert!(reconstruct_mnemonic(&[&shares.recovery], Some(&password)).is_err());
536        assert!(reconstruct_mnemonic(&[&shares.local], Some(&password)).is_err());
537        assert!(reconstruct_mnemonic(&[&shares.backup], Some(&password)).is_err());
538    }
539
540    #[test]
541    fn test_split_recover_mnemonic_example() {
542        let password: SecretSlice<u8> = "mnemonic share password".to_string().into_bytes().into();
543
544        // the shares below have been generated with this code:
545        // let mnemonic = iota_sdk::client::Client::generate_mnemonic().unwrap();
546        // println!("{}", mnemonic.to_string());
547        // let shares = creae_shares_from_mnemonic(mnemonic.clone(), &password).unwrap();
548        // println!(
549        //     "{}\n{}\n{}\n\n",
550        //     shares.recovery.to_string().expose_secret(),
551        //     shares.backup.to_string().expose_secret(),
552        //     shares.local.to_string().expose_secret(),
553        // );
554
555        // Perform and check
556        let mnemonic_str = "carpet liberty rent fox panic length romance slide item verb parade expose boss ladder reason vacuum fortune drip lizard dice main gate enrich aisle";
557
558        let shares = [
559            "ME-RS-N-Mi0xLUNBRVFBaGdESXFBRStPVUZYZTJnMTdLRFY1L2pWRllQTHdtZ0dCWExJbitjTERReFRyRHArWGNVMG5yY3UyVmFONFEvZkVoeXNadm5qNFhmRDVIZXZ3eHB2bENTYnZIZTFtOTlXdjJwby8zVWl0d2VhMnVWOTZaejB5WmhEdHlkRDFYcEg1R0RIYXFvZDBpTHdpcDZ3d1k5T0VWdEJhZmtkUVRGaTNNM3gvY2dsK0FDWVQ5WG50TlJycnRtWFRTUGZ4MG54R1lVc0NWUnNKY3h5Q0JxSHBlRGVRekpSTlFxVldMNGpJU3JCZkFRcEpYMnJoT1o4OXM1V3VLaW5PWFd0YUZncTRnd2t1VzR0ZkJJZzVUMjFlaXpGNEpWNzlMcXFXSDZoY3N0Z1huYzZYWTJvZjRvaytlYnJWOFBmR1lOU1NxRWQ4VFpqUzlBL0h0clJGNThEbUdaL2Z2Nmp5MjJjS01hUWllK1ZqdFZ4OUJyblJjWThYYTgxWmNTWlF4YlFLbFQ3MC9tRk5aQlN4ZXNLTWVTU24vV2hycEs0OU80ZW4zRkZJVTJqd2lLcGwybHpHMk0vdThJTzRZSlNCL1B6aVp4cGczcVk5Z25PRHNQR2lDZGNyejErcTVhYUdoMDdXUGlISFg5K1VpbVJjRThZS1BBNXUwNTBkQ2l2eVM2a2VhZkpFalQ0UXkxcElPaFRUd3ZrMWxrR0ZmeWp3bVBqL3JMRGY4YUc3ZXZlVWQveGwxbzlKMnh5ckhvQW9heTNVNVpHYjFCZGJ2OGFGNHJLb2wwTkorUlZBSTZJSHJCUnE4OGxJeGtzSlFxTm9GQ3o5b051N011OTVkMUJpZ3ErNjZiYzBuTWcyWXZYQXdaMkh3RjAzS0xRWEFWYjZVekZ1Lzc0MjYraElNUlR1M01mZDZoa01vMllMVzlxSS9odlBsaWg4RG5qaUFTUG9Fbkx2cVFidVpXaVBnQ3h2c1F4eXFBQWdDazlzckhwaG51UnpTck95M1JmZzRYa0lndHhlb3ArUmZJOVgyaDBRcEVmcjgzYzExd0xhQkxDUmgwMlFXazA2Ty8yM2s2cWZNZHBxNVZ2b0ZnTkNJYlY1V01sSFpaV3RnVXFzaGtXRVJycjduZnVvd1BQQ0NUaHdxMC9tbUQ1NDVDb0VNWU16bUtQYlIyYmF4RkVTbUswTlRRT3VWR3A2Y3JqNWlYOGxzaU9kZ3FVNHhuSVpRcDRsT1lJcTlBOUhFS3NZZ1RuYysxRlRNazJEN05ydThlalh4UUR3amFqUTFNTmJ5cldBS0MvZ3RTWW9ONTFKY25FWFlUOWI1MVZOWWF4anArTE9oeDA0M3RNUW9TejNvN1kxbWtORlJUTmJMZWhDKzV0UkNKNjdQYk5Va29DbWxXbjFYODZxVlVsRFU0MjkxaXVLaE1YQ0lsVHlscGU2dw==",
560            "ME-RS-AesGcm-K3vx+e6IF6BOUJ2DemvsdflQq2CbolcFqdazfapauZTHdY/Hovh5zC8s5Qmfb2tRmRaluRX1gxMZfGDP52rakFnZpOzOCNGyHiI/dsiFDFbty0fEheEw+p1LrOI4zNwy7NE7ZsK0C756ggVfrhCin2Yw0KA6pALFqfWnQokx5Q43pUFd6ZGD8fwathC4NGx/hVTi9lxA2L6ScNQY9V3bEie40MKdpLQ6ELsPq+38UVJtqIgE0wJs8fDKSIGJVEPvP6wbVa+oPB/uFl5h56YeuYB2UGHdMJ54DCEoUBSd5QGoeKwjIylrZ+wXzchPXhtAfaCmqlf0fmKi9f5FQGrFwH9drf5HFE5Z/JWQC1FMKJTeBZ2CgcFvCtHuVm8VNnhhes1fUc7gL8VNOqE25LHFFQp3fBfeHXRkCmX+PAU+1N8KU6SFX0XqDr5anKAMH6thViBdno2m6K9tzqyucUnfgHYgp/cc+XXo9Ffw7v6lVTW3ls9diZwdwcs9JYqoKhWAs9dVGPz0017glpeAz01moJDPSMkhZwQh9GGWvhyeTWE9T28NS1G3cOBkW0GbgmIDjKeDDXAOjDyN7Db0FFL3TRAXthFtRXjJyZD1Xu2quYyjz1ZG70ILp0rDzzDaikUPUt1TCsAz+8NfLwHKz+H4oPUGprdUqgBVSGOySH+lKZaUbN17qIXjEKg58jh686s6i4GTD7Ndf6Xqsdc00PRDlm+jHwK7bNvkqkcChQHockIaIi4ETHCz/jqrca7uY8RIABv9Ni46+Ix1CrNY4qCUhep9oYZBGSLy2fQWWNk2nZgbrkipwUbgoV1IJV/kWCQ6ycjGG005kv3AFb6sZyrnFbvT7sa/JCKlo8gcVtzXlrJJqiO/7Qb1nTfj9dLd+/4ihpmwpFwPmKHi6zrZjJ8FbaDGkXSg+a82RQqz/AsH10hBd/tSZeZ5chdwgxTouoGix99HZipTKXLiAqW7Mo0N93+atNb9EWeHPBfsVbwJ2shBT5030QrY2qQfhTb4GUl52vPQBvpjxCzjPlvCWzFMlO8wrCP1sJm5egEb0F6Fpa9H3blBdMcb2NuKJ2VfSQzuJrbLzirnX3X0Pbk93S2dE5vs/2xsL6fqV18EPkVXO1mQtqsM8sMF8o6G/PLILN268Ga7CwcCL3qnoaCvahN3sHbciy38UH6s5hRTDvV75nWDj4oIaByrYx+JdgSZ4sucAn/bEQJCDSTVQ3sYQbEJGxc+xImNWudEoxdCmKYZPDFhUEIfO8pQRHTX8ZHZST+m97kJMuPvWg49UlrGu2YE6KbkNBEz7cSoWOuWpbrNjv1I8XKf8Sd82dvRWn3ZDc/4GXXE5oscG8UHTlz3XIpWNNrpE+wmn+AvmU0+n5r4Nv0LOFrlqH8Z2DcfjGqAVJkQMWFriruEcsPOvRgvGUeUtjulxEwcqX/UVmE5871rx0C2aJhazTnLkzt9TDFTaAf7J7zkIkhvKx8AU2A=",
561        ];
562
563        let shares: Vec<Share> = shares.iter().map(|&s| s.parse::<Share>().unwrap()).collect();
564        let shares: Vec<&Share> = shares.iter().collect();
565
566        assert_eq!(
567            reconstruct_mnemonic(&shares, Some(&password)).unwrap().to_string(),
568            mnemonic_str,
569        );
570
571        let shares = [
572            "ME-RS-AesGcm-k0X9b0HVeq8HrVh8hAIs4LFD57alOXQ9BwXiAnHsOhmb6JmnD0w85Lchg65dedqbA2D+C7TFod2izbKcXw+k+rEEoPPFQDKDC/SjYORJnqNIOjAii8VNF714jAUqOMNXgLeXlLBWf1ExHxqLyzG81VGJMjcaNo1Z+sMnvsVCp+RbdZm4iOZweBqaftkX0xXTA7Nn5uxEHdblm6Z9KzvHnWYpx/uwX7XMk60mHoLQ6FoB+Jj8sq10Cy6eMolDly1MDD3+ynCt68Cswfr2iJGUOjF2Gebgdb5CkefbGX1mMLmDHC+coi6hUyj5+7WATNd+avGjTL37r+j4tX523H4QQBcvu459x/P5OhB5vP2qSO6oNe12ACv0n6j1qjU1qBLr/OUu870/uWpXFKi7cDKDokST9uAz+2t5N0kez1T3P3BsjLtJouWqsc64C0G+/qwX7UHaNe3cUiu9J6I8aLTtbDwK3PGRQdlcZ4Y0Gb2MaxwJsg85G0iHxzM1ODJqkRQK48Q5xPXfMqztjjxf355/T47yHanmfvq4p44osJSC8FvFB03BFzT7/WspUcTL3BKZNIAvNQf04T+NHdliM7x5kg4J0Ctwk8YB/h3HtmBqyTJym51WOsNvzHACWjRt64MjOYWy04sgjj4i3vrrkQTs93bUuH4bZ/1utFjqiHho2PaE9TOgyMP5y36LWhQRJEHoLTJXVTsJX5uRbqGoUX5OWWSEMuAxg8VrdLxdsQpikbjranf1ywIAA8vLK/HSNQJM/DbXUOh9W61yLJp35ONQEAmOC+l9HnYJrwvcQVCxMaL40D/JT7AkZmya9V2+SsO8wz+mjZT0pfEGmk7c/o8I/3/rYjJoISAwipz6hSol44IdPh57V3SbTxx0nykcVRAYP0/UIPWo4dui22LlnMUOwuH+WAJFmp+Dr/AK6PTxyaz0ILcq9w+7wTRx/Hi2AmFnPWiSLTxEPw8l3SS4CXnT6o7/vWk/YgN1pJWCsI4vhhB1Nui43+VI6e3lkvdaKiLtIXmbb+CRrS89R0cGX3C8XU6E9/ai6y+TaxRCQxH9qtyeILH/qBz9ZmOjpExHWtn09MAukWkbfpBWF3p3DltHnkzXInPDpVDBCb4/9gY857LTQgo0tmtdpT9/pmdUbIqUe/0KhnYnLVkRkK2wkjB7OqfM6+vJ++vrI+2Zq0b+jgnT78Bq5xd8lwnyxiqlEp4BUbdt24v/XgM/vhXbiyastFVPbyOQ/XGjuAarC5CgvjfO9NYostkzlm1yeylRD8igxJxFbCnLUR7ANXs0BpkEQokHZZx0BN6K2E2XIta2TRDGzm8EY1V4cZ/zMYv/mO4wz6Z7yDDwPv5I+/xkbTnuun+G0kABI+L7gkgzg8RqHnor7aAjQkEcC85sYL6lnZP60mQVUY6pjKwqvaAvgQCfd4Nc3yYC/jnOdZ6FqkAunmYcnV/XCQYtjiTT6ItN75YJjRVrdLCee4wQFFQ=",
573            "ME-RS-N-Mi0yLUNBSVFBaGdESXFBRWk3b296TFVtbzNscG1jZEIxMXFESnVHbUpRUGYxUk9nOG5WQVNkR1NSTE5YQk41VytwV1dUcWVNSnVOakN3ZVd4eFk0Ylh6Y0NWTlhzYWFLNmM5UEUxWHVFT2lNVW9BeHdQNkRtM3BCT29EVklHWlFYbXZxQk9FOVYyU3FCZTlsblVRcUZBak9SQUllQjFVcWEvdlJMbXN3VWNqZlNJN0pVQWgvL3ZKZVBvbUxGUVFYcjZVSUE5dnpsaDgxVVNjaXZHUXlnOVQydWRNS202RTdveXQvcVpGam1DdUlYSFlkR1FCdkxrK01oaEErVmh2NlM2a0FkSU5veWRGUlZTdXpnU25zT25wcUJxb21oRWZaNkdmb1dsaHM5UUFadXRmeUgzdkxRT0hQeXc1TEZLbUE3dnpOTTJmMkc2dGZaZGR1Q2dnT2gydmZCUnh1ZmJSdStHN2VGSGtLdnVoOW16ekQ3YUF2Z3BRbldKakdrai9paGcyR1EyQVlBWWkrM200SXR5V3J3Q1ZRNDZlUjJCRGpmZllQK3BOTFNnL2xKNlNmbUswd204R2cvL2ZpbVBHODF4alcrckdIQkczUTV4U1JnWnlUcmt0TEFBWlY0VndJOENzdTlmSUNlT0tTYm9UVTVrOFJoWnZRS0pUNnhGS1d1K0l3OTVoWUlUZUdmVGxLa1NtdW93WmVXcFI1TjIyQUEyWENaVWVqVVBLdHlQWUYxOVNGTjRjUDlvTURuUng3bkxkY3B6cGE3QmJWQm1jRGNhTENZVW1PeXJKcDQwK1hoekJHVmlxVnBJTS9qNTJJQTg1TSt1TDVtM0xNUk12UFc4cEliNkpVYVlKV0FXSldWV3JKdlpzUGJrdmh3T0NlOXo5VWJUTXE1WUJzOC9OcFJnN0F4L2lmSTJ5ZHdxbDRacXd4N29MM0ZrK0daK1FDeHRyTWJSK2oxTzhROXFXOEJ5eEcveXFBQWdDazlzckhwaG51UnpTck95M1JmZzRYa0lndHhlb3ArUmZJOVgyaDBRcEVmcjgzYzExd0xhQkxDUmgwMlFXazA2Ty8yM2s2cWZNZHBxNVZ2b0ZnTkNJYlY1V01sSFpaV3RnVXFzaGtXRVJycjduZnVvd1BQQ0NUaHdxMC9tbUQ1NDVDb0VNWU16bUtQYlIyYmF4RkVTbUswTlRRT3VWR3A2Y3JqNWlYOGxzaU9kZ3FVNHhuSVpRcDRsT1lJcTlBOUhFS3NZZ1RuYysxRlRNazJEN05ydThlalh4UUR3amFqUTFNTmJ5cldBS0MvZ3RTWW9ONTFKY25FWFlUOWI1MVZOWWF4anArTE9oeDA0M3RNUW9TejNvN1kxbWtORlJUTmJMZWhDKzV0UkNKNjdQYk5Va29DbWxXbjFYODZxVlVsRFU0MjkxaXVLaE1YQ0lsVHlscGU2dw==",
574        ];
575        let shares: Vec<Share> = shares.iter().map(|&s| s.parse::<Share>().unwrap()).collect();
576        let shares: Vec<&Share> = shares.iter().collect();
577        assert_eq!(
578            reconstruct_mnemonic(&shares, Some(&password)).unwrap().to_string(),
579            mnemonic_str
580        );
581
582        let shares = [
583            "ME-RS-N-Mi0xLUNBRVFBaGdESXFBRStPVUZYZTJnMTdLRFY1L2pWRllQTHdtZ0dCWExJbitjTERReFRyRHArWGNVMG5yY3UyVmFONFEvZkVoeXNadm5qNFhmRDVIZXZ3eHB2bENTYnZIZTFtOTlXdjJwby8zVWl0d2VhMnVWOTZaejB5WmhEdHlkRDFYcEg1R0RIYXFvZDBpTHdpcDZ3d1k5T0VWdEJhZmtkUVRGaTNNM3gvY2dsK0FDWVQ5WG50TlJycnRtWFRTUGZ4MG54R1lVc0NWUnNKY3h5Q0JxSHBlRGVRekpSTlFxVldMNGpJU3JCZkFRcEpYMnJoT1o4OXM1V3VLaW5PWFd0YUZncTRnd2t1VzR0ZkJJZzVUMjFlaXpGNEpWNzlMcXFXSDZoY3N0Z1huYzZYWTJvZjRvaytlYnJWOFBmR1lOU1NxRWQ4VFpqUzlBL0h0clJGNThEbUdaL2Z2Nmp5MjJjS01hUWllK1ZqdFZ4OUJyblJjWThYYTgxWmNTWlF4YlFLbFQ3MC9tRk5aQlN4ZXNLTWVTU24vV2hycEs0OU80ZW4zRkZJVTJqd2lLcGwybHpHMk0vdThJTzRZSlNCL1B6aVp4cGczcVk5Z25PRHNQR2lDZGNyejErcTVhYUdoMDdXUGlISFg5K1VpbVJjRThZS1BBNXUwNTBkQ2l2eVM2a2VhZkpFalQ0UXkxcElPaFRUd3ZrMWxrR0ZmeWp3bVBqL3JMRGY4YUc3ZXZlVWQveGwxbzlKMnh5ckhvQW9heTNVNVpHYjFCZGJ2OGFGNHJLb2wwTkorUlZBSTZJSHJCUnE4OGxJeGtzSlFxTm9GQ3o5b051N011OTVkMUJpZ3ErNjZiYzBuTWcyWXZYQXdaMkh3RjAzS0xRWEFWYjZVekZ1Lzc0MjYraElNUlR1M01mZDZoa01vMllMVzlxSS9odlBsaWg4RG5qaUFTUG9Fbkx2cVFidVpXaVBnQ3h2c1F4eXFBQWdDazlzckhwaG51UnpTck95M1JmZzRYa0lndHhlb3ArUmZJOVgyaDBRcEVmcjgzYzExd0xhQkxDUmgwMlFXazA2Ty8yM2s2cWZNZHBxNVZ2b0ZnTkNJYlY1V01sSFpaV3RnVXFzaGtXRVJycjduZnVvd1BQQ0NUaHdxMC9tbUQ1NDVDb0VNWU16bUtQYlIyYmF4RkVTbUswTlRRT3VWR3A2Y3JqNWlYOGxzaU9kZ3FVNHhuSVpRcDRsT1lJcTlBOUhFS3NZZ1RuYysxRlRNazJEN05ydThlalh4UUR3amFqUTFNTmJ5cldBS0MvZ3RTWW9ONTFKY25FWFlUOWI1MVZOWWF4anArTE9oeDA0M3RNUW9TejNvN1kxbWtORlJUTmJMZWhDKzV0UkNKNjdQYk5Va29DbWxXbjFYODZxVlVsRFU0MjkxaXVLaE1YQ0lsVHlscGU2dw==",
584            "ME-RS-N-Mi0yLUNBSVFBaGdESXFBRWk3b296TFVtbzNscG1jZEIxMXFESnVHbUpRUGYxUk9nOG5WQVNkR1NSTE5YQk41VytwV1dUcWVNSnVOakN3ZVd4eFk0Ylh6Y0NWTlhzYWFLNmM5UEUxWHVFT2lNVW9BeHdQNkRtM3BCT29EVklHWlFYbXZxQk9FOVYyU3FCZTlsblVRcUZBak9SQUllQjFVcWEvdlJMbXN3VWNqZlNJN0pVQWgvL3ZKZVBvbUxGUVFYcjZVSUE5dnpsaDgxVVNjaXZHUXlnOVQydWRNS202RTdveXQvcVpGam1DdUlYSFlkR1FCdkxrK01oaEErVmh2NlM2a0FkSU5veWRGUlZTdXpnU25zT25wcUJxb21oRWZaNkdmb1dsaHM5UUFadXRmeUgzdkxRT0hQeXc1TEZLbUE3dnpOTTJmMkc2dGZaZGR1Q2dnT2gydmZCUnh1ZmJSdStHN2VGSGtLdnVoOW16ekQ3YUF2Z3BRbldKakdrai9paGcyR1EyQVlBWWkrM200SXR5V3J3Q1ZRNDZlUjJCRGpmZllQK3BOTFNnL2xKNlNmbUswd204R2cvL2ZpbVBHODF4alcrckdIQkczUTV4U1JnWnlUcmt0TEFBWlY0VndJOENzdTlmSUNlT0tTYm9UVTVrOFJoWnZRS0pUNnhGS1d1K0l3OTVoWUlUZUdmVGxLa1NtdW93WmVXcFI1TjIyQUEyWENaVWVqVVBLdHlQWUYxOVNGTjRjUDlvTURuUng3bkxkY3B6cGE3QmJWQm1jRGNhTENZVW1PeXJKcDQwK1hoekJHVmlxVnBJTS9qNTJJQTg1TSt1TDVtM0xNUk12UFc4cEliNkpVYVlKV0FXSldWV3JKdlpzUGJrdmh3T0NlOXo5VWJUTXE1WUJzOC9OcFJnN0F4L2lmSTJ5ZHdxbDRacXd4N29MM0ZrK0daK1FDeHRyTWJSK2oxTzhROXFXOEJ5eEcveXFBQWdDazlzckhwaG51UnpTck95M1JmZzRYa0lndHhlb3ArUmZJOVgyaDBRcEVmcjgzYzExd0xhQkxDUmgwMlFXazA2Ty8yM2s2cWZNZHBxNVZ2b0ZnTkNJYlY1V01sSFpaV3RnVXFzaGtXRVJycjduZnVvd1BQQ0NUaHdxMC9tbUQ1NDVDb0VNWU16bUtQYlIyYmF4RkVTbUswTlRRT3VWR3A2Y3JqNWlYOGxzaU9kZ3FVNHhuSVpRcDRsT1lJcTlBOUhFS3NZZ1RuYysxRlRNazJEN05ydThlalh4UUR3amFqUTFNTmJ5cldBS0MvZ3RTWW9ONTFKY25FWFlUOWI1MVZOWWF4anArTE9oeDA0M3RNUW9TejNvN1kxbWtORlJUTmJMZWhDKzV0UkNKNjdQYk5Va29DbWxXbjFYODZxVlVsRFU0MjkxaXVLaE1YQ0lsVHlscGU2dw==",
585        ];
586        let shares: Vec<Share> = shares.iter().map(|&s| s.parse::<Share>().unwrap()).collect();
587        let shares: Vec<&Share> = shares.iter().collect();
588        assert_eq!(reconstruct_mnemonic(&shares, None).unwrap().to_string(), mnemonic_str);
589    }
590
591    #[test]
592    fn test_aes_gcm_encrypt_decrypt() {
593        let key: SecretSlice<u8> = "key".to_string().into_bytes().into();
594        let data = ShareData("my secret data".as_bytes().to_vec().into());
595
596        assert_eq!(
597            decrypt_with_password(&encrypt_with_password(&data, &key).unwrap(), &key).unwrap(),
598            data,
599        );
600    }
601
602    #[test]
603    fn test_aes_gcm_encrypt_decrypt_wrong_key() {
604        let key: SecretSlice<u8> = "key".to_string().into_bytes().into();
605        let wrong_key: SecretSlice<u8> = "wrong key".to_string().into_bytes().into();
606
607        let data = ShareData("my secret data".as_bytes().to_vec().into());
608
609        assert!(decrypt_with_password(&encrypt_with_password(&data, &key).unwrap(), &wrong_key).is_err());
610    }
611
612    #[test]
613    fn test_aes_gcm_decrypt_examples() {
614        let key: SecretSlice<u8> = "key three".to_string().into_bytes().into();
615        let data = ShareData("secret".as_bytes().to_vec().into());
616
617        // Tests to make sure old generated encrypted data is still recoverable. The three examples
618        // are generated with this code:
619        // println!("{:?}", encrypt_with_password(data, &key).unwrap());
620
621        assert_eq!(
622            decrypt_with_password(
623                &ShareData(Box::new([
624                    32, 112, 222, 26, 190, 160, 235, 203, 235, 74, 13, 213, 181, 30, 151, 28, 60, 146, 145, 37, 128,
625                    57, 80, 202, 77, 21, 179, 21, 100, 60, 85, 127, 68, 223,
626                ])),
627                &key
628            )
629            .unwrap(),
630            data
631        );
632
633        assert_eq!(
634            decrypt_with_password(
635                &ShareData(Box::new([
636                    76, 61, 16, 170, 160, 112, 228, 107, 253, 241, 246, 102, 145, 90, 79, 73, 157, 173, 81, 106, 1,
637                    200, 23, 180, 127, 225, 147, 226, 233, 110, 94, 50, 150, 110
638                ])),
639                &key
640            )
641            .unwrap(),
642            data
643        );
644
645        assert_eq!(
646            decrypt_with_password(
647                &ShareData(Box::new([
648                    138, 139, 139, 160, 101, 108, 251, 7, 211, 55, 8, 160, 244, 248, 42, 23, 172, 229, 68, 143, 129,
649                    245, 6, 117, 192, 226, 109, 184, 0, 84, 68, 165, 143, 201
650                ])),
651                &key
652            )
653            .unwrap(),
654            data
655        );
656    }
657}