foxmark3 0.1.0

Send/receive Proxmark 3 commands
Documentation
//! Responses which may be received using a raw [`Proxmark`](crate::raw::Proxmark). This includes
//! [`ResponseOld`], [`ResponseMix`], and [`ResponseNg`]. Further, the responses are generalized
//! using the enum [`Response`].

use std::{io, time::Duration};

use bytemuck::{Pod, Zeroable, bytes_of_mut, from_bytes};
use tracing::{Level, error, span, trace};

use crate::util::BytesExt;

use super::{
    NgPayload,
    common::{self, Command, MixShim, NgShim, SZ_ARGS, SZ_DATA},
};

/// An **OLD** frame, sent from device to client.
pub type ResponseOld = common::FrameOld;

/// A **MIX** frame, sent from device to client.
pub type ResponseMix = ResponseNgFrame<MixShim>;

/// An **NG** frame, sent from device to client.
pub type ResponseNg = ResponseNgFrame<NgShim>;

/// An **NG** frame parameterized over a generic shim, sent from device to client.
#[derive(Debug, Copy, Clone)]
pub struct ResponseNgFrame<D> {
    /// The command code of the frame
    pub cmd: Command,
    /// The "status" of the response. The use of this field varies from command to command.
    pub status: i8,
    /// The "reason" of the response. The use of this field varies from command to command.
    pub reason: i8,
    /// The shim which parameterizes this generic frame over **MIX** and **NG** responses.
    pub shim: D,
}

pub(super) const SZ_BUF: usize = if SZ_OLD_BUF > SZ_NG_BUF {
    SZ_OLD_BUF
} else {
    SZ_NG_BUF
};

/// A response, sent from device to client.
#[derive(Debug, Clone)]
pub enum Response {
    /// A [`ResponseOld`]
    Old(ResponseOld),
    /// A [`ResponseMix`]
    Mix(ResponseMix),
    /// A [`ResponseNg`]
    Ng(ResponseNg),
}

impl Response {
    /// The command code of the response.
    #[must_use]
    pub fn cmd(&self) -> Command {
        match self {
            Response::Old(frame_old) => frame_old.cmd,
            Response::Mix(frame_mix) => frame_mix.cmd,
            Response::Ng(frame_ng) => frame_ng.cmd,
        }
    }

    /// The data bytes of the response.
    #[must_use]
    pub fn payload(&self) -> &[u8] {
        match self {
            Response::Old(frame_old) => &frame_old.payload,
            Response::Mix(frame_mix) => frame_mix.shim.payload(),
            Response::Ng(frame_ng) => frame_ng.shim.payload(),
        }
    }
}

/// An error caused by receiving a [`response`](self).
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// The error came from reading the serial port.
    #[error("IO error")]
    Io(#[from] std::io::Error),
    /// The error came from configuring the serial port.
    #[error("serialport error")]
    Serialport(#[from] serialport::Error),
    /// A **MIX/NG** frame was received, but the NG payload was too large.
    #[error("payload was too large: {0} bytes. Max: {SZ_DATA}")]
    PayloadTooLarge(usize),
    /// A **MIX** frame was received, but the NG payload was too small to support a MIX shim.
    #[error("payload was too small for MIX frame: {0} bytes. Min: {SZ_ARGS}")]
    MixPayloadTooSmall(usize),
    /// An **NG** frame was received, but its CRC checksum was invalid, indicating the data is
    /// possibly corrupt.
    #[error(
        "invalid CRC checksum. Actual: 0x{actual:02X}, expected: 0x{expected:02X} (or fallback: 0x{NG_RESPONSE_CRC_FALLBACK:02X})"
    )]
    InvalidCrc {
        /// The CRC expected of the frame.
        expected: u16,
        /// The CRC provided by the frame.
        actual: u16,
    },
    /// An **OLD** frame was received, but its [`u64`] command code was outside the bounds
    /// of the expected [`u16`] command codes.
    #[error("an OLD frame contained a u64 command outside the bounds supported by u16: {0}")]
    CommandOutOfBounds(u64),
    /// While awaiting a response, the timeout duration had elapsed.
    #[error("response was not received before timeout ({}+{} ms)", original.as_millis(), extensions.as_millis())]
    Timeout {
        /// The original timeout duration provided.
        original: Duration,
        /// The duration by which the timeout was extended, through the use of **WTX** packets.
        extensions: Duration,
    },
}

