Android实现类Apple Pay虚拟卡

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Apple Pay H5是苹果公司推出的一种移动支付解决方案,可以让用户通过手机在网页上完成支付。它结合了苹果设备的安全功能和苹果支付平台的便利性,为用户提供更快速、更安全的在线支付体验。 使用Apple Pay H5进行支付非常简单。用户只需在支持Apple Pay H5的网站上选择使用Apple Pay进行支付,然后通过苹果设备上的Touch ID或Face ID进行身份验证。一旦身份验证成功,支付信息通过加密方式传输到支付平台完成交易,无需输入信用或账号信息,极大地提升了支付安全性。 除了便捷和安全性,Apple Pay H5还提供了更好的支付体验。用户可以在购物流程中使用苹果设备的钱包应用来管理他们的支付,包括信用、借记和礼品,简化了支付过程。用户无需记住各种账号和密码,只需使用已经保存在苹果设备中的支付信息即可完成支付。 Apple Pay H5也具有广泛的适用性。作为一种跨平台解决方案,它支持不同的浏览器和操作系统,用户无需担心兼容性问题。因此,商家在接入Apple Pay H5后,可以为更多的用户提供便捷的支付方式,吸引更多的消费者。 综上所述,Apple Pay H5是苹果公司推出的一种移动支付解决方案,具有便捷、安全和良好的适应性。它为用户提供了更好的支付体验,为商家提供了更多的支付选择,有助于推动移动支付的普及和发展。 ### 回答2: Apple Pay H5是苹果公司推出的一种支付方式。它结合了苹果的iOS系统和移动网页技术,使用户可以在网页上使用Apple Pay进行支付。 与传统的Apple Pay相比,Apple Pay H5无需下载和安装特定的App,只需要在支持Apple Pay的网站上选择Apple Pay作为支付方式,并使用设备上保存的信用或借记信息进行支付。用户在支付过程中需要使用设备上的Touch ID或Face ID进行身份验证,确保安全性和可靠性。 通过Apple Pay H5,用户可以在电商平台上更方便地完成购买。与传统的输入个人信用信息不同,Apple Pay H5直接使用用户在Apple Wallet中保存的片信息,减少了输入信息的复杂性,提高了支付速度。 Apple Pay H5具有很高的安全性。苹果公司采用了多层次的安全措施,包括设备上的Touch ID和Face ID、设备和Apple服务器之间的加密信道等,确保用户的支付信息不会被泄露或被盗用。 Apple Pay H5目前已经在一些电商平台上得到了广泛应用。用户只需要使用支持Apple Pay的浏览器,即可享受快捷、安全的支付体验。对于商家来说,集成Apple Pay H5可以提高用户支付的便利性和购买欲望,增加交易成功率。 总之,Apple Pay H5是一种方便、安全的支付方式,通过结合iOS系统和移动网页技术,为用户提供了更好的支付体验。 ### 回答3: Apple Pay H5是苹果公司推出的一种线上支付解决方案。它结合了Apple Pay和H5技术,使用户能够通过手机或其他移动设备进行线上支付。 Apple Pay是一种近场通信(NFC)支付技术,旨在提供便捷、安全的支付方式。用户可以在支持Apple Pay的商家店铺中,使用iPhone或Apple Watch进行支付。只需将设备靠近POS机,通过指纹识别或Face ID(面容识别)进行身份验证即可完成支付。这一技术在线下支付场景中得到了广泛应用。 而H5技术是一种基于网页的开发技术,允许开发者在不需要用户下载和安装应用的情况下,通过网页实现一系列交互功能。因此,Apple Pay H5是将Apple Pay应用于网页支付的一种解决方案。 Apple Pay H5的成功无疑为用户提供了更便利、快捷的支付方式。用户可以通过打开支持Apple Pay H5的商户网站,选择Apple Pay作为支付方式,并通过手机或其他移动设备完成支付。而无需下载安装应用程序,可以节省用户的存储空间和下载时间。 此外,Apple Pay H5也提供了更高的安全性。借助借助Apple Pay的安全机制,用户可以通过指纹识别或面容识别进行身份验证,避免了传统网页支付中输入密码可能存在的风险,提供了更加安全的支付环境。 综上所述,Apple Pay H5是一种结合了Apple Pay和H5技术的线上支付解决方案。它既具有Apple Pay的便捷和安全性,又能够通过网页实现支付功能,为用户提供更快捷、便利的支付体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值