etopay_sdk/wallet/
share.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD};
2use iota_sdk::crypto::{
3    hashes::{Digest, blake2b::Blake2b256},
4    keys::bip39::Mnemonic,
5};
6use secrecy::{ExposeSecret, SecretBox, SecretSlice, SecretString};
7use std::str::FromStr;
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.
220pub fn create_shares_from_mnemonic(
221    mnemonic: impl Into<Mnemonic>,
222    password: &SecretSlice<u8>,
223) -> super::error::Result<GeneratedShares> {
224    let mnemonic: Mnemonic = mnemonic.into();
225
226    // convert the mnemonic string into the raw entropy it encodes
227    let entropy =
228        iota_sdk::crypto::keys::bip39::wordlist::decode(&mnemonic, &iota_sdk::crypto::keys::bip39::wordlist::ENGLISH)?;
229
230    let entropy_bytes: &[u8] = entropy.as_ref();
231    create_shares_from_secret(PayloadType::MnemonicEntropy, &entropy_bytes.to_vec().into(), password)
232        .map_err(Into::into)
233}
234
235/// Reconstruct a [`Mnemonic`] from the shares. Can be used to initialize a wallet using the
236/// [`iota_sdk::client::secret::mnemonic::MnemonicSecretManager::try_from_mnemonic`] function.
237pub fn reconstruct_mnemonic(
238    shares: &[&Share],
239    password: Option<&SecretSlice<u8>>,
240) -> super::error::Result<SecretBox<Mnemonic>> {
241    let (payload_type, secret) = reconstruct_secret(shares, password)?;
242    match payload_type {
243        PayloadType::MnemonicEntropy => Ok(SecretBox::new(Box::new(
244            iota_sdk::crypto::keys::bip39::wordlist::encode(
245                secret.expose_secret(),
246                &iota_sdk::crypto::keys::bip39::wordlist::ENGLISH,
247            )?,
248        ))),
249    }
250}
251
252/// Creates shares from any secret represented as a vector of bytes.
253fn create_shares_from_secret(
254    payload_type: PayloadType,
255    secret: &SecretSlice<u8>,
256    password: &SecretSlice<u8>,
257) -> Result<GeneratedShares, ShareError> {
258    let out = rusty_secrets::dss::ss1::split_secret(
259        2,
260        3,
261        secret.expose_secret(),
262        // we specify reproducibility since we want to be able to regenerate the local share from
263        // the others, and we need the signatures to match
264        rusty_secrets::dss::ss1::Reproducibility::seeded("etopay".to_owned().into_bytes()),
265        &None,
266    )?;
267
268    let mut share_data_iter = out.into_iter().map(|s| Share {
269        payload_type,
270        encoding: Encoding::RustySecrets,
271        encryption: Encryption::None,
272        data: ShareData(s.into_string().into_bytes().into()),
273    });
274
275    let recovery = share_data_iter.next().ok_or(ShareError::NotEnoughParts)?;
276    let local = share_data_iter.next().ok_or(ShareError::NotEnoughParts)?;
277    let mut backup = share_data_iter.next().ok_or(ShareError::NotEnoughParts)?;
278
279    // encrypt the backup / recovery share(s) with the password
280    backup.encryption = Encryption::AesGcm;
281    backup.data = encrypt_with_password(&backup.data, password)?;
282
283    Ok(GeneratedShares {
284        recovery,
285        local,
286        backup,
287    })
288}
289
290/// Reconstruct the secret from provided shares.
291fn reconstruct_secret(
292    shares: &[&Share],
293    password: Option<&SecretSlice<u8>>,
294) -> Result<(PayloadType, SecretSlice<u8>), ShareError> {
295    let Some(share) = shares.first() else {
296        return Err(ShareError::NotEnoughShares {
297            provided: shares.len(),
298            required: 2,
299        });
300    };
301
302    // make sure all shares have the same payload type
303    let payload_type = share.payload_type;
304    if !shares.iter().all(|s| s.payload_type == payload_type) {
305        return Err(ShareError::IncompatibleShares(format!(
306            "All shares must have the same PayloadType, first share is `{payload_type}`"
307        )));
308    }
309
310    // make sure all shares have the same encoding
311    let encoding = share.encoding;
312    if !shares.iter().all(|s| s.encoding == encoding) {
313        return Err(ShareError::IncompatibleShares(format!(
314            "All shares must use the same Encoding, first share uses `{encoding}`"
315        )));
316    }
317
318    // decrypt any encrypted shares with the password
319    let share_data = shares
320        .iter()
321        .map(|&s| match s.encryption {
322            Encryption::None => Ok(s.data.clone()),
323            Encryption::AesGcm => {
324                let Some(password) = password else {
325                    return Err(ShareError::PasswordNotProvided);
326                };
327                decrypt_with_password(&s.data, password)
328            }
329        })
330        .collect::<Result<Vec<ShareData>, ShareError>>()?;
331
332    match encoding {
333        Encoding::RustySecrets => {
334            // use the rusty_secrets to get the secret back
335            let rusty_secrets_shares = share_data
336                .iter()
337                .map(|s| rusty_secrets::dss::ss1::Share::from_string(&String::from_utf8_lossy(&s.0)))
338                .collect::<Result<Vec<rusty_secrets::dss::ss1::Share>, _>>()?;
339
340            let (secret, _access_structure, _metadata) =
341                rusty_secrets::dss::ss1::recover_secret(&rusty_secrets_shares)?;
342
343            Ok((payload_type, SecretBox::new(secret.into())))
344        }
345    }
346}
347
348fn encrypt_with_password(data: &ShareData, key: &SecretSlice<u8>) -> Result<ShareData, ShareError> {
349    use aes_gcm::{
350        Aes256Gcm, Key,
351        aead::{Aead, AeadCore, KeyInit, OsRng},
352    };
353
354    // create a random nonce value (96-bit for AesGcm256) since it needs to be unique for each
355    // encryption with the same key
356    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
357
358    // hash the key string with the nonce to use as encryption key
359    let key = Blake2b256::new()
360        .chain_update(key.expose_secret())
361        .chain_update(nonce)
362        .finalize();
363    // panics if length is invalid but since we have provided exactly 256 bits it is fine
364    let key = Key::<aes_gcm::Aes256Gcm>::from_slice(key.as_slice());
365
366    let cipher = Aes256Gcm::new(key);
367
368    // encrypt the data and prepend the nonce value (to use while decrypting)
369    let encrypted = cipher
370        .encrypt(&nonce, data.0.as_ref())
371        .map_err(|_| ShareError::EncryptionError("Error encrypting share using password"))?;
372
373    let mut data = nonce.to_vec();
374    data.extend(encrypted);
375    Ok(ShareData(data.into()))
376}
377
378fn decrypt_with_password(data: &ShareData, key: &SecretSlice<u8>) -> Result<ShareData, ShareError> {
379    use aes_gcm::{
380        Aes256Gcm, Key, Nonce,
381        aead::{Aead, KeyInit},
382    };
383
384    let data = &data.0;
385
386    // split the nonce and the data
387    let nonce_bytes = 96 / 8;
388    if data.len() <= nonce_bytes {
389        return Err(ShareError::InvalidShareFormat(format!(
390            "not enough data for decryption, need at least {nonce_bytes} but {} bytes provided",
391            data.len()
392        )));
393    }
394    let (nonce, data) = data.split_at(nonce_bytes);
395
396    let nonce = Nonce::from_slice(nonce);
397
398    // hash the key string and nonce to use as encryption key
399    let key = Blake2b256::new()
400        .chain_update(key.expose_secret())
401        .chain_update(nonce)
402        .finalize();
403    // panics if length is invalid but since we have provided exactly 256 bits it is fine
404    let key = Key::<aes_gcm::Aes256Gcm>::from_slice(key.as_slice());
405
406    let cipher = Aes256Gcm::new(key);
407
408    Ok(ShareData(
409        cipher
410            .decrypt(nonce, data)
411            .map_err(|_| ShareError::EncryptionError("Error decrypting share using password"))?
412            .into(),
413    ))
414}
415
416#[cfg(test)]
417mod test {
418    use super::*;
419    use secrecy::SecretBox;
420
421    #[test]
422    fn test_string_serialization() {
423        let s = Share {
424            payload_type: super::PayloadType::MnemonicEntropy,
425            encoding: Encoding::RustySecrets,
426            encryption: super::Encryption::None,
427            data: ShareData("data".to_string().into_bytes().into()),
428        };
429        println!("{:?}, {}, {:?}", s.to_string(), s.to_string().expose_secret(), s);
430
431        let parsed = s.to_string().expose_secret().parse::<Share>().unwrap();
432        assert_eq!(parsed, s);
433    }
434
435    #[test]
436    fn test_split_recover_secret() {
437        let secret = SecretBox::new("secret".to_string().into_bytes().into());
438        let password = SecretBox::new("password".to_string().into_bytes().into());
439
440        let shares = create_shares_from_secret(PayloadType::MnemonicEntropy, &secret, &password).unwrap();
441
442        assert_eq!(
443            reconstruct_secret(&[&shares.backup, &shares.local], Some(&password))
444                .unwrap()
445                .1
446                .expose_secret(),
447            secret.expose_secret()
448        );
449        assert_eq!(
450            reconstruct_secret(&[&shares.backup, &shares.recovery], Some(&password))
451                .unwrap()
452                .1
453                .expose_secret(),
454            secret.expose_secret()
455        );
456        assert_eq!(
457            reconstruct_secret(&[&shares.recovery, &shares.local], None)
458                .unwrap()
459                .1
460                .expose_secret(),
461            secret.expose_secret()
462        );
463        assert_eq!(
464            reconstruct_secret(&[&shares.recovery, &shares.local, &shares.backup], Some(&password))
465                .unwrap()
466                .1
467                .expose_secret(),
468            secret.expose_secret()
469        );
470
471        assert!(reconstruct_secret(&[&shares.recovery], Some(&password)).is_err());
472        assert!(reconstruct_secret(&[&shares.local], Some(&password)).is_err());
473        assert!(reconstruct_secret(&[&shares.backup], Some(&password)).is_err());
474    }
475
476    #[test]
477    fn test_split_recover_local() {
478        // Arrange
479        let secret = SecretBox::new("my hex string".to_string().into_bytes().into());
480        let password = SecretBox::new("password".to_string().into_bytes().into());
481
482        let shares = create_shares_from_secret(PayloadType::MnemonicEntropy, &secret, &password).unwrap();
483
484        // reconstruct using backup and recovery
485        let (_, reconstructed_secret) =
486            reconstruct_secret(&[&shares.backup, &shares.recovery], Some(&password)).unwrap();
487
488        // now create shares again and make sure we
489        let new_shares =
490            create_shares_from_secret(PayloadType::MnemonicEntropy, &reconstructed_secret, &password).unwrap();
491
492        // reconstruct using a mix of old and "new" shares
493        let (_, final_secret) = reconstruct_secret(&[&shares.backup, &new_shares.local], Some(&password)).unwrap();
494
495        assert_eq!(final_secret.expose_secret(), secret.expose_secret());
496    }
497
498    #[test]
499    fn test_split_recover_mnemonic() {
500        // Arrange
501        let password = SecretBox::new("password".to_string().into_bytes().into());
502
503        let mnemonic = iota_sdk::client::Client::generate_mnemonic().unwrap();
504
505        // Perform and check
506        let shares = create_shares_from_mnemonic(mnemonic.clone(), &password).unwrap();
507
508        assert_eq!(
509            reconstruct_mnemonic(&[&shares.backup, &shares.local], Some(&password))
510                .unwrap()
511                .expose_secret()
512                .as_bytes(),
513            mnemonic.as_bytes()
514        );
515        assert_eq!(
516            reconstruct_mnemonic(&[&shares.backup, &shares.recovery], Some(&password))
517                .unwrap()
518                .expose_secret()
519                .as_bytes(),
520            mnemonic.as_bytes()
521        );
522        assert_eq!(
523            reconstruct_mnemonic(&[&shares.recovery, &shares.local], None)
524                .unwrap()
525                .expose_secret()
526                .as_bytes(),
527            mnemonic.as_bytes()
528        );
529        assert_eq!(
530            reconstruct_mnemonic(&[&shares.recovery, &shares.local, &shares.backup], Some(&password))
531                .unwrap()
532                .expose_secret()
533                .as_bytes(),
534            mnemonic.as_bytes()
535        );
536
537        assert!(reconstruct_mnemonic(&[&shares.recovery], Some(&password)).is_err());
538        assert!(reconstruct_mnemonic(&[&shares.local], Some(&password)).is_err());
539        assert!(reconstruct_mnemonic(&[&shares.backup], Some(&password)).is_err());
540    }
541
542    #[test]
543    fn test_split_recover_mnemonic_example() {
544        let password: SecretSlice<u8> = "mnemonic share password".to_string().into_bytes().into();
545
546        // the shares below have been generated with this code:
547        // let mnemonic = iota_sdk::client::Client::generate_mnemonic().unwrap();
548        // println!("{}", mnemonic.to_string());
549        // let shares = creae_shares_from_mnemonic(mnemonic.clone(), &password).unwrap();
550        // println!(
551        //     "{}\n{}\n{}\n\n",
552        //     shares.recovery.to_string().expose_secret(),
553        //     shares.backup.to_string().expose_secret(),
554        //     shares.local.to_string().expose_secret(),
555        // );
556
557        // Perform and check
558        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";
559
560        let shares = [
561            "ME-RS-N-Mi0xLUNBRVFBaGdESXFBRStPVUZYZTJnMTdLRFY1L2pWRllQTHdtZ0dCWExJbitjTERReFRyRHArWGNVMG5yY3UyVmFONFEvZkVoeXNadm5qNFhmRDVIZXZ3eHB2bENTYnZIZTFtOTlXdjJwby8zVWl0d2VhMnVWOTZaejB5WmhEdHlkRDFYcEg1R0RIYXFvZDBpTHdpcDZ3d1k5T0VWdEJhZmtkUVRGaTNNM3gvY2dsK0FDWVQ5WG50TlJycnRtWFRTUGZ4MG54R1lVc0NWUnNKY3h5Q0JxSHBlRGVRekpSTlFxVldMNGpJU3JCZkFRcEpYMnJoT1o4OXM1V3VLaW5PWFd0YUZncTRnd2t1VzR0ZkJJZzVUMjFlaXpGNEpWNzlMcXFXSDZoY3N0Z1huYzZYWTJvZjRvaytlYnJWOFBmR1lOU1NxRWQ4VFpqUzlBL0h0clJGNThEbUdaL2Z2Nmp5MjJjS01hUWllK1ZqdFZ4OUJyblJjWThYYTgxWmNTWlF4YlFLbFQ3MC9tRk5aQlN4ZXNLTWVTU24vV2hycEs0OU80ZW4zRkZJVTJqd2lLcGwybHpHMk0vdThJTzRZSlNCL1B6aVp4cGczcVk5Z25PRHNQR2lDZGNyejErcTVhYUdoMDdXUGlISFg5K1VpbVJjRThZS1BBNXUwNTBkQ2l2eVM2a2VhZkpFalQ0UXkxcElPaFRUd3ZrMWxrR0ZmeWp3bVBqL3JMRGY4YUc3ZXZlVWQveGwxbzlKMnh5ckhvQW9heTNVNVpHYjFCZGJ2OGFGNHJLb2wwTkorUlZBSTZJSHJCUnE4OGxJeGtzSlFxTm9GQ3o5b051N011OTVkMUJpZ3ErNjZiYzBuTWcyWXZYQXdaMkh3RjAzS0xRWEFWYjZVekZ1Lzc0MjYraElNUlR1M01mZDZoa01vMllMVzlxSS9odlBsaWg4RG5qaUFTUG9Fbkx2cVFidVpXaVBnQ3h2c1F4eXFBQWdDazlzckhwaG51UnpTck95M1JmZzRYa0lndHhlb3ArUmZJOVgyaDBRcEVmcjgzYzExd0xhQkxDUmgwMlFXazA2Ty8yM2s2cWZNZHBxNVZ2b0ZnTkNJYlY1V01sSFpaV3RnVXFzaGtXRVJycjduZnVvd1BQQ0NUaHdxMC9tbUQ1NDVDb0VNWU16bUtQYlIyYmF4RkVTbUswTlRRT3VWR3A2Y3JqNWlYOGxzaU9kZ3FVNHhuSVpRcDRsT1lJcTlBOUhFS3NZZ1RuYysxRlRNazJEN05ydThlalh4UUR3amFqUTFNTmJ5cldBS0MvZ3RTWW9ONTFKY25FWFlUOWI1MVZOWWF4anArTE9oeDA0M3RNUW9TejNvN1kxbWtORlJUTmJMZWhDKzV0UkNKNjdQYk5Va29DbWxXbjFYODZxVlVsRFU0MjkxaXVLaE1YQ0lsVHlscGU2dw==",
562            "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=",
563        ];
564
565        let shares: Vec<Share> = shares.iter().map(|&s| s.parse::<Share>().unwrap()).collect();
566        let shares: Vec<&Share> = shares.iter().collect();
567
568        assert_eq!(
569            reconstruct_mnemonic(&shares, Some(&password))
570                .unwrap()
571                .expose_secret()
572                .to_string(),
573            mnemonic_str,
574        );
575
576        let shares = [
577            "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=",
578            "ME-RS-N-Mi0yLUNBSVFBaGdESXFBRWk3b296TFVtbzNscG1jZEIxMXFESnVHbUpRUGYxUk9nOG5WQVNkR1NSTE5YQk41VytwV1dUcWVNSnVOakN3ZVd4eFk0Ylh6Y0NWTlhzYWFLNmM5UEUxWHVFT2lNVW9BeHdQNkRtM3BCT29EVklHWlFYbXZxQk9FOVYyU3FCZTlsblVRcUZBak9SQUllQjFVcWEvdlJMbXN3VWNqZlNJN0pVQWgvL3ZKZVBvbUxGUVFYcjZVSUE5dnpsaDgxVVNjaXZHUXlnOVQydWRNS202RTdveXQvcVpGam1DdUlYSFlkR1FCdkxrK01oaEErVmh2NlM2a0FkSU5veWRGUlZTdXpnU25zT25wcUJxb21oRWZaNkdmb1dsaHM5UUFadXRmeUgzdkxRT0hQeXc1TEZLbUE3dnpOTTJmMkc2dGZaZGR1Q2dnT2gydmZCUnh1ZmJSdStHN2VGSGtLdnVoOW16ekQ3YUF2Z3BRbldKakdrai9paGcyR1EyQVlBWWkrM200SXR5V3J3Q1ZRNDZlUjJCRGpmZllQK3BOTFNnL2xKNlNmbUswd204R2cvL2ZpbVBHODF4alcrckdIQkczUTV4U1JnWnlUcmt0TEFBWlY0VndJOENzdTlmSUNlT0tTYm9UVTVrOFJoWnZRS0pUNnhGS1d1K0l3OTVoWUlUZUdmVGxLa1NtdW93WmVXcFI1TjIyQUEyWENaVWVqVVBLdHlQWUYxOVNGTjRjUDlvTURuUng3bkxkY3B6cGE3QmJWQm1jRGNhTENZVW1PeXJKcDQwK1hoekJHVmlxVnBJTS9qNTJJQTg1TSt1TDVtM0xNUk12UFc4cEliNkpVYVlKV0FXSldWV3JKdlpzUGJrdmh3T0NlOXo5VWJUTXE1WUJzOC9OcFJnN0F4L2lmSTJ5ZHdxbDRacXd4N29MM0ZrK0daK1FDeHRyTWJSK2oxTzhROXFXOEJ5eEcveXFBQWdDazlzckhwaG51UnpTck95M1JmZzRYa0lndHhlb3ArUmZJOVgyaDBRcEVmcjgzYzExd0xhQkxDUmgwMlFXazA2Ty8yM2s2cWZNZHBxNVZ2b0ZnTkNJYlY1V01sSFpaV3RnVXFzaGtXRVJycjduZnVvd1BQQ0NUaHdxMC9tbUQ1NDVDb0VNWU16bUtQYlIyYmF4RkVTbUswTlRRT3VWR3A2Y3JqNWlYOGxzaU9kZ3FVNHhuSVpRcDRsT1lJcTlBOUhFS3NZZ1RuYysxRlRNazJEN05ydThlalh4UUR3amFqUTFNTmJ5cldBS0MvZ3RTWW9ONTFKY25FWFlUOWI1MVZOWWF4anArTE9oeDA0M3RNUW9TejNvN1kxbWtORlJUTmJMZWhDKzV0UkNKNjdQYk5Va29DbWxXbjFYODZxVlVsRFU0MjkxaXVLaE1YQ0lsVHlscGU2dw==",
579        ];
580        let shares: Vec<Share> = shares.iter().map(|&s| s.parse::<Share>().unwrap()).collect();
581        let shares: Vec<&Share> = shares.iter().collect();
582        assert_eq!(
583            reconstruct_mnemonic(&shares, Some(&password))
584                .unwrap()
585                .expose_secret()
586                .to_string(),
587            mnemonic_str
588        );
589
590        let shares = [
591            "ME-RS-N-Mi0xLUNBRVFBaGdESXFBRStPVUZYZTJnMTdLRFY1L2pWRllQTHdtZ0dCWExJbitjTERReFRyRHArWGNVMG5yY3UyVmFONFEvZkVoeXNadm5qNFhmRDVIZXZ3eHB2bENTYnZIZTFtOTlXdjJwby8zVWl0d2VhMnVWOTZaejB5WmhEdHlkRDFYcEg1R0RIYXFvZDBpTHdpcDZ3d1k5T0VWdEJhZmtkUVRGaTNNM3gvY2dsK0FDWVQ5WG50TlJycnRtWFRTUGZ4MG54R1lVc0NWUnNKY3h5Q0JxSHBlRGVRekpSTlFxVldMNGpJU3JCZkFRcEpYMnJoT1o4OXM1V3VLaW5PWFd0YUZncTRnd2t1VzR0ZkJJZzVUMjFlaXpGNEpWNzlMcXFXSDZoY3N0Z1huYzZYWTJvZjRvaytlYnJWOFBmR1lOU1NxRWQ4VFpqUzlBL0h0clJGNThEbUdaL2Z2Nmp5MjJjS01hUWllK1ZqdFZ4OUJyblJjWThYYTgxWmNTWlF4YlFLbFQ3MC9tRk5aQlN4ZXNLTWVTU24vV2hycEs0OU80ZW4zRkZJVTJqd2lLcGwybHpHMk0vdThJTzRZSlNCL1B6aVp4cGczcVk5Z25PRHNQR2lDZGNyejErcTVhYUdoMDdXUGlISFg5K1VpbVJjRThZS1BBNXUwNTBkQ2l2eVM2a2VhZkpFalQ0UXkxcElPaFRUd3ZrMWxrR0ZmeWp3bVBqL3JMRGY4YUc3ZXZlVWQveGwxbzlKMnh5ckhvQW9heTNVNVpHYjFCZGJ2OGFGNHJLb2wwTkorUlZBSTZJSHJCUnE4OGxJeGtzSlFxTm9GQ3o5b051N011OTVkMUJpZ3ErNjZiYzBuTWcyWXZYQXdaMkh3RjAzS0xRWEFWYjZVekZ1Lzc0MjYraElNUlR1M01mZDZoa01vMllMVzlxSS9odlBsaWg4RG5qaUFTUG9Fbkx2cVFidVpXaVBnQ3h2c1F4eXFBQWdDazlzckhwaG51UnpTck95M1JmZzRYa0lndHhlb3ArUmZJOVgyaDBRcEVmcjgzYzExd0xhQkxDUmgwMlFXazA2Ty8yM2s2cWZNZHBxNVZ2b0ZnTkNJYlY1V01sSFpaV3RnVXFzaGtXRVJycjduZnVvd1BQQ0NUaHdxMC9tbUQ1NDVDb0VNWU16bUtQYlIyYmF4RkVTbUswTlRRT3VWR3A2Y3JqNWlYOGxzaU9kZ3FVNHhuSVpRcDRsT1lJcTlBOUhFS3NZZ1RuYysxRlRNazJEN05ydThlalh4UUR3amFqUTFNTmJ5cldBS0MvZ3RTWW9ONTFKY25FWFlUOWI1MVZOWWF4anArTE9oeDA0M3RNUW9TejNvN1kxbWtORlJUTmJMZWhDKzV0UkNKNjdQYk5Va29DbWxXbjFYODZxVlVsRFU0MjkxaXVLaE1YQ0lsVHlscGU2dw==",
592            "ME-RS-N-Mi0yLUNBSVFBaGdESXFBRWk3b296TFVtbzNscG1jZEIxMXFESnVHbUpRUGYxUk9nOG5WQVNkR1NSTE5YQk41VytwV1dUcWVNSnVOakN3ZVd4eFk0Ylh6Y0NWTlhzYWFLNmM5UEUxWHVFT2lNVW9BeHdQNkRtM3BCT29EVklHWlFYbXZxQk9FOVYyU3FCZTlsblVRcUZBak9SQUllQjFVcWEvdlJMbXN3VWNqZlNJN0pVQWgvL3ZKZVBvbUxGUVFYcjZVSUE5dnpsaDgxVVNjaXZHUXlnOVQydWRNS202RTdveXQvcVpGam1DdUlYSFlkR1FCdkxrK01oaEErVmh2NlM2a0FkSU5veWRGUlZTdXpnU25zT25wcUJxb21oRWZaNkdmb1dsaHM5UUFadXRmeUgzdkxRT0hQeXc1TEZLbUE3dnpOTTJmMkc2dGZaZGR1Q2dnT2gydmZCUnh1ZmJSdStHN2VGSGtLdnVoOW16ekQ3YUF2Z3BRbldKakdrai9paGcyR1EyQVlBWWkrM200SXR5V3J3Q1ZRNDZlUjJCRGpmZllQK3BOTFNnL2xKNlNmbUswd204R2cvL2ZpbVBHODF4alcrckdIQkczUTV4U1JnWnlUcmt0TEFBWlY0VndJOENzdTlmSUNlT0tTYm9UVTVrOFJoWnZRS0pUNnhGS1d1K0l3OTVoWUlUZUdmVGxLa1NtdW93WmVXcFI1TjIyQUEyWENaVWVqVVBLdHlQWUYxOVNGTjRjUDlvTURuUng3bkxkY3B6cGE3QmJWQm1jRGNhTENZVW1PeXJKcDQwK1hoekJHVmlxVnBJTS9qNTJJQTg1TSt1TDVtM0xNUk12UFc4cEliNkpVYVlKV0FXSldWV3JKdlpzUGJrdmh3T0NlOXo5VWJUTXE1WUJzOC9OcFJnN0F4L2lmSTJ5ZHdxbDRacXd4N29MM0ZrK0daK1FDeHRyTWJSK2oxTzhROXFXOEJ5eEcveXFBQWdDazlzckhwaG51UnpTck95M1JmZzRYa0lndHhlb3ArUmZJOVgyaDBRcEVmcjgzYzExd0xhQkxDUmgwMlFXazA2Ty8yM2s2cWZNZHBxNVZ2b0ZnTkNJYlY1V01sSFpaV3RnVXFzaGtXRVJycjduZnVvd1BQQ0NUaHdxMC9tbUQ1NDVDb0VNWU16bUtQYlIyYmF4RkVTbUswTlRRT3VWR3A2Y3JqNWlYOGxzaU9kZ3FVNHhuSVpRcDRsT1lJcTlBOUhFS3NZZ1RuYysxRlRNazJEN05ydThlalh4UUR3amFqUTFNTmJ5cldBS0MvZ3RTWW9ONTFKY25FWFlUOWI1MVZOWWF4anArTE9oeDA0M3RNUW9TejNvN1kxbWtORlJUTmJMZWhDKzV0UkNKNjdQYk5Va29DbWxXbjFYODZxVlVsRFU0MjkxaXVLaE1YQ0lsVHlscGU2dw==",
593        ];
594        let shares: Vec<Share> = shares.iter().map(|&s| s.parse::<Share>().unwrap()).collect();
595        let shares: Vec<&Share> = shares.iter().collect();
596        assert_eq!(
597            reconstruct_mnemonic(&shares, None).unwrap().expose_secret().to_string(),
598            mnemonic_str
599        );
600    }
601
602    #[test]
603    fn test_aes_gcm_encrypt_decrypt() {
604        let key: SecretSlice<u8> = "key".to_string().into_bytes().into();
605        let data = ShareData("my secret data".as_bytes().to_vec().into());
606
607        assert_eq!(
608            decrypt_with_password(&encrypt_with_password(&data, &key).unwrap(), &key).unwrap(),
609            data,
610        );
611    }
612
613    #[test]
614    fn test_aes_gcm_encrypt_decrypt_wrong_key() {
615        let key: SecretSlice<u8> = "key".to_string().into_bytes().into();
616        let wrong_key: SecretSlice<u8> = "wrong key".to_string().into_bytes().into();
617
618        let data = ShareData("my secret data".as_bytes().to_vec().into());
619
620        assert!(decrypt_with_password(&encrypt_with_password(&data, &key).unwrap(), &wrong_key).is_err());
621    }
622
623    #[test]
624    fn test_aes_gcm_decrypt_examples() {
625        let key: SecretSlice<u8> = "key three".to_string().into_bytes().into();
626        let data = ShareData("secret".as_bytes().to_vec().into());
627
628        // Tests to make sure old generated encrypted data is still recoverable. The three examples
629        // are generated with this code:
630        // println!("{:?}", encrypt_with_password(data, &key).unwrap());
631
632        assert_eq!(
633            decrypt_with_password(
634                &ShareData(Box::new([
635                    32, 112, 222, 26, 190, 160, 235, 203, 235, 74, 13, 213, 181, 30, 151, 28, 60, 146, 145, 37, 128,
636                    57, 80, 202, 77, 21, 179, 21, 100, 60, 85, 127, 68, 223,
637                ])),
638                &key
639            )
640            .unwrap(),
641            data
642        );
643
644        assert_eq!(
645            decrypt_with_password(
646                &ShareData(Box::new([
647                    76, 61, 16, 170, 160, 112, 228, 107, 253, 241, 246, 102, 145, 90, 79, 73, 157, 173, 81, 106, 1,
648                    200, 23, 180, 127, 225, 147, 226, 233, 110, 94, 50, 150, 110
649                ])),
650                &key
651            )
652            .unwrap(),
653            data
654        );
655
656        assert_eq!(
657            decrypt_with_password(
658                &ShareData(Box::new([
659                    138, 139, 139, 160, 101, 108, 251, 7, 211, 55, 8, 160, 244, 248, 42, 23, 172, 229, 68, 143, 129,
660                    245, 6, 117, 192, 226, 109, 184, 0, 84, 68, 165, 143, 201
661                ])),
662                &key
663            )
664            .unwrap(),
665            data
666        );
667    }
668}