NFC卡片模拟HarmonyOS APP开发实战

举报
Jack20 发表于 2026/06/19 20:18:22 2026/06/19
【摘要】 NFC卡片模拟HarmonyOS APP开发实战上一篇我们聊了NFC标签的读写,今天换个角度——让你的手机变成一张NFC卡。这就是NFC卡模拟(Card Emulation)技术,它让手机可以模拟门禁卡、公交卡、银行卡等,实现"刷手机"的便捷体验。咱们来看看鸿蒙系统中的NFC卡模拟如何实现。 一、背景与动机 1.1 卡模拟的应用场景NFC卡模拟技术让手机可以替代实体卡,应用场景非常广泛:应...

NFC卡片模拟HarmonyOS APP开发实战

上一篇我们聊了NFC标签的读写,今天换个角度——让你的手机变成一张NFC卡。这就是NFC卡模拟(Card Emulation)技术,它让手机可以模拟门禁卡、公交卡、银行卡等,实现"刷手机"的便捷体验。咱们来看看鸿蒙系统中的NFC卡模拟如何实现。

一、背景与动机

1.1 卡模拟的应用场景

NFC卡模拟技术让手机可以替代实体卡,应用场景非常广泛:

应用场景 卡片类型 优势
门禁系统 Mifare、IC卡 不用带门禁卡,手机即卡
公交地铁 交通卡 充值方便,查询记录
银行支付 银行卡 安全便捷,支持云闪付
停车场 停车卡 自动缴费,无感通行
会员卡 会员卡 积分查询,优惠推送

1.2 卡模拟的技术原理

NFC卡模拟有两种主要模式:
图片.png

两种模式对比

对比项 SE模式 HCE模式
安全性 极高(硬件加密) 较高(软件加密)
灵活性 低(受限于SE) 高(完全可定制)
兼容性 好(标准协议) 需要应用支持
成本 需要SE芯片 无额外成本
典型应用 银行卡、公交卡 门禁卡、会员卡

1.3 卡模拟流程

图片.png

二、核心原理

2.1 APDU指令

APDU(Application Protocol Data Unit)是智能卡通信的标准指令格式:

// APDU指令结构
interface ApduCommand {
  // 类别字节(Class Byte)
  CLA: number;
  
  // 指令字节(Instruction Byte)
  INS: number;
  
  // 参数字节(Parameter Bytes)
  P1: number;
  P2: number;
  
  // 数据长度(Length)
  Lc?: number;
  
  // 数据字段(Data)
  Data?: number[];
  
  // 期望响应长度(Expected Length)
  Le?: number;
}

// APDU响应结构
interface ApduResponse {
  // 响应数据
  data: number[];
  
  // 状态字(Status Word)
  SW1: number;
  SW2: number;
}

// 常见状态字
const STATUS_WORDS = {
  SUCCESS: [0x90, 0x00],           // 执行成功
  MORE_DATA: [0x61, 0x00],         // 还有数据
  WRONG_LENGTH: [0x6C, 0x00],      // 长度错误
  WRONG_INS: [0x6D, 0x00],         // 指令不支持
  WRONG_CLA: [0x6E, 0x00],         // 类别不支持
  WRONG_P1P2: [0x6B, 0x00],        // 参数错误
  NOT_ALLOWED: [0x6A, 0x82],       // 操作不允许
  NOT_FOUND: [0x6A, 0x82],         // 文件未找到
  WRONG_DATA: [0x6A, 0x80],        // 数据错误
  MEMORY_ERROR: [0x92, 0x00]       // 内存错误
};

2.2 常见APDU指令

// 常见APDU指令
const APDU_COMMANDS = {
  // 选择文件
  SELECT: {
    CLA: 0x00,
    INS: 0xA4,
    P1: 0x04,
    P2: 0x00
  },
  
  // 读取二进制
  READ_BINARY: {
    CLA: 0x00,
    INS: 0xB0,
    P1: 0x00,
    P2: 0x00
  },
  
  // 写入二进制
  WRITE_BINARY: {
    CLA: 0x00,
    INS: 0xD0,
    P1: 0x00,
    P2: 0x00
  },
  
  // 获取响应
  GET_RESPONSE: {
    CLA: 0x00,
    INS: 0xC0,
    P1: 0x00,
    P2: 0x00
  },
  
  // 验证PIN
  VERIFY: {
    CLA: 0x00,
    INS: 0x20,
    P1: 0x00,
    P2: 0x00
  },
  
  // 读取记录
  READ_RECORD: {
    CLA: 0x00,
    INS: 0xB2,
    P1: 0x00,
    P2: 0x04
  }
};

/**
 * 构建选择文件APDU
 */
function buildSelectCommand(aid: number[]): ApduCommand {
  return {
    CLA: 0x00,
    INS: 0xA4,
    P1: 0x04,
    P2: 0x00,
    Lc: aid.length,
    Data: aid
  };
}

/**
 * 构建读取APDU
 */
function buildReadCommand(offset: number, length: number): ApduCommand {
  return {
    CLA: 0x00,
    INS: 0xB0,
    P1: (offset >> 8) & 0xFF,
    P2: offset & 0xFF,
    Le: length
  };
}

2.3 AID(Application Identifier)

AID用于标识卡上的应用:

// 常见AID
const AID = {
  // 支付系统
  VISA: [0xA0, 0x00, 0x00, 0x00, 0x00, 0x10, 0x10, 0x00],
  MASTERCARD: [0xA0, 0x00, 0x00, 0x00, 0x04, 0x10, 0x10, 0x00],
  UNIONPAY: [0xA0, 0x00, 0x00, 0x03, 0x33, 0x01, 0x01],
  
  // 交通卡
  BEIJING_BUS: [0xA0, 0x00, 0x00, 0x00, 0x03, 0x86, 0x98, 0x07, 0x01],
  SHANGHAI_METRO: [0xA0, 0x00, 0x00, 0x00, 0x03, 0x86, 0x98, 0x07, 0x02],
  
  // 门禁卡(自定义)
  ACCESS_CONTROL: [0xA0, 0x00, 0x00, 0x00, 0x00, 0x01]
};

三、代码实战

3.1 HCE服务实现

import nfc from '@ohos.nfc';
import { BusinessError } from '@ohos.base';

/**
 * HCE服务基类
 * 提供APDU处理的基础框架
 */
export abstract class HceService {
  // 服务AID
  protected abstract aid: number[];
  
  // 服务名称
  protected abstract serviceName: string;

  /**
   * 处理APDU指令
   * 子类需要实现具体的处理逻辑
   */
  protected abstract processApdu(apdu: ApduCommand): Promise<ApduResponse>;

  /**
   * 获取服务AID
   */
  public getAid(): number[] {
    return this.aid;
  }

  /**
   * 获取服务名称
   */
  public getServiceName(): string {
    return this.serviceName;
  }

  /**
   * 解析APDU指令
   */
  protected parseApdu(data: number[]): ApduCommand {
    if (data.length < 4) {
      throw new Error('APDU数据长度不足');
    }

    const apdu: ApduCommand = {
      CLA: data[0],
      INS: data[1],
      P1: data[2],
      P2: data[3]
    };

    if (data.length === 4) {
      // 无数据,无Le
      return apdu;
    } else if (data.length === 5) {
      // 只有Le
      apdu.Le = data[4];
    } else {
      // 有Lc和Data
      apdu.Lc = data[4];
      apdu.Data = data.slice(5, 5 + apdu.Lc);
      
      if (data.length > 5 + apdu.Lc) {
        apdu.Le = data[5 + apdu.Lc];
      }
    }

    return apdu;
  }

  /**
   * 构建响应
   */
  protected buildResponse(data: number[], sw: number[] = [0x90, 0x00]): number[] {
    return [...data, ...sw];
  }

  /**
   * 构建成功响应
   */
  protected buildSuccessResponse(data: number[] = []): number[] {
    return this.buildResponse(data, STATUS_WORDS.SUCCESS);
  }

  /**
   * 构建错误响应
   */
  protected buildErrorResponse(sw: number[]): number[] {
    return sw;
  }
}

/**
 * 门禁卡HCE服务
 * 模拟简单的门禁卡
 */
