pitangent's picture
Implement cryptographically secure password generator with glassmorphism UI
d5bf7fe verified
Raw
History Blame Contribute Delete
9.05 kB
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();
});