Compare commits
No commits in common. "ccf21675d64bd989e853b6e2fd7db6e5a572cf9c" and "6396f170128f6950562339b3d089fd86a3744eda" have entirely different histories.
ccf21675d6
...
6396f17012
11 changed files with 8 additions and 837 deletions
|
@ -1,23 +1,14 @@
|
||||||
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)
|
||||||
|
@ -45,17 +36,6 @@ 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
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
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
|
||||||
|
@ -26,18 +23,12 @@ 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
|
||||||
}
|
}
|
||||||
|
@ -47,15 +38,6 @@ 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) {
|
||||||
|
@ -91,10 +73,6 @@ 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) {
|
||||||
|
@ -110,10 +88,6 @@ 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) {
|
||||||
|
@ -126,10 +100,6 @@ 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) }
|
||||||
|
@ -145,10 +115,6 @@ 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) }
|
||||||
}
|
}
|
||||||
|
@ -196,37 +162,11 @@ 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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
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
|
||||||
|
@ -9,8 +7,6 @@ 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
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
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
|
||||||
|
@ -13,25 +11,13 @@ import android.hardware.usb.UsbDevice
|
||||||
* 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 _device internal_ sources for EuiccChannels, return them and have all
|
* Scan all possible 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.
|
||||||
|
|
|
@ -1,159 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,348 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -1,14 +1,6 @@
|
||||||
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
|
||||||
|
@ -20,7 +12,6 @@ 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
|
||||||
|
@ -29,7 +20,6 @@ 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>
|
||||||
|
@ -40,28 +30,6 @@ 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)
|
||||||
|
@ -75,15 +43,6 @@ 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 {
|
||||||
|
@ -103,15 +62,8 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
||||||
position: Int,
|
position: Int,
|
||||||
id: Long
|
id: Long
|
||||||
) {
|
) {
|
||||||
if (position < fragments.size) {
|
supportFragmentManager.beginTransaction()
|
||||||
supportFragmentManager.beginTransaction()
|
.replace(R.id.fragment_root, fragments[position]).commit()
|
||||||
.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<*>?) {
|
||||||
|
@ -154,22 +106,12 @@ 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
|
||||||
|
@ -178,27 +120,4 @@ 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -180,9 +180,8 @@ 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 -> {
|
||||||
|
@ -190,9 +189,8 @@ 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
|
||||||
|
|
|
@ -74,12 +74,11 @@ 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 ?: true
|
get() = (port as RealUiccPortInfoCompat).card.isRemovable
|
||||||
|
|
||||||
val EuiccChannel.cardId
|
val EuiccChannel.cardId
|
||||||
get() = (port as? RealUiccPortInfoCompat)?.card?.cardId ?: -1
|
get() = (port as RealUiccPortInfoCompat).card.cardId
|
||||||
|
|
||||||
val EuiccChannel.isMEP
|
val EuiccChannel.isMEP
|
||||||
get() = (port as? RealUiccPortInfoCompat)?.card?.isMultipleEnabledProfilesSupported ?: false
|
get() = (port as RealUiccPortInfoCompat).card.isMultipleEnabledProfilesSupported
|
Loading…
Add table
Reference in a new issue