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-License-Identifier: MIT OR Apache-2.0

use std::io::BufReader;
use std::path::PathBuf;

use anyhow::{anyhow, Result};
use clap::Parser;
use openpgp_card::ocard::KeyType;
use openpgp_card_rpgp::CardSlot;
use pgp::composed::{Esk, Message, PlainSessionKey};

use crate::util;
use crate::util::DebugWrapper;

#[derive(Parser, Debug)]
pub struct DecryptCommand {
    #[arg(
        name = "card ident",
        short = 'c',
        long = "card",
        help = "Identifier of the card to use"
    )]
    ident: String,

    #[arg(
        name = "User PIN file",
        short = 'p',
        long = "user-pin",
        help = "Optionally, get User PIN from a file"
    )]
    pin_file: Option<PathBuf>,

    /// Input file (stdin if unset)
    #[arg(name = "input")]
    input: Option<PathBuf>,

    /// Output file (stdout if unset)
    #[arg(name = "output", long = "output", short = 'o')]
    pub output: Option<PathBuf>,
}

pub fn decrypt(command: DecryptCommand) -> Result<(), Box<dyn std::error::Error>> {
    let mut input = util::open_or_stdin(command.input.as_deref())?;
    let (message, _headers) = Message::from_reader(BufReader::new(DebugWrapper(&mut input)))?;

    let mut open = util::open_card(&command.ident)?;
    let mut tx = open.transaction()?;

    if tx.fingerprints()?.decryption().is_none() {
        return Err(anyhow!("Can't decrypt: this card has no key in the decryption slot.").into());
    }

    let user_pin = util::get_pin(&mut tx, command.pin_file, crate::ENTER_USER_PIN)?;
    let _ = util::verify_to_user(&mut tx, user_pin)?;

    let cs = CardSlot::init_from_card(&mut tx, KeyType::Decryption, &|| {
        eprintln!("Touch confirmation needed for decryption");
    })?;

    let Message::Encrypted { esk, .. } = &message else {
        return Err(anyhow::anyhow!("message not encrypted").into());
    };

    // Try all ESK, until we can decrypt one
    for e in esk {
        // We only consider PKESK (OpenPGP card doesn't apply to SKESK)
        if let Esk::PublicKeyEncryptedSessionKey(pgp::packet::PublicKeyEncryptedSessionKey::V3 {
            ref values,
            ..
        }) = e
        {
            // Attempt to decrypt this PKESK with the card
            if let Ok((session_key, session_key_algorithm)) = cs.decrypt(values) {
                // Session key decrypted! The card-related part of the operation is done.
                let plain_session_key = PlainSessionKey::V3_4 {
                    key: session_key.into(),
                    sym_alg: session_key_algorithm,
                };

                // Symmetrically decrypt the edata
                let decrypted = message.decrypt_with_session_key(plain_session_key)?;

                // Try to extract the pure plaintext from the decrypted inner message
                // (which could still be compressed and/or signed)
                let data = unpack_unencrypted_msg(decrypted)?;

                // Write out the decrypted plaintext
                let mut sink = util::open_or_stdout(command.output.as_deref())?;
                sink.write_all(&data)?;

                return Ok(());
            }
        }
    }

    Err(anyhow::anyhow!("Couldn't decrypt message").into())
}

fn unpack_unencrypted_msg(mut msg: Message) -> Result<Vec<u8>> {
    // we are willing to strip away a few layers of compression
    let mut i = 0;
    while msg.is_compressed() && i < 3 {
        msg = msg.decompress()?;

        i += 1;
    }

    let cleartext = msg.as_data_vec()?;

    Ok(cleartext)
}