|
|
@@ -0,0 +1,199 @@
|
|
|
+package com.example.uicceditor
|
|
|
+
|
|
|
+import android.telephony.TelephonyManager
|
|
|
+import android.util.Log
|
|
|
+import java.math.BigDecimal
|
|
|
+import java.math.RoundingMode
|
|
|
+import kotlin.math.roundToInt
|
|
|
+
|
|
|
+fun String.swapNibbles(): String {
|
|
|
+ val sb = StringBuilder()
|
|
|
+ var i = 0
|
|
|
+ while (i < length) {
|
|
|
+ sb.append(this[i + 1]).append(this[i])
|
|
|
+ i += 2
|
|
|
+ }
|
|
|
+ return sb.toString()
|
|
|
+}
|
|
|
+
|
|
|
+fun String.stripF(): String {
|
|
|
+ return this.replace(Regex("^f+"), "").replace(Regex("f+$"), "")
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+class ApduChannel(private val telephonyManager: TelephonyManager, private val adm: String) {
|
|
|
+
|
|
|
+ class ApduResponse(rawStr: String) {
|
|
|
+
|
|
|
+ val data: String
|
|
|
+ val sw: String
|
|
|
+ val sw1: String
|
|
|
+ val sw2: String
|
|
|
+
|
|
|
+ init {
|
|
|
+ data = rawStr.substring(0, rawStr.length - 4)
|
|
|
+ sw = rawStr.substring(rawStr.length - 4)
|
|
|
+ sw1 = sw.substring(0, 2)
|
|
|
+ sw2 = sw.substring(2)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private val TAG = "ApduShell"
|
|
|
+
|
|
|
+ private var authorized: Boolean = false
|
|
|
+
|
|
|
+ fun execute(cmd: String): ApduResponse {
|
|
|
+ val cla = cmd.substring(0, 2).toInt(16)
|
|
|
+ val ins = cmd.substring(2, 4).toInt(16)
|
|
|
+ val p1 = cmd.substring(4, 6).toInt(16)
|
|
|
+ val p2 = cmd.substring(6, 8).toInt(16)
|
|
|
+ val p3 = cmd.substring(8, 10).toInt(16)
|
|
|
+ val data = cmd.substring(10)
|
|
|
+ Log.i(TAG, "-> $cmd")
|
|
|
+ val res =
|
|
|
+ telephonyManager.iccTransmitApduBasicChannel(
|
|
|
+ cla,
|
|
|
+ ins,
|
|
|
+ p1,
|
|
|
+ p2,
|
|
|
+ p3,
|
|
|
+ data
|
|
|
+ )
|
|
|
+ Log.i(TAG, "<- $res")
|
|
|
+ return ApduResponse(res)
|
|
|
+ }
|
|
|
+
|
|
|
+ fun asciiToHex(ascii: String): String {
|
|
|
+ return ascii.toByteArray().joinToString("") { "%02x".format(it) }
|
|
|
+ }
|
|
|
+
|
|
|
+ fun authorize() {
|
|
|
+ if (adm.length != 8) {
|
|
|
+ Log.e(TAG, "ADM must be 8 characters long")
|
|
|
+ throw IllegalArgumentException("ADM must be 8 characters long")
|
|
|
+ }
|
|
|
+ val res = execute("0020000A08${asciiToHex(adm)}")
|
|
|
+
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ Log.e(TAG, "Authorization failed")
|
|
|
+ throw SecurityException("Authorization failed")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ init {
|
|
|
+ authorize()
|
|
|
+ }
|
|
|
+
|
|
|
+ fun readIccid(): String {
|
|
|
+ var res = execute("00a40004023f00")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to select MF")
|
|
|
+ }
|
|
|
+ res = execute("00a40004022fe2")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to select EF.ICCID")
|
|
|
+ }
|
|
|
+ res = execute("00b000000a")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to read EF.ICCID")
|
|
|
+ }
|
|
|
+ return res.data.swapNibbles().stripF()
|
|
|
+ }
|
|
|
+
|
|
|
+ fun readImsi(): String {
|
|
|
+ var res = execute("00a4040410a0000000871002ff86ff0389ffffffff")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to select AF.USIM")
|
|
|
+ }
|
|
|
+ res = execute("00a40004026f07")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to select EF.IMSI")
|
|
|
+ }
|
|
|
+ res = execute("00b0000009")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to read EF.IMSI")
|
|
|
+ }
|
|
|
+ return decImsi(res.data)!!
|
|
|
+ }
|
|
|
+
|
|
|
+ fun decImsi(ef: String): String? {
|
|
|
+ if (ef.length < 4) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ var l = ef.substring(0, 2).toInt(16) * 2
|
|
|
+ l -= 1
|
|
|
+ val swapped: String = ef.substring(2).swapNibbles().stripF()
|
|
|
+ if (swapped.isEmpty()) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ val oe = (swapped[0].toString().toInt() shr 3) and 1
|
|
|
+ if (oe == 0) {
|
|
|
+ l -= 1
|
|
|
+ }
|
|
|
+ if (l != swapped.length - 1) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ val imsi = swapped.substring(1)
|
|
|
+ return imsi
|
|
|
+ }
|
|
|
+
|
|
|
+ fun encImsi(imsi: String): String {
|
|
|
+ val l = BigDecimal(((imsi.length + 1) / 2f).toDouble()).setScale(0, RoundingMode.HALF_UP)
|
|
|
+ .toInt()
|
|
|
+ val oe = imsi.length and 1
|
|
|
+ val ei =
|
|
|
+ String.format("%02x", l) +
|
|
|
+ (String.format("%01x", (oe shl 3) or 1) + imsi.padEnd(
|
|
|
+ 15,
|
|
|
+ 'f'
|
|
|
+ )).swapNibbles()
|
|
|
+ return ei
|
|
|
+ }
|
|
|
+
|
|
|
+ fun encIccid(iccid: String): String {
|
|
|
+ return iccid.padEnd(20, 'f').swapNibbles()
|
|
|
+ }
|
|
|
+
|
|
|
+ fun writeIccid(iccid: String) {
|
|
|
+ var res = execute("00a40004023f00")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to select MF")
|
|
|
+ }
|
|
|
+ res = execute("00a40004022fe2")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to select EF.ICCID")
|
|
|
+ }
|
|
|
+ res = execute("00d600000a${encIccid(iccid)}")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to write EF.ICCID")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fun writeImsi(imsi: String) {
|
|
|
+ var res = execute("00a40004023f00")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to select MF")
|
|
|
+ }
|
|
|
+ res = execute("00a4040410a0000000871002ff86ff0389ffffffff")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to select ADF.USIM")
|
|
|
+ }
|
|
|
+ res = execute("00a40004026f07")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to select EF.IMSI")
|
|
|
+ }
|
|
|
+ res = execute("00d6000009083901428002362719")
|
|
|
+ var s = telephonyManager.iccTransmitApduBasicChannel(
|
|
|
+ 0x00,
|
|
|
+ 0xd6,
|
|
|
+ 0x00,
|
|
|
+ 0x00,
|
|
|
+ 0x09,
|
|
|
+ "083901428002362719"
|
|
|
+ )
|
|
|
+ Log.i(TAG, "writeImsi: $s")
|
|
|
+ if (res.sw != "9000") {
|
|
|
+ throw Exception("Failed to write EF.IMSI")
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|