security/
lib.rs

1// SPDX-FileCopyrightText: 2023 Foundation Devices, Inc. <hello@foundation.xyz>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4pub 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
16/// FIDO attestation private key for software signing.
17/// Corresponding pubkey for testing:
18/// 044c0fef3ee1ac94a1cb113e87db62ba64ac3666cce5690c333c7f801d7d4254f1dcc700b76d2ce311170bf543967f4e6b8204cb9ba99f44d3039ee76d1d527560
19pub 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 word seed.
35    Twelve([u8; 16]),
36    /// Twenty-four word seed.
37    TwentyFour([u8; 32]),
38}
39
40impl Seed {
41    /// Creates a new `Seed` from a byte slice. The slice must be either 16 bytes (for a 12-word seed) or 32
42    /// bytes (for a 24-word seed).
43    ///
44    /// # Panics
45    ///
46    /// Panics if the length of the slice is not 16 or 32 bytes.
47    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
107/// Parse standard, compact, or plaintext mnemonic SeedQR format.
108/// <https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md>
109pub fn parse_seedqr(qr_data: &[u8]) -> Result<Mnemonic, ParseSeedQrError> {
110    // 12 or 24 word standard qr
111    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/// Determines what data apart from the seed the lockout will erase.
161/// The seed is always erased.
162#[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    /// User does not need to be logged in. Use this when setting the seed and PIN for the first
211    /// time.
212    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    /// User must be [logged in](Login) to set a new pin.
220    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    /// Fetches the [Seed] from SE.
282    ///
283    /// # Returns
284    ///
285    /// - `None` if `otp_key` field of SECURAM is set to all zeros.
286    /// - `Some(seed)` otherwise.
287    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    /// User must be [logged in](Login) to change the seed. This is because a XOR operation will
295    /// be performed between the seed and the PIN hash before storing it in the SE.
296    ///
297    /// In case the user is setting the seed for the first time, use [`SetSeedAndPin`] instead.
298    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    /// Get the bluetooth HMAC challenge secret and whether it was shared with the BT chip already.
439    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    /// Subscribe to the `DiskEncryptionKeysReady` event. The event fires once, when the security server
468    /// has written disk encryption keys into SECURAM. Subscribers that arrive after the event has already
469    /// fired receive it immediately on subscription.
470    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/// The state of the master key determined by the combination of the secrets available to the security server.
480#[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/// A message sent from the device to the server, serving to prove that the device knows the private key
539/// corresponding to the public key it claims to own. The message has the following binary format:
540/// ```text
541/// ----------------------------------------------------------------------------------------
542/// | challenge | deadline | device pubkey | device nonce | bootloader version | signature |
543/// | 32 bytes  | 8 bytes  | 33 bytes      | 32 bytes     | 20 bytes           | 64 bytes  |
544/// ----------------------------------------------------------------------------------------
545/// ```
546#[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        // Standard SeedQR format: 48 bytes (12 words * 4)
621        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        // Invalid UTF-8 in standard format (48 bytes)
697        let invalid_utf8 = vec![0xFF; 48];
698        assert!(matches!(parse_seedqr(&invalid_utf8), Err(ParseSeedQrError::InvalidUtf8(_))));
699
700        // Invalid number format in standard format
701        let invalid_number = b"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"; // 48 bytes
702        assert!(matches!(parse_seedqr(invalid_number), Err(ParseSeedQrError::InvalidWordIndex(_))));
703
704        // Out of range index
705        let out_of_range = b"999999999999999999999999999999999999999999999999"; // 48 bytes
706        assert!(matches!(parse_seedqr(out_of_range), Err(ParseSeedQrError::WordIndexOutOfRange(9999))));
707
708        // Invalid compact format (not a valid entropy length)
709        let invalid_compact = b"invalid"; // 7 bytes
710        assert!(matches!(parse_seedqr(invalid_compact), Err(ParseSeedQrError::InvalidMnemonic(_))));
711    }
712}