Add PN532 toolkit firmware, web UI, and embedded SPIFFS assets

Includes ESP-IDF NFC stack (deep capture, 4K Classic geometry, UL write API,
open SoftAP), React dashboard with live tag diagnostics, and docs. README
updated for APIs and lab Wi-Fi defaults.

Made-with: Cursor
This commit is contained in:
drjones
2026-03-29 09:32:55 -07:00
parent d1068d965b
commit 8968560565
73 changed files with 8802 additions and 28 deletions

113
web/src/pages/Brute.tsx Normal file
View File

@@ -0,0 +1,113 @@
import { useState } from "react";
import { apiUrl } from "../api";
import { useToast } from "../toast";
export default function Brute() {
const toast = useToast();
const [reader, setReader] = useState<"classic1k" | "classic4k">("classic1k");
const [variations, setVariations] = useState(true);
const [extraKeys, setExtraKeys] = useState(
"FFFFFFFFFFFF\nA0A1A2A3A4A5\nD3F7D3F7D3F7",
);
const [busy, setBusy] = useState(false);
const [result, setResult] = useState<string>("");
const run = async () => {
setBusy(true);
setResult("");
try {
const keysHex = extraKeys
.split(/\r?\n/)
.map((l) => l.replace(/\s/g, "").toUpperCase())
.filter((l) => l.length === 12);
const body = {
readerType: reader,
variations,
keysHex,
};
const r = await fetch(apiUrl("/api/mifare/dictionary-attack"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const text = await r.text();
if (!r.ok) {
throw new Error(text);
}
try {
const j = JSON.parse(text) as object;
setResult(JSON.stringify(j, null, 2));
} catch {
setResult(text);
}
toast("Dictionary pass finished");
} catch (e) {
toast(String(e), "err");
} finally {
setBusy(false);
}
};
return (
<div className="glass space-y-6 p-6">
<h1 className="font-display text-2xl font-bold">Dictionary attack</h1>
<p className="text-sm text-slate-400">
Select card family, optionally enable <strong>bounded variations</strong> (XOR / low-nibble tweaks per
key). Firmware tries a <strong>built-in community list</strong> (Proxmark3 / MCT-style defaults) plus
your lines below. This is <strong>not</strong> a full 2 exhaustive search only keys you and the
community already know. Hold a <strong>MIFARE Classic</strong> on the coil.{" "}
<a
className="text-bubble-mint underline"
href="https://github.com/RfidResearchGroup/proxmark3/blob/master/client/dictionaries/mfc_default_keys.dic"
target="_blank"
rel="noreferrer"
>
More keys online
</a>
.
</p>
<label className="block text-sm">
Reader / map
<select
value={reader}
onChange={(e) => setReader(e.target.value as "classic1k" | "classic4k")}
className="mt-1 w-full rounded-2xl border border-white/10 bg-black/25 px-4 py-2"
>
<option value="classic1k">MIFARE Classic 1K (sectors 015)</option>
<option value="classic4k">MIFARE Classic 4K (sectors 039, proper 4/16-block geometry)</option>
</select>
</label>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={variations} onChange={(e) => setVariations(e.target.checked)} />
Variations (extra tries per key slower, wider net)
</label>
<label className="block text-sm">
Extra keys (one 12-hex key per line, merged after built-in list)
<textarea
value={extraKeys}
onChange={(e) => setExtraKeys(e.target.value)}
rows={6}
className="mt-1 w-full rounded-2xl border border-white/10 bg-black/25 p-3 font-mono text-xs"
/>
</label>
<button
type="button"
disabled={busy}
onClick={run}
className="rounded-2xl bg-gradient-to-r from-bubble-rose to-orange-500 px-6 py-3 font-bold text-white disabled:opacity-50"
>
{busy ? "Running…" : "Run dictionary attack"}
</button>
{result && (
<pre className="max-h-96 overflow-auto rounded-2xl border border-white/10 bg-black/40 p-4 text-xs text-bubble-mint">
{result}
</pre>
)}
</div>
);
}