use core::ops::Range;
use arrayvec::ArrayVec;
use thiserror::Error;
use crate::types::file_settings::{Offset, PiccData, Sdm};
use crate::types::{KeyNumber, TagTamperStatusReadout};
use super::hex::{decode_hex_array, decode_hex_into, ensure_len};
use super::keys::{SdmKeys, aes_ecb_encrypt_block, derive_sdm_keys_aes, derive_sdm_keys_lrp};
use super::picc::{decrypt_picc_data_aes, decrypt_picc_data_lrp};
use crate::crypto::suite::aes_cbc_decrypt;
use crate::types::file_settings::CryptoMode;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum SdmError {
#[error("MAC verification failed")]
MacMismatch,
#[error("NDEF data too short: need {needed} bytes, have {have}")]
NdefTooShort { needed: usize, have: usize },
#[error("invalid hex character at byte offset {offset}")]
InvalidHex { offset: usize },
#[error("invalid tag identity data tag byte: {0:#04x}")]
InvalidPiccDataTag(u8),
#[error("SDM configuration invalid: {0}")]
InvalidConfiguration(&'static str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SdmVerification {
pub uid: Option<[u8; 7]>,
pub read_ctr: Option<u32>,
pub tamper_status: Option<TagTamperStatusReadout>,
#[cfg(feature = "alloc")]
pub enc_file_data: Option<alloc::vec::Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
enum PiccSource {
Encrypted { offset: u32 },
Plain {
uid_offset: Option<u32>,
read_ctr_offset: Option<u32>,
},
None,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Verifier {
mode: CryptoMode,
picc_source: PiccSource,
meta_read_key: Option<KeyNumber>,
file_read_key: KeyNumber,
mac_input_offset: u32,
mac_offset: u32,
tamper_status_offset: Option<u32>,
enc_data: Option<Range<u32>>,
}
const MAX_SDM_ENC_FILE_DATA_BYTES: usize = 256;
impl Verifier {
pub fn try_new(settings: &Sdm, mode: CryptoMode) -> Result<Self, SdmError> {
let file_read = settings.file_read().ok_or(SdmError::InvalidConfiguration(
"SDM read access is missing - no MAC key configured",
))?;
let file_read_key = file_read.key();
let (picc_source, meta_read_key) = match settings.picc_data() {
PiccData::Encrypted { key, offset, .. } => (
PiccSource::Encrypted {
offset: offset.get(),
},
Some(key),
),
PiccData::Plain(plain) => (
PiccSource::Plain {
uid_offset: plain.uid_offset().map(Offset::get),
read_ctr_offset: plain.rctr_offset().map(Offset::get),
},
None,
),
PiccData::None => (PiccSource::None, None),
};
let mac_input_offset = file_read.window().input.get();
let mac_offset = file_read.window().mac.get();
let enc_data = file_read.enc().map(|enc| {
let start = enc.start.get();
let end = start + enc.length.get();
start..end
});
let tamper_status_offset = settings.tamper_status().map(Offset::get);
Ok(Self {
mode,
picc_source,
meta_read_key,
file_read_key,
mac_input_offset,
mac_offset,
tamper_status_offset,
enc_data,
})
}
pub fn with_offset(&self, inc: i32) -> Option<Self> {
fn add(a: u32, b: i32) -> Option<u32> {
if b < 0 {
a.checked_sub(b.checked_neg()? as u32)
} else {
a.checked_add(b as u32)
}
.filter(|&r| r <= 255)
}
let mut new = self.clone();
match new.picc_source {
PiccSource::Encrypted {
offset: ref mut picc_off,
} => {
*picc_off = add(*picc_off, inc)?;
}
PiccSource::Plain {
ref mut uid_offset,
ref mut read_ctr_offset,
} => {
if let Some(uid_off) = uid_offset {
*uid_off = add(*uid_off, inc)?;
}
if let Some(rctr_off) = read_ctr_offset {
*rctr_off = add(*rctr_off, inc)?;
}
}
PiccSource::None => {}
}
new.mac_input_offset = add(new.mac_input_offset, inc)?;
if let Some(tt) = new.tamper_status_offset {
new.tamper_status_offset = Some(add(tt, inc)?);
}
new.mac_offset = add(new.mac_offset, inc)?;
if let Some(range) = new.enc_data {
let start = add(range.start, inc)?;
let end = add(range.end, inc)?;
new.enc_data = Some(start..end);
}
Some(new)
}
pub fn decrypt_picc_data(
&self,
ndef_data: &[u8],
meta_key: &[u8; 16],
) -> Result<(Option<[u8; 7]>, Option<u32>), SdmError> {
let (uid, ctr_bytes) = self.extract_picc_data(ndef_data, meta_key)?;
Ok((
uid,
ctr_bytes.map(|c| u32::from_le_bytes([c[0], c[1], c[2], 0])),
))
}
pub fn mode(&self) -> CryptoMode {
self.mode
}
pub fn file_read_key(&self) -> KeyNumber {
self.file_read_key
}
pub fn meta_read_key(&self) -> Option<KeyNumber> {
self.meta_read_key
}
pub fn verify(&self, ndef_data: &[u8], key: &[u8; 16]) -> Result<SdmVerification, SdmError> {
self.verify_inner(ndef_data, key, key)
}
pub fn verify_with_meta_key(
&self,
ndef_data: &[u8],
sdm_file_read_key: &[u8; 16],
sdm_meta_read_key: &[u8; 16],
) -> Result<SdmVerification, SdmError> {
self.verify_inner(ndef_data, sdm_file_read_key, sdm_meta_read_key)
}
fn verify_inner(
&self,
ndef_data: &[u8],
sdm_file_read_key: &[u8; 16],
sdm_meta_read_key: &[u8; 16],
) -> Result<SdmVerification, SdmError> {
let (uid, read_ctr_bytes) = self.extract_picc_data(ndef_data, sdm_meta_read_key)?;
let keys = match self.mode {
CryptoMode::Aes => {
derive_sdm_keys_aes(sdm_file_read_key, uid.as_ref(), read_ctr_bytes.as_ref())
}
CryptoMode::Lrp => {
derive_sdm_keys_lrp(sdm_file_read_key, uid.as_ref(), read_ctr_bytes.as_ref())
}
};
let mac_input_off = self.mac_input_offset as usize;
let mac_off = self.mac_offset as usize;
ensure_len(ndef_data, mac_off + 16)?;
let mac_input = &ndef_data[mac_input_off..mac_off];
let expected_mac = decode_hex_array::<8>(ndef_data, mac_off)?;
if !keys.verify_mac(mac_input, &expected_mac) {
return Err(SdmError::MacMismatch);
}
let enc_file_data =
self.decrypt_enc_file_data(ndef_data, &keys, read_ctr_bytes.as_ref())?;
let tamper_status = self.extract_tamper_status(
ndef_data,
enc_file_data.as_ref().map(|data| data.as_slice()),
)?;
Ok(SdmVerification {
uid,
read_ctr: read_ctr_bytes.map(|c| u32::from_le_bytes([c[0], c[1], c[2], 0])),
tamper_status,
#[cfg(feature = "alloc")]
enc_file_data: enc_file_data.map(|data| data.into_iter().collect()),
})
}
#[allow(clippy::type_complexity)]
fn extract_picc_data(
&self,
ndef_data: &[u8],
meta_key: &[u8; 16],
) -> Result<(Option<[u8; 7]>, Option<[u8; 3]>), SdmError> {
match &self.picc_source {
PiccSource::Encrypted { offset } => {
let offset = *offset as usize;
ensure_len(ndef_data, offset + self.mode.picc_blob_ascii_len() as usize)?;
match self.mode {
CryptoMode::Aes => {
let enc = decode_hex_array::<16>(ndef_data, offset)?;
let picc = decrypt_picc_data_aes(meta_key, &enc)?;
Ok((picc.uid, picc.read_ctr))
}
CryptoMode::Lrp => {
let wire = decode_hex_array::<24>(ndef_data, offset)?;
let picc = decrypt_picc_data_lrp(meta_key, &wire)?;
Ok((picc.uid, picc.read_ctr))
}
}
}
PiccSource::Plain {
uid_offset,
read_ctr_offset,
} => {
let uid = if let Some(offset) = uid_offset {
let offset = *offset as usize;
ensure_len(ndef_data, offset + 14)?;
Some(decode_hex_array::<7>(ndef_data, offset)?)
} else {
None
};
let read_ctr = if let Some(offset) = read_ctr_offset {
let offset = *offset as usize;
ensure_len(ndef_data, offset + 6)?;
let mut ctr = decode_hex_array::<3>(ndef_data, offset)?;
ctr.reverse();
Some(ctr)
} else {
None
};
Ok((uid, read_ctr))
}
PiccSource::None => Ok((None, None)),
}
}
fn decrypt_enc_file_data(
&self,
ndef_data: &[u8],
keys: &SdmKeys,
read_ctr: Option<&[u8; 3]>,
) -> Result<Option<ArrayVec<u8, MAX_SDM_ENC_FILE_DATA_BYTES>>, SdmError> {
let range = match &self.enc_data {
Some(r) => r,
None => return Ok(None),
};
let start = range.start as usize;
let ascii_len = (range.end - range.start) as usize;
ensure_len(ndef_data, start + ascii_len)?;
let binary_len = ascii_len / 2;
let mut ct = ArrayVec::<u8, MAX_SDM_ENC_FILE_DATA_BYTES>::new();
for _ in 0..binary_len {
ct.try_push(0).map_err(|_| {
SdmError::InvalidConfiguration(
"enc_data decrypted length exceeds verifier buffer limit",
)
})?;
}
decode_hex_into(ct.as_mut_slice(), ndef_data, start)?;
let ctr = read_ctr.copied().unwrap_or([0; 3]);
match keys {
SdmKeys::Aes { enc_key, .. } => {
let mut iv_input = [0u8; 16];
iv_input[..3].copy_from_slice(&ctr);
let iv = aes_ecb_encrypt_block(enc_key, &iv_input);
aes_cbc_decrypt(enc_key, &iv, ct.as_mut_slice());
}
SdmKeys::Lrp { enc, .. } => {
let mut counter = [0u8; 6];
counter[..3].copy_from_slice(&ctr);
enc.lricb_decrypt_in_place(&mut counter, ct.as_mut_slice())
.ok_or(SdmError::InvalidConfiguration(
"LRICB decryption failed: invalid buffer length",
))?;
}
}
Ok(Some(ct))
}
fn extract_tamper_status(
&self,
ndef_data: &[u8],
enc_file_data: Option<&[u8]>,
) -> Result<Option<TagTamperStatusReadout>, SdmError> {
let Some(offset) = self.tamper_status_offset.map(|offset| offset as usize) else {
return Ok(None);
};
if let Some(range) = &self.enc_data {
let start = range.start as usize;
let end = range.end as usize;
if offset >= start && offset < end {
let relative_plain = offset - start;
let plain = enc_file_data.ok_or(SdmError::InvalidConfiguration(
"tamper_status inside enc_data requires decrypted file data",
))?;
if relative_plain + 2 > plain.len() {
return Err(SdmError::InvalidConfiguration(
"tamper_status offset exceeds enc_data bounds",
));
}
return Ok(Some(TagTamperStatusReadout::new(
plain[relative_plain],
plain[relative_plain + 1],
)));
}
}
ensure_len(ndef_data, offset + 2)?;
Ok(Some(TagTamperStatusReadout::new(
ndef_data[offset],
ndef_data[offset + 1],
)))
}
}