Accept Proxmark dump uploads, mfc v2 JSON, and key.bin

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.
This commit is contained in:
Cursor Agent
2026-08-24 07:12:44 +00:00
parent 21e2592f92
commit 5fcc106a3a
8 changed files with 1184 additions and 91 deletions
+92 -1
View File
@@ -1,7 +1,10 @@
import { describe, expect, it } from "vitest";
import { parseMct, parseImport } from "@/lib/rfid/parsers";
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
@@ -60,3 +63,91 @@ describe("MCT parser/exporter", () => {
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;
}