Android 13中的 Open Mobile API
admin
2024-01-28 16:19:35

Open Mobile API Specification v3.2 简称 OMAPI

Android OMApi 接口架构和实现方式参考

Android OMApi 特性支持参考

aospxref Android 13 源码浏览网站

建议扩展阅读:

Android SE的多种形态: eSE、UICC、SD Card

Android 9.0 SecureElementService 初始化流程分析 很清晰的UML类图和调用流程

API 接口在 Specification 6.2 章节,本文基于 Android 13 的 OMAPI 实现(遵循 v3.3 规范),涉及了 framework 和 packages 层,具体实现与原始规范有些许不同


一、SE涉及场景及用途

SE 也就是 Secure Element,译为 “安全元素”

主要应用场景在 手机手表交通卡、门禁、虚拟钱包、虚拟SIM卡,以及其他身份认证的且对安全级别有一定要求的业务。

目前 Android 手机主要有三种SE的实现 eSEUICCmicroSD,这些带有SE的芯片有独立的储存和计算能力,可以进行 Applet 安装与个人化等一些列自定义的安全行为操作。

简单提一下这三种SE实现:

eSE:Embedded ,硬件内嵌在手机形式,通过 NFC Controller 使用
UICC:物理插槽形式,日常使用的SIM卡,在Android层面叫UICC,现在 eSIM 阶段新增了 EUICC,而且 Android 提供了一套另外的 eSIM 服务框架
microSD:物理插槽形式,早期使用的外部 SD 存储卡

Tips:
Android9 之前需引入 org.simalliance.openmobileapi.jar,从 Android9 开始并入了Android SDK,在 android.se.omapi 包下,且在 /packages/apps/ 下新增了SE相关模块(SecureElement


二、上层 framework

包路径:/frameworks/base/omapi/core/java/android/se/omapi/

目录下的所有文件:

Channel.java	
Reader.java
SEService.java
Session.java
ISecureElementChannel.aidl
ISecureElementListener.aidl	
ISecureElementReader.aidl
ISecureElementService.aidl
ISecureElementSession.aidl

Transport API class diagram overview

和原始的 OMAPI规范 还是有点区别
本地图片插入 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EEMPBi9Y-1668668098943)(C:\Users\bzb\Pictures\ccsmec\智能卡\OMA类图.jpg)]


一、SEService.java

SEService(Context context, Executor executor, OnConnectedListener listener)

对应底层类 SecureElementService.java

内部接口: OnConnectedListener

初始化为耗时操作,异步进行,成功连接后进行回调

1.void onConnected();

方法:

- Reader getUiccReader(int)
- Reader[] getReaders() 返回可用的Reader列表,元素不能重复,即使没有插卡也返回,底层是 Terminal内部类SecureElementReader(Reader相关的通信也由它完成), 内部有 ArrayList mSessions
- boolean isConnected() 当此服务成功连接时为true
- void shutdown() 释放所有由此SEService分配的SE资源,建议在程序结束时调用,此方法执行后,isConnected返回false
- String getVersion() 返回此实现基于哪一个 Open Mobile API 规范版本计算方法:版本号 = 主版本号*1000 + 副版本号,(例如:“3001”是基于OMAPI规范v3.1的实现)

二、Reader.java

Reader(@NonNull SEService service, @NonNull String name, @NonNull ISecureElementReader reader)

对应底层类 Terminal.java,此类非常重要(/packages/apps/SecureElement/src/com/android/se/Terminal.java)

方法:

- String getName() 
- SEService getSEService() 
- boolean reset()
- boolean isSecureElementPresent() 
- Session openSession() 
- void closeSessions() 

三、Session.java

Reader getReader() 
byte[] getATR() 
void close() 
boolean isClosed() 
void closeChannels() 
Channel openBasicChannel(byte[] aid) 
Channel openLogicalChannel(byte[] aid) 
Channel openBasicChannel(byte[] aid, Byte P2) 
Channel openLogicalChannel(byte[] aid, Byte P2)

四、Channel.java

void close() 
boolean isClosed()
boolean selectNext() 遍历匹配相同部分AID的所有applet, true: 在该通道上成功选择了一个新的Applet; false: 保持原有选中
boolean isBasicChannel()
Session getSession()
byte[] getSelectResponse()
byte[] transmit(byte[] command)
void setTransmitExpectDataWithWarningSW(boolean expectData)

三、framework流程:

通过 SEService 类构造传入注册服务接口,获取 Reader 得到 Session 实例,通过 Session 可以打开 Channel(包括 BasicChannelLogicalChannel),Channel 中提供了发送 APDU 命令的方法。

