Android开发Android知识程序员

Android实现类Apple Pay虚拟卡

2016-08-23  本文已影响268人  Louis_陆

相信大家早已对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 的通信并不熟悉,阅读起来会十分枯燥困难,所以做新技术研发的过程中,非常考验一个开发者的耐心和能力(因为可参考的资料甚少,且所需要的新知识库比较大)

上一篇下一篇

猜你喜欢

热点阅读