export class AccessCardService extends HceService {
  protected aid = [0xA0, 0x00, 0x00, 0x00, 0x00, 0x01];
  protected serviceName = 'AccessCard';

  // 卡片数据
  private cardId: string = '';
  private cardData: Map<string, number[]> = new Map();

  /**
   * 设置卡片ID
   */
  public setCardId(id: string): void {
    this.cardId = id;
    console.info(`[门禁卡] 设置卡号: ${id}`);
  }

  /**
   * 设置卡片数据
   */
  public setCardData(key: string, data: number[]): void {
    this.cardData.set(key, data);
  }

  /**
   * 处理APDU指令
   */
  protected async processApdu(apdu: ApduCommand): Promise<ApduResponse> {
    console.info(`[门禁卡] 收到指令: CLA=${apdu.CLA.toString(16)}, INS=${apdu.INS.toString(16)}`);

    switch (apdu.INS) {
      case 0xA4: // SELECT
        return this.handleSelect(apdu);
      
      case 0xB0: // READ BINARY
        return this.handleReadBinary(apdu);
      
      case 0xD0: // WRITE BINARY
        return this.handleWriteBinary(apdu);
      
      case 0xC0: // GET RESPONSE
        return this.handleGetResponse(apdu);
      
      default:
        return {
          data: [],
          SW1: 0x6D,
          SW2: 0x00
        };
    }
  }

  /**
   * 处理选择指令
   */
  private handleSelect(apdu: ApduCommand): ApduResponse {
    // 检查AID是否匹配
    if (apdu.Data && this.arraysEqual(apdu.Data, this.aid)) {
      console.info('[门禁卡] AID匹配,选择成功');
      return {
        data: [],
        SW1: 0x90,
        SW2: 0x00
      };
    }
    
    return {
      data: [],
      SW1: 0x6A,
      SW2: 0x82
    };
  }

  /**
   * 处理读取指令
   */
  private handleReadBinary(apdu: ApduCommand): ApduResponse {
    const offset = (apdu.P1 << 8) | apdu.P2;
    const length = apdu.Le || 16;

    // 返回卡片ID
    const cardIdBytes = this.stringToBytes(this.cardId);
    
    if (offset < cardIdBytes.length) {
      const end = Math.min(offset + length, cardIdBytes.length);
      const data = cardIdBytes.slice(offset, end);
      
      return {
        data: data,
        SW1: 0x90,
        SW2: 0x00
      };
    }

    return {
      data: [],
      SW1: 0x6A,
      SW2: 0x82
    };
  }

  /**
   * 处理写入指令
   */
  private handleWriteBinary(apdu: ApduCommand): ApduResponse {
    // 门禁卡通常不允许写入
    return {
      data: [],
      SW1: 0x6A,
      SW2: 0x82
    };
  }

  /**
   * 处理获取响应指令
   */
  private handleGetResponse(apdu: ApduCommand): ApduResponse {
    // 返回之前的数据
    return {
      data: [],
      SW1: 0x90,
      SW2: 0x00
    };
  }

  /**
   * 工具方法
   */
  private arraysEqual(a: number[], b: number[]): boolean {
    if (a.length !== b.length) return false;
    return a.every((val, idx) => val === b[idx]);
  }

  private stringToBytes(str: string): number[] {
    return Array.from(new TextEncoder().encode(str));
  }
}

/**
 * HCE管理器
 * 管理HCE服务的注册和APDU分发
 */
export class HceManager {
  private static instance: HceManager;
  
  // 注册的服务列表
  private services: Map<string, HceService> = new Map();
  
  // 当前选中的服务
  private selectedService: HceService | null = null;
  
  // NFC卡模拟实例
  private cardEmulation: nfc.CardEmulation | null = null;

  private constructor() {}

  /**
   * 获取单例实例
   */
  public static getInstance(): HceManager {
    if (!HceManager.instance) {
      HceManager.instance = new HceManager();
    }
    return HceManager.instance;
  }

  /**
   * 初始化HCE
   */
  public async init(): Promise<boolean> {
    try {
      // 创建卡模拟实例
      this.cardEmulation = nfc.createCardEmulation();
      
      // 设置APDU处理回调
      this.cardEmulation.on('hceApduData', async (data: ArrayBuffer) => {
        await this.handleApduData(data);
      });

      console.info('[HCE] 初始化成功');
      return true;
    } catch (error) {
      console.error('[HCE] 初始化失败:', error);
      return false;
    }
  }

  /**
   * 注册HCE服务
   */
  public async registerService(service: HceService): Promise<boolean> {
    try {
      const aid = service.getAid();
      const aidStr = aid.map(b => b.toString(16).padStart(2, '0')).join(':');
      
      this.services.set(aidStr, service);
      
      // 注册AID路由
      if (this.cardEmulation) {
        await this.cardEmulation.registerAids(aid);
      }
      
      console.info(`[HCE] 注册服务: ${service.getServiceName()}, AID: ${aidStr}`);
      return true;
    } catch (error) {
      console.error('[HCE] 注册服务失败:', error);
      return false;
    }
  }

  /**
   * 注销HCE服务
   */
  public async unregisterService(service: HceService): Promise<boolean> {
    try {
      const aid = service.getAid();
      const aidStr = aid.map(b => b.toString(16).padStart(2, '0')).join(':');
      
      this.services.delete(aidStr);
      
      if (this.cardEmulation) {
        await this.cardEmulation.unregisterAids(aid);
      }
      
      console.info(`[HCE] 注销服务: ${service.getServiceName()}`);
      return true;
    } catch (error) {
      console.error('[HCE] 注销服务失败:', error);
      return false;
    }
  }

  /**
   * 处理APDU数据
   */
  private async handleApduData(data: ArrayBuffer): Promise<void> {
    try {
      // 转换为字节数组
      const bytes = Array.from(new Uint8Array(data));
      
      // 解析APDU
      const apdu = this.parseApdu(bytes);
      
      // 根据指令类型分发
      if (apdu.INS === 0xA4) {
        // SELECT指令,选择对应的服务
        await this.handleSelect(apdu);
      } else if (this.selectedService) {
        // 其他指令,由选中的服务处理
        const response = await this.selectedService.processApdu(apdu);
        await this.sendResponse(response);
      } else {
        // 未选择服务,返回错误
        await this.sendResponse({
          data: [],
          SW1: 0x6E,
          SW2: 0x00
        });
      }
    } catch (error) {
      console.error('[HCE] 处理APDU失败:', error);
      await this.sendResponse({
        data: [],
        SW1: 0x6F,
        SW2: 0x00
      });
    }
  }

  /**
   * 处理选择指令
   */
  private async handleSelect(apdu: ApduCommand): Promise<void> {
    if (!apdu.Data) {
      await this.sendResponse({
        data: [],
        SW1: 0x6A,
        SW2: 0x82
      });
      return;
    }

    // 查找匹配的服务
    const aidStr = apdu.Data.map(b => b.toString(16).padStart(2, '0')).join(':');
    const service = this.services.get(aidStr);
    
    if (service) {
      this.selectedService = service;
      console.info(`[HCE] 选择服务: ${service.getServiceName()}`);
      
      await this.sendResponse({
        data: [],
        SW1: 0x90,
        SW2: 0x00
      });
    } else {
      this.selectedService = null;
      await this.sendResponse({
        data: [],
        SW1: 0x6A,
        SW2: 0x82
      });
    }
  }

  /**
   * 发送响应
   */
  private async sendResponse(response: ApduResponse): Promise<void> {
    if (!this.cardEmulation) return;
    
    const data = [...response.data, response.SW1, response.SW2];
    const buffer = new Uint8Array(data).buffer;
    
    await this.cardEmulation.sendApduResponse(buffer);
  }

  /**
   * 解析APDU
   */
  private parseApdu(data: number[]): ApduCommand {
    const apdu: ApduCommand = {
      CLA: data[0],
      INS: data[1],
      P1: data[2],
      P2: data[3]
    };

    if (data.length === 4) {
      return apdu;
    } else if (data.length === 5) {
      apdu.Le = data[4];
    } else {
      apdu.Lc = data[4];
      apdu.Data = data.slice(5, 5 + apdu.Lc);
      if (data.length > 5 + apdu.Lc) {
        apdu.Le = data[5 + apdu.Lc];
      }
    }

    return apdu;
  }

