NFC卡片模拟HarmonyOS APP开发实战
【摘要】 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卡模拟有两种主要模式:

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

二、核心原理
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卡模拟技术让手机具备了替代实体卡的能力,为用户带来极大的便利。本文全面讲解了鸿蒙系统中卡模拟的核心要点:
核心要点回顾:
- HCE原理:理解APDU指令和响应机制
- 服务实现:正确处理SELECT、READ、WRITE等指令
- AID路由:合理设置应用标识符和路由规则
- 安全考虑:敏感数据加密存储,传输加密
- 状态管理:处理屏幕状态、应用状态对HCE的影响
最佳实践建议:
- 封装HCE服务基类,简化具体服务实现
- 实现完善的APDU解析和响应构建
- 做好错误处理和超时机制
- 注意安全性和隐私保护
下一步学习:
- 红外遥控技术(下一篇文章)
- SE安全芯片编程
- 银行卡支付协议
- 多卡片管理
NFC卡模拟看似复杂,但掌握了APDU机制后,实现起来其实有章可循。希望本文能帮助你掌握卡模拟的核心技能,让你的应用具备"刷卡"的能力!
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)