微信小程序 NFC HCE卡模拟

需要实现带模拟一张智能卡(门禁卡或者其他业务卡),使用带NFC设备根据指定协议进行读取模拟卡数据(效果图如下):
微信小程序 NFC HCE卡模拟_第1张图片
微信小程序 NFC HCE卡模拟_第2张图片

微信小程序 NFC HCE卡模拟_第3张图片

1. 模拟卡设计
这里使用设备自带NFC模拟卡(HCE)模式,模拟出一张虚拟卡,类似华为钱包,applepay钱包等。选择添加的卡,提供读取。
1.1 数据包交互协议
类TLV数据包格式,及Tag Length Value(和银联IC卡返回数据协议类似)。
1.2 AID规定
固定AID=F223344556 (注意不要超过10为长度)
1.3 Apdu协议
1.3.1 select apdu header

00A40400

1.3.2 update apdu header

00B40400
交互Apdu格式:[apdu header]+[dataLength]+[data]+[status]

1.4 界面设计
HCE界面分为2个,以上图为例,第二幅图为提供选择需要被读卡,第三幅图为卡界面,开启HCE开始交互。

1.5 小程序端HCE代码片段

HCE封装核心模块 nfc_hce_core.js:

var comm = require('comm_util.js')
function Action(){
  Action_GETHCESTATUS=0;Action_STARTHCE=1;Action_SENDMESSAGE=2
  Action_RECEIVEMESSAGE=3;Action_STOPHCE=4
}
var Status=[
  {code:'0',msg:'OK'},
  { code: '13000', msg: '当前设备不支持 NFC' },
  { code: '13001', msg: '当前设备支持 NFC,但系统NFC开关未开启' },
  { code: '13002', msg: '当前设备支持 NFC,但不支持HCE' },
  { code: '13003', msg: 'AID 列表参数格式错误' },
  { code: '13004', msg: '未设置微信为默认NFC支付应用' },
  { code: '13005', msg: '返回的指令不合法' },
  { code: '13006', msg: '注册 AID 失败' }
]
class NfcHCECore{
  constructor(mContext,_aids,mMsgCallBack,onNfcMessageLinsener){
    this.context=mContext
    this.aids = _aids
    this.mCallBack = mMsgCallBack
    this.nfcMessageCallBack = onNfcMessageLinsener
  }
  //获取当前状态
  getNfcStatus(){
    var that=this
    wx.getHCEState({
      success:function(res){
        console.log('NfcHCECore-->getNfcStatus::success:',res)
        that._runCallBack(res.errCode, res.errMsg)
      },
      fail:function(err){
        console.error('NfcHCECore-->getNfcStatus::fail:', err)
        that.callError(err)
      }
    })
  }
  //开启HCE环境
  startNfcHCE(){
    var that = this
    wx.startHCE({
      aid_list: this.aids,
      success:function(res){
        console.log('NfcHCECore-->startNfcHCE::success:', res)
        that._runCallBack(res.errCode, res.errMsg)
      },
      fail:function(err){
        console.error('NfcHCECore-->startNfcHCE::fail:', err)
        that.callError(err)
      }
    })
  }
  //发消息
  sendNfcHCEMessage(hexApdu){
    console.log('开始发送发回')
    var that = this
    var byteArrays = comm.hex2Bytes(hexApdu)
    console.log(byteArrays.length)
    var retbuffer = new ArrayBuffer(byteArrays.length)
    var dataView = new DataView(retbuffer)
    for (var i = 0; i < dataView.byteLength; i++) {
      dataView.setInt8(i, byteArrays[i])
    }
    wx.sendHCEMessage({
      data: retbuffer,
      success:function(res){
        console.log('NfcHCECore-->sendNfcHCEMessage::success:', res)
        that._runCallBack(res.errCode, res.errMsg)
      },
      fail:function(err){
        console.error('NfcHCECore-->sendNfcHCEMessage::fail:', err)
        that.callError(err)
      }
    })
  }
  /**
   * 收到读卡器发来的消息
   */
  onNfcHCEMessageHadnler(){
    var that = this
    wx.onHCEMessage(function(res){
      console.log('NfcHCECore-->onHCEMessage:', res)
      that.nfcMessageCallBack(res.messageType, res.reason, comm.ab2hex(res.data))
    })
  }
  /**
   * 停止HCE环境
   */
  stopNfcHCE(){
    var that = this
    wx.stopHCE({
      success:function(res){
        console.log('NfcHCECore-->stopNfcHCE::success:', res)
        that._runCallBack(res.errCode,res.errMsg)
      },
      fail:function(err){
        console.error('NfcHCECore-->stopNfcHCE::fail:', err)
        that.callError(err)
      }
    })
  }
  simple(){
    var that = this
    wx.getHCEState({
      success:function(res){
        console.log('NfcHCECore-->simple::getHCEState:', res)
        console.log(that.aids)
        that._runCallBack(res.errCode, res.errMsg)
        wx.startHCE({
          aid_list: that.aids,
          success:function(res){
            console.log('NfcHCECore-->simple::startHCE:', res)
            that._runCallBack(res.errCode, res.errMsg)
            wx.onHCEMessage(function(res){
              console.log('NfcHCECore-->simple::onHCEMessage:', res)
              that.nfcMessageCallBack(res.messageType, res.reason, comm.ab2hex(res.data))
            })
          },
          fail:function(err){
            that.callError(err)
          }
        })
      },
      fail:function(err){
        that.callError(err)
      }
    })
  }
  callError(err){
    var that=this
    Status.forEach(function (value, index, list) {
      if (value.code === err.errCode) {
        that._runCallBack(value, value.msg)
      }
    })
  }
  _runCallBack(status,data){
      this.mCallBack(status,data)
  }
}
module.exports = NfcHCECore

