977 lines
54 KiB
HTML
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"> </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>
|
|
|