美文网首页Android开发Android知识程序员
Android实现类Apple Pay虚拟卡

Android实现类Apple Pay虚拟卡

作者: Louis_陆 | 来源:发表于2016-08-23 14:30 被阅读268次

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

    相关文章

      网友评论

        本文标题:Android实现类Apple Pay虚拟卡

        本文链接:https://www.haomeiwen.com/subject/blagsttx.html