mirror of
https://github.com/Chewbaccalakis/rfid-database.git
synced 2026-09-10 00:11:56 -07:00
Parse object-shaped mfc v2 dumps (the format Proxmark writes today) and let the new-tag form upload dump.json/dump.bin plus an optional binary hf-mf-*-key.bin instead of only pasting text.
154 lines
4.9 KiB
TypeScript
154 lines
4.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { parseMct, parseImport, parseDump, parseKeyBin } from "@/lib/rfid/parsers";
|
|
import { exportTag } from "@/lib/rfid/exporters";
|
|
import { mifareClassicDumpSchema } from "@/lib/validation/rfid";
|
|
import { isMifareClassicDump } from "@/lib/rfid/dump-types";
|
|
|
|
const sampleMct = `+UID: 04A1B2C3
|
|
+Sector: 0
|
|
04A1B2C304A1B2C304A1B2C304A1B2C3
|
|
00000000000000000000000000000000
|
|
00000000000000000000000000000000
|
|
FFFFFFFFFFFFFF078069FFFFFFFFFFFF
|
|
+Sector: 1
|
|
00000000000000000000000000000000
|
|
00000000000000000000000000000000
|
|
00000000000000000000000000000000
|
|
FFFFFFFFFFFFFF078069FFFFFFFFFFFF
|
|
`;
|
|
|
|
describe("MCT parser/exporter", () => {
|
|
it("parses MCT dumps", () => {
|
|
const tag = parseMct(sampleMct);
|
|
expect(tag.uid).toBe("04A1B2C3");
|
|
expect(tag.protocol).toBe("MIFARE_CLASSIC_1K");
|
|
expect(mifareClassicDumpSchema.safeParse(tag.dumpData).success).toBe(true);
|
|
});
|
|
|
|
it("round-trips via export mct", () => {
|
|
const tag = parseMct(sampleMct);
|
|
const exported = exportTag(
|
|
{
|
|
id: "00000000-0000-4000-8000-000000000001",
|
|
label: "test",
|
|
frequency: tag.frequency,
|
|
protocol: tag.protocol,
|
|
uid: tag.uid,
|
|
dumpData: tag.dumpData,
|
|
keys: null,
|
|
notes: null,
|
|
},
|
|
"mct"
|
|
);
|
|
const again = parseImport(exported.body, "test.mct");
|
|
expect(again.uid).toBe(tag.uid);
|
|
expect(again.protocol).toBe(tag.protocol);
|
|
});
|
|
|
|
it("parses canonical JSON", () => {
|
|
const json = JSON.stringify({
|
|
label: "Dock",
|
|
frequency: "HF",
|
|
protocol: "MIFARE_CLASSIC_1K",
|
|
uid: "04:A1:B2:C3",
|
|
dumpData: {
|
|
size: "1K",
|
|
sectors: [{ index: 0, blocks: ["AABBCCDDEEFF00112233445566778899"] }],
|
|
},
|
|
});
|
|
const tag = parseImport(json, "x.json");
|
|
expect(tag.uid).toBe("04A1B2C3");
|
|
expect(tag.label).toBe("Dock");
|
|
});
|
|
});
|
|
|
|
describe("Proxmark dump import", () => {
|
|
const fixturePath = path.join(
|
|
import.meta.dirname,
|
|
"../fixtures/hf-mf-91A2F020-dump.json"
|
|
);
|
|
const proxmarkV2 = fs.readFileSync(fixturePath, "utf8");
|
|
|
|
it("parses mfc v2 JSON with object blocks and SectorKeys", () => {
|
|
const tag = parseImport(proxmarkV2, "hf-mf-91A2F020-dump.json");
|
|
expect(tag.uid).toBe("91A2F020");
|
|
expect(tag.protocol).toBe("MIFARE_CLASSIC_1K");
|
|
expect(mifareClassicDumpSchema.safeParse(tag.dumpData).success).toBe(true);
|
|
expect(isMifareClassicDump(tag.dumpData)).toBe(true);
|
|
if (isMifareClassicDump(tag.dumpData)) {
|
|
expect(tag.dumpData.sectors).toHaveLength(16);
|
|
expect(tag.dumpData.sectors[0].blocks[0]).toBe(
|
|
"91A2F020E30804000497F47A359E4798"
|
|
);
|
|
}
|
|
expect(tag.keys?.A?.[0]).toBe("A1D4FAFD4CAF");
|
|
expect(tag.keys?.B?.[0]).toBe("FFFFFFFFFFFF");
|
|
expect(tag.keys?.A?.[1]).toBe("2A2C13CC242A");
|
|
expect(tag.keys?.A).toHaveLength(16);
|
|
});
|
|
|
|
it("parses mfc v2 JSON pasted inside a markdown fence", () => {
|
|
const tag = parseImport(
|
|
"```json\n" + proxmarkV2 + "\n```",
|
|
"paste.txt"
|
|
);
|
|
expect(tag.uid).toBe("91A2F020");
|
|
expect(tag.keys?.A?.[0]).toBe("A1D4FAFD4CAF");
|
|
});
|
|
|
|
it("parses a 192-byte 1K key.bin", () => {
|
|
const bytes = new Uint8Array(192);
|
|
bytes.set(hexToBytes("A1D4FAFD4CAF"), 0);
|
|
bytes.set(hexToBytes("FFFFFFFFFFFF"), 6);
|
|
bytes.set(hexToBytes("2A2C13CC242A"), 12);
|
|
bytes.set(hexToBytes("FFFFFFFFFFFF"), 18);
|
|
const keys = parseKeyBin(bytes, "hf-mf-91A2F020-key.bin");
|
|
expect(keys.A).toHaveLength(16);
|
|
expect(keys.A?.[0]).toBe("A1D4FAFD4CAF");
|
|
expect(keys.B?.[0]).toBe("FFFFFFFFFFFF");
|
|
expect(keys.A?.[1]).toBe("2A2C13CC242A");
|
|
});
|
|
|
|
it("merges key.bin onto a dump that has no SectorKeys", () => {
|
|
const dump = JSON.parse(proxmarkV2) as { SectorKeys?: unknown };
|
|
delete dump.SectorKeys;
|
|
const keys = new Uint8Array(192);
|
|
keys.set(hexToBytes("AABBCCDDEEFF"), 0);
|
|
keys.set(hexToBytes("112233445566"), 6);
|
|
const tag = parseDump({
|
|
text: JSON.stringify(dump),
|
|
filename: "hf-mf-91A2F020-dump.json",
|
|
keysBytes: keys,
|
|
keysFilename: "hf-mf-91A2F020-key.bin",
|
|
});
|
|
expect(tag.keys?.A?.[0]).toBe("AABBCCDDEEFF");
|
|
expect(tag.keys?.B?.[0]).toBe("112233445566");
|
|
});
|
|
|
|
it("parses a 1024-byte dump.bin", () => {
|
|
const bytes = new Uint8Array(1024);
|
|
const block0 = hexToBytes("91A2F020E30804000497F47A359E4798");
|
|
bytes.set(block0, 0);
|
|
const tag = parseDump({ bytes, filename: "hf-mf-91A2F020.bin" });
|
|
expect(tag.uid).toBe("91A2F020");
|
|
expect(tag.protocol).toBe("MIFARE_CLASSIC_1K");
|
|
});
|
|
|
|
it("rejects key.bin uploaded as the dump", () => {
|
|
const bytes = new Uint8Array(192);
|
|
expect(() =>
|
|
parseDump({ bytes, filename: "hf-mf-91A2F020-key.bin" })
|
|
).toThrow(/key\.bin/i);
|
|
});
|
|
});
|
|
|
|
function hexToBytes(hex: string): Uint8Array {
|
|
const out = new Uint8Array(hex.length / 2);
|
|
for (let i = 0; i < out.length; i++) {
|
|
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
}
|
|
return out;
|
|
}
|