SmartCard.java

/*
 * Copyright 2024 Emmanuel Bourg
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.jsign.jca;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminal;
import javax.smartcardio.CardTerminals;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.smartcardio.TerminalFactory;

import org.apache.commons.io.HexDump;

/**
 * Base class for the smart card implementations.
 *
 * @since 6.0
 */
abstract class SmartCard {

    private final Logger log = Logger.getLogger(getClass().getName());

    private final CardChannel channel;

    /** Personal Identification Number */
    protected String pin;

    /** Data Object cache */
    protected final Map<Integer, byte[]> dataObjectCache = new HashMap<>();

    protected SmartCard(CardChannel channel) {
        this.channel = channel;
    }

    /**
     * Set the PIN for the verify operation.
     */
    public void verify(String pin) {
        this.pin = pin;
    }

    /**
     * Transmit the command to the card and display the APDU request/response if debug is enabled.
     */
    protected ResponseAPDU transmit(CommandAPDU command) throws CardException {
        if (log.isLoggable(Level.FINEST)) {
            log.finest(command.toString());
            try {
                StringBuffer out = new StringBuffer();
                HexDump.dump(command.getBytes(), 0, out, 0, command.getBytes().length);
                log.finest(out.toString());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        long t1 = System.nanoTime();
        ResponseAPDU response = channel.transmit(command);
        long t2 = System.nanoTime();

        if (log.isLoggable(Level.FINEST)) {
            log.finest(response + " (" + (t2 - t1) / 1000000 + " ms)");
            if (response.getData().length > 0) {
                try {
                    StringBuffer out = new StringBuffer();
                    HexDump.dump(response.getData(), 0, out, 0, response.getData().length);
                    log.finest(out.toString());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            log.finest("");
        }

        return response;
    }

    /**
     * Throws a CardException with a meaningful message if the APDU response status indicates an error.
     */
    protected void handleError(ResponseAPDU response) throws CardException {
        switch (response.getSW()) {
            case 0x9000:
                return;
            case 0x63C0:
            case 0x63C1:
            case 0x63C2:
            case 0x63C3:
            case 0x63C4:
            case 0x63C5:
            case 0x63C6:
            case 0x63C7:
            case 0x63C8:
            case 0x63C9:
            case 0x63CA:
            case 0x63CB:
            case 0x63CC:
            case 0x63CD:
            case 0x63CE:
            case 0x63CF:
                throw new CardException("PIN verification failed, " + (response.getSW() & 0x0F) + " tries left");
            case 0x6700:
                throw new CardException("Wrong length");
            case 0x6982:
                throw new CardException("PIN verification required");
            case 0x6983:
                throw new CardException("PIN blocked");
            case 0x6985:
                throw new CardException("Conditions of use not satisfied");
            case 0x6A80:
                throw new CardException("The parameters in the data field are incorrect");
            case 0x6A82:
                throw new CardException("Incorrect P1 or P2 parameter");
            case 0x6D00:
                throw new CardException("Instruction code not supported or invalid");
            default:
                throw new CardException("Error " + Integer.toHexString(response.getSW()));
        }
    }

    /**
     * Opens a channel to the first available smart card matching the specified name.
     *
     * @param name the partial name of the terminal
     */
    static CardChannel openChannel(String name) throws CardException {
        CardTerminal terminal = getTerminal(name);
        if (terminal != null) {
            try {
                Card card = terminal.connect("T=1");
                return card.getBasicChannel();
            } catch (CardException e) {
                e.printStackTrace();
            }
        }

        return null;
    }

    /**
     * Returns the first available smart card terminal matching the specified name.
     *
     * @param name the partial name of the terminal
     */
    static CardTerminal getTerminal(String name) throws CardException {
        CardTerminals terminals = TerminalFactory.getDefault().terminals();
        for (CardTerminal terminal : terminals.list(CardTerminals.State.CARD_PRESENT)) {
            if (name == null || terminal.getName().toLowerCase().contains(name.toLowerCase())) {
                return terminal;
            }
        }

        return null;
    }
}