Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android App访问SIM卡应用 #32

Open
chenpengcong opened this issue Jan 16, 2021 · 0 comments
Open

Android App访问SIM卡应用 #32

chenpengcong opened this issue Jan 16, 2021 · 0 comments

Comments

@chenpengcong
Copy link
Owner

chenpengcong commented Jan 16, 2021

本文介绍Android app如何使用TelephonyManagerSEService提供的API与SIM卡应用(Applet)进行交互,并重点介绍不同API对于SIM卡访问规则(access rules)数据的要求

tips: GlobalPlatform Secure Element Access Control specification规范内容是预备知识,描述了Secure Element是如何进行权限控制的,本文不作展开

TelephonyManager API

首先介绍TelephonyManager API,从官方文档UICC Carrier Privileges描述可知使用该API访问SIM卡需要App具有carrier权限且该权限基于GlobalPlatform Secure Element Access Control specification规范,访问规则(access rules)数据存放在SIM卡上的ARA-M(Access Rule Application Master)应用或者ARF(Access Rule File)文件中

下文以ARF架构为例进行介绍

Access rule file (ARF) support描述:

Android then reads the access control rules file (ACRF) at 0x4300 and looks for entries with AID FFFFFFFFFFFF

Android会寻找4300文件中AID为FFFFFFFFFFFF的那一条访问控制规则

因此,我这里往SIM卡4300文件中写入AC规则如下

30 10 A0 08 04 06 FF FF FF FF FF FF 30 04 04 02 43 04

4300文件每条规则记录了AID与ACCF(Access Control Conditions File)文件的对应关系,比如上面这条记录表示AID为FFFFFFFFFFFF的应用的访问控制条件数据存放在4304文件

接下来往4304文件写入APK签名证书的SHA1值,签名证书SHA1值可以使用keytool工具从签名文件*.jks读取出来

$ keytool.exe -list -v -keystore <name.jks>

这里我用于演示的App的签名证书SHA1值为: 1AE0C6D304289DAA7B06E7A2DD11822FFFF19D25

4304文件最终写入数据如下

30 16 04 14 1A E0 C6 D3 04 28 9D AA 7B 06 E7 A2 DD 11 82 2F FF F1 9D 25

到这里,App已具备使用TelephonyManager API访问SIM卡上应用权限了

值得注意的是:使用TelephonyManager API访问SIM卡时,一旦授予了UICC Carrier权限,App可以访问SIM卡上的所有卡应用,这与下文使用SEService API有所区别

Android App的示例代码如下,已验证成功访问SIM卡的PKCS15应用(AID: A000000063504B43532D3135)

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "UICC_ACCESS";
    private static final String PKCS15_AID_STR = "A000000063504B43532D3135";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.accessByTelephonyApiBtn:
                accessUiccByTelephonyApi();
                break;
        }
    }


    private void accessUiccByTelephonyApi() {
        TelephonyManager manager = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);
        if (manager != null && !manager.hasCarrierPrivileges()) {
            Log.d(TAG, "没有运营商权限");
            return;
        }
        IccOpenLogicalChannelResponse res = manager.iccOpenLogicalChannel(PKCS15_AID_STR, 0x00);
        int channelId = res.getChannel();
        Log.d(TAG, "已打开访问PKCS15 Application的逻辑通道,channel id:" + channelId);
        manager.iccCloseLogicalChannel(channelId);
        Log.d(TAG, "已正常关闭逻辑通道");
    }
}

SEService API

SEService API实际是Open Mobile API规范的具体实现, 在https://globalplatform.org/specs-library/ 可以下载,该文档对API的描述更为详细

TelephonyManager API相比,SEService API可以更细粒度的控制App访问某个SIM卡应用的权限,App要想访问指定卡应用必须在4300文件中有该卡应用AID的一条规则。不像TelephonyManager只需要一条AID为6字节FF的规则,且无法控制App仅允许访问指定卡应用

接下来使用SEService API访问PKCS15应用(AID:A000000063504B43532D3135),仍旧以ARF架构为例进行演示

首先往4300文件写入AC规则

30 16 A0 0E 04 0C A0 00 00 00 63 50 4B 43 53 2D 31 35 30 04 04 02 43 04

该记录表示AID为A000000063504B43532D3135的应用的访问控制条件数据存放在4304文件

接下来往4304文件写入签名证书SHA1值,与上文一致

30 16 04 14 1A E0 C6 D3 04 28 9D AA 7B 06 E7 A2 DD 11 82 2F FF F1 9D 25

Android App的示例代码如下,已验证成功访问SIM卡的PKCS15应用

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "UICC_ACCESS";
    private static final byte[] PKCS15_AID_ARR = {(byte) 0xA0, 0x00, 0x00, 0x00, 0x63, 0x50, 0x4B, 0x43, 0x53, 0x2D, 0x31, 0x35};
    private SEService seService = null;
    private Session session = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.initSEServiceBtn:
                initSEService();
                break;
            case R.id.accessBySEServiceApiBtn:
                accessUiccBySEServiceApi();
                break;
            default:
                break;
        }
    }

    private void initSEService() {
        this.seService = new SEService(this, new Executor() {
            public void execute(Runnable command) {
                command.run();
            }
        }, new SEService.OnConnectedListener() {
            public void onConnected() {
                Log.d("UICC_ACCESS", "SEService connected");
                for (Reader reader : MainActivity.this.seService.getReaders()) {
                    if (reader.isSecureElementPresent() && reader.getName().contains("SIM")) {
                        try {
                            MainActivity.this.session = reader.openSession();
                            if (MainActivity.this.session == null) {
                                Log.d(TAG, "openSession return null");
                                return;
                            }
                            Log.d(TAG, "openSession success");
                        } catch (Exception e) {
                            Log.d(TAG, "init se service exception:" + e.toString());
                        }
                    }
                }
            }
        });
    }

    private void accessUiccBySEServiceApi() {
        if (this.session == null) {
            Log.d(TAG, "session is null, can't open channel");
            return;
        }
        Channel channel;
        try {
            channel = MainActivity.this.session.openLogicalChannel(PKCS15_AID_ARR);
        } catch (Exception e) {
            Log.d(TAG, "openLogicalChannel error: " + e.toString());
            return;
        }
        if (channel == null) {
            Log.d(TAG, "the Secure Element is unable to provide a new logical channel");
            return;
        }
        Log.d(TAG, "已正常打开访问PKCS15 Application的逻辑通道");
        session.closeChannels();
        Log.d(TAG, "已正常关闭逻辑通道");
    }
}

使用TelephonyManager API的注意点

实际生产环境中,为了兼容两套API,可能会往4300文件写入超过256字节大小的AC规则,要特别注意AID为6字节FF的那一条AC规则需要写在4300文件的前256字节,因为TelephonyManager API的内部实现仅会读取4300文件的前256字节数据进行解析,这个问题我们实际踩过坑

通过阅读TelephonyManager源码,定位到解析ACRF文件的实现类是UiccPkcs15.java,可以看到FileHandler的readBinary方法实现逻辑是固定发送一次read binary指令00 B0 00 00 00读取256字节数据,并没有根据文件实际大小进行读取,因此使用TelephonyManager API时如果6字节FF的AC规则所在位置超过了256字节将会无法被读取到

SEService API则不存在该问题,内部实现会根据ACRF文件实际大小进行读取,实现类见com/android/se/security/arf/PKCS15/EF.java的readbinary方法

参考

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant