• Android eSIM-LPA基于Android13的实现


    eSIM-Android-LPA基于 Android 13的实现

    国际对ESIM相关所有规范定义在:GSMA Spec (SGP) - eSIM Consumer and IoT Specifications

    国内对EID相关规定在:电信终端产业协会-EID管理实施规定

    Android 官方文档在:eSIM实现

    开发前知

    什么是 eSIM

    嵌入式SIM(又称 eSIMeUICC)技术,移动用户可以在没有实体 SIM 卡的情况下,下载运营商配置文件并激活运营商服务。该技术是由 GSMA 推动的全球规范,支持在任何移动设备上进行远程 SIM 配置 (RSP)。从 Android9 开始,Android 框架为访问 eSIM 和管理 eSIM 上的订阅配置文件提供了标准 API。借助这些 eUICC API,第三方可以在支持 eSIM 的 Android 设备上开发自己的运营商应用和 Local Profile Assistant (LPA)

    1. 为访问系统隐藏API,首先需编译相应 compileSdkVersionframework.jartelephony-common.jar,供 LPA 打包时编译检查使用

    2. 其次,应用需要作为系统应用存在,自签名后在使用系统应用签名,然后安装在 system/priv-app/ 下,且需要在 etc/permissions/privapp-permissions-platform.xml 文件里配置应用所必须的特殊权限,例如:

    <privapp-permissions package="com.xxx.lpa">
        <permission name="android.permission.INTERNET"/>
        <permission name="android.permission.READ_PHONE_STATE"/>
        <permission name="android.permission.MODIFY_PHONE_STATE"/>
        <permission name="android.permission.BIND_EUICC_SERVICE"/>
        <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
        <permission name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
        <permission name="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS"/>
    privapp-permissions>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    1. 同样,在系统使用到 Euicc 这种特殊硬件功能,需要在 etc/permissions/android.hardware.telephony.euicc.xml 文件里配置需要启用的硬件特性
    <permissions>
        <feature name="android.hardware.telephony.euicc" />
        <feature name="android.hardware.telephony.radio.access" />
    	<feature name="android.hardware.telephony.subscription" />
    permissions>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 最后使用 EuiccManager.isEnabled 检查得到结果 true ,表明可以 正常使用 eSIM相关API进行开发

    系统应用安装流程:

    1. adb root
    2. adb remount
    3. adb push lpa.apk system/priv-app/lpa/lpa.apk 推送apk到系统目录
    4. adb shell am restart/ adb shell reboot 重启am或者系统,让系统自动检查并安装
    
    • 1
    • 2
    • 3
    • 4

    涉及名词

    id类

    • AID 32位十六进制

      com.android.internal.telephony.uicc.euicc.EuiccCard

      样例:A0000005591010FFFFFFFF8900000100

    • eid 32位十进制

      样例:89033023001211360000000007226088

    • cardId 20位十六进制

      eSIM卡中是 eid,普通SIM卡中是 iccid

    • ICCID 20位十六进制

    Integrate circuit card identity 集成电路卡识别码(固化在手机SIM卡中) ICCID为IC卡的唯一识别号码,共有20位数字组成

    样例1:898602F30918A0009913
    样例2:89860922780011058162
    
    • 1
    • 2
    • slotId/SlotIndex 卡槽ID

      0(卡槽1)、1(卡槽2)

    其他类

    • APDU

      APDUISO/IEC 7816-4 规范中定义的,用于和SIM卡交互(发送/响应)的 协议,具体呈现为一串 16进制的编码指令

    样例:

    80E2910006BF3E035C015A(发送)、BF3E125A10890860302022000000220000153565489000(响应)
    
    • 1

    Android 13 中的 Euicc

    从 Android 9 后就开始有 Euicc 的相关API,10、11、12、13新增了一些扩展或者标记了一些过时,比如新增了多卡功能、支持和运营商APP交互…

    涉及关键入口类:

    • android.service.euicc.EuiccService -> EuiccServiceImpl 需要我们自己实现
    • android.telephony.euicc.EuiccManager -> EuiccController -> EuiccConnector

    EuiccManager中保存的重要变量

    private final EuiccConnector mConnector;
    private final SubscriptionManager mSubscriptionManager;
    private final TelephonyManager mTelephonyManager;
    private final AppOpsManager mAppOpsManager;
    private final PackageManager mPackageManager;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • android.telephony.euicc.EuiccCardManager -> EuiccCardController -> EuiccCard/UiccController/SubscriptionController

    • android.telephony.TelephonyManager -> SubscriptionManager

    • android.telephony.SubscriptionManager -> SubscriptionController

    涉及关键底层类:

    com.android.internal.telephony.euicc.EuiccConnector(连接 EuiccManager 和 EuiccService)

    com.android.internal.telephony.uicc包下:

    UiccController(直接操作卡槽)
    euicc.EuiccCard(EuiccController 中部分Card方法的实现)
    euicc.apdu.ApduSender(EuiccCard 中进行 APDU 命令发送)
     ApduSender错误举例:android.telephony.IccOpenLogicalChannelResponse(Response to the TelephonyManager.iccOpenLogicalChannel command.)
    euicc.Tags(EuiccCard 中使用于 APDU 命令发送的 ASN.1 tag 定义)
    euicc.EuiccCardErrorException(Card相关方法产生的异常定义)
    euicc.apdu.ApduException(APDU 命令产生的异常定义)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    总结一下:

    EuiccManager 类中的方法,一部分通过各种 xxxManager 实现(最后发送APDU命令),一部分通过 EuiccConnector 类调用到 EuiccService 中需要开发者自行实现。

    大致调用栈:

    xxxManager -> xxxController -> (EuiccConnector -> EuiccService/ EuiccCard -> ApduSender)
    
    • 1

    EuiccService的实现

    从 Lui 层面来看,主要需要实现其中的5个方法:

    1. onDownloadSubcription(下载订阅文件)
    2. onGetEuiccProfileInfoList(获取所有 Profile 信息)
    3. onSwitchToSubscription(切换 Profile)
    4. onUpdateSubscriptionNickname(编辑某 Profile 的昵称字段)
    5. onDeleteSubscription(删除某 Profile)

    但其实内部还需实现最基础的方法: onGetEid(获取 eid),这一个跟上面5个方法不一样,需区分开来,下面方法未按照顺序排列,除了 getEid 都是由易入难

    前提:

    入口类:EuiccCardManager
    除了 getEid 每个方法均回调为:
      EuiccCardManager.ResultCallback(resultCode, result)
    resultCode:响应码,0为成功(EuiccService.RESULT_OK),其他负数值为失败,失败需处理返回异常
    result:APDU响应指令
    
    • 1
    • 2
    • 3
    • 4
    • 5

    onGetEid 实现:

    入口函数:

    public String EuiccManager.getEid() {
    	return getIEuiccController().getEid(...)
    }
    
    • 1
    • 2
    • 3

    EuiccController 层通过 EuiccConnector ,最终调用到 EuiccService.onGetEid 方法里实现,这里一不小心就会进到死循环的坑!

    实现很简单,Android 10 及以上版本使用新增方法 getUiccCardsInfo ,以下版本使用旧 getUiccSlotsInfo,获取所有卡的信息列表,因为同时只能启用一个卡槽功能,所以直接找不为空的就行了。其实在 EuiccCard 类中有 getEid() 的公开实现,但系统没有提供上层入口方法。

    override fun onGetEid(slotId: Int): String {
        val eid =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
                tm.uiccCardsInfo.find {
                    it.isEuicc && !it.eid.isNullOrEmpty()
                }?.eid ?: ""
            } else {
                tm.uiccSlotsInfo.find {
                    it.isEuicc && !it.cardId.isNullOrEmpty()
                }?.cardId ?: ""
            }
    XLog.d("Service onGetEid slotId: $slotId, eid:$eid")
    return eid
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    双卡槽下插入单eSIM卡,对比一下两个Info

    TelephonyManager.getUiccSlotInfo():

    UiccSlotInfo (mIsActive=true, mIsEuicc=true, mCardId=89033023001211360000000007226088, cardState=2, phoneId=0, mIsExtendedApduSupported=false, mIsRemovable=true)

    UiccSlotInfo (mIsActive=true, mIsEuicc=false, mCardId=, cardState=1, phoneId=1, mIsExtendedApduSupported=false, mIsRemovable=true)

    TelephonyManager.getUiccCardInfo():

    UiccCardInfo (mIsEuicc=true, mCardId=5, mEid=89033023001211360000000007226088, mIccId=89861000000000000022, mSlotIndex=0, mIsRemovable=true)

    UiccCardInfo (mIsEuicc=false, mCardId=-2, mEid=null, mIccId=, mSlotIndex=1, mIsRemovable=true)

    onGetEuiccProfileInfoList 实现

    调用函数:

    public void EuiccCardManager.requestAllProfiles(
    	String cardId, 
        @CallbackExecutor Executor executor, 
    	ResultCallback callback
    ) {
    	getIEuiccCardController().getAllProfiles(...)
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    ProfileInfoList样例: 最后需展示在 Lui 上供用户操作

    [
      {
        "iccid": "898602F30918A0009911",
        "nickname": "CM",
        "profileName": "",
        "providerName": "",
        "state": 1
      },
      {
        "iccid": "89860922780011058161",
        "nickname": "CU",
        "profileName": "",
        "providerName": "",
        "state": 0
      },
      {
        "iccid": "89861122215036305451",
        "nickname": "CT",
        "profileName": "",
        "providerName": "",
        "state": 0
      }
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    onSwitchToSubscription 实现

    调用函数:

    public void EuiccCardManager.switchToProfile(
    	String cardId, String iccid, boolean refresh,
        @CallbackExecutor Executor executor, 
    	ResultCallback callback
    ) {
    	getIEuiccCardController().switchToProfile(...)
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    遇到的错误 1:

    逻辑通道被占用
    switchToProfile callback onException: 
      com.android.internal.telephony.uicc.euicc.EuiccCardException: Cannot send APDU.
      ApduException: The logical channel is in use. (apduStatus=0)
    
    • 1
    • 2
    • 3
    • 4

    出现原因:

    1. 在下载订阅后立即进行切换操作
    2. 多线程并发调用了底层 EuiccCard 的同一个接口(一个逻辑通道启用并占用时需要释放后才能被重新使用)

    如何排查或解决:

    1. 在下载后进行延时几秒,再调用切换
    2. 检查 Lui 内有没有并发调用同一接口

    onUpdateSubscriptionNickname 实现

    调用函数:

    public void EuiccCardManager.setNickname(
    	String cardId, String iccid, String nickname,
        @CallbackExecutor Executor executor, 
    	ResultCallback callback
    ) {
    	getIEuiccCardController().setNickname(...)
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    onDeleteSubscription 实现

    调用函数:

    public void EuiccCardManager.deleteProfile(
    	String cardId, String iccid,
        @CallbackExecutor Executor executor, 
    	ResultCallback callback
    ) {
    	getIEuiccCardController().deleteProfile(...)
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    DownloadSubscription 实现:

    一、下载流程

    下载流程较为复杂

    运营商APP 拉起 LPA 发起下载并携带 ActivationCode(激活码通常是扫描二维码得到),Lui 上的 EuiccManager.downloadSubscription 为入口函数,参数 PendingIntent 包含 action:ACTION_DOWNLOAD ,同时注册 BroadcastReceiver 接收下载结果。下载途中可能需要到运营商APP端进行下载码的确认。

    最终需实现方法 EuiccService.onDownloadSubscription:

    override fun onDownloadSubscription(
        slotId: Int,
        subscription: DownloadableSubscription?,
        switchAfterDownload: Boolean,
        forceDeactivateSim: Boolean,
        resolvedBundle: Bundle?,
    ): DownloadSubscriptionResult {
    	return null
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    二、下载实现步骤

    所有接口调用,header 里是状态字段,response 里是验证信息。

    header样例:

    {
      "header": {
        "functionExecutionStatus": {
          "status": "Executed-Success"
        }
      },
      "transactionId": "FD913D91F79702FBF27DE2B4ABBD468A"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    response样例:

    {"pendingNotification":"vzeBrr8naIAQzS/mWM/RNgcHY2VAyc6yGr8vJoABB4ECB4AMEWRwcGx1cy5jY3NtZWMuY29tWgqYaAEAAAAAAAAzBgorBgEEAYOOA2UCoh+gHU8QoAAABVkQEP+JAAARAAQJMAegBTADgAEAXzdA0iN1N/yZEKdsbUub2PXOW9eT5NJi97ajNnu44rMTVimQnDzwvk4+hvsQFHH293xn7SUnfZM6gJswqaiF9gpONB=="}
    
    • 1
    1. getEid 是前提,操作卡时使用
    val cardId = onGetEid(slotId)
    
    • 1
    2. 解析并验证激活码

    获取激活码,为空或非法需中断操作返回错误:

    val activationCode = subscription?.encodedActivationCode
    // 激活码样例:1$dpplus.xxx.com:8445/xxfreexx-dp-service$2TSU5-NKVPM-MIS64-9CLYD-KD411
    
    val args = ac.split('$')
    val smdpAddress = "https://${args[1]}/"
    val matchingId = args[2] // 服务端验证使用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    3. 初始化、双向验证,使用激活码到 SM-DP+ 拉取订阅信息

    接口调用流程:

    1. initiateAuthentication
    2. authenticateClient
    3. getBoundProfilePackage
    4. handleNotification
    
    • 1
    • 2
    • 3
    • 4

    接口响应重要标记:

    1. euiccChallenge 字段
    2. authenticateServerResponse 字段
    3. prepareDownloadResponse 字段
    4. pendingNotification 字段
    
    • 1
    • 2
    • 3
    • 4

    如果顺利且成功,上面的每一次调用和一次响应是对应的,表明下载流程完整。


    下面是完整的、带前因后果的 API 流程:

    • API:EuiccCardManager.onGetEuiccChallenge
    • API:EuiccCardManager.onGetEuiccInfo1

    1. initiateAuthentication 接口调用

    euiccChallenge 响应,返回(euiccChallenge,euiccInfo1,SM-DP+服务器地址)

    // SM-DP+服务连接,初始化验证
    val initAuthResp = es9.initiateAuthentication(
        InitiateAuthentication.Request(
            euiccChallenge.toBase64(),
            euiccInfo1.toBase64(),
            smdpAddress
        )
    ).execute().body()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • API:EuiccCardManager.onAuthenticateServer
    val serverSigned1 = initAuthResp.serverSigned1.base64ToBytes()
    val serverSignature1 = initAuthResp.serverSignature1.base64ToBytes()
    val ciPkIdToBeUsed = initAuthResp.euiccCiPKIdToBeUsed.base64ToBytes()
    val serverCertificate = initAuthResp.serverCertificate.base64ToBytes()
    
    euiccCardManager.authenticateServer(cardId,
        matchingId,
        serverSigned1,
        serverSignature1,
        ciPkIdToBeUsed,
        serverCertificate,
        AsyncTask.THREAD_POOL_EXECUTOR
    ) { resultCode, result ->
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2. authenticateClient 接口调用

    authenticateServerResponse 响应,返回 functionExecutionStatus.status=Executed-Success 继续

    val authClientResp =
        es9.authenticateClient(AuthenticateClient.Request(initAuthResp.transactionId, authServerResp.toBase64()))
            .execute().body()
    
    • 1
    • 2
    • 3
    • API:EuiccCardManager.onPrepareDownload
    euiccCardManager.prepareDownload(
        cardId,
        if (subscription.getEncodedActivationCode().endsWith("\$1")) {
            sha256(sha256(subscription.confirmationCode.toByteArray()) + authClientResp.transactionId.hexStringToBytes())
        } else {
            null
        },
        authClientResp.smdpSigned2.base64ToBytes(),
        authClientResp.smdpSignature2.base64ToBytes(),
        authClientResp.smdpCertificate.base64ToBytes(),
        AsyncTask.THREAD_POOL_EXECUTOR
    ) { resultCode, result ->
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3. getBoundProfilePackage 接口调用

    prepareDownloadResponse 响应

     val getBoundProfilePackageResp = es9.getBoundProfilePackage(
         GetBoundProfilePackage.Request(
             authClientResp.transactionId,
             prepareDownloadResp.toBase64()
         )
     ).execute().body()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • API:EuiccCardManager.onLoadBoundProfilePackage
    euiccCardManager.loadBoundProfilePackage(cardId, bpp, AsyncTask.THREAD_POOL_EXECUTOR) { resultCode, result ->
    	...
    }
    
    • 1
    • 2
    • 3

    4. handleNotification 接口调用

    pendingNotification 响应

    es9.handleNotification(HandleNotification.Request(installationResult.toBase64()))
        .execute().body()
    
    • 1
    • 2
    4. 判断是否需要下载后切换Profile,需要的话执行

    遇到的错误1:多线程并发调用 或 下载后立即切换 Profile 产生,检查调用链,对下载后切换中间延时

    E  getProfile in switchToProfile callback onException: 
     com.android.internal.telephony.uicc.euicc.EuiccCardException: Cannot parse response: BF2D03810101
     at com.android.internal.telephony.uicc.euicc.EuiccCard$2.onResult(EuiccCard.java:1251)
     at com.android.internal.telephony.uicc.euicc.EuiccCard$2.onResult(EuiccCard.java:1242)
     at com.android.internal.telephony.uicc.euicc.apdu.ApduSender$4.onResult(ApduSender.java:275)
     at com.android.internal.telephony.uicc.euicc.apdu.ApduSender$4.onResult(ApduSender.java:266)
     at com.android.internal.telephony.uicc.euicc.async.AsyncMessageInvocation.handleMessage(AsyncMessageInvocation.java:57)
     at android.os.Handler.dispatchMessage(Handler.java:102)
     at android.os.Looper.loop(Looper.java:223)
     at android.app.ActivityThread.main(ActivityThread.java:7822)
     at java.lang.reflect.Method.invoke(Native Method)
     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952)
    
    Caused by: com.android.internal.telephony.uicc.asn1.TagNotFoundException: null (tag=160)
     at com.android.internal.telephony.uicc.asn1.Asn1Node.getChild(Asn1Node.java:330)
     at com.android.internal.telephony.uicc.euicc.EuiccCard.lambda$getProfile$5(EuiccCard.java:288)
     at com.android.internal.telephony.uicc.euicc.-$$Lambda$EuiccCard$TTvsStUIyUFrPpvGTlsjBCy3NyM.handleResult(Unknown Source:0)
     at com.android.internal.telephony.uicc.euicc.EuiccCard$2.onResult(EuiccCard.java:1246)
     at com.android.internal.telephony.uicc.euicc.EuiccCard$2.onResult(EuiccCard.java:1242) 
     at com.android.internal.telephony.uicc.euicc.apdu.ApduSender$4.onResult(ApduSender.java:275) 
     at com.android.internal.telephony.uicc.euicc.apdu.ApduSender$4.onResult(ApduSender.java:266) 
     at com.android.internal.telephony.uicc.euicc.async.AsyncMessageInvocation.handleMessage(AsyncMessageInvocation.java:57) 
     ...
     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    遇到的错误 2:
    由于 eid 过长,在 onDownloadSubscription 返回实体类中传入 cardId: Int 字段过长(这个字段可不传,内部未使用)

    java.lang.NumberFormatException: For input string: "89033023001211360000000007226077"
     at java.lang.Integer.parseInt(Integer.java:618)
     at java.lang.Integer.parseInt(Integer.java:650)
     at com.xxx.lpa.service.EuiccService.onDownloadSubscription(EuiccService.kt:193)
     at android.service.euicc.EuiccService$IEuiccServiceWrapper$1.run(EuiccService.java:669)
     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
     at java.lang.Thread.run(Thread.java:923)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    下载流程结束

  • 相关阅读:
    接口自动化测试之预期结果的处理
    九、蜂鸣器
    关于线程池概念使用
    实验5 二叉树的应用程序设计
    绝对路径与相对路径
    华为云云耀云服务器L实例评测|华为云上安装kafka
    java rsa生成公钥和私钥与C++生成的rsa
    ElasticSearch全文搜索引擎
    TypeScript配置文件设置详解
    文件的打开方式
  • 原文地址:https://blog.csdn.net/qq_39420519/article/details/127903469