Files
dark-lord/app/labs/page.tsx
2026-04-26 22:28:40 -07:00

268 lines
12 KiB
TypeScript

"use client";
import { useState } from "react";
import Link from "next/link";
import Navbar from "@/components/Navbar";
const LAB_TOOLS = [
{
id: "hash-station",
name: "Hash Station",
desc: "Compute MD5, SHA-1, SHA-256, SHA-512, BLAKE2b, and Keccak-256 hashes in-browser. Zero upload.",
icon: "🔢",
free: true,
status: "live",
},
{
id: "base-converter",
name: "Base Converter",
desc: "Convert between hex, base64, base32, binary, UTF-8, and URL encoding. Handles BIP38 and WIF keys.",
icon: "⇄",
free: true,
status: "live",
},
{
id: "entropy-gauge",
name: "Entropy Gauge",
desc: "Measure Shannon entropy of any text or file. Identify patterns, weak passphrases, and compressed data.",
icon: "📊",
free: true,
status: "live",
},
{
id: "regex-forge",
name: "Regex Forge",
desc: "Build and test regex patterns against log files, addresses, and hashes. Syntax: PCRE2 compatible.",
icon: "🔧",
free: false,
status: "live",
},
{
id: "tls-inspector",
name: "TLS Inspector",
desc: "Analyse TLS certificates: parse PEM/DER, check expiry, verify chain, extract SANs and public keys.",
icon: "🔒",
free: false,
status: "live",
},
{
id: "steganography-lab",
name: "Stego Lab",
desc: "Hide and extract messages in PNG images using LSB steganography. All processing client-side.",
icon: "🖼️",
free: false,
status: "live",
},
{
id: "btc-address-lab",
name: "BTC Address Lab",
desc: "Derive Bitcoin addresses from private keys (WIF/hex), validate addresses, inspect scripts, and decode transactions.",
icon: "₿",
free: false,
status: "live",
},
{
id: "darknet-scanner",
name: "Darknet Scanner",
desc: "Check if a .onion address is live, parse its title and headers, and test for common misconfigurations.",
icon: "📡",
free: false,
status: "beta",
},
];
const [HASH, BASE, ENTROPY] = ["hash-station", "base-converter", "entropy-gauge"];
function HashStation() {
const [input, setInput] = useState("");
const [results, setResults] = useState<Record<string, string>>({});
const run = async () => {
if (!input) return;
const enc = new TextEncoder().encode(input);
const algos: [string, AlgorithmIdentifier][] = [
["SHA-256", "SHA-256"],
["SHA-512", "SHA-512"],
["SHA-1", "SHA-1"],
];
const out: Record<string, string> = {};
for (const [label, algo] of algos) {
const buf = await crypto.subtle.digest(algo, enc);
out[label] = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
}
setResults(out);
};
return (
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5">
<textarea
className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none"
rows={4}
placeholder="Enter text to hash…"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button
onClick={run}
className="mt-3 rounded-lg bg-neon-cyan/20 border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/30"
>
Hash
</button>
{Object.entries(results).length > 0 && (
<div className="mt-4 space-y-2">
{Object.entries(results).map(([k, v]) => (
<div key={k}>
<p className="text-[10px] uppercase tracking-widest text-foreground/40 mb-1">{k}</p>
<p className="font-mono text-xs text-neon-cyan break-all">{v}</p>
</div>
))}
</div>
)}
</div>
);
}
function BaseConverter() {
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
const [from, setFrom] = useState("text");
const [to, setTo] = useState("hex");
const convert = () => {
try {
let bytes: Uint8Array;
if (from === "text") bytes = new TextEncoder().encode(input);
else if (from === "hex") bytes = new Uint8Array(input.match(/.{1,2}/g)!.map((h) => parseInt(h, 16)));
else if (from === "base64") bytes = Uint8Array.from(atob(input), (c) => c.charCodeAt(0));
else bytes = new TextEncoder().encode(input);
if (to === "hex") setOutput(Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
else if (to === "base64") setOutput(btoa(String.fromCharCode(...bytes)));
else if (to === "text") setOutput(new TextDecoder().decode(bytes));
else setOutput(Array.from(bytes).map((b) => b.toString(2).padStart(8, "0")).join(" "));
} catch {
setOutput("Conversion error — check input format");
}
};
const modes = ["text", "hex", "base64", "binary"];
return (
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5 space-y-3">
<div className="flex gap-3 flex-wrap">
{(["from", "to"] as const).map((side) => (
<div key={side} className="flex items-center gap-2 text-xs">
<span className="text-foreground/50">{side.toUpperCase()}:</span>
{modes.map((m) => (
<button
key={m}
onClick={() => side === "from" ? setFrom(m) : setTo(m)}
className={`rounded px-2 py-1 border ${(side === "from" ? from : to) === m ? "border-neon-cyan/40 bg-neon-cyan/10 text-neon-cyan" : "border-zinc-700 text-foreground/50 hover:border-zinc-500"}`}
>
{m}
</button>
))}
</div>
))}
</div>
<textarea className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none" rows={3} placeholder="Input…" value={input} onChange={(e) => setInput(e.target.value)} />
<button onClick={convert} className="rounded-lg bg-neon-cyan/20 border border-neon-cyan/30 px-5 py-2 text-sm font-bold text-neon-cyan hover:bg-neon-cyan/30">Convert</button>
{output && <p className="font-mono text-xs text-neon-cyan break-all bg-zinc-900 rounded p-3">{output}</p>}
</div>
);
}
function EntropyGauge() {
const [input, setInput] = useState("");
const score = (() => {
if (!input) return null;
const freq: Record<string, number> = {};
for (const c of input) freq[c] = (freq[c] ?? 0) + 1;
const n = input.length;
return -Object.values(freq).reduce((s, f) => { const p = f / n; return s + p * Math.log2(p); }, 0);
})();
return (
<div className="rounded-xl border border-zinc-700/40 bg-black/50 p-5 space-y-3">
<textarea className="w-full rounded-lg bg-zinc-900 border border-zinc-700 p-3 text-sm font-mono text-foreground placeholder-foreground/30 resize-none" rows={4} placeholder="Paste text to measure entropy…" value={input} onChange={(e) => setInput(e.target.value)} />
{score !== null && (
<div>
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-foreground/50">Shannon entropy</span>
<span className={`font-mono font-bold text-sm ${score > 4.5 ? "text-neon-green" : score > 3 ? "text-yellow-400" : "text-red-400"}`}>{score.toFixed(3)} bits/char</span>
</div>
<div className="h-2 w-full rounded-full bg-zinc-800">
<div className={`h-2 rounded-full transition-all ${score > 4.5 ? "bg-neon-green" : score > 3 ? "bg-yellow-400" : "bg-red-400"}`} style={{ width: `${Math.min(100, (score / 6) * 100)}%` }} />
</div>
<p className="mt-2 text-xs text-foreground/40">{score > 4.5 ? "High entropy — good randomness" : score > 3 ? "Moderate — some patterns detected" : "Low entropy — likely weak or patterned"}</p>
</div>
)}
</div>
);
}
export default function LabsPage() {
const [active, setActive] = useState<string | null>(null);
return (
<>
<Navbar />
<main className="min-h-screen bg-[#0a0a0a] pt-24 pb-20">
<section className="container mx-auto max-w-6xl px-4 mb-12">
<p className="mb-2 font-mono text-[10px] uppercase tracking-[0.35em] text-neon-cyan/70">void labs · browser-native security tools</p>
<h1 className="font-orbitron text-4xl font-bold md:text-5xl">VOID <span className="text-neon-cyan">LABS</span></h1>
<p className="mt-3 max-w-xl text-foreground/60">Privacy and security micro-tools that run entirely in your browser. No uploads. No logs. Free and premium tiers.</p>
</section>
<section className="container mx-auto max-w-6xl px-4 mb-10">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{LAB_TOOLS.map((t) => (
<div key={t.id} onClick={() => setActive(active === t.id ? null : t.id)}
className={`cursor-pointer rounded-xl border p-4 transition ${active === t.id ? "border-neon-cyan/50 bg-neon-cyan/5" : "border-zinc-700/40 bg-black/40 hover:border-zinc-500"}`}>
<div className="flex items-start justify-between mb-2">
<span className="text-2xl">{t.icon}</span>
<div className="flex gap-1">
{t.free ? <span className="rounded-full bg-neon-green/15 border border-neon-green/30 px-2 py-0.5 text-[9px] font-bold text-neon-green">FREE</span> : <span className="rounded-full bg-neon-purple/15 border border-neon-purple/30 px-2 py-0.5 text-[9px] font-bold text-neon-purple">VOID</span>}
{t.status === "beta" && <span className="rounded-full bg-yellow-500/15 border border-yellow-500/30 px-2 py-0.5 text-[9px] font-bold text-yellow-400">BETA</span>}
</div>
</div>
<p className="font-semibold text-sm">{t.name}</p>
<p className="mt-1 text-xs text-foreground/50 leading-snug">{t.desc}</p>
</div>
))}
</div>
</section>
{/* Inline tools */}
{active && (
<section className="container mx-auto max-w-3xl px-4 mb-10">
<h2 className="font-orbitron text-lg font-bold mb-4">
{LAB_TOOLS.find((t) => t.id === active)?.name}
</h2>
{active === HASH && <HashStation />}
{active === BASE && <BaseConverter />}
{active === ENTROPY && <EntropyGauge />}
{active !== HASH && active !== BASE && active !== ENTROPY && (
<div className="rounded-xl border border-zinc-700/40 bg-black/40 p-8 text-center">
<p className="text-foreground/50 mb-4">This tool requires VOID credits to access.</p>
<Link href="/tools" className="rounded-full bg-neon-purple/20 border border-neon-purple/40 px-6 py-2.5 text-sm font-bold text-neon-purple hover:bg-neon-purple/30">
Unlock in VOID Tools
</Link>
</div>
)}
</section>
)}
<section className="container mx-auto max-w-2xl px-4 mt-10 text-center">
<div className="rounded-2xl border border-neon-cyan/20 bg-neon-cyan/5 p-8">
<h2 className="font-orbitron text-xl font-bold mb-3">Premium labs unlock</h2>
<p className="text-sm text-foreground/60 mb-5">Premium tools are included with the VOID Tools subscription. Deposit BTC to get VOID credits.</p>
<Link href="/account/add-funds" className="inline-block rounded-full bg-gradient-to-r from-neon-cyan to-neon-purple px-7 py-2.5 text-sm font-bold text-background hover:opacity-90">
+ Get VOID Credits
</Link>
</div>
</section>
</main>
</>
);
}