  /**
   * 检查是否支持HCE
   */
  public async isHceSupported(): Promise<boolean> {
    try {
      return nfc.isHceSupported();
    } catch (error) {
      return false;
    }
  }

  /**
   * 设置默认服务
   */
  public async setDefaultService(service: HceService): Promise<boolean> {
    try {
      if (!this.cardEmulation) return false;
      
      const aid = service.getAid();
      await this.cardEmulation.setDefaultAid(aid);
      
      console.info(`[HCE] 设置默认服务: ${service.getServiceName()}`);
      return true;
    } catch (error) {
      console.error('[HCE] 设置默认服务失败:', error);
      return false;
    }
  }
}

3.2 卡模拟管理UI

import { HceManager, AccessCardService } from './HceManager';

@Entry
@Component
struct CardEmulationPage {
  private hceManager: HceManager = HceManager.getInstance();
  private accessCardService: AccessCardService = new AccessCardService();
  
  @State isHceSupported: boolean = false;
  @State isServiceRegistered: boolean = false;
  @State cardId: string = '';
  @State statusText: string = '';

  aboutToAppear(): void {
    this.initHce();
  }

  aboutToDisappear(): void {
    if (this.isServiceRegistered) {
      this.hceManager.unregisterService(this.accessCardService);
    }
  }

  /**
   * 初始化HCE
   */
  async initHce(): Promise<void> {
    this.isHceSupported = await this.hceManager.isHceSupported();
    
    if (this.isHceSupported) {
      const success = await this.hceManager.init();
      if (success) {
        this.statusText = 'HCE已就绪';
      }
    } else {
      this.statusText = '设备不支持HCE';
    }
  }

  /**
   * 注册门禁卡服务
   */
  async registerService(): Promise<void> {
    if (!this.cardId) {
      this.statusText = '请输入卡号';
      return;
    }
    
    // 设置卡号
    this.accessCardService.setCardId(this.cardId);
    
    // 注册服务
    const success = await this.hceManager.registerService(this.accessCardService);
    
    if (success) {
      this.isServiceRegistered = true;
      this.statusText = '服务已注册,可以刷卡了';
      
      // 设置为默认服务
      await this.hceManager.setDefaultService(this.accessCardService);
    } else {
      this.statusText = '注册失败';
    }
  }

  /**
   * 注销服务
   */
  async unregisterService(): Promise<void> {
    const success = await this.hceManager.unregisterService(this.accessCardService);
    
    if (success) {
      this.isServiceRegistered = false;
      this.statusText = '服务已注销';
    }
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('NFC卡模拟')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
      }
      .width('100%')
      .padding(20)
      .backgroundColor('#1A1A2E')

      // 状态显示
      Column() {
        Row() {
          Circle()
            .width(12)
            .height(12)
            .fill(this.isHceSupported ? '#4CAF50' : '#FF5722')

          Text(this.isHceSupported ? 'HCE支持' : 'HCE不支持')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .margin({ left: 10 })
        }
        .width('100%')
        .margin({ bottom: 15 })

        Text(this.statusText)
          .fontSize(14)
          .fontColor('#AAAAAA')
          .width('100%')
          .textAlign(TextAlign.Center)
      }
      .width('90%')
      .padding(20)
      .backgroundColor('#16213E')
      .borderRadius(12)
      .margin({ top: 20 })

