Android基于HCE实现模拟Ndef格式的标签卡

背景

当前Android NFC手机基本都能支持基于Host的卡模拟(HCE),其实现原理上需要读卡端和模拟端互相通过校验特定AID来实现通信,这个特性就让手机模拟的普通HCE卡无法被其他手机上通用的NFC三方APP直接读取到数据,比如NFC手机通过HCE模拟出一张银行卡,只有对该HCE银行卡AID 支持的POS机或特殊定制APP才可以与该HCE 银行卡通信,完成交易。其他三方APP由于不知道该卡AID,都是无法直接与之通信的。

鉴于上面的特殊,这样限制了HCE被普通用户使用。比如我们期望自己手机通过HCE分享出BT,WIFI秘钥,或分享的联系人等信息 能够被其他手机的NFC三方APP直接读取数据。为实现这样的目的,我们就需要基于HCE实现一个Ndef格式的标签卡。因为Ndef格式是NFC联盟定义的一种NFC Data Exchange Format,NFC三方APP基本可以直接读取。

实现HCE模拟Ndef标签卡

1. 首次Android studio创建一个新的HceNdefTag APP,并保证可以正常编译出APK。

2. 在APP内创建NdefHceService.java并继承HostApduService,之后覆写相关抽象方法。

package com.example.hcendeftag;

import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;

public class NdefHceService extends HostApduService {
    @Override
    public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
        return new byte[0];
    }

    @Override
    public void onDeactivated(int reason) {

    }
}

3.实现Ndef主卡模拟服务实现,并在其定义简单的Ndef消息。

package com.example.hcendeftag;

import android.content.ComponentName;
import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.util.Log;

import java.util.Arrays;


public class NdefHceService extends HostApduService {

    // source: https://github.com/TechBooster/C85-Android-4.4-Sample/blob/master/chapter08/NdefCard/src/com/example/ndefcard/NdefHostApduService.java
    public static final ComponentName COMPONENT = new ComponentName("com.example.hcendeftag", NdefHceService.class.getName());

    private final static String TAG = "NfcTest_NdefHostApduService";

    private static final byte[] SELECT_APPLICATION = {
            (byte) 0x00, // CLA	- Class - Class of instruction
            (byte) 0xA4, // INS	- Instruction - Instruction code
            (byte) 0x04, // P1	- Parameter 1 - Instruction parameter 1
            (byte) 0x00, // P2	- Parameter 2 - Instruction parameter 2
            (byte) 0x07, // Lc field	- Number of bytes present in the data field of the command
            (byte) 0xD2, (byte) 0x76, (byte) 0x00, (byte) 0x00, (byte) 0x85, (byte) 0x01,
            (byte) 0x01, // NDEF Tag Application name D2 76 00 00 85 01 01
            (byte) 0x00  // Le field	- Maximum number of bytes expected in the data field of
            // the response to the command
    };

    private static final byte[] SELECT_CAPABILITY_CONTAINER = {
            (byte) 0x00, // CLA	- Class - Class of instruction
            (byte) 0xa4, // INS	- Instruction - Instruction code
            (byte) 0x00, // P1	- Parameter 1 - Instruction parameter 1
            (byte) 0x0c, // P2	- Parameter 2 - Instruction parameter 2
            (byte) 0x02, // Lc field	- Number of bytes present in the data field of the command
            (byte) 0xe1, (byte) 0x03 // file identifier of the CC file
    };

    private static final byte[] SELECT_NDEF_FILE = {
            (byte) 0x00, // CLA	- Class - Class of instruction
            (byte) 0xa4, // Instruction byte (INS) for Select command
            (byte) 0x00, // Parameter byte (P1), select by identifier
            (byte) 0x0c, // Parameter byte (P1), select by identifier
            (byte) 0x02, // Lc field	- Number of bytes present in the data field of the command
            (byte) 0xE1, (byte) 0x04 // file identifier of the NDEF file retrieved from the CC file
    };

    private final static byte[] CAPABILITY_CONTAINER_FILE = new byte[]{
            0x00, 0x0f, // CCLEN
            0x20, // Mapping Version
            0x00, 0x3b, // Maximum R-APDU data size
            0x00, 0x34, // Maximum C-APDU data size
            0x04, 0x06, // Tag & Length
            (byte) 0xe1, 0x04, // NDEF File Identifier
            (byte) 0x00, (byte) 0xff, // Maximum NDEF size, do NOT extend this value
            0x00, // NDEF file read access granted
            (byte) 0xff, // NDEF File write access denied
    };

