001// SPDX-License-Identifier: GPL-3.0-or-later
002// Copyright © 2026 Aurélien Pierre
003
004package com.aurelienpierre.sioul;
005
006import java.io.IOException;
007import java.util.Arrays;
008
009/**
010 * A smart card's messages over USB (CCID, the USB Integrated Circuit(s) Cards
011 * Interface Device class, revision 1.1, §6), for your security key plugged
012 * into the phone (SecurityKeyLink.java). Pure Java over a {@link Pipe}, the
013 * key's two bulk endpoints, so that android/jvm-checks runs it against a
014 * stand-in reader.
015 *
016 * <ul>
017 * <li><b>Messages</b>: PC_to_RDR_IccPowerOn (0x62) answered by the card's ATR,
018 * PC_to_RDR_XfrBlock (0x6F) carrying one APDU, PC_to_RDR_IccPowerOff (0x63);
019 * each with a sequence number its answer must repeat, slot 0.</li>
020 * <li><b>Sent</b> in packets of the endpoint's size, with a zero-length packet
021 * when a message fills its last packet exactly, so that the key knows it
022 * ended.</li>
023 * <li><b>A key waiting for a touch</b> says so with a time extension (status
024 * 0x80), or stays silent: Sioul reads again, as long as {@link #WAIT_MS} at
025 * most (a YubiKey waits 15 seconds for a touch), while the band asks for the
026 * touch, so that no touch setting breaks the operation; a key unplugged
027 * meanwhile fails at once. Any other failure is said as a code, never
028 * bytes.</li>
029 * <li><b>Nothing kept</b>: each message's buffer is wiped once sent or
030 * read.</li>
031 * </ul>
032 */
033final class Ccid {
034    /** The key's two bulk endpoints. */
035    interface Pipe {
036        /** Sends {@code length} bytes from {@code offset}: how many went, or a negative number. */
037        int send(byte[] data, int offset, int length, int timeoutMs);
038
039        /** Receives one transfer into {@code into}: how many came, or a negative number (nothing within the time, or a failure). */
040        int receive(byte[] into, int timeoutMs);
041
042        /** Whether the key is still plugged in. */
043        boolean present();
044    }
045
046    /** The key's answer that said nothing sensible, or a failure of its own: a code, never its bytes. */
047    static final class Failure extends IOException {
048        private static final long serialVersionUID = 1L;
049
050        Failure(String code) {
051            super(code);
052        }
053    }
054
055    /** One bulk transfer at most. */
056    static final int TRANSFER_MS = 1000;
057    /** How long a key may keep Sioul waiting for an answer, a touch included: a YubiKey gives up waiting for one after 15 seconds; ten more as a margin. */
058    static final int WAIT_MS = 25000;
059    /** A message's header. */
060    static final int HEADER = 10;
061    /** The longest answer read: an extended APDU's, with room. */
062    static final int LONGEST = 65538 + HEADER;
063
064    static final byte POWER_ON = 0x62;
065    static final byte POWER_OFF = 0x63;
066    static final byte XFR_BLOCK = 0x6F;
067    static final byte DATA_BLOCK = (byte) 0x80;
068    static final byte SLOT_STATUS = (byte) 0x81;
069
070    private final Pipe pipe;
071    private final int packet;
072    private byte sequence;
073
074    /** Messages over {@code pipe}, whose endpoints take {@code packet} bytes a packet. */
075    Ccid(Pipe pipe, int packet) {
076        this.pipe = pipe;
077        this.packet = Math.max(8, packet);
078    }
079
080    /** The key powered on: its ATR. */
081    byte[] powerOn() throws IOException {
082        return exchange(POWER_ON, new byte[0], DATA_BLOCK);
083    }
084
085    /** The key powered off, its state with it. */
086    void powerOff() throws IOException {
087        exchange(POWER_OFF, new byte[0], SLOT_STATUS);
088    }
089
090    /** One APDU to the key: its answer, status words included. The APDU is the caller's to wipe. */
091    byte[] transmit(byte[] apdu) throws IOException {
092        return exchange(XFR_BLOCK, apdu, DATA_BLOCK);
093    }
094
095    /** The header and the data of one message to the key. */
096    static byte[] message(byte type, byte[] data, byte sequence) {
097        byte[] message = new byte[HEADER + data.length];
098        message[0] = type;
099        message[1] = (byte) data.length;
100        message[2] = (byte) (data.length >> 8);
101        message[3] = (byte) (data.length >> 16);
102        message[4] = (byte) (data.length >> 24);
103        message[5] = 0; // slot
104        message[6] = sequence;
105        // bPowerSelect (automatic), bBWI, wLevelParameter: all 0.
106        System.arraycopy(data, 0, message, HEADER, data.length);
107        return message;
108    }
109
110    private byte[] exchange(byte type, byte[] data, byte expected) throws IOException {
111        byte number = sequence++;
112        byte[] message = message(type, data, number);
113        try {
114            send(message);
115        } finally {
116            Arrays.fill(message, (byte) 0);
117        }
118        long until = System.nanoTime() + WAIT_MS * 1_000_000L;
119        while (true) {
120            byte[] answer = receive(until);
121            try {
122                if (answer[0] != expected)
123                    throw new Failure("CCID answer of type " + (answer[0] & 0xFF));
124                if (answer[6] != number)
125                    throw new Failure("CCID answer to another message");
126                int status = answer[7] & 0xFF;
127                switch (status >> 6) {
128                    case 0:
129                        return Arrays.copyOfRange(answer, HEADER, answer.length);
130                    case 2:
131                        // Time extension: the key waits, for a touch most often.
132                        if (System.nanoTime() > until)
133                            throw new Failure("CCID: the key kept waiting");
134                        continue;
135                    default:
136                        throw new Failure("CCID failure " + (answer[8] & 0xFF) + ", slot status " + (status & 0x03));
137                }
138            } finally {
139                Arrays.fill(answer, (byte) 0);
140            }
141        }
142    }
143
144    private void send(byte[] message) throws IOException {
145        int at = 0;
146        while (at < message.length) {
147            int length = Math.min(packet, message.length - at);
148            int sent = pipe.send(message, at, length, TRANSFER_MS);
149            if (sent <= 0)
150                throw new Failure("USB: nothing sent");
151            at += sent;
152        }
153        if (message.length % packet == 0 && pipe.send(message, 0, 0, TRANSFER_MS) < 0)
154            throw new Failure("USB: no end sent");
155    }
156
157    /** One whole message from the key, its header first. */
158    private byte[] receive(long until) throws IOException {
159        byte[] buffer = new byte[Math.max(packet, 4096)];
160        byte[] whole = new byte[0];
161        try {
162            int total = -1;
163            int empty = 0;
164            while (total < 0 || whole.length < total) {
165                int got = pipe.receive(buffer, TRANSFER_MS);
166                // Nothing within a transfer's time: a key waiting for a touch may
167                // stay silent; read again until the deadline, unless it is gone.
168                if (got < 0) {
169                    if (!pipe.present())
170                        throw new Failure("USB: the key is gone");
171                    if (System.nanoTime() > until)
172                        throw new Failure("USB: nothing came");
173                    continue;
174                }
175                // A zero-length packet ends an answer that filled its last packet: the
176                // one before this, read whole already. Within an answer, it cuts it short.
177                if (got == 0) {
178                    if (whole.length > 0 || ++empty > 2)
179                        throw new Failure("USB: an answer cut short");
180                    continue;
181                }
182                byte[] grown = Arrays.copyOf(whole, whole.length + got);
183                System.arraycopy(buffer, 0, grown, whole.length, got);
184                Arrays.fill(whole, (byte) 0);
185                whole = grown;
186                if (total < 0 && whole.length >= HEADER) {
187                    long length = (whole[1] & 0xFFL) | (whole[2] & 0xFFL) << 8 | (whole[3] & 0xFFL) << 16 | (whole[4] & 0xFFL) << 24;
188                    if (length > LONGEST - HEADER)
189                        throw new Failure("CCID answer too long");
190                    total = HEADER + (int) length;
191                }
192            }
193            return Arrays.copyOf(whole, total);
194        } finally {
195            Arrays.fill(buffer, (byte) 0);
196            Arrays.fill(whole, (byte) 0);
197        }
198    }
199}