      if (this.isHceSupported) {
        // 卡号输入
        Column() {
          Text('门禁卡号')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .width('100%')
            .margin({ bottom: 10 })

          TextInput({ text: $$this.cardId, placeholder: '请输入卡号' })
            .width('100%')
            .height(50)
            .backgroundColor('#1A1A2E')
            .fontColor('#FFFFFF')
            .enabled(!this.isServiceRegistered)
        }
        .width('90%')
        .padding(20)
        .backgroundColor('#16213E')
        .borderRadius(12)
        .margin({ top: 20 })

        // 操作按钮
        if (this.isServiceRegistered) {
          Column() {
            Text('门禁卡模拟中...')
              .fontSize(18)
              .fontColor('#4CAF50')
              .margin({ bottom: 20 })

            Text('请将手机靠近读卡器')
              .fontSize(14)
              .fontColor('#AAAAAA')
              .margin({ bottom: 30 })

            Button('停止模拟')
              .width('100%')
              .height(50)
              .backgroundColor('#FF5722')
              .onClick(() => {
                this.unregisterService();
              })
          }
          .width('90%')
          .padding(20)
          .backgroundColor('#16213E')
          .borderRadius(12)
          .margin({ top: 20 })
        } else {
          Button('开始模拟')
            .width('90%')
            .height(50)
            .backgroundColor('#4A90E2')
            .onClick(() => {
              this.registerService();
            })
            .margin({ top: 20 })
        }

        // 使用说明
        Column() {
          Text('使用说明')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .width('100%')
            .margin({ bottom: 15 })

          Text('1. 输入门禁卡号')
            .fontSize(14)
            .fontColor('#AAAAAA')
            .width('100%')
            .margin({ bottom: 8 })

          Text('2. 点击"开始模拟"')
            .fontSize(14)
            .fontColor('#AAAAAA')
            .width('100%')
            .margin({ bottom: 8 })

          Text('3. 将手机靠近门禁读卡器')
            .fontSize(14)
            .fontColor('#AAAAAA')
            .width('100%')
            .margin({ bottom: 8 })

          Text('4. 听到"滴"声表示刷卡成功')
            .fontSize(14)
            .fontColor('#AAAAAA')
            .width('100%')
        }
        .width('90%')
        .padding(20)
        .backgroundColor('#16213E')
        .borderRadius(12)
        .margin({ top: 20 })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0F0F1A')
  }
}

3.3 交通卡模拟示例

/**
 * 交通卡HCE服务
 * 模拟公交/地铁卡
 */
export class TransitCardService extends HceService {
  protected aid = [0xA0, 0x00, 0x00, 0x00, 0x03, 0x86, 0x98, 0x07, 0x01];
  protected serviceName = 'TransitCard';

  // 卡片信息
  private cardNumber: string = '';
  private balance: number = 0;
  private transactionLog: Array<{ time: number, amount: number, type: string }> = [];

  /**
   * 设置卡片信息
   */
  public setCardInfo(number: string, balance: number): void {
    this.cardNumber = number;
    this.balance = balance;
    console.info(`[交通卡] 卡号: ${number}, 余额: ${balance}`);
  }

  /**
   * 充值
   */
  public recharge(amount: number): void {
    this.balance += amount;
    this.transactionLog.push({
      time: Date.now(),
      amount: amount,
      type: 'recharge'
    });
    console.info(`[交通卡] 充值 ${amount},当前余额: ${this.balance}`);
  }

  /**
   * 消费
   */
  public consume(amount: number): boolean {
    if (this.balance < amount) {
      console.warn('[交通卡] 余额不足');
      return false;
    }
    
    this.balance -= amount;
    this.transactionLog.push({
      time: Date.now(),
      amount: -amount,
      type: 'consume'
    });
    console.info(`[交通卡] 消费 ${amount},当前余额: ${this.balance}`);
    return true;
  }

  /**
   * 获取余额
   */
  public getBalance(): number {
    return this.balance;
  }

  /**
   * 获取交易记录
   */
  public getTransactionLog(): Array<{ time: number, amount: number, type: string }> {
    return this.transactionLog;
  }

  /**
   * 处理APDU指令
   */
  protected async processApdu(apdu: ApduCommand): Promise<ApduResponse> {
    console.info(`[交通卡] 收到指令: INS=${apdu.INS.toString(16)}`);

    switch (apdu.INS) {
      case 0xA4: // SELECT
        return this.handleSelect(apdu);
      
      case 0xB0: // READ BINARY
        return this.handleReadBinary(apdu);
      
      case 0xD6: // UPDATE BINARY
        return this.handleUpdateBinary(apdu);
      
      case 0xB2: // READ RECORD
        return this.handleReadRecord(apdu);
      
      default:
        return {
          data: [],
          SW1: 0x6D,
          SW2: 0x00
        };
    }
  }

  private handleSelect(apdu: ApduCommand): ApduResponse {
    return {
      data: [0x00, 0x00, 0x00, 0x00], // FCI数据
      SW1: 0x90,
      SW2: 0x00
    };
  }