    // Status Word success
    private final static byte[] SUCCESS_SW = new byte[]{
            (byte) 0x90,
            (byte) 0x00,
    };
    // Status Word failure
    private final static byte[] FAILURE_SW = new byte[]{
            (byte) 0x6a,
            (byte) 0x82,
    };
    public static String mNdefMessage = "default NDEF-message----test123456";
    private byte[] mNdefRecordFile;
    private boolean mAppSelected; // true when SELECT_APPLICATION detected
    private boolean mCcSelected; // true when SELECT_CAPABILITY_CONTAINER detected
    private boolean mNdefSelected; // true when SELECT_NDEF_FILE detected

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
        mAppSelected = false;
        mCcSelected = false;
        mNdefSelected = false;

        // default NDEF-message
        NdefMessage ndefDefaultMessage = getNdefMessage(mNdefMessage);
        // the maximum length is 246 so do not extend this value
        int nlen = ndefDefaultMessage.getByteArrayLength();
        mNdefRecordFile = new byte[nlen + 2];
        mNdefRecordFile[0] = (byte) ((nlen & 0xff00) / 256);
        mNdefRecordFile[1] = (byte) (nlen & 0xff);
        System.arraycopy(ndefDefaultMessage.toByteArray(), 0, mNdefRecordFile, 2,
                ndefDefaultMessage.getByteArrayLength());
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent != null) {
            // intent contains a text message
            if (intent.hasExtra("ndefMessage")) {
                String msg = intent.getStringExtra("ndefMessage");
                NdefMessage ndefMessage = ndefMessage = getNdefMessage(intent.getStringExtra("ndefMessage"));
                ;
                Log.d(TAG, "onStartCommand msg:" + msg);
                if (ndefMessage != null) {
                    int nlen = ndefMessage.getByteArrayLength();
                    mNdefRecordFile = new byte[nlen + 2];
                    mNdefRecordFile[0] = (byte) ((nlen & 0xff00) / 256);
                    mNdefRecordFile[1] = (byte) (nlen & 0xff);
                    System.arraycopy(ndefMessage.toByteArray(), 0, mNdefRecordFile, 2,
                            ndefMessage.getByteArrayLength());
                }
            }
            // intent contains an URL
            if (intent.hasExtra("ndefUrl")) {
                NdefMessage ndefMessage = getNdefUrlMessage(intent.getStringExtra("ndefUrl"));
                if (ndefMessage != null) {
                    int nlen = ndefMessage.getByteArrayLength();
                    mNdefRecordFile = new byte[nlen + 2];
                    mNdefRecordFile[0] = (byte) ((nlen & 0xff00) / 256);
                    mNdefRecordFile[1] = (byte) (nlen & 0xff);
                    System.arraycopy(ndefMessage.toByteArray(), 0, mNdefRecordFile, 2,
                            ndefMessage.getByteArrayLength());
                }
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

    private NdefMessage getNdefMessage(String ndefData) {
        NdefRecord ndefRecord = NdefRecord.createTextRecord("en", ndefData);
        return new NdefMessage(ndefRecord);
    }

    private NdefMessage getNdefUrlMessage(String ndefData) {
        if (ndefData.length() == 0) {
            return null;
        }
        NdefRecord ndefRecord;
        ndefRecord = NdefRecord.createUri(ndefData);
        return new NdefMessage(ndefRecord);
    }

    /**
     * emulates an NFC Forum Tag Type 4
     */
    @Override
    public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
        //if (Arrays.equals(SELECT_APP, commandApdu)) {
        // check if commandApdu qualifies for SELECT_APPLICATION
        if (Arrays.equals(SELECT_APPLICATION, commandApdu)) {
            mAppSelected = true;
            mCcSelected = false;
            mNdefSelected = false;
            return SUCCESS_SW;
            // check if commandApdu qualifies for SELECT_CAPABILITY_CONTAINER
        } else if (mAppSelected && Arrays.equals(SELECT_CAPABILITY_CONTAINER, commandApdu)) {
            mCcSelected = true;
            mNdefSelected = false;
            return SUCCESS_SW;
            // check if commandApdu qualifies for SELECT_NDEF_FILE
        } else if (mAppSelected && Arrays.equals(SELECT_NDEF_FILE, commandApdu)) {
            // NDEF
            mCcSelected = false;
            mNdefSelected = true;
            return SUCCESS_SW;
            // check if commandApdu qualifies for // READ_BINARY
        } else if (commandApdu[0] == (byte) 0x00 && commandApdu[1] == (byte) 0xb0) {
            // READ_BINARY
            // get the offset an le (length) data
            //System.out.println("** " + Utils.bytesToHex(commandApdu) + " in else if
            // (commandApdu[0] == (byte)0x00 && commandApdu[1] == (byte)0xb0) {");
            int offset = (0x00ff & commandApdu[2]) * 256 + (0x00ff & commandApdu[3]);
            int le = 0x00ff & commandApdu[4];

            byte[] responseApdu = new byte[le + SUCCESS_SW.length];

            if (mCcSelected && offset == 0 && le == CAPABILITY_CONTAINER_FILE.length) {
                System.arraycopy(CAPABILITY_CONTAINER_FILE, offset, responseApdu, 0, le);
                System.arraycopy(SUCCESS_SW, 0, responseApdu, le, SUCCESS_SW.length);
                return responseApdu;
            } else if (mNdefSelected) {
                if (offset + le <= mNdefRecordFile.length) {
                    System.arraycopy(mNdefRecordFile, offset, responseApdu, 0, le);
                    System.arraycopy(SUCCESS_SW, 0, responseApdu, le, SUCCESS_SW.length);
                    return responseApdu;
                }
            }
        }

        // The tag should return different errors for different reasons
        // this emulation just returns the general error message
        return FAILURE_SW;
    }

    /**
     * onDeactivated is called when reading ends
     * reset the status boolean values
     */
    @Override
    public void onDeactivated(int reason) {
        mAppSelected = false;
        mCcSelected = false;
        mNdefSelected = false;
    }
}

