/* (C) 2024 by sysmocom s.f.m.c. GmbH * All Rights Reserved * * Author: Philipp Maier * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ package org.osmocom.androidApduProxy; import java.io.*; import java.net.InetAddress; import java.net.Socket; import android.util.Log; interface VpcdCallback { /** * Request ATR from smartcard * @return array of bytes that contains the ATR */ public byte[] vpcdAtr(); /** * Request to reset the smartcard */ public void vpcdReset(); /** * Request to turn VCC of the smartcard on */ public void vpcdPwrOn(); /** * Request to turn VCC of the smartcard off */ public void vpcdPwrOff(); /** * Request a TPDU transaction * @param tpdu array of bytes that contains the request TPDU * @return array of bytes that contains the response TPDU */ public byte[] vpcdTransact(byte[] tpdu); } class Vpcd { /** * VPCD client. * The VPCD client maintains a TCP connection towards a VPCD server and forwards the requests * from the server to caller provided callback methods. */ // Allowed control byte value constants private static final char VPCD_CTRL_OFF = 0x00; private static final char VPCD_CTRL_ON = 0x01; private static final char VPCD_CTRL_RESET = 0x02; private static final char VPCD_CTRL_ATR = 0x04; private VpcdCallback callback = null; private OutputStream tcpTx = null; private InputStream tcpRx = null; private boolean receiving = false; /** * Create a new VPCD client instance. * @param callback instance of VpcdClientCallback that implements the VPCD callback methods */ Vpcd(VpcdCallback callback) { this.callback = callback; } //extract the length value from two byte length field private int decMsgLen(byte[] message) { int hlen = message[0]; int llen = message[1]; return (hlen << 8 | llen); } //encode the length value into a two byte length field private byte[] encMsgLen(int length) { byte[] result = new byte[2]; result[0] = (byte) ((length >> 8) & 0xff); result[1] = (byte) (length & 0xff); return result; } //send data to VPCD server private int send(byte[] message){ byte[] messageLength; try { //TODO: clarify if this is necessary if (message == null) { messageLength = encMsgLen(0); Log.d("VPCD", String.format("TCP TX: %s (empty VPCD message)\n", Utils.b2h(messageLength))); tcpTx.write(messageLength); } else { messageLength = encMsgLen(message.length); Log.d("VPCD", String.format("TCP TX: %s%s\n", Utils.b2h(messageLength), Utils.b2h(message))); tcpTx.write(messageLength); tcpTx.write(message); } tcpTx.flush(); return 0; } catch (Exception e) { Log.e("VPCD", e.getMessage() + "\n"); return -1; } } /** * Open and maintain a new VPCD connection. This method will run as long as the VPCD connection * exists, so it must only be called from an executor that runs it in a separate thread. * @param hostname hostname or IP-address of the VPCD server * @param port of the VPCD server */ public void open(String hostname, int port) throws Exception { Socket socket; //create TCP socket try { InetAddress serverAddr = InetAddress.getByName(hostname); Log.d("VPCD", String.format("connecting to %s:%d...\n", hostname, port)); socket = new Socket(serverAddr, port); } catch (Exception e) { Log.e("VPCD", e.getMessage() + "\n"); throw e; } //handle input/output of the TCP connection try { //tie the input and output stream of the socket to PrintWriter and a BufferedReader object tcpRx = socket.getInputStream(); tcpTx = socket.getOutputStream(); //tell the outside world that the connection is ready Log.d("VPCD", "connection successful!\n"); //continously receive from TCP socket and pass VPCD requests to the outside world this.receiving = true; byte[] message = new byte[0xFFFF]; int messageLen; int rc; byte controlByte; while (receiving) { // Receive VPCD message length if (tcpRx.read(message,0,2) < 0) throw new Exception("Unable to read from TCP socket!"); //Stop in case we are not receiving any more if (!receiving) break; messageLen = decMsgLen(message); Log.d("VPCD", String.format("TCP RX: %s => messageLen = %d\n", Utils.b2h(Utils.trimByteArray(message,2)), messageLen)); // Receive VPCD message body if(tcpRx.read(message,0, messageLen) < 0) throw new Exception("Unable to read from TCP socket!"); //Stop in case we are not receiving any more if (!receiving) break; if (messageLen == 1) { //Control byte controlByte = message[0]; Log.d("VPCD", String.format("TCP RX: %s => controlByte = %02X\n", Utils.b2h(Utils.trimByteArray(message, messageLen)), controlByte)); switch (controlByte) { case VPCD_CTRL_OFF: Log.d("VPCD", "remote end asks to power card OFF\n"); callback.vpcdPwrOff(); break; case VPCD_CTRL_ON: Log.d("VPCD", "remote end asks to power card ON\n"); callback.vpcdPwrOn(); break; case VPCD_CTRL_RESET: Log.d("VPCD", "remote end asks for RESET\n"); callback.vpcdReset(); break; case VPCD_CTRL_ATR: Log.d("VPCD", "remote end asks for ATR\n"); byte[] atr; atr = callback.vpcdAtr(); if (atr == null) throw new Exception("target didn't respond to ATR request!"); send(atr); break; default: Log.d("VPCD", "received invalid control byte from remote end\n"); break; } } else { //TPDU data byte[] reqTpdu = new byte[messageLen]; byte[] resTpdu; System.arraycopy(message, 0, reqTpdu,0, messageLen); Log.d("VPCD", String.format("remote end asks to send TPDU: %s\n", Utils.b2h(reqTpdu))); resTpdu = callback.vpcdTransact(reqTpdu); if (resTpdu == null) throw new Exception("target didn't respond to TPDU!\n"); Log.d("VPCD", String.format("got response TPDU: %s\n", Utils.b2h(resTpdu))); send(resTpdu); } } } catch (Exception e) { Log.e("VPCD", e.getMessage() + "\n"); receiving = false; throw e; } //close TCP socket try { InetAddress serverAddr = InetAddress.getByName(hostname); Log.d("VPCD", "closing connection to " + hostname + ":" + port + "...\n"); tcpTx = null; tcpRx = null; socket.close(); Log.d("VPCD", "connection closed!\n"); } catch (Exception e) { Log.e("VPCD", e.getMessage() + "\n"); receiving = false; throw e; } } /** * Close VPCD connection */ public void close() { this.receiving = false; } }