/* (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 android.content.Context; import android.se.omapi.Channel; import android.se.omapi.Reader; import android.se.omapi.SEService; import android.se.omapi.SEService.OnConnectedListener; import android.se.omapi.Session; import java.util.Map; import java.util.HashMap; import java.util.concurrent.Executor; import android.util.Log; interface OmapiCallback { /** * Tell application that the SE-Service is connected. */ public void omapiConnected(Omapi omapi); /** * Tell application that the SE-Service is disconnected. */ public void omapiDisconnected(); } class Omapi { /** * OMAPI client. * The OMAPI client maintains an SE-Service connection towards a secure element (UICC, eUICC) */ private SEService seService = null; private boolean seServiceConnected = false; private OmapiCallback callback = null; Map sessions = new HashMap(); Map channels = new HashMap(); Integer channelIdCounter = 0; Omapi omapi = this; /** * Create a new OMAPI client instance. * @param context application environment context * @param callback instance of OmapiCallback that implements the OMAPI callback methods */ Omapi(Context context, OmapiCallback callback) { seService = new SEService(context, new syncExec(), mListener); assert(callback != null); this.callback = callback; Log.d("OMAPI", "Connecting SE-Service...\n"); } private final OnConnectedListener mListener = new OnConnectedListener() { @Override public void onConnected() { Log.d("OMAPI", "SE-Service connection successful!\n"); callback.omapiConnected(omapi); } }; private class syncExec implements Executor { public void execute(Runnable r) { r.run(); } } //Ensure SE-Service is present and connected private void ensureSeService() throws Exception { if (seService == null) throw new Exception("cannot get channel, SE-Service not present!"); if (!seService.isConnected()) throw new Exception("cannot get channel, SE-Service not connected!"); } //Get a session for a specified reader. In case the specified reader has no session open yet, //create a new one. Sessions stay open throughout the whole lifetime of this object. private Session getOrCreateSession(String readerName) throws Exception { Session session; Reader[] readers = seService.getReaders(); for (Reader reader : readers) { if (reader.getName().equals(readerName) && reader.isSecureElementPresent()) { if (sessions.containsKey(readerName)) { session = sessions.get(readerName); assert (session != null); } else { session = reader.openSession(); sessions.put(readerName, session); } return session; } } throw new Exception("reader " + readerName + " not found!\n"); } /** * Scan for useable readers. *

The result is a Map that contains the available readers as key value pairs, where the * key is the reader name and the value the ATR of the card. Empty readers will be ignored. * @return Map that contains reader names and card ATR values */ public Map scan() { try { Map usable_readers = new HashMap(); ensureSeService(); Reader[] readers = seService.getReaders(); if (readers.length == 0) throw new Exception("no reader available!"); for (Reader reader : readers) { if (reader.isSecureElementPresent()) { Session session = reader.openSession(); byte[] atr = session.getATR(); if (atr != null) { Log.d("OMAPI", "found reader: " + reader.getName() + " (ATR=" + Utils.b2h(atr) + ")\n"); usable_readers.put(reader.getName(), atr); } else { Log.d("OMAPI", "found reader: " + reader.getName() + " (no ATR, unresponsive?)\n"); } session.close(); } else { Log.d("OMAPI","found reader: " + reader.getName() + " (empty)\n"); } } return usable_readers; } catch (Exception e) { Log.e("OMAPI",e.getMessage() + "\n"); return null; } } /** * Perform an APDU transaction on the card. * @param readerName string that contains the reader name (e.g. "SIM1") * @return array of bytes that contains the ATR (from card) or, throws Exception on error */ public byte[] getAtr(String readerName) throws Exception { try { ensureSeService(); Session session = getOrCreateSession(readerName); byte[] atr = session.getATR(); if (atr != null) { Log.d("OMAPI", "ATR: " + Utils.b2h(atr) + "\n"); return atr; } throw new Exception( "ATR request failed!\n"); } catch (Exception e) { Log.e("OMAPI", e.getMessage() + "\n"); throw e; } } /** * Open a channel on the specified reader to the specified AID. * @param readerName string that contains the reader name (e.g. "SIM1") * @param aid array of bytes that contains the AID of the application to access * @return OMAPI Channel number on success, throws Exception on error */ public int open(String readerName, byte[] aid) throws Exception { try { ensureSeService(); Session session = getOrCreateSession(readerName); Channel channel = session.openLogicalChannel(aid); if (channel == null) throw new Exception(String.format("could not open channel for AID (%s) on reader: %s!\n", Utils.b2h(aid), readerName)); Log.d("OMAPI", String.format("sucessfully opend channel for AID (%s) on reader: %s\n", Utils.b2h(aid), readerName)); channelIdCounter++; channels.put(channelIdCounter, channel); return channelIdCounter; } catch (Exception e) { Log.e("OMAPI",e.getMessage() + "\n"); throw e; } } /** * Perform an APDU transaction on the card. * @param channelId id-number of the OMAPI Channel * @param apdu array of bytes that contains the command APDU (to card) * @return array of bytes that contains the response APDU (from card) or throws Exception on error */ public byte[] transact(int channelId, byte[] apdu) throws Exception { try { ensureSeService(); if (!channels.containsKey(channelId)) throw new Exception(String.format("no channel open under channelId = %d", channelId)); Channel channel = channels.get(channelId); Log.d("OMAPI","APDU TX: " + Utils.b2h(apdu) + "\n"); byte[] response = channel.transmit(apdu); if (response == null) throw new Exception("unresponsive card!"); Log.d("OMAPI","APDU RX: " + Utils.b2h(response) + "\n"); return response; } catch (Exception e) { Log.e("OMAPI",e.getMessage() + "\n"); throw e; } } /** * Close an OMAPI channel. * @param channelId id-number of the OMAPI Channel */ public void close(int channelId) { try { ensureSeService(); if (!channels.containsKey(channelId)) throw new Exception(String.format("no channel open under channelId = %d", channelId)); Channel channel = channels.get(channelId); Log.d("OMAPI", String.format("closing reader channel %d ...\n", channelId)); channel.close(); Log.d("OMAPI", String.format("channel %d closed.\n", channelId)); channels.remove(channel); } catch (Exception e) { Log.e("OMAPI",e.getMessage() + "\n"); } } /** * Shutdown OMAPI SE-Service. This shuts down the SE-Service. There is no method to recover * from the shutdown state again. The APU user must create a new instance. */ public void shutdown() { /* Inform callback handler that the SE-Service is going to disconnect. */ callback.omapiDisconnected(); /* Make sure all channels and sessions are orderly closed. */ for (Channel channel : channels.values()) channel.close(); for (Session session : sessions.values()) session.close(); sessions = new HashMap(); channels = new HashMap(); /* Shutdown SE-Service */ seService.shutdown(); } }