From 8f65670a8f8ccf3ed0c34bad3c4c622effdba667 Mon Sep 17 00:00:00 2001 From: fromost Date: Sat, 29 Aug 2026 21:41:07 +0800 Subject: [PATCH] refactor MAC auth --- pn532/Cargo.toml | 2 + pn532/src/driver.rs | 9 +- pn532/src/error.rs | 16 +- pn532/src/felica/error.rs | 13 +- pn532/src/felica/felica_lite_s.rs | 226 ------------------ pn532/src/felica/felica_lite_s/issuance.rs | 7 + pn532/src/felica/felica_lite_s/mod.rs | 171 +++++++++++++ pn532/src/felica/mod.rs | 14 +- pn532/src/felica/types/felica_lite_s/auth.rs | 44 ---- .../types/felica_lite_s/auth/card_key.rs | 53 ++++ .../types/felica_lite_s/auth/mac_input.rs | 57 +++++ .../felica/types/felica_lite_s/auth/mod.rs | 23 ++ .../src/felica/types/felica_lite_s/auth/rc.rs | 58 +++++ .../src/felica/types/felica_lite_s/auth/sk.rs | 68 ++++++ pn532/src/felica/types/felica_lite_s/block.rs | 9 - pn532/src/felica/types/felica_lite_s/mod.rs | 3 +- pn532/src/felica/types/ic_type.rs | 2 + pn532/src/felica/types/idm.rs | 15 ++ pn532/src/felica/types/mod.rs | 31 +++ pn532/src/felica/types/pmm.rs | 15 ++ pn532/src/felica/types/polling.rs | 15 ++ pn532/src/interface.rs | 4 +- src/components/card_reader.rs | 58 ++--- 23 files changed, 557 insertions(+), 356 deletions(-) delete mode 100644 pn532/src/felica/felica_lite_s.rs create mode 100644 pn532/src/felica/felica_lite_s/issuance.rs create mode 100644 pn532/src/felica/felica_lite_s/mod.rs delete mode 100644 pn532/src/felica/types/felica_lite_s/auth.rs create mode 100644 pn532/src/felica/types/felica_lite_s/auth/card_key.rs create mode 100644 pn532/src/felica/types/felica_lite_s/auth/mac_input.rs create mode 100644 pn532/src/felica/types/felica_lite_s/auth/mod.rs create mode 100644 pn532/src/felica/types/felica_lite_s/auth/rc.rs create mode 100644 pn532/src/felica/types/felica_lite_s/auth/sk.rs delete mode 100644 pn532/src/felica/types/felica_lite_s/block.rs diff --git a/pn532/Cargo.toml b/pn532/Cargo.toml index 70ec41d..55cb969 100644 --- a/pn532/Cargo.toml +++ b/pn532/Cargo.toml @@ -23,6 +23,8 @@ heapless = "~0.9" thiserror = { version = "~2", default-features = false } bitfields = "3.1.0" rand_core = "0.10.1" +bytemuck = "1.25.2" +readonly = "0.2.14" [features] default = [] diff --git a/pn532/src/driver.rs b/pn532/src/driver.rs index 63c864b..ff48a07 100644 --- a/pn532/src/driver.rs +++ b/pn532/src/driver.rs @@ -8,6 +8,7 @@ use core::time::Duration; /// A card UID read by [`Pn532::read_passive_target_id`]. #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Uid { pub bytes: heapless::Vec, } @@ -25,15 +26,9 @@ impl core::fmt::Display for Uid { } } -#[cfg(feature = "defmt")] -impl defmt::Format for Uid { - fn format(&self, fmt: defmt::Formatter) { - defmt::write!(fmt, "{}", defmt::Display2Format(self)); - } -} - /// Outcome of [`Pn532::tg_init_as_target`] / [`Pn532::tg_init_as_target_default`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum TargetInitStatus { /// The PN532 entered target mode. Success, diff --git a/pn532/src/error.rs b/pn532/src/error.rs index bbbd764..50f35e9 100644 --- a/pn532/src/error.rs +++ b/pn532/src/error.rs @@ -1,10 +1,10 @@ use core::fmt::Debug; use bitfields::bitflag; -use defmt::Debug2Format; use crate::felica; /// Driver-level error type, generic over the transport (I2C) error. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Error { /// The underlying transport (I2C) reported an error. #[error("transport error: {0:?}")] @@ -34,14 +34,8 @@ pub enum Error { FelicaError(#[from] felica::error::Error), } -#[cfg(feature = "defmt")] -impl defmt::Format for Error { - fn format(&self, fmt: defmt::Formatter) { - defmt::write!(fmt, "{:?}", Debug2Format(self)) - } -} - #[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[bitflag(u8)] pub enum StatusCode { #[base] @@ -74,9 +68,3 @@ pub enum StatusCode { NADMissing = 0x2E, Max = 0x2F } - -impl defmt::Format for StatusCode { - fn format(&self, fmt: defmt::Formatter) { - defmt::write!(fmt, "{}", Debug2Format(self)) - } -} diff --git a/pn532/src/felica/error.rs b/pn532/src/felica/error.rs index d4d4e96..a7bdc49 100644 --- a/pn532/src/felica/error.rs +++ b/pn532/src/felica/error.rs @@ -1,12 +1,9 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Error { #[error("Status error: sf1:{0}, sf2:{1}")] - Status(u8, u8) -} - -#[cfg(feature = "defmt")] -impl defmt::Format for Error { - fn format(&self, f: defmt::Formatter) { - defmt::write!(f, "{}", defmt::Display2Format(self)) - } + Status(u8, u8), + #[cfg(feature = "felica-lite-s")] + #[error("MAC mismatch")] + MACMismatch, } \ No newline at end of file diff --git a/pn532/src/felica/felica_lite_s.rs b/pn532/src/felica/felica_lite_s.rs deleted file mode 100644 index a9e89b2..0000000 --- a/pn532/src/felica/felica_lite_s.rs +++ /dev/null @@ -1,226 +0,0 @@ -use des::cipher::{Block, BlockCipherEncrypt, Key, KeyInit}; -use des::TdesEde2; -use rand_core::Rng; -use crate::{Interface, Pn532}; -use crate::felica::types::{FelicaLiteSBlock, FelicaLiteSServiceCode, IDm, PMm}; - -impl Pn532 { - pub fn set_random_challenge(&mut self, card: &IDm, pmm: &PMm, rng: &mut impl Rng) - -> Result<([u8; 8], [u8; 8]), crate::Error> - { - let mut data = [[0u8; 16]]; - rng.fill_bytes(&mut data[0]); - self.felica_write_without_encryption( - card, pmm, - &[FelicaLiteSServiceCode::Write], - &[FelicaLiteSBlock::RC], - &data, - )?; - let rc1 = data[0][0..8].try_into().unwrap(); - let rc2 = data[0][8..16].try_into().unwrap(); - Ok((rc1, rc2)) - } - - pub fn get_session_keys( - &mut self, - rc: &[u8; 16], - card_key: &[u8; 16], - ) -> Result<([u8; 8], [u8; 8]), crate::Error> { - // FeliCa byte order: - // - // CK1 = CK[0..8] - // CK2 = CK[8..16] - // - // The DES interface receives each half reversed. - let mut des_key = [0u8; 16]; - - des_key[0..8].copy_from_slice(&{ - let mut x = [0u8; 8]; - x.copy_from_slice(&card_key[0..8]); - x.reverse(); - x - }); - - des_key[8..16].copy_from_slice(&{ - let mut x = [0u8; 8]; - x.copy_from_slice(&card_key[8..16]); - x.reverse(); - x - }); - - // RC1 || RC2, with each 8-byte half reversed. - let mut des_rc = [0u8; 16]; - - des_rc[0..8].copy_from_slice(&{ - let mut x = [0u8; 8]; - x.copy_from_slice(&rc[0..8]); - x.reverse(); - x - }); - - des_rc[8..16].copy_from_slice(&{ - let mut x = [0u8; 8]; - x.copy_from_slice(&rc[8..16]); - x.reverse(); - x - }); - - let key = Key::::try_from(des_key).unwrap(); - let cipher = TdesEde2::new(&key); - - // 2-key 3DES-CBC, IV = 0. - let mut state = Block::::default(); - - // RC1 - for i in 0..8 { - state[i] ^= des_rc[i]; - } - cipher.encrypt_block(&mut state); - - let sk1_des: [u8; 8] = *state.as_array().unwrap(); - - // RC2 - for i in 0..8 { - state[i] ^= des_rc[8 + i]; - } - cipher.encrypt_block(&mut state); - - let sk2_des: [u8; 8] = *state.as_array().unwrap(); - - // Convert the DES-side values back to FeliCa byte order. - let mut sk1 = sk1_des; - sk1.reverse(); - - let mut sk2 = sk2_des; - sk2.reverse(); - - crate::debug!("RC: {:02X}", rc); - crate::debug!("CK DES key: {:02X}", des_key); - crate::debug!("RC DES input: {:02X}", des_rc); - crate::debug!("SK1 DES: {:02X}", sk1_des); - crate::debug!("SK1: {:02X}", sk1); - crate::debug!("SK2 DES: {:02X}", sk2_des); - crate::debug!("SK2: {:02X}", sk2); - - Ok((sk1, sk2)) - } - - pub fn get_plain_text(&mut self, card: &IDm, pmm: &PMm) - -> Result<([u8; 56], [u8; 8]), crate::Error> - { - let mut data = [[0u8; 16]; 3]; - self.felica_read_without_encryption( - card, pmm, - &[FelicaLiteSServiceCode::Read], - &[FelicaLiteSBlock::ID, FelicaLiteSBlock::CKV, FelicaLiteSBlock::MACA], - &mut data - )?; - let id = data[0]; - let ckv = data[1]; - let maca = data[2]; - let mut data = [0u8; 56]; - data[0] = FelicaLiteSBlock::ID as u8; - data[1] = 0x00; - data[2] = FelicaLiteSBlock::CKV as u8; - data[3] = 0x00; - data[4] = FelicaLiteSBlock::MACA as u8; - data[5] = 0x00; - data[6] = 0xFF; - data[7] = 0xFF; - data[8..24].copy_from_slice(&id); - data[24..40].copy_from_slice(&ckv); - data[40..56].copy_from_slice(&maca); - crate::debug!("{} {}", maca[0..8], maca[8..16]); - let maca: [u8; 8] = maca[0..8].try_into().unwrap(); - Ok((data, maca)) - } - - pub fn check_mac( - &mut self, - card: &IDm, - pmm: &PMm, - sk1: &[u8; 8], - sk2: &[u8; 8], - iv: &[u8; 8], - ) -> Result> { - let (data, mac_a) = self.get_plain_text(card, pmm)?; - - // data layout from get_plain_text(): - // - // [0..8] = block list - // [8..24] = ID block - // [24..40] = CKV block - // [40..56] = MAC_A block (NOT used in MAC calculation) - - let block_list = &data[0..8]; - let block_data = &data[8..40]; - - // FeliCa Lite-S uses the session keys with their byte order reversed - // at the DES interface. - let mut key_bytes = [0u8; 16]; - - for i in 0..8 { - key_bytes[i] = sk1[7 - i]; - key_bytes[8 + i] = sk2[7 - i]; - } - - let key = Key::::try_from(key_bytes).unwrap(); - let cipher = TdesEde2::new(&key); - - // RC1 is also byte-reversed at the DES interface. - let mut state_bytes = *iv; - state_bytes.reverse(); - - // The initial plaintext for MAC_A is the block-number list. - // Byte order is reversed before the DES operation. - let mut first = [0u8; 8]; - first.copy_from_slice(block_list); - first.reverse(); - - for i in 0..8 { - state_bytes[i] ^= first[i]; - } - - let mut state = Block::::try_from(state_bytes).unwrap(); - cipher.encrypt_block(&mut state); - - crate::debug!( - "MAC block list: {:02X}", - block_list - ); - - crate::debug!( - "MAC first input: {:02X}", - state_bytes - ); - - // Process ID and CKV. - // - // Each 8-byte piece is byte-reversed before entering the - // 2-key 3DES CBC operation. - for chunk in block_data.chunks_exact(8) { - let mut input = [0u8; 8]; - input.copy_from_slice(chunk); - input.reverse(); - - for i in 0..8 { - input[i] ^= state[i]; - } - - crate::debug!("MAC input: {:02X}", input); - - let mut block = Block::::try_from(input).unwrap(); - cipher.encrypt_block(&mut block); - - state = block; - } - - // Reverse the final result back to FeliCa byte order. - let mut mac: [u8; 8] = *state.as_array().unwrap(); - mac.reverse(); - - crate::debug!("mac: {:02X}, mac_a: {:02X}", mac, mac_a); - - Ok(mac == mac_a[0..8]) - } -} \ No newline at end of file diff --git a/pn532/src/felica/felica_lite_s/issuance.rs b/pn532/src/felica/felica_lite_s/issuance.rs new file mode 100644 index 0000000..6db0a0c --- /dev/null +++ b/pn532/src/felica/felica_lite_s/issuance.rs @@ -0,0 +1,7 @@ +use crate::{Interface, Pn532}; + +impl Pn532 { + pub fn setup_first_issuance(&mut self) { + + } +} \ No newline at end of file diff --git a/pn532/src/felica/felica_lite_s/mod.rs b/pn532/src/felica/felica_lite_s/mod.rs new file mode 100644 index 0000000..2881444 --- /dev/null +++ b/pn532/src/felica/felica_lite_s/mod.rs @@ -0,0 +1,171 @@ +pub mod issuance; + +use des::cipher::{Block, BlockCipherEncrypt, Key, KeyInit}; +use des::TdesEde2; +use rand_core::Rng; +use crate::{Interface, Pn532}; +use crate::felica::error::Error; +use crate::felica::types::{AsBlock, CardSession, FelicaLiteSBlock, FelicaLiteSServiceCode}; +use crate::felica::types::felica_lite_s::auth::{AuthSession, CardKey, MacInput, RandomChallenge, SessionKeys}; + +impl Pn532 { + pub fn try_authenticate(&mut self, rng: &mut impl Rng, card: &CardSession, key: &CardKey) + -> Result, crate::Error> + { + let rc = self.set_random_challenge(card, rng)?; + let sk = self.get_session_keys(&rc, key)?; + let session = AuthSession::new(sk, rc); + let is_valid_auth = self.check_auth(card, &session)?; + if is_valid_auth { + Ok(Some(session)) + } else { + Ok(None) + } + } + + pub fn read_with_mac( + &mut self, + card: &CardSession, + session: &AuthSession, + block: impl AsBlock + ) -> Result<[u8; 16], crate::Error> { + let mut block_data = [[0u8; 16]; 2]; + let block_list = [block.as_block(), FelicaLiteSBlock::MACA as u16]; + self.felica_read_without_encryption( + card, + &[FelicaLiteSServiceCode::Read], + &block_list, + &mut block_data + )?; + let input = MacInput::new(&block_data, &block_list); + let mac = self.compute_mac(session, &input); + if mac != input.maca() { + return Err(crate::Error::FelicaError(Error::MACMismatch)); + } + + Ok(block_data[0]) + } + + fn set_random_challenge(&mut self, card: &CardSession, rng: &mut impl Rng) + -> Result> + { + let mut data = [[0u8; 16]]; + rng.fill_bytes(&mut data[0]); + self.felica_write_without_encryption( + card, + &[FelicaLiteSServiceCode::Write], + &[FelicaLiteSBlock::RC], + &data, + )?; + let rc = RandomChallenge::from(data[0]); + Ok(rc) + } + + fn get_session_keys( + &mut self, + rc: &RandomChallenge, + card_key: &CardKey, + ) -> Result> { + let des_key = card_key.as_des(); + + let key = Key::::try_from(des_key).unwrap(); + let cipher = TdesEde2::new(&key); + + Ok(SessionKeys::new(&cipher, &rc)) + + // crate::trace!("RC: {:02X}", rc); + // crate::trace!("CK DES key: {:02X}", des_key); + // crate::trace!("RC DES input: {:02X}", des_rc); + // crate::trace!("SK1 DES: {:02X}", sk1_des); + // crate::trace!("SK1: {:02X}", sk1); + // crate::trace!("SK2 DES: {:02X}", sk2_des); + // crate::trace!("SK2: {:02X}", sk2); + } + + fn compute_mac( + &mut self, + session: &AuthSession, + input: &MacInput + ) -> [u8; 8] { + // data layout from get_plain_text(): + // + // [0..8] = block list + // [8..24] = ID block + // [24..40] = CKV block + // [40..56] = MAC_A block (NOT used in MAC calculation) + + let block_list = &input.block_list(); + let block_data = &input.block_data(); + + // FeliCa Lite-S uses the session keys with their byte order reversed + // at the DES interface. + let key_bytes = session.sk.as_des(); + + let key = Key::::try_from(key_bytes).unwrap(); + let cipher = TdesEde2::new(&key); + + // RC1 is also byte-reversed at the DES interface. + let mut state_bytes = session.rc.rc1(); + state_bytes.reverse(); + + // The initial plaintext for MAC_A is the block-number list. + // Byte order is reversed before the DES operation. + let mut first = [0u8; 8]; + first.copy_from_slice(block_list); + first.reverse(); + + for i in 0..8 { + state_bytes[i] ^= first[i]; + } + + let mut state = Block::::try_from(state_bytes).unwrap(); + cipher.encrypt_block(&mut state); + + // Process ID and CKV. + // + // Each 8-byte piece is byte-reversed before entering the + // 2-key 3DES CBC operation. + for chunk in block_data.chunks_exact(8) { + let mut input = [0u8; 8]; + input.copy_from_slice(chunk); + input.reverse(); + + for i in 0..8 { + input[i] ^= state[i]; + } + + crate::trace!("MAC input: {:02X}", input); + + let mut block = Block::::try_from(input).unwrap(); + cipher.encrypt_block(&mut block); + + state = block; + } + + // Reverse the final result back to FeliCa byte order. + let mut mac: [u8; 8] = state.0; + mac.reverse(); + crate::trace!("mac: {:02X}, maca: {:02X}", mac, input.maca()); + mac + } + + fn check_auth( + &mut self, + card: &CardSession, + session: &AuthSession, + ) -> Result> { + let mut data = [[0u8; 16]; 3]; + self.felica_read_without_encryption( + card, + &[FelicaLiteSServiceCode::Read], + &[FelicaLiteSBlock::ID, FelicaLiteSBlock::CKV, FelicaLiteSBlock::MACA], + &mut data + )?; + let block_list = &[FelicaLiteSBlock::ID, FelicaLiteSBlock::CKV, FelicaLiteSBlock::MACA]; + let input = MacInput::new( + &data, block_list + ); + let mac = self.compute_mac(session, &input); + Ok(mac == input.maca()) + } +} \ No newline at end of file diff --git a/pn532/src/felica/mod.rs b/pn532/src/felica/mod.rs index 5f00e7a..039de1d 100644 --- a/pn532/src/felica/mod.rs +++ b/pn532/src/felica/mod.rs @@ -126,8 +126,7 @@ impl Pn532 { /// Note: the number of blocks is limited by the 64-byte response buffer. pub fn felica_read_without_encryption( &mut self, - card: &IDm, - pmm: &PMm, + card: &CardSession, service_code_list: &[impl AsServiceCode], block_list: &[impl AsBlock], block_data: &mut [[u8; 16]], @@ -160,7 +159,7 @@ impl Pn532 { // command let mut cmd = heapless::Vec::::new(); cmd.push(FELICA_CMD_READ_WITHOUT_ENCRYPTION).unwrap(); - cmd.extend_from_slice(&card.into_bytes()).unwrap(); + cmd.extend_from_slice(&card.idm.into_bytes()).unwrap(); cmd.push(num_service as u8).unwrap(); for sc in service_code_list { let sc: [u8; 2] = sc.as_service_code().to_le_bytes(); @@ -180,7 +179,7 @@ impl Pn532 { let response_len = self.felica_send_command( &cmd, &mut response, - pmm.get_read_timeout(num_block, Some(Duration::from_millis(50))) + card.pmm.get_read_timeout(num_block, Some(Duration::from_millis(50))) )?; crate::trace!("Read Response: {:02X}", response); if response_len != 12 + 16 * num_block { @@ -201,8 +200,7 @@ impl Pn532 { /// Send a FeliCa "Write Without Encryption" command. pub fn felica_write_without_encryption( &mut self, - card: &IDm, - pmm: &PMm, + card: &CardSession, service_code_list: &[impl AsServiceCode], block_list: &[impl AsBlock], block_data: &[[u8; 16]], @@ -234,7 +232,7 @@ impl Pn532 { // command let mut cmd = heapless::Vec::::new(); cmd.push(FELICA_CMD_WRITE_WITHOUT_ENCRYPTION).unwrap(); - cmd.extend_from_slice(&card.into_bytes()).unwrap(); + cmd.extend_from_slice(&card.idm.into_bytes()).unwrap(); cmd.push(num_service as u8).unwrap(); for sc in service_code_list { let sc = sc.as_service_code().to_le_bytes(); @@ -254,7 +252,7 @@ impl Pn532 { let response_len = self.felica_send_command( &cmd, &mut response, - pmm.get_write_timeout(num_block, Some(Duration::from_millis(50))) + card.pmm.get_write_timeout(num_block, Some(Duration::from_millis(50))) )?; if response_len != RESPONSE_SIZE { return Err(Error::InvalidFrame); diff --git a/pn532/src/felica/types/felica_lite_s/auth.rs b/pn532/src/felica/types/felica_lite_s/auth.rs deleted file mode 100644 index f6a14e2..0000000 --- a/pn532/src/felica/types/felica_lite_s/auth.rs +++ /dev/null @@ -1,44 +0,0 @@ -// use core::fmt::{Display, Formatter}; -// use bitfields::bitfield; -// -// #[bitfield([u8; 16])] -// #[derive(Eq, PartialEq)] -// pub struct ChallengeKey { -// pub ck1: [u8; 8], -// pub ck2: [u8; 8], -// } -// -// impl ChallengeKey { -// pub fn try_from_block(block: &[u8; 16]) -> Result>::Error> { -// let mut ck1: [u8; 8] = block[0..8].try_into()?; -// ck1.reverse(); -// let mut ck2: [u8; 8] = block[8..16].try_into()?; -// ck2.reverse(); -// let mut instance = Self::new(); -// instance.set_ck1(ck1); -// instance.set_ck2(ck2); -// Ok(instance) -// } -// } -// -// impl Display for ChallengeKey { -// fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { -// #[cfg(feature = "alloc")] -// { -// let ck1 = hex::encode(self.ck1()); -// let ck2 = hex::encode(self.ck2()); -// write!(f, "{ck1},{ck2}") -// } -// #[cfg(not(feature = "alloc"))] -// { -// write!(f, "{:?}", self) -// } -// } -// } -// -// #[cfg(feature = "defmt")] -// impl defmt::Format for ChallengeKey { -// fn format(&self, fmt: defmt::Formatter) { -// defmt::write!(fmt, "{}", defmt::Display2Format(self)) -// } -// } \ No newline at end of file diff --git a/pn532/src/felica/types/felica_lite_s/auth/card_key.rs b/pn532/src/felica/types/felica_lite_s/auth/card_key.rs new file mode 100644 index 0000000..4394979 --- /dev/null +++ b/pn532/src/felica/types/felica_lite_s/auth/card_key.rs @@ -0,0 +1,53 @@ +use core::fmt::Display; +use core::ops::Deref; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct CardKey(pub [u8; 16]); + +impl CardKey { + /// FeliCa byte order: + /// + /// CK1 = CK[0..8] + /// + /// CK2 = CK[8..16] + /// + /// The DES interface receives each half reversed. + pub fn as_des(&self) -> [u8; 16] { + let card_key = self.0; + let mut des_key = [0u8; 16]; + + for i in 0..8 { + des_key[7-i] = card_key[i]; + des_key[15-i] = card_key[i + 8]; + } + + des_key + } +} + +impl Deref for CardKey { + type Target = [u8; 16]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<[u8; 16]> for CardKey { + fn from(val: [u8; 16]) -> Self { + CardKey(val) + } +} + +impl Display for CardKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "alloc")] + { + write!(f, "{}", hex::encode_upper(self.0)) + } + #[cfg(not(feature = "alloc"))] + { + write!(f, "{:?}", self.0) + } + } +} \ No newline at end of file diff --git a/pn532/src/felica/types/felica_lite_s/auth/mac_input.rs b/pn532/src/felica/types/felica_lite_s/auth/mac_input.rs new file mode 100644 index 0000000..20add88 --- /dev/null +++ b/pn532/src/felica/types/felica_lite_s/auth/mac_input.rs @@ -0,0 +1,57 @@ +use crate::felica::types::AsBlock; +use crate::FELICA_READ_MAX_BLOCK_NUM; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct MacInput { + block_data: [[u8; 16]; N], + block_list: [u8; N] +} + +impl MacInput { + pub fn new(block_data: &[[u8; 16]; N], blocks_list: &[impl AsBlock; N]) -> MacInput { + if N > FELICA_READ_MAX_BLOCK_NUM { + panic!("too many blocks"); + } + if N < 2 { + panic!("block should have at least 2 blocks"); + } + let mut block_nums= [0u8; N]; + for (i, block) in blocks_list.iter().enumerate(){ + block_nums[i] = block.as_block() as u8; + } + Self { + block_data: *block_data, + block_list: block_nums + } + } + + pub fn as_vec(&self) -> heapless::Vec { + let mut data = heapless::Vec::new(); + data.extend(self.block_list()); + data.extend_from_slice(&self.block_data()).unwrap(); + data + } + + pub fn block_data(&self) -> heapless::Vec { + let mut data = heapless::Vec::::new(); + for block in self.block_data.iter().take(N - 1) { + data.extend_from_slice(block).unwrap(); + } + data + } + + + pub fn block_list(&self) -> [u8; 8] { + let mut data = [0xFFu8; 8]; + for (i, block) in self.block_list.iter().enumerate() { + data[2 * i] = *block; + data[1 + 2 * i] = 0x00; + } + data + } + + pub fn maca(&self) -> [u8; 8] { + self.block_data[N - 1][0..8].try_into().unwrap() + } +} \ No newline at end of file diff --git a/pn532/src/felica/types/felica_lite_s/auth/mod.rs b/pn532/src/felica/types/felica_lite_s/auth/mod.rs new file mode 100644 index 0000000..8a9bf0a --- /dev/null +++ b/pn532/src/felica/types/felica_lite_s/auth/mod.rs @@ -0,0 +1,23 @@ +mod card_key; +mod rc; +mod sk; +mod mac_input; + +pub use card_key::*; +pub use rc::*; +pub use sk::*; +pub use mac_input::*; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[readonly::make] +pub struct AuthSession { + pub sk: SessionKeys, + pub rc: RandomChallenge +} + +impl AuthSession { + pub fn new(sk: SessionKeys, rc: RandomChallenge) -> Self { + AuthSession { sk, rc } + } +} \ No newline at end of file diff --git a/pn532/src/felica/types/felica_lite_s/auth/rc.rs b/pn532/src/felica/types/felica_lite_s/auth/rc.rs new file mode 100644 index 0000000..9b34630 --- /dev/null +++ b/pn532/src/felica/types/felica_lite_s/auth/rc.rs @@ -0,0 +1,58 @@ +use core::fmt::Display; +use core::ops::Deref; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct RandomChallenge([u8; 16]); + +impl Deref for RandomChallenge { + type Target = [u8; 16]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<[u8; 16]> for RandomChallenge { + fn from(data: [u8; 16]) -> Self { + RandomChallenge(data) + } +} + +impl RandomChallenge { + /// RC1 || RC2, with each 8-byte half reversed. + pub fn as_des(&self) -> [u8; 16] { + let rc = self.0; + let mut des = [0u8; 16]; + + for i in 0..8 { + des[7-i] = rc[i]; + des[15-i] = rc[i + 8]; + } + + des + } + + pub fn rc1(&self) -> [u8; 8] { + let rc = self.0; + rc[0..8].try_into().unwrap() + } + + pub fn rc2(&self) -> [u8; 8] { + let rc = self.0; + rc[8..16].try_into().unwrap() + } +} + +impl Display for RandomChallenge { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "alloc")] + { + write!(f, "{},{}", hex::encode_upper(self.rc1()), hex::encode_upper(self.rc2())) + } + #[cfg(not(feature = "alloc"))] + { + write!(f, "{:?}", self.0) + } + } +} \ No newline at end of file diff --git a/pn532/src/felica/types/felica_lite_s/auth/sk.rs b/pn532/src/felica/types/felica_lite_s/auth/sk.rs new file mode 100644 index 0000000..0ea81df --- /dev/null +++ b/pn532/src/felica/types/felica_lite_s/auth/sk.rs @@ -0,0 +1,68 @@ +use cbc::cipher::{Block, BlockCipherEncrypt}; +use des::TdesEde2; +use crate::felica::types::felica_lite_s::auth::RandomChallenge; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SessionKeys { + sk1: [u8; 8], + sk2: [u8; 8], +} + +impl SessionKeys { + pub fn new(cipher: &TdesEde2, rc: &RandomChallenge) -> Self { + let des_rc = rc.as_des(); + // 2-key 3DES-CBC, IV = 0. + let mut state = Block::::default(); + + // RC1 + for i in 0..8 { + state[i] ^= des_rc[i]; + } + cipher.encrypt_block(&mut state); + + let sk1_des: [u8; 8] = *state.as_array().unwrap(); + + // RC2 + for i in 0..8 { + state[i] ^= des_rc[8 + i]; + } + cipher.encrypt_block(&mut state); + + let sk2_des: [u8; 8] = *state.as_array().unwrap(); + + // Convert the DES-side values back to FeliCa byte order. + let mut sk1 = sk1_des; + sk1.reverse(); + + let mut sk2 = sk2_des; + sk2.reverse(); + Self { sk1, sk2 } + } + + pub fn as_des(&self) -> [u8; 16] { + let mut key_bytes = [0u8; 16]; + + for i in 0..8 { + key_bytes[i] = self.sk1[7 - i]; + key_bytes[8 + i] = self.sk2[7 - i]; + } + + key_bytes + } + + pub fn as_bytes(&self) -> [u8; 16] { + let mut key_bytes = [0u8; 16]; + key_bytes[0..8].copy_from_slice(&self.sk1); + key_bytes[8..16].copy_from_slice(&self.sk2); + key_bytes + } + + pub fn sk1(&self) -> [u8; 8] { + self.sk1 + } + + pub fn sk2(&self) -> [u8; 8] { + self.sk2 + } +} \ No newline at end of file diff --git a/pn532/src/felica/types/felica_lite_s/block.rs b/pn532/src/felica/types/felica_lite_s/block.rs deleted file mode 100644 index 49ef885..0000000 --- a/pn532/src/felica/types/felica_lite_s/block.rs +++ /dev/null @@ -1,9 +0,0 @@ -use bitfields::bitfield; - -#[bitfield([u8; 16])] -#[derive(Eq, PartialEq)] -pub struct SubstractionRegisterBlock { - pub reg_a: [u8; 4], - pub reg_b: [u8; 4], - pub reg_c: [u8; 8], -} \ No newline at end of file diff --git a/pn532/src/felica/types/felica_lite_s/mod.rs b/pn532/src/felica/types/felica_lite_s/mod.rs index 4624ad7..c455958 100644 --- a/pn532/src/felica/types/felica_lite_s/mod.rs +++ b/pn532/src/felica/types/felica_lite_s/mod.rs @@ -1,4 +1,3 @@ -pub mod block; pub mod auth; ///! Types for Felica Lite-S (RC-S966) @@ -10,6 +9,7 @@ use super::AsServiceCode; /// Service Code #[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[bitflag(u16)] pub enum ServiceCode { #[base] @@ -32,6 +32,7 @@ impl AsServiceCode for ServiceCode { /// FeliCa Lite system block numbers. #[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[bitflag(u16)] pub enum Block { #[base] diff --git a/pn532/src/felica/types/ic_type.rs b/pn532/src/felica/types/ic_type.rs index 5761450..ca01699 100644 --- a/pn532/src/felica/types/ic_type.rs +++ b/pn532/src/felica/types/ic_type.rs @@ -2,6 +2,7 @@ use bitfields::bitflag; /// https://www.sony.co.jp/en/Products/felica/business/tech-support/list.html #[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[bitflag(u8)] pub enum CardICType { #[base] @@ -29,6 +30,7 @@ pub enum CardICType { /// https://www.sony.co.jp/en/Products/felica/business/tech-support/list.html #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum MobileICType { /// 0x14~0x1F V3(u8), diff --git a/pn532/src/felica/types/idm.rs b/pn532/src/felica/types/idm.rs index 5c0e9fc..52440db 100644 --- a/pn532/src/felica/types/idm.rs +++ b/pn532/src/felica/types/idm.rs @@ -1,8 +1,23 @@ +use core::fmt::{Display, Formatter}; use bitfields::bitfield; #[bitfield([u8; 8], from_endian = big, into_endian = big)] #[derive(PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct IDm { pub manufacturer: [u8; 2], pub card_id: [u8; 6] +} + +impl Display for IDm { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "alloc")] + { + write!(f, "{}", hex::encode_upper(self.0)) + } + #[cfg(not(feature = "alloc"))] + { + write!(f, "{:?}", self) + } + } } \ No newline at end of file diff --git a/pn532/src/felica/types/mod.rs b/pn532/src/felica/types/mod.rs index 18dd6ea..0e489cb 100644 --- a/pn532/src/felica/types/mod.rs +++ b/pn532/src/felica/types/mod.rs @@ -5,12 +5,14 @@ mod idm; pub mod felica_lite_s; pub mod ic_type; +use core::fmt::{Display, Formatter}; pub use idm::*; pub use pmm::*; pub use polling::PollingRequestCode as FelicaPollingRequestCode; pub use polling::PollingResponse as FelicaPollingResponse; #[cfg(feature = "felica-lite-s")] pub use felica_lite_s::{Block as FelicaLiteSBlock, ServiceCode as FelicaLiteSServiceCode}; +use crate::felica::types::polling::PollingResponse; pub trait AsServiceCode { fn as_service_code(&self) -> u16; @@ -35,4 +37,33 @@ impl AsServiceCode for u16 { fn from_service_code(val: u16) -> Self { val } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[readonly::make] +pub struct CardSession { + pub idm: IDm, + pub pmm: PMm +} + +impl CardSession { + pub fn new(idm: IDm, pmm: PMm) -> Self { + Self { idm, pmm } + } +} + +impl From for CardSession { + fn from(value: PollingResponse) -> Self { + Self { + idm: value.idm, + pmm: value.pmm + } + } +} + +impl Display for CardSession { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "IDm: {}, PMm: {}", self.idm, self.pmm) + } } \ No newline at end of file diff --git a/pn532/src/felica/types/pmm.rs b/pn532/src/felica/types/pmm.rs index b675551..64ee2d9 100644 --- a/pn532/src/felica/types/pmm.rs +++ b/pn532/src/felica/types/pmm.rs @@ -1,8 +1,10 @@ +use core::fmt::{Display, Formatter}; use core::time::Duration; use bitfields::bitfield; #[bitfield([u8; 8])] #[derive(PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PMm { pub rom_type: u8, pub ic_type: u8, @@ -37,4 +39,17 @@ impl PMm { // Round up: a timeout must never underestimate the max response time. Duration::from_micros(us as u64 + 1) } +} + +impl Display for PMm { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "alloc")] + { + write!(f, "{}", hex::encode_upper(self.0)) + } + #[cfg(not(feature = "alloc"))] + { + write!(f, "{:?}", self) + } + } } \ No newline at end of file diff --git a/pn532/src/felica/types/polling.rs b/pn532/src/felica/types/polling.rs index deb91f7..50d556a 100644 --- a/pn532/src/felica/types/polling.rs +++ b/pn532/src/felica/types/polling.rs @@ -1,7 +1,10 @@ +use alloc::string::ToString; +use core::fmt::Display; use bitfields::bitflag; use super::{IDm, PMm}; #[derive(Debug, PartialEq, Eq, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[bitflag(u8)] pub enum PollingRequestCode { NoRequest = 0x00, @@ -11,6 +14,7 @@ pub enum PollingRequestCode { } #[derive(Debug, PartialEq, Eq, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[bitflag(u8)] pub enum PollingTimeSlot { #[default] @@ -24,6 +28,7 @@ pub enum PollingTimeSlot { /// Result of a successful FeliCa polling request. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PollingResponse { /// The card's IDm (NFCID2). pub idm: IDm, @@ -31,4 +36,14 @@ pub struct PollingResponse { pub pmm: PMm, /// The card's system code, when returned. pub system_code_response: Option, +} + +impl Display for PollingResponse { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + let system_code = match self.system_code_response { + Some(system_code) => hex::encode_upper(system_code.to_be_bytes().to_vec()), + None => "None".to_string() + }; + write!(f, "IDm: {}, PMm, {}, System Code: {}", self.idm, self.pmm, system_code) + } } \ No newline at end of file diff --git a/pn532/src/interface.rs b/pn532/src/interface.rs index 9d9b149..14ba626 100644 --- a/pn532/src/interface.rs +++ b/pn532/src/interface.rs @@ -214,11 +214,11 @@ where frame[idx] = (!sum).wrapping_add(1); frame[idx + 1] = PN532_POSTAMBLE; - crate::debug!("pn532: write cmd=0x{:02X} len={}", header[0], frame_len); + crate::trace!("pn532: write cmd=0x{:02X} len={}", header[0], frame_len); self.i2c .write(PN532_I2C_ADDRESS, &frame[..frame_len]) .map_err(Error::Transport)?; - crate::debug!("pn532: write ACKed, reading ACK frame"); + crate::trace!("pn532: write ACKed, reading ACK frame"); self.read_ack_frame() } diff --git a/src/components/card_reader.rs b/src/components/card_reader.rs index d2066b5..64fe78c 100644 --- a/src/components/card_reader.rs +++ b/src/components/card_reader.rs @@ -4,17 +4,18 @@ use embassy_sync::once_lock::OnceLock; use embassy_time::{Delay, Timer}; use esp_hal::gpio; use esp_hal::rng::Trng; -use pn532::felica::types::{FelicaPollingRequestCode}; +use pn532::felica::types::{CardSession, FelicaLiteSBlock, FelicaPollingRequestCode}; use pn532::{BaudRate, Pn532}; +use pn532::felica::types::felica_lite_s::auth::CardKey; -static CARD_KEY: OnceLock<[u8; 16]> = OnceLock::new(); +static CARD_KEY: OnceLock = OnceLock::new(); const CARD_KEY_STR: &str = env!("CARD_KEY"); -fn get_card_key() -> &'static [u8; 16] { +fn get_card_key() -> &'static CardKey { CARD_KEY.get_or_init(|| { let mut buffer = [0u8; 16]; hex::decode_to_slice(CARD_KEY_STR, &mut buffer).unwrap(); - buffer + CardKey(buffer) }) } @@ -43,7 +44,7 @@ pub async fn start_card_reader( mut trng: Trng ) -> () { let card_key = get_card_key(); - defmt::trace!("card_key: {}", hex::encode_upper(card_key)); + defmt::trace!("card_key: {}", card_key); loop { Timer::after_millis(50).await; @@ -63,6 +64,7 @@ pub async fn start_card_reader( continue; }; defmt::info!("Polling card: {}", Debug2Format(&response)); + let card = CardSession::from(response); // if let Err(e) = card_reader.driver.felica_write_without_encryption( // &response.idm, &response.pmm, @@ -74,46 +76,28 @@ pub async fn start_card_reader( // continue; // } - let (rc1, rc2) = match card_reader.driver.set_random_challenge( - &response.idm, - &response.pmm, - &mut trng - ) { - Ok(rc) => rc, + let auth_result = card_reader.driver.try_authenticate( + &mut trng, &card, card_key + ); + let session = match auth_result { + Ok(Some(sk)) => sk, + Ok(None) => { + defmt::warn!("card key is invalid"); + continue; + }, Err(e) => { - defmt::error!("card_reader rc error: {}", e); + defmt::error!("card_reader auth error: {}", e); continue; } }; - defmt::info!("RC: rc1: {:02X}, rc2: {:02X}", rc1, rc2); - - let mut rc = [0u8; 16]; - rc[..8].copy_from_slice(&rc1); - rc[8..].copy_from_slice(&rc2); - - let (sk1, sk2) = match card_reader.driver.get_session_keys(&rc, card_key) { - Ok(v) => v, + let read_result = card_reader.driver.read_with_mac(&card, &session, FelicaLiteSBlock::PAD0); + let _read_data = match read_result { + Ok(d) => d, Err(e) => { - defmt::error!("card_reader sk error: {}", e); + defmt::error!("card_reader read with mac error: {}", e); continue; } }; - defmt::info!("sk1: {:02X} sk2: {:02X}", sk1, sk2); - - let mac = match card_reader.driver.check_mac( - &response.idm, - &response.pmm, - &sk1, - &sk2, - &rc1 - ) { - Ok(v) => v, - Err(e) => { - defmt::error!("card_driver mac check error: {}", e); - continue; - } - }; - defmt::info!("mac check: {}", mac); match card_reader.driver.release() { Ok(_) => {},