工具模块 用来做数据处理等 comm_util.js:

/**
 * 生成指定长度随机数
 */
function genRandom(n) {
  let a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; //生成的随机数的集合  
  let res = [];
  for (let i = 0; i < n; i++) {
    let index = parseInt(Math.random() * (a.length));    //生成一个的随机索引,索引值的范围随数组a的长度而变化  
    res.push(a[index]);
    a.splice(index, 1)  //已选用的数,从数组a中移除, 实现去重复  
  }
  return res.join('');
} 

function isFunctinMethod(name) {
  if (name != undefined && typeof name === 'function') {
    return true
  }
  return false
}
const formatNumber = n => {
  n = n.toString()
  return n[1] ? n : '0' + n
}
// ArrayBuffer转16进度字符串
function ab2hex(buffer) {
  var hexArr = Array.prototype.map.call(
    new Uint8Array(buffer),
    function (bit) {
      return ('00' + bit.toString(16)).slice(-2)
    }
  )
  return hexArr.join('');
}
//十六进制字符串转字节数组  
function hex2Bytes(str) {
  var pos = 0;
  var len = str.length;
  if (len % 2 != 0) {
    return null;
  }
  len /= 2;
  var hexA = new Array();

  for (var i = 0; i < len; i++) {
    var s = str.substr(pos, 2);
    var v = parseInt(s, 16);
    hexA.push(v);
    pos += 2;
  }
  return hexA;
}
function hex2ArrayBuffer(hex){
  var pos = 0;
  var len = hex.length;
  if (len % 2 != 0) {
    return null;
  }
  len /= 2;
  var buffer = new ArrayBuffer(len)
  var dataview=new DataView(buffer)
  for (var i = 0; i < len; i++) {
    var s = hex.substr(pos, 2);
    var v = parseInt(s, 16);
    dataview.setInt16(i,v)
    pos += 2;
  }
  return buffer
}
//string转16进制
function stringToHex(str) {
  var val = "";
  for (var i = 0; i < str.length; i++) {
    if (val == "")
      val = str.charCodeAt(i).toString(16);
    else
      val += str.charCodeAt(i).toString(16);
  }
  return val;
}
//16进制转string
function hexCharCodeToStr(hexCharCodeStr) {
    var trimedStr = hexCharCodeStr.trim();
    var rawStr =
      trimedStr.substr(0, 2).toLowerCase() === "0x"
        ?
        trimedStr.substr(2)
        :
        trimedStr;
    var len = rawStr.length;
    if (len % 2 !== 0) {
        alert("Illegal Format ASCII Code!");
        return "";
    }
    var curCharCode;
    var resultStr = [];
    for (var i = 0; i < len; i = i + 2) {
        curCharCode = parseInt(rawStr.substr(i, 2), 16); // ASCII Code Value
        resultStr.push(String.fromCharCode(curCharCode));
    }
    return resultStr.join("");
}

