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}