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