pub(super) fn read_from(mut r: impl io::Read, msg_b: &mut [u8; SZ_BUF]) -> Result<Response, Error> {
    // This function mirrors the logic from [client/src/comms.c:488-639] within `uart_communication`.

    r.read_exact(&mut msg_b[..SZ_NG_HEADER])?;
    let head: &NgHeader = from_bytes(&msg_b[..SZ_NG_HEADER]);

    // Read header, now what type of packet is it?
    let response = if head.magic == NG_RESPONSE_MAGIC {
        // The packet type is:
        let _span = span!(Level::TRACE, "NG/MIX packet");

        let length_ng = common::NgLength::from_bits(head.length_ng);

        let payload_len = length_ng.length() as usize;
        if payload_len > SZ_DATA {
            return Err(Error::PayloadTooLarge(payload_len));
        }

        let checksum_len = SZ_NG_HEADER + payload_len;
        let frame_len = checksum_len + SZ_NG_FOOTER;

        r.read_exact(&mut msg_b[SZ_NG_HEADER..frame_len])?;
        let foot: &NgFooter = from_bytes(&msg_b[checksum_len..frame_len]);

        // Let's compute the CRC now
        let crc_expected = common::CRC.checksum(&msg_b[..checksum_len]);
        let crc_got = foot.crc; // alignment
        if crc_got != crc_expected && crc_got != NG_RESPONSE_CRC_FALLBACK {
            error!(
                "bad packet CRC! expected: 0x{crc_expected:04X} or fallback 0x{NG_RESPONSE_CRC_FALLBACK:04X}, got: 0x{crc_got:04X}",
            );

            return Err(Error::InvalidCrc {
                expected: crc_expected,
                actual: foot.crc,
            });
        }

        // Need to reborrow here, the buffer has been mutated since
        let head: &NgHeader = from_bytes(&msg_b[..SZ_NG_HEADER]);
        if length_ng.ng() {
            // NG frame

            let frame = ResponseNgFrame {
                cmd: head.cmd,
                status: head.status,
                reason: head.reason,
                shim: NgShim {
                    len: payload_len,
                    buf: msg_b[SZ_NG_HEADER..checksum_len].to_buffer(),
                },
            };

            Response::Ng(frame)
        } else {
            // MIX frame

            if payload_len < SZ_ARGS {
                return Err(Error::MixPayloadTooSmall(payload_len));
            }

            let mut args = [0u64; 3];
            bytes_of_mut(&mut args).copy_from_slice(&msg_b[SZ_NG_HEADER..][..SZ_ARGS]);

            let frame = ResponseNgFrame {
                cmd: head.cmd,
                status: head.status,
                reason: head.reason,
                shim: MixShim {
                    args,
                    len: checksum_len - (SZ_NG_HEADER + SZ_ARGS),
                    buf: msg_b[SZ_NG_HEADER + SZ_ARGS..checksum_len].to_buffer(),
                },
            };

            Response::Mix(frame)
        }
    } else {
        const SZ_FRAME_OLD: usize = size_of::<FrameOldPacked>();

        // Packet type is:
        let _span = span!(Level::TRACE, "OLD packet");

        trace!("reading packet remainder...");
        r.read_exact(&mut msg_b[SZ_NG_HEADER..SZ_FRAME_OLD])?;
        let old: &FrameOldPacked = from_bytes(&msg_b[..SZ_FRAME_OLD]);

        let cmd = u16::try_from(old.cmd)
            .map(Command::from)
            .map_err(|_| Error::CommandOutOfBounds(old.cmd))?;

        let frame = common::FrameOld {
            cmd,
            args: old.arg,
            payload: old.data.to_buffer(),
        };

        Response::Old(frame)
    };

    Ok(response)
}

// [include/pm3_cmd.h:93] #define RESPONSENG_PREAMBLE_MAGIC  0x62334d50 // PM3b
const NG_RESPONSE_MAGIC: u32 = 0x62_33_4d_50;
// [include/pm3_cmd.h:94] #define RESPONSENG_POSTAMBLE_MAGIC 0x3362     // b3
const NG_RESPONSE_CRC_FALLBACK: u16 = 0x33_62;

const SZ_NG_HEADER: usize = size_of::<NgHeader>();
const SZ_NG_FOOTER: usize = size_of::<NgFooter>();
const SZ_NG_BUF: usize = SZ_NG_HEADER + SZ_DATA + SZ_NG_FOOTER;

const SZ_OLD_BUF: usize = size_of::<FrameOldPacked>();

// [pm3_cmd.h]
crate::mod_const! {
    pub errors: i8, "Common error codes";

    SUCCESS = 0,
    ERFTRANS = -7,
}

// [include/pm3_cmd.h:84-91] typedef struct { ... } PACKED PacketResponseNGPreamble
#[derive(Debug, Copy, Clone, Zeroable, Pod)]
#[repr(C, packed)]
struct NgHeader {
    magic: u32,
    length_ng: u16,
    status: i8,
    reason: i8,
    cmd: Command,
}

// [include/pm3_cmd.h:96-98] typedef struct { ... } PACKED PacketResponseNGPostamble
#[derive(Debug, Copy, Clone, Zeroable, Pod)]
#[repr(C, packed)]
struct NgFooter {
    crc: u16,
}

// [include/pm3_cmd.h:75-82] typedef struct { ... } PACKED PacketResponseOLD
#[derive(Debug, Copy, Clone, Zeroable, Pod)]
#[repr(C, packed)]
struct FrameOldPacked {
    cmd: u64,
    arg: [u64; 3],
    data: [u8; SZ_DATA],
}