foxmark3 0.1.1

Send/receive Proxmark 3 commands
Documentation
//! Commands and structures for the HID iClass chipset.

use std::time::Duration;

use bitflags::bitflags;
use bytemuck::{Pod, Zeroable, from_bytes};
use tracing::error;

use crate::{
    commands::Error,
    raw::{
        Command, Response, request,
        response::{ResponseNgFrame, errors},
    },
};

use super::Proxmark;

bitflags! {
    #[repr(transparent)]
    #[derive(Copy, Clone, PartialEq, Eq, Pod, Zeroable)]
    struct RequestFlags: u8 {
        const Init = 1 << 0;
        const ClearTrace = 1 << 1;
        // const OnlyOnce = 1 << 2;
        const CreditKey = 1 << 3;
        const AIA = 1 << 4;
        const ShallowMod = 1 << 5;
    }
}

bitflags! {
    #[repr(transparent)]
    #[derive(Copy, Clone, PartialEq, Eq, Pod, Zeroable)]
    struct ResponseFlags: u8 {
        const Null = 1 << 0;
        const CSN = 1 << 1;
        const CC = 1 << 2;
        const Conf = 1 << 3;
        const AIA = 1 << 4;
    }
}

mod fuses {
    #![allow(missing_docs)] // idk what the fuses mean

    bitflags::bitflags! {
        /// Contained within a [`PicopassConfBlock`](super::PicopassConfBlock).
        #[repr(transparent)]
        #[derive(Debug, Copy, Clone, PartialEq, Eq, bytemuck::Pod, bytemuck::Zeroable)]
        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
        pub struct Fuses: u8 {
            const RA = 1 << 0;
            const FProd0 = 1 << 1;
            const FProd1 = 1 << 2;
            const Crypt0 = 1 << 3;
            const Crypt1 = 1 << 4;
            const Coding0 = 1 << 5;
            const Coding1 = 1 << 6;
            const FPERS = 1 << 7;
        }
    }
}

pub use fuses::Fuses;

/// The size of a [`Block`] in bytes.
pub const SZ_BLOCK: usize = 8;

/// A row of associated data within an HID iClass chipset.
#[derive(
    Debug,
    Copy,
    Clone,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    derive_more::From,
    derive_more::Into,
    derive_more::AsRef,
    derive_more::AsMut,
    derive_more::Deref,
    derive_more::DerefMut,
    derive_more::Index,
    derive_more::IndexMut,
    derive_more::IntoIterator,
    Pod,
    Zeroable,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(C)]
pub struct Block([u8; SZ_BLOCK]);

#[repr(C, packed)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct PackedPicopassConfBlock {
    app_limit: u8,
    otp: [u8; 2],
    block_writelock: u8,
    chip_config: u8,
    mem_config: u8,
    eas: u8,
    fuses: Fuses,
}

#[repr(C, packed)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct Header {
    status: ResponseFlags,
    csn: Block,
    conf: PackedPicopassConfBlock,
}

#[repr(C, packed)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct SecureBody {
    epurse: Block,
    key_debit: Block,
    key_credit: Block,
}

#[repr(C, packed)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct Footer {
    app_issuer_area: Block,
}

/// An HID iClass chipset representation.
///
/// [`Proxmark::read_iclass`] requests this data.
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Card {
    /// The chipset's identifier, if available.
    pub csn: Option<Csn>,
    /// The chipset's configuration, if available.
    pub conf: Option<PicopassConfBlock>,
    /// Arbitrary data associated with the chipset, for use by whatever system the chipset is employed
    /// within.
    pub aia: ApplicationIssuerArea,
    /// The card's secure mode information, if accessible.
    pub secure_mode: Option<SecureMode>,
}

/// A **c**ard **s**erial **n**umber.
pub type Csn = Block;

/// A **a**pplication **i**ssuer **a**rea. The use of this field varies depending on the
/// application issuer (smile!)
pub type ApplicationIssuerArea = Block;

/// Data associated with the configuration of an HID iClass chipset.
#[allow(missing_docs)] // idk what these mean LOL
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PicopassConfBlock {
    pub app_limit: u8,
    pub otp: [u8; 2],
    pub block_writelock: u8,
    pub chip_config: u8,
    pub mem_config: u8,
    pub eas: u8,
    pub fuses: Fuses,
}

/// Data associated with secure mode operation of an HID iClass chipset.
#[allow(missing_docs)] // idk what these mean LOL
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SecureMode {
    pub epurse: Block,
    pub key_debit: Block,
    pub key_credit: Block,
}

