Initial password gen commit
This commit is contained in:
committed by
GitHub
parent
edd35dc2c1
commit
ac7772c24f
+976
@@ -0,0 +1,976 @@
|
||||
<!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>
|
||||
|
||||
Reference in New Issue
Block a user