Replace remote with local repo
This commit is contained in:
267
app/labs/page.tsx
Normal file
267
app/labs/page.tsx
Normal file
@@ -0,0 +1,267 @@
|
||||
"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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user