This commit is contained in:
septs 2025-02-26 12:00:27 +08:00
parent 5fa10351a7
commit eb7d21c8d6
Signed by: septs
SSH key fingerprint: SHA256:ElK0p6DNkbsqYUdJ3I9QHDVf21SQD0c2r+hd7s/r5Co
13 changed files with 117 additions and 155 deletions

View file

@ -30,7 +30,10 @@ interface EuiccChannel {
*/
val intrinsicChannelName: String?
fun forkApduInterface(): ApduInterface
/**
* The underlying APDU interface for this channel
*/
val apduInterface: ApduInterface
fun close()
}

View file

@ -13,7 +13,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 {
@ -36,8 +36,6 @@ class EuiccChannelImpl(
override val atr: ByteArray?
get() = (apduInterface as? ApduInterfaceAtrProvider)?.atr
override fun forkApduInterface() = apduInterface.clone()
override val valid: Boolean
get() = lpa.valid

View file

@ -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,24 +39,21 @@ 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.getAndAdd(1)
channels[id] = channel
return id
}
override fun logicalChannelClose(handle: Int) {
check(handle == 1 && !this::lastChannel.isInitialized) {
"Unknown channel"
}
lastChannel.close()
val channel = channels[handle]
check(channel != null) { "Channel $handle does not exist" }
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) { "Channel $handle does not exist" }
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
@ -63,7 +61,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()}")
}
@ -83,10 +81,4 @@ class OmapiApduInterface(
throw e
}
}
override fun clone() = OmapiApduInterface(
service,
port,
verboseLoggingFlow,
)
}

View file

@ -21,8 +21,6 @@ class UsbApduInterface(
private lateinit var ccidDescription: UsbCcidDescription
private lateinit var transceiver: UsbCcidTransceiver
private var channelId = -1
override var atr: ByteArray? = null
override fun connect() {
@ -46,11 +44,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 +64,7 @@ class UsbApduInterface(
return -1
}
channelId = resp[0].toInt()
val channelId = resp[0].toInt()
Log.d(TAG, "channelId = $channelId")
// Then, select AID
@ -82,27 +80,21 @@ class UsbApduInterface(
}
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())
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
}
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 {
return transmitApduByChannel(tx, handle.toByte())
}
override val valid: Boolean
get() = channelId != -1
get() = atr != null
private fun isSuccessResponse(resp: ByteArray): Boolean =
resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte()
@ -169,11 +161,4 @@ class UsbApduInterface(
return resp
}
override fun clone() = UsbApduInterface(
conn,
bulkIn,
bulkOut,
verboseLoggingFlow,
)
}

View file

@ -23,9 +23,9 @@ data class UsbCcidDescription(
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 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)

View file

@ -183,31 +183,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 +213,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 +278,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 +307,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
}

View file

@ -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)
val UsbDevice.interfaces: Iterable<UsbInterface>
get() = (0 until interfaceCount).map(::getInterface)
val UsbInterface.endpoints: Iterable<UsbEndpoint>
get() = (0 until endpointCount).map(::getEndpoint)
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 endpoints = endpoints
.filter { it.type == UsbConstants.USB_ENDPOINT_XFER_BULK }
return Pair(
endpoints.first { it.direction == UsbConstants.USB_DIR_IN },
endpoints.first { it.direction == UsbConstants.USB_DIR_OUT },
)
}
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
}
fun UsbDevice.getSmartCardInterface() =
interfaces.first { it.interfaceClass == UsbConstants.USB_CLASS_CSCID }

View file

@ -104,7 +104,7 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
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))
getESTKmeInfo(channel.forkApduInterface())?.let {
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))

View file

@ -1,6 +1,7 @@
package im.angry.openeuicc.vendored
import android.util.Log
import im.angry.openeuicc.util.TAG
import net.typeblog.lpac_jni.ApduInterface
data class ESTKmeInfo(
@ -19,18 +20,18 @@ fun getESTKmeInfo(iface: ApduInterface): ESTKmeInfo? {
)
// @formatter:on
return try {
iface.connect()
iface.logicalChannelOpen(aid)
fun transmit(p1: Byte) =
decode(iface.transmit(byteArrayOf(0x00, 0x00, p1, 0x00, 0x00)).clone())
return ESTKmeInfo(
transmit(0x00), // serial number
transmit(0x01), // bootloader version
transmit(0x02), // firmware version
transmit(0x03), // sku name
)
iface.openChannel(aid) { transmit ->
fun invoke(p1: Byte) =
decode(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("ESTKme", "Failed to get ESTKme info", e)
Log.d(TAG, "Failed to get ESTKmeInfo", e)
null
}
}

View file

@ -18,12 +18,7 @@ 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
override var valid: Boolean = false
override fun connect() {
// Do nothing
@ -31,29 +26,24 @@ 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}")
}
lastChannel = channel.channel
return lastChannel
valid = true
return channel.channel
}
override fun logicalChannelClose(handle: Int) {
check(handle == lastChannel) { "Invalid channel handle " }
tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
lastChannel = -1
valid = false
}
override fun transmit(tx: ByteArray): ByteArray {
check(lastChannel != -1) { "Uninitialized" }
override fun transmit(handle: Int, tx: ByteArray): ByteArray {
if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}")
}
@ -67,7 +57,7 @@ class TelephonyManagerApduInterface(
// @formatter:off
val result = tm.iccTransmitApduLogicalChannelByPortCompat(
port.card.physicalSlotIndex, port.portIndex, lastChannel,
port.card.physicalSlotIndex, port.portIndex, handle,
cla, ins, p1, p2, p3, p4
)
// @formatter:on
@ -75,10 +65,4 @@ class TelephonyManagerApduInterface(
Log.d(TAG, "TelephonyManager APDU response: $result")
return result?.decodeHex() ?: byteArrayOf()
}
override fun clone() = TelephonyManagerApduInterface(
port,
tm,
verboseLoggingFlow
)
}

View file

@ -3,13 +3,12 @@ package net.typeblog.lpac_jni
/*
* Should reflect euicc_apdu_interface in lpac/euicc/interface.h
*/
interface ApduInterface : Cloneable {
interface ApduInterface {
fun connect()
fun disconnect()
fun logicalChannelOpen(aid: ByteArray): Int
fun logicalChannelClose(handle: Int)
fun transmit(tx: ByteArray): ByteArray
public override fun clone(): ApduInterface
fun transmit(handle: Int, tx: ByteArray): ByteArray
/**
* Is this APDU connection still valid?
@ -17,4 +16,16 @@ interface ApduInterface : Cloneable {
* callers should further check with the LPA to fully determine the validity of a channel
*/
val valid: Boolean
}
fun <T> openChannel(
aid: ByteArray,
callback: (transmit: (ByteArray) -> ByteArray) -> T
): T {
val handle = logicalChannelOpen(aid)
return try {
callback { transmit(handle, it) }
} finally {
logicalChannelClose(handle)
}
}
}

View file

@ -29,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
}

View file

@ -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));