yb-core 0.7.1

Secure blob storage on a YubiKey — core library
// SPDX-FileCopyrightText: 2025 - 2026 Frederic Ruget <fred@atlant.is> (GitHub: @douzebis)
//
// SPDX-License-Identifier: MIT

//! Card info queries: serial number, firmware version.

use super::transport::{connect_reader_mode, RESET_RETRIES, SELECT_PIV};
use anyhow::{bail, Result};

fn query_card_apdu(reader: &str, query: &[u8]) -> Result<Vec<u8>> {
    let ctx = pcsc::Context::establish(pcsc::Scope::User)?;
    let mut buf = vec![0u8; 258];
    // As PcscSession::open: a reset by another program just as this
    // connection takes the lock fails the first command; connect again.
    let mut attempts = 0;
    let card = loop {
        let card = connect_reader_mode(&ctx, reader, pcsc::ShareMode::Shared)?;
        match card.transmit(SELECT_PIV, &mut buf) {
            Ok(_) => break card,
            Err(pcsc::Error::ResetCard | pcsc::Error::ProtoMismatch)
                if attempts < RESET_RETRIES =>
            {
                attempts += 1;
            }
            Err(e) => return Err(e.into()),
        }
    };
    let resp = card.transmit(query, &mut buf)?.to_vec();
    // Read-only: leave the card as it is (spec 0048), rather than reset it.
    let _ = card.disconnect(pcsc::Disposition::LeaveCard);
    Ok(resp)
}

pub(crate) fn serial_from_reader(reader: &str) -> Result<u32> {
    let resp = query_card_apdu(reader, &[0x00, 0xF8, 0x00, 0x00, 0x00])?;
    if resp.len() < 4 {
        bail!("GET_SERIAL response too short");
    }
    Ok(u32::from_be_bytes(resp[0..4].try_into().unwrap()))
}

pub(crate) fn version_from_reader(reader: &str) -> Option<String> {
    let resp = query_card_apdu(reader, &[0x00, 0xFD, 0x00, 0x00, 0x00]).ok()?;
    if resp.len() < 3 {
        return None;
    }
    Some(format!("{}.{}.{}", resp[0], resp[1], resp[2]))
}