建议扩展阅读:
Android SE的多种形态: eSE、UICC、SD Card
Android 9.0 SecureElementService 初始化流程分析 很清晰的UML类图和调用流程
SE 也就是 Secure Element,译为 “安全元素”
主要应用场景在 手机手表交通卡、门禁、虚拟钱包、虚拟SIM卡,以及其他身份认证的且对安全级别有一定要求的业务。
目前 Android 手机主要有三种SE的实现 eSE、UICC、microSD,这些带有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)
包路径:/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<SecureElementSession> 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)
通过 SEService 类构造传入注册服务接口,获取 Reader 得到 Session 实例,通过 Session 可以打开 Channel(包括 BasicChannel 和 LogicalChannel),Channel 中提供了发送 APDU 命令的方法。
总结步骤:
包路径:/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
// 年轻人我劝你不要去看后面的源码了, 深得很你把握不住, 硬件层的各种 CPP
mSEHal.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)
framework 层的 SEService.java 在构造里通过 ISecureElementChannel.aidl 类绑定到了
packages 层的服务 SecureElementService.java 上,后续都通过此服务交互。以下三个比较重要的入口类,SecureElementService 会在 onCreate() 里初始化,并创建多个 Terminal 实例,主要是 eSE 和 UICC(SIM),它们可能会有多个,类似(eSE1、eSE2、SIM1、SIM2)。
这里即是上层调用的 openSeesion、openBasicChannel()、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(),没太细究它们俩直接的区别。
可以使用 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")
}
使用了 framework 下的类 SEService.java,也就是SDK自带的包 android.se.omapi,调用的相关日志在后续有贴出来,主要是经历了一个错误记录下来
// Android 客户端代码
import android.se.omapi.SEService
private var mUICCReader: Reader? = null
private 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=false
for (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? = null
mUICCReader?.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.selectResponse
log("返回的resp: ${resp?.bytesToHexString()}")
log("返回是否成功: ${isSendSuccess(resp)}")
it.close()
}
openSession.close()
} catch (e: Exception) {
log("发送APDU异常: $e")
}
} ?: kotlin.run {
log("mUICCReader还未初始化完成, SEService还在连接中")
}
return resp
}
执行 openSession.openLogicalChannel(aid, p2) 遇到错误:
java.lang.SecurityException:
Exception in setUpChannelAccess()
java.security.AccessControlException:SecureElement-AccessControlEnforcerno APDU access allowed!
查看源码:
/frameworks/base/omapi/java/android/se/omapi/Session.java
Session.java
public @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.aidl
ISecureElementChannel 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());
}
}
}
后续调用离开了 framework 层到了底层目录 packages:
错误里提到一个关键类和一个方法:AccessControlEnforcerno.java 和 setUpChannelAccess(),该类在 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);
}
接下来就到了最终目的类 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 procedure
if (mInitialChannelAccess.getAccess() == ChannelAccess.ACCESS.DENIED) {
throw new AccessControlException(mTag + "access denied: " + mInitialChannelAccess.getReason());
}
// this is the new GP Access Control Enforcer implementation
if (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 修改为 Debug
if (!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);
}
在系统已经 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
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() eSE1
17: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