  private handleReadBinary(apdu: ApduCommand): ApduResponse {
    const offset = (apdu.P1 << 8) | apdu.P2;
    
    // 根据偏移返回不同数据
    if (offset === 0x0000) {
      // 返回卡片基本信息
      const data = [
        ...this.stringToBytes(this.cardNumber),
        ...this.numberToBytes(this.balance, 4)
      ];
      return { data: data, SW1: 0x90, SW2: 0x00 };
    }
    
    return { data: [], SW1: 0x6A, SW2: 0x82 };
  }

  private handleUpdateBinary(apdu: ApduCommand): ApduResponse {
    // 处理扣款/充值
    if (apdu.Data && apdu.Data.length >= 4) {
      const amount = this.bytesToNumber(apdu.Data.slice(0, 4));
      
      if (apdu.P2 === 0x01) {
        // 扣款
        if (this.consume(amount)) {
          return { data: [], SW1: 0x90, SW2: 0x00 };
        } else {
          return { data: [], SW1: 0x6A, SW2: 0x84 }; // 余额不足
        }
      } else if (apdu.P2 === 0x02) {
        // 充值
        this.recharge(amount);
        return { data: [], SW1: 0x90, SW2: 0x00 };
      }
    }
    
    return { data: [], SW1: 0x6A, SW2: 0x80 };
  }

  private handleReadRecord(apdu: ApduCommand): ApduResponse {
    // 返回交易记录
    const recordNum = apdu.P1;
    
    if (recordNum > 0 && recordNum <= this.transactionLog.length) {
      const log = this.transactionLog[recordNum - 1];
      const data = [
        ...this.numberToBytes(log.time, 4),
        ...this.numberToBytes(Math.abs(log.amount), 4),
        log.type === 'recharge' ? 0x01 : 0x02
      ];
      return { data: data, SW1: 0x90, SW2: 0x00 };
    }
    
    return { data: [], SW1: 0x6A, SW2: 0x83 }; // 记录未找到
  }

  private stringToBytes(str: string): number[] {
    return Array.from(new TextEncoder().encode(str));
  }

  private numberToBytes(num: number, length: number): number[] {
    const bytes: number[] = [];
    for (let i = 0; i < length; i++) {
      bytes.push((num >> (8 * (length - 1 - i))) & 0xFF);
    }
    return bytes;
  }

  private bytesToNumber(bytes: number[]): number {
    let num = 0;
    for (let i = 0; i < bytes.length; i++) {
      num = (num << 8) | bytes[i];
    }
    return num;
  }
}

四、踩坑与注意事项

4.1 权限配置

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.NFC",
        "reason": "使用NFC功能"
      },
      {
        "name": "ohos.permission.NFC_CARD_EMULATION",
        "reason": "NFC卡模拟"
      },
      {
        "name": "ohos.permission.SECURE_ELEMENT",
        "reason": "访问安全芯片(SE模式需要)"
      }
    ]
  }
}

4.2 屏幕状态处理

坑点:屏幕关闭时HCE可能不工作。

/**
 * 监听屏幕状态
 */
import display from '@ohos.display';

async function setupScreenStateListener(): Promise<void> {
  display.on('change', (data: display.Display) => {
    if (data.state === display.DisplayState.STATE_ON) {
      console.info('[HCE] 屏幕开启,HCE可用');
      // 重新注册HCE服务
    } else if (data.state === display.DisplayState.STATE_OFF) {
      console.info('[HCE] 屏幕关闭');
      // HCE可能受限
    }
  });
}

4.3 多服务冲突处理

坑点:多个应用注册相同AID会冲突。

/**
 * 检查AID冲突
 */
async function checkAidConflict(aid: number[]): Promise<boolean> {
  try {
    const registered = await nfc.getRegisteredAids();
    const aidStr = aid.map(b => b.toString(16).padStart(2, '0')).join(':');
    
    if (registered.includes(aidStr)) {
      console.warn(`[HCE] AID ${aidStr} 已被注册`);
      return true;
    }
    
    return false;
  } catch (error) {
    return false;
  }
}

4.4 响应超时处理

坑点:APDU响应需要在超时时间内返回。

