Skip to main content

casc_lib/blte/
encryption.rs

1//! TACT encryption support for BLTE mode-E blocks.
2//!
3//! Blizzard uses TACT (Trusted Application Content Transfer) to encrypt
4//! sensitive game data inside BLTE containers. Each encrypted block carries
5//! an encryption header that names the key (a `u64` key name), provides an
6//! IV, and identifies the cipher:
7//!
8//! - **Salsa20** (`S`) - the 16-byte TACT key is doubled to 32 bytes and
9//!   used with an 8-byte nonce derived from the IV.
10//! - **ARC4** (`A`) - RC4 with the standard WoW 1024-byte keystream skip.
11//!
12//! Keys are stored in a [`TactKeyStore`](crate::blte::encryption::TactKeyStore) which can be populated from the
13//! bundled community-known key list or loaded from a text key file.
14
15use std::collections::HashMap;
16use std::path::Path;
17
18use salsa20::Salsa20;
19use salsa20::cipher::{KeyIvInit, StreamCipher};
20
21use crate::error::{CascError, Result};
22
23// ---------------------------------------------------------------------------
24// TactKeyStore
25// ---------------------------------------------------------------------------
26
27/// Stores TACT encryption keys (key_name u64 -> 16-byte key value).
28pub struct TactKeyStore {
29    keys: HashMap<u64, [u8; 16]>,
30}
31
32impl Default for TactKeyStore {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl TactKeyStore {
39    /// Create an empty key store.
40    pub fn new() -> Self {
41        Self {
42            keys: HashMap::new(),
43        }
44    }
45
46    /// Create a key store pre-populated with community-known WoW TACT keys.
47    pub fn with_known_keys() -> Self {
48        let mut keys = HashMap::new();
49        for (name, value) in known_keys() {
50            keys.insert(name, value);
51        }
52        Self { keys }
53    }
54
55    /// Load a key store from a text file.
56    ///
57    /// Format: one key per line as `hex_key_name hex_key_value`.
58    /// Lines starting with `#` and blank lines are ignored.
59    pub fn load_keyfile(path: &Path) -> Result<Self> {
60        let content = std::fs::read_to_string(path)?;
61        let mut keys = HashMap::new();
62
63        for line in content.lines() {
64            let trimmed = line.trim();
65            if trimmed.is_empty() || trimmed.starts_with('#') {
66                continue;
67            }
68
69            let parts: Vec<&str> = trimmed.split_whitespace().collect();
70            if parts.len() < 2 {
71                return Err(CascError::InvalidFormat(format!(
72                    "invalid keyfile line: {}",
73                    trimmed
74                )));
75            }
76
77            let key_name = u64::from_str_radix(parts[0], 16).map_err(|e| {
78                CascError::InvalidFormat(format!("invalid key name '{}': {}", parts[0], e))
79            })?;
80
81            let key_value = hex_to_key_result(parts[1])?;
82            keys.insert(key_name, key_value);
83        }
84
85        Ok(Self { keys })
86    }
87
88    /// Merge keys from another store into this one.
89    /// Existing keys are overwritten if the other store has the same key name.
90    pub fn merge(&mut self, other: &TactKeyStore) {
91        for (&name, &value) in &other.keys {
92            self.keys.insert(name, value);
93        }
94    }
95
96    /// Look up a key by name.
97    pub fn get(&self, key_name: u64) -> Option<&[u8; 16]> {
98        self.keys.get(&key_name)
99    }
100
101    /// Return the number of keys in the store.
102    pub fn len(&self) -> usize {
103        self.keys.len()
104    }
105
106    /// Return true if the store contains no keys.
107    pub fn is_empty(&self) -> bool {
108        self.keys.is_empty()
109    }
110}
111
112// ---------------------------------------------------------------------------
113// Encryption header types
114// ---------------------------------------------------------------------------
115
116/// Encryption algorithm used by a BLTE encrypted block.
117#[derive(Debug, PartialEq)]
118pub enum EncryptionAlgorithm {
119    /// Salsa20 stream cipher (mode byte `S`). The 16-byte key is doubled to 32 bytes.
120    Salsa20,
121    /// ARC4 (RC4) stream cipher with 1024-byte keystream skip (mode byte `A`).
122    ARC4,
123}
124
125/// Parsed encryption header from a BLTE mode-E block.
126#[derive(Debug)]
127pub struct EncryptionHeader {
128    /// 64-bit key name used to look up the decryption key in the [`TactKeyStore`].
129    pub key_name: u64,
130    /// Initialization vector for the cipher.
131    pub iv: Vec<u8>,
132    /// The encryption algorithm used (Salsa20 or ARC4).
133    pub algorithm: EncryptionAlgorithm,
134}
135
136// ---------------------------------------------------------------------------
137// Header parsing
138// ---------------------------------------------------------------------------
139
140/// Parse an encryption header from the data following the `E` mode byte.
141///
142/// Returns the parsed header and a slice of the remaining encrypted payload.
143pub fn parse_encryption_header(data: &[u8]) -> Result<(EncryptionHeader, &[u8])> {
144    if data.is_empty() {
145        return Err(CascError::InvalidFormat(
146            "encryption header: empty data".into(),
147        ));
148    }
149
150    let mut pos = 0;
151
152    // key_count (u8)
153    let key_count = data[pos] as usize;
154    pos += 1;
155
156    if key_count == 0 {
157        return Err(CascError::InvalidFormat(
158            "encryption header: key_count is 0".into(),
159        ));
160    }
161
162    // We only use the first key but must skip through all of them.
163    let mut key_name: u64 = 0;
164    for i in 0..key_count {
165        if pos >= data.len() {
166            return Err(CascError::InvalidFormat(
167                "encryption header: truncated key_name_size".into(),
168            ));
169        }
170        let key_name_size = data[pos] as usize;
171        pos += 1;
172
173        if pos + key_name_size > data.len() {
174            return Err(CascError::InvalidFormat(
175                "encryption header: truncated key_name".into(),
176            ));
177        }
178
179        if i == 0 {
180            if key_name_size != 8 {
181                return Err(CascError::InvalidFormat(format!(
182                    "encryption header: expected key_name_size 8, got {}",
183                    key_name_size
184                )));
185            }
186            key_name = u64::from_le_bytes(data[pos..pos + 8].try_into().map_err(|_| {
187                CascError::InvalidFormat("encryption header: failed to read key_name".into())
188            })?);
189        }
190        pos += key_name_size;
191    }
192
193    // IV size (u32 LE)
194    if pos + 4 > data.len() {
195        return Err(CascError::InvalidFormat(
196            "encryption header: truncated IV size".into(),
197        ));
198    }
199    let iv_size = u32::from_le_bytes(data[pos..pos + 4].try_into().map_err(|_| {
200        CascError::InvalidFormat("encryption header: failed to read IV size".into())
201    })?) as usize;
202    pos += 4;
203
204    // IV bytes
205    if pos + iv_size > data.len() {
206        return Err(CascError::InvalidFormat(
207            "encryption header: truncated IV".into(),
208        ));
209    }
210    let iv = data[pos..pos + iv_size].to_vec();
211    pos += iv_size;
212
213    // encryption_type (u8)
214    if pos >= data.len() {
215        return Err(CascError::InvalidFormat(
216            "encryption header: missing encryption type".into(),
217        ));
218    }
219    let algorithm = match data[pos] {
220        b'S' => EncryptionAlgorithm::Salsa20,
221        b'A' => EncryptionAlgorithm::ARC4,
222        other => {
223            return Err(CascError::InvalidFormat(format!(
224                "encryption header: unknown algorithm 0x{:02X}",
225                other
226            )));
227        }
228    };
229    pos += 1;
230
231    Ok((
232        EncryptionHeader {
233            key_name,
234            iv,
235            algorithm,
236        },
237        &data[pos..],
238    ))
239}
240
241// ---------------------------------------------------------------------------
242// ARC4 (RC4 with 1024-byte skip)
243// ---------------------------------------------------------------------------
244
245struct Arc4 {
246    s: [u8; 256],
247    i: u8,
248    j: u8,
249}
250
251impl Arc4 {
252    fn new(key: &[u8]) -> Self {
253        let mut s = [0u8; 256];
254        for (i, slot) in s.iter_mut().enumerate() {
255            *slot = i as u8;
256        }
257        // KSA requires in-place swaps driven by computed j, so indexing is necessary
258        let mut j: u8 = 0;
259        #[allow(clippy::needless_range_loop)]
260        for i in 0..256 {
261            j = j.wrapping_add(s[i]).wrapping_add(key[i % key.len()]);
262            s.swap(i, j as usize);
263        }
264        // WoW-specific: skip first 1024 bytes of keystream
265        let mut arc4 = Arc4 { s, i: 0, j: 0 };
266        let mut discard = [0u8; 1024];
267        arc4.process(&mut discard);
268        arc4
269    }
270
271    fn process(&mut self, data: &mut [u8]) {
272        for byte in data.iter_mut() {
273            self.i = self.i.wrapping_add(1);
274            self.j = self.j.wrapping_add(self.s[self.i as usize]);
275            self.s.swap(self.i as usize, self.j as usize);
276            let k =
277                self.s[(self.s[self.i as usize].wrapping_add(self.s[self.j as usize])) as usize];
278            *byte ^= k;
279        }
280    }
281}
282
283// ---------------------------------------------------------------------------
284// Salsa20 decryption helper
285// ---------------------------------------------------------------------------
286
287fn decrypt_salsa20(key: &[u8; 16], iv: &[u8], data: &mut [u8]) {
288    // Salsa20 requires a 32-byte key; double the 16-byte TACT key.
289    let mut full_key = [0u8; 32];
290    full_key[..16].copy_from_slice(key);
291    full_key[16..].copy_from_slice(key);
292
293    // Pad IV to 8-byte nonce.
294    let mut nonce = [0u8; 8];
295    let copy_len = iv.len().min(8);
296    nonce[..copy_len].copy_from_slice(&iv[..copy_len]);
297
298    let mut cipher = Salsa20::new(&full_key.into(), &nonce.into());
299    cipher.apply_keystream(data);
300}
301
302// ---------------------------------------------------------------------------
303// Public decrypt entry point
304// ---------------------------------------------------------------------------
305
306/// Decrypt a BLTE mode-E block.
307///
308/// `data` is everything after the `E` mode byte. Returns the decrypted
309/// payload whose first byte is the inner compression mode (N, Z, 4, etc.).
310pub fn decrypt_block(data: &[u8], keystore: &TactKeyStore) -> Result<Vec<u8>> {
311    let (header, encrypted) = parse_encryption_header(data)?;
312
313    let key = keystore
314        .get(header.key_name)
315        .ok_or_else(|| CascError::EncryptionKeyMissing(format!("0x{:016X}", header.key_name)))?;
316
317    let mut output = encrypted.to_vec();
318    match header.algorithm {
319        EncryptionAlgorithm::Salsa20 => {
320            decrypt_salsa20(key, &header.iv, &mut output);
321        }
322        EncryptionAlgorithm::ARC4 => {
323            let mut cipher = Arc4::new(key);
324            cipher.process(&mut output);
325        }
326    }
327
328    Ok(output)
329}
330
331// ---------------------------------------------------------------------------
332// Known TACT keys
333// ---------------------------------------------------------------------------
334
335/// Convert a hex string to a 16-byte key. Panics on invalid input.
336fn hex_to_key(hex_str: &str) -> [u8; 16] {
337    let bytes = hex::decode(hex_str).expect("invalid hex in known key");
338    let mut key = [0u8; 16];
339    key.copy_from_slice(&bytes);
340    key
341}
342
343/// Convert a hex string to a 16-byte key, returning an error on failure.
344fn hex_to_key_result(hex_str: &str) -> Result<[u8; 16]> {
345    let bytes = hex::decode(hex_str).map_err(|e| {
346        CascError::InvalidFormat(format!("invalid hex key value '{}': {}", hex_str, e))
347    })?;
348    if bytes.len() != 16 {
349        return Err(CascError::InvalidFormat(format!(
350            "key value must be 16 bytes, got {}",
351            bytes.len()
352        )));
353    }
354    let mut key = [0u8; 16];
355    key.copy_from_slice(&bytes);
356    Ok(key)
357}
358
359fn known_keys() -> Vec<(u64, [u8; 16])> {
360    vec![
361        (
362            0xFA505078126ACB3E,
363            hex_to_key("BDC51862ABED79B2DE48C8E7E66C6200"),
364        ),
365        (
366            0xFF813F7D062AC0BC,
367            hex_to_key("AA0B5C77F088CCC2D39049BD267F066D"),
368        ),
369        (
370            0xD1E9B5EDF9283668,
371            hex_to_key("8E4A2579894E38B4AB9058BA5C7328EE"),
372        ),
373        (
374            0xB76729641141CB34,
375            hex_to_key("9849D1AA7B1FD09819C5C66283A326EC"),
376        ),
377        (
378            0xFFB9469FF16E6BF8,
379            hex_to_key("D514BD1909A9E5DC8703F4B8BB1DFD9A"),
380        ),
381        (
382            0x23C5B5DF837A226C,
383            hex_to_key("1406E2D873B6FC99217A180881DA8D62"),
384        ),
385        (
386            0x3AE403EF40AC3037,
387            hex_to_key("EB31B554C67D603E2F10AA8C4584F1CE"),
388        ),
389        (
390            0xE2854509C471C381,
391            hex_to_key("A970FEF382CE86A53A1674C8F36C8F1B"),
392        ),
393        (
394            0x8EE2CB82178C995A,
395            hex_to_key("5FA43C8E204D2F1BFAF1FB26FFE5A34B"),
396        ),
397        (
398            0x5813810F4EC9B005,
399            hex_to_key("7F3DDA67B4A94DE6D3F3B8D4E45FC076"),
400        ),
401        (
402            0x7F3DDA67B4A94DE6,
403            hex_to_key("13AC5E1474618778916727B21F37B31E"),
404        ),
405        (
406            0x402CD9D8D6BFED98,
407            hex_to_key("AEB0EADFE24A0742C24B8FFC2DC28C69"),
408        ),
409        (
410            0xFB680CB6A8BF81F3,
411            hex_to_key("62D90EFA7F36D71C398AE2F1FE37C5F5"),
412        ),
413        (
414            0xDBD3371554F60306,
415            hex_to_key("34E397ACE6DD30EEFDC98A2AB093CD3C"),
416        ),
417        (
418            0x11A9203C9A2D0DC8,
419            hex_to_key("2E609EA137A31F85DE06A14A9FF04AA1"),
420        ),
421        (
422            0x279C3FFB7E3229BC,
423            hex_to_key("53D25B2053C58F053AA4A6EA4E2D1625"),
424        ),
425        (
426            0xC7459A25DC3B7A4C,
427            hex_to_key("C54CF38B19EA7ABCB17B1D5086423A90"),
428        ),
429    ]
430}
431
432// ===========================================================================
433// Tests
434// ===========================================================================
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    // -----------------------------------------------------------------------
441    // TactKeyStore tests
442    // -----------------------------------------------------------------------
443
444    #[test]
445    fn keystore_new_is_empty() {
446        let ks = TactKeyStore::new();
447        assert!(ks.is_empty());
448        assert_eq!(ks.len(), 0);
449    }
450
451    #[test]
452    fn keystore_with_known_keys_not_empty() {
453        let ks = TactKeyStore::with_known_keys();
454        assert!(!ks.is_empty());
455        assert!(ks.len() >= 10);
456    }
457
458    #[test]
459    fn keystore_get_known_key() {
460        let ks = TactKeyStore::with_known_keys();
461        let key = ks.get(0xFA505078126ACB3E);
462        assert!(key.is_some());
463        assert_eq!(key.unwrap().len(), 16);
464    }
465
466    #[test]
467    fn keystore_get_unknown_returns_none() {
468        let ks = TactKeyStore::with_known_keys();
469        assert!(ks.get(0xDEADBEEFCAFEBABE).is_none());
470    }
471
472    #[test]
473    fn keystore_merge() {
474        let mut ks1 = TactKeyStore::new();
475        let mut ks2 = TactKeyStore::new();
476        ks2.keys.insert(0x1234, [0xAA; 16]);
477        ks1.merge(&ks2);
478        assert!(ks1.get(0x1234).is_some());
479        assert_eq!(ks1.get(0x1234).unwrap(), &[0xAA; 16]);
480    }
481
482    #[test]
483    fn keystore_load_keyfile() {
484        use std::io::Write;
485
486        let dir = std::env::temp_dir().join("casc_test_keyfile");
487        std::fs::create_dir_all(&dir).ok();
488        let path = dir.join("test.keys");
489        let mut f = std::fs::File::create(&path).unwrap();
490        writeln!(f, "# Comment line").unwrap();
491        writeln!(f, "FA505078126ACB3E BDC51862ABED79B2DE48C8E7E66C6200").unwrap();
492        writeln!(f).unwrap(); // blank line
493        writeln!(f, "FF813F7D062AC0BC AA0B5C77F088CCC2D39049BD267F066D").unwrap();
494        drop(f);
495
496        let ks = TactKeyStore::load_keyfile(&path).unwrap();
497        assert_eq!(ks.len(), 2);
498        assert!(ks.get(0xFA505078126ACB3E).is_some());
499        assert!(ks.get(0xFF813F7D062AC0BC).is_some());
500
501        std::fs::remove_dir_all(&dir).ok();
502    }
503
504    #[test]
505    fn keystore_load_keyfile_missing_file_errors() {
506        let path = Path::new("nonexistent_keyfile.txt");
507        assert!(TactKeyStore::load_keyfile(path).is_err());
508    }
509
510    #[test]
511    fn keystore_load_keyfile_invalid_hex_errors() {
512        use std::io::Write;
513
514        let dir = std::env::temp_dir().join("casc_test_keyfile_bad");
515        std::fs::create_dir_all(&dir).ok();
516        let path = dir.join("bad.keys");
517        let mut f = std::fs::File::create(&path).unwrap();
518        writeln!(f, "ZZZZ INVALID_HEX_VALUE_HERE_TOO").unwrap();
519        drop(f);
520
521        assert!(TactKeyStore::load_keyfile(&path).is_err());
522        std::fs::remove_dir_all(&dir).ok();
523    }
524
525    // -----------------------------------------------------------------------
526    // Encryption header parsing tests
527    // -----------------------------------------------------------------------
528
529    #[test]
530    fn parse_encryption_header_salsa20() {
531        let mut data = Vec::new();
532        data.push(1u8); // key_count = 1
533        data.push(8u8); // key_name_size = 8
534        data.extend_from_slice(&0xFA505078126ACB3Eu64.to_le_bytes());
535        data.extend_from_slice(&4u32.to_le_bytes()); // iv_size = 4
536        data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]); // IV
537        data.push(b'S'); // Salsa20
538        data.extend_from_slice(b"encrypted_payload");
539
540        let (header, remaining) = parse_encryption_header(&data).unwrap();
541        assert_eq!(header.key_name, 0xFA505078126ACB3E);
542        assert_eq!(header.iv, vec![0x01, 0x02, 0x03, 0x04]);
543        assert_eq!(header.algorithm, EncryptionAlgorithm::Salsa20);
544        assert_eq!(remaining, b"encrypted_payload");
545    }
546
547    #[test]
548    fn parse_encryption_header_arc4() {
549        let mut data = Vec::new();
550        data.push(1u8);
551        data.push(8u8);
552        data.extend_from_slice(&0xDEADBEEFu64.to_le_bytes());
553        data.extend_from_slice(&4u32.to_le_bytes());
554        data.extend_from_slice(&[0x0A, 0x0B, 0x0C, 0x0D]);
555        data.push(b'A'); // ARC4
556        data.extend_from_slice(b"payload");
557
558        let (header, remaining) = parse_encryption_header(&data).unwrap();
559        assert_eq!(header.algorithm, EncryptionAlgorithm::ARC4);
560        assert_eq!(header.key_name, 0xDEADBEEF);
561        assert_eq!(remaining, b"payload");
562    }
563
564    #[test]
565    fn parse_encryption_header_empty_errors() {
566        assert!(parse_encryption_header(&[]).is_err());
567    }
568
569    #[test]
570    fn parse_encryption_header_unknown_algo_errors() {
571        let mut data = Vec::new();
572        data.push(1u8);
573        data.push(8u8);
574        data.extend_from_slice(&0u64.to_le_bytes());
575        data.extend_from_slice(&4u32.to_le_bytes());
576        data.extend_from_slice(&[0; 4]);
577        data.push(b'X'); // unknown
578
579        assert!(parse_encryption_header(&data).is_err());
580    }
581
582    // -----------------------------------------------------------------------
583    // ARC4 tests
584    // -----------------------------------------------------------------------
585
586    #[test]
587    fn arc4_round_trip() {
588        let key = b"test_key_16bytes"; // 16 bytes
589        let plaintext = b"Hello World! This is a test of ARC4 encryption.";
590
591        let mut encrypted = plaintext.to_vec();
592        let mut cipher1 = Arc4::new(key);
593        cipher1.process(&mut encrypted);
594
595        // encrypted should differ from plaintext
596        assert_ne!(&encrypted[..], &plaintext[..]);
597
598        // Decrypt with fresh cipher
599        let mut decrypted = encrypted.clone();
600        let mut cipher2 = Arc4::new(key);
601        cipher2.process(&mut decrypted);
602
603        assert_eq!(&decrypted[..], &plaintext[..]);
604    }
605
606    #[test]
607    fn arc4_empty_data() {
608        let key = b"some_key_16bytes";
609        let mut data = Vec::new();
610        let mut cipher = Arc4::new(key);
611        cipher.process(&mut data);
612        assert!(data.is_empty());
613    }
614
615    // -----------------------------------------------------------------------
616    // Salsa20 tests
617    // -----------------------------------------------------------------------
618
619    #[test]
620    fn salsa20_round_trip() {
621        let key = [0x42u8; 16];
622        let iv = [0x01, 0x02, 0x03, 0x04];
623        let plaintext = b"test salsa20 encryption data";
624
625        // Encrypt
626        let mut data = plaintext.to_vec();
627        decrypt_salsa20(&key, &iv, &mut data);
628        assert_ne!(&data[..], &plaintext[..]);
629
630        // Decrypt (XOR is self-inverse with same keystream)
631        let mut roundtrip = data.clone();
632        decrypt_salsa20(&key, &iv, &mut roundtrip);
633        assert_eq!(&roundtrip[..], &plaintext[..]);
634    }
635
636    // -----------------------------------------------------------------------
637    // Full decrypt_block tests
638    // -----------------------------------------------------------------------
639
640    #[test]
641    fn decrypt_block_missing_key_errors() {
642        let ks = TactKeyStore::new(); // empty - no keys
643        let mut data = Vec::new();
644        data.push(1u8); // key_count
645        data.push(8u8); // key_name_size
646        data.extend_from_slice(&0xDEADBEEFu64.to_le_bytes());
647        data.extend_from_slice(&4u32.to_le_bytes());
648        data.extend_from_slice(&[0; 4]);
649        data.push(b'S');
650        data.extend_from_slice(b"encrypted");
651
652        let result = decrypt_block(&data, &ks);
653        assert!(result.is_err());
654        match result.unwrap_err() {
655            CascError::EncryptionKeyMissing(_) => {}
656            e => panic!("Expected EncryptionKeyMissing, got: {:?}", e),
657        }
658    }
659
660    #[test]
661    fn decrypt_block_salsa20_round_trip() {
662        let key_name: u64 = 0xFA505078126ACB3E;
663        let ks = TactKeyStore::with_known_keys();
664        let key = ks.get(key_name).unwrap();
665
666        // Prepare a "plaintext" that starts with an inner mode byte
667        let plaintext = b"Nhello world inner content";
668        let iv_bytes = [0x10, 0x20, 0x30, 0x40];
669
670        // Encrypt the plaintext with Salsa20 to build a test encrypted payload
671        let mut encrypted_payload = plaintext.to_vec();
672        decrypt_salsa20(key, &iv_bytes, &mut encrypted_payload);
673
674        // Build the full encrypted block (header + encrypted payload)
675        let mut block_data = Vec::new();
676        block_data.push(1u8);
677        block_data.push(8u8);
678        block_data.extend_from_slice(&key_name.to_le_bytes());
679        block_data.extend_from_slice(&4u32.to_le_bytes());
680        block_data.extend_from_slice(&iv_bytes);
681        block_data.push(b'S');
682        block_data.extend_from_slice(&encrypted_payload);
683
684        // Decrypt and verify we get the original plaintext back
685        let decrypted = decrypt_block(&block_data, &ks).unwrap();
686        assert_eq!(&decrypted[..], &plaintext[..]);
687    }
688
689    #[test]
690    fn decrypt_block_arc4_round_trip() {
691        let key_name: u64 = 0xFA505078126ACB3E;
692        let ks = TactKeyStore::with_known_keys();
693        let key = ks.get(key_name).unwrap();
694
695        let plaintext = b"Zcompressed inner data here";
696
697        // Encrypt with ARC4
698        let mut encrypted_payload = plaintext.to_vec();
699        let mut cipher = Arc4::new(key);
700        cipher.process(&mut encrypted_payload);
701
702        // Build header
703        let mut block_data = Vec::new();
704        block_data.push(1u8);
705        block_data.push(8u8);
706        block_data.extend_from_slice(&key_name.to_le_bytes());
707        block_data.extend_from_slice(&4u32.to_le_bytes());
708        block_data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
709        block_data.push(b'A');
710        block_data.extend_from_slice(&encrypted_payload);
711
712        let decrypted = decrypt_block(&block_data, &ks).unwrap();
713        assert_eq!(&decrypted[..], &plaintext[..]);
714    }
715
716    #[test]
717    fn hex_to_key_result_valid() {
718        let key = hex_to_key_result("BDC51862ABED79B2DE48C8E7E66C6200").unwrap();
719        assert_eq!(key.len(), 16);
720        assert_eq!(key[0], 0xBD);
721        assert_eq!(key[15], 0x00);
722    }
723
724    #[test]
725    fn hex_to_key_result_invalid_hex_errors() {
726        assert!(hex_to_key_result("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ").is_err());
727    }
728
729    #[test]
730    fn hex_to_key_result_wrong_length_errors() {
731        assert!(hex_to_key_result("AABB").is_err());
732    }
733
734    #[test]
735    fn known_keys_all_valid() {
736        let keys = known_keys();
737        assert!(keys.len() >= 15);
738        for (name, value) in &keys {
739            assert_ne!(*name, 0, "key name should not be zero");
740            assert_ne!(*value, [0u8; 16], "key value should not be all zeros");
741        }
742    }
743}