Skip to main content
KeyOS API Reference

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/// The 32-byte per-app seed the security element derives for a calling app.
93///
94/// A distinct type (annotated like [`Seed`]) so the zeroize-on-drop treatment follows the
95/// bytes wherever an `AppSeed` flows, and a call site can't quietly pass an unrelated array
96/// where a seed is expected. Raw `[u8; 32]` copies a caller makes from `as_bytes` still escape
97/// scrubbing (Rust move semantics), so this narrows the exposure window rather than closing it.
98#[derive(Clone, ZeroizeOnDrop, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
99pub struct AppSeed([u8; 32]);
100
101impl AppSeed {
102    pub fn new(bytes: [u8; 32]) -> Self { Self(bytes) }
103
104    pub fn as_bytes(&self) -> &[u8; 32] { &self.0 }
105}
106
107#[derive(Clone, Debug, thiserror::Error)]
108pub enum ParseSeedQrError {
109    #[error("Invalid UTF-8 in word index: {0}")]
110    InvalidUtf8(#[from] Utf8Error),
111
112    #[error("Failed to parse word index: {0}")]
113    InvalidWordIndex(#[from] ParseIntError),
114
115    #[error("Word index {0} out of range")]
116    WordIndexOutOfRange(usize),
117
118    #[error("Invalid mnemonic: {0}")]
119    InvalidMnemonic(#[from] Bip39Error),
120}
121
122/// Parse standard, compact, or plaintext mnemonic SeedQR format.
123/// <https://github.com/SeedSigner/seedsigner/blob/dev/docs/seed_qr/README.md>
124pub fn parse_seedqr(qr_data: &[u8]) -> Result<Mnemonic, ParseSeedQrError> {
125    // 12 or 24 word standard qr
126    if qr_data.len() == 48 || qr_data.len() == 96 {
127        let words = qr_data
128            .chunks(4)
129            .map(|index| -> Result<&'static str, ParseSeedQrError> {
130                let index_str = std::str::from_utf8(index)?;
131                let index: usize = index_str.parse()?;
132                let word = Language::English
133                    .word_list()
134                    .get(index)
135                    .copied()
136                    .ok_or(ParseSeedQrError::WordIndexOutOfRange(index))?;
137                Ok(word)
138            })
139            .collect::<Result<Vec<&'static str>, _>>()?
140            .join(" ");
141
142        return Mnemonic::parse(words.as_str()).map_err(Into::into);
143    }
144
145    if let Ok(text) = std::str::from_utf8(qr_data) {
146        if let Ok(mnemonic) = Mnemonic::parse_normalized(text) {
147            return Ok(mnemonic);
148        }
149    }
150
151    Mnemonic::from_entropy(qr_data).map_err(Into::into)
152}
153
154#[derive(Debug, Default, Copy, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, PartialEq, Eq)]
155pub enum PinEntryMode {
156    #[default]
157    Pin = 0,
158    Passphrase = 1,
159}
160
161impl From<u8> for PinEntryMode {
162    fn from(value: u8) -> Self {
163        match value {
164            0 => PinEntryMode::Pin,
165            1 => PinEntryMode::Passphrase,
166            _ => PinEntryMode::Pin,
167        }
168    }
169}
170
171impl From<PinEntryMode> for u8 {
172    fn from(mode: PinEntryMode) -> u8 { mode as u8 }
173}
174
175/// Determines what data apart from the seed the lockout will erase.
176/// The seed is always erased.
177#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, PartialEq, Eq)]
178pub struct LockoutOptions {
179    pub seed_fingerprint: bool,
180    pub aes_keys: bool,
181}
182
183impl LockoutOptions {
184    pub const fn erase_seed_only() -> Self { Self { seed_fingerprint: false, aes_keys: false } }
185
186    pub const fn erase_all() -> Self { Self { seed_fingerprint: true, aes_keys: true } }
187
188    pub const fn erase_aes_keys() -> Self { Self { seed_fingerprint: false, aes_keys: true } }
189}
190
191#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, PartialEq, Eq)]
192pub struct FirmwareTimestamp(pub [u8; 4]);
193
194impl From<FirmwareTimestamp> for u32 {
195    fn from(ts: FirmwareTimestamp) -> u32 { u32::from_le_bytes(ts.0) }
196}
197
198impl From<u32> for FirmwareTimestamp {
199    fn from(ts: u32) -> FirmwareTimestamp { FirmwareTimestamp(ts.to_le_bytes()) }
200}
201
202impl Default for FirmwareTimestamp {
203    fn default() -> Self { 0u32.into() }
204}
205
206pub struct LastSuccess {
207    pub num_fails: u32,
208    pub attempts_left: u32,
209}
210
211#[macro_export]
212macro_rules! use_api {
213    () => {
214        mod security_permissions {
215            use security::messages::*;
216            #[derive(Clone, Default, server::Permissions)]
217            #[server_name = "os/security"]
218            pub struct SecurityPermissions;
219        }
220        type Security = security::Security<security_permissions::SecurityPermissions>;
221    };
222}
223
224impl<P: server::CheckedPermissions> Security<P> {
225    /// User does not need to be logged in. Use this when setting the seed and PIN for the first
226    /// time.
227    pub fn set_seed_and_pin(&self, seed: Seed, pin: String, pin_entry: PinEntryMode) -> Result<(), PinError>
228    where
229        P: server::MessageAllowed<SetSeedAndPin>,
230    {
231        self.conn.send_blocking_archive(SetSeedAndPin { seed, pin: RawPin(pin), pin_entry })
232    }
233
234    /// User must be [logged in](Login) to set a new pin.
235    pub fn change_pin(
236        &self,
237        raw_pin: String,
238        seed: Option<Seed>,
239        pin_entry: PinEntryMode,
240    ) -> Result<(), PinError>
241    where
242        P: server::MessageAllowed<ChangePin>,
243    {
244        self.conn.send_blocking_archive(ChangePin { pin: RawPin(raw_pin), seed, pin_entry })
245    }
246
247    pub fn is_pin_set(&self) -> Result<bool, AccessDenied>
248    where
249        P: server::MessageAllowed<IsPinSet>,
250    {
251        self.conn.send_blocking_archive(IsPinSet)
252    }
253
254    pub fn get_pin_entry_mode(&self) -> PinEntryMode
255    where
256        P: server::MessageAllowed<GetPinEntryMode>,
257    {
258        self.conn.send_blocking_archive(GetPinEntryMode)
259    }
260
261    pub fn log_in(&self, pin: String) -> Result<(), LoginFailed>
262    where
263        P: server::MessageAllowed<Login>,
264    {
265        self.conn.send_blocking_archive(Login { pin: RawPin(pin) })
266    }
267
268    pub fn log_out(&self)
269    where
270        P: server::MessageAllowed<Logout>,
271    {
272        self.conn.send_blocking_scalar(Logout)
273    }
274
275    pub fn logged_in(&self) -> bool
276    where
277        P: server::MessageAllowed<LoggedIn>,
278    {
279        self.conn.send_blocking_scalar(LoggedIn)
280    }
281
282    pub fn attempts_remaining(&self) -> Result<u32, AccessDenied>
283    where
284        P: server::MessageAllowed<GetAttemptsRemaining>,
285    {
286        self.conn.send_blocking_archive(GetAttemptsRemaining)
287    }
288
289    pub fn factory_reset_counter(&self) -> Result<u32, AccessDenied>
290    where
291        P: server::MessageAllowed<GetFactoryResetCounter>,
292    {
293        self.conn.send_blocking_archive(GetFactoryResetCounter)
294    }
295
296    /// Fetches the [Seed] from SE.
297    ///
298    /// # Returns
299    ///
300    /// - `None` if `otp_key` field of SECURAM is set to all zeros.
301    /// - `Some(seed)` otherwise.
302    pub fn seed(&self) -> Result<Option<Seed>, AccessDenied>
303    where
304        P: server::MessageAllowed<GetSeed>,
305    {
306        self.conn.send_blocking_archive(GetSeed)
307    }
308
309    /// User must be [logged in](Login) to change the seed. This is because a XOR operation will
310    /// be performed between the seed and the PIN hash before storing it in the SE.
311    ///
312    /// In case the user is setting the seed for the first time, use [`SetSeedAndPin`] instead.
313    pub fn set_seed(&self, seed: Seed) -> Result<(), AccessDenied>
314    where
315        P: server::MessageAllowed<SetSeed>,
316    {
317        self.conn.send_blocking_archive(SetSeed(seed))
318    }
319
320    pub fn app_seed(&self) -> Result<AppSeed, AccessDenied>
321    where
322        P: server::MessageAllowed<GetAppSeed>,
323    {
324        self.conn.send_blocking_archive(GetAppSeed)
325    }
326
327    pub fn lockout(&self, lockout_options: LockoutOptions) -> Result<(), AccessDenied>
328    where
329        P: server::MessageAllowed<Lockout>,
330    {
331        self.conn.send_blocking_archive(Lockout { lockout_options, reboot: true })
332    }
333
334    pub fn sign_with_security_check_key(&self, data: [u8; 32]) -> Result<[u8; 64], AccessDenied>
335    where
336        P: server::MessageAllowed<SignWithSecurityCheckKey>,
337    {
338        self.conn.send_blocking_archive(SignWithSecurityCheckKey(data))
339    }
340
341    pub fn sign_with_fido_key(&self, data: [u8; 32]) -> Result<[u8; 64], AccessDenied>
342    where
343        P: server::MessageAllowed<SignWithFidoKey>,
344    {
345        self.conn.send_blocking_archive(SignWithFidoKey(data))
346    }
347
348    pub fn get_fido_pubkey(&self) -> Result<[u8; 64], AccessDenied>
349    where
350        P: server::MessageAllowed<GetFidoPubkey>,
351    {
352        self.conn.send_blocking_archive(GetFidoPubkey)
353    }
354
355    pub fn security_words(&self, pin_prefix: &str) -> Result<[SecurityWord; 2], AccessDenied>
356    where
357        P: server::MessageAllowed<GetSecurityWords>,
358    {
359        self.conn.send_blocking_archive(GetSecurityWords { pin_prefix: pin_prefix.as_bytes().to_vec() })
360    }
361
362    pub fn firmware_timestamp(&self) -> Result<FirmwareTimestamp, AccessDenied>
363    where
364        P: server::MessageAllowed<GetFirmwareTimestamp>,
365    {
366        self.conn.send_blocking_archive(GetFirmwareTimestamp)
367    }
368
369    pub fn set_firmware_timestamp(&self, timestamp: FirmwareTimestamp) -> Result<(), AccessDenied>
370    where
371        P: server::MessageAllowed<SetFirmwareTimestamp>,
372    {
373        self.conn.send_blocking_archive(SetFirmwareTimestamp(timestamp))
374    }
375
376    pub fn seed_fingerprint(&self) -> Result<[u8; 32], AccessDenied>
377    where
378        P: server::MessageAllowed<GetSeedFingerprint>,
379    {
380        self.conn.send_blocking_archive(GetSeedFingerprint)
381    }
382
383    pub fn fingerprint(&self, seed: &Seed) -> Result<[u8; 32], AccessDenied>
384    where
385        P: server::MessageAllowed<ComputeSeedFingerprint>,
386    {
387        self.conn.send_blocking_archive(ComputeSeedFingerprint(seed.clone()))
388    }
389
390    pub fn os_version_info(&self) -> Result<Option<OsVersionInfo>, AccessDenied>
391    where
392        P: server::MessageAllowed<GetOsVersionInfo>,
393    {
394        self.conn.send_blocking_archive(GetOsVersionInfo)
395    }
396
397    pub fn bootloader_build_date(&self) -> Result<Option<u64>, AccessDenied>
398    where
399        P: server::MessageAllowed<GetBootloaderBuildDate>,
400    {
401        self.conn.send_blocking_archive(GetBootloaderBuildDate)
402    }
403
404    pub fn sc_challenge(&self, challenge: [u8; ScChallenge::SIZE]) -> Result<ScProof, ScChallengeError>
405    where
406        P: server::MessageAllowed<ScChallenge>,
407    {
408        self.conn.send_blocking_archive(ScChallenge(challenge))
409    }
410
411    pub fn device_id(&self) -> Result<DeviceId, GetDeviceIdError>
412    where
413        P: server::MessageAllowed<GetDeviceId>,
414    {
415        self.conn.send_blocking_archive(GetDeviceId)
416    }
417
418    pub fn get_random(&self) -> Result<[u8; 32], AccessDenied>
419    where
420        P: server::MessageAllowed<GetRandom>,
421    {
422        self.conn.send_blocking_archive(GetRandom)
423    }
424
425    pub fn keycard_authenticity_mac(&self, msg: [u8; 32]) -> Result<[u8; 32], AccessDenied>
426    where
427        P: server::MessageAllowed<KeycardAuthenticityMac>,
428    {
429        self.conn.send_blocking_archive(KeycardAuthenticityMac(msg))
430    }
431
432    #[cfg(not(keyos))]
433    pub fn get_pin(&self) -> String
434    where
435        P: server::MessageAllowed<GetPin>,
436    {
437        self.conn.send_blocking_archive(GetPin)
438    }
439
440    #[cfg(not(keyos))]
441    pub fn set_attempts_remaining(&self, attempts: u32) -> Result<(), SecurityError>
442    where
443        P: server::MessageAllowed<SetAttempts>,
444    {
445        if attempts > MAX_LOGIN_ATTEMPTS {
446            return Err(SecurityError::AttemptsOutOfBounds(attempts));
447        }
448
449        self.conn.send_blocking_archive(SetAttempts(MAX_LOGIN_ATTEMPTS - attempts));
450        Ok(())
451    }
452
453    /// Get the bluetooth HMAC challenge secret and whether it was shared with the BT chip already.
454    pub fn bluetooth_challenge_secret(&self) -> BluetoothChallengeSecret
455    where
456        P: server::MessageAllowed<GetBluetoothChallengeSecret>,
457    {
458        self.conn.send_blocking_archive(GetBluetoothChallengeSecret)
459    }
460
461    pub fn set_bluetooth_challenge_secret_sent(&self)
462    where
463        P: server::MessageAllowed<SetBluetoothCheckSecretSent>,
464    {
465        self.conn.send_blocking_scalar(SetBluetoothCheckSecretSent)
466    }
467
468    pub fn set_bluetooth_device_id(&self, device_id: [u8; 8])
469    where
470        P: server::MessageAllowed<SetBluetoothDeviceId>,
471    {
472        self.conn.send_blocking_archive(SetBluetoothDeviceId(device_id))
473    }
474
475    pub fn master_key_state(&self) -> MasterKeyState
476    where
477        P: server::MessageAllowed<GetMasterKeyState>,
478    {
479        self.conn.send_blocking_scalar(GetMasterKeyState)
480    }
481
482    /// Subscribe to the `DiskEncryptionKeysReady` event. The event fires once, when the security server
483    /// has written disk encryption keys into SECURAM. Subscribers that arrive after the event has already
484    /// fired receive it immediately on subscription.
485    pub fn subscribe_disk_encryption_keys_ready<SR>(&self, context: &mut server::ServerContext<SR>)
486    where
487        P: server::MessageAllowed<SubscribeDiskEncryptionKeysReady>,
488        SR: server::ScalarEventHandler<DiskEncryptionKeysReady>,
489    {
490        self.conn.subscribe_scalar_infallible(SubscribeDiskEncryptionKeysReady, context)
491    }
492}
493
494/// The state of the master key determined by the combination of the secrets available to the security server.
495#[derive(Debug, Copy, Clone)]
496pub enum MasterKeyState {
497    Onboarding,
498    Erased,
499    Normal,
500    Unknown,
501}
502
503#[cfg(not(keyos))]
504#[derive(Debug, thiserror::Error)]
505pub enum SecurityError {
506    #[error("Attempts remaining must not be greater than max attempts of {}: {0:?}", MAX_LOGIN_ATTEMPTS)]
507    AttemptsOutOfBounds(u32),
508}
509
510#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
511pub struct SecurityWord(pub usize);
512
513impl std::fmt::Display for SecurityWord {
514    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515        bip39::Language::English.word_list()[self.0].fmt(f)
516    }
517}
518
519#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, thiserror::Error)]
520pub struct AccessDenied;
521
522impl std::fmt::Display for AccessDenied {
523    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Access denied") }
524}
525
526#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, thiserror::Error)]
527pub enum PinError {
528    #[error("Access denied")]
529    AccessDenied,
530    #[error("PIN too short")]
531    TooShort,
532}
533
534impl From<AccessDenied> for PinError {
535    fn from(_: AccessDenied) -> Self { PinError::AccessDenied }
536}
537
538#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
539pub struct LoginFailed {
540    pub attempts_left: u32,
541}
542
543impl std::fmt::Display for LoginFailed {
544    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Login failed") }
545}
546
547#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
548pub struct OsVersionInfo {
549    pub bootloader_version: [u8; 8],
550    pub keyos_version: [u8; 20],
551}
552
553/// A message sent from the device to the server, serving to prove that the device knows the private key
554/// corresponding to the public key it claims to own. The message has the following binary format:
555/// ```text
556/// ----------------------------------------------------------------------------------------
557/// | challenge | deadline | device pubkey | device nonce | bootloader version | signature |
558/// | 32 bytes  | 8 bytes  | 33 bytes      | 32 bytes     | 20 bytes           | 64 bytes  |
559/// ----------------------------------------------------------------------------------------
560/// ```
561#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
562pub struct ScProof(pub [u8; Self::SIZE]);
563
564impl ScProof {
565    pub const SIZE: usize = 189;
566}
567
568#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
569#[repr(u8)]
570pub enum ScError {
571    Ok = 0,
572    InvalidMessageLength = 1,
573    InvalidSignature = 3,
574    DeadlineExpired = 4,
575    UnknownChallenge = 6,
576    InvalidBootloaderVersion = 7,
577}
578
579#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
580pub enum ScChallengeError {
581    Sc(ScError),
582    CryptoAuthLib(i32),
583    Crypto(CryptoError),
584    AccessDenied,
585    Internal(String),
586}
587
588#[derive(Debug, thiserror::Error, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
589pub enum GetDeviceIdError {
590    #[error("crypto auth lib error: {0}")]
591    CryptoAuthLib(i32),
592    #[error(transparent)]
593    Crypto(CryptoError),
594    #[error("no bluetooth serial yet")]
595    NoBluetoothSerialYet,
596}
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
599pub struct DeviceId(pub [u8; 32]);
600
601impl std::fmt::Display for DeviceId {
602    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603        write!(
604            f,
605            "{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}-{:02X}{:02X}",
606            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7]
607        )
608    }
609}
610
611#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
612pub struct BluetoothChallengeSecret {
613    pub secret: [u8; 32],
614    pub sent: bool,
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620
621    #[test]
622    fn test_seed_mnemonic_roundtrip() {
623        let seed = Seed::Twelve([0x7Au8; 16]);
624        let mnemonic = seed.to_mnemonic().unwrap();
625        let recovered_seed = Seed::from_mnemonic(&mnemonic).to_vec();
626
627        assert_eq!(
628            &seed.bytes()[..mnemonic.to_entropy().len()],
629            &recovered_seed[..mnemonic.to_entropy().len()]
630        );
631    }
632
633    #[test]
634    fn test_parse_seedqr_standard_12_word() {
635        // Standard SeedQR format: 48 bytes (12 words * 4)
636        let qr_data = b"192402220235174306311124037817700641198012901210";
637
638        let result = parse_seedqr(qr_data).unwrap().word_indices().collect::<Vec<_>>();
639
640        let expected = vec![1924, 222, 235, 1743, 631, 1124, 378, 1770, 641, 1980, 1290, 1210];
641        assert_eq!(result, expected, "Word indices should match expected values");
642    }
643
644    #[test]
645    fn test_parse_seedqr_standard_24_word() {
646        let entropy = [0x35u8; 32];
647        let mnemonic = Mnemonic::from_entropy(&entropy).unwrap();
648
649        let indices: String = mnemonic.word_indices().map(|idx| format!("{idx:04}")).collect();
650        let qr_data = indices.as_bytes();
651        let result = parse_seedqr(qr_data).unwrap();
652
653        assert_eq!(result, mnemonic);
654        assert_eq!(result.word_count(), 24);
655    }
656
657    #[test]
658    fn test_parse_seedqr_compact() {
659        fn test(entropy: &[u8]) {
660            let mnemonic = Mnemonic::from_entropy(entropy).unwrap();
661            let result = parse_seedqr(entropy).unwrap();
662            assert_eq!(result, mnemonic);
663        }
664
665        test(&[0x11u8; 16]);
666        test(&[0x22u8; 32]);
667    }
668
669    #[test]
670    fn test_parse_seedqr_plaintext() {
671        let mnemonic = Mnemonic::from_entropy(&[0x5Au8; 16]).unwrap();
672        let qr_data = mnemonic.to_string();
673        let result = parse_seedqr(qr_data.as_bytes()).unwrap();
674
675        assert_eq!(result, mnemonic);
676    }
677
678    #[test]
679    fn test_parse_seedqr_plaintext_with_extra_whitespace() {
680        let mnemonic = Mnemonic::from_entropy(&[0xA5u8; 32]).unwrap();
681        let words = mnemonic.words().collect::<Vec<_>>();
682        let qr_data = format!("  {}\n{}\n  ", words[..12].join("  "), words[12..].join("\n"));
683
684        let result = parse_seedqr(qr_data.as_bytes()).unwrap();
685        assert_eq!(result, mnemonic);
686    }
687
688    #[test]
689    fn test_seedqr_generation_roundtrip() {
690        let seed = Seed::Twelve([0x6Cu8; 16]);
691
692        let standard_data = seed.to_standard_seed_qr_data().unwrap();
693        let parsed_standard = parse_seedqr(&standard_data).unwrap();
694        let recovered_seed = Seed::from_mnemonic(&parsed_standard).to_vec();
695        assert_eq!(
696            &seed.bytes()[..parsed_standard.to_entropy().len()],
697            &recovered_seed[..parsed_standard.to_entropy().len()]
698        );
699
700        let compact_data = seed.to_compact_seed_qr_data().unwrap();
701        let parsed_compact = parse_seedqr(&compact_data).unwrap();
702        let recovered_seed = Seed::from_mnemonic(&parsed_compact).to_vec();
703        assert_eq!(
704            &seed.bytes()[..parsed_compact.to_entropy().len()],
705            &recovered_seed[..parsed_compact.to_entropy().len()]
706        );
707    }
708
709    #[test]
710    fn test_parse_seedqr_errors() {
711        // Invalid UTF-8 in standard format (48 bytes)
712        let invalid_utf8 = vec![0xFF; 48];
713        assert!(matches!(parse_seedqr(&invalid_utf8), Err(ParseSeedQrError::InvalidUtf8(_))));
714
715        // Invalid number format in standard format
716        let invalid_number = b"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"; // 48 bytes
717        assert!(matches!(parse_seedqr(invalid_number), Err(ParseSeedQrError::InvalidWordIndex(_))));
718
719        // Out of range index
720        let out_of_range = b"999999999999999999999999999999999999999999999999"; // 48 bytes
721        assert!(matches!(parse_seedqr(out_of_range), Err(ParseSeedQrError::WordIndexOutOfRange(9999))));
722
723        // Invalid compact format (not a valid entropy length)
724        let invalid_compact = b"invalid"; // 7 bytes
725        assert!(matches!(parse_seedqr(invalid_compact), Err(ParseSeedQrError::InvalidMnemonic(_))));
726    }
727}