feat: adding new way to use Random.org api to generate seed phrase

This commit is contained in:
LC mac
2026-02-13 01:05:13 +08:00
parent 9cc74005f2
commit cf6299a510
3 changed files with 391 additions and 2 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "seedpgp-web",
"private": true,
"version": "1.4.6",
"version": "1.4.7",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -16,6 +16,7 @@ import Footer from './components/Footer';
import { SeedBlender } from './components/SeedBlender';
import CameraEntropy from './components/CameraEntropy';
import DiceEntropy from './components/DiceEntropy';
import RandomOrgEntropy from './components/RandomOrgEntropy';
import { InteractionEntropy } from './lib/interactionEntropy';
import AudioEntropy from './AudioEntropy';
@@ -78,7 +79,7 @@ function App() {
const [isNetworkBlocked, setIsNetworkBlocked] = useState(false);
// Entropy generation states
const [entropySource, setEntropySource] = useState<'camera' | 'dice' | 'audio' | null>(null);
const [entropySource, setEntropySource] = useState<'camera' | 'dice' | 'audio' | 'randomorg' | null>(null);
const [entropyStats, setEntropyStats] = useState<any>(null);
const interactionEntropyRef = useRef(new InteractionEntropy());
@@ -770,6 +771,16 @@ function App() {
</div>
</div>
</button>
<button onClick={() => setEntropySource('randomorg')} className="w-full p-4 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg hover:bg-[#1a1a2e] hover:border-[#00f0ff] hover:shadow-[0_0_20px_rgba(0,240,255,0.3)] transition-all text-left">
<div className="flex items-center gap-3">
<Dices size={24} className="text-[#00f0ff]" />
<div>
<p className="text-sm font-bold text-[#00f0ff]">🌐 Random.org D6</p>
<p className="text-[10px] text-[#6ef3f7]">Manual entropy via random.org</p>
</div>
</div>
</button>
</div>
<div className="flex items-start gap-2 p-3 bg-[#16213e] border border-[#00f0ff]/30 rounded-lg">
@@ -824,6 +835,17 @@ function App() {
/>
)}
{/* Random.org Entropy Component */}
{entropySource === 'randomorg' && !generatedSeed && (
<RandomOrgEntropy
key={`randomorg-${resetCounter}`}
wordCount={seedWordCount}
onEntropyGenerated={handleEntropyGenerated}
onCancel={() => setEntropySource(null)}
interactionEntropy={interactionEntropyRef.current}
/>
)}
{/* Generated Seed Display + Destination Selector */}
{generatedSeed && (
<div className="space-y-4">

View File

@@ -0,0 +1,367 @@
import React, { useMemo, useState } from "react";
import { Globe, Copy, ExternalLink, CheckCircle2, AlertCircle, X, Eye, EyeOff, Info } from "lucide-react";
import { InteractionEntropy } from "../lib/interactionEntropy";
import { entropyToMnemonic } from "../lib/seedblend";
type RandomOrgStats = {
source: "randomorg";
nRequested: number;
nUsed: number;
distribution: number[]; // counts for faces 1..6
interactionSamples: number;
totalBits: number;
};
interface RandomOrgEntropyProps {
wordCount: 12 | 24;
onEntropyGenerated: (mnemonic: string, stats: RandomOrgStats) => void;
onCancel: () => void;
interactionEntropy: InteractionEntropy;
}
function buildRequest(apiKey: string, n: number) {
return {
jsonrpc: "2.0",
method: "generateIntegers",
params: { apiKey, n, min: 1, max: 6, replacement: true, base: 10 },
id: 1,
};
}
function parseD6FromPaste(text: string): number[] {
const t = text.trim();
if (!t) throw new Error("Paste the random.org response JSON (or an array) first.");
// Allow direct array paste: [1,6,2,...]
if (t.startsWith("[") && t.endsWith("]")) {
const arr = JSON.parse(t);
if (!Array.isArray(arr)) throw new Error("Expected an array.");
return arr;
}
const obj = JSON.parse(t);
const data = obj?.result?.random?.data;
if (!Array.isArray(data)) throw new Error("Could not find result.random.data in pasted JSON.");
return data;
}
async function mnemonicFromD6(
d6: number[],
wordCount: 12 | 24,
interactionEntropy: InteractionEntropy
): Promise<string> {
const interactionBytes = await interactionEntropy.getEntropyBytes();
const cryptoBytes = crypto.getRandomValues(new Uint8Array(32));
// Keep it simple + consistent with your other sources: concatenate strings, SHA-256, slice entropy.
const combined = [
d6.join(""),
performance.now().toString(),
Array.from(interactionBytes).join(","),
Array.from(cryptoBytes).join(","),
].join("|");
const data = new TextEncoder().encode(combined);
const hash = await crypto.subtle.digest("SHA-256", data);
const entropyLength = wordCount === 12 ? 16 : 32;
const finalEntropy = new Uint8Array(hash.slice(0, entropyLength));
return entropyToMnemonic(finalEntropy);
}
const RandomOrgEntropy: React.FC<RandomOrgEntropyProps> = ({
wordCount,
onEntropyGenerated,
onCancel,
interactionEntropy,
}) => {
const [apiKey, setApiKey] = useState("");
const [showKey, setShowKey] = useState(false);
const [n, setN] = useState(30); // min 30
const [paste, setPaste] = useState("");
const [error, setError] = useState<string>("");
const [copied, setCopied] = useState(false);
const [processing, setProcessing] = useState(false);
const [stats, setStats] = useState<RandomOrgStats | null>(null);
const [generatedMnemonic, setGeneratedMnemonic] = useState("");
const requestJson = useMemo(() => {
const key = apiKey.trim() || "PASTE_YOUR_API_KEY_HERE";
return JSON.stringify(buildRequest(key, n), null, 2);
}, [apiKey, n]);
const copyRequest = async () => {
setError("");
try {
await navigator.clipboard.writeText(requestJson);
setCopied(true);
window.setTimeout(() => setCopied(false), 1200);
} catch {
setError("Clipboard write failed. Tap the JSON box to select all, then copy manually.");
}
};
const generate = async () => {
setError("");
setProcessing(true);
try {
const raw = parseD6FromPaste(paste);
if (raw.length < n) throw new Error(`Need at least ${n} D6 samples, got ${raw.length}.`);
const d6 = raw.slice(0, n);
const dist = [0, 0, 0, 0, 0, 0];
for (let i = 0; i < d6.length; i++) {
const v = d6[i];
if (!Number.isInteger(v) || v < 1 || v > 6) throw new Error(`Invalid D6 at index ${i}: ${String(v)}`);
dist[v - 1]++;
}
const mnemonic = await mnemonicFromD6(d6, wordCount, interactionEntropy);
setGeneratedMnemonic(mnemonic);
setStats({
source: "randomorg",
nRequested: n,
nUsed: d6.length,
distribution: dist,
interactionSamples: interactionEntropy.getSampleCount().total,
totalBits: 256,
});
} catch (e) {
setStats(null);
setGeneratedMnemonic("");
setError(e instanceof Error ? e.message : "Failed.");
} finally {
setProcessing(false);
}
};
return (
<div className="space-y-4 max-h-[calc(100vh-200px)] overflow-y-auto">
{!stats && !processing && (
<>
<div className="p-4 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 space-y-3">
<div className="flex items-center gap-2">
<Globe size={20} className="text-[#00f0ff]" />
<h3 className="text-sm font-bold text-[#00f0ff] uppercase">🌍 Random.org Entropy</h3>
</div>
<div className="flex items-start gap-2 text-xs text-[#6ef3f7]">
<Info size={14} className="shrink-0 mt-0.5 text-[#00f0ff]" />
<p>
SeedPGP will not contact random.org. You run the request in another tab/tool and paste the response here.
</p>
</div>
</div>
<div className="space-y-1">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest">random.org API key</label>
<div className="relative">
<input
type={showKey ? "text" : "password"}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Paste API key (optional; not stored)"
className="w-full pl-3 pr-10 py-2 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg text-[#00f0ff] text-xs placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] transition-all"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
/>
<button
type="button"
className="absolute right-2 top-2 text-[#6ef3f7] hover:text-[#00f0ff]"
onClick={() => setShowKey((s) => !s)}
>
{showKey ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest">D6 samples</label>
<span className="text-xs text-[#00f0ff] font-mono">{n}</span>
</div>
<input
type="range"
min={30}
max={200}
step={10}
value={n}
onChange={(e) => setN(parseInt(e.target.value, 10))}
className="w-full accent-[#ff006e]"
/>
<p className="text-[10px] text-[#6ef3f7]">Minimum 30. Step 10.</p>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest">Request JSON</label>
<div className="flex gap-2">
<a
href="https://api.random.org/json-rpc/2/request-builder"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 px-2 py-1 rounded-lg border-2 border-[#00f0ff]/50 text-[#00f0ff] text-[10px] hover:bg-[#00f0ff]/10 transition-all"
>
<ExternalLink size={12} />
Request Builder
</a>
<button
type="button"
onClick={copyRequest}
className="inline-flex items-center gap-1 px-2 py-1 rounded-lg border-2 border-[#00f0ff]/50 text-[#00f0ff] text-[10px] hover:bg-[#00f0ff]/10 transition-all"
>
<Copy size={12} />
{copied ? "Copied" : "Copy"}
</button>
</div>
</div>
<textarea
readOnly
value={requestJson}
onFocus={(e) => e.currentTarget.select()}
className="w-full h-36 p-3 bg-[#0a0a0f] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-[10px] text-[#00f0ff] focus:outline-none"
/>
<p className="text-[10px] text-[#6ef3f7]">
Endpoint: <span className="font-mono text-[#00f0ff]">https://api.random.org/json-rpc/1/invoke</span>
</p>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold text-[#00f0ff] uppercase tracking-widest">Paste response JSON</label>
<textarea
value={paste}
onChange={(e) => setPaste(e.target.value)}
placeholder="Paste JSON-RPC response, or paste a [1,6,2,...] array"
className="w-full h-36 p-3 bg-[#16213e] border-2 border-[#00f0ff]/50 rounded-lg font-mono text-[10px] text-[#00f0ff] placeholder-[#9d84b7] focus:outline-none focus:border-[#ff006e] transition-all"
/>
</div>
{error && (
<div className="flex items-start gap-2 p-3 bg-[#0a0a0f] border border-[#ff006e] rounded-lg">
<AlertCircle size={16} className="text-[#ff006e] shrink-0 mt-0.5" />
<p className="text-xs text-[#ff006e]">{error}</p>
</div>
)}
<div className="flex gap-3">
<button
onClick={generate}
className="flex-1 py-2.5 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white text-sm rounded-lg font-bold uppercase hover:shadow-[0_0_20px_rgba(255,0,110,0.5)] transition-all"
>
Generate Seed
</button>
<button
onClick={onCancel}
className="px-4 py-2.5 bg-[#16213e] border-2 border-[#ff006e] text-[#ff006e] rounded-lg font-bold text-sm hover:bg-[#ff006e]/20 transition-all"
>
<X size={16} />
</button>
</div>
</>
)}
{processing && (
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#00f0ff]/30 text-center space-y-3">
<div className="animate-spin mx-auto w-12 h-12 border-4 border-[#00f0ff]/30 border-t-[#00f0ff] rounded-full" />
<p className="text-sm text-[#00f0ff]">Processing entropy...</p>
</div>
)}
{stats && !processing && (
<div className="p-6 bg-[#16213e] rounded-xl border-2 border-[#39ff14] shadow-[0_0_30px_rgba(57,255,20,0.3)] space-y-4 mb-6">
<div className="flex items-center gap-2 text-[#39ff14]">
<CheckCircle2 size={24} />
<h3 className="text-sm font-bold uppercase">Random.org Entropy Analysis</h3>
</div>
<div className="space-y-3 text-xs">
<div>
<p className="text-[#00f0ff] font-bold mb-1">Primary Source</p>
<p className="text-[#6ef3f7]">random.org D6 integers (pasted manually)</p>
</div>
<div>
<p className="text-[#00f0ff] font-bold mb-2">SAMPLES</p>
<div className="grid grid-cols-2 gap-2 font-mono text-[10px]">
<div>Requested</div><div className="text-[#39ff14]">{stats.nRequested}</div>
<div>Used</div><div className="text-[#39ff14]">{stats.nUsed}</div>
<div>Interaction samples</div><div className="text-[#39ff14]">{stats.interactionSamples}</div>
</div>
</div>
<div>
<p className="text-[#00f0ff] font-bold mb-2">DISTRIBUTION</p>
<div className="space-y-1 font-mono text-[10px]">
{stats.distribution.map((count, i) => {
const pct = (count / stats.nUsed) * 100;
return (
<div key={i} className="flex justify-between">
<span>Face {i + 1}</span>
<span className="text-[#39ff14]">{count} ({pct.toFixed(1)}%)</span>
</div>
);
})}
</div>
</div>
<div>
<p className="text-[#00f0ff] font-bold mb-2">GENERATED SEED</p>
<div className="p-3 bg-[#0a0a0f] rounded-lg border border-[#39ff14]/50">
<p className="font-mono text-[10px] text-[#39ff14] blur-sensitive" title="Hover to reveal">
{generatedMnemonic}
</p>
<p className="text-[9px] text-[#6ef3f7] mt-1">👆 Hover to reveal - Write this down securely</p>
</div>
</div>
<div>
<p className="text-[#00f0ff] font-bold mb-2">MIXED WITH</p>
<div className="space-y-1 text-[10px] text-[#6ef3f7]">
<div>- crypto.getRandomValues() </div>
<div>- performance.now() </div>
<div>- Interaction timing ({stats.interactionSamples} samples) </div>
</div>
</div>
<div className="pt-2 border-t border-[#00f0ff]/30">
<div className="flex justify-between items-center">
<span className="text-[#00f0ff] font-bold">Total Entropy</span>
<span className="text-lg font-bold text-[#39ff14]">{stats.totalBits} bits</span>
</div>
</div>
</div>
<div className="pt-4 border-t border-[#00f0ff]/30 space-y-3">
<button
onClick={() => onEntropyGenerated(generatedMnemonic, stats)}
className="w-full py-3 bg-gradient-to-r from-[#ff006e] to-[#ff4d8f] text-white text-sm rounded-xl font-bold uppercase hover:shadow-[0_0_30px_rgba(255,0,110,0.8)] transition-all"
>
Continue with this Seed
</button>
<button
onClick={() => {
setStats(null);
setGeneratedMnemonic("");
setPaste("");
setError("");
}}
className="w-full py-2 bg-[#16213e] border-2 border-[#00f0ff] text-[#00f0ff] rounded-lg text-sm font-bold hover:bg-[#00f0ff]/20 transition-all"
>
Paste a different response
</button>
</div>
</div>
)}
</div>
);
};
export default RandomOrgEntropy;