总结步骤:

  1. 连接 SEService,需要几百毫秒时间
  2. 指定 Reader 或者 getReaders 遍历取出能使用的 Reader
  3. 通过 Reader 连接一个 Session
  4. 通过 Session 的 openBasicChannel(aid, p2) / openLogicChannel(aid, p2) 打开一个 Channel
  5. 通过 Channel.transmit(cmd) 发送 APDU 指令
  6. 通过 Channel.close() 关闭通道
  7. 通过 Session 关闭会话,或者通过 Reader.closeSessions() 关闭所有已打开的会话
  8. 通过 SEService.shutdown() 断开自身连接,此方法内包了含上一步骤

四、底层 packages

包路径:/packages/apps/SecureElement/src/com/android/se/

目录下的文夹件和类:

internal/
security/
Channel.java	
CommandApduValidator.java	
SEApplication.java	
SecureElementService.java	
Terminal.java

一、SecureElementService.java

getReader()
createTerminals() {addTerminals(ESE_TERMINAL);addTerminals(UICC_TERMINAL);
}
addTerminals(String terminalName) {...name = terminalName + Integer.toString(index);Terminal terminal = new Terminal(name, this);terminal.initialize(index == 1);mTerminals.put(name, terminal);...
}// Seesion 的Binder桥接
final class SecureElementSession extends ISecureElementSession.Stub {closeChannels()openBasicChannel(...)openLogicalChannel(...)
}

二、Terminal.java

setUpChannelAccess(...)
isSecureElementPresent()
initialize(boolean retryOnFail)
initializeAccessControl() {if (mAccessControlEnforcer == null) {mAccessControlEnforcer = new AccessControlEnforcer(this);}mAccessControlEnforcer.initialize();
}byte[] transmit(byte[] cmd);
byte[] transmitInternal(byte[] cmd) {...// mSEHal 也就是 SecureElement.java// 年轻人我劝你不要去看后面的源码了, 深得很你把握不住, 硬件层的各种 CPPmSEHal.transmit(byteArrayToArrayList(cmd));
}// Reader 的Binder桥接
final class SecureElementReader extends ISecureElementReader.Stub {getAtr()reset()openSeesion()isSecureElementPresent()private getTerminal()
}

三、AccessControlEnforcer.java

initialize()
reset()
setUpChannelAccess(...)
readSecurityProfile()
arf...PKCS15
ara...

四、Channel.java

byte[] transmit(byte[] command) {CommandApduValidator.execute(command);checkCommand(command);// 一大堆命令检查, 接着还是通过 Terminal 发送return mTerminal.transmit(command);
}final class SecureElementChannel extends ISecureElementChannel.Stub {close()boolean selectNext()byte[] transmit(byte[] command) {Channel.this.transmit(command);}
}

常用名词

Trusted Service Manager (TSM)
Issuer Security Domain (ISD) 
Access Rule Files (ARF)
Access Rule Application (ARA)
Access Rule Application Master (ARA-M)
Access Rule Application Client (ARA-C)

五、packages流程

framework 层的 SEService.java 在构造里通过 ISecureElementChannel.aidl 类绑定到了
packages 层的服务 SecureElementService.java 上,后续都通过此服务交互。以下三个比较重要的入口类,SecureElementService 会在 onCreate() 里初始化,并创建多个 Terminal 实例,主要是 eSEUICC(SIM),它们可能会有多个,类似(eSE1、eSE2、SIM1、SIM2)。

这里即是上层调用的 openSeesionopenBasicChannel()openLogicalChannel()C++ 层的入口,也就是在这里做了 AccessRule 的校验(我在这儿就遇到了问题),规则校验具体实现入口在 AccessControlEnforcer.java,此类在 Terminal 中初始化。

顺便提两句,第一个是在上层调用 openSeesion 过程中有一个 isSecureElementPresent() 的检查,如果返回 false 会直接抛出异常 “Secure Element is not present.”,也就是当前 SE 不可用(具体原因暂时未解,查看了一些cpp的实现里直接返回的true,但上层确实有返回false,排除异常的情况),所以调用前需要在 Reader 里先行判断。

第二个是 openXXXChannel() 过程中,会有一个 setUpChannelAccess() 方法进行访问规则的校验,关于 AccessRule GPD 有一本非常厚的 SE_Access_Control_v1.1.pdf 的规范描述。

