拆解华为一碰传,制作 NFC 快捷标签
众所周知华子的生态尤其是万物互联是相当的好用,在网上看到的一碰传的nfc贴纸相当方便。
它本质上是标准的 NFC Forum Type 2 标签,里面写了两条 NDEF 记录:一条华为/荣耀支持页 URI,一条私有 MIME 记录。私有记录里是 44 字节 payload:固定头 + 电脑蓝牙 MAC + SHA-256(SN) + 4 字节机型 ID。
我手里只有一台华为鸿蒙手机和一台联想电脑。由于华为手机对nfc写功能要求比较严格,原本可以使用安卓手机通过软件来写,在华子上不能用,所以就需要借用。
开源地址Github
总体方案
安卓手机 + 官方一碰传助手
这是最省事的方式:0 将sn,mac地址和id做成二维码让华为一碰传助手来扫码,用手机写nfc,这个很简单,从抖音就能搜到。
NFC 射频模块 + 自己写
第二条路是用 NFC 读写模块,比如 PN532、ACR122U 等,连接到电脑,自己构造 NDEF 数据并写入。
我用的是基于PN532的PCR532模块
- 价格便宜,常见且资料多,集成图形化的通用操作界面;
- 支持 ISO14443A,能读写 NTAG213/215/216;
- 串口协议不算复杂,适合把整个链路拆开看;
- 可以用 Python 批量写、自动备份、自动校验。
NFC封装格式
首先,华为的一碰传本质是封装的电脑的网络信息,但是格式不是常见的封装方式。
网上的“坑”
动手之前,我搜了一圈“华为一碰传 NFC 标签制作”。能找到的教程大概分两类:
- 母卡克隆法:先用官方助手写一张“母卡”,再用 PN532/ACR122U 把母卡整盘 dump 出来,然后克隆到空白标签;
- NFC Tools 法:用手机 App 读一张现成标签,照着记录抄。
这些方法能成功,但都没说清楚格式。更麻烦的是,网上还流传着一些错误的“格式”:
- 说 payload 是
SN=xxx|MAC=xxx|MODELID=xxx的明文字符串; - 说第二条记录是 AAR(Android Application Record);
- 说 payload 里有华为内部校验字段,手工构造成功率很低,必须克隆母卡。
实际上网络的方式基本都无法实现使用射频模块通过这种底层数据这种方式来实现,而且官方似乎也没有公开这种封装算法,所以需要自己来获取。
反编译目标
寻找最直接的方式就是一碰传助手,这个是网上普遍可行的方式,所以逆向这个APK来获取封装格式是可行性最大的。
研究对象是官方“一碰传助手 1.0.4.0”,包名 com.huawei.touchshare,应用名 Huawei Share OneHop。
工具用的是 Python + Androguard 4.1.4,做静态分析,不修改、不重打包。项目里的脚本分三步:
| 脚本 | 作用 |
|---|---|
research/scan.py | 以 zip 方式列出 APK 内容,按字符串定位关键逻辑 |
research/explore.py | 遍历 DEX,列出 com.huawei.touchshare.* 相关类 |
research/decomp.py | 用 Androguard 反编译目标类,输出 research/decomp.txt |
需要说明的是,Androguard 的方法级反编译并不完美,decomp.txt 里不少方法只拿到了类结构和伪代码;但关键常量、字段偏移和调用关系已经足够还原格式了。
如果还需要更加专业的逆向,可以用REA来进行逆向。
关键类与调用链
反编译结果里,最重要的几个类如下:
| 类 | 作用 |
|---|---|
com.huawei.touchshare.ui.FNCTouchActivity | 主界面:解析扫码结果,组装 44 字节 payload,等待 NFC 标签 |
com.huawei.touchshare.f.b | 计算 SHA-256 |
com.huawei.touchshare.f.e | 十六进制解析、字符串分割、字段提取 |
com.huawei.touchshare.f.c | 创建 URI + MIME 两条 NDEF 记录,写入标签 |
调用链可以简化成:
扫码结果
→ 提取 mac / sn / pctype
→ 组装 44 字节 payload
→ 创建两条 NDEF 记录
→ 等待 NFC 标签
→ 写入并回调结果
生成核心payload
核心代码在 FNCTouchActivity 的 a(String p10, String p11, String p12) 方法里。把无关逻辑去掉后,大概是这样的:
this.q[0] = (byte) 0x00;
this.q[1] = (byte) 0x64; // 100
// p10 是 12 位十六进制 MAC,解析成 6 字节
if (p10.matches("[0-9A-Fa-f]{12}")) {
this.q[2] = hexPair(p10.charAt(0), p10.charAt(1));
this.q[3] = hexPair(p10.charAt(2), p10.charAt(3));
this.q[4] = hexPair(p10.charAt(4), p10.charAt(5));
this.q[5] = hexPair(p10.charAt(6), p10.charAt(7));
this.q[6] = hexPair(p10.charAt(8), p10.charAt(9));
this.q[7] = hexPair(p10.charAt(10), p10.charAt(11));
// p11 是 SN,取 SHA-256 的 32 字节,放到偏移 8
System.arraycopy(sha256(p11), 0, this.q, 8, 32);
// p12 是 8 位十六进制机型 ID,解析成 4 字节,放到偏移 40
byte[] model = hexStringToBytes(p12, p12.length());
this.q[40] = model[0];
this.q[41] = model[1];
this.q[42] = model[2];
this.q[43] = model[3];
}
于是 44 字节 payload 的结构就出来了:
| 偏移 | 长度 | 内容 | 说明 |
|---|---|---|---|
| 0 | 1 | 0x00 | 固定头 |
| 1 | 1 | 0x64 | 固定值 100 |
| 2–7 | 6 | 蓝牙 MAC | 原始 6 字节,不是 ASCII |
| 8–39 | 32 | SHA-256(SN) | 对 SN 的 ASCII 字节做哈希 |
| 40–43 | 4 | MODELID / pcType | 4 字节原始值,大端写入 |
这里有两个容易写错的地方:
- SHA-256 是对 SN 字符串的 ASCII 字节做哈希,不是对 SN 的十六进制解码结果做哈希;
- MAC 是 6 字节原始值,不是 12 个 ASCII 字符。比如
AA:BB:CC:DD:EE:FF写进去是AA BB CC DD EE FF。
两条 NDEF 记录
payload 组装好之后,com.huawei.touchshare.f.c 会创建两条 NDEF 记录:
NdefRecord[] records = new NdefRecord[2];
if (!String.valueOf(payload[42]).equals("5")) {
records[0] = NdefRecord.createUri("https://www.honor.cn/support/magic_link/");
} else {
records[0] = NdefRecord.createUri("https://consumer.huawei.com/cn/support/huaweishare/");
}
records[1] = NdefRecord.createMime(
"application/vnd.huawei.handoff.ndefmsg",
payload
);
NdefMessage msg = new NdefMessage(records);
关键结论:
- 只有两条记录:URI + MIME;
- 没有 AAR 记录。网上说的
android.com:pkg并不在这个标签里; - URI 是华为还是荣耀,取决于
payload[42]是否等于0x05; - 真正承载配对信息的是第二条 MIME 记录,MIME 类型是
application/vnd.huawei.handoff.ndefmsg。
另外,官方写入逻辑会先看标签是否支持 Ndef;不支持则尝试 NdefFormatable,再不行才报错。ST25 Type 5 标签还有单独的锁定流程。我们用的 NTAG213 属于已格式化的 Ndef,走的是最直接的分支。
payload[42] 是 4 字节 MODELID 的第三个字节。比如:
00000506→00 00 05 06→payload[42] = 0x05→ 华为 URI;00000804→00 00 08 04→payload[42] = 0x08→ 荣耀 URI。
这也解释了为什么有些教程里 00000505 和 00000506 都能用:它们的第三个字节都是 0x05,只是最后一个字节不同。
NFC 封装格式:136 字节到底怎么写
NDEF 记录逐字节拆解
以华为 URI 为例,两条记录的实际字节结构如下。
Record 1:URI
91 01 2C 55 04 63 6F 6E 73 75 6D 65 72 ...
| 字段 | 值 | 说明 |
|---|---|---|
| Header | 0x91 | MB=1,ME=0,SR=1,TNF=1 |
| Type Length | 0x01 | 类型名 1 字节 |
| Payload Length | 0x2C | 44 字节 |
| Type | 0x55 | ASCII 'U',URI 记录 |
| Payload | 04 ... | 首字节 0x04 表示 https://,后面是去掉 scheme 的 URL |
Record 2:MIME
52 26 2C 61 70 70 6C 69 63 61 74 69 6F 6E ...
| 字段 | 值 | 说明 |
|---|---|---|
| Header | 0x52 | MB=0,ME=1,SR=1,TNF=2 |
| Type Length | 0x26 | 38 字节 |
| Payload Length | 0x2C | 44 字节 |
| Type | application/vnd.huawei.handoff.ndefmsg | 华为私有 MIME |
| Payload | 44 字节 | 前面拆出来的 payload |
两条记录加起来:
48 + 85 = 133 字节
再加 TLV 头尾:
2 + 133 + 1 = 136 字节
正好是 34 页,从 page 4 写到 page 37。
安装华为电脑管家
标签只解决“手机读到什么”,电脑侧还得让华为电脑管家跑起来。联想笔记本直接装官方电脑管家通常会被拦截,这里用 huawei-pc-manager-bootstrap。
用 huawei-pc-manager-bootstrap 安装电脑管家
项目地址:https://github.com/hamflx/huawei-pc-manager-bootstrap
它的作用是解决非华为电脑无法安装华为电脑管家的问题。大致流程:
- 下载最新 release 里的
huawei-pc-manager-bootstrap; - 从华为官网下载电脑管家安装包;
- 把电脑管家安装包解压到与 bootstrap 可执行文件相同的目录;
- 右键以管理员身份运行 bootstrap;
- 点击“自动扫描”,确认它找到安装包,然后点击“安装”;
- 按提示完成电脑管家安装。
这个工具会通过 version.dll 劫持 GetSystemFirmwareTable 等函数,让电脑管家以为自己在华为电脑上。它属于第三方工具,使用前请自行评估风险;电脑管家安装包请从官方渠道下载。
这个在网上也有很多教程。
拿到电脑管家认的 SN
一碰传标签里存的是 SHA-256(SN)。手机把标签内容发给电脑后,从字段设计看,电脑管家会用自己当前的 SN 算一遍哈希,对得上才继续。
在华为电脑管家的快捷服务就能看到sn码了。
拿到蓝牙 MAC 和 MODELID
蓝牙 MAC 是电脑蓝牙适配器的物理地址。可以在 PowerShell 里跑:
ipconfig /all
找到“蓝牙网络连接”那一项,看“物理地址”,把 - 去掉、转成大写,得到 12 位十六进制。例如:
AA-BB-CC-DD-EE-FF → AABBCCDDEEFF
注意:要的是蓝牙适配器的 MAC,不是 Wi-Fi 或有线网卡的 MAC。换过蓝牙模块、或者插了新的 USB 蓝牙适配器后,这个地址可能会变。
MODELID 是 4 字节机型标识,8 位十六进制。项目里用的是 00000506,这个是Matebook X pro;也有资料用 00000505,这个是最通用的ID。
写进 sn.txt
项目把三个值放在 sn.txt 里,格式如下:
SN=1234567890ABCDEF|MAC=AABBCCDDEEFF|MODELID=00000506
要求:
- SN 是 ASCII 字符串,项目里按 16 字符上限做提示;
- MAC 是 12 位十六进制,不带分隔符;
- MODELID 是 8 位十六进制;
- 三个值都不要加空格。
用 Python 把数据封装出来
项目结构
项目里和写卡相关的文件不多:
| 文件 | 作用 |
|---|---|
onehop.py | 纯数据逻辑:payload、NDEF、TLV,无硬件依赖 |
pn532_hsu.py | PN532 串口驱动:帧、轮询、读页、写页 |
huawei_tag.py | 主程序:备份、写入、回读、校验 |
validate_ndef.py | 用第三方 ndeflib 独立解析校验 |
build_check.py | 离线计算 payload / NDEF / TLV |
probe_tag.py | 标签诊断、逐页读取 |
diagnostics/uart_probe.py | PN532 串口排查 |
research/ | 官方 APK 与反编译分析 |
依赖只有两个:
pip install pyserial ndeflib
build_check.py 只用标准库;probe_tag.py、validate_ndef.py 和主程序才需要 pyserial。
构造 44 字节 payload
核心代码如下:
import hashlib
def build_payload(sn, mac, modelid):
if len(sn) < 2:
raise SystemExit("SN too short")
if len(sn) > 16:
print(f"warning: SN is {len(sn)} chars; official app uses max 16")
payload = bytearray(44)
payload[0] = 0x00
payload[1] = 0x64
payload[2:8] = bytes.fromhex(mac)
payload[8:40] = hashlib.sha256(sn.encode("ascii")).digest()
payload[40:44] = bytes.fromhex(modelid)
return bytes(payload)
这段代码就是前面 Java 逻辑的 Python 翻译,逐项对应:
payload[2:8]:MAC;payload[8:40]:SN 的 SHA-256;payload[40:44]:MODELID。
构造 NDEF + TLV
HUAWEI_URI = "https://consumer.huawei.com/cn/support/huaweishare/"
HONOR_URI = "https://www.honor.cn/support/magic_link/"
MIME = b"application/vnd.huawei.handoff.ndefmsg"
def build_ndef(sn, mac, modelid):
payload = build_payload(sn, mac, modelid)
url = HUAWEI_URI if payload[42] == 0x05 else HONOR_URI
# URI 记录:TNF=1,type='U',payload[0]=0x04 表示 https://
uri_payload = bytes([0x04]) + url[len("https://"):].encode("ascii")
r1 = bytes([0x91, len(b"U"), len(uri_payload)]) + b"U" + uri_payload
# MIME 记录:TNF=2,type 是华为私有 MIME,payload 是 44 字节
r2 = bytes([0x52, len(MIME), len(payload)]) + MIME + payload
msg = r1 + r2
tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE])
return tlv, msg, payload, url
几个常量解释一下:
0x91:URI 记录头,MB=1、ME=0、SR=1、TNF=1;0x52:MIME 记录头,MB=0、ME=1、SR=1、TNF=2;0x04:URI 前缀,表示https://;0x03:NDEF TLV 类型;0xFE:TLV 终止符。
离线自检
在接硬件之前,先跑:
python build_check.py
输出会包含:
payload: 00 64 ...
payload[42]: 0x5
url: https://consumer.huawei.com/cn/support/huaweishare/
NDEF: 133 bytes
TLV: 136 bytes
这一步能提前发现 SN/MAC/MODELID 格式错误、URL 选错、长度不对等问题,不需要碰标签。
PN532 串口驱动层适配
硬件与接线
我用的 PN532 模块通过 CH340 转 USB,PCR532接到电脑后会显示串口地址。
代码默认是 COM3,但项目实际用的是 COM4。换 USB 口后 COM 号会变,以设备管理器/上面的命令为准。
HSU 帧格式
PN532 的 HSU 帧格式:
00 00 FF LEN LCS D4 CMD DATA DCS 00
00 00 FF:前导 + 起始;LEN:TFI + CMD + DATA的长度;LCS:(-LEN) & 0xFF;D4:Host → PN532 的 TFI;CMD:命令字;DCS:(-sum(TFI + CMD + DATA)) & 0xFF;00:后导。
比如 GetFirmwareVersion(命令 0x02)的帧是:
00 00 FF 02 FE D4 02 2A 00
PN532 会先回一个 ACK:
00 00 FF 00 FF 00
再回响应帧,响应的 TFI 是 0xD5,命令字是原命令 + 1。
这里有个坑:ACK 帧的 LEN 是 0,LCS 是 0xFF,不是常规的补码。如果按普通帧去校验,第一帧就会解析错。
发送与接收
发送帧的核心代码:
def _send_frame(self, body):
"""body = TFI(0xD4) + command + data"""
length = len(body)
lcs = (-length) & 0xFF
dcs = (-sum(body)) & 0xFF
frame = bytes([0x00, 0x00, 0xFF, length, lcs]) + body + bytes([dcs, 0x00])
self.ser.write(frame)
self.ser.flush()
发送后要处理“先 ACK、再响应”的节奏:
def call(self, cmd, data=b"", timeout=None):
self._send_frame(bytes([0xD4, cmd]) + data)
deadline = time.time() + (timeout if timeout is not None else self.timeout)
while True:
frame = self._read_frame(deadline)
if frame is None: # ACK 帧,继续等响应
continue
if frame[0] != 0xD5:
raise PN532Error(f"unexpected TFI {frame[0]:#x}")
if frame[1] == 0x7F: # PN532 错误帧
code = frame[2] if len(frame) > 2 else 0x7F
raise PN532Error(f"PN532 error frame, code={code:#x}")
if frame[1] != ((cmd + 1) & 0xFF):
raise PN532Error("unexpected response")
return frame[2:]
_read_frame 还要做几件事:
- 找到
00 00 FF起始; - 读 LEN/LCS,校验
(LEN + LCS) & 0xFF == 0; - 读 body/DCS/后导,校验 DCS;
- 单独处理 LEN=0 的 ACK 帧。
这些细节都放进驱动里之后,上层就可以像调用普通 API 一样发命令了。
读页与写页
NTAG 的读写通过 PN532 的 InDataExchange(0x40)完成。对 MIFARE Ultralight/NTAG:
0x30:读,一次返回 4 页 = 16 字节;0xA2:写,一次写 1 页 = 4 字节。
读页:
def read_page(self, page, tg=1):
data, status = self.in_data_exchange(bytes([0x30, page & 0xFF]), tg=tg)
if status != 0:
raise PN532Error(f"read page {page} failed, status={status:#x}")
if len(data) < 16:
raise PN532Error(f"read page {page} short reply {data.hex()}")
return data[:16]
写页:
def write_page(self, page, data4, tg=1, retries=2):
if len(data4) != 4:
raise ValueError("write needs exactly 4 bytes")
last = None
for _ in range(retries + 1):
_, status = self.in_data_exchange(
bytes([0xA2, page & 0xFF]) + bytes(data4), tg=tg
)
if status == 0:
time.sleep(0.012)
return True
last = status
time.sleep(0.02)
raise PN532Error(f"write page {page} failed, status={last:#x}")
几个关键点:
in_data_exchange返回的第一个字节是 PN532 的状态,0 表示成功;- 写页必须正好 4 字节,所以 TLV 要补零到 4 的倍数;
- 写完一页延时 12ms,给标签内部写周期留时间;
- 失败会重试,重试还失败就抛错,不会静默跳过。
初始化时还要做:
def get_firmware_version(self):
r = self.call(0x02)
return {"ic": r[0], "version": f"{r[1]}.{r[2]}", "support": r[3]}
def sam_configuration(self):
return self.call(0x14, bytes([0x01, 0x14, 0x01]))
0x02 是获取固件版本,0x14 是 SAMConfiguration。轮询标签用 0x4A(InListPassiveTarget),拿到 UID、SENS_RES、SEL_RES 和目标号 tg,后面的读写都带上这个 tg。
正式写卡
写入流程
主程序的写入流程是:
- 读
sn.txt,解析 SN、MAC、MODELID; - 构造 payload、NDEF 消息、TLV;
- 打开 PN532,唤醒,读固件版本,配置 SAM;
- 轮询标签,拿 UID 和目标号;
- 读 page 3 的 CC,检查是不是 NDEF 格式;
- 按 CC 计算数据区大小,检查 TLV 是否放得下;
- 整盘 dump 45 页(180 字节),保存
.mfd备份; - 解码当前标签内容并打印;
- 没有
--yes就停在 dry run; - 从 page 4 开始按页写入;
- 回读并逐字节比较;
- 解码回读内容,确认 NDEF 结构正确;
- 如果需要,再运行
validate_ndef.py,用ndeflib做一次独立解析。
这个流程里,备份和回读校验是最重要的两道保险。
命令与实测结果
在 华为一碰传标签 目录下执行:
# 只看标签信息
python huawei_tag.py --port COM4 --wait 15 info
# 写入(自动备份 + 回读校验)
python huawei_tag.py --port COM4 --wait 15 write --yes
# 校验当前标签是否与 sn.txt 一致
python huawei_tag.py --port COM4 --wait 15 verify
# 用 ndeflib 独立解析
python validate_ndef.py COM4
# 只读诊断 / 逐页打印
python probe_tag.py COM4
# 离线计算,不需要硬件
python build_check.py
实测中,两张 NTAG213 都完成了写入和回读校验:
- 一张用
MODELID=00000505,一张用00000506; huawei_tag.py verify回读逐字节一致;validate_ndef.py用ndeflib独立解析出 2 条记录,输出ALL CHECKS PASSED;- 写入前保留了整盘
.mfd备份,出问题可以对照。
validate_ndef.py 的核心校验逻辑:
records = list(ndef.message_decoder(bytearray(msg_bytes)))
assert len(records) == 2, f"expected 2 records, got {len(records)}"
uri, mime = records
assert isinstance(uri, ndef.UriRecord)
assert uri.uri == expected_uri
mime_type = mime.type if isinstance(mime.type, str) else bytes(mime.type).decode("ascii")
assert mime_type == "application/vnd.huawei.handoff.ndefmsg"
assert bytes(mime.data) == expected_payload
print("ALL CHECKS PASSED")
这一步很有价值:主程序自己的解析器可能和写入逻辑“同错”,用第三方库再解一遍,能发现头部、长度、TNF 之类的问题。
标签碰上去之后发生了什么
把标签贴到手机 NFC 区域后,大致过程是:
- 手机读取 NDEF,发现 URI +
application/vnd.huawei.handoff.ndefmsg; - 系统/华为分享识别到一碰传记录,取出蓝牙 MAC、SN 哈希、MODELID;
- 手机通过蓝牙找到电脑(后续大文件传输可能走 Wi-Fi Direct);
- 从字段设计看,电脑管家会用自己当前的 SN 计算 SHA-256,和标签里的哈希比对;
- 匹配后建立华为分享/多屏协同连接;
- 后续的文件传输由华为分享决定,和标签无关。
所以标签只是“敲门砖”。这也解释了为什么:
- 换蓝牙模块后标签会失效——MAC 变了;
- 改了 SN、或者电脑管家重装后识别到的 SN 变了——哈希对不上;
- 电脑管家没运行、蓝牙没开时碰了没反应——敲门没人应;
- 标签丢了不用太慌——它本身不存文件,但别人可能尝试连接你的电脑,注意电脑端配对安全。
总结
可能很多人都是图方便去选择安卓手机代写,但是我所思考和做的是去开辟不同的方案,使用安卓软件本质还是黑盒操作,不会有人思考这些数据是怎样封装的,实际来说,方案并不是很难,但是网上没有解释如何进行的封装。
从“网上找不到格式”,到反编译 APK 找到 44 字节布局,再到用 Python 封装、PN532 串口按页写入、回读校验、ndeflib 独立解析,这条路走通之后,一张几块钱的标签就不再神秘了。
注:目前一碰传贴纸方案只能快捷启动多屏协同,而一碰传输图片等功能还无法使用,大概率非华为电脑无法使用。
开源地址Github