Compare commits

...

4 commits

Author SHA1 Message Date
ccf21675d6 [3/n] Handle USB permission responses properly
Some checks failed
/ build-debug (push) Failing after 14m40s
2024-06-30 17:06:58 -04:00
87fc1cd2f8 NotificationsActivity: Prevent simulatenous handleNotification + refresh 2024-06-30 16:55:55 -04:00
3667f578d7 [2/n] USB CCID Reader support
*cough* copied CCID driver from OpenKeychains
2024-06-30 16:53:00 -04:00
803b88f74e feat: USB CCID reader support [1/n] 2024-06-29 20:02:48 -04:00
11 changed files with 837 additions and 8 deletions

View file

@ -1,14 +1,23 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.content.Context import android.content.Context
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbInterface
import android.hardware.usb.UsbManager
import android.se.omapi.SEService import android.se.omapi.SEService
import android.util.Log import android.util.Log
import im.angry.openeuicc.core.usb.UsbApduInterface
import im.angry.openeuicc.core.usb.getIoEndpoints
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory { open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccChannelFactory {
private var seService: SEService? = null private var seService: SEService? = null
private val usbManager by lazy {
context.getSystemService(Context.USB_SERVICE) as UsbManager
}
private suspend fun ensureSEService() { private suspend fun ensureSEService() {
if (seService == null || !seService!!.isConnected) { if (seService == null || !seService!!.isConnected) {
seService = connectSEService(context) seService = connectSEService(context)
@ -36,6 +45,17 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
return null return null
} }
override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? {
val (bulkIn, bulkOut) = usbInterface.getIoEndpoints()
if (bulkIn == null || bulkOut == null) return null
val conn = usbManager.openDevice(usbDevice) ?: return null
if (!conn.claimInterface(usbInterface, true)) return null
return EuiccChannel(
FakeUiccPortInfoCompat(FakeUiccCardInfoCompat(EuiccChannelManager.USB_CHANNEL_ID)),
UsbApduInterface(conn, bulkIn, bulkOut)
)
}
override fun cleanup() { override fun cleanup() {
seService?.shutdown() seService?.shutdown()
seService = null seService = null

View file

@ -1,8 +1,11 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.content.Context import android.content.Context
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager
import android.util.Log import android.util.Log
import im.angry.openeuicc.core.usb.getSmartCardInterface
import im.angry.openeuicc.di.AppContainer import im.angry.openeuicc.di.AppContainer
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -23,12 +26,18 @@ open class DefaultEuiccChannelManager(
private val channelCache = mutableListOf<EuiccChannel>() private val channelCache = mutableListOf<EuiccChannel>()
private var usbChannel: EuiccChannel? = null
private val lock = Mutex() private val lock = Mutex()
protected val tm by lazy { protected val tm by lazy {
appContainer.telephonyManager appContainer.telephonyManager
} }
private val usbManager by lazy {
context.getSystemService(Context.USB_SERVICE) as UsbManager
}
private val euiccChannelFactory by lazy { private val euiccChannelFactory by lazy {
appContainer.euiccChannelFactory appContainer.euiccChannelFactory
} }
@ -38,6 +47,15 @@ open class DefaultEuiccChannelManager(
private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? { private suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? {
lock.withLock { lock.withLock {
if (port.card.physicalSlotIndex == EuiccChannelManager.USB_CHANNEL_ID) {
return if (usbChannel != null && usbChannel!!.valid) {
usbChannel
} else {
usbChannel = null
null
}
}
val existing = val existing =
channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex } channelCache.find { it.slotId == port.card.physicalSlotIndex && it.portId == port.portIndex }
if (existing != null) { if (existing != null) {
@ -73,6 +91,10 @@ open class DefaultEuiccChannelManager(
override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? = override fun findEuiccChannelBySlotBlocking(logicalSlotId: Int): EuiccChannel? =
runBlocking { runBlocking {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (logicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return@withContext usbChannel
}
for (card in uiccCards) { for (card in uiccCards) {
for (port in card.ports) { for (port in card.ports) {
if (port.logicalSlotIndex == logicalSlotId) { if (port.logicalSlotIndex == logicalSlotId) {
@ -88,6 +110,10 @@ open class DefaultEuiccChannelManager(
override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? = override fun findEuiccChannelByPhysicalSlotBlocking(physicalSlotId: Int): EuiccChannel? =
runBlocking { runBlocking {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return@withContext usbChannel
}
for (card in uiccCards) { for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue if (card.physicalSlotIndex != physicalSlotId) continue
for (port in card.ports) { for (port in card.ports) {
@ -100,6 +126,10 @@ open class DefaultEuiccChannelManager(
} }
override suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? { override suspend fun findAllEuiccChannelsByPhysicalSlot(physicalSlotId: Int): List<EuiccChannel>? {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return usbChannel?.let { listOf(it) }
}
for (card in uiccCards) { for (card in uiccCards) {
if (card.physicalSlotIndex != physicalSlotId) continue if (card.physicalSlotIndex != physicalSlotId) continue
return card.ports.mapNotNull { tryOpenEuiccChannel(it) } return card.ports.mapNotNull { tryOpenEuiccChannel(it) }
@ -115,6 +145,10 @@ open class DefaultEuiccChannelManager(
override suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? = override suspend fun findEuiccChannelByPort(physicalSlotId: Int, portId: Int): EuiccChannel? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (physicalSlotId == EuiccChannelManager.USB_CHANNEL_ID) {
return@withContext usbChannel
}
uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card -> uiccCards.find { it.physicalSlotIndex == physicalSlotId }?.let { card ->
card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) } card.ports.find { it.portIndex == portId }?.let { tryOpenEuiccChannel(it) }
} }
@ -162,11 +196,37 @@ open class DefaultEuiccChannelManager(
} }
} }
override suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?> =
withContext(Dispatchers.IO) {
usbManager.deviceList.values.forEach { device ->
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
val iface = device.getSmartCardInterface() ?: return@forEach
// If we don't have permission, tell UI code that we found a candidate device, but we
// need permission to be able to do anything with it
if (!usbManager.hasPermission(device)) return@withContext Pair(device, null)
Log.i(TAG, "Found CCID interface on ${device.deviceId}:${device.vendorId}, and has permission; trying to open channel")
try {
val channel = euiccChannelFactory.tryOpenUsbEuiccChannel(device, iface)
if (channel != null && channel.lpa.valid) {
usbChannel = channel
return@withContext Pair(device, channel)
}
} catch (e: Exception) {
// Ignored -- skip forward
e.printStackTrace()
}
Log.i(TAG, "No valid eUICC channel found on USB device ${device.deviceId}:${device.vendorId}")
}
return@withContext Pair(null, null)
}
override fun invalidate() { override fun invalidate() {
for (channel in channelCache) { for (channel in channelCache) {
channel.close() channel.close()
} }
usbChannel?.close()
usbChannel = null
channelCache.clear() channelCache.clear()
euiccChannelFactory.cleanup() euiccChannelFactory.cleanup()
} }

View file

@ -1,5 +1,7 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbInterface
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
// This class is here instead of inside DI because it contains a bit more logic than just // This class is here instead of inside DI because it contains a bit more logic than just
@ -7,6 +9,8 @@ import im.angry.openeuicc.util.*
interface EuiccChannelFactory { interface EuiccChannelFactory {
suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel? suspend fun tryOpenEuiccChannel(port: UiccPortInfoCompat): EuiccChannel?
fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel?
/** /**
* Release all resources used by this EuiccChannelFactory * Release all resources used by this EuiccChannelFactory
* Note that the same instance may be reused; any resources allocated must be automatically * Note that the same instance may be reused; any resources allocated must be automatically

View file

@ -1,5 +1,7 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import android.hardware.usb.UsbDevice
/** /**
* EuiccChannelManager holds references to, and manages the lifecycles of, individual * EuiccChannelManager holds references to, and manages the lifecycles of, individual
* APDU channels to SIM cards. The find* methods will create channels when needed, and * APDU channels to SIM cards. The find* methods will create channels when needed, and
@ -11,13 +13,25 @@ package im.angry.openeuicc.core
* Holding references independent of EuiccChannelManagerService is unsupported. * Holding references independent of EuiccChannelManagerService is unsupported.
*/ */
interface EuiccChannelManager { interface EuiccChannelManager {
companion object {
const val USB_CHANNEL_ID = 99
}
/** /**
* Scan all possible sources for EuiccChannels, return them and have all * Scan all possible _device internal_ sources for EuiccChannels, return them and have all
* scanned channels cached; these channels will remain open for the entire lifetime of * scanned channels cached; these channels will remain open for the entire lifetime of
* this EuiccChannelManager object, unless disconnected externally or invalidate()'d * this EuiccChannelManager object, unless disconnected externally or invalidate()'d
*/ */
suspend fun enumerateEuiccChannels(): List<EuiccChannel> suspend fun enumerateEuiccChannels(): List<EuiccChannel>
/**
* Scan all possible USB devices for CCID readers that may contain eUICC cards.
* If found, try to open it for access, and add it to the internal EuiccChannel cache
* as a "port" with id 99. When user interaction is required to obtain permission
* to interact with the device, the second return value (EuiccChannel) will be null.
*/
suspend fun enumerateUsbEuiccChannel(): Pair<UsbDevice?, EuiccChannel?>
/** /**
* Wait for a slot + port to reconnect (i.e. become valid again) * Wait for a slot + port to reconnect (i.e. become valid again)
* If the port is currently valid, this function will return immediately. * If the port is currently valid, this function will return immediately.

View file

@ -0,0 +1,159 @@
package im.angry.openeuicc.core.usb
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.util.Log
import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface
class UsbApduInterface(
private val conn: UsbDeviceConnection,
private val bulkIn: UsbEndpoint,
private val bulkOut: UsbEndpoint
): ApduInterface {
companion object {
private const val TAG = "UsbApduInterface"
}
private lateinit var ccidDescription: UsbCcidDescription
private lateinit var transceiver: UsbCcidTransceiver
private var channelId = -1
override fun connect() {
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
if (!ccidDescription.hasT0Protocol) {
throw IllegalArgumentException("Unsupported card reader; T=0 support is required")
}
transceiver = UsbCcidTransceiver(conn, bulkIn, bulkOut, ccidDescription)
try {
transceiver.iccPowerOn()
} catch (e: Exception) {
e.printStackTrace()
throw e
}
}
override fun disconnect() {
conn.close()
}
override fun logicalChannelOpen(aid: ByteArray): Int {
check(channelId == -1) { "Logical channel already opened" }
// OPEN LOGICAL CHANNEL
val req = manageChannelCmd(true, 0)
Log.d(TAG, "OPEN LOGICAL CHANNEL: ${req.encodeHex()}")
val resp = try {
transmitApduByChannel(req, 0)
} catch (e: Exception) {
e.printStackTrace()
return -1
}
Log.d(TAG, "OPEN LOGICAL CHANNEL response: ${resp.encodeHex()}")
return if (resp.size >= 2 && resp.sliceArray((resp.size - 2) until resp.size).contentEquals(
byteArrayOf(0x90.toByte(), 0x00)
)
) {
channelId = resp[0].toInt()
Log.d(TAG, "channelId = $channelId")
// Then, select AID
val selectAid = selectByDfCmd(aid, channelId.toByte())
Log.d(TAG, "Select DF command: ${selectAid.encodeHex()}")
val selectAidResp = transmitApduByChannel(selectAid, channelId.toByte())
Log.d(TAG, "Select DF resp: ${selectAidResp.encodeHex()}")
channelId
} else {
-1
}
}
override fun logicalChannelClose(handle: Int) {
check(handle == channelId) { "Logical channel ID mismatch" }
check(channelId != -1) { "Logical channel is not opened" }
// CLOSE LOGICAL CHANNEL
val req = manageChannelCmd(false, channelId.toByte())
Log.d(TAG, "CLOSE LOGICAL CHANNEL: ${req.encodeHex()}")
val resp = transmitApduByChannel(req, channelId.toByte())
Log.d(TAG, "CLOSE LOGICAL CHANNEL response: ${resp.encodeHex()}")
channelId = -1
}
override fun transmit(tx: ByteArray): ByteArray {
check(channelId != -1) { "Logical channel is not opened" }
Log.d(TAG, "USB APDU command: ${tx.encodeHex()}")
val resp = transmitApduByChannel(tx, channelId.toByte())
Log.d(TAG, "USB APDU response: ${resp.encodeHex()}")
return resp
}
override val valid: Boolean
get() = true
private fun buildCmd(cla: Byte, ins: Byte, p1: Byte, p2: Byte, data: ByteArray?, le: Byte?) =
byteArrayOf(cla, ins, p1, p2).let {
if (data != null) {
it + data.size.toByte() + data
} else {
it
}
}.let {
if (le != null) {
it + byteArrayOf(le)
} else {
it
}
}
private fun manageChannelCmd(open: Boolean, channel: Byte) =
if (open) {
buildCmd(0x00, 0x70, 0x00, 0x00, null, 0x01)
} else {
buildCmd(channel, 0x70, 0x80.toByte(), channel, null, null)
}
private fun selectByDfCmd(aid: ByteArray, channel: Byte) =
buildCmd(channel, 0xA4.toByte(), 0x04, 0x00, aid, null)
private fun transmitApduByChannel(tx: ByteArray, channel: Byte): ByteArray {
val realTx = tx.copyOf()
// OR the channel mask into the CLA byte
realTx[0] = ((realTx[0].toInt() and 0xFC) or channel.toInt()).toByte()
var resp = transceiver.sendXfrBlock(realTx)!!.data!!
if (resp.size >= 2) {
var sw1 = resp[resp.size - 2].toInt() and 0xFF
var sw2 = resp[resp.size - 1].toInt() and 0xFF
if (sw1 == 0x6C) {
realTx[realTx.size - 1] = resp[resp.size - 1]
resp = transceiver.sendXfrBlock(realTx)!!.data!!
} else if (sw1 == 0x61) {
do {
val getResponseCmd = byteArrayOf(
realTx[0], 0xC0.toByte(), 0x00, 0x00, sw2.toByte()
)
val tmp = transceiver.sendXfrBlock(getResponseCmd)!!.data!!
resp = resp.sliceArray(0 until (resp.size - 2)) + tmp
sw1 = resp[resp.size - 2].toInt() and 0xFF
sw2 = resp[resp.size - 1].toInt() and 0xFF
} while (sw1 == 0x61)
}
}
return resp
}
}

View file

@ -0,0 +1,104 @@
package im.angry.openeuicc.core.usb
import java.nio.ByteBuffer
import java.nio.ByteOrder
data class UsbCcidDescription(
private val bMaxSlotIndex: Byte,
private val bVoltageSupport: Byte,
private val dwProtocols: Int,
private val dwFeatures: Int
) {
companion object {
private const val DESCRIPTOR_LENGTH: Byte = 0x36
private const val DESCRIPTOR_TYPE: Byte = 0x21
// dwFeatures Masks
private const val FEATURE_AUTOMATIC_VOLTAGE = 0x00008
private const val FEATURE_AUTOMATIC_PPS = 0x00080
private const val FEATURE_EXCHANGE_LEVEL_TPDU = 0x10000
private const val FEATURE_EXCHANGE_LEVEL_SHORT_APDU = 0x20000
private const val FEATURE_EXCHAGE_LEVEL_EXTENDED_APDU = 0x40000
// bVoltageSupport Masks
private const val VOLTAGE_5V: Byte = 1
private const val VOLTAGE_3V: Byte = 2
private const val VOLTAGE_1_8V: Byte = 4
private const val SLOT_OFFSET = 4
private const val FEATURES_OFFSET = 40
private const val MASK_T0_PROTO = 1
private const val MASK_T1_PROTO = 2
fun fromRawDescriptors(desc: ByteArray): UsbCcidDescription? {
var dwProtocols = 0
var dwFeatures = 0
var bMaxSlotIndex: Byte = 0
var bVoltageSupport: Byte = 0
var hasCcidDescriptor = false
val byteBuffer = ByteBuffer.wrap(desc).order(ByteOrder.LITTLE_ENDIAN)
while (byteBuffer.hasRemaining()) {
byteBuffer.mark()
val len = byteBuffer.get()
val type = byteBuffer.get()
if (type == DESCRIPTOR_TYPE && len == DESCRIPTOR_LENGTH) {
byteBuffer.reset()
byteBuffer.position(byteBuffer.position() + SLOT_OFFSET)
bMaxSlotIndex = byteBuffer.get()
bVoltageSupport = byteBuffer.get()
dwProtocols = byteBuffer.int
byteBuffer.reset()
byteBuffer.position(byteBuffer.position() + FEATURES_OFFSET)
dwFeatures = byteBuffer.int
hasCcidDescriptor = true
break
} else {
byteBuffer.position(byteBuffer.position() + len - 2)
}
}
return if (hasCcidDescriptor) {
UsbCcidDescription(bMaxSlotIndex, bVoltageSupport, dwProtocols, dwFeatures)
} else {
null
}
}
}
enum class Voltage(powerOnValue: Int, mask: Int) {
AUTO(0, 0), _5V(1, VOLTAGE_5V.toInt()), _3V(2, VOLTAGE_3V.toInt()), _1_8V(
3,
VOLTAGE_1_8V.toInt()
);
val mask = powerOnValue.toByte()
val powerOnValue = mask.toByte()
}
private fun hasFeature(feature: Int): Boolean =
(dwFeatures and feature) != 0
val voltages: Array<Voltage>
get() =
if (hasFeature(FEATURE_AUTOMATIC_VOLTAGE)) {
arrayOf(Voltage.AUTO)
} else {
Voltage.values().mapNotNull {
if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) {
it
} else {
null
}
}.toTypedArray()
}
val hasAutomaticPps: Boolean
get() = hasFeature(FEATURE_AUTOMATIC_PPS)
val hasT0Protocol: Boolean
get() = (dwProtocols and MASK_T0_PROTO) != 0
}

View file

@ -0,0 +1,348 @@
package im.angry.openeuicc.core.usb
import android.hardware.usb.UsbDeviceConnection
import android.hardware.usb.UsbEndpoint
import android.os.SystemClock
import android.util.Log
import im.angry.openeuicc.util.*
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* Provides raw, APDU-agnostic transmission to the CCID reader
* Adapted from <https://github.com/open-keychain/open-keychain/blob/master/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java>
*/
class UsbCcidTransceiver(
private val usbConnection: UsbDeviceConnection,
private val usbBulkIn: UsbEndpoint,
private val usbBulkOut: UsbEndpoint,
private val usbCcidDescription: UsbCcidDescription
) {
companion object {
private const val TAG = "UsbCcidTransceiver"
private const val CCID_HEADER_LENGTH = 10
private const val MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK = 0x80
private const val MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_ON = 0x62
private const val MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_OFF = 0x63
private const val MESSAGE_TYPE_PC_TO_RDR_XFR_BLOCK = 0x6f
private const val COMMAND_STATUS_SUCCESS: Byte = 0
private const val COMMAND_STATUS_TIME_EXTENSION_RQUESTED: Byte = 2
/**
* Level Parameter: APDU is a single command.
*
* "the command APDU begins and ends with this command"
* -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
* § 6.1.1.3
*/
const val LEVEL_PARAM_START_SINGLE_CMD_APDU: Short = 0x0000
/**
* Level Parameter: First APDU in a multi-command APDU.
*
* "the command APDU begins with this command, and continue in the
* next PC_to_RDR_XfrBlock"
* -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
* § 6.1.1.3
*/
const val LEVEL_PARAM_START_MULTI_CMD_APDU: Short = 0x0001
/**
* Level Parameter: Final APDU in a multi-command APDU.
*
* "this abData field continues a command APDU and ends the command APDU"
* -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
* § 6.1.1.3
*/
const val LEVEL_PARAM_END_MULTI_CMD_APDU: Short = 0x0002
/**
* Level Parameter: Next command in a multi-command APDU.
*
* "the abData field continues a command APDU and another block is to follow"
* -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
* § 6.1.1.3
*/
const val LEVEL_PARAM_CONTINUE_MULTI_CMD_APDU: Short = 0x0003
/**
* Level Parameter: Request the device continue sending APDU.
*
* "empty abData field, continuation of response APDU is expected in the next
* RDR_to_PC_DataBlock"
* -- DWG Smart-Card USB Integrated Circuit(s) Card Devices rev 1.0
* § 6.1.1.3
*/
const val LEVEL_PARAM_CONTINUE_RESPONSE: Short = 0x0010
private const val SLOT_NUMBER = 0x00
private const val ICC_STATUS_SUCCESS: Byte = 0
private const val DEVICE_COMMUNICATE_TIMEOUT_MILLIS = 5000
private const val DEVICE_SKIP_TIMEOUT_MILLIS = 100
}
data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) :
Exception(msg)
data class CcidDataBlock(
val dwLength: Int,
val bSlot: Byte,
val bSeq: Byte,
val bStatus: Byte,
val bError: Byte,
val bChainParameter: Byte,
val data: ByteArray?
) {
companion object {
fun parseHeaderFromBytes(headerBytes: ByteArray): CcidDataBlock {
val buf = ByteBuffer.wrap(headerBytes)
buf.order(ByteOrder.LITTLE_ENDIAN)
val type = buf.get()
require(type == MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK.toByte()) { "Header has incorrect type value!" }
val dwLength = buf.int
val bSlot = buf.get()
val bSeq = buf.get()
val bStatus = buf.get()
val bError = buf.get()
val bChainParameter = buf.get()
return CcidDataBlock(dwLength, bSlot, bSeq, bStatus, bError, bChainParameter, null)
}
}
fun withData(d: ByteArray): CcidDataBlock {
require(data == null) { "Cannot add data twice" }
return CcidDataBlock(dwLength, bSlot, bSeq, bStatus, bError, bChainParameter, d)
}
val iccStatus: Byte
get() = (bStatus.toInt() and 0x03).toByte()
val commandStatus: Byte
get() = ((bStatus.toInt() shr 6) and 0x03).toByte()
val isStatusTimeoutExtensionRequest: Boolean
get() = commandStatus == COMMAND_STATUS_TIME_EXTENSION_RQUESTED
val isStatusSuccess: Boolean
get() = iccStatus == ICC_STATUS_SUCCESS && commandStatus == COMMAND_STATUS_SUCCESS
}
val hasAutomaticPps = usbCcidDescription.hasAutomaticPps
private val inputBuffer = ByteArray(usbBulkIn.maxPacketSize)
private var currentSequenceNumber: Byte = 0
private fun sendRaw(data: ByteArray, offset: Int, length: Int) {
val tr1 = usbConnection.bulkTransfer(
usbBulkOut, data, offset, length, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
)
if (tr1 != length) {
throw UsbTransportException(
"USB error - failed to transmit data ($tr1/$length)"
)
}
}
private fun receiveDataBlock(expectedSequenceNumber: Byte): CcidDataBlock? {
var response: CcidDataBlock?
do {
response = receiveDataBlockImmediate(expectedSequenceNumber)
} while (response!!.isStatusTimeoutExtensionRequest)
if (!response.isStatusSuccess) {
throw UsbCcidErrorException("USB-CCID error!", response)
}
return response
}
private fun receiveDataBlockImmediate(expectedSequenceNumber: Byte): CcidDataBlock? {
/*
* Some USB CCID devices (notably NitroKey 3) may time-out and need a subsequent poke to
* carry on communications. No particular reason why the number 3 was chosen. If we get a
* zero-sized reply (or a time-out), we try again. Clamped retries prevent an infinite loop
* if things really turn sour.
*/
var attempts = 3
Log.d(TAG, "Receive data block immediate seq=$expectedSequenceNumber")
var readBytes: Int
do {
readBytes = usbConnection.bulkTransfer(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
)
Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex())
} while (readBytes <= 0 && attempts-- > 0)
if (readBytes < CCID_HEADER_LENGTH) {
throw UsbTransportException("USB-CCID error - failed to receive CCID header")
}
if (inputBuffer[0] != MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK.toByte()) {
if (expectedSequenceNumber != inputBuffer[6]) {
throw UsbTransportException(
((("USB-CCID error - bad CCID header, type " + inputBuffer[0]) + " (expected " +
MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK) + "), sequence number " + inputBuffer[6]
) + " (expected " +
expectedSequenceNumber + ")"
)
}
throw UsbTransportException(
"USB-CCID error - bad CCID header type " + inputBuffer[0]
)
}
var result = CcidDataBlock.parseHeaderFromBytes(inputBuffer)
if (expectedSequenceNumber != result.bSeq) {
throw UsbTransportException(
("USB-CCID error - expected sequence number " +
expectedSequenceNumber + ", got " + result)
)
}
val dataBuffer = ByteArray(result.dwLength)
var bufferedBytes = readBytes - CCID_HEADER_LENGTH
System.arraycopy(inputBuffer, CCID_HEADER_LENGTH, dataBuffer, 0, bufferedBytes)
while (bufferedBytes < dataBuffer.size) {
readBytes = usbConnection.bulkTransfer(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
)
if (readBytes < 0) {
throw UsbTransportException(
"USB error - failed reading response data! Header: $result"
)
}
System.arraycopy(inputBuffer, 0, dataBuffer, bufferedBytes, readBytes)
bufferedBytes += readBytes
}
result = result.withData(dataBuffer)
return result
}
private fun skipAvailableInput() {
var ignoredBytes: Int
do {
ignoredBytes = usbConnection.bulkTransfer(
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_SKIP_TIMEOUT_MILLIS
)
if (ignoredBytes > 0) {
Log.e(TAG, "Skipped $ignoredBytes bytes")
}
} while (ignoredBytes > 0)
}
/**
* Transmits XfrBlock
* 6.1.4 PC_to_RDR_XfrBlock
*
* @param payload payload to transmit
*/
fun sendXfrBlock(payload: ByteArray): CcidDataBlock? {
return sendXfrBlock(payload, LEVEL_PARAM_START_SINGLE_CMD_APDU)
}
/**
* Receives a continued XfrBlock. Should be called when a multiblock response is indicated
* 6.1.4 PC_to_RDR_XfrBlock
*/
fun receiveContinuedResponse(): CcidDataBlock? {
return sendXfrBlock(ByteArray(0), LEVEL_PARAM_CONTINUE_RESPONSE)
}
/**
* Transmits XfrBlock
* 6.1.4 PC_to_RDR_XfrBlock
*
* @param payload payload to transmit
* @param levelParam Level parameter
*/
private fun sendXfrBlock(payload: ByteArray, levelParam: Short): CcidDataBlock? {
val startTime = SystemClock.elapsedRealtime()
val l = payload.size
val sequenceNumber: Byte = currentSequenceNumber++
val headerData = byteArrayOf(
MESSAGE_TYPE_PC_TO_RDR_XFR_BLOCK.toByte(),
l.toByte(),
(l shr 8).toByte(),
(l shr 16).toByte(),
(l shr 24).toByte(),
SLOT_NUMBER.toByte(),
sequenceNumber,
0x00.toByte(),
(levelParam.toInt() and 0x00ff).toByte(),
(levelParam.toInt() shr 8).toByte()
)
val data: ByteArray = headerData + payload
var sentBytes = 0
while (sentBytes < data.size) {
val bytesToSend = Math.min(usbBulkOut.maxPacketSize, data.size - sentBytes)
sendRaw(data, sentBytes, bytesToSend)
sentBytes += bytesToSend
}
val ccidDataBlock = receiveDataBlock(sequenceNumber)
val elapsedTime = SystemClock.elapsedRealtime() - startTime
Log.d(TAG, "USB XferBlock call took " + elapsedTime + "ms")
return ccidDataBlock
}
fun iccPowerOn(): CcidDataBlock {
val startTime = SystemClock.elapsedRealtime()
skipAvailableInput()
var response: CcidDataBlock? = null
for (v in usbCcidDescription.voltages) {
Log.v(TAG, "CCID: attempting to power on with voltage $v")
response = try {
iccPowerOnVoltage(v.powerOnValue)
} catch (e: UsbCcidErrorException) {
if (e.errorResponse.bError.toInt() == 7) { // Power select error
Log.v(TAG, "CCID: failed to power on with voltage $v")
iccPowerOff()
Log.v(TAG, "CCID: powered off")
continue
}
throw e
}
break
}
if (response == null) {
throw UsbTransportException("Couldn't power up ICC2")
}
val elapsedTime = SystemClock.elapsedRealtime() - startTime
Log.d(
TAG,
"Usb transport connected, took " + elapsedTime + "ms, ATR=" +
response.data?.encodeHex()
)
return response
}
private fun iccPowerOnVoltage(voltage: Byte): CcidDataBlock? {
val sequenceNumber = currentSequenceNumber++
val iccPowerCommand = byteArrayOf(
MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_ON.toByte(),
0x00, 0x00, 0x00, 0x00,
SLOT_NUMBER.toByte(),
sequenceNumber,
voltage,
0x00, 0x00 // reserved for future use
)
sendRaw(iccPowerCommand, 0, iccPowerCommand.size)
return receiveDataBlock(sequenceNumber)
}
private fun iccPowerOff() {
val sequenceNumber = currentSequenceNumber++
val iccPowerCommand = byteArrayOf(
MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_OFF.toByte(),
0x00, 0x00, 0x00, 0x00,
0x00,
sequenceNumber,
0x00
)
sendRaw(iccPowerCommand, 0, iccPowerCommand.size)
}
}

View file

@ -0,0 +1,36 @@
// Adapted from <https://github.com/open-keychain/open-keychain/blob/master/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb>
package im.angry.openeuicc.core.usb
import android.hardware.usb.UsbConstants
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbEndpoint
import android.hardware.usb.UsbInterface
class UsbTransportException(msg: String) : Exception(msg)
fun UsbInterface.getIoEndpoints(): Pair<UsbEndpoint?, UsbEndpoint?> {
var bulkIn: UsbEndpoint? = null
var bulkOut: UsbEndpoint? = null
for (i in 0 until endpointCount) {
val endpoint = getEndpoint(i)
if (endpoint.type != UsbConstants.USB_ENDPOINT_XFER_BULK) {
continue
}
if (endpoint.direction == UsbConstants.USB_DIR_IN) {
bulkIn = endpoint
} else if (endpoint.direction == UsbConstants.USB_DIR_OUT) {
bulkOut = endpoint
}
}
return Pair(bulkIn, bulkOut)
}
fun UsbDevice.getSmartCardInterface(): UsbInterface? {
for (i in 0 until interfaceCount) {
val anInterface = getInterface(i)
if (anInterface.interfaceClass == UsbConstants.USB_CLASS_CSCID) {
return anInterface
}
}
return null
}

View file

@ -1,6 +1,14 @@
package im.angry.openeuicc.ui package im.angry.openeuicc.ui
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.util.Log import android.util.Log
@ -12,6 +20,7 @@ import android.widget.ArrayAdapter
import android.widget.Spinner import android.widget.Spinner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import im.angry.openeuicc.common.R import im.angry.openeuicc.common.R
import im.angry.openeuicc.core.EuiccChannel
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,6 +29,7 @@ import kotlinx.coroutines.withContext
open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker { open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
companion object { companion object {
const val TAG = "MainActivity" const val TAG = "MainActivity"
const val ACTION_USB_PERMISSION = "im.angry.openeuicc.USB_PERMISSION"
} }
private lateinit var spinnerAdapter: ArrayAdapter<String> private lateinit var spinnerAdapter: ArrayAdapter<String>
@ -30,6 +40,28 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
protected lateinit var tm: TelephonyManager protected lateinit var tm: TelephonyManager
private val usbManager: UsbManager by lazy {
getSystemService(USB_SERVICE) as UsbManager
}
private var usbDevice: UsbDevice? = null
private var usbChannel: EuiccChannel? = null
private lateinit var usbPendingIntent: PendingIntent
private val usbPermissionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == ACTION_USB_PERMISSION) {
if (usbDevice != null && usbManager.hasPermission(usbDevice)) {
lifecycleScope.launch(Dispatchers.Main) {
switchToUsbFragmentIfPossible()
}
}
}
}
}
@SuppressLint("WrongConstant", "UnspecifiedRegisterReceiverFlag")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
@ -43,6 +75,15 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
tm = telephonyManager tm = telephonyManager
spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item) spinnerAdapter = ArrayAdapter<String>(this, R.layout.spinner_item)
usbPendingIntent = PendingIntent.getBroadcast(this, 0,
Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE)
val filter = IntentFilter(ACTION_USB_PERMISSION)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(usbPermissionReceiver, filter, Context.RECEIVER_EXPORTED)
} else {
registerReceiver(usbPermissionReceiver, filter)
}
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -62,8 +103,15 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
position: Int, position: Int,
id: Long id: Long
) { ) {
supportFragmentManager.beginTransaction() if (position < fragments.size) {
.replace(R.id.fragment_root, fragments[position]).commit() supportFragmentManager.beginTransaction()
.replace(R.id.fragment_root, fragments[position]).commit()
} else if (position == fragments.size) {
// If we are at the last position, this is the USB device
lifecycleScope.launch(Dispatchers.Main) {
switchToUsbFragmentIfPossible()
}
}
} }
override fun onNothingSelected(parent: AdapterView<*>?) { override fun onNothingSelected(parent: AdapterView<*>?) {
@ -106,12 +154,22 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
} }
} }
withContext(Dispatchers.IO) {
val res = euiccChannelManager.enumerateUsbEuiccChannel()
usbDevice = res.first
usbChannel = res.second
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
knownChannels.sortedBy { it.logicalSlotId }.forEach { channel -> knownChannels.sortedBy { it.logicalSlotId }.forEach { channel ->
spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId)) spinnerAdapter.add(getString(R.string.channel_name_format, channel.logicalSlotId))
fragments.add(appContainer.uiComponentFactory.createEuiccManagementFragment(channel)) fragments.add(appContainer.uiComponentFactory.createEuiccManagementFragment(channel))
} }
// If USB readers exist, add them at the very last
// The adapter logic depends on this assumption
usbDevice?.let { spinnerAdapter.add(it.productName) }
if (fragments.isNotEmpty()) { if (fragments.isNotEmpty()) {
if (this@MainActivity::spinner.isInitialized) { if (this@MainActivity::spinner.isInitialized) {
spinnerItem.isVisible = true spinnerItem.isVisible = true
@ -120,4 +178,27 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
} }
} }
} }
private suspend fun switchToUsbFragmentIfPossible() {
if (usbDevice != null && usbChannel == null) {
if (!usbManager.hasPermission(usbDevice)) {
usbManager.requestPermission(usbDevice, usbPendingIntent)
return
} else {
val (device, channel) = withContext(Dispatchers.IO) {
euiccChannelManager.enumerateUsbEuiccChannel()
}
if (device != null && channel != null) {
usbDevice = device
usbChannel = channel
}
}
}
if (usbChannel != null) {
supportFragmentManager.beginTransaction().replace(R.id.fragment_root,
appContainer.uiComponentFactory.createEuiccManagementFragment(usbChannel!!)).commit()
}
}
} }

View file

@ -180,8 +180,9 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
euiccChannel.lpa.handleNotification(notification.inner.seqNumber) euiccChannel.lpa.handleNotification(notification.inner.seqNumber)
} }
refresh()
} }
refresh()
true true
} }
R.id.notification_delete -> { R.id.notification_delete -> {
@ -189,8 +190,9 @@ class NotificationsActivity: BaseEuiccAccessActivity(), OpenEuiccContextMarker {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
euiccChannel.lpa.deleteNotification(notification.inner.seqNumber) euiccChannel.lpa.deleteNotification(notification.inner.seqNumber)
} }
refresh()
} }
refresh()
true true
} }
else -> false else -> false

View file

@ -74,11 +74,12 @@ fun SubscriptionManager.tryRefreshCachedEuiccInfo(cardId: Int) {
} }
// Every EuiccChannel we use here should be backed by a RealUiccPortInfoCompat // Every EuiccChannel we use here should be backed by a RealUiccPortInfoCompat
// except when it is from a USB card reader
val EuiccChannel.removable val EuiccChannel.removable
get() = (port as RealUiccPortInfoCompat).card.isRemovable get() = (port as? RealUiccPortInfoCompat)?.card?.isRemovable ?: true
val EuiccChannel.cardId val EuiccChannel.cardId
get() = (port as RealUiccPortInfoCompat).card.cardId get() = (port as? RealUiccPortInfoCompat)?.card?.cardId ?: -1
val EuiccChannel.isMEP val EuiccChannel.isMEP
get() = (port as RealUiccPortInfoCompat).card.isMultipleEnabledProfilesSupported get() = (port as? RealUiccPortInfoCompat)?.card?.isMultipleEnabledProfilesSupported ?: false