卡号形态详解:十六进制、十进制与字节序
2026-09-30 · 1318 词
一张卡承载的是几个字节的不透明标识。读卡器显示给你的那个数字,只是这些字节的一个投影 —— 进制与字节序由写固件的人决定,此外什么都不是。所以同一张卡,在安装人员的笔记本上是 04A2243A,在控制器上是 04:A2:24:3A,在一个数据库里是 77734970,在另一个库里是 975479300。四个正确答案,零个共识。
一旦把形态看成"字节 + 两个决定" —— 用哪个进制、字节串的哪一端是第 0 字节 —— 转换就不再像巫术,而像一张清单。
卡 UID 是字节,数字只是一个投影
125 kHz 门禁卡和 Mifare Classic 携带 4 字节 UID;DESFire、Ultralight 以及很多 ISO 14443-4 卡携带 7 字节;少数是 10 字节。这些字节是标识符,不是数量:UID 没有"真正的"十进制值,只有某个系统从它算出来的值。
因此数字的位数本身就是线索。超过 4,294,967,295(2^32 − 1)的十进制数不可能来自 4 字节;超过约 72,057,594,037,927,935(2^56 − 1)的不可能来自 7 字节。这两个边界通常在你还没看别的之前就把卡片族缩小了一半 —— 它们也是"16 位十进制卡号"必然自相矛盾的原因,除非对方打印的根本不是原始 UID,而是数据库里另一列的印刷编号。
几种形态,以及它们的对应关系
拿一张 UID 为 04 A2 24 3A 的卡:
卡上的字节 04 A2 24 3A
大端十六进制 04A2243A ← 标准 hex 打印
冒号分组 04:A2:24:3A ← 同样的字节,便于阅读
十进制(大端) 77,734,970 ← 04A2243A 当作 32 位整数
小端十六进制 3A24A204 ← 字节串反转
反向十进制 975,479,300 ← 反转后当作整数
再读一遍这张表,因为问题全在里面:77,734,970 和 975,479,300 是同一张卡。系统拿到错的那个,轻则什么都匹配不上,重则匹配上另一张卡的号码。
实际会遇到的方向只有两个:
- 大端(网络序)。第一个字节是最高有效字节。hex 打印、按字节
printf循环、以及多数西方门禁软件存的都是这个。 - 小端 / 反转。字节串先被翻转再当作数字。大量 125 kHz 读卡器打印的是这个,尤其在亚洲 —— 这就是持卡人手上的凭条与控制器里的号码很少对得上的原因。
映射是字节反转,不是数字位反转。把 975,479,300 按位反转得到 003,974,579,那根本不是卡号;按字节反转得到 3A24A204,再反转回来才是 04A2243A。凡是把十进制数字逐位反转的工具,都误解了这套格式,而它输出的数字看上去完全合理。
转换在哪里凭空造出卡号
"这张卡能开一号门、开不了二号门"的上报,绝大多数出在三种情况上。
千分位分隔符。 从 Excel 里复制一列,拿到的是显示文本:2,864,434,397,带逗号。天真地按逗号切分,一张卡就变成两个单元格 2 和 864,434,397 —— 而一个"报告成功"的转换器,刚刚凭空造出了两个不存在的卡号。正确读法是 2,864,434,397 为一个值(十六进制是 AABBCCDD),而歧义情况下正确的行为是拒收而不是猜。这里的判断是"逗号是千分位还是列分隔符",它需要一条测试,而不是一句注释。
前导零。 字节 0x0A 十进制是 10、十六进制是 0A —— 前导零在 hex 里保留,在十进制里消失。4 字节 UID 的第一个字节为 0x00 是合法的,而它一旦被当成整数存储,就和 3 字节的数字无法区分。UID 请存成字节串或定宽 hex,永远不要存整数,否则你丢掉的第一个字节,恰好就是让这张卡唯一的那一个。
精度。 7 字节 UID 不一定装得进 JavaScript 的 Number。Number.MAX_SAFE_INTEGER 是 9,007,199,254,740,992(2^53),而任何首字节 ≥ 0x20 的 7 字节 UID 都会越界。以 0x04 开头的 NXP 风格 UID 最大到 1,407,374,883,553,279,恰好还在安全区内 —— 那是那一个首字节的运气,不该被依赖。字段宽度是 7 字节时,用 BigInt 解析十六进制:
const s = '461B6C44F53981';
parseInt(s, 16); // 19733400197085570 ← 错的
BigInt('0x' + s).toString(10); // 19733400197085569 ← 这张卡
两个值只差最后一位,而错的那个不是报错:它是一个合法数字,会顺利入库,然后永远匹配不上那张卡。这正是这门手艺的典型故障 —— 给一个看起来合理的错答案,而不是失败。
按列解析,而不是按单个值
真实工作是从表格里粘出的一整列,而不是一个干净的字符串,而且这一列通常不统一:有的行是读卡器打印的十进制,有的是带冒号的 hex,有的是反转的。逐格猜格式,就是半张表算错而工具报告零失败的原因。
需要的是能说清"哪一行被拒、为什么被拒"的转换器。"第 14 行:5 组冒号分隔不是 4 字节或 7 字节 UID"是可行动的;200 行都报"输入无效"不是。卡号形态转换器 就是按这个原则做的:粘贴任意形态的一列,它会列出每个值的全部形态,拒收它无法无歧义读取的行,并导出 CSV 交给下一个系统。它读的是字节,输出的全是投影 —— 从不把投影"四舍五入"回字节,再把那个结果当成卡号。
两套系统对不上时,按这个顺序查
- 长度。4 字节、7 字节还是 10 字节?这一条就能排除一半猜想。
- 字节序。把字节反转再比一次。如果反转后的值对上了,问题已经找到,而且不是硬件故障。
- 前导零。比 hex 形态而不是十进制形态;如果一边少一个字节,差异就在这里。
- 进制。
10000000可能是十进制、可能是十六进制、也可能只是一个看起来整的卡号。先统一成定宽 hex,再下"两边是不同的卡"的结论。 - 帧,而不是 UID。如果这个数字来自韦根帧而非原始 UID,那涉及的是设施码(facility code)与卡号在帧内的分界 —— 见 韦根 26 与 34,同一张凭证在那里会合法地变成两个不同的号码。
排查时还要把 hex 和 Base64 分清楚。hex 是每个半字节一个字符,只是字节的表示;Base64 是另一套编码,有 4 比 3 的膨胀,而一个看起来像十六进制的字符串并不自动就是 hex —— hex、Base32、Base64 与 Ascii85 的对比 讲的就是怎么从字符串本身判断它们。
能避免上述大部分麻烦的习惯只有一条:入口处归一,中间保留字节,只按接收系统的要求打印它要的形态。每多绕一次十进制整数,就多一次把已经反转过的值再反转一次的机会。