@huitl/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ aesCmac: () => aesCmac,
34
+ decryptPiccData: () => decryptPiccData,
35
+ deriveSessionKey: () => deriveSessionKey,
36
+ parseSunMessage: () => parseSunMessage,
37
+ verifySun: () => verifySun
38
+ });
39
+ module.exports = __toCommonJS(src_exports);
40
+
41
+ // src/crypto/aes-cmac.ts
42
+ var import_crypto2 = __toESM(require("crypto"), 1);
43
+
44
+ // src/constants.ts
45
+ var ZERO = Buffer.alloc(16, 0);
46
+ var RB = Buffer.from("00000000000000000000000000000087", "hex");
47
+
48
+ // src/crypto/primitives.ts
49
+ var import_crypto = __toESM(require("crypto"), 1);
50
+ function xor(a, b) {
51
+ const r = Buffer.alloc(a.length);
52
+ for (let i = 0; i < a.length; i++) r[i] = a[i] ^ b[i];
53
+ return r;
54
+ }
55
+ function leftShift(buf) {
56
+ const r = Buffer.alloc(buf.length);
57
+ let carry = 0;
58
+ for (let i = buf.length - 1; i >= 0; i--) {
59
+ r[i] = (buf[i] << 1 | carry) & 255;
60
+ carry = buf[i] >> 7 & 1;
61
+ }
62
+ return r;
63
+ }
64
+ function subKeys(key) {
65
+ const c = import_crypto.default.createCipheriv("aes-128-ecb", key, null);
66
+ c.setAutoPadding(false);
67
+ const l = Buffer.concat([c.update(Buffer.alloc(16, 0)), c.final()]);
68
+ let k1 = leftShift(l);
69
+ if (l[0] & 128) k1 = xor(k1, RB);
70
+ let k2 = leftShift(k1);
71
+ if (k1[0] & 128) k2 = xor(k2, RB);
72
+ return { k1, k2 };
73
+ }
74
+
75
+ // src/crypto/aes-cmac.ts
76
+ function aesCmac(key, msg) {
77
+ const { k1, k2 } = subKeys(key);
78
+ const n = msg.length === 0 ? 1 : Math.ceil(msg.length / 16);
79
+ const complete = msg.length > 0 && msg.length % 16 === 0;
80
+ const padded = Buffer.alloc(n * 16, 0);
81
+ msg.copy(padded);
82
+ if (!complete) padded[msg.length] = 128;
83
+ const last = (n - 1) * 16;
84
+ xor(padded.subarray(last, last + 16), complete ? k1 : k2).copy(padded, last);
85
+ const c = import_crypto2.default.createCipheriv("aes-128-cbc", key, ZERO);
86
+ c.setAutoPadding(false);
87
+ const enc = Buffer.concat([c.update(padded), c.final()]);
88
+ return enc.subarray(enc.length - 16);
89
+ }
90
+
91
+ // src/crypto/decrypt.ts
92
+ var import_crypto3 = __toESM(require("crypto"), 1);
93
+ function decryptPiccData(key, enc) {
94
+ const d = import_crypto3.default.createDecipheriv("aes-128-cbc", key, ZERO);
95
+ d.setAutoPadding(false);
96
+ const dec = Buffer.concat([d.update(enc), d.final()]);
97
+ return {
98
+ uid: dec.subarray(1, 8).toString("hex").toUpperCase(),
99
+ readCounter: dec.readUIntLE(8, 3)
100
+ };
101
+ }
102
+
103
+ // src/crypto/session-key.ts
104
+ function deriveSessionKey(sdmKey, uid, counter, isMac) {
105
+ const ctrLE = Buffer.alloc(3);
106
+ ctrLE.writeUIntLE(counter, 0, 3);
107
+ const label = isMac ? Buffer.from([60, 195, 0, 1, 0, 128]) : Buffer.from([195, 60, 0, 1, 0, 128]);
108
+ return aesCmac(sdmKey, Buffer.concat([label, uid, ctrLE]));
109
+ }
110
+
111
+ // src/crypto/verify.ts
112
+ var import_crypto4 = __toESM(require("crypto"), 1);
113
+
114
+ // src/crypto/truncate.ts
115
+ function truncate(cmac) {
116
+ const t = Buffer.alloc(8);
117
+ for (let i = 0; i < 8; i++) t[i] = cmac[i * 2 + 1];
118
+ return t;
119
+ }
120
+
121
+ // src/crypto/verify.ts
122
+ function verifySun(sdmKey, encPiccData, cmac, macInput) {
123
+ const picc = decryptPiccData(sdmKey, encPiccData);
124
+ const uid = Buffer.from(picc.uid, "hex");
125
+ const sk = deriveSessionKey(sdmKey, uid, picc.readCounter, true);
126
+ const expected = truncate(aesCmac(sk, macInput));
127
+ const valid = import_crypto4.default.timingSafeEqual(expected, cmac);
128
+ return { valid, uid: picc.uid, readCounter: picc.readCounter };
129
+ }
130
+
131
+ // src/url/parse.ts
132
+ function parseSunMessage(fullUrl) {
133
+ let url;
134
+ try {
135
+ url = new URL(fullUrl);
136
+ } catch {
137
+ throw new Error("Invalid URL format");
138
+ }
139
+ const piccHex = url.searchParams.get("picc_data");
140
+ const cmacHex = url.searchParams.get("cmac");
141
+ if (!piccHex) {
142
+ throw new Error("Missing required parameter: picc_data");
143
+ }
144
+ if (!cmacHex) {
145
+ throw new Error("Missing required parameter: cmac");
146
+ }
147
+ if (piccHex.length !== 32) {
148
+ throw new Error(
149
+ `Invalid picc_data length: expected 32 hex characters, got ${piccHex.length}`
150
+ );
151
+ }
152
+ if (cmacHex.length !== 16) {
153
+ throw new Error(
154
+ `Invalid cmac length: expected 16 hex characters, got ${cmacHex.length}`
155
+ );
156
+ }
157
+ if (!/^[0-9a-fA-F]+$/.test(piccHex)) {
158
+ throw new Error("Invalid picc_data: not valid hexadecimal");
159
+ }
160
+ if (!/^[0-9a-fA-F]+$/.test(cmacHex)) {
161
+ throw new Error("Invalid cmac: not valid hexadecimal");
162
+ }
163
+ const piccDataPos = fullUrl.indexOf(piccHex);
164
+ const cmacPos = fullUrl.indexOf(cmacHex);
165
+ return {
166
+ encPiccData: Buffer.from(piccHex, "hex"),
167
+ cmac: Buffer.from(cmacHex, "hex"),
168
+ macInput: Buffer.from(fullUrl.substring(piccDataPos, cmacPos), "ascii")
169
+ };
170
+ }
171
+ // Annotate the CommonJS export names for ESM import in node:
172
+ 0 && (module.exports = {
173
+ aesCmac,
174
+ decryptPiccData,
175
+ deriveSessionKey,
176
+ parseSunMessage,
177
+ verifySun
178
+ });
179
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/crypto/aes-cmac.ts","../src/constants.ts","../src/crypto/primitives.ts","../src/crypto/decrypt.ts","../src/crypto/session-key.ts","../src/crypto/verify.ts","../src/crypto/truncate.ts","../src/url/parse.ts"],"sourcesContent":["export { aesCmac, decryptPiccData, deriveSessionKey, verifySun } from \"./crypto/index.js\";\nexport { parseSunMessage } from \"./url/index.js\";\nexport type { PiccData, VerifyResult, SunMessage } from \"./types.js\";\n","import crypto from \"crypto\";\nimport { ZERO } from \"../constants.js\";\nimport { xor, subKeys } from \"./primitives.js\";\n\n/** Compute AES-CMAC per RFC 4493 */\nexport function aesCmac(key: Buffer, msg: Buffer): Buffer {\n const { k1, k2 } = subKeys(key);\n const n = msg.length === 0 ? 1 : Math.ceil(msg.length / 16);\n const complete = msg.length > 0 && msg.length % 16 === 0;\n const padded = Buffer.alloc(n * 16, 0);\n msg.copy(padded);\n if (!complete) padded[msg.length] = 0x80;\n const last = (n - 1) * 16;\n xor(padded.subarray(last, last + 16), complete ? k1 : k2).copy(padded, last);\n const c = crypto.createCipheriv(\"aes-128-cbc\", key, ZERO);\n c.setAutoPadding(false);\n const enc = Buffer.concat([c.update(padded), c.final()]);\n return enc.subarray(enc.length - 16);\n}\n","/** 16-byte zero buffer used as IV for AES-CBC operations */\nexport const ZERO = Buffer.alloc(16, 0);\n\n/** AES-CMAC polynomial constant Rb for 128-bit block size (RFC 4493) */\nexport const RB = Buffer.from(\"00000000000000000000000000000087\", \"hex\");\n","import crypto from \"crypto\";\nimport { RB } from \"../constants.js\";\n\nexport function xor(a: Buffer, b: Buffer): Buffer {\n const r = Buffer.alloc(a.length);\n for (let i = 0; i < a.length; i++) r[i] = a[i] ^ b[i];\n return r;\n}\n\nexport function leftShift(buf: Buffer): Buffer {\n const r = Buffer.alloc(buf.length);\n let carry = 0;\n for (let i = buf.length - 1; i >= 0; i--) {\n r[i] = ((buf[i] << 1) | carry) & 0xff;\n carry = (buf[i] >> 7) & 1;\n }\n return r;\n}\n\nexport function subKeys(key: Buffer): { k1: Buffer; k2: Buffer } {\n const c = crypto.createCipheriv(\"aes-128-ecb\", key, null);\n c.setAutoPadding(false);\n const l = Buffer.concat([c.update(Buffer.alloc(16, 0)), c.final()]);\n let k1 = leftShift(l);\n if (l[0] & 0x80) k1 = xor(k1, RB);\n let k2 = leftShift(k1);\n if (k1[0] & 0x80) k2 = xor(k2, RB);\n return { k1, k2 };\n}\n","import crypto from \"crypto\";\nimport { ZERO } from \"../constants.js\";\nimport type { PiccData } from \"../types.js\";\n\n/** Decrypt encrypted PICC data from an NTAG 424 DNA chip (AES-128-CBC, zero IV) */\nexport function decryptPiccData(key: Buffer, enc: Buffer): PiccData {\n const d = crypto.createDecipheriv(\"aes-128-cbc\", key, ZERO);\n d.setAutoPadding(false);\n const dec = Buffer.concat([d.update(enc), d.final()]);\n // PICCData format: [0xC7 tag byte] [7-byte UID] [3-byte ReadCtr LE] [padding]\n return {\n uid: dec.subarray(1, 8).toString(\"hex\").toUpperCase(),\n readCounter: dec.readUIntLE(8, 3),\n };\n}\n","import { aesCmac } from \"./aes-cmac.js\";\n\n/**\n * Derive a session key for SDM MAC or ENC operations.\n * Uses the SV (Session Vector) construction per NXP AN12196.\n */\nexport function deriveSessionKey(\n sdmKey: Buffer,\n uid: Buffer,\n counter: number,\n isMac: boolean\n): Buffer {\n const ctrLE = Buffer.alloc(3);\n ctrLE.writeUIntLE(counter, 0, 3);\n const label = isMac\n ? Buffer.from([0x3c, 0xc3, 0x00, 0x01, 0x00, 0x80])\n : Buffer.from([0xc3, 0x3c, 0x00, 0x01, 0x00, 0x80]);\n return aesCmac(sdmKey, Buffer.concat([label, uid, ctrLE]));\n}\n","import crypto from \"crypto\";\nimport type { VerifyResult } from \"../types.js\";\nimport { decryptPiccData } from \"./decrypt.js\";\nimport { deriveSessionKey } from \"./session-key.js\";\nimport { aesCmac } from \"./aes-cmac.js\";\nimport { truncate } from \"./truncate.js\";\n\n/**\n * Verify a SUN (Secure Unique NFC) message from an NTAG 424 DNA chip.\n *\n * @param sdmKey - The SDM meta-read key (AES-128, 16 bytes)\n * @param encPiccData - Encrypted PICC data from the URL (16 bytes)\n * @param cmac - Truncated CMAC from the URL (8 bytes)\n * @param macInput - The portion of the URL used as MAC input\n */\nexport function verifySun(\n sdmKey: Buffer,\n encPiccData: Buffer,\n cmac: Buffer,\n macInput: Buffer\n): VerifyResult {\n const picc = decryptPiccData(sdmKey, encPiccData);\n const uid = Buffer.from(picc.uid, \"hex\");\n const sk = deriveSessionKey(sdmKey, uid, picc.readCounter, true);\n const expected = truncate(aesCmac(sk, macInput));\n const valid = crypto.timingSafeEqual(expected, cmac);\n return { valid, uid: picc.uid, readCounter: picc.readCounter };\n}\n","/** Truncate a 16-byte CMAC to 8 bytes per NTAG 424 DNA spec (odd-indexed bytes) */\nexport function truncate(cmac: Buffer): Buffer {\n const t = Buffer.alloc(8);\n for (let i = 0; i < 8; i++) t[i] = cmac[i * 2 + 1];\n return t;\n}\n","import type { SunMessage } from \"../types.js\";\n\n/**\n * Parse a SUN message URL from an NTAG 424 DNA chip.\n * Extracts the encrypted PICC data, CMAC, and MAC input from the URL.\n *\n * @param fullUrl - The complete URL produced by the NFC chip tap\n * @returns Parsed SUN message components\n * @throws {Error} If the URL is missing required parameters or has invalid format\n */\nexport function parseSunMessage(fullUrl: string): SunMessage {\n let url: URL;\n try {\n url = new URL(fullUrl);\n } catch {\n throw new Error(\"Invalid URL format\");\n }\n\n const piccHex = url.searchParams.get(\"picc_data\");\n const cmacHex = url.searchParams.get(\"cmac\");\n\n if (!piccHex) {\n throw new Error(\"Missing required parameter: picc_data\");\n }\n if (!cmacHex) {\n throw new Error(\"Missing required parameter: cmac\");\n }\n if (piccHex.length !== 32) {\n throw new Error(\n `Invalid picc_data length: expected 32 hex characters, got ${piccHex.length}`\n );\n }\n if (cmacHex.length !== 16) {\n throw new Error(\n `Invalid cmac length: expected 16 hex characters, got ${cmacHex.length}`\n );\n }\n if (!/^[0-9a-fA-F]+$/.test(piccHex)) {\n throw new Error(\"Invalid picc_data: not valid hexadecimal\");\n }\n if (!/^[0-9a-fA-F]+$/.test(cmacHex)) {\n throw new Error(\"Invalid cmac: not valid hexadecimal\");\n }\n\n const piccDataPos = fullUrl.indexOf(piccHex);\n const cmacPos = fullUrl.indexOf(cmacHex);\n\n return {\n encPiccData: Buffer.from(piccHex, \"hex\"),\n cmac: Buffer.from(cmacHex, \"hex\"),\n macInput: Buffer.from(fullUrl.substring(piccDataPos, cmacPos), \"ascii\"),\n };\n}\n"],"mappings":"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","names":["import_crypto","crypto","crypto","import_crypto","crypto","import_crypto","crypto"]}
@@ -0,0 +1,59 @@
1
+ /** Compute AES-CMAC per RFC 4493 */
2
+ declare function aesCmac(key: Buffer, msg: Buffer): Buffer;
3
+
4
+ /** Decrypted PICC data from an NTAG 424 DNA chip */
5
+ interface PiccData {
6
+ /** 7-byte UID as uppercase hex (14 characters) */
7
+ uid: string;
8
+ /** SDM read counter value */
9
+ readCounter: number;
10
+ }
11
+ /** Result of SUN message verification */
12
+ interface VerifyResult {
13
+ /** Whether the CMAC signature is valid */
14
+ valid: boolean;
15
+ /** Chip UID as uppercase hex */
16
+ uid: string;
17
+ /** SDM read counter value */
18
+ readCounter: number;
19
+ }
20
+ /** Parsed SUN (Secure Unique NFC) message components */
21
+ interface SunMessage {
22
+ /** Encrypted PICC data (16 bytes) */
23
+ encPiccData: Buffer;
24
+ /** Truncated CMAC signature (8 bytes) */
25
+ cmac: Buffer;
26
+ /** The portion of the URL used as MAC input */
27
+ macInput: Buffer;
28
+ }
29
+
30
+ /** Decrypt encrypted PICC data from an NTAG 424 DNA chip (AES-128-CBC, zero IV) */
31
+ declare function decryptPiccData(key: Buffer, enc: Buffer): PiccData;
32
+
33
+ /**
34
+ * Derive a session key for SDM MAC or ENC operations.
35
+ * Uses the SV (Session Vector) construction per NXP AN12196.
36
+ */
37
+ declare function deriveSessionKey(sdmKey: Buffer, uid: Buffer, counter: number, isMac: boolean): Buffer;
38
+
39
+ /**
40
+ * Verify a SUN (Secure Unique NFC) message from an NTAG 424 DNA chip.
41
+ *
42
+ * @param sdmKey - The SDM meta-read key (AES-128, 16 bytes)
43
+ * @param encPiccData - Encrypted PICC data from the URL (16 bytes)
44
+ * @param cmac - Truncated CMAC from the URL (8 bytes)
45
+ * @param macInput - The portion of the URL used as MAC input
46
+ */
47
+ declare function verifySun(sdmKey: Buffer, encPiccData: Buffer, cmac: Buffer, macInput: Buffer): VerifyResult;
48
+
49
+ /**
50
+ * Parse a SUN message URL from an NTAG 424 DNA chip.
51
+ * Extracts the encrypted PICC data, CMAC, and MAC input from the URL.
52
+ *
53
+ * @param fullUrl - The complete URL produced by the NFC chip tap
54
+ * @returns Parsed SUN message components
55
+ * @throws {Error} If the URL is missing required parameters or has invalid format
56
+ */
57
+ declare function parseSunMessage(fullUrl: string): SunMessage;
58
+
59
+ export { type PiccData, type SunMessage, type VerifyResult, aesCmac, decryptPiccData, deriveSessionKey, parseSunMessage, verifySun };
@@ -0,0 +1,59 @@
1
+ /** Compute AES-CMAC per RFC 4493 */
2
+ declare function aesCmac(key: Buffer, msg: Buffer): Buffer;
3
+
4
+ /** Decrypted PICC data from an NTAG 424 DNA chip */
5
+ interface PiccData {
6
+ /** 7-byte UID as uppercase hex (14 characters) */
7
+ uid: string;
8
+ /** SDM read counter value */
9
+ readCounter: number;
10
+ }
11
+ /** Result of SUN message verification */
12
+ interface VerifyResult {
13
+ /** Whether the CMAC signature is valid */
14
+ valid: boolean;
15
+ /** Chip UID as uppercase hex */
16
+ uid: string;
17
+ /** SDM read counter value */
18
+ readCounter: number;
19
+ }
20
+ /** Parsed SUN (Secure Unique NFC) message components */
21
+ interface SunMessage {
22
+ /** Encrypted PICC data (16 bytes) */
23
+ encPiccData: Buffer;
24
+ /** Truncated CMAC signature (8 bytes) */
25
+ cmac: Buffer;
26
+ /** The portion of the URL used as MAC input */
27
+ macInput: Buffer;
28
+ }
29
+
30
+ /** Decrypt encrypted PICC data from an NTAG 424 DNA chip (AES-128-CBC, zero IV) */
31
+ declare function decryptPiccData(key: Buffer, enc: Buffer): PiccData;
32
+
33
+ /**
34
+ * Derive a session key for SDM MAC or ENC operations.
35
+ * Uses the SV (Session Vector) construction per NXP AN12196.
36
+ */
37
+ declare function deriveSessionKey(sdmKey: Buffer, uid: Buffer, counter: number, isMac: boolean): Buffer;
38
+
39
+ /**
40
+ * Verify a SUN (Secure Unique NFC) message from an NTAG 424 DNA chip.
41
+ *
42
+ * @param sdmKey - The SDM meta-read key (AES-128, 16 bytes)
43
+ * @param encPiccData - Encrypted PICC data from the URL (16 bytes)
44
+ * @param cmac - Truncated CMAC from the URL (8 bytes)
45
+ * @param macInput - The portion of the URL used as MAC input
46
+ */
47
+ declare function verifySun(sdmKey: Buffer, encPiccData: Buffer, cmac: Buffer, macInput: Buffer): VerifyResult;
48
+
49
+ /**
50
+ * Parse a SUN message URL from an NTAG 424 DNA chip.
51
+ * Extracts the encrypted PICC data, CMAC, and MAC input from the URL.
52
+ *
53
+ * @param fullUrl - The complete URL produced by the NFC chip tap
54
+ * @returns Parsed SUN message components
55
+ * @throws {Error} If the URL is missing required parameters or has invalid format
56
+ */
57
+ declare function parseSunMessage(fullUrl: string): SunMessage;
58
+
59
+ export { type PiccData, type SunMessage, type VerifyResult, aesCmac, decryptPiccData, deriveSessionKey, parseSunMessage, verifySun };
package/dist/index.js ADDED
@@ -0,0 +1,138 @@
1
+ // src/crypto/aes-cmac.ts
2
+ import crypto2 from "crypto";
3
+
4
+ // src/constants.ts
5
+ var ZERO = Buffer.alloc(16, 0);
6
+ var RB = Buffer.from("00000000000000000000000000000087", "hex");
7
+
8
+ // src/crypto/primitives.ts
9
+ import crypto from "crypto";
10
+ function xor(a, b) {
11
+ const r = Buffer.alloc(a.length);
12
+ for (let i = 0; i < a.length; i++) r[i] = a[i] ^ b[i];
13
+ return r;
14
+ }
15
+ function leftShift(buf) {
16
+ const r = Buffer.alloc(buf.length);
17
+ let carry = 0;
18
+ for (let i = buf.length - 1; i >= 0; i--) {
19
+ r[i] = (buf[i] << 1 | carry) & 255;
20
+ carry = buf[i] >> 7 & 1;
21
+ }
22
+ return r;
23
+ }
24
+ function subKeys(key) {
25
+ const c = crypto.createCipheriv("aes-128-ecb", key, null);
26
+ c.setAutoPadding(false);
27
+ const l = Buffer.concat([c.update(Buffer.alloc(16, 0)), c.final()]);
28
+ let k1 = leftShift(l);
29
+ if (l[0] & 128) k1 = xor(k1, RB);
30
+ let k2 = leftShift(k1);
31
+ if (k1[0] & 128) k2 = xor(k2, RB);
32
+ return { k1, k2 };
33
+ }
34
+
35
+ // src/crypto/aes-cmac.ts
36
+ function aesCmac(key, msg) {
37
+ const { k1, k2 } = subKeys(key);
38
+ const n = msg.length === 0 ? 1 : Math.ceil(msg.length / 16);
39
+ const complete = msg.length > 0 && msg.length % 16 === 0;
40
+ const padded = Buffer.alloc(n * 16, 0);
41
+ msg.copy(padded);
42
+ if (!complete) padded[msg.length] = 128;
43
+ const last = (n - 1) * 16;
44
+ xor(padded.subarray(last, last + 16), complete ? k1 : k2).copy(padded, last);
45
+ const c = crypto2.createCipheriv("aes-128-cbc", key, ZERO);
46
+ c.setAutoPadding(false);
47
+ const enc = Buffer.concat([c.update(padded), c.final()]);
48
+ return enc.subarray(enc.length - 16);
49
+ }
50
+
51
+ // src/crypto/decrypt.ts
52
+ import crypto3 from "crypto";
53
+ function decryptPiccData(key, enc) {
54
+ const d = crypto3.createDecipheriv("aes-128-cbc", key, ZERO);
55
+ d.setAutoPadding(false);
56
+ const dec = Buffer.concat([d.update(enc), d.final()]);
57
+ return {
58
+ uid: dec.subarray(1, 8).toString("hex").toUpperCase(),
59
+ readCounter: dec.readUIntLE(8, 3)
60
+ };
61
+ }
62
+
63
+ // src/crypto/session-key.ts
64
+ function deriveSessionKey(sdmKey, uid, counter, isMac) {
65
+ const ctrLE = Buffer.alloc(3);
66
+ ctrLE.writeUIntLE(counter, 0, 3);
67
+ const label = isMac ? Buffer.from([60, 195, 0, 1, 0, 128]) : Buffer.from([195, 60, 0, 1, 0, 128]);
68
+ return aesCmac(sdmKey, Buffer.concat([label, uid, ctrLE]));
69
+ }
70
+
71
+ // src/crypto/verify.ts
72
+ import crypto4 from "crypto";
73
+
74
+ // src/crypto/truncate.ts
75
+ function truncate(cmac) {
76
+ const t = Buffer.alloc(8);
77
+ for (let i = 0; i < 8; i++) t[i] = cmac[i * 2 + 1];
78
+ return t;
79
+ }
80
+
81
+ // src/crypto/verify.ts
82
+ function verifySun(sdmKey, encPiccData, cmac, macInput) {
83
+ const picc = decryptPiccData(sdmKey, encPiccData);
84
+ const uid = Buffer.from(picc.uid, "hex");
85
+ const sk = deriveSessionKey(sdmKey, uid, picc.readCounter, true);
86
+ const expected = truncate(aesCmac(sk, macInput));
87
+ const valid = crypto4.timingSafeEqual(expected, cmac);
88
+ return { valid, uid: picc.uid, readCounter: picc.readCounter };
89
+ }
90
+
91
+ // src/url/parse.ts
92
+ function parseSunMessage(fullUrl) {
93
+ let url;
94
+ try {
95
+ url = new URL(fullUrl);
96
+ } catch {
97
+ throw new Error("Invalid URL format");
98
+ }
99
+ const piccHex = url.searchParams.get("picc_data");
100
+ const cmacHex = url.searchParams.get("cmac");
101
+ if (!piccHex) {
102
+ throw new Error("Missing required parameter: picc_data");
103
+ }
104
+ if (!cmacHex) {
105
+ throw new Error("Missing required parameter: cmac");
106
+ }
107
+ if (piccHex.length !== 32) {
108
+ throw new Error(
109
+ `Invalid picc_data length: expected 32 hex characters, got ${piccHex.length}`
110
+ );
111
+ }
112
+ if (cmacHex.length !== 16) {
113
+ throw new Error(
114
+ `Invalid cmac length: expected 16 hex characters, got ${cmacHex.length}`
115
+ );
116
+ }
117
+ if (!/^[0-9a-fA-F]+$/.test(piccHex)) {
118
+ throw new Error("Invalid picc_data: not valid hexadecimal");
119
+ }
120
+ if (!/^[0-9a-fA-F]+$/.test(cmacHex)) {
121
+ throw new Error("Invalid cmac: not valid hexadecimal");
122
+ }
123
+ const piccDataPos = fullUrl.indexOf(piccHex);
124
+ const cmacPos = fullUrl.indexOf(cmacHex);
125
+ return {
126
+ encPiccData: Buffer.from(piccHex, "hex"),
127
+ cmac: Buffer.from(cmacHex, "hex"),
128
+ macInput: Buffer.from(fullUrl.substring(piccDataPos, cmacPos), "ascii")
129
+ };
130
+ }
131
+ export {
132
+ aesCmac,
133
+ decryptPiccData,
134
+ deriveSessionKey,
135
+ parseSunMessage,
136
+ verifySun
137
+ };
138
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/crypto/aes-cmac.ts","../src/constants.ts","../src/crypto/primitives.ts","../src/crypto/decrypt.ts","../src/crypto/session-key.ts","../src/crypto/verify.ts","../src/crypto/truncate.ts","../src/url/parse.ts"],"sourcesContent":["import crypto from \"crypto\";\nimport { ZERO } from \"../constants.js\";\nimport { xor, subKeys } from \"./primitives.js\";\n\n/** Compute AES-CMAC per RFC 4493 */\nexport function aesCmac(key: Buffer, msg: Buffer): Buffer {\n const { k1, k2 } = subKeys(key);\n const n = msg.length === 0 ? 1 : Math.ceil(msg.length / 16);\n const complete = msg.length > 0 && msg.length % 16 === 0;\n const padded = Buffer.alloc(n * 16, 0);\n msg.copy(padded);\n if (!complete) padded[msg.length] = 0x80;\n const last = (n - 1) * 16;\n xor(padded.subarray(last, last + 16), complete ? k1 : k2).copy(padded, last);\n const c = crypto.createCipheriv(\"aes-128-cbc\", key, ZERO);\n c.setAutoPadding(false);\n const enc = Buffer.concat([c.update(padded), c.final()]);\n return enc.subarray(enc.length - 16);\n}\n","/** 16-byte zero buffer used as IV for AES-CBC operations */\nexport const ZERO = Buffer.alloc(16, 0);\n\n/** AES-CMAC polynomial constant Rb for 128-bit block size (RFC 4493) */\nexport const RB = Buffer.from(\"00000000000000000000000000000087\", \"hex\");\n","import crypto from \"crypto\";\nimport { RB } from \"../constants.js\";\n\nexport function xor(a: Buffer, b: Buffer): Buffer {\n const r = Buffer.alloc(a.length);\n for (let i = 0; i < a.length; i++) r[i] = a[i] ^ b[i];\n return r;\n}\n\nexport function leftShift(buf: Buffer): Buffer {\n const r = Buffer.alloc(buf.length);\n let carry = 0;\n for (let i = buf.length - 1; i >= 0; i--) {\n r[i] = ((buf[i] << 1) | carry) & 0xff;\n carry = (buf[i] >> 7) & 1;\n }\n return r;\n}\n\nexport function subKeys(key: Buffer): { k1: Buffer; k2: Buffer } {\n const c = crypto.createCipheriv(\"aes-128-ecb\", key, null);\n c.setAutoPadding(false);\n const l = Buffer.concat([c.update(Buffer.alloc(16, 0)), c.final()]);\n let k1 = leftShift(l);\n if (l[0] & 0x80) k1 = xor(k1, RB);\n let k2 = leftShift(k1);\n if (k1[0] & 0x80) k2 = xor(k2, RB);\n return { k1, k2 };\n}\n","import crypto from \"crypto\";\nimport { ZERO } from \"../constants.js\";\nimport type { PiccData } from \"../types.js\";\n\n/** Decrypt encrypted PICC data from an NTAG 424 DNA chip (AES-128-CBC, zero IV) */\nexport function decryptPiccData(key: Buffer, enc: Buffer): PiccData {\n const d = crypto.createDecipheriv(\"aes-128-cbc\", key, ZERO);\n d.setAutoPadding(false);\n const dec = Buffer.concat([d.update(enc), d.final()]);\n // PICCData format: [0xC7 tag byte] [7-byte UID] [3-byte ReadCtr LE] [padding]\n return {\n uid: dec.subarray(1, 8).toString(\"hex\").toUpperCase(),\n readCounter: dec.readUIntLE(8, 3),\n };\n}\n","import { aesCmac } from \"./aes-cmac.js\";\n\n/**\n * Derive a session key for SDM MAC or ENC operations.\n * Uses the SV (Session Vector) construction per NXP AN12196.\n */\nexport function deriveSessionKey(\n sdmKey: Buffer,\n uid: Buffer,\n counter: number,\n isMac: boolean\n): Buffer {\n const ctrLE = Buffer.alloc(3);\n ctrLE.writeUIntLE(counter, 0, 3);\n const label = isMac\n ? Buffer.from([0x3c, 0xc3, 0x00, 0x01, 0x00, 0x80])\n : Buffer.from([0xc3, 0x3c, 0x00, 0x01, 0x00, 0x80]);\n return aesCmac(sdmKey, Buffer.concat([label, uid, ctrLE]));\n}\n","import crypto from \"crypto\";\nimport type { VerifyResult } from \"../types.js\";\nimport { decryptPiccData } from \"./decrypt.js\";\nimport { deriveSessionKey } from \"./session-key.js\";\nimport { aesCmac } from \"./aes-cmac.js\";\nimport { truncate } from \"./truncate.js\";\n\n/**\n * Verify a SUN (Secure Unique NFC) message from an NTAG 424 DNA chip.\n *\n * @param sdmKey - The SDM meta-read key (AES-128, 16 bytes)\n * @param encPiccData - Encrypted PICC data from the URL (16 bytes)\n * @param cmac - Truncated CMAC from the URL (8 bytes)\n * @param macInput - The portion of the URL used as MAC input\n */\nexport function verifySun(\n sdmKey: Buffer,\n encPiccData: Buffer,\n cmac: Buffer,\n macInput: Buffer\n): VerifyResult {\n const picc = decryptPiccData(sdmKey, encPiccData);\n const uid = Buffer.from(picc.uid, \"hex\");\n const sk = deriveSessionKey(sdmKey, uid, picc.readCounter, true);\n const expected = truncate(aesCmac(sk, macInput));\n const valid = crypto.timingSafeEqual(expected, cmac);\n return { valid, uid: picc.uid, readCounter: picc.readCounter };\n}\n","/** Truncate a 16-byte CMAC to 8 bytes per NTAG 424 DNA spec (odd-indexed bytes) */\nexport function truncate(cmac: Buffer): Buffer {\n const t = Buffer.alloc(8);\n for (let i = 0; i < 8; i++) t[i] = cmac[i * 2 + 1];\n return t;\n}\n","import type { SunMessage } from \"../types.js\";\n\n/**\n * Parse a SUN message URL from an NTAG 424 DNA chip.\n * Extracts the encrypted PICC data, CMAC, and MAC input from the URL.\n *\n * @param fullUrl - The complete URL produced by the NFC chip tap\n * @returns Parsed SUN message components\n * @throws {Error} If the URL is missing required parameters or has invalid format\n */\nexport function parseSunMessage(fullUrl: string): SunMessage {\n let url: URL;\n try {\n url = new URL(fullUrl);\n } catch {\n throw new Error(\"Invalid URL format\");\n }\n\n const piccHex = url.searchParams.get(\"picc_data\");\n const cmacHex = url.searchParams.get(\"cmac\");\n\n if (!piccHex) {\n throw new Error(\"Missing required parameter: picc_data\");\n }\n if (!cmacHex) {\n throw new Error(\"Missing required parameter: cmac\");\n }\n if (piccHex.length !== 32) {\n throw new Error(\n `Invalid picc_data length: expected 32 hex characters, got ${piccHex.length}`\n );\n }\n if (cmacHex.length !== 16) {\n throw new Error(\n `Invalid cmac length: expected 16 hex characters, got ${cmacHex.length}`\n );\n }\n if (!/^[0-9a-fA-F]+$/.test(piccHex)) {\n throw new Error(\"Invalid picc_data: not valid hexadecimal\");\n }\n if (!/^[0-9a-fA-F]+$/.test(cmacHex)) {\n throw new Error(\"Invalid cmac: not valid hexadecimal\");\n }\n\n const piccDataPos = fullUrl.indexOf(piccHex);\n const cmacPos = fullUrl.indexOf(cmacHex);\n\n return {\n encPiccData: Buffer.from(piccHex, \"hex\"),\n cmac: Buffer.from(cmacHex, \"hex\"),\n macInput: Buffer.from(fullUrl.substring(piccDataPos, cmacPos), \"ascii\"),\n };\n}\n"],"mappings":"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","names":["crypto","crypto","crypto","crypto","crypto","crypto"]}
package/llms.txt ADDED
@@ -0,0 +1,59 @@
1
+ # @huitl/sdk
2
+
3
+ > NTAG 424 DNA cryptographic verification SDK for Node.js and TypeScript.
4
+
5
+ ## What it does
6
+
7
+ This is the open-source SDK for the HUITL Protocol. It packages the cryptographic math needed to verify NFC taps from NTAG 424 DNA chips — the world's most deployed secure NFC chip (by NXP).
8
+
9
+ When someone taps an NTAG 424 DNA chip with their phone, the chip generates a unique, one-time cryptographic message (called a SUN message) that includes an encrypted chip identity and a CMAC signature. This SDK parses, decrypts, and verifies that message.
10
+
11
+ ## Core functions
12
+
13
+ - `parseSunMessage(url)` — Extract encrypted PICC data and CMAC from a tap URL
14
+ - `verifySun(sdmKey, encPiccData, cmac, macInput)` — Full SUN verification (decrypt + CMAC check)
15
+ - `decryptPiccData(key, enc)` — Decrypt encrypted PICC data to get UID and read counter
16
+ - `deriveSessionKey(sdmKey, uid, counter, isMac)` — Derive session keys per NXP AN12196
17
+ - `aesCmac(key, msg)` — AES-CMAC per RFC 4493
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @huitl/sdk
23
+ ```
24
+
25
+ ## Quick example
26
+
27
+ ```typescript
28
+ import { parseSunMessage, verifySun } from "@huitl/sdk";
29
+
30
+ const sdmKey = Buffer.from("your-32-char-hex-key", "hex");
31
+ const parsed = parseSunMessage(tapUrl);
32
+ const result = verifySun(sdmKey, parsed.encPiccData, parsed.cmac, parsed.macInput);
33
+ // result: { valid: boolean, uid: string, readCounter: number }
34
+ ```
35
+
36
+ ## CLI tools
37
+
38
+ - `huitl verify <url> --key <hex>` — Verify a SUN message URL
39
+ - `huitl keygen [--count N]` — Generate test AES key + UID pairs
40
+ - `huitl dev --local` — Start a local dev server with SQLite for testing
41
+ - `huitl init` — Scaffold a new HUITL project
42
+
43
+ ## Key concepts
44
+
45
+ - **SUN (Secure Unique NFC)**: NXP's protocol where each tap produces a unique encrypted+signed message
46
+ - **PICC data**: Encrypted payload containing the chip's 7-byte UID and a rolling read counter
47
+ - **SDM key**: The AES-128 key shared between the chip and the verification server
48
+ - **Anti-replay**: The read counter increments on each tap and must always increase
49
+
50
+ ## Architecture
51
+
52
+ Zero runtime dependencies for core crypto — uses only Node.js built-in `crypto` module.
53
+ Optional dependencies (better-sqlite3, hono) are only needed for the `huitl dev` command.
54
+
55
+ ## Links
56
+
57
+ - npm: https://www.npmjs.com/package/@huitl/sdk
58
+ - GitHub: https://github.com/huitl/sdk
59
+ - Protocol: https://huitl.com
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@huitl/sdk",
3
+ "version": "0.1.0",
4
+ "description": "NTAG 424 DNA cryptographic verification SDK — parse, decrypt, and verify NFC SUN messages",
5
+ "license": "MIT",
6
+ "author": "Fabio Seva <seva@huitlprotocol.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/huitl/sdk.git"
10
+ },
11
+ "keywords": [
12
+ "nfc",
13
+ "ntag424",
14
+ "ntag-424-dna",
15
+ "sun",
16
+ "aes-cmac",
17
+ "authentication",
18
+ "verification",
19
+ "nxp",
20
+ "picc",
21
+ "digital-product-passport",
22
+ "anti-counterfeiting",
23
+ "iot"
24
+ ],
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "import": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "require": {
33
+ "types": "./dist/index.d.cts",
34
+ "default": "./dist/index.cjs"
35
+ }
36
+ }
37
+ },
38
+ "main": "./dist/index.cjs",
39
+ "module": "./dist/index.js",
40
+ "types": "./dist/index.d.ts",
41
+ "bin": {
42
+ "huitl": "./dist/cli.cjs"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "llms.txt",
47
+ "README.md",
48
+ "LICENSE"
49
+ ],
50
+ "scripts": {
51
+ "build": "tsup",
52
+ "dev": "tsup --watch",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "lint": "tsc --noEmit",
56
+ "prepublishOnly": "npm run build"
57
+ },
58
+ "devDependencies": {
59
+ "@types/better-sqlite3": "^7.6.8",
60
+ "@types/node": "^22.0.0",
61
+ "better-sqlite3": "^11.0.0",
62
+ "commander": "^13.0.0",
63
+ "hono": "^4.0.0",
64
+ "tsup": "^8.0.0",
65
+ "typescript": "^5.7.0",
66
+ "vitest": "^3.0.0"
67
+ },
68
+ "optionalDependencies": {
69
+ "better-sqlite3": "^11.0.0"
70
+ },
71
+ "peerDependencies": {
72
+ "better-sqlite3": ">=9.0.0",
73
+ "hono": ">=4.0.0"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "better-sqlite3": {
77
+ "optional": true
78
+ },
79
+ "hono": {
80
+ "optional": true
81
+ }
82
+ },
83
+ "engines": {
84
+ "node": ">=18.0.0"
85
+ }
86
+ }