@huitl/sdk 0.1.0 → 0.2.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/README.md +5 -3
- package/dist/cli.cjs +489 -18
- package/dist/cli.cjs.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -22,17 +22,19 @@ For questions, partnerships, or enterprise inquiries: [seva@huitlprotocol.com](m
|
|
|
22
22
|
|
|
23
23
|
## Try It Now
|
|
24
24
|
|
|
25
|
-
No chips, no hardware, no setup
|
|
25
|
+
No chips, no hardware, no setup:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
|
|
28
|
+
npm install -g @huitl/sdk
|
|
29
|
+
|
|
30
|
+
huitl verify "https://huitlprotocol.com/tap?picc_data=2C309D265CFFD369A4B62C6B9F40B669&cmac=6A6595D519DF58BF" --key 00000000000000000000000000000000
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
```json
|
|
32
34
|
{ "valid": true, "uid": "04112233445566", "readCounter": 42 }
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
That's a real cryptographic verification — the same math that runs on every NFC tap. The URL contains an encrypted chip identity and a one-time signature. The SDK decrypts, derives a session key, and validates the CMAC.
|
|
37
|
+
That's a real cryptographic verification — the same math that runs on every NFC tap. The URL contains an encrypted chip identity and a one-time signature. The SDK decrypts, derives a session key, and validates the CMAC.
|
|
36
38
|
|
|
37
39
|
## Installation
|
|
38
40
|
|
package/dist/cli.cjs
CHANGED
|
@@ -1156,8 +1156,8 @@ var require_command = __commonJS({
|
|
|
1156
1156
|
"use strict";
|
|
1157
1157
|
var EventEmitter = require("events").EventEmitter;
|
|
1158
1158
|
var childProcess = require("child_process");
|
|
1159
|
-
var
|
|
1160
|
-
var
|
|
1159
|
+
var path4 = require("path");
|
|
1160
|
+
var fs3 = require("fs");
|
|
1161
1161
|
var process2 = require("process");
|
|
1162
1162
|
var { Argument: Argument2, humanReadableArgName } = require_argument();
|
|
1163
1163
|
var { CommanderError: CommanderError2 } = require_error();
|
|
@@ -2138,7 +2138,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2138
2138
|
* @param {string} subcommandName
|
|
2139
2139
|
*/
|
|
2140
2140
|
_checkForMissingExecutable(executableFile, executableDir, subcommandName) {
|
|
2141
|
-
if (
|
|
2141
|
+
if (fs3.existsSync(executableFile)) return;
|
|
2142
2142
|
const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
|
|
2143
2143
|
const executableMissing = `'${executableFile}' does not exist
|
|
2144
2144
|
- if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
|
|
@@ -2156,11 +2156,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2156
2156
|
let launchWithNode = false;
|
|
2157
2157
|
const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
2158
2158
|
function findFile(baseDir, baseName) {
|
|
2159
|
-
const localBin =
|
|
2160
|
-
if (
|
|
2161
|
-
if (sourceExt.includes(
|
|
2159
|
+
const localBin = path4.resolve(baseDir, baseName);
|
|
2160
|
+
if (fs3.existsSync(localBin)) return localBin;
|
|
2161
|
+
if (sourceExt.includes(path4.extname(baseName))) return void 0;
|
|
2162
2162
|
const foundExt = sourceExt.find(
|
|
2163
|
-
(ext) =>
|
|
2163
|
+
(ext) => fs3.existsSync(`${localBin}${ext}`)
|
|
2164
2164
|
);
|
|
2165
2165
|
if (foundExt) return `${localBin}${foundExt}`;
|
|
2166
2166
|
return void 0;
|
|
@@ -2172,21 +2172,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2172
2172
|
if (this._scriptPath) {
|
|
2173
2173
|
let resolvedScriptPath;
|
|
2174
2174
|
try {
|
|
2175
|
-
resolvedScriptPath =
|
|
2175
|
+
resolvedScriptPath = fs3.realpathSync(this._scriptPath);
|
|
2176
2176
|
} catch {
|
|
2177
2177
|
resolvedScriptPath = this._scriptPath;
|
|
2178
2178
|
}
|
|
2179
|
-
executableDir =
|
|
2180
|
-
|
|
2179
|
+
executableDir = path4.resolve(
|
|
2180
|
+
path4.dirname(resolvedScriptPath),
|
|
2181
2181
|
executableDir
|
|
2182
2182
|
);
|
|
2183
2183
|
}
|
|
2184
2184
|
if (executableDir) {
|
|
2185
2185
|
let localFile = findFile(executableDir, executableFile);
|
|
2186
2186
|
if (!localFile && !subcommand._executableFile && this._scriptPath) {
|
|
2187
|
-
const legacyName =
|
|
2187
|
+
const legacyName = path4.basename(
|
|
2188
2188
|
this._scriptPath,
|
|
2189
|
-
|
|
2189
|
+
path4.extname(this._scriptPath)
|
|
2190
2190
|
);
|
|
2191
2191
|
if (legacyName !== this._name) {
|
|
2192
2192
|
localFile = findFile(
|
|
@@ -2197,7 +2197,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2197
2197
|
}
|
|
2198
2198
|
executableFile = localFile || executableFile;
|
|
2199
2199
|
}
|
|
2200
|
-
launchWithNode = sourceExt.includes(
|
|
2200
|
+
launchWithNode = sourceExt.includes(path4.extname(executableFile));
|
|
2201
2201
|
let proc;
|
|
2202
2202
|
if (process2.platform !== "win32") {
|
|
2203
2203
|
if (launchWithNode) {
|
|
@@ -3044,7 +3044,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3044
3044
|
* @return {Command}
|
|
3045
3045
|
*/
|
|
3046
3046
|
nameFromFilename(filename) {
|
|
3047
|
-
this._name =
|
|
3047
|
+
this._name = path4.basename(filename, path4.extname(filename));
|
|
3048
3048
|
return this;
|
|
3049
3049
|
}
|
|
3050
3050
|
/**
|
|
@@ -3058,9 +3058,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
3058
3058
|
* @param {string} [path]
|
|
3059
3059
|
* @return {(string|null|Command)}
|
|
3060
3060
|
*/
|
|
3061
|
-
executableDir(
|
|
3062
|
-
if (
|
|
3063
|
-
this._executableDir =
|
|
3061
|
+
executableDir(path5) {
|
|
3062
|
+
if (path5 === void 0) return this._executableDir;
|
|
3063
|
+
this._executableDir = path5;
|
|
3064
3064
|
return this;
|
|
3065
3065
|
}
|
|
3066
3066
|
/**
|
|
@@ -3713,6 +3713,380 @@ var init_app = __esm({
|
|
|
3713
3713
|
}
|
|
3714
3714
|
});
|
|
3715
3715
|
|
|
3716
|
+
// src/cli/commands/login.ts
|
|
3717
|
+
var login_exports = {};
|
|
3718
|
+
__export(login_exports, {
|
|
3719
|
+
getApiKey: () => getApiKey,
|
|
3720
|
+
registerLogin: () => registerLogin,
|
|
3721
|
+
saveApiKey: () => saveApiKey
|
|
3722
|
+
});
|
|
3723
|
+
function getApiKey() {
|
|
3724
|
+
try {
|
|
3725
|
+
const config = JSON.parse(import_fs2.default.readFileSync(CONFIG_FILE, "utf-8"));
|
|
3726
|
+
return config.apiKey ?? null;
|
|
3727
|
+
} catch {
|
|
3728
|
+
return null;
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
function saveApiKey(apiKey) {
|
|
3732
|
+
if (!import_fs2.default.existsSync(CONFIG_DIR)) import_fs2.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
3733
|
+
import_fs2.default.writeFileSync(CONFIG_FILE, JSON.stringify({ apiKey }, null, 2));
|
|
3734
|
+
}
|
|
3735
|
+
function registerLogin(program3) {
|
|
3736
|
+
program3.command("login").description("Authenticate with HUITL Cloud").option("--api-key <key>", "Set API key directly").action((opts) => {
|
|
3737
|
+
if (opts.apiKey) {
|
|
3738
|
+
saveApiKey(opts.apiKey);
|
|
3739
|
+
console.log("API key saved. You can now use huitl provision.");
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
console.log("To get an API key:");
|
|
3743
|
+
console.log(" 1. Visit https://huitl-cloud.vercel.app/api/auth/google");
|
|
3744
|
+
console.log(" 2. Sign in with Google");
|
|
3745
|
+
console.log(" 3. Copy your API key");
|
|
3746
|
+
console.log(" 4. Run: huitl login --api-key <your-key>");
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
var import_fs2, import_path3, import_os, CONFIG_DIR, CONFIG_FILE;
|
|
3750
|
+
var init_login = __esm({
|
|
3751
|
+
"src/cli/commands/login.ts"() {
|
|
3752
|
+
"use strict";
|
|
3753
|
+
import_fs2 = __toESM(require("fs"), 1);
|
|
3754
|
+
import_path3 = __toESM(require("path"), 1);
|
|
3755
|
+
import_os = __toESM(require("os"), 1);
|
|
3756
|
+
CONFIG_DIR = import_path3.default.join(import_os.default.homedir(), ".huitl");
|
|
3757
|
+
CONFIG_FILE = import_path3.default.join(CONFIG_DIR, "config.json");
|
|
3758
|
+
}
|
|
3759
|
+
});
|
|
3760
|
+
|
|
3761
|
+
// src/cloud/client.ts
|
|
3762
|
+
var client_exports = {};
|
|
3763
|
+
__export(client_exports, {
|
|
3764
|
+
HuitlCloudClient: () => HuitlCloudClient
|
|
3765
|
+
});
|
|
3766
|
+
var DEFAULT_BASE_URL, HuitlCloudClient;
|
|
3767
|
+
var init_client = __esm({
|
|
3768
|
+
"src/cloud/client.ts"() {
|
|
3769
|
+
"use strict";
|
|
3770
|
+
DEFAULT_BASE_URL = "https://huitl-cloud.vercel.app";
|
|
3771
|
+
HuitlCloudClient = class {
|
|
3772
|
+
apiKey;
|
|
3773
|
+
baseUrl;
|
|
3774
|
+
constructor(config) {
|
|
3775
|
+
this.apiKey = config.apiKey;
|
|
3776
|
+
this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
3777
|
+
}
|
|
3778
|
+
async request(path4, options = {}) {
|
|
3779
|
+
const resp = await fetch(`${this.baseUrl}/api${path4}`, {
|
|
3780
|
+
...options,
|
|
3781
|
+
headers: {
|
|
3782
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
3783
|
+
"Content-Type": "application/json",
|
|
3784
|
+
...options.headers
|
|
3785
|
+
}
|
|
3786
|
+
});
|
|
3787
|
+
const data = await resp.json();
|
|
3788
|
+
if (!resp.ok) throw new Error(data.error || data.message || `API error: ${resp.status}`);
|
|
3789
|
+
return data;
|
|
3790
|
+
}
|
|
3791
|
+
async provision(name) {
|
|
3792
|
+
return this.request("/v1/provision", {
|
|
3793
|
+
method: "POST",
|
|
3794
|
+
body: JSON.stringify({ name })
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
async registerUid(chipId, uid) {
|
|
3798
|
+
await this.request("/v1/chips", {
|
|
3799
|
+
method: "PATCH",
|
|
3800
|
+
body: JSON.stringify({ chipId, uid })
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
async listChips() {
|
|
3804
|
+
return this.request("/v1/chips");
|
|
3805
|
+
}
|
|
3806
|
+
async me() {
|
|
3807
|
+
return this.request("/v1/me");
|
|
3808
|
+
}
|
|
3809
|
+
};
|
|
3810
|
+
}
|
|
3811
|
+
});
|
|
3812
|
+
|
|
3813
|
+
// src/nfc/reader.ts
|
|
3814
|
+
var reader_exports = {};
|
|
3815
|
+
__export(reader_exports, {
|
|
3816
|
+
waitForCard: () => waitForCard
|
|
3817
|
+
});
|
|
3818
|
+
function loadNfcPcsc() {
|
|
3819
|
+
try {
|
|
3820
|
+
return require("nfc-pcsc");
|
|
3821
|
+
} catch {
|
|
3822
|
+
console.error(
|
|
3823
|
+
"Error: nfc-pcsc is required for chip provisioning.\nInstall it with: npm install nfc-pcsc\n\nYou also need a PC/SC compatible NFC reader (e.g. ACR122U).\nOn macOS, PC/SC is built-in. On Linux, install pcscd:\n sudo apt install pcscd"
|
|
3824
|
+
);
|
|
3825
|
+
process.exit(1);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
function waitForCard() {
|
|
3829
|
+
return new Promise((resolve, reject) => {
|
|
3830
|
+
const { NFC } = loadNfcPcsc();
|
|
3831
|
+
const nfc = new NFC();
|
|
3832
|
+
nfc.on("reader", (reader) => {
|
|
3833
|
+
console.log(`Reader found: ${reader.name}`);
|
|
3834
|
+
reader.aid = "D2760000850101";
|
|
3835
|
+
reader.on("card", async (card) => {
|
|
3836
|
+
let uid = card.uid?.toUpperCase() || "";
|
|
3837
|
+
if (!uid || uid.length < 14) {
|
|
3838
|
+
try {
|
|
3839
|
+
const getUid = Buffer.from([255, 202, 0, 0, 0]);
|
|
3840
|
+
const resp = await reader.transmit(getUid, 64);
|
|
3841
|
+
if (resp.length >= 9 && resp[resp.length - 2] === 144) {
|
|
3842
|
+
uid = resp.subarray(0, 7).toString("hex").toUpperCase();
|
|
3843
|
+
}
|
|
3844
|
+
} catch {
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
resolve({ uid, reader });
|
|
3848
|
+
});
|
|
3849
|
+
reader.on("error", (err) => reject(err));
|
|
3850
|
+
});
|
|
3851
|
+
nfc.on("error", (err) => {
|
|
3852
|
+
reject(new Error(`NFC error: ${err.message}. Make sure your reader is connected.`));
|
|
3853
|
+
});
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
var init_reader = __esm({
|
|
3857
|
+
"src/nfc/reader.ts"() {
|
|
3858
|
+
"use strict";
|
|
3859
|
+
}
|
|
3860
|
+
});
|
|
3861
|
+
|
|
3862
|
+
// src/nfc/ntag424.ts
|
|
3863
|
+
var ntag424_exports = {};
|
|
3864
|
+
__export(ntag424_exports, {
|
|
3865
|
+
authenticateEV2First: () => authenticateEV2First,
|
|
3866
|
+
buildApdu: () => buildApdu,
|
|
3867
|
+
buildNdefUrl: () => buildNdefUrl,
|
|
3868
|
+
calcSessionMac: () => calcSessionMac,
|
|
3869
|
+
calculateOffsets: () => calculateOffsets,
|
|
3870
|
+
changeFileSettings: () => changeFileSettings,
|
|
3871
|
+
changeKey2: () => changeKey2,
|
|
3872
|
+
checkSW: () => checkSW,
|
|
3873
|
+
crc32: () => crc32,
|
|
3874
|
+
deriveAuthSessionKeys: () => deriveAuthSessionKeys,
|
|
3875
|
+
encryptSessionData: () => encryptSessionData,
|
|
3876
|
+
rotateLeft: () => rotateLeft,
|
|
3877
|
+
sendCommand: () => sendCommand,
|
|
3878
|
+
writeNdef: () => writeNdef
|
|
3879
|
+
});
|
|
3880
|
+
function buildApdu(ins, p1, p2, data) {
|
|
3881
|
+
if (data && data.length > 0) {
|
|
3882
|
+
const apdu2 = Buffer.alloc(5 + data.length + 1);
|
|
3883
|
+
apdu2[0] = 144;
|
|
3884
|
+
apdu2[1] = ins;
|
|
3885
|
+
apdu2[2] = p1;
|
|
3886
|
+
apdu2[3] = p2;
|
|
3887
|
+
apdu2[4] = data.length;
|
|
3888
|
+
data.copy(apdu2, 5);
|
|
3889
|
+
apdu2[5 + data.length] = 0;
|
|
3890
|
+
return apdu2;
|
|
3891
|
+
}
|
|
3892
|
+
const apdu = Buffer.alloc(5);
|
|
3893
|
+
apdu[0] = 144;
|
|
3894
|
+
apdu[1] = ins;
|
|
3895
|
+
apdu[2] = p1;
|
|
3896
|
+
apdu[3] = p2;
|
|
3897
|
+
apdu[4] = 0;
|
|
3898
|
+
return apdu;
|
|
3899
|
+
}
|
|
3900
|
+
function checkSW(resp, label) {
|
|
3901
|
+
const sw1 = resp[resp.length - 2];
|
|
3902
|
+
const sw2 = resp[resp.length - 1];
|
|
3903
|
+
if (sw1 === 145 && (sw2 === 0 || sw2 === 175)) {
|
|
3904
|
+
return resp.subarray(0, resp.length - 2);
|
|
3905
|
+
}
|
|
3906
|
+
throw new Error(`${label} failed: SW=${resp.subarray(resp.length - 2).toString("hex").toUpperCase()}`);
|
|
3907
|
+
}
|
|
3908
|
+
function buildNdefUrl(url) {
|
|
3909
|
+
const uriPayload = Buffer.concat([Buffer.from([0]), Buffer.from(url, "ascii")]);
|
|
3910
|
+
const ndefRecord = Buffer.concat([
|
|
3911
|
+
Buffer.from([209]),
|
|
3912
|
+
Buffer.from([1]),
|
|
3913
|
+
Buffer.from([uriPayload.length]),
|
|
3914
|
+
Buffer.from("U", "ascii"),
|
|
3915
|
+
uriPayload
|
|
3916
|
+
]);
|
|
3917
|
+
const lenBuf = Buffer.alloc(2);
|
|
3918
|
+
lenBuf.writeUInt16BE(ndefRecord.length);
|
|
3919
|
+
return Buffer.concat([lenBuf, ndefRecord]);
|
|
3920
|
+
}
|
|
3921
|
+
function calculateOffsets(url) {
|
|
3922
|
+
const HEADER = 7;
|
|
3923
|
+
const piccPos = url.indexOf("picc_data=") + "picc_data=".length;
|
|
3924
|
+
const cmacPos = url.indexOf("cmac=") + "cmac=".length;
|
|
3925
|
+
return {
|
|
3926
|
+
piccOffset: HEADER + piccPos,
|
|
3927
|
+
macOffset: HEADER + cmacPos,
|
|
3928
|
+
macInputOffset: HEADER + piccPos
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
function xor2(a, b) {
|
|
3932
|
+
const r = Buffer.alloc(a.length);
|
|
3933
|
+
for (let i = 0; i < a.length; i++) r[i] = a[i] ^ b[i];
|
|
3934
|
+
return r;
|
|
3935
|
+
}
|
|
3936
|
+
function rotateLeft(buf) {
|
|
3937
|
+
const r = Buffer.alloc(buf.length);
|
|
3938
|
+
buf.copy(r, 0, 1);
|
|
3939
|
+
r[r.length - 1] = buf[0];
|
|
3940
|
+
return r;
|
|
3941
|
+
}
|
|
3942
|
+
function aesEncrypt(key, iv, data) {
|
|
3943
|
+
const c = import_crypto7.default.createCipheriv("aes-128-cbc", key, iv);
|
|
3944
|
+
c.setAutoPadding(false);
|
|
3945
|
+
return Buffer.concat([c.update(data), c.final()]);
|
|
3946
|
+
}
|
|
3947
|
+
function aesDecrypt(key, iv, data) {
|
|
3948
|
+
const d = import_crypto7.default.createDecipheriv("aes-128-cbc", key, iv);
|
|
3949
|
+
d.setAutoPadding(false);
|
|
3950
|
+
return Buffer.concat([d.update(data), d.final()]);
|
|
3951
|
+
}
|
|
3952
|
+
function crc32(data) {
|
|
3953
|
+
let crc = 4294967295;
|
|
3954
|
+
for (let i = 0; i < data.length; i++) {
|
|
3955
|
+
crc ^= data[i];
|
|
3956
|
+
for (let j = 0; j < 8; j++) {
|
|
3957
|
+
if (crc & 1) crc = crc >>> 1 ^ 3988292384;
|
|
3958
|
+
else crc = crc >>> 1;
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
crc = crc >>> 0;
|
|
3962
|
+
const buf = Buffer.alloc(4);
|
|
3963
|
+
buf.writeUInt32LE(crc);
|
|
3964
|
+
return buf;
|
|
3965
|
+
}
|
|
3966
|
+
function deriveAuthSessionKeys(rndA, rndB, key) {
|
|
3967
|
+
const xorPart = Buffer.alloc(6);
|
|
3968
|
+
for (let i = 0; i < 6; i++) xorPart[i] = rndA[2 + i] ^ rndB[i];
|
|
3969
|
+
const svSuffix = Buffer.concat([
|
|
3970
|
+
rndA.subarray(0, 2),
|
|
3971
|
+
xorPart,
|
|
3972
|
+
rndB.subarray(6, 16),
|
|
3973
|
+
rndA.subarray(8, 16)
|
|
3974
|
+
]);
|
|
3975
|
+
const sv1 = Buffer.concat([Buffer.from("A55A00010080", "hex"), svSuffix]);
|
|
3976
|
+
const sv2 = Buffer.concat([Buffer.from("5AA500010080", "hex"), svSuffix]);
|
|
3977
|
+
return { kEnc: aesCmac(key, sv1), kMac: aesCmac(key, sv2) };
|
|
3978
|
+
}
|
|
3979
|
+
function calcSessionMac(session, cmd, data) {
|
|
3980
|
+
const ctrLE = Buffer.alloc(2);
|
|
3981
|
+
ctrLE.writeUInt16LE(session.cmdCtr);
|
|
3982
|
+
const macInput = Buffer.concat([Buffer.from([cmd]), ctrLE, session.ti, data]);
|
|
3983
|
+
const full = aesCmac(session.kSesAuthMac, macInput);
|
|
3984
|
+
const t = Buffer.alloc(8);
|
|
3985
|
+
for (let i = 0; i < 8; i++) t[i] = full[i * 2 + 1];
|
|
3986
|
+
return t;
|
|
3987
|
+
}
|
|
3988
|
+
function encryptSessionData(session, cmd, data) {
|
|
3989
|
+
const ctrLE = Buffer.alloc(2);
|
|
3990
|
+
ctrLE.writeUInt16LE(session.cmdCtr);
|
|
3991
|
+
const ivInput = Buffer.alloc(16, 0);
|
|
3992
|
+
ivInput[0] = 165;
|
|
3993
|
+
ivInput[1] = 90;
|
|
3994
|
+
session.ti.copy(ivInput, 2);
|
|
3995
|
+
ctrLE.copy(ivInput, 6);
|
|
3996
|
+
const ivC = import_crypto7.default.createCipheriv("aes-128-ecb", session.kSesAuthEnc, null);
|
|
3997
|
+
ivC.setAutoPadding(false);
|
|
3998
|
+
const iv = Buffer.concat([ivC.update(ivInput), ivC.final()]);
|
|
3999
|
+
const padLen = (16 - data.length % 16) % 16;
|
|
4000
|
+
const padded = Buffer.alloc(data.length + padLen, 0);
|
|
4001
|
+
data.copy(padded);
|
|
4002
|
+
if (padLen > 0) padded[data.length] = 128;
|
|
4003
|
+
return aesEncrypt(session.kSesAuthEnc, iv, padded);
|
|
4004
|
+
}
|
|
4005
|
+
async function sendCommand(reader, ins, p1, p2, data) {
|
|
4006
|
+
const apdu = buildApdu(ins, p1, p2, data);
|
|
4007
|
+
return reader.transmit(apdu, 512);
|
|
4008
|
+
}
|
|
4009
|
+
async function authenticateEV2First(reader, keyNo, key) {
|
|
4010
|
+
const resp1 = await sendCommand(reader, 113, 0, 0, Buffer.from([keyNo, 0]));
|
|
4011
|
+
const encRndB = checkSW(resp1, "AuthEV2First-part1");
|
|
4012
|
+
const rndB = aesDecrypt(key, ZERO16, encRndB);
|
|
4013
|
+
const rndA = import_crypto7.default.randomBytes(16);
|
|
4014
|
+
const rndBrot = rotateLeft(rndB);
|
|
4015
|
+
const encPart2 = aesEncrypt(key, ZERO16, Buffer.concat([rndA, rndBrot]));
|
|
4016
|
+
const resp2 = await sendCommand(reader, 175, 0, 0, encPart2);
|
|
4017
|
+
const data2 = checkSW(resp2, "AuthEV2First-part2");
|
|
4018
|
+
const decResp = aesDecrypt(key, ZERO16, data2);
|
|
4019
|
+
const ti = decResp.subarray(0, 4);
|
|
4020
|
+
const rndACheck = decResp.subarray(4, 20);
|
|
4021
|
+
if (!import_crypto7.default.timingSafeEqual(rndACheck, rotateLeft(rndA))) {
|
|
4022
|
+
throw new Error("Auth failed: RndA mismatch. Key may not be factory default.");
|
|
4023
|
+
}
|
|
4024
|
+
const { kEnc, kMac } = deriveAuthSessionKeys(rndA, rndB, key);
|
|
4025
|
+
return { ti, cmdCtr: 0, kSesAuthEnc: kEnc, kSesAuthMac: kMac };
|
|
4026
|
+
}
|
|
4027
|
+
async function writeNdef(reader, session, url) {
|
|
4028
|
+
const fileData = buildNdefUrl(url);
|
|
4029
|
+
const offset = Buffer.from([0, 0, 0]);
|
|
4030
|
+
const lenLE = Buffer.alloc(3);
|
|
4031
|
+
lenLE.writeUIntLE(fileData.length, 0, 3);
|
|
4032
|
+
const cmdData = Buffer.concat([Buffer.from([2]), offset, lenLE, fileData]);
|
|
4033
|
+
const resp = await sendCommand(reader, 141, 0, 0, cmdData);
|
|
4034
|
+
checkSW(resp, "WriteData");
|
|
4035
|
+
session.cmdCtr++;
|
|
4036
|
+
}
|
|
4037
|
+
async function changeKey2(reader, session, newKey) {
|
|
4038
|
+
const oldKey = ZERO16;
|
|
4039
|
+
const xoredKey = xor2(newKey, oldKey);
|
|
4040
|
+
const crc = crc32(newKey);
|
|
4041
|
+
const keyVer = Buffer.from([0]);
|
|
4042
|
+
const plain = Buffer.concat([xoredKey, keyVer, crc]);
|
|
4043
|
+
const padded = Buffer.alloc(32, 0);
|
|
4044
|
+
plain.copy(padded);
|
|
4045
|
+
padded[plain.length] = 128;
|
|
4046
|
+
const encData = encryptSessionData(session, 196, padded);
|
|
4047
|
+
const mac = calcSessionMac(session, 196, Buffer.concat([Buffer.from([2]), encData]));
|
|
4048
|
+
const cmdData = Buffer.concat([Buffer.from([2]), encData, mac]);
|
|
4049
|
+
const resp = await sendCommand(reader, 196, 0, 0, cmdData);
|
|
4050
|
+
checkSW(resp, "ChangeKey");
|
|
4051
|
+
session.cmdCtr++;
|
|
4052
|
+
}
|
|
4053
|
+
async function changeFileSettings(reader, session, piccOffset, macOffset, macInputOffset) {
|
|
4054
|
+
const fileOption = 64;
|
|
4055
|
+
const accessRights = Buffer.from([0, 224]);
|
|
4056
|
+
const sdmOptions = 193;
|
|
4057
|
+
const sdmAccessRights = Buffer.from([255, 34]);
|
|
4058
|
+
const piccOff = Buffer.alloc(3);
|
|
4059
|
+
piccOff.writeUIntLE(piccOffset, 0, 3);
|
|
4060
|
+
const macInputOff = Buffer.alloc(3);
|
|
4061
|
+
macInputOff.writeUIntLE(macInputOffset, 0, 3);
|
|
4062
|
+
const macOff = Buffer.alloc(3);
|
|
4063
|
+
macOff.writeUIntLE(macOffset, 0, 3);
|
|
4064
|
+
const settingsData = Buffer.concat([
|
|
4065
|
+
Buffer.from([fileOption]),
|
|
4066
|
+
accessRights,
|
|
4067
|
+
Buffer.from([sdmOptions]),
|
|
4068
|
+
sdmAccessRights,
|
|
4069
|
+
piccOff,
|
|
4070
|
+
macInputOff,
|
|
4071
|
+
macOff
|
|
4072
|
+
]);
|
|
4073
|
+
const encSettings = encryptSessionData(session, 95, settingsData);
|
|
4074
|
+
const mac = calcSessionMac(session, 95, Buffer.concat([Buffer.from([2]), encSettings]));
|
|
4075
|
+
const cmdData = Buffer.concat([Buffer.from([2]), encSettings, mac]);
|
|
4076
|
+
const resp = await sendCommand(reader, 95, 0, 0, cmdData);
|
|
4077
|
+
checkSW(resp, "ChangeFileSettings");
|
|
4078
|
+
session.cmdCtr++;
|
|
4079
|
+
}
|
|
4080
|
+
var import_crypto7, ZERO16;
|
|
4081
|
+
var init_ntag424 = __esm({
|
|
4082
|
+
"src/nfc/ntag424.ts"() {
|
|
4083
|
+
"use strict";
|
|
4084
|
+
import_crypto7 = __toESM(require("crypto"), 1);
|
|
4085
|
+
init_aes_cmac();
|
|
4086
|
+
ZERO16 = Buffer.alloc(16, 0);
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
|
|
3716
4090
|
// node_modules/commander/esm.mjs
|
|
3717
4091
|
var import_index = __toESM(require_commander(), 1);
|
|
3718
4092
|
var {
|
|
@@ -3918,12 +4292,109 @@ function registerInit(program3) {
|
|
|
3918
4292
|
});
|
|
3919
4293
|
}
|
|
3920
4294
|
|
|
4295
|
+
// src/cli/commands/provision.ts
|
|
4296
|
+
function registerProvision(program3) {
|
|
4297
|
+
program3.command("provision").description("Provision an NTAG 424 DNA chip (write AES key + SUN config)").option("--cloud", "Generate key via HUITL Cloud (default)").option("--local", "Use a locally provided key").option("--key <hex>", "AES-128 key for --local mode (32 hex chars)").option("--name <name>", "Chip name/label").option("--base-url <url>", "Base URL for the tap endpoint", "https://huitlprotocol.com").action(async (opts) => {
|
|
4298
|
+
const isLocal = opts.local === true;
|
|
4299
|
+
let aesKeyHex;
|
|
4300
|
+
let chipId;
|
|
4301
|
+
if (isLocal) {
|
|
4302
|
+
if (!opts.key || opts.key.length !== 32 || !/^[0-9a-fA-F]+$/.test(opts.key)) {
|
|
4303
|
+
console.error("Error: --local requires --key with 32 hex characters");
|
|
4304
|
+
process.exit(1);
|
|
4305
|
+
}
|
|
4306
|
+
aesKeyHex = opts.key.toUpperCase();
|
|
4307
|
+
} else {
|
|
4308
|
+
const { getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
4309
|
+
const apiKey = getApiKey2();
|
|
4310
|
+
if (!apiKey) {
|
|
4311
|
+
console.error("Not logged in. Run: huitl login --api-key <key>");
|
|
4312
|
+
console.error("Or use --local mode with --key.");
|
|
4313
|
+
process.exit(1);
|
|
4314
|
+
}
|
|
4315
|
+
console.log("Requesting key from HUITL Cloud...");
|
|
4316
|
+
const { HuitlCloudClient: HuitlCloudClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
4317
|
+
const client = new HuitlCloudClient2({ apiKey });
|
|
4318
|
+
try {
|
|
4319
|
+
const result = await client.provision(opts.name);
|
|
4320
|
+
aesKeyHex = result.aesKey;
|
|
4321
|
+
chipId = result.chipId;
|
|
4322
|
+
console.log(`Key generated (chip ID: ${chipId})`);
|
|
4323
|
+
} catch (err) {
|
|
4324
|
+
console.error("Error:", err instanceof Error ? err.message : String(err));
|
|
4325
|
+
process.exit(1);
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4328
|
+
const aesKey = Buffer.from(aesKeyHex, "hex");
|
|
4329
|
+
const ndefUrl = `${opts.baseUrl}/tap?picc_data=00000000000000000000000000000000&cmac=0000000000000000`;
|
|
4330
|
+
console.log();
|
|
4331
|
+
console.log(`AES Key: ${aesKeyHex}`);
|
|
4332
|
+
console.log(`URL: ${ndefUrl}`);
|
|
4333
|
+
console.log();
|
|
4334
|
+
console.log("Place your NTAG 424 DNA chip on the reader...");
|
|
4335
|
+
console.log();
|
|
4336
|
+
const { waitForCard: waitForCard2 } = await Promise.resolve().then(() => (init_reader(), reader_exports));
|
|
4337
|
+
const {
|
|
4338
|
+
authenticateEV2First: authenticateEV2First2,
|
|
4339
|
+
writeNdef: writeNdef2,
|
|
4340
|
+
changeKey2: changeKey22,
|
|
4341
|
+
changeFileSettings: changeFileSettings2,
|
|
4342
|
+
calculateOffsets: calculateOffsets2
|
|
4343
|
+
} = await Promise.resolve().then(() => (init_ntag424(), ntag424_exports));
|
|
4344
|
+
const { uid, reader } = await waitForCard2();
|
|
4345
|
+
console.log(`Chip detected \u2014 UID: ${uid}`);
|
|
4346
|
+
console.log();
|
|
4347
|
+
try {
|
|
4348
|
+
console.log("Step 1/5: Authenticating with factory key...");
|
|
4349
|
+
const session = await authenticateEV2First2(reader, 0, Buffer.alloc(16, 0));
|
|
4350
|
+
console.log("Step 2/5: Writing NDEF URL...");
|
|
4351
|
+
await writeNdef2(reader, session, ndefUrl);
|
|
4352
|
+
console.log("Step 3/5: Writing AES key...");
|
|
4353
|
+
try {
|
|
4354
|
+
await changeKey22(reader, session, aesKey);
|
|
4355
|
+
} catch (err) {
|
|
4356
|
+
if (err.message.includes("911E")) {
|
|
4357
|
+
console.log(" Key 2 may already be set. Skipping.");
|
|
4358
|
+
} else throw err;
|
|
4359
|
+
}
|
|
4360
|
+
console.log("Step 4/5: Re-authenticating...");
|
|
4361
|
+
const session2 = await authenticateEV2First2(reader, 0, Buffer.alloc(16, 0));
|
|
4362
|
+
console.log("Step 5/5: Configuring SUN (Secure Unique NFC)...");
|
|
4363
|
+
const { piccOffset, macOffset, macInputOffset } = calculateOffsets2(ndefUrl);
|
|
4364
|
+
await changeFileSettings2(reader, session2, piccOffset, macOffset, macInputOffset);
|
|
4365
|
+
if (chipId) {
|
|
4366
|
+
console.log("Registering UID with HUITL Cloud...");
|
|
4367
|
+
const { getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
4368
|
+
const { HuitlCloudClient: HuitlCloudClient2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
4369
|
+
const client = new HuitlCloudClient2({ apiKey: getApiKey2() });
|
|
4370
|
+
await client.registerUid(chipId, uid);
|
|
4371
|
+
}
|
|
4372
|
+
console.log();
|
|
4373
|
+
console.log("Chip provisioned successfully!");
|
|
4374
|
+
console.log(` UID: ${uid}`);
|
|
4375
|
+
console.log(` AES Key: ${aesKeyHex}`);
|
|
4376
|
+
console.log(` Tap URL: ${opts.baseUrl}/tap`);
|
|
4377
|
+
console.log();
|
|
4378
|
+
console.log("Tap the chip with your phone to test it.");
|
|
4379
|
+
process.exit(0);
|
|
4380
|
+
} catch (err) {
|
|
4381
|
+
console.error();
|
|
4382
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
4383
|
+
console.error("Make sure you're using an NTAG 424 DNA chip with factory default keys.");
|
|
4384
|
+
process.exit(1);
|
|
4385
|
+
}
|
|
4386
|
+
});
|
|
4387
|
+
}
|
|
4388
|
+
|
|
3921
4389
|
// src/cli/index.ts
|
|
4390
|
+
init_login();
|
|
3922
4391
|
var program2 = new Command();
|
|
3923
|
-
program2.name("huitl").description("HUITL Protocol CLI \u2014 NTAG 424 DNA verification tools").version("0.
|
|
4392
|
+
program2.name("huitl").description("HUITL Protocol CLI \u2014 NTAG 424 DNA verification tools").version("0.2.0");
|
|
3924
4393
|
registerVerify(program2);
|
|
3925
4394
|
registerKeygen(program2);
|
|
3926
4395
|
registerDev(program2);
|
|
3927
4396
|
registerInit(program2);
|
|
4397
|
+
registerProvision(program2);
|
|
4398
|
+
registerLogin(program2);
|
|
3928
4399
|
program2.parse();
|
|
3929
4400
|
//# sourceMappingURL=cli.cjs.map
|