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
+269
View File
@@ -0,0 +1,269 @@
{
"Created": "proxmark3",
"FileType": "mfc v2",
"Card": {
"UID": "91A2F020",
"ATQA": "0400",
"SAK": "08"
},
"blocks": {
"0": "91A2F020E30804000497F47A359E4798",
"1": "54C017569969134569ED95BD2C8A95FE",
"2": "8A000400010000000000000000000000",
"3": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"4": "2200020000000000000000C10000001E",
"5": "2200020000000000000000C10000001E",
"6": "00000000000000000000000000000000",
"7": "2A2C13CC242AFF078069FFFFFFFFFFFF",
"8": "00000000000000000000000000000000",
"9": "00000000000000000000000000000000",
"10": "00000000000000000000000000000000",
"11": "FFFFFFFFFFFFFF078069FFFFFFFFFFFF",
"12": "00000000000000000000000000000000",
"13": "00000000000000000000000000000000",
"14": "00000000000000000000000000000000",
"15": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"16": "AE48B81460060028AE48B81C200600F0",
"17": "00000000000000000000000000000000",
"18": "00000000000000000000000000000000",
"19": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"20": "00000000000000000000000000000000",
"21": "00000000000000000000000000000000",
"22": "00000000000000000000000000000000",
"23": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"24": "00000000000000000000000000000000",
"25": "00000000000000000000000000000000",
"26": "00000000000000000000000000000000",
"27": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"28": "00000000000000000000000000000000",
"29": "00000000000000000000000000000000",
"30": "00000000000000000000000000000000",
"31": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"32": "00000000000000000000000000000000",
"33": "00000000000000000000000000000000",
"34": "00000000000000000000000000000000",
"35": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"36": "00000000000000000000000000000000",
"37": "00000000000000000000000000000000",
"38": "00000000000000000000000000000000",
"39": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"40": "00000000000000000000000000000000",
"41": "00000000000000000000000000000000",
"42": "00000000000000000000000000000000",
"43": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"44": "00000000000000000000000000000000",
"45": "00000000000000000000000000000000",
"46": "00000000000000000000000000000000",
"47": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"48": "00000000000000000000000000000000",
"49": "00000000000000000000000000000000",
"50": "00000000000000000000000000000000",
"51": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"52": "00000000000000000000000000000000",
"53": "00000000000000000000000000000000",
"54": "00000000000000000000000000000000",
"55": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"56": "00000000000000000000000000000000",
"57": "00000000000000000000000000000000",
"58": "00000000000000000000000000000000",
"59": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF",
"60": "00000000000000000000000000000000",
"61": "00000000000000000000000000000000",
"62": "00000000000000000000000000000000",
"63": "A1D4FAFD4CAFFF078069FFFFFFFFFFFF"
},
"SectorKeys": {
"0": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block0": "read AB; write AB; increment AB; decrement transfer restore AB",
"block1": "read AB; write AB; increment AB; decrement transfer restore AB",
"block2": "read AB; write AB; increment AB; decrement transfer restore AB",
"block3": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"1": {
"KeyA": "2A2C13CC242A",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block4": "read AB; write AB; increment AB; decrement transfer restore AB",
"block5": "read AB; write AB; increment AB; decrement transfer restore AB",
"block6": "read AB; write AB; increment AB; decrement transfer restore AB",
"block7": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"2": {
"KeyA": "FFFFFFFFFFFF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block8": "read AB; write AB; increment AB; decrement transfer restore AB",
"block9": "read AB; write AB; increment AB; decrement transfer restore AB",
"block10": "read AB; write AB; increment AB; decrement transfer restore AB",
"block11": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"3": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block12": "read AB; write AB; increment AB; decrement transfer restore AB",
"block13": "read AB; write AB; increment AB; decrement transfer restore AB",
"block14": "read AB; write AB; increment AB; decrement transfer restore AB",
"block15": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"4": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block16": "read AB; write AB; increment AB; decrement transfer restore AB",
"block17": "read AB; write AB; increment AB; decrement transfer restore AB",
"block18": "read AB; write AB; increment AB; decrement transfer restore AB",
"block19": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"5": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block20": "read AB; write AB; increment AB; decrement transfer restore AB",
"block21": "read AB; write AB; increment AB; decrement transfer restore AB",
"block22": "read AB; write AB; increment AB; decrement transfer restore AB",
"block23": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"6": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block24": "read AB; write AB; increment AB; decrement transfer restore AB",
"block25": "read AB; write AB; increment AB; decrement transfer restore AB",
"block26": "read AB; write AB; increment AB; decrement transfer restore AB",
"block27": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"7": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block28": "read AB; write AB; increment AB; decrement transfer restore AB",
"block29": "read AB; write AB; increment AB; decrement transfer restore AB",
"block30": "read AB; write AB; increment AB; decrement transfer restore AB",
"block31": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"8": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block32": "read AB; write AB; increment AB; decrement transfer restore AB",
"block33": "read AB; write AB; increment AB; decrement transfer restore AB",
"block34": "read AB; write AB; increment AB; decrement transfer restore AB",
"block35": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"9": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block36": "read AB; write AB; increment AB; decrement transfer restore AB",
"block37": "read AB; write AB; increment AB; decrement transfer restore AB",
"block38": "read AB; write AB; increment AB; decrement transfer restore AB",
"block39": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"10": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block40": "read AB; write AB; increment AB; decrement transfer restore AB",
"block41": "read AB; write AB; increment AB; decrement transfer restore AB",
"block42": "read AB; write AB; increment AB; decrement transfer restore AB",
"block43": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"11": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block44": "read AB; write AB; increment AB; decrement transfer restore AB",
"block45": "read AB; write AB; increment AB; decrement transfer restore AB",
"block46": "read AB; write AB; increment AB; decrement transfer restore AB",
"block47": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"12": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block48": "read AB; write AB; increment AB; decrement transfer restore AB",
"block49": "read AB; write AB; increment AB; decrement transfer restore AB",
"block50": "read AB; write AB; increment AB; decrement transfer restore AB",
"block51": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"13": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block52": "read AB; write AB; increment AB; decrement transfer restore AB",
"block53": "read AB; write AB; increment AB; decrement transfer restore AB",
"block54": "read AB; write AB; increment AB; decrement transfer restore AB",
"block55": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"14": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block56": "read AB; write AB; increment AB; decrement transfer restore AB",
"block57": "read AB; write AB; increment AB; decrement transfer restore AB",
"block58": "read AB; write AB; increment AB; decrement transfer restore AB",
"block59": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
},
"15": {
"KeyA": "A1D4FAFD4CAF",
"KeyB": "FFFFFFFFFFFF",
"AccessConditions": "FF078069",
"AccessConditionsText": {
"block60": "read AB; write AB; increment AB; decrement transfer restore AB",
"block61": "read AB; write AB; increment AB; decrement transfer restore AB",
"block62": "read AB; write AB; increment AB; decrement transfer restore AB",
"block63": "write A by A; read/write ACCESS by A; read/write B by A",
"UserData": "69"
}
}
}
}
+34
View File
@@ -270,4 +270,38 @@ FFFFFFFFFFFFFF078069FFFFFFFFFFFF
);
expect(restore.status).toBe(200);
});
it("imports Proxmark mfc v2 JSON dumps", async () => {
const auth = { Authorization: `Bearer ${bearer}` };
const siteRes = await postSite(
req("http://localhost/api/v1/sites", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ code: "PM3", name: "Proxmark" }),
})
);
const site = await siteRes.json();
const dump = fs.readFileSync(
path.join(import.meta.dirname, "../fixtures/hf-mf-91A2F020-dump.json"),
"utf8"
);
const imported = await importTag(
req("http://localhost/api/v1/tags/import", {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({
siteId: site.id,
label: "Proxmark v2",
content: dump,
filename: "hf-mf-91A2F020-dump.json",
}),
})
);
expect(imported.status).toBe(201);
const tag = await imported.json();
expect(tag.uid).toBe("91A2F020");
expect(tag.protocol).toBe("MIFARE_CLASSIC_1K");
expect(tag.keys.A[0]).toBe("A1D4FAFD4CAF");
});
});
+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;
}