feat: detect use product
This commit is contained in:
parent
03bfdf373c
commit
a4598b8b10
24 changed files with 302 additions and 220 deletions
6
.idea/AndroidProjectSystem.xml
generated
Normal file
6
.idea/AndroidProjectSystem.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
|
@ -4,6 +4,14 @@
|
|||
<selectionStates>
|
||||
<SelectionState runConfigName="app-unpriv">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-02-26T05:33:18.646829Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=44161JEKB15351" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
|
|
|
@ -8,7 +8,8 @@ import android.se.omapi.SEService
|
|||
import android.util.Log
|
||||
import im.angry.openeuicc.common.R
|
||||
import im.angry.openeuicc.core.usb.UsbApduInterface
|
||||
import im.angry.openeuicc.core.usb.getIoEndpoints
|
||||
import im.angry.openeuicc.core.usb.bulkPair
|
||||
import im.angry.openeuicc.core.usb.endpoints
|
||||
import im.angry.openeuicc.util.*
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
|
@ -61,7 +62,7 @@ open class DefaultEuiccChannelFactory(protected val context: Context) : EuiccCha
|
|||
}
|
||||
|
||||
override fun tryOpenUsbEuiccChannel(usbDevice: UsbDevice, usbInterface: UsbInterface): EuiccChannel? {
|
||||
val (bulkIn, bulkOut) = usbInterface.getIoEndpoints()
|
||||
val (bulkIn, bulkOut) = usbInterface.endpoints.bulkPair
|
||||
if (bulkIn == null || bulkOut == null) return null
|
||||
val conn = usbManager.openDevice(usbDevice) ?: return null
|
||||
if (!conn.claimInterface(usbInterface, true)) return null
|
||||
|
|
|
@ -5,7 +5,8 @@ import android.hardware.usb.UsbDevice
|
|||
import android.hardware.usb.UsbManager
|
||||
import android.telephony.SubscriptionManager
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.core.usb.getSmartCardInterface
|
||||
import im.angry.openeuicc.core.usb.smartCard
|
||||
import im.angry.openeuicc.core.usb.interfaces
|
||||
import im.angry.openeuicc.di.AppContainer
|
||||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -244,7 +245,7 @@ open class DefaultEuiccChannelManager(
|
|||
withContext(Dispatchers.IO) {
|
||||
usbManager.deviceList.values.forEach { device ->
|
||||
Log.i(TAG, "Scanning USB device ${device.deviceId}:${device.vendorId}")
|
||||
val iface = device.getSmartCardInterface() ?: return@forEach
|
||||
val iface = device.interfaces.smartCard ?: 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, false)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import im.angry.openeuicc.util.*
|
||||
import im.angry.openeuicc.vendored.ESTKmeInfo
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
||||
interface EuiccChannel {
|
||||
|
@ -28,5 +30,10 @@ interface EuiccChannel {
|
|||
*/
|
||||
val intrinsicChannelName: String?
|
||||
|
||||
/**
|
||||
* The underlying APDU interface for this channel
|
||||
*/
|
||||
val apduInterface: ApduInterface
|
||||
|
||||
fun close()
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import im.angry.openeuicc.util.*
|
||||
import im.angry.openeuicc.util.UiccPortInfoCompat
|
||||
import im.angry.openeuicc.util.decodeHex
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
@ -11,7 +12,7 @@ class EuiccChannelImpl(
|
|||
override val type: String,
|
||||
override val port: UiccPortInfoCompat,
|
||||
override val intrinsicChannelName: String?,
|
||||
private val apduInterface: ApduInterface,
|
||||
override val apduInterface: ApduInterface,
|
||||
verboseLoggingFlow: Flow<Boolean>,
|
||||
ignoreTLSCertificateFlow: Flow<Boolean>
|
||||
) : EuiccChannel {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package im.angry.openeuicc.core
|
||||
|
||||
import im.angry.openeuicc.util.*
|
||||
import net.typeblog.lpac_jni.LocalProfileAssistant
|
||||
|
||||
class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
||||
class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel by orig {
|
||||
private var _inner: EuiccChannel? = orig
|
||||
|
||||
private val channel: EuiccChannel
|
||||
|
@ -15,28 +14,11 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
|
|||
return _inner!!
|
||||
}
|
||||
|
||||
override val type: String
|
||||
get() = channel.type
|
||||
override val port: UiccPortInfoCompat
|
||||
get() = channel.port
|
||||
override val slotId: Int
|
||||
get() = channel.slotId
|
||||
override val logicalSlotId: Int
|
||||
get() = channel.logicalSlotId
|
||||
override val portId: Int
|
||||
get() = channel.portId
|
||||
private val lpaDelegate = lazy {
|
||||
LocalProfileAssistantWrapper(channel.lpa)
|
||||
}
|
||||
override val lpa: LocalProfileAssistant by lpaDelegate
|
||||
override val valid: Boolean
|
||||
get() = channel.valid
|
||||
override val intrinsicChannelName: String?
|
||||
get() = channel.intrinsicChannelName
|
||||
override val atr: ByteArray?
|
||||
get() = channel.atr
|
||||
|
||||
override fun close() = channel.close()
|
||||
override val lpa: LocalProfileAssistant by lpaDelegate
|
||||
|
||||
fun invalidateWrapper() {
|
||||
_inner = null
|
||||
|
|
|
@ -7,9 +7,9 @@ import android.util.Log
|
|||
import im.angry.openeuicc.util.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.single
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class OmapiApduInterface(
|
||||
private val service: SEService,
|
||||
|
@ -21,7 +21,8 @@ class OmapiApduInterface(
|
|||
}
|
||||
|
||||
private lateinit var session: Session
|
||||
private lateinit var lastChannel: Channel
|
||||
private val channels = mutableMapOf<Int, Channel>()
|
||||
private val index = AtomicInteger(0)
|
||||
|
||||
override val valid: Boolean
|
||||
get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
|
||||
|
@ -38,23 +39,25 @@ class OmapiApduInterface(
|
|||
}
|
||||
|
||||
override fun logicalChannelOpen(aid: ByteArray): Int {
|
||||
check(!this::lastChannel.isInitialized) {
|
||||
"Can only open one channel"
|
||||
}
|
||||
lastChannel = session.openLogicalChannel(aid)!!
|
||||
return 1
|
||||
val channel = session.openLogicalChannel(aid)!!
|
||||
val id = index.addAndGet(1)
|
||||
channels[id] = channel
|
||||
return id
|
||||
}
|
||||
|
||||
override fun logicalChannelClose(handle: Int) {
|
||||
check(handle == 1 && !this::lastChannel.isInitialized) {
|
||||
"Unknown channel"
|
||||
val channel = channels[handle]
|
||||
check(channel != null) {
|
||||
"Invalid logical channel handle $handle"
|
||||
}
|
||||
lastChannel.close()
|
||||
channels.remove(handle)
|
||||
channel.close()
|
||||
}
|
||||
|
||||
override fun transmit(tx: ByteArray): ByteArray {
|
||||
check(this::lastChannel.isInitialized) {
|
||||
"Unknown channel"
|
||||
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
|
||||
val channel = channels[handle]
|
||||
check(channel != null) {
|
||||
"Invalid logical channel handle $handle"
|
||||
}
|
||||
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
|
@ -63,7 +66,7 @@ class OmapiApduInterface(
|
|||
|
||||
try {
|
||||
for (i in 0..10) {
|
||||
val res = lastChannel.transmit(tx)
|
||||
val res = channel.transmit(tx)
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}")
|
||||
}
|
||||
|
|
|
@ -21,10 +21,13 @@ class UsbApduInterface(
|
|||
private lateinit var ccidDescription: UsbCcidDescription
|
||||
private lateinit var transceiver: UsbCcidTransceiver
|
||||
|
||||
private var channelId = -1
|
||||
|
||||
override var atr: ByteArray? = null
|
||||
|
||||
override val valid: Boolean
|
||||
get() = channels.isNotEmpty()
|
||||
|
||||
private var channels = mutableSetOf<Int>()
|
||||
|
||||
override fun connect() {
|
||||
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
|
||||
|
||||
|
@ -46,11 +49,11 @@ class UsbApduInterface(
|
|||
|
||||
override fun disconnect() {
|
||||
conn.close()
|
||||
|
||||
atr = null
|
||||
}
|
||||
|
||||
override fun logicalChannelOpen(aid: ByteArray): Int {
|
||||
check(channelId == -1) { "Logical channel already opened" }
|
||||
|
||||
// OPEN LOGICAL CHANNEL
|
||||
val req = manageChannelCmd(true, 0)
|
||||
|
||||
|
@ -66,7 +69,7 @@ class UsbApduInterface(
|
|||
return -1
|
||||
}
|
||||
|
||||
channelId = resp[0].toInt()
|
||||
val channelId = resp[0].toInt()
|
||||
Log.d(TAG, "channelId = $channelId")
|
||||
|
||||
// Then, select AID
|
||||
|
@ -78,32 +81,32 @@ class UsbApduInterface(
|
|||
return -1
|
||||
}
|
||||
|
||||
channels.add(channelId)
|
||||
|
||||
return channelId
|
||||
}
|
||||
|
||||
override fun logicalChannelClose(handle: Int) {
|
||||
check(handle == channelId) { "Logical channel ID mismatch" }
|
||||
check(channelId != -1) { "Logical channel is not opened" }
|
||||
|
||||
check(channels.contains(handle)) {
|
||||
"Invalid logical channel handle $handle"
|
||||
}
|
||||
// CLOSE LOGICAL CHANNEL
|
||||
val req = manageChannelCmd(false, channelId.toByte())
|
||||
val resp = transmitApduByChannel(req, channelId.toByte())
|
||||
val req = manageChannelCmd(false, handle.toByte())
|
||||
val resp = transmitApduByChannel(req, handle.toByte())
|
||||
|
||||
if (!isSuccessResponse(resp)) {
|
||||
Log.d(TAG, "CLOSE LOGICAL CHANNEL failed: ${resp.encodeHex()}")
|
||||
}
|
||||
|
||||
channelId = -1
|
||||
channels.remove(handle)
|
||||
}
|
||||
|
||||
override fun transmit(tx: ByteArray): ByteArray {
|
||||
check(channelId != -1) { "Logical channel is not opened" }
|
||||
return transmitApduByChannel(tx, channelId.toByte())
|
||||
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
|
||||
check(channels.contains(handle)) {
|
||||
"Invalid logical channel handle $handle"
|
||||
}
|
||||
return transmitApduByChannel(tx, handle.toByte())
|
||||
}
|
||||
|
||||
override val valid: Boolean
|
||||
get() = channelId != -1
|
||||
|
||||
private fun isSuccessResponse(resp: ByteArray): Boolean =
|
||||
resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte()
|
||||
|
||||
|
|
|
@ -20,12 +20,12 @@ data class UsbCcidDescription(
|
|||
|
||||
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
|
||||
private const val FEATURE_EXCHANGE_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 VOLTAGE_5V0: Byte = 1
|
||||
private const val VOLTAGE_3V0: Byte = 2
|
||||
private const val VOLTAGE_1V8: Byte = 4
|
||||
|
||||
private const val SLOT_OFFSET = 4
|
||||
private const val FEATURES_OFFSET = 40
|
||||
|
@ -71,10 +71,12 @@ data class UsbCcidDescription(
|
|||
}
|
||||
|
||||
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()
|
||||
);
|
||||
// @formatter:off
|
||||
AUTO(0, 0),
|
||||
V50(1, VOLTAGE_5V0.toInt()),
|
||||
V30(2, VOLTAGE_3V0.toInt()),
|
||||
V18(3, VOLTAGE_1V8.toInt());
|
||||
// @formatter:on
|
||||
|
||||
val mask = powerOnValue.toByte()
|
||||
val powerOnValue = mask.toByte()
|
||||
|
@ -84,18 +86,14 @@ data class UsbCcidDescription(
|
|||
(dwFeatures and feature) != 0
|
||||
|
||||
val voltages: Array<Voltage>
|
||||
get() =
|
||||
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()
|
||||
return arrayOf(Voltage.AUTO)
|
||||
}
|
||||
return Voltage.entries
|
||||
.mapNotNull { if ((it.mask.toInt() and bVoltageSupport.toInt()) != 0) it else null }
|
||||
.toTypedArray()
|
||||
}
|
||||
|
||||
val hasAutomaticPps: Boolean
|
||||
get() = hasFeature(FEATURE_AUTOMATIC_PPS)
|
||||
|
|
|
@ -95,6 +95,7 @@ class UsbCcidTransceiver(
|
|||
data class UsbCcidErrorException(val msg: String, val errorResponse: CcidDataBlock) :
|
||||
Exception(msg)
|
||||
|
||||
@Suppress("ArrayInDataClass")
|
||||
data class CcidDataBlock(
|
||||
val dwLength: Int,
|
||||
val bSlot: Byte,
|
||||
|
@ -183,31 +184,26 @@ class UsbCcidTransceiver(
|
|||
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
|
||||
)
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "Received " + readBytes + " bytes: " + inputBuffer.encodeHex())
|
||||
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]
|
||||
)
|
||||
throw UsbTransportException(buildString {
|
||||
append("USB-CCID error - bad CCID header")
|
||||
append(", type ")
|
||||
append("%d (expected %d)".format(inputBuffer[0], MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK))
|
||||
if (expectedSequenceNumber != inputBuffer[6]) {
|
||||
append(", sequence number ")
|
||||
append("%d (expected %d)".format(inputBuffer[6], expectedSequenceNumber))
|
||||
}
|
||||
})
|
||||
}
|
||||
var result = CcidDataBlock.parseHeaderFromBytes(inputBuffer)
|
||||
if (expectedSequenceNumber != result.bSeq) {
|
||||
throw UsbTransportException(
|
||||
("USB-CCID error - expected sequence number " +
|
||||
expectedSequenceNumber + ", got " + result)
|
||||
)
|
||||
throw UsbTransportException("USB-CCID error - expected sequence number $expectedSequenceNumber, got $result")
|
||||
}
|
||||
|
||||
val dataBuffer = ByteArray(result.dwLength)
|
||||
|
@ -218,9 +214,7 @@ class UsbCcidTransceiver(
|
|||
usbBulkIn, inputBuffer, inputBuffer.size, DEVICE_COMMUNICATE_TIMEOUT_MILLIS
|
||||
)
|
||||
if (readBytes < 0) {
|
||||
throw UsbTransportException(
|
||||
"USB error - failed reading response data! Header: $result"
|
||||
)
|
||||
throw UsbTransportException("USB error - failed reading response data! Header: $result")
|
||||
}
|
||||
System.arraycopy(inputBuffer, 0, dataBuffer, bufferedBytes, readBytes)
|
||||
bufferedBytes += readBytes
|
||||
|
@ -285,7 +279,7 @@ class UsbCcidTransceiver(
|
|||
}
|
||||
val ccidDataBlock = receiveDataBlock(sequenceNumber)
|
||||
val elapsedTime = SystemClock.elapsedRealtime() - startTime
|
||||
Log.d(TAG, "USB XferBlock call took " + elapsedTime + "ms")
|
||||
Log.d(TAG, "USB XferBlock call took ${elapsedTime}ms")
|
||||
return ccidDataBlock
|
||||
}
|
||||
|
||||
|
@ -314,8 +308,11 @@ class UsbCcidTransceiver(
|
|||
val elapsedTime = SystemClock.elapsedRealtime() - startTime
|
||||
Log.d(
|
||||
TAG,
|
||||
"Usb transport connected, took " + elapsedTime + "ms, ATR=" +
|
||||
response.data?.encodeHex()
|
||||
buildString {
|
||||
append("Usb transport connected")
|
||||
append(", took ", elapsedTime, "ms")
|
||||
append(", ATR=", response.data?.encodeHex())
|
||||
}
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -6,31 +6,22 @@ import android.hardware.usb.UsbDevice
|
|||
import android.hardware.usb.UsbEndpoint
|
||||
import android.hardware.usb.UsbInterface
|
||||
|
||||
class UsbTransportException(msg: String) : Exception(msg)
|
||||
class UsbTransportException(message: String) : Exception(message)
|
||||
|
||||
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)
|
||||
}
|
||||
val UsbDevice.interfaces: Iterable<UsbInterface>
|
||||
get() = (0 until interfaceCount).map(::getInterface)
|
||||
|
||||
fun UsbDevice.getSmartCardInterface(): UsbInterface? {
|
||||
for (i in 0 until interfaceCount) {
|
||||
val anInterface = getInterface(i)
|
||||
if (anInterface.interfaceClass == UsbConstants.USB_CLASS_CSCID) {
|
||||
return anInterface
|
||||
}
|
||||
val Iterable<UsbInterface>.smartCard: UsbInterface?
|
||||
get() = find { it.interfaceClass == UsbConstants.USB_CLASS_CSCID }
|
||||
|
||||
val UsbInterface.endpoints: Iterable<UsbEndpoint>
|
||||
get() = (0 until endpointCount).map(::getEndpoint)
|
||||
|
||||
val Iterable<UsbEndpoint>.bulkPair: Pair<UsbEndpoint?, UsbEndpoint?>
|
||||
get() {
|
||||
val endpoints = filter { it.type == UsbConstants.USB_ENDPOINT_XFER_BULK }
|
||||
return Pair(
|
||||
endpoints.find { it.direction == UsbConstants.USB_DIR_IN },
|
||||
endpoints.find { it.direction == UsbConstants.USB_DIR_OUT },
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -23,6 +23,8 @@ import im.angry.openeuicc.common.R
|
|||
import im.angry.openeuicc.core.EuiccChannel
|
||||
import im.angry.openeuicc.core.EuiccChannelManager
|
||||
import im.angry.openeuicc.util.*
|
||||
import im.angry.openeuicc.vendored.getESTKmeInfo
|
||||
import im.angry.openeuicc.vendored.getSIMLinkVersion
|
||||
import kotlinx.coroutines.launch
|
||||
import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
|
||||
import net.typeblog.lpac_jni.impl.PKID_GSMA_TEST_CI
|
||||
|
@ -100,24 +102,22 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
|
||||
private fun buildEuiccInfoItems(channel: EuiccChannel) = buildList {
|
||||
add(Item(R.string.euicc_info_access_mode, channel.type))
|
||||
add(
|
||||
Item(
|
||||
R.string.euicc_info_removable,
|
||||
formatByBoolean(channel.port.card.isRemovable, YES_NO)
|
||||
)
|
||||
)
|
||||
add(
|
||||
Item(
|
||||
R.string.euicc_info_eid,
|
||||
channel.lpa.eID,
|
||||
copiedToastResId = R.string.toast_eid_copied
|
||||
)
|
||||
)
|
||||
add(Item(R.string.euicc_info_removable, formatByBoolean(channel.port.card.isRemovable, YES_NO)))
|
||||
add(Item(R.string.euicc_info_eid, channel.lpa.eID, copiedToastResId = R.string.toast_eid_copied))
|
||||
getESTKmeInfo(channel.apduInterface)?.let {
|
||||
add(Item(R.string.euicc_info_sku, it.skuName))
|
||||
add(Item(R.string.euicc_info_sn, it.serialNumber, copiedToastResId = R.string.toast_sn_copied))
|
||||
add(Item(R.string.euicc_info_bl_ver, it.bootloaderVersion))
|
||||
add(Item(R.string.euicc_info_fw_ver, it.firmwareVersion))
|
||||
}
|
||||
getSIMLinkVersion(channel.lpa.eID, channel.lpa.euiccInfo2?.euiccFirmwareVersion)?.let {
|
||||
add(Item(R.string.euicc_info_sku, "9eSIM $it"))
|
||||
}
|
||||
channel.lpa.euiccInfo2.let { info ->
|
||||
add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version))
|
||||
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion))
|
||||
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion))
|
||||
add(Item(R.string.euicc_info_pp_version, info?.ppVersion))
|
||||
add(Item(R.string.euicc_info_sgp22_version, info?.sgp22Version.toString()))
|
||||
add(Item(R.string.euicc_info_firmware_version, info?.euiccFirmwareVersion.toString()))
|
||||
add(Item(R.string.euicc_info_globalplatform_version, info?.globalPlatformVersion.toString()))
|
||||
add(Item(R.string.euicc_info_pp_version, info?.ppVersion.toString()))
|
||||
add(Item(R.string.euicc_info_sas_accreditation_number, info?.sasAccreditationNumber))
|
||||
add(Item(R.string.euicc_info_free_nvram, info?.freeNvram?.let(::formatFreeSpace)))
|
||||
}
|
||||
|
@ -134,13 +134,8 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
|
|||
}
|
||||
add(Item(R.string.euicc_info_ci_type, getString(resId)))
|
||||
}
|
||||
add(
|
||||
Item(
|
||||
R.string.euicc_info_atr,
|
||||
channel.atr?.encodeHex() ?: getString(R.string.information_unavailable),
|
||||
copiedToastResId = R.string.toast_atr_copied,
|
||||
)
|
||||
)
|
||||
val atr = channel.atr?.encodeHex() ?: getString(R.string.information_unavailable)
|
||||
add(Item(R.string.euicc_info_atr, atr, copiedToastResId = R.string.toast_atr_copied))
|
||||
}
|
||||
|
||||
private fun formatByBoolean(b: Boolean, res: Pair<Int, Int>): String =
|
||||
|
|
|
@ -12,13 +12,10 @@ fun String.decodeHex(): ByteArray {
|
|||
return out
|
||||
}
|
||||
|
||||
fun ByteArray.encodeHex(): String {
|
||||
val sb = StringBuilder()
|
||||
val length = size
|
||||
for (i in 0 until length) {
|
||||
sb.append(String.format("%02X", this[i]))
|
||||
fun ByteArray.encodeHex(): String = buildString {
|
||||
for (element in this@encodeHex) {
|
||||
append(String.format("%02X", element))
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun formatFreeSpace(size: Int): String =
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
package im.angry.openeuicc.vendored
|
||||
|
||||
import android.util.Log
|
||||
import im.angry.openeuicc.util.TAG
|
||||
import im.angry.openeuicc.util.decodeHex
|
||||
import net.typeblog.lpac_jni.ApduInterface
|
||||
|
||||
data class ESTKmeInfo(
|
||||
val serialNumber: String?,
|
||||
val bootloaderVersion: String?,
|
||||
val firmwareVersion: String?,
|
||||
val skuName: String?,
|
||||
)
|
||||
|
||||
fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? {
|
||||
fun decode(b: ByteArray): String? {
|
||||
if (b.size < 2) return null
|
||||
if (b[b.size - 2] != 0x90.toByte() || b[b.size - 1] != 0x00.toByte()) return null
|
||||
return b.sliceArray(0 until b.size - 2).decodeToString()
|
||||
}
|
||||
return try {
|
||||
iface.openChannel("A06573746B6D65FFFFFFFFFFFF6D6774".decodeHex()) {
|
||||
fun invoke(p1: Byte) = decode(it.transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00)))
|
||||
ESTKmeInfo(
|
||||
invoke(0x00), // serial number
|
||||
invoke(0x01), // bootloader version
|
||||
invoke(0x02), // firmware version
|
||||
invoke(0x03), // sku name
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Failed to get ESTKmeInfo", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package im.angry.openeuicc.vendored
|
||||
|
||||
import net.typeblog.lpac_jni.Version
|
||||
|
||||
private val prefix = Regex("^89044045(84|21)67274948") // SIMLink EID prefix
|
||||
|
||||
fun getSIMLinkVersion(eid: String, version: Version?): String? {
|
||||
if (version == null || prefix.find(eid, 0) == null) return null
|
||||
return when {
|
||||
// @formatter:off
|
||||
version >= Version(36, 18, 5) -> "v3 (final)"
|
||||
version >= Version(36, 17, 39) -> "v3 (beta)"
|
||||
version >= Version(36, 17, 4) -> "v2s"
|
||||
version >= Version(36, 9, 3) -> "v2.1"
|
||||
version >= Version(36, 7, 2) -> "v2"
|
||||
// @formatter:on
|
||||
else -> null
|
||||
}
|
||||
}
|
|
@ -31,6 +31,7 @@
|
|||
<string name="toast_profile_enable_failed">Cannot switch to new eSIM profile.</string>
|
||||
<string name="toast_profile_delete_confirm_text_mismatched">Confirmation string mismatch</string>
|
||||
<string name="toast_iccid_copied">ICCID copied to clipboard</string>
|
||||
<string name="toast_sn_copied">Serial Number copied to clipboard</string>
|
||||
<string name="toast_eid_copied">EID copied to clipboard</string>
|
||||
<string name="toast_atr_copied">ATR copied to clipboard</string>
|
||||
|
||||
|
@ -125,6 +126,10 @@
|
|||
<string name="euicc_info_activity_title">eUICC Info (%s)</string>
|
||||
<string name="euicc_info_access_mode">Access Mode</string>
|
||||
<string name="euicc_info_removable">Removable</string>
|
||||
<string name="euicc_info_sku">Product Name</string>
|
||||
<string name="euicc_info_sn">Product Serial Number</string>
|
||||
<string name="euicc_info_bl_ver">Product Bootloader Version</string>
|
||||
<string name="euicc_info_fw_ver">Product Firmware Version</string>
|
||||
<string name="euicc_info_eid" translatable="false">EID</string>
|
||||
<string name="euicc_info_sgp22_version">SGP.22 Version</string>
|
||||
<string name="euicc_info_firmware_version">eUICC OS Version</string>
|
||||
|
|
|
@ -18,12 +18,10 @@ class TelephonyManagerApduInterface(
|
|||
const val TAG = "TelephonyManagerApduInterface"
|
||||
}
|
||||
|
||||
private var lastChannel: Int = -1
|
||||
|
||||
override val valid: Boolean
|
||||
// TelephonyManager channels will never become truly "invalid",
|
||||
// just that transactions might return errors or nonsense
|
||||
get() = lastChannel != -1
|
||||
get() = channels.isNotEmpty()
|
||||
|
||||
private var channels = mutableSetOf<Int>()
|
||||
|
||||
override fun connect() {
|
||||
// Do nothing
|
||||
|
@ -31,52 +29,39 @@ class TelephonyManagerApduInterface(
|
|||
|
||||
override fun disconnect() {
|
||||
// Do nothing
|
||||
lastChannel = -1
|
||||
}
|
||||
|
||||
override fun logicalChannelOpen(aid: ByteArray): Int {
|
||||
check(lastChannel == -1) { "Already initialized" }
|
||||
val hex = aid.encodeHex()
|
||||
val channel = tm.iccOpenLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, hex, 0)
|
||||
if (channel.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR || channel.channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
|
||||
throw IllegalArgumentException("Cannot open logical channel $hex via TelephonManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
|
||||
throw IllegalArgumentException("Cannot open logical channel $hex via TelephonyManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
|
||||
}
|
||||
lastChannel = channel.channel
|
||||
return lastChannel
|
||||
channels.add(channel.channel)
|
||||
return channel.channel
|
||||
}
|
||||
|
||||
override fun logicalChannelClose(handle: Int) {
|
||||
check(handle == lastChannel) { "Invalid channel handle " }
|
||||
check(channels.contains(handle)) {
|
||||
"Invalid logical channel handle $handle"
|
||||
}
|
||||
tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
|
||||
lastChannel = -1
|
||||
channels.remove(handle)
|
||||
}
|
||||
|
||||
override fun transmit(tx: ByteArray): ByteArray {
|
||||
check(lastChannel != -1) { "Uninitialized" }
|
||||
|
||||
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
|
||||
check(channels.contains(handle)) {
|
||||
"Invalid logical channel handle $handle"
|
||||
}
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}")
|
||||
}
|
||||
|
||||
val cla = tx[0].toUByte().toInt()
|
||||
val instruction = tx[1].toUByte().toInt()
|
||||
val p1 = tx[2].toUByte().toInt()
|
||||
val p2 = tx[3].toUByte().toInt()
|
||||
val p3 = tx[4].toUByte().toInt()
|
||||
val p4 = tx.drop(5).toByteArray().encodeHex()
|
||||
|
||||
return tm.iccTransmitApduLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, lastChannel,
|
||||
cla,
|
||||
instruction,
|
||||
p1,
|
||||
p2,
|
||||
p3,
|
||||
p4
|
||||
).also {
|
||||
if (runBlocking { verboseLoggingFlow.first() }) {
|
||||
Log.d(TAG, "TelephonyManager APDU response: $it")
|
||||
}
|
||||
}?.decodeHex() ?: byteArrayOf()
|
||||
val result = tm.iccTransmitApduLogicalChannelByPortCompat(
|
||||
port.card.physicalSlotIndex, port.portIndex, handle,
|
||||
tx,
|
||||
)
|
||||
if (runBlocking { verboseLoggingFlow.first() })
|
||||
Log.d(TAG, "TelephonyManager APDU response: $result")
|
||||
return result?.decodeHex() ?: byteArrayOf()
|
||||
}
|
||||
|
||||
}
|
|
@ -111,15 +111,26 @@ fun TelephonyManager.iccCloseLogicalChannelByPortCompat(
|
|||
}
|
||||
|
||||
fun TelephonyManager.iccTransmitApduLogicalChannelByPortCompat(
|
||||
slotIndex: Int, portIndex: Int, channel: Int,
|
||||
cla: Int, inst: Int, p1: Int, p2: Int, p3: Int, data: String?
|
||||
): String? =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
slotIndex: Int,
|
||||
portIndex: Int,
|
||||
channel: Int,
|
||||
tx: ByteArray
|
||||
): String? {
|
||||
val cla = tx[0].toUByte().toInt()
|
||||
val ins = tx[1].toUByte().toInt()
|
||||
val p1 = tx[2].toUByte().toInt()
|
||||
val p2 = tx[3].toUByte().toInt()
|
||||
val p3 = tx[4].toUByte().toInt()
|
||||
val p4 = tx.drop(5).toByteArray().encodeHex()
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
iccTransmitApduLogicalChannelByPort(
|
||||
slotIndex, portIndex, channel, cla, inst, p1, p2, p3, data
|
||||
slotIndex, portIndex, channel,
|
||||
cla, ins, p1, p2, p3, p4
|
||||
)
|
||||
} else {
|
||||
iccTransmitApduLogicalChannelBySlot(
|
||||
slotIndex, channel, cla, inst, p1, p2, p3, data
|
||||
slotIndex, channel,
|
||||
cla, ins, p1, p2, p3, p4
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ interface ApduInterface {
|
|||
fun disconnect()
|
||||
fun logicalChannelOpen(aid: ByteArray): Int
|
||||
fun logicalChannelClose(handle: Int)
|
||||
fun transmit(tx: ByteArray): ByteArray
|
||||
fun transmit(handle: Int, tx: ByteArray): ByteArray
|
||||
|
||||
/**
|
||||
* Is this APDU connection still valid?
|
||||
|
@ -16,4 +16,19 @@ interface ApduInterface {
|
|||
* callers should further check with the LPA to fully determine the validity of a channel
|
||||
*/
|
||||
val valid: Boolean
|
||||
|
||||
fun <T> openChannel(aid: ByteArray, callback: (TransmitProvider) -> T): T {
|
||||
val handle = logicalChannelOpen(aid)
|
||||
return try {
|
||||
callback(object : TransmitProvider {
|
||||
override fun transmit(tx: ByteArray) = transmit(handle, tx)
|
||||
})
|
||||
} finally {
|
||||
logicalChannelClose(handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TransmitProvider {
|
||||
fun transmit(tx: ByteArray): ByteArray
|
||||
}
|
|
@ -2,14 +2,31 @@ package net.typeblog.lpac_jni
|
|||
|
||||
/* Corresponds to EuiccInfo2 in SGP.22 */
|
||||
data class EuiccInfo2(
|
||||
val sgp22Version: String,
|
||||
val profileVersion: String,
|
||||
val euiccFirmwareVersion: String,
|
||||
val globalPlatformVersion: String,
|
||||
val sgp22Version: Version,
|
||||
val profileVersion: Version,
|
||||
val euiccFirmwareVersion: Version,
|
||||
val globalPlatformVersion: Version,
|
||||
val sasAccreditationNumber: String,
|
||||
val ppVersion: String,
|
||||
val ppVersion: Version,
|
||||
val freeNvram: Int,
|
||||
val freeRam: Int,
|
||||
val euiccCiPKIdListForSigning: Array<String>,
|
||||
val euiccCiPKIdListForVerification: Array<String>,
|
||||
)
|
||||
val euiccCiPKIdListForSigning: Set<String>,
|
||||
val euiccCiPKIdListForVerification: Set<String>,
|
||||
)
|
||||
|
||||
data class Version(
|
||||
val major: Int,
|
||||
val minor: Int,
|
||||
val patch: Int,
|
||||
) {
|
||||
constructor(version: String) : this(version.split('.').map(String::toInt))
|
||||
private constructor(parts: List<Int>) : this(parts[0], parts[1], parts[2])
|
||||
|
||||
operator fun compareTo(other: Version): Int {
|
||||
if (major != other.major) return major - other.major
|
||||
if (minor != other.minor) return minor - other.minor
|
||||
return patch - other.patch
|
||||
}
|
||||
|
||||
override fun toString() = "$major.$minor.$patch"
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import net.typeblog.lpac_jni.LocalProfileAssistant
|
|||
import net.typeblog.lpac_jni.LocalProfileInfo
|
||||
import net.typeblog.lpac_jni.LocalProfileNotification
|
||||
import net.typeblog.lpac_jni.ProfileDownloadCallback
|
||||
import net.typeblog.lpac_jni.Version
|
||||
|
||||
class LocalProfileAssistantImpl(
|
||||
isdrAid: ByteArray,
|
||||
|
@ -28,9 +29,9 @@ class LocalProfileAssistantImpl(
|
|||
var lastApduResponse: ByteArray? = null
|
||||
var lastApduException: Exception? = null
|
||||
|
||||
override fun transmit(tx: ByteArray): ByteArray =
|
||||
override fun transmit(handle: Int, tx: ByteArray): ByteArray =
|
||||
try {
|
||||
apduInterface.transmit(tx).also {
|
||||
apduInterface.transmit(handle, tx).also {
|
||||
lastApduException = null
|
||||
lastApduResponse = it
|
||||
}
|
||||
|
@ -84,8 +85,8 @@ class LocalProfileAssistantImpl(
|
|||
throw IllegalArgumentException("Failed to initialize LPA")
|
||||
}
|
||||
|
||||
val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: arrayOf()
|
||||
httpInterface.usePublicKeyIds(pkids)
|
||||
val pkids = euiccInfo2?.euiccCiPKIdListForVerification ?: setOf()
|
||||
httpInterface.usePublicKeyIds(pkids.toTypedArray())
|
||||
}
|
||||
|
||||
override fun setEs10xMss(mss: Byte) {
|
||||
|
@ -172,16 +173,16 @@ class LocalProfileAssistantImpl(
|
|||
}
|
||||
|
||||
val ret = EuiccInfo2(
|
||||
LpacJni.euiccInfo2GetSGP22Version(cInfo),
|
||||
LpacJni.euiccInfo2GetProfileVersion(cInfo),
|
||||
LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo),
|
||||
LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo),
|
||||
Version(LpacJni.euiccInfo2GetSGP22Version(cInfo)),
|
||||
Version(LpacJni.euiccInfo2GetProfileVersion(cInfo)),
|
||||
Version(LpacJni.euiccInfo2GetEuiccFirmwareVersion(cInfo)),
|
||||
Version(LpacJni.euiccInfo2GetGlobalPlatformVersion(cInfo)),
|
||||
LpacJni.euiccInfo2GetSasAcreditationNumber(cInfo),
|
||||
LpacJni.euiccInfo2GetPpVersion(cInfo),
|
||||
Version(LpacJni.euiccInfo2GetPpVersion(cInfo)),
|
||||
LpacJni.euiccInfo2GetFreeNonVolatileMemory(cInfo).toInt(),
|
||||
LpacJni.euiccInfo2GetFreeVolatileMemory(cInfo).toInt(),
|
||||
euiccCiPKIdListForSigning.toTypedArray(),
|
||||
euiccCiPKIdListForVerification.toTypedArray()
|
||||
euiccCiPKIdListForSigning.toTypedArray().toSet(),
|
||||
euiccCiPKIdListForVerification.toTypedArray().toSet(),
|
||||
)
|
||||
|
||||
LpacJni.euiccInfo2Free(cInfo)
|
||||
|
|
|
@ -14,7 +14,7 @@ const val DEFAULT_PKID_GSMA_RSP2_ROOT_CI1 = "81370f5125d0b1d408d4c3b232e6d25e795
|
|||
|
||||
// List of GSMA Live CIs
|
||||
// https://www.gsma.com/solutions-and-impact/technologies/esim/gsma-root-ci/
|
||||
val PKID_GSMA_LIVE_CI = arrayOf(
|
||||
val PKID_GSMA_LIVE_CI = setOf(
|
||||
// GSMA RSP2 Root CI1 (SGP.22 v2+v3, CA: DigiCert)
|
||||
// https://euicc-manual.osmocom.org/docs/pki/ci/files/81370f.txt
|
||||
DEFAULT_PKID_GSMA_RSP2_ROOT_CI1,
|
||||
|
@ -25,7 +25,7 @@ val PKID_GSMA_LIVE_CI = arrayOf(
|
|||
|
||||
// SGP.26 v3.0, 2023-12-01
|
||||
// https://www.gsma.com/solutions-and-impact/technologies/esim/wp-content/uploads/2023/12/SGP.26-v3.0.pdf
|
||||
val PKID_GSMA_TEST_CI = arrayOf(
|
||||
val PKID_GSMA_TEST_CI = setOf(
|
||||
// Test CI (SGP.26, NIST P256)
|
||||
// https://euicc-manual.osmocom.org/docs/pki/ci/files/34eecf.txt
|
||||
"34eecf13156518d48d30bdf06853404d115f955d",
|
||||
|
|
|
@ -22,7 +22,7 @@ void interface_wrapper_init() {
|
|||
"([B)I");
|
||||
method_apdu_logical_channel_close = (*env)->GetMethodID(env, apdu_class, "logicalChannelClose",
|
||||
"(I)V");
|
||||
method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "([B)[B");
|
||||
method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "(I[B)[B");
|
||||
|
||||
jclass http_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface");
|
||||
method_http_transmit = (*env)->GetMethodID(env, http_class, "transmit",
|
||||
|
@ -66,11 +66,14 @@ static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx, uint8_t
|
|||
static int
|
||||
apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx,
|
||||
uint32_t tx_len) {
|
||||
const int logic_channel = ctx->apdu._internal.logic_channel;
|
||||
LPAC_JNI_SETUP_ENV;
|
||||
jbyteArray txArr = (*env)->NewByteArray(env, tx_len);
|
||||
(*env)->SetByteArrayRegion(env, txArr, 0, tx_len, (const jbyte *) tx);
|
||||
jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
|
||||
method_apdu_transmit, txArr);
|
||||
jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(
|
||||
env, LPAC_JNI_CTX(ctx)->apdu_interface,
|
||||
method_apdu_transmit, logic_channel, txArr
|
||||
);
|
||||
LPAC_JNI_EXCEPTION_RETURN;
|
||||
*rx_len = (*env)->GetArrayLength(env, ret);
|
||||
*rx = calloc(*rx_len, sizeof(uint8_t));
|
||||
|
|
Loading…
Add table
Reference in a new issue