4.1 AndroidManifest.xml为NdefHceService增加service注册,并添加meta-data  ndef_apduservice.xml,以设置AID让nfc-service可以转发读卡请求给该HCE服务。

4.2 增加NFC permission及设置NdefHceServce为disable状态,当需要激活时再使能该服务。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.NFC" />
    <uses-feature
        android:name="android.hardware.nfc.hce"
        android:required="true" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.HceNdefTag"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:theme="@style/Theme.HceNdefTag">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".NdefHceService"
            android:enabled="false"
            android:exported="true"
            android:permission="android.permission.BIND_NFC_SERVICE">
            <intent-filter>
                <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
                <!-- category required!!! this was not included in official android HCE documentation -->
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>

            <meta-data
                android:name="android.nfc.cardemulation.host_apdu_service"
                android:resource="@xml/ndef_apduservice" />
        </service>
    </application>

</manifest>

5. ndef_apduservice.xml中设置读卡是亮屏读卡还是灭屏读卡,及设置aid, 这里aid D2760000850101是Android上ndef读卡通用的aid。

<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:requireDeviceScreenOn="true"
    android:requireDeviceUnlock="false">
    <aid-group
        android:category="other">
        <!-- Sample for the demo application -->
        <aid-filter android:name="D2760000850101" />
    </aid-group>
</host-apdu-service>

6. MainActivity.java增加使能HceNdefService的逻辑。至此完成基本功能开发。

package com.example.hcendeftag;

import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

import android.view.View;

import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;

import com.example.hcendeftag.databinding.ActivityMainBinding;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private AppBarConfiguration appBarConfiguration;
    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        setSupportActionBar(binding.toolbar);

        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);

        binding.fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                enableComponent(getPackageManager(), NdefHceService.COMPONENT);
                Toast.makeText(MainActivity.this, "Enable NdefService", Toast.LENGTH_SHORT).show();
            }
        });
    }

    public static void enableComponent(PackageManager pm, ComponentName component) {
        pm.setComponentEnabledSetting(
                component,
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                PackageManager.DONT_KILL_APP);
    }
}

7.编译APK安装后,点击按钮使能NdefHceService并用另一台NFC 手机APP去读卡。

7.1一个手机安装后如下图点击APP按钮去使能Ndef服务。

7.2 另一个支持NFC功能的手机安装NFC tool pro APP,背靠背读取另一台手机的ndef数据。

demo 代码及apk见下面链接

链接: https://pan.baidu.com/s/14Hw46nhzBz8ll9q-0gFpaQ?pwd=4cm5 提取码: 4cm5

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值