Skip to main content

km003c_lib/
auth.rs

1//! Authentication module for KM003C device
2//!
3//! This module provides data structures and encryption helpers for the
4//! StreamingAuth (0x4C) and MemoryRead (0x44) commands required for AdcQueue streaming.
5//!
6//! # Authentication Flow
7//!
8//! 1. Read HardwareID from device memory at 0x40010450 using MemoryRead (0x44)
9//! 2. Send StreamingAuth (0x4C) with HardwareID embedded in encrypted payload
10//! 3. Device validates and grants auth level 1 (AdcQueue access)
11//!
12//! Firmware V1.9.9 also grants level 2 when the same 12-byte credential field
13//! matches the beginning of its selected calibration record. Response
14//! attribute bits 1-2 contain the resulting authentication level.
15//!
16//! # AES Keys
17//!
18//! - StreamingAuth encrypt (host→device): `Fa0b4tA25f4R038a`
19//! - StreamingAuth decrypt (device→host): `FX0b4tA25f4R038a`
20//! - MemoryRead: `Lh2yfB7n6X7d9a5Z`
21//!
22//! # Usage
23//!
24//! Use the `Packet::MemoryRead` and `Packet::StreamingAuth` variants from the
25//! message module to send authentication commands. This module provides the
26//! underlying encryption and data structures.
27
28use aes::Aes128;
29use aes::cipher::{BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
30
31use crate::packet::{PacketType, StreamingAuthHeader};
32
33const STREAMING_AUTH_HEADER_SIZE: usize = 4;
34const STREAMING_AUTH_PAYLOAD_SIZE: usize = 32;
35const STREAMING_AUTH_RESULT_FLAG: u16 = 1;
36const STREAMING_AUTH_LEVEL_SHIFT: u32 = 1;
37const STREAMING_AUTH_LEVEL_MASK: u16 = 0b11 << STREAMING_AUTH_LEVEL_SHIFT;
38const AES_BLOCK_SIZE: usize = 16;
39pub(crate) const STREAMING_AUTH_RESPONSE_ID: u8 = 0;
40
41/// AES-128 key for StreamingAuth encryption (host → device)
42pub const STREAMING_AUTH_KEY_ENC: &[u8; 16] = b"Fa0b4tA25f4R038a";
43
44/// AES-128 key for StreamingAuth decryption (device → host)
45pub const STREAMING_AUTH_KEY_DEC: &[u8; 16] = b"FX0b4tA25f4R038a";
46
47/// AES-128 key for MemoryRead command
48pub const MEMORY_READ_KEY: &[u8; 16] = b"Lh2yfB7n6X7d9a5Z";
49
50/// Memory address where HardwareID is stored
51pub const HARDWARE_ID_ADDRESS: u32 = 0x40010450;
52
53/// Size of HardwareID in bytes
54pub const HARDWARE_ID_SIZE: usize = 12;
55
56/// Memory address for DeviceInfo1 (model, HW version, mfg date)
57pub const DEVICE_INFO_ADDRESS: u32 = 0x00000420;
58
59/// Memory address for FirmwareInfo (FW version, build date)
60pub const FIRMWARE_INFO_ADDRESS: u32 = 0x00004420;
61
62/// Memory address for CalibrationData (serial, UUID, timestamp)
63pub const CALIBRATION_ADDRESS: u32 = 0x03000C00;
64
65/// Preferred calibration record used for level-2 authentication when present.
66pub const PREFERRED_CALIBRATION_ADDRESS: u32 = 0x03000D80;
67
68/// Size of info blocks (DeviceInfo, FirmwareInfo, Calibration)
69pub const INFO_BLOCK_SIZE: usize = 64;
70
71/// Size of the credential embedded in a StreamingAuth request.
72pub const STREAMING_AUTH_CREDENTIAL_SIZE: usize = 12;
73
74/// 12-byte Hardware ID read from device memory at 0x40010450
75///
76/// Structure:
77/// - Bytes 0-5: Serial prefix (e.g., "071KBP" ASCII)
78/// - Bytes 6-7: Separator (typically 0x0D 0xFF)
79/// - Bytes 8-9: Device ID (little-endian u16)
80/// - Bytes 10-11: Padding (typically 0xFF 0xFF)
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct HardwareId {
83    bytes: [u8; HARDWARE_ID_SIZE],
84}
85
86impl HardwareId {
87    /// Create HardwareId from raw bytes
88    pub fn from_bytes(bytes: [u8; HARDWARE_ID_SIZE]) -> Self {
89        Self { bytes }
90    }
91
92    /// Get the serial prefix (first 6 bytes as ASCII string)
93    pub fn serial_prefix(&self) -> Option<String> {
94        let prefix = &self.bytes[0..6];
95        if prefix.iter().all(|&b| b.is_ascii_alphanumeric()) {
96            Some(String::from_utf8_lossy(prefix).to_string())
97        } else {
98            None
99        }
100    }
101
102    /// Get the device ID (bytes 8-9 as little-endian u16)
103    pub fn device_id(&self) -> u16 {
104        u16::from_le_bytes([self.bytes[8], self.bytes[9]])
105    }
106
107    /// Get raw bytes
108    pub fn as_bytes(&self) -> &[u8; HARDWARE_ID_SIZE] {
109        &self.bytes
110    }
111}
112
113impl std::fmt::Display for HardwareId {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        write!(f, "{}", hex::encode(self.bytes))
116    }
117}
118
119/// Opaque 12-byte credential embedded in a StreamingAuth request.
120///
121/// Firmware V1.9.9 accepts the device HardwareID for auth level 1 and the
122/// beginning of its selected calibration record for auth level 2.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct AuthCredential {
125    bytes: [u8; STREAMING_AUTH_CREDENTIAL_SIZE],
126}
127
128impl AuthCredential {
129    /// Create a credential from its exact wire bytes.
130    pub fn from_bytes(bytes: [u8; STREAMING_AUTH_CREDENTIAL_SIZE]) -> Self {
131        Self { bytes }
132    }
133
134    /// Return the exact credential bytes.
135    pub fn as_bytes(&self) -> &[u8; STREAMING_AUTH_CREDENTIAL_SIZE] {
136        &self.bytes
137    }
138}
139
140impl From<&HardwareId> for AuthCredential {
141    fn from(hardware_id: &HardwareId) -> Self {
142        Self::from_bytes(*hardware_id.as_bytes())
143    }
144}
145
146impl From<HardwareId> for AuthCredential {
147    fn from(hardware_id: HardwareId) -> Self {
148        Self::from(&hardware_id)
149    }
150}
151
152/// Device information parsed from memory blocks
153#[derive(Debug, Clone, Default)]
154pub struct DeviceInfo {
155    /// Model name (e.g., "KM003C")
156    pub model: String,
157    /// Hardware version (e.g., "2.1")
158    pub hw_version: String,
159    /// Manufacturing date (e.g., "2022.11.7")
160    pub mfg_date: String,
161    /// Firmware version (e.g., "1.9.9")
162    pub fw_version: String,
163    /// Firmware build date (e.g., "2025.9.22")
164    pub fw_date: String,
165    /// Serial ID from calibration (e.g., "007965")
166    pub serial_id: String,
167    /// UUID/hash from calibration
168    pub uuid: String,
169}
170
171impl DeviceInfo {
172    /// Parse DeviceInfo1 block (64 bytes from 0x420)
173    ///
174    /// Layout:
175    /// - 0x00-0x0F: reserved
176    /// - 0x10-0x1B: model (12 bytes, null-terminated)
177    /// - 0x1C-0x27: hw_version (12 bytes, null-terminated)
178    /// - 0x28-0x3F: mfg_date (24 bytes, null-terminated)
179    pub fn parse_device_info(&mut self, data: &[u8]) {
180        if data.len() >= 64 {
181            self.model = extract_string(data, 0x10, 0x1C);
182            self.hw_version = extract_string(data, 0x1C, 0x28);
183            self.mfg_date = extract_string(data, 0x28, 0x40);
184        }
185    }
186
187    /// Parse FirmwareInfo block (64 bytes from 0x4420)
188    ///
189    /// Layout:
190    /// - 0x00-0x03: magic (0x00004000 or 0xFFFFFFFF if invalid)
191    /// - 0x10-0x1B: model (12 bytes)
192    /// - 0x1C-0x27: fw_version (12 bytes, null-terminated)
193    /// - 0x28-0x33: fw_date (12 bytes, null-terminated)
194    pub fn parse_firmware_info(&mut self, data: &[u8]) {
195        if data.len() >= 64 {
196            // Check magic - if 0xFFFFFFFF, firmware info is invalid
197            let magic = u32::from_le_bytes(data[0..4].try_into().unwrap_or([0xFF; 4]));
198            if magic != 0xFFFFFFFF {
199                self.fw_version = extract_string(data, 0x1C, 0x28);
200                self.fw_date = extract_string(data, 0x28, 0x34);
201            }
202        }
203    }
204
205    /// Parse CalibrationData block (64 bytes from 0x3000C00)
206    ///
207    /// Layout:
208    /// - 0x00-0x06: serial_id (7 bytes, space-padded)
209    /// - 0x07-0x26: UUID (32 bytes hex string)
210    pub fn parse_calibration(&mut self, data: &[u8]) {
211        if data.len() >= 64 {
212            self.serial_id = extract_string(data, 0x00, 0x07).trim().to_string();
213            self.uuid = extract_string(data, 0x07, 0x27);
214        }
215    }
216}
217
218/// Extract null-terminated string from byte slice
219fn extract_string(data: &[u8], start: usize, end: usize) -> String {
220    if start >= data.len() || end > data.len() || start >= end {
221        return String::new();
222    }
223    let slice = &data[start..end];
224    // Find null terminator or end of slice
225    let len = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
226    String::from_utf8_lossy(&slice[..len]).to_string()
227}
228
229/// Result of StreamingAuth command
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct StreamingAuthResult {
232    /// Whether authentication was successful (AdcQueue access granted)
233    pub success: bool,
234    /// Raw attribute value from response
235    pub attribute: u16,
236    /// Auth level: 0 = failed, 1 = device auth, 2 = calibration auth
237    pub auth_level: u8,
238    /// Decrypted response payload (32 bytes)
239    pub decrypted_payload: [u8; 32],
240}
241
242impl StreamingAuthResult {
243    /// Check if AdcQueue streaming is enabled
244    pub fn adcqueue_enabled(&self) -> bool {
245        self.auth_level != 0
246    }
247}
248
249/// Result of device initialization
250///
251/// Convenience struct for protocol research scripts that manage
252/// the init sequence manually rather than using `KM003C::new()`.
253#[derive(Debug, Clone)]
254pub struct InitResult {
255    /// Device information (model, versions, serial, etc.)
256    pub device_info: DeviceInfo,
257    /// Hardware ID used for authentication
258    pub hardware_id: HardwareId,
259    /// Authentication result
260    pub auth: StreamingAuthResult,
261}
262
263impl InitResult {
264    /// Check if device is ready for AdcQueue streaming
265    pub fn is_authenticated(&self) -> bool {
266        self.auth.success && self.auth.adcqueue_enabled()
267    }
268}
269
270/// Build MemoryRead encrypted payload (32 bytes)
271///
272/// # Arguments
273/// * `address` - Memory address to read from
274/// * `size` - Number of bytes to read
275///
276/// # Returns
277/// 32-byte AES-encrypted payload
278pub fn build_memory_read_payload(address: u32, size: u32) -> [u8; 32] {
279    // Build 32-byte plaintext
280    let mut plaintext = [0xFFu8; 32];
281
282    // Bytes 0-3: Address (little-endian)
283    plaintext[0..4].copy_from_slice(&address.to_le_bytes());
284
285    // Bytes 4-7: Size (little-endian)
286    plaintext[4..8].copy_from_slice(&size.to_le_bytes());
287
288    // Bytes 8-11: Magic (0xFFFFFFFF)
289    plaintext[8..12].copy_from_slice(&0xFFFFFFFFu32.to_le_bytes());
290
291    // Bytes 12-15: CRC32 of bytes 0-11
292    let crc = crc32fast::hash(&plaintext[0..12]);
293    plaintext[12..16].copy_from_slice(&crc.to_le_bytes());
294
295    // Bytes 16-31: Already 0xFF from initialization
296
297    // Encrypt with AES-128-ECB
298    aes_ecb_encrypt(&plaintext, MEMORY_READ_KEY)
299}
300
301/// Decrypt and validate a 32-byte MemoryRead request payload.
302pub fn parse_memory_read_payload(ciphertext: &[u8]) -> Option<(u32, u32)> {
303    let plaintext = decrypt_memory_read_response(ciphertext)?;
304    if plaintext.len() != 32 {
305        return None;
306    }
307
308    let address = u32::from_le_bytes(plaintext[0..4].try_into().ok()?);
309    let size = u32::from_le_bytes(plaintext[4..8].try_into().ok()?);
310    let magic = u32::from_le_bytes(plaintext[8..12].try_into().ok()?);
311    let checksum = u32::from_le_bytes(plaintext[12..16].try_into().ok()?);
312    if magic != u32::MAX || checksum != crc32fast::hash(&plaintext[..12]) || plaintext[16..] != [0xFF; 16] {
313        return None;
314    }
315
316    Some((address, size))
317}
318
319/// Size of the confirmation body the device echoes for a MemoryRead request.
320pub const MEMORY_READ_CONFIRMATION_SIZE: usize = 16;
321
322/// Fields echoed by the device in a MemoryRead confirmation body.
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub struct MemoryReadConfirmation {
325    pub address: u32,
326    pub size: u32,
327    pub magic: u32,
328    pub checksum: u32,
329    /// CRC-32 recomputed over the first 12 bytes of the body.
330    pub expected_checksum: u32,
331}
332
333impl MemoryReadConfirmation {
334    /// Decode the 16-byte confirmation body that follows the packet header.
335    pub fn from_bytes(body: &[u8]) -> Option<Self> {
336        let body: &[u8; MEMORY_READ_CONFIRMATION_SIZE] = body.first_chunk()?;
337        let word = |range: std::ops::Range<usize>| {
338            u32::from_le_bytes(body[range].try_into().expect("fixed-size confirmation field"))
339        };
340
341        Some(Self {
342            address: word(0..4),
343            size: word(4..8),
344            magic: word(8..12),
345            checksum: word(12..16),
346            expected_checksum: crc32fast::hash(&body[..12]),
347        })
348    }
349
350    /// Whether the magic word and the CRC-32 both match.
351    pub fn is_intact(&self) -> bool {
352        self.magic == u32::MAX && self.checksum == self.expected_checksum
353    }
354}
355
356/// Decode a MemoryRead confirmation body into its echoed address and size.
357///
358/// Returns `None` when the body is truncated, the magic word is wrong, or the
359/// CRC-32 does not match.
360pub fn parse_memory_read_confirmation(body: &[u8]) -> Option<(u32, u32)> {
361    let confirmation = MemoryReadConfirmation::from_bytes(body)?;
362    confirmation
363        .is_intact()
364        .then_some((confirmation.address, confirmation.size))
365}
366
367/// Build a MemoryRead (0x44) request packet
368///
369/// The wire layout lives in [`crate::message::Packet::to_raw_packet`]; this is
370/// a convenience wrapper for callers that want the finished bytes.
371///
372/// # Arguments
373/// * `address` - Memory address to read from
374/// * `size` - Number of bytes to read
375/// * `tid` - Transaction ID
376///
377/// # Returns
378/// 36-byte packet: 4-byte header + 32-byte AES-encrypted payload
379pub fn build_memory_read_packet(address: u32, size: u32, tid: u8) -> Vec<u8> {
380    serialize_infallible(crate::message::Packet::MemoryRead { address, size }, tid)
381}
382
383/// Build StreamingAuth encrypted payload (32 bytes)
384///
385/// # Arguments
386/// * `credential` - 12-byte device or calibration credential
387///
388/// # Returns
389/// 32-byte AES-encrypted payload
390pub fn build_streaming_auth_payload(credential: &AuthCredential) -> [u8; 32] {
391    // Build 32-byte plaintext
392    let mut plaintext = [0u8; 32];
393
394    // Bytes 0-7: Timestamp (milliseconds since epoch)
395    let timestamp = std::time::SystemTime::now()
396        .duration_since(std::time::UNIX_EPOCH)
397        .map(|d| d.as_millis() as u64)
398        .unwrap_or(0);
399    plaintext[0..8].copy_from_slice(&timestamp.to_le_bytes());
400
401    // Bytes 8-19: device or calibration authentication credential
402    plaintext[8..20].copy_from_slice(credential.as_bytes());
403
404    // Bytes 20-31: Random padding
405    let random_bytes: [u8; 12] = rand::random();
406    plaintext[20..32].copy_from_slice(&random_bytes);
407
408    // Encrypt with AES-128-ECB
409    aes_ecb_encrypt(&plaintext, STREAMING_AUTH_KEY_ENC)
410}
411
412/// Decrypt a host-to-device StreamingAuth request and extract its credential.
413pub fn parse_streaming_auth_request_payload(ciphertext: &[u8]) -> Option<AuthCredential> {
414    let encrypted: [u8; STREAMING_AUTH_PAYLOAD_SIZE] = ciphertext.try_into().ok()?;
415    let plaintext = aes_ecb_decrypt(&encrypted, STREAMING_AUTH_KEY_ENC);
416    Some(AuthCredential::from_bytes(plaintext[8..20].try_into().ok()?))
417}
418
419/// Encrypt a StreamingAuth payload (for serializing responses)
420///
421/// # Arguments
422/// * `plaintext` - 32-byte plaintext payload
423///
424/// # Returns
425/// 32-byte AES-encrypted payload
426pub fn encrypt_streaming_auth_response_payload(plaintext: &[u8; 32]) -> [u8; 32] {
427    aes_ecb_encrypt(plaintext, STREAMING_AUTH_KEY_DEC)
428}
429
430/// Build a StreamingAuth (0x4C) request packet
431///
432/// The wire layout lives in [`crate::message::Packet::to_raw_packet`]; this is
433/// a convenience wrapper for callers that want the finished bytes.
434///
435/// # Arguments
436/// * `credential` - 12-byte device or calibration credential
437/// * `tid` - Transaction ID
438///
439/// # Returns
440/// 36-byte packet: 4-byte header + 32-byte AES-encrypted payload
441pub fn build_streaming_auth_packet(credential: &AuthCredential, tid: u8) -> Vec<u8> {
442    serialize_infallible(
443        crate::message::Packet::StreamingAuth {
444            credential: credential.clone(),
445        },
446        tid,
447    )
448}
449
450/// Serialize a packet whose `to_raw_packet` conversion cannot fail.
451fn serialize_infallible(packet: crate::message::Packet, tid: u8) -> Vec<u8> {
452    let raw = packet
453        .to_raw_packet(tid)
454        .expect("auth command packets are always serializable");
455    bytes::Bytes::from(raw).to_vec()
456}
457
458/// Parse StreamingAuth (0x4C) response from full packet
459///
460/// # Arguments
461/// * `response` - Raw response bytes (at least 36 bytes: 4-byte header + 32-byte payload)
462///
463/// # Returns
464/// Parsed authentication result
465pub fn parse_streaming_auth_response(response: &[u8]) -> Option<StreamingAuthResult> {
466    if response.len() < STREAMING_AUTH_HEADER_SIZE + STREAMING_AUTH_PAYLOAD_SIZE {
467        return None;
468    }
469
470    let (header_bytes, payload) = response.split_at(STREAMING_AUTH_HEADER_SIZE);
471    let header = StreamingAuthHeader::from_bytes(header_bytes.try_into().ok()?);
472    parse_streaming_auth_response_parts(header, payload)
473}
474
475pub(crate) fn parse_streaming_auth_response_parts(
476    header: StreamingAuthHeader,
477    payload: &[u8],
478) -> Option<StreamingAuthResult> {
479    if header.packet_type() != u8::from(PacketType::StreamingAuth)
480        || header.reserved_flag()
481        || header.id() != STREAMING_AUTH_RESPONSE_ID
482        || header.attribute() & STREAMING_AUTH_RESULT_FLAG == 0
483    {
484        return None;
485    }
486
487    parse_streaming_auth_response_payload(payload, header.attribute())
488}
489
490/// Parse and decrypt a StreamingAuth response payload with its raw response attribute.
491pub fn parse_streaming_auth_response_payload(payload: &[u8], attribute: u16) -> Option<StreamingAuthResult> {
492    let encrypted: [u8; STREAMING_AUTH_PAYLOAD_SIZE] = payload.get(..STREAMING_AUTH_PAYLOAD_SIZE)?.try_into().ok()?;
493    let decrypted = aes_ecb_decrypt(&encrypted, STREAMING_AUTH_KEY_DEC);
494    let auth_level = ((attribute & STREAMING_AUTH_LEVEL_MASK) >> STREAMING_AUTH_LEVEL_SHIFT) as u8;
495
496    Some(StreamingAuthResult {
497        success: auth_level != 0,
498        attribute,
499        auth_level,
500        decrypted_payload: decrypted,
501    })
502}
503
504/// AES-128-ECB encrypt 32 bytes
505fn aes_ecb_encrypt(plaintext: &[u8; 32], key: &[u8; 16]) -> [u8; 32] {
506    let cipher = Aes128::new(key.into());
507    let mut output = *plaintext;
508
509    for block in output.as_chunks_mut::<AES_BLOCK_SIZE>().0 {
510        encrypt_aes_block(&cipher, block);
511    }
512
513    output
514}
515
516/// Decrypt an unframed MemoryRead response payload
517///
518/// The response payload is AES-encrypted with MEMORY_READ_KEY.
519/// Useful for protocol research and manual memory reads.
520pub fn decrypt_memory_read_response(ciphertext: &[u8]) -> Option<Vec<u8>> {
521    if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(AES_BLOCK_SIZE) {
522        return None;
523    }
524
525    // Decrypt in 16-byte blocks
526    let cipher = Aes128::new(MEMORY_READ_KEY.into());
527    let mut output = ciphertext.to_vec();
528
529    for block in output.chunks_mut(AES_BLOCK_SIZE) {
530        if block.len() == AES_BLOCK_SIZE {
531            decrypt_aes_block(&cipher, block);
532        }
533    }
534
535    Some(output)
536}
537
538/// AES-128-ECB decrypt 32 bytes
539fn aes_ecb_decrypt(ciphertext: &[u8; 32], key: &[u8; 16]) -> [u8; 32] {
540    let cipher = Aes128::new(key.into());
541    let mut output = *ciphertext;
542
543    for block in output.as_chunks_mut::<AES_BLOCK_SIZE>().0 {
544        decrypt_aes_block(&cipher, block);
545    }
546
547    output
548}
549
550fn encrypt_aes_block(cipher: &Aes128, bytes: &mut [u8]) {
551    let block: &mut [u8; AES_BLOCK_SIZE] = bytes.try_into().expect("AES chunk has the block size");
552    cipher.encrypt_block(block.into());
553}
554
555fn decrypt_aes_block(cipher: &Aes128, bytes: &mut [u8]) {
556    let block: &mut [u8; AES_BLOCK_SIZE] = bytes.try_into().expect("AES chunk has the block size");
557    cipher.decrypt_block(block.into());
558}
559
560/// AES-128-ECB decrypt a single 16-byte block
561///
562/// Useful for protocol research and manual decryption of individual blocks.
563pub fn aes_ecb_decrypt_block(ciphertext: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
564    let cipher = Aes128::new(key.into());
565    let mut output = *ciphertext;
566    decrypt_aes_block(&cipher, &mut output);
567    output
568}
569
570/// AES-128-ECB decrypt multiple 16-byte blocks
571///
572/// # Arguments
573/// * `ciphertext` - Encrypted data (must be multiple of 16 bytes)
574/// * `key` - 16-byte AES key
575///
576/// # Returns
577/// Decrypted data, or error if ciphertext length is not a multiple of 16
578pub fn aes_ecb_decrypt_blocks(ciphertext: &[u8], key: &[u8; 16]) -> Result<Vec<u8>, crate::error::KMError> {
579    if !ciphertext.len().is_multiple_of(AES_BLOCK_SIZE) {
580        return Err(crate::error::KMError::InvalidPacket(format!(
581            "AES ciphertext must be a multiple of {AES_BLOCK_SIZE} bytes, got {}",
582            ciphertext.len()
583        )));
584    }
585
586    let cipher = Aes128::new(key.into());
587    let mut output = ciphertext.to_vec();
588
589    for block in output.as_chunks_mut::<AES_BLOCK_SIZE>().0 {
590        decrypt_aes_block(&cipher, block);
591    }
592
593    Ok(output)
594}
595
596#[cfg(test)]
597mod tests {
598    use super::*;
599
600    // Known working hardcoded packets from CLI (captured from working session)
601    // MemoryRead packet (tid=2): reads HardwareID from 0x40010450
602    const HARDCODED_MEMORY_READ: &str = "4402010133f8860c0054288cdc7e52729826872dd18b539a39c407d5c063d91102e36a9e";
603    // StreamingAuth packet (tid=6): authenticates with embedded HardwareID
604    const HARDCODED_STREAMING_AUTH: &str = "4c0600025538815b69a452c83e54ef1d70f3bc9ae6aac1b12a6ac07c20fde58c7bf517ca";
605
606    #[test]
607    fn test_hardware_id_parsing() {
608        // Example HardwareID: "071KBP" + separator + device ID 2577
609        let bytes: [u8; 12] = [
610            0x30, 0x37, 0x31, 0x4b, 0x42, 0x50, // "071KBP"
611            0x0d, 0xff, // separator
612            0x11, 0x0a, // device ID 2577 (LE)
613            0xff, 0xff, // padding
614        ];
615
616        let hw_id = HardwareId::from_bytes(bytes);
617        assert_eq!(hw_id.serial_prefix(), Some("071KBP".to_string()));
618        assert_eq!(hw_id.device_id(), 2577);
619    }
620
621    #[test]
622    fn test_aes_roundtrip() {
623        let plaintext = [0x42u8; 32];
624        let encrypted = aes_ecb_encrypt(&plaintext, STREAMING_AUTH_KEY_ENC);
625        // Note: Can't decrypt with same key in real protocol, but can test structure
626        assert_ne!(encrypted, plaintext);
627        assert_eq!(encrypted.len(), 32);
628    }
629
630    #[test]
631    fn test_memory_read_packet_structure() {
632        let packet = build_memory_read_packet(HARDWARE_ID_ADDRESS, HARDWARE_ID_SIZE as u32, 0x02);
633        assert_eq!(packet.len(), 36);
634        assert_eq!(packet[0], 0x44); // MemoryRead
635        assert_eq!(packet[1], 0x02); // TID
636        assert_eq!(packet[2], 0x01); // Attribute low
637        assert_eq!(packet[3], 0x01); // Attribute high
638    }
639
640    #[test]
641    fn test_streaming_auth_packet_structure() {
642        let hw_id = HardwareId::from_bytes([0x30, 0x37, 0x31, 0x4b, 0x42, 0x50, 0x0d, 0xff, 0x11, 0x0a, 0xff, 0xff]);
643        let packet = build_streaming_auth_packet(&AuthCredential::from(&hw_id), 0x06);
644        assert_eq!(packet.len(), 36);
645        assert_eq!(packet[0], 0x4C); // StreamingAuth
646        assert_eq!(packet[1], 0x06); // TID
647        assert_eq!(packet[2], 0x00); // Attribute low
648        assert_eq!(packet[3], 0x02); // Attribute high (0x0002)
649    }
650
651    #[test]
652    fn test_streaming_auth_calibration_credential_roundtrip() {
653        let credential = AuthCredential::from_bytes(*b"007965 CDFDD");
654        let encrypted = build_streaming_auth_payload(&credential);
655
656        assert_eq!(parse_streaming_auth_request_payload(&encrypted), Some(credential));
657    }
658
659    #[test]
660    fn test_streaming_auth_response_uses_header_result() {
661        use crate::{Packet, RawPacket};
662        use bytes::Bytes;
663
664        let plaintext = [0x5a; STREAMING_AUTH_PAYLOAD_SIZE];
665        let encrypted = aes_ecb_encrypt(&plaintext, STREAMING_AUTH_KEY_DEC);
666
667        let response = |attribute| {
668            let header = StreamingAuthHeader::new()
669                .with_packet_type(PacketType::StreamingAuth.into())
670                .with_reserved_flag(false)
671                .with_id(0)
672                .with_attribute(attribute);
673            let mut bytes = Vec::from(header.into_bytes());
674            bytes.extend_from_slice(&encrypted);
675            bytes
676        };
677
678        let device_auth = parse_streaming_auth_response(&response(0x0203)).unwrap();
679        assert!(device_auth.success);
680        assert!(device_auth.adcqueue_enabled());
681        assert_eq!(device_auth.auth_level, 1);
682        assert_eq!(device_auth.attribute, 0x0203);
683        assert_eq!(device_auth.decrypted_payload, plaintext);
684
685        let calibration_auth = parse_streaming_auth_response(&response(0x0205)).unwrap();
686        assert!(calibration_auth.success);
687        assert!(calibration_auth.adcqueue_enabled());
688        assert_eq!(calibration_auth.auth_level, 2);
689        assert_eq!(calibration_auth.attribute, 0x0205);
690        assert_eq!(calibration_auth.decrypted_payload, plaintext);
691
692        let serialized = Bytes::from(
693            Packet::StreamingAuthResponse(calibration_auth.clone())
694                .to_raw_packet(99)
695                .unwrap(),
696        );
697        let reparsed = RawPacket::try_from(serialized).unwrap();
698        assert_eq!(
699            Packet::try_from(reparsed).unwrap(),
700            Packet::StreamingAuthResponse(calibration_auth)
701        );
702
703        let raw_failure = RawPacket::try_from(Bytes::from(response(0x0201))).unwrap();
704        let Packet::StreamingAuthResponse(failure) = Packet::try_from(raw_failure).unwrap() else {
705            panic!("expected StreamingAuthResponse");
706        };
707        assert!(!failure.success);
708        assert!(!failure.adcqueue_enabled());
709        assert_eq!(failure.auth_level, 0);
710        assert_eq!(failure.attribute, 0x0201);
711        assert_eq!(failure.decrypted_payload, plaintext);
712    }
713
714    #[test]
715    fn test_decrypt_hardcoded_memory_read() {
716        // Decrypt the hardcoded MemoryRead packet to see what it reads
717        // NOTE: The hardcoded packet reads from 0x420 (size 64), NOT 0x40010450 (HardwareID)
718        let packet = hex::decode(HARDCODED_MEMORY_READ).unwrap();
719
720        // Check header
721        assert_eq!(packet[0], 0x44, "MemoryRead type");
722        assert_eq!(packet[1], 0x02, "TID=2");
723        assert_eq!(packet[2], 0x01, "Attribute low");
724        assert_eq!(packet[3], 0x01, "Attribute high");
725
726        // Decrypt payload (bytes 4-35)
727        let ciphertext: [u8; 32] = packet[4..36].try_into().unwrap();
728        let plaintext = aes_ecb_decrypt(&ciphertext, MEMORY_READ_KEY);
729
730        // Extract address (bytes 0-3, little-endian)
731        let address = u32::from_le_bytes([plaintext[0], plaintext[1], plaintext[2], plaintext[3]]);
732        // Extract size (bytes 4-7, little-endian)
733        let size = u32::from_le_bytes([plaintext[4], plaintext[5], plaintext[6], plaintext[7]]);
734
735        println!("Decrypted MemoryRead: address=0x{:08X}, size={}", address, size);
736        println!("Plaintext: {:02x?}", plaintext);
737
738        // The hardcoded packet actually reads from 0x420 with size 64 (different memory region)
739        assert_eq!(address, 0x420, "Hardcoded reads from 0x420");
740        assert_eq!(size, 64, "Hardcoded reads 64 bytes");
741    }
742
743    #[test]
744    fn test_memory_read_for_hardware_id() {
745        // Verify our library generates correct MemoryRead for HardwareID
746        let generated = build_memory_read_packet(HARDWARE_ID_ADDRESS, HARDWARE_ID_SIZE as u32, 0x02);
747
748        // Check header
749        assert_eq!(generated[0], 0x44, "MemoryRead type");
750        assert_eq!(generated[1], 0x02, "TID=2");
751        assert_eq!(generated[2], 0x01, "Attribute low");
752        assert_eq!(generated[3], 0x01, "Attribute high");
753
754        // Decrypt and verify address/size
755        let ciphertext: [u8; 32] = generated[4..36].try_into().unwrap();
756        let plaintext = aes_ecb_decrypt(&ciphertext, MEMORY_READ_KEY);
757
758        let address = u32::from_le_bytes([plaintext[0], plaintext[1], plaintext[2], plaintext[3]]);
759        let size = u32::from_le_bytes([plaintext[4], plaintext[5], plaintext[6], plaintext[7]]);
760
761        assert_eq!(address, HARDWARE_ID_ADDRESS, "Should read from 0x40010450");
762        assert_eq!(size, HARDWARE_ID_SIZE as u32, "Should read 12 bytes");
763
764        // Verify CRC32 is correct (bytes 12-15)
765        let expected_crc = crc32fast::hash(&plaintext[0..12]);
766        let actual_crc = u32::from_le_bytes([plaintext[12], plaintext[13], plaintext[14], plaintext[15]]);
767        assert_eq!(actual_crc, expected_crc, "CRC32 should be valid");
768    }
769
770    #[test]
771    fn test_decrypt_hardcoded_streaming_auth() {
772        // Decrypt the hardcoded StreamingAuth packet to extract HardwareID
773        let packet = hex::decode(HARDCODED_STREAMING_AUTH).unwrap();
774
775        // Check header
776        assert_eq!(packet[0], 0x4C, "StreamingAuth type");
777        assert_eq!(packet[1], 0x06, "TID=6");
778        assert_eq!(packet[2], 0x00, "Attribute low");
779        assert_eq!(packet[3], 0x02, "Attribute high");
780
781        // Decrypt payload (bytes 4-35) using ENCRYPT key (since host->device uses encrypt key)
782        let ciphertext: [u8; 32] = packet[4..36].try_into().unwrap();
783        let plaintext = aes_ecb_decrypt(&ciphertext, STREAMING_AUTH_KEY_ENC);
784
785        // Structure: timestamp(8) + HardwareID(12) + padding(12)
786        let timestamp = u64::from_le_bytes(plaintext[0..8].try_into().unwrap());
787        let hardware_id_bytes: [u8; 12] = plaintext[8..20].try_into().unwrap();
788        let hardware_id = HardwareId::from_bytes(hardware_id_bytes);
789
790        println!("Decrypted StreamingAuth:");
791        println!("  Timestamp: {} ms", timestamp);
792        println!("  HardwareID: {}", hardware_id);
793        println!("  Serial prefix: {:?}", hardware_id.serial_prefix());
794        println!("  Device ID: {}", hardware_id.device_id());
795        println!("  Plaintext: {:02x?}", plaintext);
796
797        // Verify the extracted HardwareID
798        assert_eq!(hardware_id.serial_prefix(), Some("071KBP".to_string()));
799        assert_eq!(hardware_id.device_id(), 2577);
800        assert!(timestamp > 0, "Timestamp should be non-zero");
801    }
802
803    #[test]
804    fn test_streaming_auth_with_known_hardware_id() {
805        // Use the HardwareID extracted from the working hardcoded packet
806        // HardwareID: 071KBP, device ID 2577
807        let hw_id = HardwareId::from_bytes([
808            0x30, 0x37, 0x31, 0x4b, 0x42, 0x50, // "071KBP"
809            0x0d, 0xff, // separator
810            0x11, 0x0a, // device ID 2577 (LE)
811            0xff, 0xff, // padding
812        ]);
813
814        // Generate StreamingAuth packet
815        let packet = build_streaming_auth_packet(&AuthCredential::from(&hw_id), 0x06);
816
817        // Verify header matches hardcoded
818        let hardcoded = hex::decode(HARDCODED_STREAMING_AUTH).unwrap();
819        assert_eq!(&packet[0..4], &hardcoded[0..4], "Headers must match");
820
821        // Decrypt our generated packet to verify HardwareID is embedded correctly
822        let ciphertext: [u8; 32] = packet[4..36].try_into().unwrap();
823        let plaintext = aes_ecb_decrypt(&ciphertext, STREAMING_AUTH_KEY_ENC);
824
825        // Extract and verify HardwareID
826        let extracted_hw: [u8; 12] = plaintext[8..20].try_into().unwrap();
827        assert_eq!(extracted_hw, hw_id.bytes, "HardwareID must be embedded correctly");
828
829        // Timestamp should be recent (within last hour - 3600000 ms)
830        let timestamp = u64::from_le_bytes(plaintext[0..8].try_into().unwrap());
831        let now = std::time::SystemTime::now()
832            .duration_since(std::time::UNIX_EPOCH)
833            .unwrap()
834            .as_millis() as u64;
835        assert!(timestamp > now - 3600000, "Timestamp should be recent");
836        assert!(timestamp <= now + 1000, "Timestamp should not be in future");
837    }
838}