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#[derive(Debug, Clone)] #[cfg_attr(test, derive(PartialEq))]
12pub struct Share {
13 payload_type: PayloadType,
15
16 encoding: Encoding,
18
19 encryption: Encryption,
21
22 data: ShareData,
25}
26
27#[derive(zeroize::ZeroizeOnDrop, Clone)]
29#[cfg_attr(test, derive(PartialEq))] struct ShareData(Box<[u8]>);
31
32impl 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 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 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#[derive(Debug, Clone, Copy, Eq, PartialEq)]
90enum PayloadType {
91 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#[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#[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)]
178pub 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)]
209pub struct GeneratedShares {
211 pub recovery: Share,
213 pub local: Share,
215 pub backup: Share,
217}
218
219pub 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 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
235pub 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
252fn 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 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 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
290fn 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 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 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 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 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 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
357
358 let key = Blake2b256::new()
360 .chain_update(key.expose_secret())
361 .chain_update(nonce)
362 .finalize();
363 let key = Key::<aes_gcm::Aes256Gcm>::from_slice(key.as_slice());
365
366 let cipher = Aes256Gcm::new(key);
367
368 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 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 let key = Blake2b256::new()
400 .chain_update(key.expose_secret())
401 .chain_update(nonce)
402 .finalize();
403 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 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 let (_, reconstructed_secret) =
486 reconstruct_secret(&[&shares.backup, &shares.recovery], Some(&password)).unwrap();
487
488 let new_shares =
490 create_shares_from_secret(PayloadType::MnemonicEntropy, &reconstructed_secret, &password).unwrap();
491
492 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 let password = SecretBox::new("password".to_string().into_bytes().into());
502
503 let mnemonic = iota_sdk::client::Client::generate_mnemonic().unwrap();
504
505 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 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 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}