Spaces:
Running
Running
| document.addEventListener("DOMContentLoaded", () => { | |
| // DOM Elements | |
| const passwordOutput = document.getElementById("password-output"); | |
| const btnRegenerate = document.getElementById("btn-regenerate"); | |
| const btnCopy = document.getElementById("btn-copy"); | |
| const copyTooltip = document.getElementById("copy-tooltip"); | |
| const lengthSlider = document.getElementById("length-slider"); | |
| const lengthVal = document.getElementById("length-val"); | |
| const chkUpper = document.getElementById("chk-upper"); | |
| const chkLower = document.getElementById("chk-lower"); | |
| const chkNumbers = document.getElementById("chk-numbers"); | |
| const chkSymbols = document.getElementById("chk-symbols"); | |
| const chkExcludeAmbiguous = document.getElementById("chk-exclude-ambiguous"); | |
| const entropyVal = document.getElementById("entropy-val"); | |
| const strengthLabel = document.getElementById("strength-label"); | |
| const strengthMeter = document.getElementById("strength-meter"); | |
| const crackTime = document.getElementById("crack-time"); | |
| // Cryptographically secure random integer generation [0, max - 1] | |
| function getRandomInt(max) { | |
| const array = new Uint32Array(1); | |
| const maxVal = 4294967296; // 2^32 | |
| const limit = maxVal - (maxVal % max); | |
| do { | |
| window.crypto.getRandomValues(array); | |
| } while (array[0] >= limit); | |
| return array[0] % max; | |
| } | |
| // Cryptographically secure Fisher-Yates shuffle | |
| function secureShuffle(array) { | |
| for (let i = array.length - 1; i > 0; i--) { | |
| const j = getRandomInt(i + 1); | |
| const temp = array[i]; | |
| array[i] = array[j]; | |
| array[j] = temp; | |
| } | |
| return array; | |
| } | |
| // Core Password Generation Logic | |
| function generatePassword(length, useUpper, useLower, useNumbers, useSymbols, excludeAmbiguous) { | |
| let upperPool = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |
| let lowerPool = "abcdefghijklmnopqrstuvwxyz"; | |
| let numberPool = "0123456789"; | |
| let symbolPool = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`"; | |
| if (excludeAmbiguous) { | |
| upperPool = upperPool.replace(/[IO]/g, ""); | |
| lowerPool = lowerPool.replace(/[lo]/g, ""); | |
| numberPool = numberPool.replace(/[01]/g, ""); | |
| symbolPool = symbolPool.replace(/[|]/g, ""); // Exclude vertical bar as it looks like I/l/1 | |
| } | |
| const pools = []; | |
| if (useUpper) pools.push(upperPool); | |
| if (useLower) pools.push(lowerPool); | |
| if (useNumbers) pools.push(numberPool); | |
| if (useSymbols) pools.push(symbolPool); | |
| if (pools.length === 0) return { password: "", poolSize: 0 }; | |
| const combinedPool = pools.join(""); | |
| const passwordChars = []; | |
| // 1. Guarantee at least one character from each selected class to satisfy constraints | |
| pools.forEach(pool => { | |
| const randIdx = getRandomInt(pool.length); | |
| passwordChars.push(pool[randIdx]); | |
| }); | |
| // 2. Fill the remaining spots up to target length | |
| while (passwordChars.length < length) { | |
| const randIdx = getRandomInt(combinedPool.length); | |
| passwordChars.push(combinedPool[randIdx]); | |
| } | |
| // 3. Shuffle array elements securely to remove sequential pattern | |
| secureShuffle(passwordChars); | |
| return { | |
| password: passwordChars.join(""), | |
| poolSize: combinedPool.length | |
| }; | |
| } | |
| // Determine Strength and Crack Time Estimates | |
| function getStrengthMetrics(entropy) { | |
| if (entropy === 0) { | |
| return { | |
| rating: 0, | |
| label: "Rating: -", | |
| crackTime: "Select at least one character class", | |
| colorClass: "" | |
| }; | |
| } | |
| if (entropy < 28) { | |
| return { | |
| rating: 0, | |
| label: "Rating: 0/5 (Very Weak)", | |
| crackTime: "Can be broken in seconds/minutes", | |
| colorClass: "strength-label-0" | |
| }; | |
| } else if (entropy < 40) { | |
| return { | |
| rating: 1, | |
| label: "Rating: 1/5 (Weak)", | |
| crackTime: "Can be broken in hours", | |
| colorClass: "strength-label-1" | |
| }; | |
| } else if (entropy < 60) { | |
| return { | |
| rating: 2, | |
| label: "Rating: 2/5 (Fair)", | |
| crackTime: "Can be broken in days or months", | |
| colorClass: "strength-label-2" | |
| }; | |
| } else if (entropy < 80) { | |
| return { | |
| rating: 3, | |
| label: "Rating: 3/5 (Good)", | |
| crackTime: "Can be broken in years", | |
| colorClass: "strength-label-3" | |
| }; | |
| } else if (entropy < 100) { | |
| return { | |
| rating: 4, | |
| label: "Rating: 4/5 (Strong)", | |
| crackTime: "Takes decades to break", | |
| colorClass: "strength-label-4" | |
| }; | |
| } else { | |
| return { | |
| rating: 5, | |
| label: "Rating: 5/5 (Centurial)", | |
| crackTime: "Takes centuries or millennia to break", | |
| colorClass: "strength-label-5" | |
| }; | |
| } | |
| } | |
| // Update UI Elements | |
| function updateApp() { | |
| const length = parseInt(lengthSlider.value, 10); | |
| lengthVal.textContent = length; | |
| const useUpper = chkUpper.checked; | |
| const useLower = chkLower.checked; | |
| const useNumbers = chkNumbers.checked; | |
| const useSymbols = chkSymbols.checked; | |
| const excludeAmbiguous = chkExcludeAmbiguous.checked; | |
| const result = generatePassword(length, useUpper, useLower, useNumbers, useSymbols, excludeAmbiguous); | |
| passwordOutput.value = result.password; | |
| // Entropy: L * log2(R) | |
| let entropy = 0; | |
| if (result.poolSize > 0 && result.password.length > 0) { | |
| entropy = result.password.length * Math.log2(result.poolSize); | |
| } | |
| entropyVal.textContent = entropy.toFixed(1); | |
| // Strength metrics | |
| const metrics = getStrengthMetrics(entropy); | |
| strengthLabel.textContent = metrics.label; | |
| strengthLabel.className = "metric-label " + metrics.colorClass; | |
| crackTime.textContent = metrics.crackTime; | |
| // Visual Strength Meter Bars Update | |
| const segments = strengthMeter.querySelectorAll(".meter-segment"); | |
| segments.forEach(segment => { | |
| segment.className = "meter-segment"; | |
| }); | |
| if (result.poolSize > 0) { | |
| const activeCount = Math.max(1, metrics.rating); // light up at least 1 bar if pool is selected | |
| for (let i = 0; i < activeCount; i++) { | |
| segments[i].classList.add(`active-${metrics.rating}`); | |
| } | |
| } | |
| } | |
| // Clipboard Copy Action | |
| function copyToClipboard(text) { | |
| if (navigator.clipboard && navigator.clipboard.writeText) { | |
| return navigator.clipboard.writeText(text); | |
| } else { | |
| const textArea = document.createElement("textarea"); | |
| textArea.value = text; | |
| textArea.style.position = "fixed"; | |
| document.body.appendChild(textArea); | |
| textArea.focus(); | |
| textArea.select(); | |
| try { | |
| document.execCommand("copy"); | |
| document.body.removeChild(textArea); | |
| return Promise.resolve(); | |
| } catch (err) { | |
| document.body.removeChild(textArea); | |
| return Promise.reject(err); | |
| } | |
| } | |
| } | |
| // Event Handlers | |
| lengthSlider.addEventListener("input", updateApp); | |
| [chkUpper, chkLower, chkNumbers, chkSymbols, chkExcludeAmbiguous].forEach(el => { | |
| el.addEventListener("change", updateApp); | |
| }); | |
| btnRegenerate.addEventListener("click", () => { | |
| // Trigger subtle spin animation on click | |
| const icon = btnRegenerate.querySelector(".icon-refresh"); | |
| icon.classList.add("spinning"); | |
| setTimeout(() => { | |
| icon.classList.remove("spinning"); | |
| }, 500); | |
| updateApp(); | |
| }); | |
| btnCopy.addEventListener("click", () => { | |
| if (!passwordOutput.value) return; | |
| copyToClipboard(passwordOutput.value) | |
| .then(() => { | |
| btnCopy.classList.add("copied"); | |
| copyTooltip.textContent = "Copied!"; | |
| setTimeout(() => { | |
| btnCopy.classList.remove("copied"); | |
| copyTooltip.textContent = "Copy"; | |
| }, 2000); | |
| }) | |
| .catch(err => { | |
| console.error("Copy failed:", err); | |
| copyTooltip.textContent = "Failed!"; | |
| setTimeout(() => { | |
| copyTooltip.textContent = "Copy"; | |
| }, 2000); | |
| }); | |
| }); | |
| // Initialize state | |
| updateApp(); | |
| }); | |