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#[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
219#[allow(clippy::result_large_err)]
221pub fn create_shares_from_mnemonic(
222 mnemonic: &Mnemonic,
223 password: &SecretSlice<u8>,
224) -> super::error::Result<GeneratedShares> {
225 let entropy = mnemonic.entropy();
227
228 create_shares_from_secret(PayloadType::MnemonicEntropy, &entropy.to_vec().into(), password).map_err(Into::into)
229}
230
231#[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#[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 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 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#[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 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 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 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 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 let mut nonce = Nonce::<U12>::default();
353 let mut rng = rand::rng();
354 rng.fill_bytes(&mut nonce);
355
356 let key = Blake2b256::new()
358 .chain_update(key.expose_secret())
359 .chain_update(nonce)
360 .finalize();
361 let key = Key::<aes_gcm::Aes256Gcm>::from_slice(key.as_slice());
363
364 let cipher = Aes256Gcm::new(key);
365
366 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 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 let key = Blake2b256::new()
399 .chain_update(key.expose_secret())
400 .chain_update(nonce)
401 .finalize();
402 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 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 let (_, reconstructed_secret) =
485 reconstruct_secret(&[&shares.backup, &shares.recovery], Some(&password)).unwrap();
486
487 let new_shares =
489 create_shares_from_secret(PayloadType::MnemonicEntropy, &reconstructed_secret, &password).unwrap();
490
491 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 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 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 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 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}