use core::ops::Range;
use arrayvec::ArrayVec;
use thiserror::Error;
use crate::types::file_settings::{EncryptedContent, Offset, PiccData, ReadCtrMirror, Sdm};
use crate::types::{FileSettingsError, 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)]
#[non_exhaustive]
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 mac_range(&self) -> Range<u32> {
self.mac_input_offset..self.mac_offset
}
pub fn validate(&self) -> Result<(), FileSettingsError> {
use crate::types::file_settings::{
CtrRetAccess, EncFileData, EncLength, FileRead, MacWindow, PiccData, PlainMirror,
ReadCtrFeatures,
};
if !matches!(self.picc_source, PiccSource::Encrypted { .. }) && self.meta_read_key.is_some()
{
return Err(FileSettingsError::InvalidSdmFlags);
}
let picc_data = match self.picc_source {
PiccSource::Encrypted { offset } => PiccData::Encrypted {
offset: Offset::new(offset)?,
key: self
.meta_read_key
.ok_or(FileSettingsError::InvalidSdmFlags)?,
content: EncryptedContent::Both(ReadCtrFeatures {
limit: None,
ret_access: CtrRetAccess::Free,
}),
},
PiccSource::Plain {
uid_offset: Some(off),
read_ctr_offset: None,
} => PiccData::Plain(PlainMirror::Uid {
uid: Offset::new(off)?,
}),
PiccSource::Plain {
uid_offset: None,
read_ctr_offset: Some(off),
} => PiccData::Plain(PlainMirror::RCtr {
read_ctr: ReadCtrMirror {
offset: Offset::new(off)?,
features: ReadCtrFeatures {
limit: None,
ret_access: CtrRetAccess::Free,
},
},
}),
PiccSource::Plain {
uid_offset: Some(uid_off),
read_ctr_offset: Some(rctr_off),
} => PiccData::Plain(PlainMirror::Both {
uid: Offset::new(uid_off)?,
read_ctr: ReadCtrMirror {
offset: Offset::new(rctr_off)?,
features: ReadCtrFeatures {
limit: None,
ret_access: CtrRetAccess::Free,
},
},
}),
PiccSource::Plain {
uid_offset: None,
read_ctr_offset: None,
} => PiccData::None,
PiccSource::None => PiccData::None,
};
let mac_window = MacWindow {
input: Offset::new(self.mac_input_offset)?,
mac: Offset::new(self.mac_offset)?,
};
let file_read = match &self.enc_data {
Some(range) => FileRead::MacAndEnc {
key: self.file_read_key,
window: mac_window,
enc: EncFileData {
start: Offset::new(range.start)?,
length: EncLength::new(
range
.end
.checked_sub(range.start)
.ok_or(FileSettingsError::EncLengthInvalid(0))?,
)?,
},
},
None => FileRead::MacOnly {
key: self.file_read_key,
window: mac_window,
},
};
let tamper_status = self.tamper_status_offset.map(Offset::new).transpose()?;
Sdm::try_new(picc_data, Some(file_read), tamper_status, self.mode)?;
Ok(())
}
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 start(&self) -> u32 {
let picc_offset = match self.picc_source {
PiccSource::Encrypted { offset } => offset,
PiccSource::Plain {
uid_offset,
read_ctr_offset,
} => uid_offset
.unwrap_or(u32::MAX)
.min(read_ctr_offset.unwrap_or(u32::MAX)),
PiccSource::None => u32::MAX,
};
picc_offset
.min(self.mac_input_offset)
.min(self.mac_offset)
.min(self.tamper_status_offset.unwrap_or(u32::MAX))
.min(self.enc_data.as_ref().map(|r| r.start).unwrap_or(u32::MAX))
}
pub fn decrypt_picc_data(
&self,
meta_key: &[u8; 16],
ndef_data: &[u8],
) -> 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()).ok_or(
SdmError::InvalidConfiguration(
"AES decryption failed: cipher buffer is not block-aligned",
),
)?;
}
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],
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::file_settings::{
CtrRetAccess, EncFileData, EncLength, EncryptedContent, FileRead, MacWindow, PlainMirror,
ReadCtrFeatures,
};
#[test]
fn validate_accepts_encrypted_picc_with_enc_data() {
let settings = Sdm::try_new(
PiccData::Encrypted {
key: KeyNumber::Key1,
offset: Offset::new(0).unwrap(),
content: EncryptedContent::Both(ReadCtrFeatures {
limit: None,
ret_access: CtrRetAccess::NoAccess,
}),
},
Some(FileRead::MacAndEnc {
key: KeyNumber::Key2,
window: MacWindow {
input: Offset::new(32).unwrap(),
mac: Offset::new(64).unwrap(),
},
enc: EncFileData {
start: Offset::new(32).unwrap(),
length: EncLength::new(32).unwrap(),
},
}),
None,
CryptoMode::Aes,
)
.unwrap();
let verifier = Verifier::try_new(&settings, CryptoMode::Aes).unwrap();
assert_eq!(verifier.validate(), Ok(()));
}
#[test]
fn validate_accepts_plain_uid_and_read_ctr() {
let settings = Sdm::try_new(
PiccData::Plain(PlainMirror::Both {
uid: Offset::new(0).unwrap(),
read_ctr: ReadCtrMirror {
offset: Offset::new(14).unwrap(),
features: ReadCtrFeatures {
limit: None,
ret_access: CtrRetAccess::NoAccess,
},
},
}),
Some(FileRead::MacOnly {
key: KeyNumber::Key2,
window: MacWindow {
input: Offset::new(0).unwrap(),
mac: Offset::new(20).unwrap(),
},
}),
None,
CryptoMode::Aes,
)
.unwrap();
let verifier = Verifier::try_new(&settings, CryptoMode::Aes).unwrap();
assert_eq!(verifier.validate(), Ok(()));
}
#[test]
fn validate_rejects_encrypted_picc_without_meta_key() {
let verifier = Verifier {
mode: CryptoMode::Aes,
picc_source: PiccSource::Encrypted { offset: 0 },
meta_read_key: None,
file_read_key: KeyNumber::Key0,
mac_input_offset: 32,
mac_offset: 32,
tamper_status_offset: None,
enc_data: None,
};
assert_eq!(verifier.validate(), Err(FileSettingsError::InvalidSdmFlags));
}
#[test]
fn validate_rejects_meta_key_without_encrypted_picc() {
let verifier = Verifier {
mode: CryptoMode::Aes,
picc_source: PiccSource::None,
meta_read_key: Some(KeyNumber::Key0),
file_read_key: KeyNumber::Key0,
mac_input_offset: 0,
mac_offset: 0,
tamper_status_offset: None,
enc_data: None,
};
assert_eq!(verifier.validate(), Err(FileSettingsError::InvalidSdmFlags));
}
#[test]
fn validate_rejects_inverted_enc_data_range_without_panicking() {
let verifier = Verifier {
mode: CryptoMode::Aes,
picc_source: PiccSource::Encrypted { offset: 0 },
meta_read_key: Some(KeyNumber::Key0),
file_read_key: KeyNumber::Key0,
mac_input_offset: 32,
mac_offset: 96,
tamper_status_offset: None,
#[allow(clippy::reversed_empty_ranges)]
enc_data: Some(80..48),
};
assert_eq!(
verifier.validate(),
Err(FileSettingsError::EncLengthInvalid(0))
);
}
#[test]
fn validate_rejects_mac_input_after_mac() {
let verifier = Verifier {
mode: CryptoMode::Aes,
picc_source: PiccSource::None,
meta_read_key: None,
file_read_key: KeyNumber::Key0,
mac_input_offset: 1,
mac_offset: 0,
tamper_status_offset: None,
enc_data: None,
};
assert_eq!(
verifier.validate(),
Err(FileSettingsError::MacInputAfterMac)
);
}
fn base_verifier() -> Verifier {
Verifier {
mode: CryptoMode::Aes,
picc_source: PiccSource::None,
meta_read_key: None,
file_read_key: KeyNumber::Key0,
mac_input_offset: 50,
mac_offset: 80,
tamper_status_offset: None,
enc_data: None,
}
}
#[test]
fn start_encrypted_picc_is_earliest() {
let v = Verifier {
picc_source: PiccSource::Encrypted { offset: 10 },
meta_read_key: Some(KeyNumber::Key1),
mac_input_offset: 50,
mac_offset: 80,
..base_verifier()
};
assert_eq!(v.start(), 10);
}
#[test]
fn start_mac_input_is_earliest() {
let v = Verifier {
picc_source: PiccSource::Encrypted { offset: 20 },
meta_read_key: Some(KeyNumber::Key1),
mac_input_offset: 15,
mac_offset: 80,
..base_verifier()
};
assert_eq!(v.start(), 15);
}
#[test]
fn start_mac_offset_is_earliest() {
let v = Verifier {
mac_input_offset: 50,
mac_offset: 5,
..base_verifier()
};
assert_eq!(v.start(), 5);
}
#[test]
fn start_tamper_status_is_earliest() {
let v = Verifier {
mac_input_offset: 50,
mac_offset: 80,
tamper_status_offset: Some(3),
..base_verifier()
};
assert_eq!(v.start(), 3);
}
#[test]
fn start_enc_data_is_earliest() {
let v = Verifier {
mac_input_offset: 50,
mac_offset: 80,
enc_data: Some(8..40),
..base_verifier()
};
assert_eq!(v.start(), 8);
}
#[test]
fn start_plain_uid_offset_is_earliest() {
let v = Verifier {
picc_source: PiccSource::Plain {
uid_offset: Some(5),
read_ctr_offset: Some(20),
},
mac_input_offset: 50,
mac_offset: 80,
..base_verifier()
};
assert_eq!(v.start(), 5);
}
#[test]
fn start_plain_read_ctr_offset_is_earliest() {
let v = Verifier {
picc_source: PiccSource::Plain {
uid_offset: Some(30),
read_ctr_offset: Some(7),
},
mac_input_offset: 50,
mac_offset: 80,
..base_verifier()
};
assert_eq!(v.start(), 7);
}
#[test]
fn start_plain_only_uid_offset_present() {
let v = Verifier {
picc_source: PiccSource::Plain {
uid_offset: Some(12),
read_ctr_offset: None,
},
mac_input_offset: 50,
mac_offset: 80,
..base_verifier()
};
assert_eq!(v.start(), 12);
}
#[test]
fn start_picc_none_falls_back_to_mac_input() {
let v = base_verifier();
assert_eq!(v.start(), 50);
}
#[test]
fn start_plain_both_none_falls_back_to_mac_input() {
let v = Verifier {
picc_source: PiccSource::Plain {
uid_offset: None,
read_ctr_offset: None,
},
mac_input_offset: 50,
mac_offset: 80,
..base_verifier()
};
assert_eq!(v.start(), 50);
}
}