当规则校验通过后,Channel 也就打开了,此时可以在上层使用 Channel.transmit() 发送 APDU 指令,且此方法进行了响应。可以是一条指令也可以是多条,发送完成后即返回结果,另外还提供了一个方法用来获取结果 getSelectResponse(),没太细究它们俩直接的区别。

五、使用案例

5.1 检查设备支持

可以使用 PackageManager.hasSystemFeature 检查设备是否支持需要的 SE 区域,或者使用 PackageManager.systemAvailableFeatures 列出所有支持的特性,从里面找如下三个

FEATURE_SE_OMAPI_ESE
FEATURE_SE_OMAPI_UICC
FEATURE_SE_OMAPI_SD

Android 官方相关描述:

Open Mobile API reader support

On Android 11 and higher, Open Mobile API (OMAPI) supports checking for eSE, SD, and UICC support hardware on devices with the following flags:

FEATURE_SE_OMAPI_ESE
FEATURE_SE_OMAPI_SD
FEATURE_SE_OMAPI_UICC

Use these values with getSystemAvailableFeatures() or hasSystemFeature() to check for device support.


if (requireContext().packageManager.hasSystemFeature(PackageManager.FEATURE_SE_OMAPI_UICC)) {SIMLog.e("系统是否支持 OMAPI UICC_SE 硬件功能:true")lifecycleScope.launch {delay(2000)mOperator.getEID() // 做了个测试调用, 里面使用 OMAPI 执行了一次 APDU 发送}} else {SIMLog.e("系统是否支持 OMAPI UICC_SE 硬件功能:false")}requireContext().packageManager.systemAvailableFeatures.forEach {Log.e("Flyme-SIM", "系统已支持的硬件功能:$it")}

5.2 客户端调用 SEService.java

使用了 framework 下的类 SEService.java,也就是SDK自带的包 android.se.omapi,调用的相关日志在后续有贴出来,主要是经历了一个错误记录下来

// Android 客户端代码import android.se.omapi.SEService private var mUICCReader: Reader? = nullprivate val mSEService = SEService(context, ThreadUtils.getSinglePool()) {SIMLog.e("SEService已连接", TAG)}init {// SEService 大概需要几百毫秒进行连接ThreadUtils.getMainHandler().postDelayed({SIMLog.e("SEService连接状态: ${mSEService.isConnected}", TAG)if (mSEService.isConnected) {//             mUICCReader = mSEService.getUiccReader(SLOT_INDEX) // 指定卡槽拿到的Reader不是SE, 直接使用遍历的方式// 我的遍历结果// 已存在的Reader: name=eSE1, isSecureElementPresent=true// 已存在的Reader: name=SIM1, isSecureElementPresent=false// 已存在的Reader: name=SIM2, isSecureElementPresent=falsefor (reader in mSEService.readers) {SIMLog.printD("已存在的Reader: name=${reader.name}, isSecureElementPresent=${reader.isSecureElementPresent}")if (reader.isSecureElementPresent) mUICCReader = reader}}}, 1000)}/*** 发送 APDU 指令* 一次发送, 包含 选择Applet、打开通道、读取响应、关闭通道* 1.打开 Reader、Session、Channel* 2.通过 Channel 选择 Applet,并打开逻辑通道* 3.发送 APDU,并接收响应* 4.关闭 Reader、Session、Channel** @param p2 '00', '04', '08', '0C'*/private fun send(command: ByteArray, aid: ByteArray = AID_BYTE, p2: Byte = 0x00): ByteArray? {var resp: ByteArray? = nullmUICCReader?.let { reader ->log("sendApdu: Reader.isSecureElementPresent=${reader.isSecureElementPresent}")try {if (!reader.isSecureElementPresent) {log("此 Reader 不支持 SE")return null}val openSession = reader.openSession()// 在执行 openSession.openLogicalChannel(aid, p2) 时,遇到一个错误// java.lang.SecurityException:// Exception in setUpChannelAccess() java.security.AccessControlException: SecureElement-AccessControlEnforcerno APDU access allowed!openSession.openLogicalChannel(aid, p2)?.let {log("发送的req: ${command.bytesToHexString()}")it.transmit(command)resp = it.selectResponselog("返回的resp: ${resp?.bytesToHexString()}")log("返回是否成功: ${isSendSuccess(resp)}")it.close()}openSession.close()} catch (e: Exception) {log("发送APDU异常: $e")}} ?: kotlin.run {log("mUICCReader还未初始化完成, SEService还在连接中")}return resp}

5.3 主要讲一下这个错误

执行 openSession.openLogicalChannel(aid, p2) 遇到错误:

java.lang.SecurityException:
Exception in setUpChannelAccess()
java.security.AccessControlException:SecureElement-AccessControlEnforcerno APDU access allowed!

5.3.1

查看源码:

/frameworks/base/omapi/java/android/se/omapi/Session.java

	Session.javapublic @Nullable Channel openLogicalChannel(@Nullable byte[] aid, @Nullable byte p2) throws IOException {if (!mService.isConnected()) {throw new IllegalStateException("service not connected to system");}synchronized (mLock) {try {// 执行到这一行, 调用到 ISecureElementChannel.aidlISecureElementChannel channel = mSession.openLogicalChannel(aid,p2,mReader.getSEService().getListener());if (channel == null) {return null;}return new Channel(mService, this, channel);} catch (ServiceSpecificException e) {...} catch (RemoteException e) {throw new IllegalStateException(e.getMessage());}}}

5.3.2

后续调用离开了 framework 层到了底层目录 packages:

错误里提到一个关键类和一个方法:AccessControlEnforcerno.javasetUpChannelAccess(),该类在 Terminal.java 中初始化

首先来看下 Terminal.java

Terminal.java/*** Initializes the Access Control for this Terminal*/private synchronized void initializeAccessControl() throws IOException,MissingResourceException{...synchronized(mLock) {if (mAccessControlEnforcer == null) {mAccessControlEnforcer = new AccessControlEnforcer (this);}try {mAccessControlEnforcer.initialize();} catch (IOException | MissingResourceException e) {mAccessControlEnforcer = null;throw e;}}}/*** Opens a logical Channel with AID for the given package name or uuid*/public Channel openLogicalChannel(SecureElementSession session, byte[] aid, byte p2,ISecureElementListener listener, String packageName,byte[] uuid, int pid) throws IOException, NoSuchElementException {...ChannelAccess channelAccess = null;if (packageName != null || uuid != null) {channelAccess = setUpChannelAccess(aid, packageName, uuid, pid, false);}...return logicalChannel;}// 在 Terminal.java 同名的 setUpChannelAccess 方法里调用了 mAccessControlEnforcer.setUpChannelAccess/*** Initialize the Access Control and set up the channel access.*/ private ChannelAccess setUpChannelAccess(byte[] aid, String packageName, byte[] uuid, int pid,boolean isBasicChannel) throws IOException, MissingResourceException {ChannelAccess channelAccess =mAccessControlEnforcer.setUpChannelAccess(aid, packageName, uuid, checkRefreshTag);}

5.3.3

接下来就到了最终目的类 AccessControlEnforcerno.java

/packages/apps/SecureElement/src/com/android/se/security/AccessControlEnforcerno.java

AccessControlEnforcerno.java/** Initializes the Access Control for the Secure Element */public synchronized void initialize() throws IOException, MissingResourceException {...readSecurityProfile();...}// 后面两个方法就是完整代码了/** Sets up the Channel Access for the given Package */public ChannelAccess setUpChannelAccess(byte[] aid, String packageName, byte[] uuid,boolean checkRefreshTag) throws IOException, MissingResourceException {ChannelAccess channelAccess = null;// check result of channel access during initialization procedureif (mInitialChannelAccess.getAccess() == ChannelAccess.ACCESS.DENIED) {throw new AccessControlException(mTag + "access denied: " + mInitialChannelAccess.getReason());}// this is the new GP Access Control Enforcer implementationif (mUseAra || mUseArf) {channelAccess = internal_setUpChannelAccess(aid, packageName, uuid,checkRefreshTag);}if (channelAccess == null || (channelAccess.getApduAccess() != ChannelAccess.ACCESS.ALLOWED&& !channelAccess.isUseApduFilter())) {// 关键点来了, 摆明了就是一个系统权限问题if (mFullAccess) {// if full access is set then we reuse the initial channel access,// since we got so far it allows everything with a descriptive reason.channelAccess = mInitialChannelAccess;} else {// 错误就是在这里抛出的, mFullAccess 是个全局方法, 在初始化时调用了 throw new AccessControlException(mTag + "no APDU access allowed!");}}channelAccess.setPackageName(packageName);return channelAccess.clone();}private void readSecurityProfile() {// 非 debug 模式下写死了 mFullAccess = false 的,可以将手机 root 或者使用 magisk app 修改为 Debugif (!Build.IS_DEBUGGABLE) {mUseArf = true;mUseAra = true;mFullAccess = false; // Per default we don't grant full access.} else {String level = SystemProperties.get("service.seek", "useara usearf");level = SystemProperties.get("persist.service.seek", level);if (level.contains("usearf")) {mUseArf = true;} else {mUseArf = false;}if (level.contains("useara")) {mUseAra = true;} else {mUseAra = false;}if (level.contains("fullaccess")) {// 全局只有这一处将 mFullAccess 赋值成了 true// 也就是上面 SystemProperties 配置的 "service.seek" 值起了决定性作用mFullAccess = true;} else {mFullAccess = false;} }if (!mTerminal.getName().startsWith(SecureElementService.UICC_TERMINAL)) {// ARF is supported only on UICC.mUseArf = false;}Log.i(mTag, "Allowed ACE mode: ara=" + mUseAra + " arf=" + mUseArf + " fullaccess="+ mFullAccess);}

5.3.4 修改系统属性

在系统已经 root 的情况下,直接使用命令修改掉这两个字段的值

adb shell setprop ro.debuggable "1" //正常情况下 已root 设备就是 1
adb shell setprop service.seek "useara usearf fullaccess"
adb shell setprop persist.service.seek "useara usearf fullaccess"#设置后也可以查看
adb shell getprop service.seek

5.3.5 调用日志

17:56:39.838 4481  SecureElementService                com.android.se    D  getReaders() eSE1
17:56:39.838 4481  SecureElementService                com.android.se    D  getReaders() SIM1
17:56:39.838 4481  SecureElementService                com.android.se    D  getReaders() SIM2
17:56:39.838 4481  SecureElementService                com.android.se    I  isCtsRunning false
17:56:39.838 4481  SecureElementService                com.android.se    D  getReader() SIM1
17:56:39.839 4481  SecureElementService                com.android.se    D  getReader() SIM2
17:56:39.839 4481  SecureElementService                com.android.se    D  getReader() eSE117:56:40.856 4481  SecureElementService                com.android.se    I  openLogicalChannel() AID = 00a4040000, P2 = 0
17:56:40.857 4481  SecureElement-Terminal-eSE1         com.android.se    I  mzoma Access Check, pkg is com.ccsmec.sim
17:56:40.859 4481  SecureElement-Terminal-eSE1         com.android.se    W  Enable access control on logical channel for com.ccsmec.sim
17:56:40.859 4481  SecureElement-AccessControlEnforcer com.android.se    I  setUpChannelAccess() aid = 00a4040000
17:56:40.859 4481  SecureElement-AccessControlEnforcer com.android.se    I  setUpChannelAccess() packageName = com.ccsmec.sim
17:56:40.860 4481  SecureElement-Terminal-eSE1         com.android.se    I  mzoma Access Check, pkg is com.ccsmec.sim
17:56:40.915 4481  SecureElement-AccessControlEnforcer com.android.se    I  checkCommand() : Access = ALLOWED APDU Access = ALLOWED Reason = Unspecified
17:56:40.928 4481  SecureElement-Terminal-eSE1         com.android.se    I  Sent : 81cadf2000
17:56:40.928 4481  SecureElement-Terminal-eSE1         com.android.se    I  Received : df20084a46485fc8d7b30b9000
17:56:40.928 4481  SecureElement-AraController         com.android.se    I  Refresh tag unchanged. Using access rules from cache.
17:56:40.962 4481  SecureElement-AccessControlEnforcer com.android.se    I  getAccessRule() appCert = dba6d7c5929b57a81387d144da3d04a5a3f32137
17:56:40.962 4481  SecureElement-AccessControlEnforcer com.android.se    I  getAccessRule() appCert = a89c2be6dbe8eefbcce61b5c02a04d99862ef42fbb147c1a7f1d14e742e7db54
17:56:40.964 4481  SecureElement-AccessRuleCache       com.android.se    I  findAccessRule() not found

相关内容

热门资讯

11月全国查处违反中央八项规定... 12月22日,中央纪委国家监委公布了2025年11月全国查处违反中央八项规定精神问题汇总情况。当月,...
全链条技术突破让“一粒种子”实... 转自:北京日报客户端12月22日,在中国科学院遗传与发育生物学研究所举行的发布会上,中国科学院院士、...
1斤240元的“盐中爱马仕”,... 来源:图虫“纯天然无污染”“富含矿物质”“颜值出众”,“玫瑰盐”因粉红色颜值备受市场热捧。商家们给“...
人类如何引雷?探访武汉大学雷州... 今年夏天,新华社记者与视频博主“吟游诗人基德”一起来到位于广东省雷州半岛的武汉大学野外雷电试验基地,...
最新或2023(历届)深圳医疗...  缴费年限  医疗保险交多少年?  根据《深圳市社会医疗保险办法》规定,在本市按月领取养老保险待遇的...