const SZ_HEADER: usize = size_of::<Header>();
const SZ_SECURE_BODY: usize = size_of::<SecureBody>();
const SZ_FOOTER: usize = size_of::<Footer>();

const SZ_RESPONSE: usize = SZ_HEADER + SZ_SECURE_BODY + SZ_FOOTER;

impl Proxmark {
    /// Reads an HID iClass chipset.
    #[tracing::instrument(skip(self), level = tracing::Level::TRACE)]
    pub fn read_iclass(&mut self) -> Result<Option<Card>, Error> {
        let resp = self.0.request_response(
            request::ng(
                Command::HF_ICLASS_READER,
                [(RequestFlags::Init | RequestFlags::ClearTrace).bits()],
            ),
            Duration::from_secs(2),
        )?;

        if matches!(
            resp,
            Response::Ng(ResponseNgFrame {
                status: errors::ERFTRANS,
                ..
            })
        ) {
            return Ok(None);
        }

        let payload = resp.payload();
        if payload.len() < SZ_RESPONSE {
            error!("payload was too short! {} < {SZ_RESPONSE}", payload.len());

            return Err(Error::MalformedResponse(Box::new(resp)));
        }

        let header: &Header = from_bytes(&payload[..SZ_HEADER]);
        if header.status == ResponseFlags::Null {
            return Ok(None);
        }

        let csn = header
            .status
            .contains(ResponseFlags::CSN)
            .then_some(header.csn);

        let conf = header.status.contains(ResponseFlags::Conf).then_some({
            let c = header.conf;
            PicopassConfBlock {
                app_limit: c.app_limit,
                otp: c.otp,
                block_writelock: c.block_writelock,
                chip_config: c.chip_config,
                mem_config: c.mem_config,
                eas: c.eas,
                fuses: c.fuses,
            }
        });

        let pagemap = (header.conf.fuses & (Fuses::Crypt0 | Fuses::Crypt1)).bits() >> 3;

        // 0x01: NON_SECURE
        let (aia, secure_mode) = if pagemap == 0x01 {
            let footer: &Footer = from_bytes(&payload[SZ_HEADER..][..SZ_FOOTER]);

            (footer.app_issuer_area, None)
        } else {
            let body: &SecureBody = from_bytes(&payload[SZ_HEADER..][..SZ_SECURE_BODY]);
            let footer: &Footer = from_bytes(&payload[SZ_HEADER + SZ_SECURE_BODY..][..SZ_FOOTER]);

            (
                footer.app_issuer_area,
                Some({
                    let b = body;
                    SecureMode {
                        epurse: b.epurse,
                        key_debit: b.key_debit,
                        key_credit: b.key_credit,
                    }
                }),
            )
        };

        Ok(Some(Card {
            csn,
            conf,
            aia,
            secure_mode,
        }))
    }
}

#[cfg(feature = "sqlx")]
mod sqlx {
    use sqlx::{Database, Decode, Encode, Type, encode::IsNull, error::BoxDynError};

    use super::{Block, SZ_BLOCK};

    impl<DB> Type<DB> for Block
    where
        DB: Database,
        [u8]: Type<DB>,
    {
        fn type_info() -> <DB as Database>::TypeInfo {
            <[u8]>::type_info()
        }
    }

    impl<'r, DB> Decode<'r, DB> for Block
    where
        DB: Database,
        &'r [u8]: Decode<'r, DB>,
    {
        fn decode(value: <DB as sqlx::Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
            let slice = <&[u8]>::decode(value)?;
            let array = <[u8; SZ_BLOCK]>::try_from(slice).map_err(|_| {
                format!(
                    "blob had unexpected length for iClass block: {} bytes",
                    slice.len()
                )
            })?;

            Ok(Block::from(array))
        }
    }

    impl<'q, DB> Encode<'q, DB> for Block
    where
        DB: Database,
        Vec<u8>: Encode<'q, DB>,
    {
        fn encode(
            self,
            buf: &mut <DB as Database>::ArgumentBuffer<'q>,
        ) -> Result<IsNull, BoxDynError> {
            self.as_slice().to_vec().encode(buf)
        }

        fn encode_by_ref(
            &self,
            buf: &mut <DB as Database>::ArgumentBuffer<'q>,
        ) -> Result<IsNull, BoxDynError> {
            (*self).encode(buf)
        }

        fn size_hint(&self) -> usize {
            self.as_slice().len()
        }
    }
}