function pad(num, n) {
  var len = num.toString().length;
  while (len < n) {
    num = "0" + num;
    len++;
  }
  return num;
}

function strToHexCharCode(str) {
    if (str === "")
        return "";
    var hexCharCode = [];
    hexCharCode.push("0x");
    for (var i = 0; i < str.length; i++) {
        hexCharCode.push((str.charCodeAt(i)).toString(16));
    }
    return hexCharCode.join("");
}
//string转byte数组
function stringToByteArray(str) {
  var bytes = new Array();
  var len, c;
  len = str.length;
  for (var i = 0; i < len; i++) {
    c = str.charCodeAt(i);
    if (c >= 0x010000 && c <= 0x10FFFF) {
      bytes.push(((c >> 18) & 0x07) | 0xF0);
      bytes.push(((c >> 12) & 0x3F) | 0x80);
      bytes.push(((c >> 6) & 0x3F) | 0x80);
      bytes.push((c & 0x3F) | 0x80);
    } else if (c >= 0x000800 && c <= 0x00FFFF) {
      bytes.push(((c >> 12) & 0x0F) | 0xE0);
      bytes.push(((c >> 6) & 0x3F) | 0x80);
      bytes.push((c & 0x3F) | 0x80);
    } else if (c >= 0x000080 && c <= 0x0007FF) {
      bytes.push(((c >> 6) & 0x1F) | 0xC0);
      bytes.push((c & 0x3F) | 0x80);
    } else {
      bytes.push(c & 0xFF);
    }
  }
  return bytes;
}
// byte数组转string
function byteToString(bytearr) {
  if (typeof arr === 'string') {
    return arr;
  }
  var str = '',
    _arr = arr;
  for (var i = 0; i < _arr.length; i++) {
    var one = _arr[i].toString(2),
      v = one.match(/^1+?(?=0)/);
    if (v && one.length == 8) {
      var bytesLength = v[0].length;
      var store = _arr[i].toString(2).slice(7 - bytesLength);
      for (var st = 1; st < bytesLength; st++) {
        store += _arr[st + i].toString(2).slice(2);
      }
      str += String.fromCharCode(parseInt(store, 2));
      i += bytesLength - 1;
    } else {
      str += String.fromCharCode(_arr[i]);
    }
  }
  return str;
}
//二进制转10
function bariny2Ten(byte){
  return parseInt(byte, 2)
}
function bariny2Hex(a){
  return parseInt(a, 16)
}
//10/16进制转2进制
function ten2Bariny(ten){
  return ten.toString(2)
}
function str2Hex(str){
  return parseInt(str, 10).toString(16)
}
//16进制转2进制
function hex2bariny(hex){
  return parseInt(hex, 16).toString(2)
}
module.exports = {
formatTime: formatTime,isFunctinMethod: isFunctinMethod,ab2hex: ab2hex,
hex2Bytes: hex2Bytes,stringToByteArray: stringToByteArray,byteToString: byteToString,hex2ArrayBuffer:hex2ArrayBuffer,bariny2Ten:bariny2Ten,bariny2Hex: bariny2Hex,ten2Bariny: ten2Bariny,str2Hex: str2Hex,hex2bariny: hex2bariny,genRandom: genRandom,stringToHex: stringToHex,hexToString: hexCharCodeToStr,pad: pad
}

卡交互页面逻辑 hcecard.js:
该js模块对应卡页面交互功能,读卡器使用nfc读卡模式会进入onHCEMessage()回调中,
返回二进制数据,此时使用console是无法打印出data,需要转成16进制才行。
流程:
获取NFC状态–>开启HCE模式–>接受读卡器消息–>发送消息给读卡器
具体API详见:
https://developers.weixin.qq.com/miniprogram/dev/api/nfc.html#wx.sendhcemessageobject

