mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-06 17:37:51 +08:00
feat: adding new way to use Random.org api to generate seed phrase
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "seedpgp-web",
|
||||
"private": true,
|
||||
"version": "1.4.6",
|
||||
"version": "1.4.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
24
src/App.tsx
24
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<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">
|
||||
|
||||
367
src/components/RandomOrgEntropy.tsx
Normal file
367
src/components/RandomOrgEntropy.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user