From cf6299a510036922f6767f1524ec61bc025949b1 Mon Sep 17 00:00:00 2001 From: LC mac Date: Fri, 13 Feb 2026 01:05:13 +0800 Subject: [PATCH] feat: adding new way to use Random.org api to generate seed phrase --- package.json | 2 +- src/App.tsx | 24 +- src/components/RandomOrgEntropy.tsx | 367 ++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 src/components/RandomOrgEntropy.tsx diff --git a/package.json b/package.json index 63ce6e2..a2bd05f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "seedpgp-web", "private": true, - "version": "1.4.6", + "version": "1.4.7", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.tsx b/src/App.tsx index 278d4ca..3cb78a7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(null); const interactionEntropyRef = useRef(new InteractionEntropy()); @@ -770,6 +771,16 @@ function App() { + +
@@ -824,6 +835,17 @@ function App() { /> )} + {/* Random.org Entropy Component */} + {entropySource === 'randomorg' && !generatedSeed && ( + setEntropySource(null)} + interactionEntropy={interactionEntropyRef.current} + /> + )} + {/* Generated Seed Display + Destination Selector */} {generatedSeed && (
diff --git a/src/components/RandomOrgEntropy.tsx b/src/components/RandomOrgEntropy.tsx new file mode 100644 index 0000000..ab5f13c --- /dev/null +++ b/src/components/RandomOrgEntropy.tsx @@ -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 { + 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 = ({ + 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(""); + const [copied, setCopied] = useState(false); + + const [processing, setProcessing] = useState(false); + const [stats, setStats] = useState(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 ( +
+ {!stats && !processing && ( + <> +
+
+ +

🌍 Random.org Entropy

+
+ +
+ +

+ SeedPGP will not contact random.org. You run the request in another tab/tool and paste the response here. +

+
+
+ +
+ +
+ 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} + /> + +
+
+ +
+
+ + {n} +
+ setN(parseInt(e.target.value, 10))} + className="w-full accent-[#ff006e]" + /> +

Minimum 30. Step 10.

+
+ +
+
+ +
+ + + Request Builder + + +
+
+