tigertag 1.0.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/src/index.js ADDED
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ // TigerTag RFID Guide
4
+ // Copyright (C) 2025 TigerTag
5
+ //
6
+ // This program is free software: you can redistribute it and/or modify
7
+ // it under the terms of the GNU General Public License as published by
8
+ // the Free Software Foundation, either version 3 of the License.
9
+
10
+ /**
11
+ * tigertag — JavaScript SDK for TigerTag RFID material identification.
12
+ *
13
+ * Spec : https://github.com/TigerTag-Project/TigerTag-RFID-Guide
14
+ * Protocol: TigerTag Open Source v2.1
15
+ *
16
+ * Quick start:
17
+ * const { TigerTag } = require('tigertag');
18
+ *
19
+ * const tag = TigerTag.fromPages(payload, uid); // from NFC SDK
20
+ * const tag = TigerTag.fromDump(data); // from binary dump
21
+ * const tag = TigerTag.fromFile('dump.bin'); // from file
22
+ *
23
+ * console.log(tag.pretty()); // human-readable
24
+ * console.log(tag.toRawDict()); // raw protocol fields
25
+ * console.log(tag.toDict()); // enriched with labels, hex colors, dates
26
+ * console.log(tag.verify()); // ECDSA signature result
27
+ */
28
+
29
+ const {
30
+ TigerTag,
31
+ ApiDiff,
32
+ CHIP_DUMP_LEN,
33
+ FULL_DATA_LEN,
34
+ MIN_DATA_LEN,
35
+ MAKER_PRODUCT_ID,
36
+ INIT_PRODUCT_ID,
37
+ ID_TIGERTAG,
38
+ ID_TIGERTAG_PLUS,
39
+ ID_TIGERTAG_INIT,
40
+ TIGERTAG_EPOCH,
41
+ } = require('./tag');
42
+
43
+ const { TigerTagDB, syncDatabases } = require('./db');
44
+ const { SignatureResult, ecdsaRawToDer } = require('./signature');
45
+
46
+ module.exports = {
47
+ TigerTag,
48
+ TigerTagDB,
49
+ SignatureResult,
50
+ ApiDiff,
51
+ syncDatabases,
52
+ ecdsaRawToDer,
53
+ CHIP_DUMP_LEN,
54
+ FULL_DATA_LEN,
55
+ MIN_DATA_LEN,
56
+ MAKER_PRODUCT_ID,
57
+ INIT_PRODUCT_ID,
58
+ ID_TIGERTAG,
59
+ ID_TIGERTAG_PLUS,
60
+ ID_TIGERTAG_INIT,
61
+ TIGERTAG_EPOCH,
62
+ };
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ // TigerTag RFID Guide
4
+ // Copyright (C) 2025 TigerTag
5
+ //
6
+ // This program is free software: you can redistribute it and/or modify
7
+ // it under the terms of the GNU General Public License as published by
8
+ // the Free Software Foundation, either version 3 of the License.
9
+
10
+ /**
11
+ * ECDSA-P256 signature verification for TigerTag chips.
12
+ */
13
+
14
+ const { createVerify } = require('crypto');
15
+
16
+ /**
17
+ * Convert raw ECDSA (r, s) components to DER-encoded signature.
18
+ * Node.js crypto.createVerify() requires DER format.
19
+ * @param {Buffer} r - 32-byte R component
20
+ * @param {Buffer} s - 32-byte S component
21
+ * @returns {Buffer} DER-encoded signature
22
+ */
23
+ function ecdsaRawToDer(r, s) {
24
+ const encodeInt = (buf) => {
25
+ let b = buf;
26
+ while (b.length > 1 && b[0] === 0) b = b.subarray(1);
27
+ if (b[0] & 0x80) b = Buffer.concat([Buffer.from([0x00]), b]);
28
+ return Buffer.concat([Buffer.from([0x02, b.length]), b]);
29
+ };
30
+ const rb = encodeInt(r);
31
+ const sb = encodeInt(s);
32
+ const seq = Buffer.concat([rb, sb]);
33
+ return Buffer.concat([Buffer.from([0x30, seq.length]), seq]);
34
+ }
35
+
36
+ /**
37
+ * Result of an ECDSA signature verification.
38
+ *
39
+ * @property {string} status - One of the class constants (VALID, INVALID, UNSIGNED, …)
40
+ * @property {boolean} ok - True only when status === VALID
41
+ * @property {string} detail - Human-readable explanation for failures
42
+ */
43
+ class SignatureResult {
44
+ /**
45
+ * @param {string} status
46
+ * @param {string} [detail]
47
+ */
48
+ constructor(status, detail = '') {
49
+ this.status = status;
50
+ this.detail = detail;
51
+ this.ok = status === SignatureResult.VALID;
52
+ }
53
+
54
+ toString() {
55
+ const base = SignatureResult._ICONS[this.status] || `? ${this.status}`;
56
+ return this.detail ? `${base} ${this.detail}` : base;
57
+ }
58
+
59
+ /**
60
+ * Serialize to a plain object for JSON output.
61
+ * @returns {{ status: string, ok: boolean, detail: string }}
62
+ */
63
+ toDict() {
64
+ return { status: this.status, ok: this.ok, detail: this.detail };
65
+ }
66
+ }
67
+
68
+ SignatureResult.VALID = 'valid';
69
+ SignatureResult.INVALID = 'invalid';
70
+ SignatureResult.UNSIGNED = 'unsigned';
71
+ SignatureResult.NO_CRYPTO = 'no_crypto';
72
+ SignatureResult.NO_KEY = 'no_key';
73
+ SignatureResult.NO_UID = 'no_uid';
74
+
75
+ SignatureResult._ICONS = {
76
+ valid: '✅ VALID',
77
+ invalid: '❌ INVALID',
78
+ unsigned: '⬜ NOT SIGNED',
79
+ no_crypto: '⚠️ crypto not available',
80
+ no_key: '⚠️ public key not found in id_version.json',
81
+ no_uid: '⚠️ UID unavailable — provide a full 180-byte chip dump',
82
+ };
83
+
84
+ /**
85
+ * Verify an ECDSA-P256 signature against TigerTag data.
86
+ *
87
+ * Signed message: SHA-256( uid_bytes + block4 + block5 )
88
+ * uid_bytes = 7 raw bytes from chip pages 0-1 (ISO 14443, raw binary)
89
+ * block4 = id_tigertag as 4-byte big-endian (page 0x04)
90
+ * block5 = id_product as 4-byte big-endian (page 0x05)
91
+ *
92
+ * @param {object} params
93
+ * @param {Buffer} params.uid - 7-byte chip UID (raw bytes, not hex)
94
+ * @param {number} params.idTigertag - TigerTag version/format identifier
95
+ * @param {number} params.idProduct - Product ID
96
+ * @param {Buffer} params.signatureR - 32-byte ECDSA R component
97
+ * @param {Buffer} params.signatureS - 32-byte ECDSA S component
98
+ * @param {string} params.publicKeyPem - PEM-encoded ECDSA-P256 public key
99
+ * @returns {SignatureResult}
100
+ */
101
+ function verifySignature({ uid, idTigertag, idProduct, signatureR, signatureS, publicKeyPem }) {
102
+ try {
103
+ const block4 = Buffer.alloc(4);
104
+ block4.writeUInt32BE(idTigertag >>> 0, 0);
105
+ const block5 = Buffer.alloc(4);
106
+ block5.writeUInt32BE(idProduct >>> 0, 0);
107
+ const message = Buffer.concat([uid, block4, block5]);
108
+
109
+ const der = ecdsaRawToDer(signatureR, signatureS);
110
+ const verify = createVerify('SHA256');
111
+ verify.update(message);
112
+ const valid = verify.verify({ key: publicKeyPem, format: 'pem', type: 'spki' }, der);
113
+
114
+ if (valid) return new SignatureResult(SignatureResult.VALID);
115
+ return new SignatureResult(
116
+ SignatureResult.INVALID,
117
+ 'Signature does not match — tag may be cloned or tampered.',
118
+ );
119
+ } catch (err) {
120
+ if (err.message && /invalid signature|bad signature|wrong signature/i.test(err.message)) {
121
+ return new SignatureResult(
122
+ SignatureResult.INVALID,
123
+ 'Signature does not match — tag may be cloned or tampered.',
124
+ );
125
+ }
126
+ return new SignatureResult(SignatureResult.INVALID, `Verification error: ${err.message}`);
127
+ }
128
+ }
129
+
130
+ module.exports = { SignatureResult, verifySignature, ecdsaRawToDer };