use aes::Aes128;
use aes::cipher::{BlockCipherDecrypt, BlockCipherEncrypt, KeyInit};
use crate::packet::{PacketType, StreamingAuthHeader};
const STREAMING_AUTH_HEADER_SIZE: usize = 4;
const STREAMING_AUTH_PAYLOAD_SIZE: usize = 32;
const STREAMING_AUTH_RESULT_FLAG: u16 = 1;
const STREAMING_AUTH_LEVEL_SHIFT: u32 = 1;
const STREAMING_AUTH_LEVEL_MASK: u16 = 0b11 << STREAMING_AUTH_LEVEL_SHIFT;
const AES_BLOCK_SIZE: usize = 16;
pub(crate) const STREAMING_AUTH_RESPONSE_ID: u8 = 0;
pub const STREAMING_AUTH_KEY_ENC: &[u8; 16] = b"Fa0b4tA25f4R038a";
pub const STREAMING_AUTH_KEY_DEC: &[u8; 16] = b"FX0b4tA25f4R038a";
pub const MEMORY_READ_KEY: &[u8; 16] = b"Lh2yfB7n6X7d9a5Z";
pub const HARDWARE_ID_ADDRESS: u32 = 0x40010450;
pub const HARDWARE_ID_SIZE: usize = 12;
pub const DEVICE_INFO_ADDRESS: u32 = 0x00000420;
pub const FIRMWARE_INFO_ADDRESS: u32 = 0x00004420;
pub const CALIBRATION_ADDRESS: u32 = 0x03000C00;
pub const PREFERRED_CALIBRATION_ADDRESS: u32 = 0x03000D80;
pub const INFO_BLOCK_SIZE: usize = 64;
pub const STREAMING_AUTH_CREDENTIAL_SIZE: usize = 12;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HardwareId {
bytes: [u8; HARDWARE_ID_SIZE],
}
impl HardwareId {
pub fn from_bytes(bytes: [u8; HARDWARE_ID_SIZE]) -> Self {
Self { bytes }
}
pub fn serial_prefix(&self) -> Option<String> {
let prefix = &self.bytes[0..6];
if prefix.iter().all(|&b| b.is_ascii_alphanumeric()) {
Some(String::from_utf8_lossy(prefix).to_string())
} else {
None
}
}
pub fn device_id(&self) -> u16 {
u16::from_le_bytes([self.bytes[8], self.bytes[9]])
}
pub fn as_bytes(&self) -> &[u8; HARDWARE_ID_SIZE] {
&self.bytes
}
}
impl std::fmt::Display for HardwareId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", hex::encode(self.bytes))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthCredential {
bytes: [u8; STREAMING_AUTH_CREDENTIAL_SIZE],
}
impl AuthCredential {
pub fn from_bytes(bytes: [u8; STREAMING_AUTH_CREDENTIAL_SIZE]) -> Self {
Self { bytes }
}
pub fn as_bytes(&self) -> &[u8; STREAMING_AUTH_CREDENTIAL_SIZE] {
&self.bytes
}
}
impl From<&HardwareId> for AuthCredential {
fn from(hardware_id: &HardwareId) -> Self {
Self::from_bytes(*hardware_id.as_bytes())
}
}
impl From<HardwareId> for AuthCredential {
fn from(hardware_id: HardwareId) -> Self {
Self::from(&hardware_id)
}
}
#[derive(Debug, Clone, Default)]
pub struct DeviceInfo {
pub model: String,
pub hw_version: String,
pub mfg_date: String,
pub fw_version: String,
pub fw_date: String,
pub serial_id: String,
pub uuid: String,
}
impl DeviceInfo {
pub fn parse_device_info(&mut self, data: &[u8]) {
if data.len() >= 64 {
self.model = extract_string(data, 0x10, 0x1C);
self.hw_version = extract_string(data, 0x1C, 0x28);
self.mfg_date = extract_string(data, 0x28, 0x40);
}
}
pub fn parse_firmware_info(&mut self, data: &[u8]) {
if data.len() >= 64 {
let magic = u32::from_le_bytes(data[0..4].try_into().unwrap_or([0xFF; 4]));
if magic != 0xFFFFFFFF {
self.fw_version = extract_string(data, 0x1C, 0x28);
self.fw_date = extract_string(data, 0x28, 0x34);
}
}
}
pub fn parse_calibration(&mut self, data: &[u8]) {
if data.len() >= 64 {
self.serial_id = extract_string(data, 0x00, 0x07).trim().to_string();
self.uuid = extract_string(data, 0x07, 0x27);
}
}
}
fn extract_string(data: &[u8], start: usize, end: usize) -> String {
if start >= data.len() || end > data.len() || start >= end {
return String::new();
}
let slice = &data[start..end];
let len = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
String::from_utf8_lossy(&slice[..len]).to_string()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StreamingAuthResult {
pub success: bool,
pub attribute: u16,
pub auth_level: u8,
pub decrypted_payload: [u8; 32],
}
impl StreamingAuthResult {
pub fn adcqueue_enabled(&self) -> bool {
self.auth_level != 0
}
}
#[derive(Debug, Clone)]
pub struct InitResult {
pub device_info: DeviceInfo,
pub hardware_id: HardwareId,
pub auth: StreamingAuthResult,
}
impl InitResult {
pub fn is_authenticated(&self) -> bool {
self.auth.success && self.auth.adcqueue_enabled()
}
}
pub fn build_memory_read_payload(address: u32, size: u32) -> [u8; 32] {
let mut plaintext = [0xFFu8; 32];
plaintext[0..4].copy_from_slice(&address.to_le_bytes());
plaintext[4..8].copy_from_slice(&size.to_le_bytes());
plaintext[8..12].copy_from_slice(&0xFFFFFFFFu32.to_le_bytes());
let crc = crc32fast::hash(&plaintext[0..12]);
plaintext[12..16].copy_from_slice(&crc.to_le_bytes());
aes_ecb_encrypt(&plaintext, MEMORY_READ_KEY)
}
pub fn parse_memory_read_payload(ciphertext: &[u8]) -> Option<(u32, u32)> {
let plaintext = decrypt_memory_read_response(ciphertext)?;
if plaintext.len() != 32 {
return None;
}
let address = u32::from_le_bytes(plaintext[0..4].try_into().ok()?);
let size = u32::from_le_bytes(plaintext[4..8].try_into().ok()?);
let magic = u32::from_le_bytes(plaintext[8..12].try_into().ok()?);
let checksum = u32::from_le_bytes(plaintext[12..16].try_into().ok()?);
if magic != u32::MAX || checksum != crc32fast::hash(&plaintext[..12]) || plaintext[16..] != [0xFF; 16] {
return None;
}
Some((address, size))
}
pub const MEMORY_READ_CONFIRMATION_SIZE: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemoryReadConfirmation {
pub address: u32,
pub size: u32,
pub magic: u32,
pub checksum: u32,
pub expected_checksum: u32,
}
impl MemoryReadConfirmation {
pub fn from_bytes(body: &[u8]) -> Option<Self> {
let body: &[u8; MEMORY_READ_CONFIRMATION_SIZE] = body.first_chunk()?;
let word = |range: std::ops::Range<usize>| {
u32::from_le_bytes(body[range].try_into().expect("fixed-size confirmation field"))
};
Some(Self {
address: word(0..4),
size: word(4..8),
magic: word(8..12),
checksum: word(12..16),
expected_checksum: crc32fast::hash(&body[..12]),
})
}
pub fn is_intact(&self) -> bool {
self.magic == u32::MAX && self.checksum == self.expected_checksum
}
}
pub fn parse_memory_read_confirmation(body: &[u8]) -> Option<(u32, u32)> {
let confirmation = MemoryReadConfirmation::from_bytes(body)?;
confirmation
.is_intact()
.then_some((confirmation.address, confirmation.size))
}
pub fn build_memory_read_packet(address: u32, size: u32, tid: u8) -> Vec<u8> {
serialize_infallible(crate::message::Packet::MemoryRead { address, size }, tid)
}
pub fn build_streaming_auth_payload(credential: &AuthCredential) -> [u8; 32] {
let mut plaintext = [0u8; 32];
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
plaintext[0..8].copy_from_slice(×tamp.to_le_bytes());
plaintext[8..20].copy_from_slice(credential.as_bytes());
let random_bytes: [u8; 12] = rand::random();
plaintext[20..32].copy_from_slice(&random_bytes);
aes_ecb_encrypt(&plaintext, STREAMING_AUTH_KEY_ENC)
}
pub fn parse_streaming_auth_request_payload(ciphertext: &[u8]) -> Option<AuthCredential> {
let encrypted: [u8; STREAMING_AUTH_PAYLOAD_SIZE] = ciphertext.try_into().ok()?;
let plaintext = aes_ecb_decrypt(&encrypted, STREAMING_AUTH_KEY_ENC);
Some(AuthCredential::from_bytes(plaintext[8..20].try_into().ok()?))
}
pub fn encrypt_streaming_auth_response_payload(plaintext: &[u8; 32]) -> [u8; 32] {
aes_ecb_encrypt(plaintext, STREAMING_AUTH_KEY_DEC)
}
pub fn build_streaming_auth_packet(credential: &AuthCredential, tid: u8) -> Vec<u8> {
serialize_infallible(
crate::message::Packet::StreamingAuth {
credential: credential.clone(),
},
tid,
)
}
fn serialize_infallible(packet: crate::message::Packet, tid: u8) -> Vec<u8> {
let raw = packet
.to_raw_packet(tid)
.expect("auth command packets are always serializable");
bytes::Bytes::from(raw).to_vec()
}
pub fn parse_streaming_auth_response(response: &[u8]) -> Option<StreamingAuthResult> {
if response.len() < STREAMING_AUTH_HEADER_SIZE + STREAMING_AUTH_PAYLOAD_SIZE {
return None;
}
let (header_bytes, payload) = response.split_at(STREAMING_AUTH_HEADER_SIZE);
let header = StreamingAuthHeader::from_bytes(header_bytes.try_into().ok()?);
parse_streaming_auth_response_parts(header, payload)
}
pub(crate) fn parse_streaming_auth_response_parts(
header: StreamingAuthHeader,
payload: &[u8],
) -> Option<StreamingAuthResult> {
if header.packet_type() != u8::from(PacketType::StreamingAuth)
|| header.reserved_flag()
|| header.id() != STREAMING_AUTH_RESPONSE_ID
|| header.attribute() & STREAMING_AUTH_RESULT_FLAG == 0
{
return None;
}
parse_streaming_auth_response_payload(payload, header.attribute())
}
pub fn parse_streaming_auth_response_payload(payload: &[u8], attribute: u16) -> Option<StreamingAuthResult> {
let encrypted: [u8; STREAMING_AUTH_PAYLOAD_SIZE] = payload.get(..STREAMING_AUTH_PAYLOAD_SIZE)?.try_into().ok()?;
let decrypted = aes_ecb_decrypt(&encrypted, STREAMING_AUTH_KEY_DEC);
let auth_level = ((attribute & STREAMING_AUTH_LEVEL_MASK) >> STREAMING_AUTH_LEVEL_SHIFT) as u8;
Some(StreamingAuthResult {
success: auth_level != 0,
attribute,
auth_level,
decrypted_payload: decrypted,
})
}
fn aes_ecb_encrypt(plaintext: &[u8; 32], key: &[u8; 16]) -> [u8; 32] {
let cipher = Aes128::new(key.into());
let mut output = *plaintext;
for block in output.as_chunks_mut::<AES_BLOCK_SIZE>().0 {
encrypt_aes_block(&cipher, block);
}
output
}
pub fn decrypt_memory_read_response(ciphertext: &[u8]) -> Option<Vec<u8>> {
if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(AES_BLOCK_SIZE) {
return None;
}
let cipher = Aes128::new(MEMORY_READ_KEY.into());
let mut output = ciphertext.to_vec();
for block in output.chunks_mut(AES_BLOCK_SIZE) {
if block.len() == AES_BLOCK_SIZE {
decrypt_aes_block(&cipher, block);
}
}
Some(output)
}
fn aes_ecb_decrypt(ciphertext: &[u8; 32], key: &[u8; 16]) -> [u8; 32] {
let cipher = Aes128::new(key.into());
let mut output = *ciphertext;
for block in output.as_chunks_mut::<AES_BLOCK_SIZE>().0 {
decrypt_aes_block(&cipher, block);
}
output
}
fn encrypt_aes_block(cipher: &Aes128, bytes: &mut [u8]) {
let block: &mut [u8; AES_BLOCK_SIZE] = bytes.try_into().expect("AES chunk has the block size");
cipher.encrypt_block(block.into());
}
fn decrypt_aes_block(cipher: &Aes128, bytes: &mut [u8]) {
let block: &mut [u8; AES_BLOCK_SIZE] = bytes.try_into().expect("AES chunk has the block size");
cipher.decrypt_block(block.into());
}
pub fn aes_ecb_decrypt_block(ciphertext: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
let cipher = Aes128::new(key.into());
let mut output = *ciphertext;
decrypt_aes_block(&cipher, &mut output);
output
}
pub fn aes_ecb_decrypt_blocks(ciphertext: &[u8], key: &[u8; 16]) -> Result<Vec<u8>, crate::error::KMError> {
if !ciphertext.len().is_multiple_of(AES_BLOCK_SIZE) {
return Err(crate::error::KMError::InvalidPacket(format!(
"AES ciphertext must be a multiple of {AES_BLOCK_SIZE} bytes, got {}",
ciphertext.len()
)));
}
let cipher = Aes128::new(key.into());
let mut output = ciphertext.to_vec();
for block in output.as_chunks_mut::<AES_BLOCK_SIZE>().0 {
decrypt_aes_block(&cipher, block);
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
const HARDCODED_MEMORY_READ: &str = "4402010133f8860c0054288cdc7e52729826872dd18b539a39c407d5c063d91102e36a9e";
const HARDCODED_STREAMING_AUTH: &str = "4c0600025538815b69a452c83e54ef1d70f3bc9ae6aac1b12a6ac07c20fde58c7bf517ca";
#[test]
fn test_hardware_id_parsing() {
let bytes: [u8; 12] = [
0x30, 0x37, 0x31, 0x4b, 0x42, 0x50, 0x0d, 0xff, 0x11, 0x0a, 0xff, 0xff, ];
let hw_id = HardwareId::from_bytes(bytes);
assert_eq!(hw_id.serial_prefix(), Some("071KBP".to_string()));
assert_eq!(hw_id.device_id(), 2577);
}
#[test]
fn test_aes_roundtrip() {
let plaintext = [0x42u8; 32];
let encrypted = aes_ecb_encrypt(&plaintext, STREAMING_AUTH_KEY_ENC);
assert_ne!(encrypted, plaintext);
assert_eq!(encrypted.len(), 32);
}
#[test]
fn test_memory_read_packet_structure() {
let packet = build_memory_read_packet(HARDWARE_ID_ADDRESS, HARDWARE_ID_SIZE as u32, 0x02);
assert_eq!(packet.len(), 36);
assert_eq!(packet[0], 0x44); assert_eq!(packet[1], 0x02); assert_eq!(packet[2], 0x01); assert_eq!(packet[3], 0x01); }
#[test]
fn test_streaming_auth_packet_structure() {
let hw_id = HardwareId::from_bytes([0x30, 0x37, 0x31, 0x4b, 0x42, 0x50, 0x0d, 0xff, 0x11, 0x0a, 0xff, 0xff]);
let packet = build_streaming_auth_packet(&AuthCredential::from(&hw_id), 0x06);
assert_eq!(packet.len(), 36);
assert_eq!(packet[0], 0x4C); assert_eq!(packet[1], 0x06); assert_eq!(packet[2], 0x00); assert_eq!(packet[3], 0x02); }
#[test]
fn test_streaming_auth_calibration_credential_roundtrip() {
let credential = AuthCredential::from_bytes(*b"007965 CDFDD");
let encrypted = build_streaming_auth_payload(&credential);
assert_eq!(parse_streaming_auth_request_payload(&encrypted), Some(credential));
}
#[test]
fn test_streaming_auth_response_uses_header_result() {
use crate::{Packet, RawPacket};
use bytes::Bytes;
let plaintext = [0x5a; STREAMING_AUTH_PAYLOAD_SIZE];
let encrypted = aes_ecb_encrypt(&plaintext, STREAMING_AUTH_KEY_DEC);
let response = |attribute| {
let header = StreamingAuthHeader::new()
.with_packet_type(PacketType::StreamingAuth.into())
.with_reserved_flag(false)
.with_id(0)
.with_attribute(attribute);
let mut bytes = Vec::from(header.into_bytes());
bytes.extend_from_slice(&encrypted);
bytes
};
let device_auth = parse_streaming_auth_response(&response(0x0203)).unwrap();
assert!(device_auth.success);
assert!(device_auth.adcqueue_enabled());
assert_eq!(device_auth.auth_level, 1);
assert_eq!(device_auth.attribute, 0x0203);
assert_eq!(device_auth.decrypted_payload, plaintext);
let calibration_auth = parse_streaming_auth_response(&response(0x0205)).unwrap();
assert!(calibration_auth.success);
assert!(calibration_auth.adcqueue_enabled());
assert_eq!(calibration_auth.auth_level, 2);
assert_eq!(calibration_auth.attribute, 0x0205);
assert_eq!(calibration_auth.decrypted_payload, plaintext);
let serialized = Bytes::from(
Packet::StreamingAuthResponse(calibration_auth.clone())
.to_raw_packet(99)
.unwrap(),
);
let reparsed = RawPacket::try_from(serialized).unwrap();
assert_eq!(
Packet::try_from(reparsed).unwrap(),
Packet::StreamingAuthResponse(calibration_auth)
);
let raw_failure = RawPacket::try_from(Bytes::from(response(0x0201))).unwrap();
let Packet::StreamingAuthResponse(failure) = Packet::try_from(raw_failure).unwrap() else {
panic!("expected StreamingAuthResponse");
};
assert!(!failure.success);
assert!(!failure.adcqueue_enabled());
assert_eq!(failure.auth_level, 0);
assert_eq!(failure.attribute, 0x0201);
assert_eq!(failure.decrypted_payload, plaintext);
}
#[test]
fn test_decrypt_hardcoded_memory_read() {
let packet = hex::decode(HARDCODED_MEMORY_READ).unwrap();
assert_eq!(packet[0], 0x44, "MemoryRead type");
assert_eq!(packet[1], 0x02, "TID=2");
assert_eq!(packet[2], 0x01, "Attribute low");
assert_eq!(packet[3], 0x01, "Attribute high");
let ciphertext: [u8; 32] = packet[4..36].try_into().unwrap();
let plaintext = aes_ecb_decrypt(&ciphertext, MEMORY_READ_KEY);
let address = u32::from_le_bytes([plaintext[0], plaintext[1], plaintext[2], plaintext[3]]);
let size = u32::from_le_bytes([plaintext[4], plaintext[5], plaintext[6], plaintext[7]]);
println!("Decrypted MemoryRead: address=0x{:08X}, size={}", address, size);
println!("Plaintext: {:02x?}", plaintext);
assert_eq!(address, 0x420, "Hardcoded reads from 0x420");
assert_eq!(size, 64, "Hardcoded reads 64 bytes");
}
#[test]
fn test_memory_read_for_hardware_id() {
let generated = build_memory_read_packet(HARDWARE_ID_ADDRESS, HARDWARE_ID_SIZE as u32, 0x02);
assert_eq!(generated[0], 0x44, "MemoryRead type");
assert_eq!(generated[1], 0x02, "TID=2");
assert_eq!(generated[2], 0x01, "Attribute low");
assert_eq!(generated[3], 0x01, "Attribute high");
let ciphertext: [u8; 32] = generated[4..36].try_into().unwrap();
let plaintext = aes_ecb_decrypt(&ciphertext, MEMORY_READ_KEY);
let address = u32::from_le_bytes([plaintext[0], plaintext[1], plaintext[2], plaintext[3]]);
let size = u32::from_le_bytes([plaintext[4], plaintext[5], plaintext[6], plaintext[7]]);
assert_eq!(address, HARDWARE_ID_ADDRESS, "Should read from 0x40010450");
assert_eq!(size, HARDWARE_ID_SIZE as u32, "Should read 12 bytes");
let expected_crc = crc32fast::hash(&plaintext[0..12]);
let actual_crc = u32::from_le_bytes([plaintext[12], plaintext[13], plaintext[14], plaintext[15]]);
assert_eq!(actual_crc, expected_crc, "CRC32 should be valid");
}
#[test]
fn test_decrypt_hardcoded_streaming_auth() {
let packet = hex::decode(HARDCODED_STREAMING_AUTH).unwrap();
assert_eq!(packet[0], 0x4C, "StreamingAuth type");
assert_eq!(packet[1], 0x06, "TID=6");
assert_eq!(packet[2], 0x00, "Attribute low");
assert_eq!(packet[3], 0x02, "Attribute high");
let ciphertext: [u8; 32] = packet[4..36].try_into().unwrap();
let plaintext = aes_ecb_decrypt(&ciphertext, STREAMING_AUTH_KEY_ENC);
let timestamp = u64::from_le_bytes(plaintext[0..8].try_into().unwrap());
let hardware_id_bytes: [u8; 12] = plaintext[8..20].try_into().unwrap();
let hardware_id = HardwareId::from_bytes(hardware_id_bytes);
println!("Decrypted StreamingAuth:");
println!(" Timestamp: {} ms", timestamp);
println!(" HardwareID: {}", hardware_id);
println!(" Serial prefix: {:?}", hardware_id.serial_prefix());
println!(" Device ID: {}", hardware_id.device_id());
println!(" Plaintext: {:02x?}", plaintext);
assert_eq!(hardware_id.serial_prefix(), Some("071KBP".to_string()));
assert_eq!(hardware_id.device_id(), 2577);
assert!(timestamp > 0, "Timestamp should be non-zero");
}
#[test]
fn test_streaming_auth_with_known_hardware_id() {
let hw_id = HardwareId::from_bytes([
0x30, 0x37, 0x31, 0x4b, 0x42, 0x50, 0x0d, 0xff, 0x11, 0x0a, 0xff, 0xff, ]);
let packet = build_streaming_auth_packet(&AuthCredential::from(&hw_id), 0x06);
let hardcoded = hex::decode(HARDCODED_STREAMING_AUTH).unwrap();
assert_eq!(&packet[0..4], &hardcoded[0..4], "Headers must match");
let ciphertext: [u8; 32] = packet[4..36].try_into().unwrap();
let plaintext = aes_ecb_decrypt(&ciphertext, STREAMING_AUTH_KEY_ENC);
let extracted_hw: [u8; 12] = plaintext[8..20].try_into().unwrap();
assert_eq!(extracted_hw, hw_id.bytes, "HardwareID must be embedded correctly");
let timestamp = u64::from_le_bytes(plaintext[0..8].try_into().unwrap());
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
assert!(timestamp > now - 3600000, "Timestamp should be recent");
assert!(timestamp <= now + 1000, "Timestamp should not be in future");
}
}