Compare commits

..

No commits in common. "76048a7942ae504a680385f6bf6dc92ca4da6223" and "03bfdf373c59e973d78f7549646ba2f0c91fd937" have entirely different histories.

12 changed files with 88 additions and 117 deletions

View file

@ -1,7 +1,6 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
interface EuiccChannel { interface EuiccChannel {
@ -29,10 +28,5 @@ interface EuiccChannel {
*/ */
val intrinsicChannelName: String? val intrinsicChannelName: String?
/**
* The underlying APDU interface for this channel
*/
val apduInterface: ApduInterface
fun close() fun close()
} }

View file

@ -1,7 +1,6 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import im.angry.openeuicc.util.UiccPortInfoCompat import im.angry.openeuicc.util.*
import im.angry.openeuicc.util.decodeHex
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
@ -12,7 +11,7 @@ class EuiccChannelImpl(
override val type: String, override val type: String,
override val port: UiccPortInfoCompat, override val port: UiccPortInfoCompat,
override val intrinsicChannelName: String?, override val intrinsicChannelName: String?,
override val apduInterface: ApduInterface, private val apduInterface: ApduInterface,
verboseLoggingFlow: Flow<Boolean>, verboseLoggingFlow: Flow<Boolean>,
ignoreTLSCertificateFlow: Flow<Boolean> ignoreTLSCertificateFlow: Flow<Boolean>
) : EuiccChannel { ) : EuiccChannel {

View file

@ -1,7 +1,6 @@
package im.angry.openeuicc.core package im.angry.openeuicc.core
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import net.typeblog.lpac_jni.ApduInterface
import net.typeblog.lpac_jni.LocalProfileAssistant import net.typeblog.lpac_jni.LocalProfileAssistant
class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel { class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
@ -34,8 +33,6 @@ class EuiccChannelWrapper(orig: EuiccChannel) : EuiccChannel {
get() = channel.valid get() = channel.valid
override val intrinsicChannelName: String? override val intrinsicChannelName: String?
get() = channel.intrinsicChannelName get() = channel.intrinsicChannelName
override val apduInterface: ApduInterface
get() = channel.apduInterface
override val atr: ByteArray? override val atr: ByteArray?
get() = channel.atr get() = channel.atr

View file

@ -7,6 +7,7 @@ import android.util.Log
import im.angry.openeuicc.util.* import im.angry.openeuicc.util.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.typeblog.lpac_jni.ApduInterface import net.typeblog.lpac_jni.ApduInterface
@ -20,12 +21,7 @@ class OmapiApduInterface(
} }
private lateinit var session: Session private lateinit var session: Session
private val channels = arrayOf<Channel?>( private lateinit var lastChannel: Channel
null,
null,
null,
null,
)
override val valid: Boolean override val valid: Boolean
get() = service.isConnected && (this::session.isInitialized && !session.isClosed) get() = service.isConnected && (this::session.isInitialized && !session.isClosed)
@ -42,24 +38,24 @@ class OmapiApduInterface(
} }
override fun logicalChannelOpen(aid: ByteArray): Int { override fun logicalChannelOpen(aid: ByteArray): Int {
val channel = session.openLogicalChannel(aid) check(!this::lastChannel.isInitialized) {
check(channel != null) { "Failed to open logical channel (${aid.encodeHex()})" } "Can only open one channel"
val index = channels.indexOf(null) }
check(index != -1) { "No free logical channel slots" } lastChannel = session.openLogicalChannel(aid)!!
synchronized(channels) { channels[index] = channel } return 1
return index
} }
override fun logicalChannelClose(handle: Int) { override fun logicalChannelClose(handle: Int) {
val channel = channels.getOrNull(handle) check(handle == 1 && !this::lastChannel.isInitialized) {
check(channel != null) { "Invalid logical channel handle $handle" } "Unknown channel"
if (channel.isOpen) channel.close() }
synchronized(channels) { channels[handle] = null } lastChannel.close()
} }
override fun transmit(handle: Int, tx: ByteArray): ByteArray { override fun transmit(tx: ByteArray): ByteArray {
val channel = channels.getOrNull(handle) check(this::lastChannel.isInitialized) {
check(channel != null) { "Invalid logical channel handle $handle" } "Unknown channel"
}
if (runBlocking { verboseLoggingFlow.first() }) { if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}") Log.d(TAG, "OMAPI APDU: ${tx.encodeHex()}")
@ -67,7 +63,7 @@ class OmapiApduInterface(
try { try {
for (i in 0..10) { for (i in 0..10) {
val res = channel.transmit(tx) val res = lastChannel.transmit(tx)
if (runBlocking { verboseLoggingFlow.first() }) { if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}") Log.d(TAG, "OMAPI APDU response: ${res.encodeHex()}")
} }

View file

@ -21,13 +21,10 @@ class UsbApduInterface(
private lateinit var ccidDescription: UsbCcidDescription private lateinit var ccidDescription: UsbCcidDescription
private lateinit var transceiver: UsbCcidTransceiver private lateinit var transceiver: UsbCcidTransceiver
private var channelId = -1
override var atr: ByteArray? = null override var atr: ByteArray? = null
override val valid: Boolean
get() = channels.isNotEmpty()
private var channels = mutableSetOf<Int>()
override fun connect() { override fun connect() {
ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!! ccidDescription = UsbCcidDescription.fromRawDescriptors(conn.rawDescriptors)!!
@ -49,11 +46,11 @@ class UsbApduInterface(
override fun disconnect() { override fun disconnect() {
conn.close() conn.close()
atr = null
} }
override fun logicalChannelOpen(aid: ByteArray): Int { override fun logicalChannelOpen(aid: ByteArray): Int {
check(channelId == -1) { "Logical channel already opened" }
// OPEN LOGICAL CHANNEL // OPEN LOGICAL CHANNEL
val req = manageChannelCmd(true, 0) val req = manageChannelCmd(true, 0)
@ -69,7 +66,7 @@ class UsbApduInterface(
return -1 return -1
} }
val channelId = resp[0].toInt() channelId = resp[0].toInt()
Log.d(TAG, "channelId = $channelId") Log.d(TAG, "channelId = $channelId")
// Then, select AID // Then, select AID
@ -81,32 +78,32 @@ class UsbApduInterface(
return -1 return -1
} }
channels.add(channelId)
return channelId return channelId
} }
override fun logicalChannelClose(handle: Int) { override fun logicalChannelClose(handle: Int) {
check(channels.contains(handle)) { check(handle == channelId) { "Logical channel ID mismatch" }
"Invalid logical channel handle $handle" check(channelId != -1) { "Logical channel is not opened" }
}
// CLOSE LOGICAL CHANNEL // CLOSE LOGICAL CHANNEL
val req = manageChannelCmd(false, handle.toByte()) val req = manageChannelCmd(false, channelId.toByte())
val resp = transmitApduByChannel(req, handle.toByte()) val resp = transmitApduByChannel(req, channelId.toByte())
if (!isSuccessResponse(resp)) { if (!isSuccessResponse(resp)) {
Log.d(TAG, "CLOSE LOGICAL CHANNEL failed: ${resp.encodeHex()}") Log.d(TAG, "CLOSE LOGICAL CHANNEL failed: ${resp.encodeHex()}")
} }
channels.remove(handle)
channelId = -1
} }
override fun transmit(handle: Int, tx: ByteArray): ByteArray { override fun transmit(tx: ByteArray): ByteArray {
check(channels.contains(handle)) { check(channelId != -1) { "Logical channel is not opened" }
"Invalid logical channel handle $handle" return transmitApduByChannel(tx, channelId.toByte())
}
return transmitApduByChannel(tx, handle.toByte())
} }
override val valid: Boolean
get() = channelId != -1
private fun isSuccessResponse(resp: ByteArray): Boolean = private fun isSuccessResponse(resp: ByteArray): Boolean =
resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte() resp.size >= 2 && resp[resp.size - 2] == 0x90.toByte() && resp[resp.size - 1] == 0x00.toByte()

View file

@ -18,10 +18,12 @@ class TelephonyManagerApduInterface(
const val TAG = "TelephonyManagerApduInterface" const val TAG = "TelephonyManagerApduInterface"
} }
override val valid: Boolean private var lastChannel: Int = -1
get() = channels.isNotEmpty()
private var channels = mutableSetOf<Int>() override val valid: Boolean
// TelephonyManager channels will never become truly "invalid",
// just that transactions might return errors or nonsense
get() = lastChannel != -1
override fun connect() { override fun connect() {
// Do nothing // Do nothing
@ -29,39 +31,52 @@ class TelephonyManagerApduInterface(
override fun disconnect() { override fun disconnect() {
// Do nothing // Do nothing
lastChannel = -1
} }
override fun logicalChannelOpen(aid: ByteArray): Int { override fun logicalChannelOpen(aid: ByteArray): Int {
check(lastChannel == -1) { "Already initialized" }
val hex = aid.encodeHex() val hex = aid.encodeHex()
val channel = tm.iccOpenLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, hex, 0) val channel = tm.iccOpenLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, hex, 0)
if (channel.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR || channel.channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL) { if (channel.status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR || channel.channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL) {
throw IllegalArgumentException("Cannot open logical channel $hex via TelephonyManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}") throw IllegalArgumentException("Cannot open logical channel $hex via TelephonManager on slot ${port.card.physicalSlotIndex} port ${port.portIndex}")
} }
channels.add(channel.channel) lastChannel = channel.channel
return channel.channel return lastChannel
} }
override fun logicalChannelClose(handle: Int) { override fun logicalChannelClose(handle: Int) {
check(channels.contains(handle)) { check(handle == lastChannel) { "Invalid channel handle " }
"Invalid logical channel handle $handle"
}
tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle) tm.iccCloseLogicalChannelByPortCompat(port.card.physicalSlotIndex, port.portIndex, handle)
channels.remove(handle) lastChannel = -1
} }
override fun transmit(handle: Int, tx: ByteArray): ByteArray { override fun transmit(tx: ByteArray): ByteArray {
check(channels.contains(handle)) { check(lastChannel != -1) { "Uninitialized" }
"Invalid logical channel handle $handle"
}
if (runBlocking { verboseLoggingFlow.first() }) { if (runBlocking { verboseLoggingFlow.first() }) {
Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}") Log.d(TAG, "TelephonyManager APDU: ${tx.encodeHex()}")
} }
val result = tm.iccTransmitApduLogicalChannelByPortCompat(
port.card.physicalSlotIndex, port.portIndex, handle, val cla = tx[0].toUByte().toInt()
tx, val instruction = tx[1].toUByte().toInt()
) val p1 = tx[2].toUByte().toInt()
if (runBlocking { verboseLoggingFlow.first() }) val p2 = tx[3].toUByte().toInt()
Log.d(TAG, "TelephonyManager APDU response: $result") val p3 = tx[4].toUByte().toInt()
return result?.decodeHex() ?: byteArrayOf() 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()
} }
} }

View file

@ -111,26 +111,15 @@ fun TelephonyManager.iccCloseLogicalChannelByPortCompat(
} }
fun TelephonyManager.iccTransmitApduLogicalChannelByPortCompat( fun TelephonyManager.iccTransmitApduLogicalChannelByPortCompat(
slotIndex: Int, slotIndex: Int, portIndex: Int, channel: Int,
portIndex: Int, cla: Int, inst: Int, p1: Int, p2: Int, p3: Int, data: String?
channel: Int, ): String? =
tx: ByteArray if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
): 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( iccTransmitApduLogicalChannelByPort(
slotIndex, portIndex, channel, slotIndex, portIndex, channel, cla, inst, p1, p2, p3, data
cla, ins, p1, p2, p3, p4
) )
} else { } else {
iccTransmitApduLogicalChannelBySlot( iccTransmitApduLogicalChannelBySlot(
slotIndex, channel, slotIndex, channel, cla, inst, p1, p2, p3, data
cla, ins, p1, p2, p3, p4
) )
} }
}

View file

@ -8,7 +8,7 @@ interface ApduInterface {
fun disconnect() fun disconnect()
fun logicalChannelOpen(aid: ByteArray): Int fun logicalChannelOpen(aid: ByteArray): Int
fun logicalChannelClose(handle: Int) fun logicalChannelClose(handle: Int)
fun transmit(handle: Int, tx: ByteArray): ByteArray fun transmit(tx: ByteArray): ByteArray
/** /**
* Is this APDU connection still valid? * Is this APDU connection still valid?
@ -16,13 +16,4 @@ interface ApduInterface {
* callers should further check with the LPA to fully determine the validity of a channel * callers should further check with the LPA to fully determine the validity of a channel
*/ */
val valid: Boolean val valid: Boolean
fun <T> withLogicalChannel(aid: ByteArray, callback: ((ByteArray) -> ByteArray) -> T): T {
val handle = logicalChannelOpen(aid)
return try {
callback { transmit(handle, it) }
} finally {
logicalChannelClose(handle)
}
}
} }

