相信大家早已对Apple Pay感到不陌生,其实早在Apple Pay流行于中国之前,谷歌早已推出 HostApduService 接口,为我们开发者提供了实现虚拟卡的方向。笔者也早早地赶上了这个潮流~
其中的技术 涉及基于ISODep、NfcA技术的NFC开发,HostApduService接口的调用,基于ISO/IEC、14443-4协议的应用层Apdu的通信,sm4加密算法 等
本节内容,笔者将给大家分享 HCE(Host-based Card Emulation) 的开发。
关于 HCE 的原理介绍,读者需自行搜索。本文主要讲解开发流程,谷歌给出的API文档:API文档 。
首先,我们要 manifest 文件中申明我们的虚拟卡服务
<service android:name=".MyHostApduService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice"/>
</service>
当然一些用到的系统权限也需要加上
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.VIBRATE"/>
然后在 xml 文件中实现 apduservice
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:apduServiceBanner="@mipmap/ic_launcher"
android:description="@string/servicedesc"
android:requireDeviceUnlock="true">
<!--The requireDeviceUnlock attribute can be used to specify that the device must be unlocked before this service can be invoked to handle APDUs.-->
<aid-group android:category="payment"
android:description="@string/aiddescription">
<!-- "2PAY.SYS.DDF01" is the name below in hex -->
<aid-filter android:name="325041592E5359532E4444463031"
android:description="@string/PPSE"/>
<!--VISA MSD AID-->
<aid-filter android:name="A0000000031010"
android:description="@string/Visa" />
</aid-group>
</host-apdu-service>
接着实现 HostApduService 接口
/**
* AID不对,会导致processCommandApdu方法不被调用,因为命令和AID对应不上,服务不做响应,
* AID应和指令配对使用。只要成功调用了select command指令,之后即可随意交互。
*/
public class MyHostApduService extends HostApduService implements SharedPreferences.OnSharedPreferenceChangeListener {
// private boolean isFound = false;
// String DEFAULT_SWIPE_DATA = "%B4046460664629718^000NETSPEND^161012100000181000000?;4046460664629718=16101210000018100000?";
//you can send a response APDU by returning the bytes of the response APDU from processCommandApdu()
@Override
public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
byte[] responseApdu = null;
Context context = getApplicationContext();
/**
* 注意commandApdu是十六进制,只能使用0~9,a~f
* substring(2,4)---2~4-1
*/
//修改卡号
if (Util.bytesToHexString(commandApdu).substring(2).startsWith("fa")) {
String newCardNumber = Util.getSavedSwipeData(context);//得到现在的数据
// 替换 B 至 ^ 之间的数字
newCardNumber = newCardNumber.replace(newCardNumber.substring(newCardNumber.indexOf("B") + 1, newCardNumber.indexOf("^")), Util.bytesToHexString(commandApdu).substring(4));
// 替换 ; 至 = 之间的数字
newCardNumber = newCardNumber.replace(newCardNumber.substring(newCardNumber.indexOf(";") + 1, newCardNumber.indexOf("=")), Util.bytesToHexString(commandApdu).substring(4));
Util.sendLog("修改卡号的newSwipeData数据: ");
Util.sendLog(newCardNumber);
Util.storeNewSwipeData(context, newCardNumber);
responseApdu = Commands.SUCCESS;
Util.sendLog("修改卡号命令", commandApdu, responseApdu);
}
//修改姓名
else if (Util.bytesToHexString(commandApdu).substring(2).startsWith("fb")) {
String newName = Util.getSavedSwipeData(context);//得到现在的数据
// 替换 ^ 至 ^ 之间的字符
newName = newName.replace(newName.substring(newName.indexOf("^") + 1, newName.lastIndexOf("^")), Util.bytesToHexString(commandApdu).substring(4));
Util.sendLog("修改姓名的newSwipeData数据: ");
Util.sendLog(newName);
Util.storeNewSwipeData(context, newName);
responseApdu = Commands.SUCCESS;
Util.sendLog("修改姓名命令", commandApdu, responseApdu);
}
//修改刷卡记录
else if (Util.bytesToHexString(commandApdu).substring(2).startsWith("fc")) {
String newSwipeData = Util.getSavedSwipeData(context);//得到现在的数据
// 替换 ^ 至 ?前面的0 之间的数字
newSwipeData = newSwipeData.replace(newSwipeData.substring(newSwipeData.lastIndexOf("^") + 1, newSwipeData.indexOf("?") - 1), Util.bytesToHexString(commandApdu).substring(4));
// 替换 = 至 ? 之间的数字
newSwipeData = newSwipeData.replace(newSwipeData.substring(newSwipeData.lastIndexOf("=") + 1, newSwipeData.length() - 1), Util.bytesToHexString(commandApdu).substring(4));
Util.sendLog("修改刷卡记录的newSwipeData数据: ");
Util.sendLog(newSwipeData);
Util.storeNewSwipeData(context, newSwipeData);
responseApdu = Commands.SUCCESS;
Util.sendLog("修改刷卡记录命令", commandApdu, responseApdu);
} else if (Arrays.equals(Commands.PPSE_APDU_SELECT, commandApdu)) {
responseApdu = Commands.PPSE_APDU_SELECT_RESP;
Util.sendLog("PPSE_APDU_SELECT", commandApdu, responseApdu);
} else if (Arrays.equals(Commands.VISA_MSD_SELECT, commandApdu)) {
responseApdu = Commands.VISA_MSD_SELECT_RESPONSE;
Util.sendLog("VISA_MSD_SELECT", commandApdu, responseApdu);
} else if (Commands.isGpoCommand(commandApdu)) {
responseApdu = Commands.GPO_COMMAND_RESPONSE;
Util.sendLog("GPO_COMMAND_SELECT", commandApdu, responseApdu);
} else if (Arrays.equals(Commands.READ_REC_COMMAND, commandApdu)) {
responseApdu = Commands.readRecResponse;
Util.sendLog("READ_REC_COMMAND_SELECT", commandApdu, responseApdu);
} else if (Arrays.equals(Commands.NUMBER_SEND, commandApdu)) {//test 0x01
responseApdu = Commands.NUMBER_RESP;// 0x02
Util.sendLog("Test Select", commandApdu, responseApdu);
} else {
responseApdu = Commands.ISO7816_UNKNOWN_ERROR_RESPONSE;
Util.sendLog("Received Unhandled", commandApdu, responseApdu);
}
return responseApdu;
}
@Override
public void onDeactivated(int reason) {
Message message = new Message();
message.obj = "onDeactivated: " + reason + "\n";
MainActivity.handler.handleMessage(message);
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Util.sendLog("onSharedPreferenceChanged: key= " + key);
if (SWIPE_DATA_PREF_KEY.equals(key)) {
String swipeData = sharedPreferences.getString(SWIPE_DATA_PREF_KEY, DEFAULT_SWIPE_DATA);
Commands.configureReadRecResponse(swipeData);
}
}
public void onCreate() {
super.onCreate();
Util.sendLog(" HostApduService onCreate");
// Attempt to get swipe data that SetCardActivity saved as a shared preference,
// otherwise use the default no-balance prepaid visa configured into the app.
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
String swipeData = prefs.getString(SWIPE_DATA_PREF_KEY, DEFAULT_SWIPE_DATA);
Commands.configureReadRecResponse(swipeData);
prefs.registerOnSharedPreferenceChangeListener(this);
}
}
其中用到的一些常量
public class Constants {
//
// We include a prepaid Visa debit card with no balance so the app has a card
// configured until the user switches to their own card:
//
public static final String DEFAULT_SWIPE_DATA = "%B4046460664629718^000NETSPEND^161012100000181000000?;4046460664629718=16101210000018100000?";
//
// Key used to store the user's Swipe data in the app's shared preferences
//
public static final String SWIPE_DATA_PREF_KEY = "SWIPE_DATA";
}
其中用到的一些命令
public class Commands {
public static final byte[] PPSE_APDU_SELECT = {
(byte) 0x00, // CLA (class of command)
(byte) 0xA4, // INS (instruction); A4 = select
(byte) 0x04, // P1 (parameter 1) (0x04: select by name)
(byte) 0x00, // P2 (parameter 2)
(byte) 0x0E, // LC (length of data) 14 (0x0E) = length("2PAY.SYS.DDF01")
// 2PAY.SYS.DDF01 (ASCII values of characters used):
// This value requests the card or payment device to list the application
// identifiers (AIDs) it supports in the response:
'2', 'P', 'A', 'Y', '.', 'S', 'Y', 'S', '.', 'D', 'D', 'F', '0', '1',
(byte) 0x00 // LE (max length of expected result, 0 implies 256)
};
public static final byte[] PPSE_APDU_SELECT_RESP = {
(byte) 0x6F, // FCI Template
(byte) 0x23, // length = 35
(byte) 0x84, // DF Name
(byte) 0x0E, // length("2PAY.SYS.DDF01")
// Data (ASCII values of characters used):
'w', 'a', 'n', 'p', 'i', 'S', 'i', '.', 'n', 'f', 'c', 't', 'e', 's',
(byte) 0xA5, // FCI Proprietary Template
(byte) 0x11, // length = 17
(byte) 0xBF, // FCI Issuer Discretionary Data
(byte) 0x0C, // length = 12
(byte) 0x0E,
(byte) 0x61, // Directory Entry
(byte) 0x0C, // Entry length = 12
(byte) 0x4F, // ADF Name
(byte) 0x07, // ADF Length = 7
// Tell the POS (point of sale terminal) that we support the standard
// Visa credit or debit applet: A0000000031010
// Visa's RID (Registered application provider IDentifier) is 5 bytes:
(byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x03,
// PIX (Proprietary application Identifier eXtension) is the last 2 bytes.
// 10 10 (means visa credit or debit)
(byte) 0x10, (byte) 0x10,
(byte) 0x87, // Application Priority Indicator
(byte) 0x01, // length = 1
(byte) 0x01,
(byte) 0x90, // SW1 (90 00 = Success)
(byte) 0x00 // SW2
};
public static final byte[] VISA_MSD_SELECT = {
(byte) 0x00, // CLA
(byte) 0xa4, // INS
(byte) 0x04, // P1
(byte) 0x00, // P2
(byte) 0x07, // LC (data length = 7)
// POS is selecting the AID (Visa debit or credit) that we specified in the PPSE
// response:
(byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x10, (byte) 0x10,
(byte) 0x00 // LE
};
public static final byte[] VISA_MSD_SELECT_RESPONSE = {
(byte) 0x6F, // File Control Information (FCI) Template
(byte) 0x1E, // length = 30 (0x1E)
(byte) 0x84, // Dedicated File (DF) Name
(byte) 0x07, // DF length = 7
// A0000000031010 (Visa debit or credit AID)
(byte) 0xA0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x03, (byte) 0x10, (byte) 0x10,
(byte) 0xA5, // File Control Information (FCI) Proprietary Template
(byte) 0x13, // length = 19 (0x13)
(byte) 0x50, // Application Label
(byte) 0x0B, // length
'V', 'I', 'S', 'A', ' ', 'C', 'R', 'E', 'D', 'I', 'T',
(byte) 0x9F, (byte) 0x38, // Processing Options Data Object List (PDOL)
(byte) 0x03, // length
(byte) 0x9F, (byte) 0x66, (byte) 0x02, // PDOL value (Does this request terminal type?)
(byte) 0x90, // SW1
(byte) 0x00 // SW2
};
public static boolean isGpoCommand(byte[] apdu) {
return (apdu.length > 4 &&
apdu[0] == GPO_COMMAND[0] &&
apdu[1] == GPO_COMMAND[1] &&
apdu[2] == GPO_COMMAND[2] &&
apdu[3] == GPO_COMMAND[3]
);
}
public static final byte[] GPO_COMMAND = {
(byte) 0x80, // CLA
(byte) 0xA8, // INS
(byte) 0x00, // P1
(byte) 0x00, // P2
(byte) 0x04, // LC (length)
// data
(byte) 0x83, // tag
(byte) 0x02, // length
(byte) 0x80, // { These 2 bytes can vary, so we'll only }
(byte) 0x00, // { compare the header of this GPO command below }
(byte) 0x00 // Le
};
public static final byte[] GPO_COMMAND_RESPONSE = {
(byte) 0x80,
(byte) 0x06, // length
(byte) 0x00,
(byte) 0x80,
(byte) 0x08,
(byte) 0x01,
(byte) 0x01,
(byte) 0x00,
(byte) 0x90, // SW1
(byte) 0x00 // SW2
};
public static final byte[] READ_REC_COMMAND = {
(byte) 0x00, // CLA
(byte) 0xB2, // INS
(byte) 0x01, // P1
(byte) 0x0C, // P2
(byte) 0x00 // length
};
public static final Pattern TRACK_2_PATTERN = Pattern.compile(".*;(\\d{12,19}=\\d{1,128})\\?.*");
/*
* Unlike the upper case commands above, the Read REC response changes depending on the track 2
* portion of the user's magnetic stripe data.
*/
public static byte[] readRecResponse = {};
/**
* <pre>
* 对应Constants.DEFAULT_SWIPE_DATA
*
* "%B4046460664629718^000NETSPEND^161012100000181000000?;
* 4046460664629718=16101210000018100000?";
*
* card number 12~19位 swipe data 7~128位(1610121:2016年10月,服务号121)
* response apdu: 70 15 57 13 4046460664629718 D 16101210000018100000 F 9000
* </pre>
*/
public static void configureReadRecResponse(String swipeData) {
Matcher matcher = TRACK_2_PATTERN.matcher(swipeData);
if (matcher.matches()) {
String track2EquivData = matcher.group(1);//4046460664629718=16101210000018100000?
// convert the track 2 data into the required byte representation
track2EquivData = track2EquivData.replace('=', 'D');
if (track2EquivData.length() % 2 != 0) {
// add an 'F' to make the hex string a whole number of bytes wide
track2EquivData += "F";
}
// Each binary byte is represented by 2 4-bit hex characters
int track2EquivByteLen = track2EquivData.length() / 2;
readRecResponse = new byte[6 + track2EquivByteLen];
ByteBuffer bb = ByteBuffer.wrap(readRecResponse);
bb.put((byte) 0x70); // EMV Record Template tag
bb.put((byte) (track2EquivByteLen + 2)); // Length with track 2 tag
bb.put((byte) 0x57); // Track 2 Equivalent Data tag
bb.put((byte) track2EquivByteLen); // Track 2 data length
bb.put(Util.hexToByteArray(track2EquivData)); // Track 2 equivalent data
bb.put((byte) 0x90); // SW1
bb.put((byte) 0x00); // SW2
} else {
Util.sendLog("PaymentService processed bad swipe data");
}
}
/**
* 测试
*/
public static final byte[] NUMBER_SEND = {(byte) 0x01};
public static final byte[] NUMBER_RESP = {(byte) 0x02};
public static final byte[] ISO7816_UNKNOWN_ERROR_RESPONSE = {
(byte) 0x6F, (byte) 0x00
};
//修改数据成功
public static final byte[] SUCCESS = {(byte) 0xff};
}
为了优化体验,提示用户选择 HCE 为默认支付卡
CardEmulation cardEmulationManager = CardEmulation.getInstance(NfcAdapter.getDefaultAdapter(this));
ComponentName paymentServiceComponent =new ComponentName(getApplicationContext(), MyHostApduService.class.getCanonicalName());
if (!cardEmulationManager.isDefaultServiceForCategory(paymentServiceComponent, CardEmulation.CATEGORY_PAYMENT)) {
Intent intent = new Intent(CardEmulation.ACTION_CHANGE_DEFAULT);
intent.putExtra(CardEmulation.EXTRA_CATEGORY, CardEmulation.CATEGORY_PAYMENT);
intent.putExtra(CardEmulation.EXTRA_SERVICE_COMPONENT, paymentServiceComponent);
startActivityForResult(intent, 0);
text += "请选择HCE为默认支付卡\n"
} else {
text += "HCE为默认支付卡\n";
}
以上贴出了许多关键的源码和指令,如果读者对 HCE 原理并不掌握,对 ISO/IEC、14443-4 协议的应用层 Apdu 的通信并不熟悉,阅读起来会十分枯燥困难,所以做新技术研发的过程中,非常考验一个开发者的耐心和能力(因为可参考的资料甚少,且所需要的新知识库比较大)
网友评论