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/CHANGELOG.md +35 -0
- package/LICENSE.md +21 -0
- package/README.md +653 -0
- package/bin/tigertag.js +132 -0
- package/database/id_aspect.json +1 -0
- package/database/id_brand.json +1 -0
- package/database/id_diameter.json +1 -0
- package/database/id_material.json +1 -0
- package/database/id_measure_unit.json +1 -0
- package/database/id_type.json +1 -0
- package/database/id_version.json +1 -0
- package/database/last_update.json +1 -0
- package/package.json +54 -0
- package/src/db.js +292 -0
- package/src/index.js +62 -0
- package/src/signature.js +130 -0
- package/src/tag.js +1245 -0
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
|
+
};
|
package/src/signature.js
ADDED
|
@@ -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 };
|