Files
gh-christianlouis-passwordgen/index.html
T
Christian Krakau-Louis ac7772c24f Initial password gen commit
2025-10-28 02:56:07 +01:00

977 lines
54 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Passwort Generator</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
.hidden { display: none; }
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; width: 20px; height: 20px;
background: #3b82f6; border-radius: 50%; cursor: pointer; margin-top: -6px;
}
input[type="range"]::-moz-range-thumb {
width: 20px; height: 20px; background: #3b82f6; border-radius: 50%; cursor: pointer;
}
#crack-time-tooltip-container:hover #crack-time-tooltip { display: block; }
.code-inline {
font-family: monospace; background-color: #f3f4f6; padding: 0.1em 0.3em;
border-radius: 0.25rem; font-size: 0.9em;
}
.spinner {
border: 2px solid rgba(0, 0, 0, 0.1); border-left-color: #3b82f6; border-radius: 50%;
width: 1em; height: 1em; animation: spin 1s linear infinite; display: inline-block;
vertical-align: middle; margin-left: 4px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.spinner.hidden { display: none; }
/* Stärke-Balken Farben */
.strength-bar-weak { background-color: #ef4444; } /* red-500 */
.strength-bar-medium { background-color: #f59e0b; } /* amber-500 */
.strength-bar-strong { background-color: #22c55e; } /* green-500 */
.strength-bar-very-strong { background-color: #16a34a; } /* green-600 */
</style>
</head>
<body class="bg-gray-100 flex items-center justify-center min-h-screen font-sans">
<div class="bg-white p-6 sm:p-8 rounded-xl shadow-lg w-full max-w-lg mx-4">
<h1 class="text-2xl sm:text-3xl font-bold text-center text-gray-800 mb-6">Passwort Generator</h1>
<!-- Passwort-Anzeige & Kopieren Button -->
<div class="relative mb-4">
<input
type="text"
id="password-display"
readonly
class="w-full p-4 pr-24 text-lg bg-gray-50 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
placeholder="Ihr Passwort..."
>
<button
id="copy-button"
title="Passwort kopieren"
class="absolute top-1/2 right-3 transform -translate-y-1.2 bg-blue-500 text-white px-3 py-2 rounded-md font-semibold hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition-all duration-150"
>
<!-- SVG Clipboard Icon -->
<svg id="copy-icon-clipboard" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<!-- SVG Checkmark Icon (versteckt) -->
<svg id="copy-icon-checkmark" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</button>
</div>
<!-- Feedback-Nachricht -->
<div id="message-box" class="text-center mb-4 min-h-[1.25rem] text-sm">&nbsp;</div>
<!-- Entropie & Stärke-Anzeige -->
<div class="mb-4 bg-gray-50 p-4 rounded-lg space-y-3">
<!-- Entropie -->
<div class="flex justify-between items-center">
<span class="text-sm text-gray-600">Geschätzte Entropie:</span>
<span id="entropy-display" class="font-bold text-gray-800">0 bits</span>
</div>
<!-- Stärke-Balken -->
<div>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="strength-bar" class="h-2.5 rounded-full transition-all duration-300 ease-out strength-bar-weak" style="width: 0%"></div>
</div>
<p id="strength-text" class="text-xs text-gray-500 text-center mt-1">Sehr schwach</p>
</div>
<hr>
<!-- Knackzeit -->
<div class="text-left text-sm text-gray-600">
<span class="font-medium">Geschätzte Knackzeit (Worst-Case 2025):</span>
<div id="crack-time-tooltip-container" class="relative inline-block ml-1 align-middle">
<span class="cursor-help text-blue-500 font-bold">[?]</span>
<div id="crack-time-tooltip" class="hidden absolute bottom-full mb-2 left-1.2 -translate-x-1.2 w-72 p-3 bg-gray-800 text-white text-xs rounded-lg shadow-lg z-10">
Annahme: Offline-Angriff (durchschnittl. 50% des Suchraums) auf einen schnellen Hash (z.B. MD5, SHA1).
<strong class="text-yellow-300">Wichtig:</strong> Moderne Webseiten nutzen langsame Hashes (KDFs wie Argon2id, bcrypt), die diese Zeiten um viele Größenordnungen erhöhen und schwächere Passwörter viel sicherer machen! Diese Schätzung ist pessimistisch.
<br><strong>Statisch:</strong> Konstante Rate (10 Billionen Versuche/s).
<br><strong>Moore:</strong> Rate verdoppelt sich alle 2 Jahre (beginnend bei 10 Billionen Versuche/s).
</div>
</div>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-gray-600 ml-2">Statisch (10 TH/s):</span>
<span id="crack-time-static" class="font-bold text-gray-800">sofort</span>
</div>
<!-- Geschätzter Schutz -->
<div class="flex justify-between items-center text-sm">
<span class="text-gray-600 ml-2 font-semibold text-blue-600">Geschätzter Schutz (Moore, 10 TH/s):</span>
<span id="protection-time-moore-10" class="font-bold text-blue-700">sofort</span>
</div>
</div>
<!-- Optionen-Container -->
<div class="space-y-5">
<!-- Modus: Passphrase vs. Zufällig -->
<div class="flex items-center justify-center p-3 bg-gray-50 rounded-lg">
<label for="human-readable-check" class="text-base font-medium text-gray-700 select-none cursor-pointer flex items-center">
<input type="checkbox" id="human-readable-check" class="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mr-3">
Sichere Passphrase (Diceware)
</label>
</div>
<!-- Container für Passphrase-Optionen -->
<div id="passphrase-options" class="space-y-4 p-4 border rounded-lg hidden">
<div class="flex items-center justify-between">
<label for="word-count-slider" class="text-base font-medium text-gray-700">Anzahl Wörter:</label>
<div class="flex items-center space-x-3">
<!-- GEÄNDERT: max="20" -->
<input type="range" id="word-count-slider" min="3" max="20" value="5" class="w-32 sm:w-40 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<input type="number" id="word-count-number" min="3" max="20" value="5" class="w-16 p-2 text-center border border-gray-300 rounded-md">
</div>
</div>
<div id="language-select-container">
<label for="language-select" class="text-base font-medium text-gray-700">Wortliste:</label>
<select id="language-select" class="w-full p-2 mt-1 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="de">Deutsch (Diceware-Standard, ~7776 Wörter)</option>
<option value="en">Englisch (EFF Large, ~7776 Wörter)</option>
</select>
</div>
</div>
<!-- Container für Zufallsstring-Optionen -->
<div id="random-char-options" class="space-y-4 p-4 border rounded-lg">
<div class="flex items-center justify-between">
<label for="length-slider" class="text-base font-medium text-gray-700">Zeichenlänge:</label>
<div class="flex items-center space-x-3">
<input type="range" id="length-slider" min="8" max="128" value="16" class="w-32 sm:w-40 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<input type="number" id="length-number" min="8" max="128" value="16" class="w-16 p-2 text-center border border-gray-300 rounded-md">
</div>
</div>
<!-- Zeichenset-Infos -->
<div class="text-xs text-gray-500 text-right -mt-2">
Pool: <span id="pool-size-info" class="font-medium">0</span> Zeichen
(<span id="pool-entropy-info" class="font-medium">0.0</span> bits/Zeichen)
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3">
<label class="flex items-center select-none cursor-pointer">
<input type="checkbox" id="uppercase-check" checked class="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<span class="ml-3 text-gray-700">Großbuchstaben (A-Z)</span>
</label>
<label class="flex items-center select-none cursor-pointer">
<input type="checkbox" id="lowercase-check" checked class="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<span class="ml-3 text-gray-700">Kleinbuchstaben (a-z)</span>
</label>
<label class="flex items-center select-none cursor-pointer">
<input type="checkbox" id="numbers-check" checked class="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<span class="ml-3 text-gray-700">Zahlen (0-9)</span>
</label>
<label class="flex items-center select-none cursor-pointer">
<input type="checkbox" id="symbols-check" class="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<span class="ml-3 text-gray-700">Sonderzeichen (!@#$)</span>
</label>
<label class="flex items-center select-none cursor-pointer col-span-1 sm:col-span-2">
<input type="checkbox" id="ambiguous-check" checked class="h-5 w-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
<span class="ml-3 text-gray-700">Doppeldeutige Zeichen vermeiden (l, I, 1, O, 0)</span>
</label>
</div>
</div>
<!-- Generator-Button -->
<button
id="generate-button"
class="w-full bg-green-500 text-white p-4 rounded-lg text-lg font-bold hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 transition-colors"
>
Passwort generieren
</button>
<!-- Empfehlungen / Ziel-Horizont -->
<div id="recommendations" class="p-4 border rounded-lg text-sm space-y-4">
<h3 class="font-medium text-center text-gray-700 mb-1">Passwortlänge für gewünschten Schutz einstellen</h3>
<!-- Schieberegler für Horizont (setzt jetzt die Länge) -->
<div class="flex items-center justify-between">
<label for="horizon-slider" class="text-base font-medium text-gray-700">Ziel-Schutz (Jahre):</label>
<div class="flex items-center space-x-3">
<input type="range" id="horizon-slider" min="1" max="200" value="100" class="w-32 sm:w-40 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
<input type="number" id="horizon-number" min="1" max="200" value="100" class="w-16 p-2 text-center border border-gray-300 rounded-md">
</div>
</div>
<!-- Statische Referenztabelle (wieder sichtbar) -->
<div class="text-xs text-gray-500 space-y-1">
<p class="font-medium text-center mb-1">Referenz: Empfohlene Mindestlängen</p>
<div class="grid grid-cols-4 gap-2 text-center border-b pb-1">
<span class="font-medium">Ziel</span>
<span class="font-medium" title="Groß/Kleinbuchstaben + Zahlen (62 Zeichen)">Alphanum</span>
<span class="font-medium" title="Alphanumerisch + Alle Sonderzeichen (94 Zeichen)">ASCII</span>
<span class="font-medium" title="Diceware Wörter (~12.9 bits/Wort)">Wörter</span>
</div>
<div class="grid grid-cols-4 gap-2 text-center">
<span>10 J.</span>
<span id="ref-10-62" class="font-mono">...</span>
<span id="ref-10-94" class="font-mono">...</span>
<span id="ref-10-dw" class="font-mono">...</span>
</div>
<div class="grid grid-cols-4 gap-2 text-center">
<span>50 J.</span>
<span id="ref-50-62" class="font-mono">...</span>
<span id="ref-50-94" class="font-mono">...</span>
<span id="ref-50-dw" class="font-mono">...</span>
</div>
<div class="grid grid-cols-4 gap-2 text-center">
<span>100 J.</span>
<span id="ref-100-62" class="font-mono">...</span>
<span id="ref-100-94" class="font-mono">...</span>
<span id="ref-100-dw" class="font-mono">...</span>
</div>
</div>
<p class="text-xs text-gray-500 mt-3 text-center">
Basiert auf Moore-Modell (10 TH/s Basis, Verdopplung alle 2 J.). Der Regler setzt die Passwortlänge/-wortanzahl auf den empfohlenen Wert.
</p>
</div>
<!-- Entropie-Fortschrittsbalken -->
<div class="pt-2">
<p id="entropy-status-text" class="text-xs text-gray-500 text-center mb-1">Bewege deine Maus, um Entropie zu sammeln...</p>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="entropy-progress-bar" class="bg-blue-600 h-2.5 rounded-full transition-all duration-150" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<script>
// --- START: Konfiguration für Knackzeit-Schätzung ---
const SECONDS_PER_YEAR = 31557600.0;
const DEFAULT_R0 = 1e13; // 10 TH/s
// ALT_R0 wird nicht mehr für die Anzeige benötigt
const DEFAULT_DOUBLING_YEARS = 2.0;
const BITS_PER_DICEWARE_WORD = Math.log2(7776); // ~12.9 bits
const SETTINGS_KEY = 'passwordGeneratorSettings_v1.3'; // Key für localStorage (Version erhöht)
// --- ENDE: Konfiguration ---
// --- Entropie-Sammler ---
const ENTROPY_POOL_SIZE = 256;
const ENTROPY_POOL_TARGET = 256;
let entropyPool = new Uint32Array(ENTROPY_POOL_SIZE);
let entropyPtr = 0;
let entropyEventsCollected = 0;
const entropyStatusTextEl = document.getElementById('entropy-status-text');
const entropyProgressBarEl = document.getElementById('entropy-progress-bar');
const entropyVisuals = ['|', '/', '—', '\\'];
let visualPtr = 0;
function gatherEntropy(event) {
if (entropyEventsCollected < ENTROPY_POOL_TARGET * 2) {
entropyEventsCollected++;
}
const timestamp = Date.now();
const x = event.clientX;
const y = event.clientY;
entropyPool[entropyPtr] ^= (timestamp & 0xFFFFFFFF); entropyPtr = (entropyPtr + 1) % ENTROPY_POOL_SIZE;
entropyPool[entropyPtr] ^= (x & 0xFFFFFFFF); entropyPtr = (entropyPtr + 1) % ENTROPY_POOL_SIZE;
entropyPool[entropyPtr] ^= (y & 0xFFFFFFFF); entropyPtr = (entropyPtr + 1) % ENTROPY_POOL_SIZE;
visualPtr = (visualPtr + 1) % entropyVisuals.length;
let statusText;
const percentage = Math.min(100, (entropyEventsCollected / ENTROPY_POOL_TARGET) * 100);
entropyProgressBarEl.style.width = `${percentage}%`;
if (entropyEventsCollected < ENTROPY_POOL_TARGET) {
statusText = `Sammle Entropie... (${entropyEventsCollected}/${ENTROPY_POOL_TARGET}) [${entropyVisuals[visualPtr]}]`;
entropyProgressBarEl.className = 'bg-blue-600 h-2.5 rounded-full transition-all duration-150';
entropyStatusTextEl.className = 'text-xs text-blue-600 text-center mb-1';
} else {
statusText = `Entropie-Pool voll. (OK) [${entropyVisuals[visualPtr]}]`;
entropyProgressBarEl.className = 'bg-green-500 h-2.5 rounded-full transition-all duration-150';
entropyStatusTextEl.className = 'text-xs text-green-600 text-center mb-1';
}
entropyStatusTextEl.textContent = statusText;
}
document.body.addEventListener('mousemove', gatherEntropy, { capture: true, passive: true });
function hashEntropyPool() {
if (entropyEventsCollected === 0) return 0;
const poolSnapshot = Uint32Array.from(entropyPool);
let hash = 0;
for (let i = 0; i < poolSnapshot.length; i++) {
hash = (hash * 33 + poolSnapshot[i]) | 0;
}
return hash;
}
function stirEntropyPool(seedHash) {
let pseudoRandom = seedHash | 0;
for (let i = 0; i < ENTROPY_POOL_SIZE; i++) {
pseudoRandom = (pseudoRandom * 1664525 + 1013904223) | 0;
entropyPool[i] ^= pseudoRandom;
}
entropyEventsCollected = 0; entropyPtr = 0;
entropyStatusTextEl.textContent = "Bewege deine Maus, um Entropie zu sammeln...";
entropyStatusTextEl.className = 'text-xs text-gray-500 text-center mb-1';
entropyProgressBarEl.style.width = '0%';
entropyProgressBarEl.className = 'bg-blue-600 h-2.5 rounded-full transition-all duration-150';
}
function getUserEntropyValue() {
const hashVal = hashEntropyPool();
stirEntropyPool(hashVal);
return hashVal;
}
// --- ENDE: Entropie-Sammler ---
// --- START: WORTLISTEN ---
const PROXY_URL = 'https://api.allorigins.win/raw?url=';
const WORDLIST_URLS = {
de: 'https://raw.githubusercontent.com/bjoernalbers/diceware-wordlist-german/refs/heads/main/wordlist-german-diceware.txt',
en: 'https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt'
};
let wordlistCache = { de: null, en: null };
// --- ENDE: WORTLISTEN ---
// --- DOM-Elemente ---
const passwordDisplay = document.getElementById('password-display');
const copyButton = document.getElementById('copy-button');
const copyIconClipboard = document.getElementById('copy-icon-clipboard');
const copyIconCheckmark = document.getElementById('copy-icon-checkmark');
const messageBox = document.getElementById('message-box');
const entropyDisplay = document.getElementById('entropy-display');
const strengthBar = document.getElementById('strength-bar');
const strengthText = document.getElementById('strength-text');
// Knackzeit
const crackTimeStaticEl = document.getElementById('crack-time-static'); // Benötigt für updateCrackTimeDisplays
const protectionTimeMoore10El = document.getElementById('protection-time-moore-10');
// Optionen
const humanCheck = document.getElementById('human-readable-check');
const passphraseOptions = document.getElementById('passphrase-options');
const randomCharOptions = document.getElementById('random-char-options');
const wordCountSlider = document.getElementById('word-count-slider');
const wordCountNumber = document.getElementById('word-count-number');
const languageSelect = document.getElementById('language-select');
const lengthSlider = document.getElementById('length-slider');
const lengthNumber = document.getElementById('length-number');
const poolSizeInfo = document.getElementById('pool-size-info');
const poolEntropyInfo = document.getElementById('pool-entropy-info');
const uppercaseCheck = document.getElementById('uppercase-check');
const lowercaseCheck = document.getElementById('lowercase-check');
const numbersCheck = document.getElementById('numbers-check');
const symbolsCheck = document.getElementById('symbols-check');
const ambiguousCheck = document.getElementById('ambiguous-check');
const generateButton = document.getElementById('generate-button');
// Horizont-Regler
const horizonSlider = document.getElementById('horizon-slider');
const horizonNumber = document.getElementById('horizon-number');
// Referenz-Tabelle
const ref10_62 = document.getElementById('ref-10-62');
const ref10_94 = document.getElementById('ref-10-94');
const ref10_dw = document.getElementById('ref-10-dw');
const ref50_62 = document.getElementById('ref-50-62');
const ref50_94 = document.getElementById('ref-50-94');
const ref50_dw = document.getElementById('ref-50-dw');
const ref100_62 = document.getElementById('ref-100-62');
const ref100_94 = document.getElementById('ref-100-94');
const ref100_dw = document.getElementById('ref-100-dw');
// --- ENDE DOM-Elemente ---
// --- Hilfsfunktionen ---
const CHAR_SETS = {
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', lowercase: 'abcdefghijklmnopqrstuvwxyz',
numbers: '0123456789', symbols: '!@#$%^&*-_=+.?'
};
const AMBIGUOUS_CHARS = /[lI1O0|]/g;
// --- BERECHNUNGSFUNKTIONEN ---
function required_bits_for_horizon(years, r0 = DEFAULT_R0, doubling_years = DEFAULT_DOUBLING_YEARS) {
const Td = doubling_years * SECONDS_PER_YEAR; const T = years * SECONDS_PER_YEAR; const C = r0 * Td / Math.log(2.0);
const exponent = T / Td; let growthFactor;
if (exponent > 100) {
if (C <= 0) return 0.0;
const log2_2C = Math.log2(2.0 * C);
if (!isFinite(log2_2C)) return 1024;
const bits = log2_2C + exponent;
return Math.min(bits, 1024);
} else {
growthFactor = Math.pow(2.0, exponent) - 1.0;
}
const val = 2.0 * C * growthFactor; if (val <= 1.0 || !isFinite(val)) return 0.0;
const bits = Math.log2(val);
return Math.min(bits, 1024);
}
function bits_to_length(bits, charset_or_bits_per_symbol) {
let bits_per_symbol = (charset_or_bits_per_symbol < 20) ? charset_or_bits_per_symbol : Math.log2(charset_or_bits_per_symbol);
if (bits_per_symbol <= 0 || bits <= 0) return 1; // Mindestens Länge 1 zurückgeben, wenn Bits > 0
// Sicherstellen, dass die Mindestlänge des Sliders nicht unterschritten wird
// GEÄNDERT: Prüfe, welcher Slider gerade aktiv ist
const minLen = document.getElementById('human-readable-check').checked ? parseInt(document.getElementById('word-count-slider').min, 10) : parseInt(document.getElementById('length-slider').min, 10);
return Math.max(minLen, Math.ceil(bits / bits_per_symbol));
}
function secureRandomWordArray(byteLength) {
const buffer = new Uint8Array(byteLength);
window.crypto.getRandomValues(buffer);
const userEntropy = getUserEntropyValue();
for(let i = 0; i < byteLength; i++) {
buffer[i] ^= (userEntropy >> ((i % 4) * 8)) & 0xFF;
}
return buffer;
}
function secureRandomIndex(max) {
if (max <= 0) return 0;
const requiredBytes = Math.ceil(Math.log2(max) / 8);
const randomBytes = secureRandomWordArray(requiredBytes + 4);
let randomVal = 0n;
const bytesToUse = Math.min(randomBytes.length, 8);
for (let i = 0; i < bytesToUse; i++) {
randomVal = (randomVal << 8n) | BigInt(randomBytes[i]);
}
return Number(randomVal % BigInt(max));
}
function getRandomChar(charSet) {
if (!charSet || charSet.length === 0) return ''; const index = secureRandomIndex(charSet.length); return charSet[index];
}
function shuffleString(str) {
let arr = str.split(''); for (let i=arr.length-1; i>0; i--) { const j = secureRandomIndex(i+1); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr.join('');
}
let messageTimer;
function showMessage(msg, type = 'info') {
clearTimeout(messageTimer); messageBox.textContent = msg;
if (type === 'success') messageBox.className = 'text-center mb-4 min-h-[1.25rem] text-sm text-green-600';
else if (type === 'error') messageBox.className = 'text-center mb-4 min-h-[1.25rem] text-sm text-red-600';
else messageBox.className = 'text-center mb-4 min-h-[1.25rem] text-sm text-gray-600';
if (type !== 'info') messageTimer = setTimeout(() => { if (messageBox.textContent === msg) { messageBox.textContent = '\u00A0'; messageBox.className = 'text-center mb-4 min-h-[1.25rem] text-sm'; } }, 2000);
}
function human_duration(seconds) {
if (!isFinite(seconds) || seconds > 3.15e19) return "Äonen";
if (seconds < 1) return 'sofort';
if (seconds < 60) return `~ ${seconds.toFixed(0)} Sek.`;
if (seconds < 3600) return `~ ${(seconds / 60).toFixed(0)} Min.`;
if (seconds < 86400) return `~ ${(seconds / 3600).toFixed(0)} Std.`;
if (seconds < 2629746) return `~ ${(seconds / 86400).toFixed(0)} Tage`;
if (seconds < 31556952) return `~ ${(seconds / 2629746).toFixed(0)} Monate`;
const years = seconds / SECONDS_PER_YEAR;
if (years < 1000) {
if (years < 10) return `~ ${years.toPrecision(2)} Jahre`;
if (years < 100) return `~ ${years.toPrecision(3)} Jahre`;
return `~ ${years.toFixed(0)} Jahre`;
}
if (years < 1e6) return `~ ${(years / 1000).toPrecision(2)} Jahrtaus.`;
if (years < 1e9) return `~ ${(years / 1e6).toPrecision(2)} Mio. J.`;
return `~ ${(years / 1e9).toPrecision(2)} Mrd. J.`;
}
// BERECHNET JETZT SCHUTZDAUER IN JAHREN
function calculateProtectionYears(bits, r0 = DEFAULT_R0, doubling_years = DEFAULT_DOUBLING_YEARS) {
const T_seconds = time_to_crack_moore(bits, r0, doubling_years);
if (!isFinite(T_seconds) || T_seconds <= 0) return 0; // Kein Schutz oder unendlich
return T_seconds / SECONDS_PER_YEAR;
}
function updateCrackTimeDisplays(entropyBits) {
const validBits = Math.max(0, entropyBits);
// Statische Knackzeit
let staticSec = Infinity;
if (validBits <= 1023) {
try {
const staticGuesses = Math.pow(2, validBits - 1);
staticSec = staticGuesses / DEFAULT_R0;
} catch (e) {
console.warn("Fehler bei statischer Knackzeit-Berechnung (zu groß?)", e);
}
}
crackTimeStaticEl.textContent = human_duration(staticSec);
// Berechne und zeige Schutzdauer in Jahren
const protectionYears = calculateProtectionYears(validBits, DEFAULT_R0, DEFAULT_DOUBLING_YEARS);
protectionTimeMoore10El.textContent = human_duration(protectionYears * SECONDS_PER_YEAR);
}
function time_to_crack_moore(bits, r0 = DEFAULT_R0, doubling_years = DEFAULT_DOUBLING_YEARS) {
const Td = doubling_years * SECONDS_PER_YEAR;
if (bits <= 0) return 0.0;
// Bei sehr hoher Entropie direkt Infinity zurückgeben, um Rechenfehler zu vermeiden
if (bits > 1000) return Infinity; // Angepasste Grenze
const exponent_keyspace = bits - 1.0;
let keyspace_half_num; // Wird jetzt als Number behandelt
// Versuche, 2^(bits-1) als Number zu berechnen
try {
// Check for large exponents that would cause Infinity immediately
if (exponent_keyspace > 1023) return Infinity; // Max exponent for JS Number
keyspace_half_num = Math.pow(2.0, exponent_keyspace);
if (!isFinite(keyspace_half_num)) {
return Infinity;
}
} catch (e) {
console.warn("Math.pow Fehler:", e);
return Infinity;
}
const C = r0 * Td / Math.log(2.0);
if (C <= 0 || !isFinite(C)) return 0.0;
// Jetzt nur noch mit Number rechnen
const ratio = keyspace_half_num / C;
// Behandlung für sehr großes Verhältnis (keyspace >> C)
if (!isFinite(ratio) || ratio > 1e15) { // Angepasste Grenze für Annäherung
try {
const log2_keyspace_approx = bits - 1.0;
const log2_C_approx = Math.log2(C);
if (!isFinite(log2_C_approx)) return Infinity;
const T_approx = Td * (log2_keyspace_approx - log2_C_approx);
return (T_approx > 0 && isFinite(T_approx)) ? T_approx : Infinity;
} catch (e) {
return Infinity;
}
}
const val = 1.0 + ratio;
if (val <= 1.0 || !isFinite(val)) {
// Behandlung für sehr kleines Verhältnis (keyspace << C)
if (keyspace_half_num > 0 && C > 0 && ratio < 1e-9 && ratio > 0) {
// log2(1+x) ≈ x / ln(2) for small x
const T_approx = Td * ratio / Math.log(2.0);
return Math.max(0, T_approx);
} else {
// Wenn ratio 0 oder negativ ist (sollte nicht passieren, aber sicherheitshalber)
return 0.0;
}
}
const log2_val = Math.log2(val); if (!isFinite(log2_val)) return Infinity;
const T = Td * log2_val; return Math.max(0, T);
}
function updateStrengthBar(bits) {
let percentage = 0; let text = 'Sehr schwach'; let colorClass = 'strength-bar-weak';
if (bits >= 128) { percentage = 100; text = 'Extrem stark'; colorClass = 'strength-bar-very-strong'; }
else if (bits >= 100) { percentage = 75 + (bits - 100) * (25 / 28); text = 'Sehr stark'; colorClass = 'strength-bar-strong'; }
else if (bits >= 70) { percentage = 40 + (bits - 70) * (35 / 30); text = 'Gut'; colorClass = 'strength-bar-medium'; }
else if (bits >= 40) { percentage = 10 + (bits - 40) * (30 / 30); text = 'Mittel'; colorClass = 'strength-bar-weak'; }
else if (bits > 0) { percentage = bits * (10 / 40); text = 'Schwach'; colorClass = 'strength-bar-weak'; }
strengthBar.style.width = `${Math.min(100, Math.max(0, percentage))}%`;
strengthBar.className = `h-2.5 rounded-full transition-all duration-300 ease-out ${colorClass}`;
strengthText.textContent = text;
if (colorClass === 'strength-bar-weak') strengthText.className = 'text-xs text-red-500 text-center mt-1';
else if (colorClass === 'strength-bar-medium') strengthText.className = 'text-xs text-amber-500 text-center mt-1';
else if (colorClass === 'strength-bar-strong') strengthText.className = 'text-xs text-green-500 text-center mt-1';
else strengthText.className = 'text-xs text-green-600 text-center mt-1';
}
function updateEntropy(bits) {
const validBits = Math.max(0, bits);
entropyDisplay.textContent = `${validBits.toFixed(1)} bits`;
updateCrackTimeDisplays(validBits);
updateStrengthBar(validBits);
}
// --- WORTLISTEN-FUNKTIONEN ---
function parseWordlist(text) {
const words = []; const lines = text.split('\n');
for (const line of lines) {
const parts = line.split('\t');
const word = (parts.length >= 2 ? parts[1] : parts[0]).trim().toLowerCase();
if (word.length >= 4 && word.length <= 8 && /^[a-z]+$/.test(word)) { words.push(word); }
}
console.log(`Wortliste geparsed, ${words.length} valide Wörter gefunden.`);
return words;
}
async function loadWordlist(lang) {
if (wordlistCache[lang]) return wordlistCache[lang];
const url = WORDLIST_URLS[lang]; const proxyFetchUrl = PROXY_URL + encodeURIComponent(url);
showMessage(`Lade Wortliste (${lang.toUpperCase()})...`, 'info');
try {
const resp = await fetch(proxyFetchUrl);
if (!resp.ok) throw new Error(`Netzwerk (${resp.status}) für ${url}`);
const text = await resp.text();
const words = parseWordlist(text);
const effectiveBitsPerWord = words.length > 0 ? Math.log2(words.length) : BITS_PER_DICEWARE_WORD;
wordlistCache[`${lang}_bits`] = effectiveBitsPerWord;
if (words.length < 1000) throw new Error(`Liste '${lang}' zu klein (${words.length}).`);
wordlistCache[lang] = words;
showMessage(`Wortliste (${lang.toUpperCase()}) mit ${words.length} Wörtern geladen (~${effectiveBitsPerWord.toFixed(1)} bits/Wort).`, 'success');
// Fülle die Tabelle neu, falls die Bits/Wort sich geändert haben (z.B. nach erstem Laden)
fillStaticRecommendations();
// Wende den aktuellen Horizont neu an, falls sich die Bits/Wort geändert haben
handleHorizonUpdate(); // Ruft toggleMode intern auf
return words;
} catch (error) {
console.error('Fehler Laden Wortliste:', error);
showMessage(`Fehler Laden '${lang}'. Prüfe Konsole.`, 'error');
wordlistCache[`${lang}_bits`] = BITS_PER_DICEWARE_WORD;
// Fülle Tabelle mit Standardwerten
fillStaticRecommendations();
throw error;
}
}
// --- Passwort-Generatoren ---
function generateHumanPass(opts, wordlist) {
const count = opts.length; const sep = '-'; let pw = []; if (!wordlist || wordlist.length === 0) throw new Error("Leere Wortliste"); for (let i=0; i<count; i++) pw.push(wordlist[secureRandomIndex(wordlist.length)]); return pw.join(sep);
}
function buildCharPool(opts) {
let pool = ''; if (opts.uppercase) pool += CHAR_SETS.uppercase; if (opts.lowercase) pool += CHAR_SETS.lowercase; if (opts.numbers) pool += CHAR_SETS.numbers; if (opts.symbols) pool += CHAR_SETS.symbols; if (opts.avoidAmbiguous) pool = pool.replace(AMBIGUOUS_CHARS, '');
const poolSize = pool.length; // Größe zwischenspeichern
const entropy = poolSize > 0 ? Math.log2(poolSize) : 0;
poolSizeInfo.textContent = poolSize;
poolEntropyInfo.textContent = entropy.toFixed(1);
return { charPool: pool, poolEntropy: entropy, poolSize: poolSize }; // Pool-Größe zurückgeben
}
function generateRandomPass(opts, charPool) {
let pw = ''; let guaranteed = [];
const upperPool = CHAR_SETS.uppercase.replace(opts.avoidAmbiguous ? AMBIGUOUS_CHARS : '', '');
if (opts.uppercase && upperPool.length > 0) guaranteed.push(getRandomChar(upperPool));
const lowerPool = CHAR_SETS.lowercase.replace(opts.avoidAmbiguous ? AMBIGUOUS_CHARS : '', '');
if (opts.lowercase && lowerPool.length > 0) guaranteed.push(getRandomChar(lowerPool));
const numPool = CHAR_SETS.numbers.replace(opts.avoidAmbiguous ? AMBIGUOUS_CHARS : '', '');
if (opts.numbers && numPool.length > 0) guaranteed.push(getRandomChar(numPool));
const symPool = CHAR_SETS.symbols.replace(opts.avoidAmbiguous ? AMBIGUOUS_CHARS : '', '');
if (opts.symbols && symPool.length > 0) guaranteed.push(getRandomChar(symPool));
const guaranteedCount = guaranteed.length;
if (charPool.length === 0 && opts.length > 0) throw new Error("Zeichenpool leer");
const remainingLen = Math.max(0, opts.length - guaranteedCount);
for (let i = 0; i < remainingLen; i++) { if (charPool.length === 0) break; pw += getRandomChar(charPool); }
pw += guaranteed.join('');
while (pw.length < opts.length) { if (charPool.length === 0) break; pw += getRandomChar(charPool); }
return shuffleString(pw);
}
// --- UI-Logik ---
function syncInputs(source, target) { target.value = source.value; }
// Globale Variable, um Rekursion zu verhindern
let isUpdatingFromHorizon = false;
// Wird aufgerufen, wenn Länge/Wortzahl oder Optionen geändert werden
function handleManualUpdate() {
if (isUpdatingFromHorizon) return; // Nicht ausführen, wenn Horizont-Slider aktiv ist
toggleMode(); // Berechnet Entropie neu etc.
}
// Wird aufgerufen, wenn der Horizont-Slider geändert wird
function handleHorizonUpdate() {
if (isUpdatingFromHorizon) return; // Verhindere Endlosschleife
isUpdatingFromHorizon = true; // Sperre setzen
try {
syncInputs(horizonSlider, horizonNumber); // Sync Nummer
const years = parseInt(horizonNumber.value, 10);
const bits = required_bits_for_horizon(years, DEFAULT_R0, DEFAULT_DOUBLING_YEARS);
// Empfohlene Längen berechnen
const currentLang = languageSelect.value;
const bitsPerWord = wordlistCache[`${currentLang}_bits`] || BITS_PER_DICEWARE_WORD;
const recLenDw = bits > 0 ? bits_to_length(bits, bitsPerWord) : 1;
if (humanCheck.checked) {
// Wortanzahl setzen
const newWordCount = Math.min(parseInt(wordCountSlider.max, 10), Math.max(parseInt(wordCountSlider.min, 10), recLenDw));
wordCountSlider.value = newWordCount;
wordCountNumber.value = newWordCount;
} else {
// Zeichenlänge setzen
const opts = readRandomCharOptions(); // Lese AKTUELLE Optionen
const { poolSize } = buildCharPool(opts); // Rufe buildCharPool auf, um poolSize zu bekommen
let targetLength;
const recLen62 = bits > 0 ? bits_to_length(bits, 62) : 1;
const recLen94 = bits > 0 ? bits_to_length(bits, 94) : 1;
if (poolSize >= 94) targetLength = recLen94;
else if (poolSize >= 62) targetLength = recLen62;
else if (poolSize > 0) targetLength = bits_to_length(bits, poolSize);
else targetLength = parseInt(lengthSlider.min, 10);
const newLength = Math.min(parseInt(lengthSlider.max, 10), Math.max(parseInt(lengthSlider.min, 10), targetLength));
lengthSlider.value = newLength;
lengthNumber.value = newLength;
}
// Jetzt die Entropie basierend auf der *neuen* Länge/Wortzahl aktualisieren
toggleMode(); // Dies aktualisiert Entropie etc.
} finally {
isUpdatingFromHorizon = false; // Sperre lösen
}
// Fülle die statische Referenztabelle (passiert immer)
fillStaticRecommendations();
saveSettings(); // Speichere die Horizont-Änderung
}
function toggleMode() {
const isHuman = humanCheck.checked;
passphraseOptions.classList.toggle('hidden', !isHuman);
randomCharOptions.classList.toggle('hidden', isHuman);
let currentEntropy = 0;
if (isHuman) {
const count = parseInt(wordCountNumber.value,10);
const lang = languageSelect.value;
const bitsPer = wordlistCache[`${lang}_bits`] || BITS_PER_DICEWARE_WORD;
currentEntropy = bitsPer * count;
} else {
const opts = readRandomCharOptions();
const { charPool, poolEntropy } = buildCharPool(opts);
currentEntropy = charPool.length > 0 ? poolEntropy * opts.length : 0;
}
updateEntropy(currentEntropy);
// Nur speichern, wenn nicht gerade vom Horizont aktualisiert wird,
// da handleHorizonUpdate bereits saveSettings aufruft.
if (!isUpdatingFromHorizon) {
saveSettings();
}
}
function readRandomCharOptions() {
return {
length: parseInt(lengthNumber.value, 10),
uppercase: uppercaseCheck.checked,
lowercase: lowercaseCheck.checked,
numbers: numbersCheck.checked,
symbols: symbolsCheck.checked,
avoidAmbiguous: ambiguousCheck.checked
};
}
// Füllt die statische Referenztabelle
function fillStaticRecommendations() {
const horizons = [10, 50, 100];
const elements = {
10: { 62: ref10_62, 94: ref10_94, dw: ref10_dw },
50: { 62: ref50_62, 94: ref50_94, dw: ref50_dw },
100: { 62: ref100_62, 94: ref100_94, dw: ref100_dw },
};
const currentLang = languageSelect.value;
const bitsPerWord = wordlistCache[`${currentLang}_bits`] || BITS_PER_DICEWARE_WORD;
horizons.forEach(years => {
const bits = required_bits_for_horizon(years, DEFAULT_R0, DEFAULT_DOUBLING_YEARS);
// Verwende bits_to_length mit den spezifischen Charsets/Bits
const len62 = bits > 0 ? bits_to_length(bits, 62) : '-';
const len94 = bits > 0 ? bits_to_length(bits, 94) : '-';
const lenDw = bits > 0 ? bits_to_length(bits, bitsPerWord) : '-';
elements[years][62].textContent = len62;
elements[years][94].textContent = len94;
elements[years]['dw'].textContent = lenDw;
});
}
async function handleGenerateClick() {
const isHumanReadable = humanCheck.checked;
let password = ''; let entropy = 0;
try {
generateButton.disabled = true; generateButton.textContent = 'Generiere...'; showMessage('Generiere...', 'info');
await new Promise(resolve => setTimeout(resolve, 0));
if (isHumanReadable) {
const lang = languageSelect.value; const wordCount = parseInt(wordCountNumber.value, 10);
const wordlist = await loadWordlist(lang);
if (!wordlist || wordlist.length === 0) throw new Error("Wortliste leer.");
const opts = { length: wordCount }; password = generateHumanPass(opts, wordlist);
const bitsPerWord = wordlistCache[`${lang}_bits`] || BITS_PER_DICEWARE_WORD; entropy = bitsPerWord * wordCount;
showMessage('Passphrase generiert.', 'success');
} else {
const opts = readRandomCharOptions(); const { charPool, poolEntropy } = buildCharPool(opts);
if (charPool.length === 0) {
showMessage('Zeichenpool leer.', 'error'); passwordDisplay.value = ''; updateEntropy(0);
generateButton.disabled = false; generateButton.textContent = 'Passwort generieren'; return;
}
password = generateRandomPass(opts, charPool); entropy = poolEntropy * opts.length;
showMessage('Passwort generiert.', 'success');
}
passwordDisplay.value = password; updateEntropy(entropy);
} catch (error) {
console.error('Generierung fehlgeschlagen:', error); passwordDisplay.value = 'Fehler.'; updateEntropy(0);
showMessage(`Fehler: ${error.message}`, 'error');
} finally {
generateButton.disabled = false; generateButton.textContent = 'Passwort generieren';
if (messageBox.textContent === 'Generiere...') { showMessage('\u00A0'); }
}
}
function saveSettings() {
const settings = {
mode: humanCheck.checked ? 'human' : 'random', wordCount: parseInt(wordCountNumber.value, 10),
language: languageSelect.value, length: parseInt(lengthNumber.value, 10),
uppercase: uppercaseCheck.checked, lowercase: lowercaseCheck.checked, numbers: numbersCheck.checked,
symbols: symbolsCheck.checked, ambiguous: ambiguousCheck.checked,
horizon: parseInt(horizonNumber.value, 10) // Speichere den Horizont
};
try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); }
catch (e) { console.warn("Speichern fehlgeschlagen:", e); }
}
function loadSettings() {
try {
const savedSettings = localStorage.getItem(SETTINGS_KEY);
if (savedSettings) {
const settings = JSON.parse(savedSettings);
humanCheck.checked = (settings.mode === 'human');
if (settings.wordCount) { wordCountSlider.value = settings.wordCount; wordCountNumber.value = settings.wordCount; }
if (settings.language) languageSelect.value = settings.language;
if (settings.length) { lengthSlider.value = settings.length; lengthNumber.value = settings.length; }
uppercaseCheck.checked = settings.uppercase !== undefined ? settings.uppercase : true;
lowercaseCheck.checked = settings.lowercase !== undefined ? settings.lowercase : true;
numbersCheck.checked = settings.numbers !== undefined ? settings.numbers : true;
symbolsCheck.checked = settings.symbols !== undefined ? settings.symbols : false;
ambiguousCheck.checked = settings.ambiguous !== undefined ? settings.ambiguous : true;
if (settings.horizon) {
horizonSlider.value = settings.horizon;
horizonNumber.value = settings.horizon;
}
console.log("Einstellungen geladen:", settings);
} else {
console.log("Keine gespeicherten Einstellungen gefunden.");
horizonSlider.value = 100;
horizonNumber.value = 100;
}
} catch (e) {
console.error("Fehler Laden Einstellungen:", e);
horizonSlider.value = 100;
horizonNumber.value = 100;
}
fillStaticRecommendations();
// Wende den geladenen/Standard-Horizont an, um Längen zu setzen
// WICHTIG: Rufe dies NACH dem Füllen der Tabelle auf
handleHorizonUpdate(); // Verwende handleHorizonUpdate statt updateRecommendations
}
// --- Event Listeners ---
// Wenn LÄNGE/WORTZAHL direkt geändert wird -> handleManualUpdate aufrufen
lengthSlider.addEventListener('input', () => { syncInputs(lengthSlider, lengthNumber); handleManualUpdate(); });
lengthNumber.addEventListener('input', () => { syncInputs(lengthNumber, lengthSlider); handleManualUpdate(); });
wordCountSlider.addEventListener('input', () => { syncInputs(wordCountSlider, wordCountNumber); handleManualUpdate(); });
wordCountNumber.addEventListener('input', () => { syncInputs(wordCountNumber, wordCountSlider); handleManualUpdate(); });
// Wenn MODUS oder OPTIONEN geändert werden -> handleManualUpdate aufrufen
humanCheck.addEventListener('change', handleManualUpdate);
[uppercaseCheck, lowercaseCheck, numbersCheck, symbolsCheck, ambiguousCheck, languageSelect].forEach(input => {
input.addEventListener('change', handleManualUpdate);
});
// Wenn HORIZONT geändert wird -> handleHorizonUpdate aufrufen (setzt Länge/Wortzahl etc.)
horizonSlider.addEventListener('input', handleHorizonUpdate);
horizonNumber.addEventListener('input', () => { syncInputs(horizonNumber, horizonSlider); handleHorizonUpdate(); }); // Sync Slider bei Nummerneingabe
// Generieren & Kopieren
generateButton.addEventListener('click', handleGenerateClick);
copyButton.addEventListener('click', () => {
const pw = passwordDisplay.value; if (!pw || pw === 'Fehler.') { showMessage('Nichts zu kopieren.', 'error'); return; }
function showCopySuccess() { /* ... unverändert ... */
copyIconClipboard.classList.add('hidden'); copyIconCheckmark.classList.remove('hidden');
copyButton.classList.replace('bg-blue-500', 'bg-green-500'); copyButton.classList.replace('hover:bg-blue-600', 'hover:bg-green-600');
setTimeout(() => {
copyIconClipboard.classList.remove('hidden'); copyIconCheckmark.classList.add('hidden');
copyButton.classList.replace('bg-green-500', 'bg-blue-500'); copyButton.classList.replace('hover:bg-green-600', 'hover:bg-blue-600');
}, 1500);
}
// Versuche zuerst den Fallback (document.execCommand), da er zuverlässiger ist
try {
// Stelle sicher, dass das Element fokussierbar ist
const originalTabIndex = passwordDisplay.getAttribute('tabindex');
passwordDisplay.setAttribute('tabindex', '-1');
passwordDisplay.focus();
passwordDisplay.select();
// Für mobile Geräte
passwordDisplay.setSelectionRange(0, 99999);
const successful = document.execCommand('copy');
// Setze tabindex zurück und entferne Fokus
if (originalTabIndex !== null) {
passwordDisplay.setAttribute('tabindex', originalTabIndex);
} else {
passwordDisplay.removeAttribute('tabindex');
}
passwordDisplay.blur();
window.getSelection().removeAllRanges(); // Entferne Markierung
if (successful) {
showMessage('Kopiert! (Fallback)', 'success');
showCopySuccess();
return; // Erfolg, nicht weiter versuchen
} else {
// Wenn execCommand fehlschlägt, versuche navigator.clipboard (falls verfügbar)
throw new Error('execCommand war nicht erfolgreich');
}
} catch (fallbackErr) {
console.warn('Clip fehlgeschlagen (Fallback execCommand):', fallbackErr);
// Wenn Fallback fehlschlägt, versuche navigator.clipboard (falls vorhanden)
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(pw).then(() => {
showMessage('Kopiert!', 'success');
showCopySuccess();
}).catch(navErr => {
console.error('Clip fehlgeschlagen (navigator.clipboard):', navErr);
showMessage('Kopieren fehlgeschlagen. Bitte manuell kopieren.', 'error');
});
} else {
// Wenn beides fehlschlägt
showMessage('Kopieren fehlgeschlagen. Bitte manuell kopieren.', 'error');
}
}
});
// --- UNIT-TESTS ---
function runUnitTests() { /* ... unverändert ... */
console.log("Starte Unit-Tests..."); let passed = 0, failed = 0;
function assert(c, m) { if (c) { console.log(` ✅ PASS: ${m}`); passed++; } else { console.error(` ❌ FAIL: ${m}`); failed++; } }
function assertApprox(v1, v2, tol=0.01, m) { const diff=Math.abs(v1-v2); const relDiff=v2!==0 ? Math.abs(diff/v2) : diff; assert(relDiff<tol || diff<1e-9, `${m} (Wert: ${v1}, Erw: ~${v2})`); }
const ttc_80=time_to_crack_moore(80,1e13,2.0); const exp_ttc_80=591894220; assertApprox(ttc_80,exp_ttc_80,0.01,"TTC(80b) ~18.76J");
const ttc_40=time_to_crack_moore(40,1e13,2.0); assert(ttc_40 < 1 && ttc_40 >= 0, `TTC(40b) < 1s (Wert: ${ttc_40.toExponential(3)})`);
const bits_10y=required_bits_for_horizon(10,1e13,2.0); const exp_bits_10y=75.58; assertApprox(bits_10y,exp_bits_10y,0.01,"Bits(10J) ~75.6b");
const bits_100y=required_bits_for_horizon(100,1e13,2.0); const exp_bits_100y=120.63; assertApprox(bits_100y,exp_bits_100y,0.01,"Bits(100J) ~120.6b");
assert(bits_to_length(75.58,62)>=13,"Len(75.6b, 62) >= 13"); // >= wegen min Länge
assert(bits_to_length(75.58,94)>=12,"Len(75.6b, 94) >= 12");
assert(bits_to_length(75.58,BITS_PER_DICEWARE_WORD)>=6,"Len(75.6b, DW) >= 6");
const rt_bits = required_bits_for_horizon(ttc_80 / SECONDS_PER_YEAR, 1e13, 2.0); assertApprox(rt_bits, 80.0, 0.05, `Roundtrip TTC->Bits => ~80b (Wert: ${rt_bits.toFixed(2)})`);
assert(hashEntropyPool()===0,"Pool Hash initial = 0");
entropyPool=new Uint32Array(ENTROPY_POOL_SIZE); entropyPtr=0; entropyEventsCollected=0;
entropyPool[0]=12345; entropyPool[10]=67890; entropyEventsCollected=2;
const h1 = hashEntropyPool();
let expected_h1 = 0;
expected_h1 = (expected_h1 * 33 + 12345) | 0;
for (let i = 1; i < 10; i++) expected_h1 = (expected_h1 * 33 + 0) | 0;
expected_h1 = (expected_h1 * 33 + 67890) | 0;
for (let i = 11; i < ENTROPY_POOL_SIZE; i++) expected_h1 = (expected_h1 * 33 + 0) | 0;
assert(h1 === expected_h1, `Pool Hash nach Sim (Ist: ${h1}, Erw: ${expected_h1})`);
let ok=true; let badIdx=-1; entropyPool=new Uint32Array(ENTROPY_POOL_SIZE); entropyPtr=0; entropyEventsCollected=0;
for(let i=0;i<500;i++){const idx=secureRandomIndex(50); if(idx<0||idx>=50){ok=false;badIdx=idx;break;}}
assert(ok, `secureRandomIndex(50) 0-49 (Fehler bei Wert: ${badIdx})`);
entropyPool.fill(0); entropyEventsCollected = 0; entropyPtr = 0;
entropyPool.fill(456);
entropyEventsCollected = 200;
entropyStatusTextEl.textContent = "TEST";
const initialPoolValue = entropyPool[0];
const hVal = getUserEntropyValue();
assert(entropyEventsCollected === 0, "getUserEntropy setzt Events zurück");
const resetTxt = "Bewege deine Maus, um Entropie zu sammeln...";
assert(entropyStatusTextEl.textContent === resetTxt, `getUserEntropy setzt Text zurück (Ist:"${entropyStatusTextEl.textContent}")`);
assert(entropyProgressBarEl.style.width === '0%', "getUserEntropy setzt Balken zurück");
let pseudoRandomForIndex0 = (hVal * 1664525 + 1013904223) | 0;
const expectedValueAfterStir = (initialPoolValue ^ pseudoRandomForIndex0) | 0;
const actualValueAfterStir = entropyPool[0] | 0;
assert(actualValueAfterStir === expectedValueAfterStir, `getUserEntropy rührt Pool (Index 0 - Ist: ${actualValueAfterStir}, Erw: ${expectedValueAfterStir} basiert auf hVal=${hVal})`);
console.log(`Tests: ${passed} bestanden, ${failed} fehlgeschlagen.`);
}
// --- Initialisierung ---
function initializeApp() {
loadSettings(); // Lädt Einstellungen und wendet sie an (inkl. initialem Setzen der Länge via Horizont)
runUnitTests();
}
document.addEventListener('DOMContentLoaded', initializeApp);
</script>
</body>
</html>