View file

@ -28,9 +28,9 @@ class LocalProfileAssistantImpl(
var lastApduResponse: ByteArray? = null var lastApduResponse: ByteArray? = null
var lastApduException: Exception? = null var lastApduException: Exception? = null
override fun transmit(handle: Int, tx: ByteArray): ByteArray = override fun transmit(tx: ByteArray): ByteArray =
try { try {
apduInterface.transmit(handle, tx).also { apduInterface.transmit(tx).also {
lastApduException = null lastApduException = null
lastApduResponse = it lastApduResponse = it
} }

View file

@ -22,7 +22,7 @@ void interface_wrapper_init() {
"([B)I"); "([B)I");
method_apdu_logical_channel_close = (*env)->GetMethodID(env, apdu_class, "logicalChannelClose", method_apdu_logical_channel_close = (*env)->GetMethodID(env, apdu_class, "logicalChannelClose",
"(I)V"); "(I)V");
method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "(I[B)[B"); method_apdu_transmit = (*env)->GetMethodID(env, apdu_class, "transmit", "([B)[B");
jclass http_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface"); jclass http_class = (*env)->FindClass(env, "net/typeblog/lpac_jni/HttpInterface");
method_http_transmit = (*env)->GetMethodID(env, http_class, "transmit", method_http_transmit = (*env)->GetMethodID(env, http_class, "transmit",
@ -53,30 +53,24 @@ apdu_interface_logical_channel_open(struct euicc_ctx *ctx, const uint8_t *aid, u
jint ret = (*env)->CallIntMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface, jint ret = (*env)->CallIntMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
method_apdu_logical_channel_open, jbarr); method_apdu_logical_channel_open, jbarr);
LPAC_JNI_EXCEPTION_RETURN; LPAC_JNI_EXCEPTION_RETURN;
LPAC_JNI_CTX(ctx)->logical_channel_id = ret;
return ret; return ret;
} }
static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx, static void apdu_interface_logical_channel_close(struct euicc_ctx *ctx, uint8_t channel) {
__attribute__((unused)) uint8_t channel) {
LPAC_JNI_SETUP_ENV; LPAC_JNI_SETUP_ENV;
jint logical_channel_id = LPAC_JNI_CTX(ctx)->logical_channel_id;
(*env)->CallVoidMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface, (*env)->CallVoidMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
method_apdu_logical_channel_close, logical_channel_id); method_apdu_logical_channel_close, channel);
(*env)->ExceptionClear(env); (*env)->ExceptionClear(env);
} }
static int static int
apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx, apdu_interface_transmit(struct euicc_ctx *ctx, uint8_t **rx, uint32_t *rx_len, const uint8_t *tx,
uint32_t tx_len) { uint32_t tx_len) {
const int logic_channel = LPAC_JNI_CTX(ctx)->logical_channel_id;
LPAC_JNI_SETUP_ENV; LPAC_JNI_SETUP_ENV;
jbyteArray txArr = (*env)->NewByteArray(env, tx_len); jbyteArray txArr = (*env)->NewByteArray(env, tx_len);
(*env)->SetByteArrayRegion(env, txArr, 0, tx_len, (const jbyte *) tx); (*env)->SetByteArrayRegion(env, txArr, 0, tx_len, (const jbyte *) tx);
jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod( jbyteArray ret = (jbyteArray) (*env)->CallObjectMethod(env, LPAC_JNI_CTX(ctx)->apdu_interface,
env, LPAC_JNI_CTX(ctx)->apdu_interface, method_apdu_transmit, txArr);
method_apdu_transmit, logic_channel, txArr
);
LPAC_JNI_EXCEPTION_RETURN; LPAC_JNI_EXCEPTION_RETURN;
*rx_len = (*env)->GetArrayLength(env, ret); *rx_len = (*env)->GetArrayLength(env, ret);
*rx = calloc(*rx_len, sizeof(uint8_t)); *rx = calloc(*rx_len, sizeof(uint8_t));

View file

@ -28,7 +28,7 @@ jint JNI_OnLoad(JavaVM *vm, void *reserved) {
string_constructor = (*env)->GetMethodID(env, string_class, "<init>", string_constructor = (*env)->GetMethodID(env, string_class, "<init>",
"([BLjava/lang/String;)V"); "([BLjava/lang/String;)V");
const jchar _unused[1]; const char _unused[1];
empty_string = (*env)->NewString(env, _unused, 0); empty_string = (*env)->NewString(env, _unused, 0);
empty_string = (*env)->NewGlobalRef(env, empty_string); empty_string = (*env)->NewGlobalRef(env, empty_string);

View file

@ -8,7 +8,6 @@ _Static_assert(sizeof(void *) <= sizeof(jlong),
"jlong must be big enough to hold a platform raw pointer"); "jlong must be big enough to hold a platform raw pointer");
struct lpac_jni_ctx { struct lpac_jni_ctx {
jint logical_channel_id;
jobject apdu_interface; jobject apdu_interface;
jobject http_interface; jobject http_interface;
}; };