/**
 * 带超时的APDU处理
 */
async function processApduWithTimeout(
  apdu: ApduCommand,
  timeout: number = 5000
): Promise<ApduResponse> {
  return new Promise(async (resolve, reject) => {
    const timer = setTimeout(() => {
      resolve({
        data: [],
        SW1: 0x6F,
        SW2: 0x00
      });
    }, timeout);

    try {
      const response = await processApdu(apdu);
      clearTimeout(timer);
      resolve(response);
    } catch (error) {
      clearTimeout(timer);
      resolve({
        data: [],
        SW1: 0x6F,
        SW2: 0x00
      });
    }
  });
}

4.5 安全考虑

坑点:敏感数据需要加密存储。

/**
 * 安全存储卡片数据
 */
import cryptoFramework from '@ohos.security.cryptoFramework';

async function secureStoreCardData(key: string, data: string): Promise<void> {
  // 加密数据
  const cipher = cryptoFramework.createCipher('AES256|GCM|PKCS7');
  // 加密逻辑...
  
  // 存储加密后的数据
  // ...
}

async function secureLoadCardData(key: string): Promise<string> {
  // 读取加密数据
  // ...
  
  // 解密数据
  // ...
  
  return '';
}

五、HarmonyOS 6适配

5.1 API变更

API HarmonyOS 5 HarmonyOS 6 说明
registerAids() 单个AID 支持AID数组 批量注册
sendApduResponse() 同步发送 支持异步 非阻塞发送
新增 - getActiveService() 获取当前激活服务

适配代码

/**
 * HarmonyOS 6 HCE适配
 */
async function registerAidsAdaptive(aids: number[][]): Promise<boolean> {
  const apiVersion = getApiVersion();

  try {
    if (apiVersion >= 6) {
      // HarmonyOS 6 批量注册
      return await cardEmulation.registerAids(aids);
    } else {
      // HarmonyOS 5 逐个注册
      for (const aid of aids) {
        await cardEmulation.registerAids(aid);
      }
      return true;
    }
  } catch (error) {
    console.error('[HCE] 注册失败:', error);
    return false;
  }
}

5.2 新增功能

HarmonyOS 6增强了卡模拟能力:

// 1. 多服务管理
const activeService = await cardEmulation.getActiveService();
console.info(`当前服务: ${activeService.serviceName}`);

// 2. 服务优先级
await cardEmulation.setServicePriority(serviceAid, 100);

// 3. 自动激活规则
await cardEmulation.setAutoActivationRule({
  type: 'location',
  condition: { latitude: 39.9, longitude: 116.4, radius: 1000 }
});

// 4. 交易通知
cardEmulation.on('transaction', (data: { type: string, amount: number }) => {
  console.info(`交易: ${data.type}, 金额: ${data.amount}`);
  // 发送通知给用户
});

5.3 性能优化

// 1. 预加载卡片数据
await cardEmulation.preloadCardData(cardData);

// 2. 快速响应模式
await cardEmulation.setFastResponseMode(true);

// 3. 缓存APDU响应
cardEmulation.enableResponseCache(true);

六、总结

NFC卡模拟技术让手机具备了替代实体卡的能力,为用户带来极大的便利。本文全面讲解了鸿蒙系统中卡模拟的核心要点:

核心要点回顾

  1. HCE原理:理解APDU指令和响应机制
  2. 服务实现:正确处理SELECT、READ、WRITE等指令
  3. AID路由:合理设置应用标识符和路由规则
  4. 安全考虑:敏感数据加密存储,传输加密
  5. 状态管理:处理屏幕状态、应用状态对HCE的影响

最佳实践建议

  • 封装HCE服务基类,简化具体服务实现
  • 实现完善的APDU解析和响应构建
  • 做好错误处理和超时机制
  • 注意安全性和隐私保护

下一步学习

  • 红外遥控技术(下一篇文章)
  • SE安全芯片编程
  • 银行卡支付协议
  • 多卡片管理

NFC卡模拟看似复杂,但掌握了APDU机制后,实现起来其实有章可循。希望本文能帮助你掌握卡模拟的核心技能,让你的应用具备"刷卡"的能力!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。