1pub mod messages;
5use {
6 bip39::{Error as Bip39Error, Language, Mnemonic},
7 crypto::error::CryptoError,
8 messages::*,
9 std::{num::ParseIntError, str::Utf8Error},
10 zeroize::ZeroizeOnDrop,
11};
12
13pub const MAX_LOGIN_ATTEMPTS: u32 = 10;
14pub const MIN_PIN_LENGTH: usize = 6;
15
16pub const DEV_FIDO_ATTESTATION_PRIVATE_KEY: [u8; 32] = [
20 0xbc, 0x2a, 0x1b, 0xfb, 0xce, 0xf4, 0xf7, 0x53, 0xb8, 0x6e, 0xbe, 0x13, 0x02, 0x13, 0x33, 0xc9, 0xbe,
21 0x7e, 0x4c, 0xd0, 0x7b, 0x2a, 0xb9, 0x94, 0xb4, 0xcf, 0x23, 0x36, 0x4b, 0x6f, 0x3c, 0x33,
22];
23
24#[derive(Default)]
25pub struct Security<P: server::CheckedPermissions> {
26 conn: server::CheckedConn<P>,
27}
28
29#[derive(Debug, Clone, ZeroizeOnDrop, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
30pub struct Pin(pub [u8; 32]);
31
32#[derive(Debug, Clone, ZeroizeOnDrop, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
33pub enum Seed {
34 Twelve([u8; 16]),
36 TwentyFour([u8; 32]),
38}
39
40impl Seed {
41 pub fn from_bytes(seed: &[u8]) -> Self {
48 match seed.len() {
49 16 => Seed::Twelve(seed.try_into().unwrap()),
50 32 => Seed::TwentyFour(seed.try_into().unwrap()),
51 _ => panic!("Invalid seed length: expected 16 or 32 bytes, got {}", seed.len()),
52 }
53 }
54
55 pub fn bytes(&self) -> &[u8] {
56 match self {
57 Seed::Twelve(bytes) => bytes,
58 Seed::TwentyFour(bytes) => bytes,
59 }
60 }
61
62 pub fn to_vec(&self) -> Vec<u8> { self.bytes().to_vec() }
63
64 pub fn from_mnemonic(mnemonic: &Mnemonic) -> Self {
65 let entropy = mnemonic.to_entropy();
66 Self::from_bytes(&entropy)
67 }
68
69 pub fn to_mnemonic(&self) -> Result<Mnemonic, Bip39Error> { Mnemonic::from_entropy(self.bytes()) }
70
71 pub fn to_mnemonic_words(&self) -> Result<Vec<String>, Bip39Error> {
72 let mnemonic = self.to_mnemonic()?;
73 Ok(mnemonic.words().map(str::to_string).collect())
74 }
75
76 pub fn to_standard_seed_qr_data(&self) -> Result<Vec<u8>, Bip39Error> {
77 let mnemonic = self.to_mnemonic()?;
78 let indices: String = mnemonic.word_indices().map(|idx| format!("{idx:04}")).collect();
79 Ok(indices.into_bytes())
80 }
81
82 pub fn to_compact_seed_qr_data(&self) -> Result<Vec<u8>, Bip39Error> {
83 let mnemonic = self.to_mnemonic()?;
84 Ok(mnemonic.to_entropy())
85 }
86}
87
88impl Default for Seed {
89 fn default() -> Self { Seed::TwentyFour([0; 32]) }
90}
91
92#[derive(Clone, Debug, thiserror::Error)]
93pub enum ParseSeedQrError {
94 #[error("Invalid UTF-8 in word index: {0}")]
95 InvalidUtf8(#[from] Utf8Error),
96
97 #[error("Failed to parse word index: {0}")]
98 InvalidWordIndex(#[from] ParseIntError),
99
100 #[error("Word index {0} out of range")]
101 WordIndexOutOfRange(usize),
102
103 #[error("Invalid mnemonic: {0}")]
104 InvalidMnemonic(#[from] Bip39Error),
105}
106
107pub fn parse_seedqr(qr_data: &[u8]) -> Result<Mnemonic, ParseSeedQrError> {
110 if qr_data.len() == 48 || qr_data.len() == 96 {
112 let words = qr_data
113 .chunks(4)
114 .map(|index| -> Result<&'static str, ParseSeedQrError> {
115 let index_str = std::str::from_utf8(index)?;
116 let index: usize = index_str.parse()?;
117 let word = Language::English
118 .word_list()
119 .get(index)
120 .copied()
121 .ok_or(ParseSeedQrError::WordIndexOutOfRange(index))?;
122 Ok(word)
123 })
124 .collect::<Result<Vec<&'static str>, _>>()?
125 .join(" ");
126
127 return Mnemonic::parse(words.as_str()).map_err(Into::into);
128 }
129
130 if let Ok(text) = std::str::from_utf8(qr_data) {
131 if let Ok(mnemonic) = Mnemonic::parse_normalized(text) {
132 return Ok(mnemonic);
133 }
134 }
135
136 Mnemonic::from_entropy(qr_data).map_err(Into::into)
137}
138
139#[derive(Debug, Default, Copy, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, PartialEq, Eq)]
140pub enum PinEntryMode {
141 #[default]
142 Pin = 0,
143 Passphrase = 1,
144}
145
146impl From<u8> for PinEntryMode {
147 fn from(value: u8) -> Self {
148 match value {
149 0 => PinEntryMode::Pin,
150 1 => PinEntryMode::Passphrase,
151 _ => PinEntryMode::Pin,
152 }
153 }
154}
155
156impl From<PinEntryMode> for u8 {
157 fn from(mode: PinEntryMode) -> u8 { mode as u8 }
158}
159
160#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, PartialEq, Eq)]
163pub struct LockoutOptions {
164 pub seed_fingerprint: bool,
165 pub aes_keys: bool,
166}
167
168impl LockoutOptions {
169 pub const fn erase_seed_only() -> Self { Self { seed_fingerprint: false, aes_keys: false } }
170
171 pub const fn erase_all() -> Self { Self { seed_fingerprint: true, aes_keys: true } }
172
173 pub const fn erase_aes_keys() -> Self { Self { seed_fingerprint: false, aes_keys: true } }
174}
175
176#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, PartialEq, Eq)]
177pub struct FirmwareTimestamp(pub [u8; 4]);
178
179impl From<FirmwareTimestamp> for u32 {
180 fn from(ts: FirmwareTimestamp) -> u32 { u32::from_le_bytes(ts.0) }
181}
182
183impl From<u32> for FirmwareTimestamp {
184 fn from(ts: u32) -> FirmwareTimestamp { FirmwareTimestamp(ts.to_le_bytes()) }
185}
186
187impl Default for FirmwareTimestamp {
188 fn default() -> Self { 0u32.into() }
189}
190
191pub struct LastSuccess {
192 pub num_fails: u32,
193 pub attempts_left: u32,
194}
195
196#[macro_export]
197macro_rules! use_api {
198 () => {
199 mod security_permissions {
200 use security::messages::*;
201 #[derive(Clone, Default, server::Permissions)]
202 #[server_name = "os/security"]
203 pub struct SecurityPermissions;
204 }
205 type Security = security::Security<security_permissions::SecurityPermissions>;
206 };
207}
208
209impl<P: server::CheckedPermissions> Security<P> {
210 pub fn set_seed_and_pin(&self, seed: Seed, pin: String, pin_entry: PinEntryMode) -> Result<(), PinError>
213 where
214 P: server::MessageAllowed<SetSeedAndPin>,
215 {
216 self.conn.send_blocking_archive(SetSeedAndPin { seed, pin: RawPin(pin), pin_entry })
217 }
218
219 pub fn change_pin(
221 &self,
222 raw_pin: String,
223 seed: Option<Seed>,
224 pin_entry: PinEntryMode,
225 ) -> Result<(), PinError>
226 where
227 P: server::MessageAllowed<ChangePin>,
228 {
229 self.conn.send_blocking_archive(ChangePin { pin: RawPin(raw_pin), seed, pin_entry })
230 }
231
232 pub fn is_pin_set(&self) -> Result<bool, AccessDenied>
233 where
234 P: server::MessageAllowed<IsPinSet>,
235 {
236 self.conn.send_blocking_archive(IsPinSet)
237 }
238
239 pub fn get_pin_entry_mode(&self) -> PinEntryMode
240 where
241 P: server::MessageAllowed<GetPinEntryMode>,
242 {
243 self.conn.send_blocking_archive(GetPinEntryMode)
244 }
245
246 pub fn log_in(&self, pin: String) -> Result<(), LoginFailed>
247 where
248 P: server::MessageAllowed<Login>,
249 {
250 self.conn.send_blocking_archive(Login { pin: RawPin(pin) })
251 }
252
253 pub fn log_out(&self)
254 where
255 P: server::MessageAllowed<Logout>,
256 {
257 self.conn.send_blocking_scalar(Logout)
258 }
259
260 pub fn logged_in(&self) -> bool
261 where
262 P: server::MessageAllowed<LoggedIn>,
263 {
264 self.conn.send_blocking_scalar(LoggedIn)
265 }
266
267 pub fn attempts_remaining(&self) -> Result<u32, AccessDenied>
268 where
269 P: server::MessageAllowed<GetAttemptsRemaining>,
270 {
271 self.conn.send_blocking_archive(GetAttemptsRemaining)
272 }
273
274 pub fn factory_reset_counter(&self) -> Result<u32, AccessDenied>
275 where
276 P: server::MessageAllowed<GetFactoryResetCounter>,
277 {
278 self.conn.send_blocking_archive(GetFactoryResetCounter)
279 }
280
281 pub fn seed(&self) -> Result<Option<Seed>, AccessDenied>
288 where
289 P: server::MessageAllowed<GetSeed>,
290 {
291 self.conn.send_blocking_archive(GetSeed)
292 }
293
294 pub fn set_seed(&self, seed: Seed) -> Result<(), AccessDenied>
299 where
300 P: server::MessageAllowed<SetSeed>,
301 {
302 self.conn.send_blocking_archive(SetSeed(seed))
303 }
304
305 pub fn app_seed(&self) -> Result<[u8; 32], AccessDenied>
306 where
307 P: server::MessageAllowed<GetAppSeed>,
308 {
309 self.conn.send_blocking_archive(GetAppSeed)
310 }
311
312 pub fn lockout(&self, lockout_options: LockoutOptions) -> Result<(), AccessDenied>
313 where
314 P: server::MessageAllowed<Lockout>,
315 {
316 self.conn.send_blocking_archive(Lockout { lockout_options, reboot: true })
317 }
318
319 pub fn sign_with_security_check_key(&self, data: [u8; 32]) -> Result<[u8; 64], AccessDenied>
320 where
321 P: server::MessageAllowed<SignWithSecurityCheckKey>,
322 {
323 self.conn.send_blocking_archive(SignWithSecurityCheckKey(data))
324 }
325
326 pub fn sign_with_fido_key(&self, data: [u8; 32]) -> Result<[u8; 64], AccessDenied>
327 where
328 P: server::MessageAllowed<SignWithFidoKey>,
329 {
330 self.conn.send_blocking_archive(SignWithFidoKey(data))
331 }
332
333 pub fn get_fido_pubkey(&self) -> Result<[u8; 64], AccessDenied>
334 where
335 P: server::MessageAllowed<GetFidoPubkey>,
336 {
337 self.conn.send_blocking_archive(GetFidoPubkey)
338 }
339
340 pub fn security_words(&self, pin_prefix: &str) -> Result<[SecurityWord; 2], AccessDenied>
341 where
342 P: server::MessageAllowed<GetSecurityWords>,
343 {
344 self.conn.send_blocking_archive(GetSecurityWords { pin_prefix: pin_prefix.as_bytes().to_vec() })
345 }
346
347 pub fn firmware_timestamp(&self) -> Result<FirmwareTimestamp, AccessDenied>
348 where
349 P: server::MessageAllowed<GetFirmwareTimestamp>,
350 {
351 self.conn.send_blocking_archive(GetFirmwareTimestamp)
352 }
353
354 pub fn set_firmware_timestamp(&self, timestamp: FirmwareTimestamp) -> Result<(), AccessDenied>
355 where
356 P: server::MessageAllowed<SetFirmwareTimestamp>,
357 {
358 self.conn.send_blocking_archive(SetFirmwareTimestamp(timestamp))
359 }
360
361 pub fn seed_fingerprint(&self) -> Result<[u8; 32], AccessDenied>
362 where
363 P: server::MessageAllowed<GetSeedFingerprint>,
364 {
365 self.conn.send_blocking_archive(GetSeedFingerprint)
366 }
367
368 pub fn fingerprint(&self, seed: &Seed) -> Result<[u8; 32], AccessDenied>
369 where
370 P: server::MessageAllowed<ComputeSeedFingerprint>,
371 {
372 self.conn.send_blocking_archive(ComputeSeedFingerprint(seed.clone()))
373 }
374
375 pub fn os_version_info(&self) -> Result<Option<OsVersionInfo>, AccessDenied>
376 where
377 P: server::MessageAllowed<GetOsVersionInfo>,
378 {
379 self.conn.send_blocking_archive(GetOsVersionInfo)
380 }
381
382 pub fn bootloader_build_date(&self) -> Result<Option<u64>, AccessDenied>
383 where
384 P: server::MessageAllowed<GetBootloaderBuildDate>,
385 {
386 self.conn.send_blocking_archive(GetBootloaderBuildDate)
387 }
388
389 pub fn sc_challenge(&self, challenge: [u8; ScChallenge::SIZE]) -> Result<ScProof, ScChallengeError>
390 where
391 P: server::MessageAllowed<ScChallenge>,
392 {
393 self.conn.send_blocking_archive(ScChallenge(challenge))
394 }
395
396 pub fn device_id(&self) -> Result<DeviceId, GetDeviceIdError>
397 where
398 P: server::MessageAllowed<GetDeviceId>,
399 {
400 self.conn.send_blocking_archive(GetDeviceId)
401 }
402
403 pub fn get_random(&self) -> Result<[u8; 32], AccessDenied>
404 where
405 P: server::MessageAllowed<GetRandom>,
406 {
407 self.conn.send_blocking_archive(GetRandom)
408 }
409
410 pub fn keycard_authenticity_mac(&self, msg: [u8; 32]) -> Result<[u8; 32], AccessDenied>
411 where
412 P: server::MessageAllowed<KeycardAuthenticityMac>,
413 {
414 self.conn.send_blocking_archive(KeycardAuthenticityMac(msg))
415 }
416
417 #[cfg(not(keyos))]
418 pub fn get_pin(&self) -> String
419 where
420 P: server::MessageAllowed<GetPin>,
421 {
422 self.conn.send_blocking_archive(GetPin)
423 }
424
425 #[cfg(not(keyos))]
426 pub fn set_attempts_remaining(&self, attempts: u32) -> Result<(), SecurityError>
427 where
428 P: server::MessageAllowed<SetAttempts>,
429 {
430 if attempts > MAX_LOGIN_ATTEMPTS {
431 return Err(SecurityError::AttemptsOutOfBounds(attempts));
432 }
433
434 self.conn.send_blocking_archive(SetAttempts(MAX_LOGIN_ATTEMPTS - attempts));
435 Ok(())
436 }
437
438 pub fn bluetooth_challenge_secret(&self) -> BluetoothChallengeSecret
440 where
441 P: server::MessageAllowed<GetBluetoothChallengeSecret>,
442 {
443 self.conn.send_blocking_archive(GetBluetoothChallengeSecret)
444 }
445
446 pub fn set_bluetooth_challenge_secret_sent(&self)
447 where
448 P: server::MessageAllowed<SetBluetoothCheckSecretSent>,
449 {
450 self.conn.send_blocking_scalar(SetBluetoothCheckSecretSent)
451 }
452
453 pub fn set_bluetooth_device_id(&self, device_id: [u8; 8])
454 where
455 P: server::MessageAllowed<SetBluetoothDeviceId>,
456 {
457 self.conn.send_blocking_archive(SetBluetoothDeviceId(device_id))
458 }
459
460 pub fn master_key_state(&self) -> MasterKeyState
461 where
462 P: server::MessageAllowed<GetMasterKeyState>,
463 {
464 self.conn.send_blocking_scalar(GetMasterKeyState)
465 }
466
467 pub fn subscribe_disk_encryption_keys_ready<SR>(&self, context: &mut server::ServerContext<SR>)
471 where
472 P: server::MessageAllowed<SubscribeDiskEncryptionKeysReady>,
473 SR: server::ScalarEventHandler<DiskEncryptionKeysReady>,
474 {
475 self.conn.subscribe_scalar_infallible(SubscribeDiskEncryptionKeysReady, context)
476 }
477}
478
479#[derive(Debug, Copy, Clone)]
481pub enum MasterKeyState {
482 Onboarding,
483 Erased,
484 Normal,
485 Unknown,
486}
487
488#[cfg(not(keyos))]
489#[derive(Debug, thiserror::Error)]
490pub enum SecurityError {
491 #[error("Attempts remaining must not be greater than max attempts of {}: {0:?}", MAX_LOGIN_ATTEMPTS)]
492 AttemptsOutOfBounds(u32),
493}
494
495#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
496pub struct SecurityWord(pub usize);
497
498impl std::fmt::Display for SecurityWord {
499 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500 bip39::Language::English.word_list()[self.0].fmt(f)
501 }
502}
503
504#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, thiserror::Error)]
505pub struct AccessDenied;
506
507impl std::fmt::Display for AccessDenied {
508 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Access denied") }
509}
510
511#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, thiserror::Error)]
512pub enum PinError {
513 #[error("Access denied")]
514 AccessDenied,
515 #[error("PIN too short")]
516 TooShort,
517}
518
519impl From<AccessDenied> for PinError {
520 fn from(_: AccessDenied) -> Self { PinError::AccessDenied }
521}
522
523#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
524pub struct LoginFailed {
525 pub attempts_left: u32,
526}
527
528impl std::fmt::Display for LoginFailed {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Login failed") }
530}
531
532#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
533pub struct OsVersionInfo {
534 pub bootloader_version: [u8; 8],
535 pub keyos_version: [u8; 20],
536}
537
538#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
547pub struct ScProof(pub [u8; Self::SIZE]);
548
549impl ScProof {
550 pub const SIZE: usize = 189;
551}
552
553#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
554#[repr(u8)]
555pub enum ScError {
556 Ok = 0,
557 InvalidMessageLength = 1,
558 InvalidSignature = 3,
559 DeadlineExpired = 4,
560 UnknownChallenge = 6,
561 InvalidBootloaderVersion = 7,
562}
563
564#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
565pub enum ScChallengeError {
566 Sc(ScError),
567 CryptoAuthLib(i32),
568 Crypto(CryptoError),
569 AccessDenied,
570 Internal(String),
571}
572
573#[derive(Debug, thiserror::Error, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
574pub enum GetDeviceIdError {
575 #[error("crypto auth lib error: {0}")]
576 CryptoAuthLib(i32),
577 #[error(transparent)]
578 Crypto(CryptoError),
579 #[error("no bluetooth serial yet")]
580 NoBluetoothSerialYet,
581}
582
583#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
584pub struct DeviceId(pub [u8; 32]);
585
586impl std::fmt::Display for DeviceId {
587 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
588 write!(
589 f,
590 "{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}",
591 self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7]
592 )
593 }
594}
595
596#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
597pub struct BluetoothChallengeSecret {
598 pub secret: [u8; 32],
599 pub sent: bool,
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 fn test_seed_mnemonic_roundtrip() {
608 let seed = Seed::Twelve([0x7Au8; 16]);
609 let mnemonic = seed.to_mnemonic().unwrap();
610 let recovered_seed = Seed::from_mnemonic(&mnemonic).to_vec();
611
612 assert_eq!(
613 &seed.bytes()[..mnemonic.to_entropy().len()],
614 &recovered_seed[..mnemonic.to_entropy().len()]
615 );
616 }
617
618 #[test]
619 fn test_parse_seedqr_standard_12_word() {
620 let qr_data = b"192402220235174306311124037817700641198012901210";
622
623 let result = parse_seedqr(qr_data).unwrap().word_indices().collect::<Vec<_>>();
624
625 let expected = vec![1924, 222, 235, 1743, 631, 1124, 378, 1770, 641, 1980, 1290, 1210];
626 assert_eq!(result, expected, "Word indices should match expected values");
627 }
628
629 #[test]
630 fn test_parse_seedqr_standard_24_word() {
631 let entropy = [0x35u8; 32];
632 let mnemonic = Mnemonic::from_entropy(&entropy).unwrap();
633
634 let indices: String = mnemonic.word_indices().map(|idx| format!("{idx:04}")).collect();
635 let qr_data = indices.as_bytes();
636 let result = parse_seedqr(qr_data).unwrap();
637
638 assert_eq!(result, mnemonic);
639 assert_eq!(result.word_count(), 24);
640 }
641
642 #[test]
643 fn test_parse_seedqr_compact() {
644 fn test(entropy: &[u8]) {
645 let mnemonic = Mnemonic::from_entropy(entropy).unwrap();
646 let result = parse_seedqr(entropy).unwrap();
647 assert_eq!(result, mnemonic);
648 }
649
650 test(&[0x11u8; 16]);
651 test(&[0x22u8; 32]);
652 }
653
654 #[test]
655 fn test_parse_seedqr_plaintext() {
656 let mnemonic = Mnemonic::from_entropy(&[0x5Au8; 16]).unwrap();
657 let qr_data = mnemonic.to_string();
658 let result = parse_seedqr(qr_data.as_bytes()).unwrap();
659
660 assert_eq!(result, mnemonic);
661 }
662
663 #[test]
664 fn test_parse_seedqr_plaintext_with_extra_whitespace() {
665 let mnemonic = Mnemonic::from_entropy(&[0xA5u8; 32]).unwrap();
666 let words = mnemonic.words().collect::<Vec<_>>();
667 let qr_data = format!(" {}\n{}\n ", words[..12].join(" "), words[12..].join("\n"));
668
669 let result = parse_seedqr(qr_data.as_bytes()).unwrap();
670 assert_eq!(result, mnemonic);
671 }
672
673 #[test]
674 fn test_seedqr_generation_roundtrip() {
675 let seed = Seed::Twelve([0x6Cu8; 16]);
676
677 let standard_data = seed.to_standard_seed_qr_data().unwrap();
678 let parsed_standard = parse_seedqr(&standard_data).unwrap();
679 let recovered_seed = Seed::from_mnemonic(&parsed_standard).to_vec();
680 assert_eq!(
681 &seed.bytes()[..parsed_standard.to_entropy().len()],
682 &recovered_seed[..parsed_standard.to_entropy().len()]
683 );
684
685 let compact_data = seed.to_compact_seed_qr_data().unwrap();
686 let parsed_compact = parse_seedqr(&compact_data).unwrap();
687 let recovered_seed = Seed::from_mnemonic(&parsed_compact).to_vec();
688 assert_eq!(
689 &seed.bytes()[..parsed_compact.to_entropy().len()],
690 &recovered_seed[..parsed_compact.to_entropy().len()]
691 );
692 }
693
694 #[test]
695 fn test_parse_seedqr_errors() {
696 let invalid_utf8 = vec![0xFF; 48];
698 assert!(matches!(parse_seedqr(&invalid_utf8), Err(ParseSeedQrError::InvalidUtf8(_))));
699
700 let invalid_number = b"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"; assert!(matches!(parse_seedqr(invalid_number), Err(ParseSeedQrError::InvalidWordIndex(_))));
703
704 let out_of_range = b"999999999999999999999999999999999999999999999999"; assert!(matches!(parse_seedqr(out_of_range), Err(ParseSeedQrError::WordIndexOutOfRange(9999))));
707
708 let invalid_compact = b"invalid"; assert!(matches!(parse_seedqr(invalid_compact), Err(ParseSeedQrError::InvalidMnemonic(_))));
711 }
712}