openpgp-card-tools 0.11.11

A tool for inspecting, configuring and using OpenPGP cards
// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
// SPDX-FileCopyrightText: 2023 David Runge <dave@sleepmap.de>
// SPDX-License-Identifier: MIT OR Apache-2.0

use anyhow::{anyhow, Result};
use card_backend_pcsc::PcscBackend;
use clap::{Parser, ValueEnum};
use openpgp_card::ocard::data::KdfDo;
use openpgp_card::Error;

use crate::util;

#[derive(Parser, Debug)]
pub struct SystemCommand {
    #[command(subcommand)]
    pub cmd: SystemSubCommand,
}

#[derive(Parser, Debug)]
pub enum SystemSubCommand {
    /// Completely reset a card (deletes all data including keys!)
    FactoryReset {
        #[arg(
            name = "card ident",
            short = 'c',
            long = "card",
            help = "Identifier of the card to use"
        )]
        ident: String,
    },

    /// Set up KDF mode
    KdfSetup {
        #[arg(
            name = "card ident",
            short = 'c',
            long = "card",
            help = "Identifier of the card to use"
        )]
        ident: String,
    },

    /// Change identity (Nitrokey Start only)
    ///
    /// A Nitrokey Start device contains three distinct virtual OpenPGP cards, select the identity
    /// of the virtual card to activate.
    SetIdentity {
        #[arg(
            name = "card ident",
            short = 'c',
            long = "card",
            help = "Identifier of the card to use"
        )]
        ident: String,

        /// Identity of the virtual card to activate
        #[arg(name = "identity", value_enum)]
        id: SetIdentityId,
    },

    /// Activate File (recover from "card terminated" state)
    ///
    /// If a card is in "Terminated" state, this command activates it again.
    /// Command can only be performed if only one card is connected to the
    /// system, and that card is in the terminated state.
    ///
    /// Otherwise an error message is printed.
    ActivateFile {},
}

pub fn system(command: SystemCommand) -> Result<(), Box<dyn std::error::Error>> {
    match command.cmd {
        SystemSubCommand::FactoryReset { ident } => factory_reset(&ident)?,
        SystemSubCommand::KdfSetup { ident } => kdf_setup(&ident)?,
        SystemSubCommand::SetIdentity { ident, id } => set_identity(&ident, id)?,
        SystemSubCommand::ActivateFile {} => activate()?,
    }
    Ok(())
}

pub fn factory_reset(ident: &str) -> Result<()> {
    eprintln!("Resetting Card {ident}");
    let mut open = util::open_card(ident)?;
    let mut card = open.transaction()?;

    card.factory_reset().map_err(|e| anyhow!(e))
}

pub fn kdf_setup(ident: &str) -> Result<()> {
    eprintln!("Setting up KDF mode for {ident}");
    let mut open = util::open_card(ident)?;
    let mut card = open.transaction()?;

    const DEFAULT_PW3: &str = "12345678";

    // present default pw3 (we expect to run this command after a factory reset)
    card.verify_admin_pin(DEFAULT_PW3.to_string().into())?;

    use rand::Rng;
    let mut rng = rand::thread_rng();
    let salt_pw1: [u8; 8] = rng.gen();
    let salt_rc: [u8; 8] = rng.gen();
    let salt_pw3: [u8; 8] = rng.gen();

    const HASH_ALGO: u8 = 0x08; // SHA256

    let kdf_do = KdfDo::iter_salted(HASH_ALGO, salt_pw1.into(), salt_rc.into(), salt_pw3.into())?;
    card.card().set_kdf_do(&kdf_do)?;

    // Try aligning stored passwords on the card.
    //
    // This should work for all ()?) cards other than Gnuk.
    // (Gnuk seems to align the passwords implicitly, so we ignore errors here, and hope for the best)
    //
    // Also see https://dev.gnupg.org/T3891
    if let Some(pw3) = kdf_do.initial_hash_pw3() {
        if card
            .card()
            .change_pw3(DEFAULT_PW3.as_bytes().to_vec().into(), pw3.to_vec().into())
            .is_ok()
        {
            if let Some(pw1) = kdf_do.initial_hash_pw1() {
                card.card()
                    .reset_retry_counter_pw1(pw1.to_vec().into(), None)?;
            }

            // FIXME: update resetting code?
        } else {
            log::debug!(
                "change_pw3: Failed to align PIN (if the card is a Gnuk, that's expected and ok)"
            );
        }
    }

    Ok(())
}

#[derive(ValueEnum, Debug, Clone)]
pub enum SetIdentityId {
    #[value(name = "0")]
    Zero,
    #[value(name = "1")]
    One,
    #[value(name = "2")]
    Two,
}

impl From<SetIdentityId> for u8 {
    fn from(id: SetIdentityId) -> Self {
        match id {
            SetIdentityId::Zero => 0,
            SetIdentityId::One => 1,
            SetIdentityId::Two => 2,
        }
    }
}

pub fn set_identity(ident: &str, id: SetIdentityId) -> Result<(), Box<dyn std::error::Error>> {
    let mut open = util::open_card(ident)?;
    let mut card = open.transaction()?;

    card.set_identity(u8::from(id))?;
    Ok(())
}

pub fn activate() -> Result<(), Box<dyn std::error::Error>> {
    let mut cards: Vec<_> = PcscBackend::card_backends(None)?.collect();

    if cards.len() != 1 {
        return Err(Error::InternalError(format!(
            "This command is only allowed if exactly one card is connected, found {}.",
            cards.len()
        ))
        .into());
    }

    let mut card = cards
        .pop()
        .ok_or_else(|| anyhow!("There are no more cards"))??;
    let mut tx = card.transaction(None)?;

    // SELECT OpenPGP
    let select_res = tx.transmit(
        &[
            0x00, 0xa4, 0x04, 0x00, 0x00, 0x00, 0x06, 0xd2, 0x76, 0x00, 0x01, 0x24, 0x01,
        ],
        254,
    )?;

    if select_res == [0x62, 0x85] {
        // ACTIVATE FILE
        let activate_res = tx.transmit(&[0x00, 0x44, 0x00, 0x00], 254)?;

        if activate_res == [0x90, 0x00] {
            Ok(())
        } else {
            Err(Error::InternalError(format!(
                "ACTIVATE FILE failed with status {activate_res:02x?}"
            ))
            .into())
        }
    } else {
        Err(Error::InternalError("Card doesn't appear to be terminated.".to_string()).into())
    }
}