var comm = require('../../utils/comm_util.js')
var NfcHCECore = require('../../utils/nfc_hce_core.js')
var app=getApp()
var msg=''
var countdown = 120;
var timer=null
//倒计时 120s退出 关闭hce
var settime = function (that) {
  if (countdown == 0) {
    wx.navigateBack({})
    return;
  } else {
    that.setData({
      last_time: countdown
    })
    countdown--;
  }
  timer=setTimeout(function () {
    settime(that)
  }, 1000)
}
Page({
  //页面的初始数据
  data: {
    currentCard:null,
    content:'',
    last_time: '',
  },
  onLoad: function (options) {
    var cardKey = options.cardkey
    var cardbean=wx.getStorageSync(cardKey)
    console.log('cardbean=' ,cardbean)
    this.setData({
      currentCard: cardbean
    })
    wx.setNavigationBarTitle({
      title: "门禁卡:"+cardbean.cardName,
    })
    this.nfcHCECore = new NfcHCECore(this, [cardbean.AID], this.onOptMessageCallBack.bind(this), this.onHCEMessageCallBack.bind(this))
    console.log("-->initNFCHCE")
    this.nfcHCECore.simple()
  },
 //hce操作相关回调
  onOptMessageCallBack(code, _msg) {
    console.log('onOptMessageCallBack')
    if (code === 0) {
      console.log("执行成功!", _msg)
    } else {
      msg = msg + '执行失败code=' + code + ",msg=" + _msg + '\n'
    }
    this.setData({
      content: msg
    })
    this.resetTime()
  },
  resetTime(){
    clearTimeout(timer)
    countdown=120
    this.setData({
      last_time:'120'
    })
    settime(this)
  },
  //收到读卡器发送指令
  onHCEMessageCallBack(messageType, reason, hexData) {
    var that = this
    console.log('onHCEMessageCallBack')
    console.log("有读卡器读我,messageType=", messageType)
    if (messageType == 1) {
      msg = msg + "有读卡器读我,数据包:" + hexData + '\n'
      that.setData({
        content: msg
      })
      this.sendDataPackage()
    }
    this.resetTime()
  },
  //发送数据及包
  sendDataPackage() {
    var cardbean = this.data.currentCard
    console.log(comm.pad(2, 2))
    //组装TLV数据包
    var header = '00A40400'
    var hexCardName = comm.stringToHex('yanglika')
    hexCardName = plusZero(hexCardName)
    console.log('cardName=>', cardbean.cardName,';hexCardName=>' + hexCardName)
    var nameTag = '1F01'
    var len = comm.stringToHex(comm.pad((hexCardName.length / 2), 2))
    var cmdname = nameTag + len + hexCardName
    console.log('cmdname.TVL=>' + cmdname)
    var hexCardNo = comm.stringToHex(cardbean.cardNo)
    hexCardNo = plusZero(hexCardNo)
    console.log('cardNo=>', cardbean.cardNo,';hexCardNo=>' + hexCardNo)
    var noTag = '5F01'
    len = comm.stringToHex(comm.pad((hexCardNo.length / 2), 2))
    var cmdNo = noTag + len + hexCardNo
    console.log('cmdNo.TVL=>' + cmdNo)
    var hexCreateDate = comm.stringToHex(cardbean.createDate)
    hexCreateDate = plusZero(hexCreateDate)
    console.log('hexCreateDate=>' + hexCreateDate)
    var createDateTag = '5F02'
    len = comm.stringToHex(comm.pad((hexCreateDate.length / 2), 2))
    var cmdDate = createDateTag + len + hexCreateDate
    console.log('cmdDate.TVL=>' + cmdDate)
    var hexCardExp = comm.stringToHex(cardbean.cardExp)
    hexCardExp = plusZero(hexCardExp)
    console.log('hexCardExp=>' + hexCardExp)
    var hexCardExpTag = '9F01'
    len = comm.stringToHex(comm.pad((hexCardExp.length / 2), 2))
    var cmdExp = hexCardExpTag + len + hexCardExp
    console.log('cmdExp.TVL=>' + cmdExp)
    len = comm.stringToHex(((cmdname.length + cmdNo.length + cmdDate.length + cmdExp.length)/2).toString();
    console.log('len='+len)
    var status="9000"
    var sendcmd = (header + len + cmdname + cmdNo + cmdDate + cmdExp + status).toUpperCase()
    msg = msg + "卡片返回读卡器指令:" + sendcmd+ '\n'
    this.setData({
      content: msg
    })
    this.nfcHCECore.sendNfcHCEMessage(sendcmd)
  },
  onShow: function () {
    this.resetTime()
  },
  //生命周期函数--监听页面卸载
  onUnload: function () {
    this.resetTime()
    _stopHCE()
  }
})
//仅在安卓系统下有效。
function _stopHCE() {
  wx.stopHCE({
    success: function (res) {
      console.log(res)
    },
    fail: function (err) {
      console.error(err)
    }
  })
}
//补零
function plusZero(_str) {
  while (_str.length % 2 != 0){
    _str += "0"
  }
  return _str
} 

1.6 android 设备端NFC Reader
CardReader.java

@TargetApi(Build.VERSION_CODES.KITKAT)
public class CardReader implements NfcAdapter.ReaderCallback {
    private static final String TAG = "CardReader";
    // ISO-DEP command HEADER for selecting an AID.
    private static final String SAMPLE_LOYALTY_CARD_AID = "F223344556";
    //select 命令报文头
    private static final String SELECT_APDU_HEADER = "00A40400";
    // "OK" status word sent in response to SELECT AID command (0x9000)
    private static final byte[] SELECT_OK_SW = {(byte) 0x90, (byte) 0x00};
    private WeakReference mAccountCallback;

    public interface AccountCallback {
        public void onAccountReceived();
    }

    public LoyaltyCardReader(AccountCallback accountCallback) {
        mAccountCallback = new WeakReference(accountCallback);
    }
    @Override
    public void onTagDiscovered(Tag tag) {
        Log.i(TAG, "New tag discovered");
        MainActivity.Companion.getSb().append("New tag discovered"+"\n");
        IsoDep isoDep = IsoDep.get(tag);
        if (isoDep != null) {
            try {
                // Connect to the remote NFC device
                isoDep.connect();
                Log.i(TAG, "Requesting remote AID: " + SAMPLE_LOYALTY_CARD_AID);
                MainActivity.Companion.getSb().append("Requesting remote AID: " + SAMPLE_LOYALTY_CARD_AID+"\n");
                mAccountCallback.get().onAccountReceived();
                byte[] command = BuildSelectApdu(SAMPLE_LOYALTY_CARD_AID);
                // Send command to remote device
                Log.i(TAG, "Sending: " + ByteArrayToHexString(command));
                byte[] result = isoDep.transceive(command);
                Log.i(TAG, "result: " + ByteArrayToHexString(result));
                int resultLength = result.length;
                //获取状态码 9000为成功
                byte[] statusWord = {result[resultLength-2], result[resultLength-1]};
                if (Arrays.equals(SELECT_OK_SW, statusWord)) {
                    byte[] payload = Arrays.copyOf(result, resultLength-2);
                    // The remote NFC device will immediately respond with its stored account number
                    String accountNumber = ByteArrayToHexString(payload);
                    Log.i(TAG, "Received: " + accountNumber);
                    // Inform CardReaderFragment of received account number
                    MainActivity.Companion.getSb().append("Received 卡片返回指令: " + accountNumber+"\n");
                    HashMap tvlsData=TLVInterpreter.parser(payload);
                    StringBuilder sbu=new StringBuilder();
                    for(Map.Entry obj:tvlsData.entrySet()){
                        sbu.append(obj.getKey()+":"+obj.getValue()+"\n");
                    }
                    MainActivity.Companion.getSb().append("解析TLV:\n" + sbu.toString()+"\n");
                    mAccountCallback.get().onAccountReceived();
                }else{
                    MainActivity.Companion.getSb().append("Received: 卡片拒绝指令,msg:"+ByteArrayToHexString(result)+ "\n");
                    mAccountCallback.get().onAccountReceived();
                }
            } catch (IOException e) {
                Log.e(TAG, "Error communicating with card: " + e.toString());
            }
        }
    }

    public static byte[] BuildSelectApdu(String aid) {
        // Format: [CLASS | INSTRUCTION | PARAMETER 1 | PARAMETER 2 | LENGTH | DATA]
        return HexStringToByteArray(SELECT_APDU_HEADER + String.format("%02X", aid.length() / 2) + aid);
    }

    public static String ByteArrayToHexString(byte[] bytes) {
        final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
        char[] hexChars = new char[bytes.length * 2];
        int v;
        for ( int j = 0; j < bytes.length; j++ ) {
            v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

    public static byte[] HexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
}

TLV解析器 TLVInterpreter.java

public class TLVInterpreter {
    private static final String TAG = "LoyaltyCardReader";
    public static HashMap parser(byte[] datas){
        HashMap params=new HashMap<>();
        if(datas!=null&&datas.length>0){
            byte[] payload =datas;
            int resultLength = payload.length;
            Log.i(TAG,"payload.length:"+resultLength);
            Log.i(TAG,"parser:"+LoyaltyCardReader.ByteArrayToHexString(payload));
            byte[] apduHeader={payload[0],payload[1],payload[2],payload[3]};
            Log.i(TAG,"apduHeader:"+LoyaltyCardReader.ByteArrayToHexString(apduHeader));
            params.put("apduHeader",LoyaltyCardReader.ByteArrayToHexString(apduHeader));
            byte[] dataLen={payload[4],payload[5]};
            int len=Integer.parseInt(hexStringToString(LoyaltyCardReader.ByteArrayToHexString(dataLen)));
            Log.i(TAG,"dataLen:"+len);
            boolean flag=true;
            int startIndex=6;
            int lenEffect=1;
            while (flag){
                //取tag
                byte[] bTag={payload[startIndex],payload[startIndex+lenEffect]};
                String tag=LoyaltyCardReader.ByteArrayToHexString(bTag);
                Log.i(TAG,"tag="+tag);
                //取长度
                startIndex=startIndex+lenEffect+1;
                byte[] datalen={payload[startIndex],payload[startIndex+lenEffect]};
                Log.i(TAG,LoyaltyCardReader.ByteArrayToHexString(datalen));
                int dlen=Integer.parseInt(hexStringToString(LoyaltyCardReader.ByteArrayToHexString(datalen)));
                Log.i(TAG,"dlen="+dlen);
                //取数据
                startIndex=startIndex+lenEffect+1;
                byte[] data=new byte[dlen];
                for(int i=0;i"data="+dt);
                Log.i(TAG,"startIndex="+startIndex);
                params.put(tag,dt);
                if(resultLength==startIndex){
                    //读取到最后结束
                    flag=false;
                }
            }
        }
        return params;
    }

    /**
     * 字符串转换为16进制字符串
     *
     * @param s
     * @return
     */
    public static String stringToHexString(String s) {
        String str = "";
        for (int i = 0; i < s.length(); i++) {
            int ch = (int) s.charAt(i);
            String s4 = Integer.toHexString(ch);
            str = str + s4;
        }
        return str;
    }

    /**
     * 16进制字符串转换为字符串
     *
     * @param s
     * @return
     */
    public static String hexStringToString(String s) {
        if (s == null || s.equals("")) {
            return null;
        }
        s = s.replace(" ", "");
        byte[] baKeyword = new byte[s.length() / 2];
        for (int i = 0; i < baKeyword.length; i++) {
            try {
                baKeyword[i] = (byte) (0xff & Integer.parseInt(
                        s.substring(i * 2, i * 2 + 2), 16));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        try {
            s = new String(baKeyword, "gbk");
            new String();
        } catch (Exception e1) {
            e1.printStackTrace();
        }
        return s;
    }
}

你可能感兴趣的:(Android,微信小程序)