let generatedResults = [];
let myChart = null;
// --- CHARACTER SETS ---
const CHARS = {
LOWERCASE: 'abcdefghijklmnopqrstuvwxyz',
UPPERCASE: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
NUMBERS: '0123456789',
SPECIAL: '!@#$%^&*()_+-=[]{}|;:,.<>?/~`'
};
const CRACK_SPEED_PER_SECOND = 10000000000; // 10 billion guesses/sec (modern GPU array)
// --- UTILITIES ---
function getElement(id) {
return document.getElementById(id);
}
function smoothScrollTo(element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function calculateEntropy(length, keyspace) {
if (keyspace <= 1 || length <= 0) return 0;
// Entropy formula: E = L * log2(R)
return length * (Math.log(keyspace) / Math.log(2));
}
function formatCrackTime(seconds) {
if (seconds === 0 || seconds === Infinity) return 'Instant';
const units = [
{ limit: 60, name: 'second' },
{ limit: 60, name: 'minute' },
{ limit: 24, name: 'hour' },
{ limit: 365.25 / 12, name: 'month' }, // Average month
{ limit: 100, name: 'year' },
{ limit: Infinity, name: 'century' }
];
let time = seconds;
let unitIndex = 0;
while (unitIndex < units.length - 1 && time >= units[unitIndex].limit) {
time /= units[unitIndex].limit;
unitIndex++;
}
const unitName = units[unitIndex].name;
const value = time < 1000 ? time.toFixed(1) : time.toExponential(2);
return `${value} ${unitName}${time > 1 && time < 1000 && unitName !== 'century' ? 's' : ''}`;
}
// --- CORE GENERATION LOGIC ---
function generatePassword(length, characterSet, minReqs, excludeChars) {
let password = '';
let availableChars = characterSet;
// 1. Apply Exclusions to the overall available character set
if (excludeChars) {
for (const char of excludeChars) {
availableChars = availableChars.split(char).join('');
}
}
if (availableChars.length === 0) {
throw new Error("Character set is empty after applying exclusions.");
}
const requiredChars = [];
const requiredCount = Object.keys(minReqs).length;
// 2. Enforce Minimum Requirements
if (minReqs.UPPERCASE) requiredChars.push(CHARS.UPPERCASE);
if (minReqs.LOWERCASE) requiredChars.push(CHARS.LOWERCASE);
if (minReqs.NUMBERS) requiredChars.push(CHARS.NUMBERS);
if (minReqs.SPECIAL) requiredChars.push(CHARS.SPECIAL);
// Ensure the total required length is not greater than the password length
if (requiredCount > length) {
throw new Error("Password length is too short to meet all inclusion requirements.");
}
// Add the mandatory characters first
for (let i = 0; i < requiredChars.length; i++) {
let reqSet = requiredChars[i];
// Apply exclusion to the requirement set before picking a character
let cleanReqSet = reqSet;
if (excludeChars) {
for (const char of excludeChars) {
cleanReqSet = cleanReqSet.split(char).join('');
}
}
if (cleanReqSet.length === 0) {
throw new Error(`Required character set (e.g., uppercase) is empty after exclusion.`);
}
const randomIndex = Math.floor(Math.random() * cleanReqSet.length);
password += cleanReqSet[randomIndex];
}
// 3. Fill the rest of the password length
const remainingLength = length - password.length;
for (let i = 0; i < remainingLength; i++) {
const randomIndex = Math.floor(Math.random() * availableChars.length);
password += availableChars[randomIndex];
}
// 4. Shuffle the password to ensure mandatory characters are not always at the start
const passwordArray = password.split('');
for (let i = passwordArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[passwordArray[i], passwordArray[j]] = [passwordArray[j], passwordArray[i]];
}
return passwordArray.join('');
}
// --- RENDERING ---
function drawChart(results) {
if (!results || results.length === 0) return;
// Determine Strength Categories based on Entropy (industry standard metrics)
const categories = {
'Very Weak (0-35 bits)': 0,
'Weak (36-59 bits)': 0,
'Moderate (60-79 bits)': 0,
'Strong (80-119 bits)': 0,
'Very Strong (120+ bits)': 0
};
let totalEntropy = 0;
results.forEach(res => {
const e = res.entropy;
totalEntropy += e;
if (e < 36) categories['Very Weak (0-35 bits)']++;
else if (e < 60) categories['Weak (36-59 bits)']++;
else if (e < 80) categories['Moderate (60-79 bits)']++;
else if (e < 120) categories['Strong (80-119 bits)']++;
else categories['Very Strong (120+ bits)']++;
});
// Update Summary Metrics
const avgEntropy = totalEntropy / results.length;
getElement('wp-passgen-avg-entropy').textContent = `${avgEntropy.toFixed(2)} bits`;
// Calculate average crack time
const avgKeyspace = Math.pow(2, avgEntropy);
const avgCrackSeconds = avgKeyspace / CRACK_SPEED_PER_SECOND;
getElement('wp-passgen-avg-cracktime').textContent = formatCrackTime(avgCrackSeconds);
const ctx = getElement('wp-passgen-strength-chart').getContext('2d');
if (myChart) myChart.destroy();
myChart = new Chart(ctx, {
type: 'bar', // Bar chart for clear strength distribution
data: {
labels: Object.keys(categories),
datasets: [{
label: 'Number of Passwords',
data: Object.values(categories),
backgroundColor: [
'rgba(220, 53, 69, 0.8)', // Very Weak (Red)
'rgba(255, 193, 7, 0.8)', // Weak (Yellow)
'rgba(23, 162, 184, 0.8)', // Moderate (Cyan)
'rgba(40, 167, 69, 0.8)', // Strong (Green)
'rgba(0, 123, 255, 0.8)' // Very Strong (Blue)
],
borderColor: [
'rgba(220, 53, 69, 1)',
'rgba(255, 193, 7, 1)',
'rgba(23, 162, 184, 1)',
'rgba(40, 167, 69, 1)',
'rgba(0, 123, 255, 1)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
title: {
display: true,
text: 'Generated Password Strength Distribution'
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Count'
}
},
x: {
display: true
}
}
}
});
}
function renderTable(results) {
const tbody = getElement('wp-passgen-table').querySelector('tbody');
tbody.innerHTML = '';
results.forEach((res, index) => {
const row = tbody.insertRow();
row.innerHTML = `
${index + 1} |
${res.password} |
${res.entropy.toFixed(2)} |
${res.crackTime} |
Copied!
|
`;
});
}
// --- MAIN CONTROLLER ---
function handleCalculation() {
const lengthInput = getElement('wp-passgen-length');
const countInput = getElement('wp-passgen-count');
const excludeInput = getElement('wp-passgen-exclude');
const msgArea = getElement('wp-passgen-message-area');
const resultsArea = getElement('wp-passgen-results-area');
msgArea.style.display = 'none';
try {
const length = parseInt(lengthInput.value) || 0;
const count = parseInt(countInput.value) || 0;
const exclude = excludeInput.value.trim();
if (length < 8 || length > 64) throw new Error("Length must be between 8 and 64 characters.");
if (count < 1 || count > 20) throw new Error("Number of passwords must be between 1 and 20.");
const minReqs = {
UPPERCASE: getElement('wp-passgen-req-uppercase').checked,
LOWERCASE: getElement('wp-passgen-req-lowercase').checked,
NUMBERS: getElement('wp-passgen-req-numbers').checked,
SPECIAL: getElement('wp-passgen-req-special').checked,
};
const requiredCount = Object.values(minReqs).filter(v => v).length;
if (requiredCount > length) {
throw new Error(`Length (${length}) is too short to meet the ${requiredCount} selected inclusion requirements.`);
}
// 1. Determine the overall available character set (Keyspace R)
let combinedChars = '';
if (minReqs.LOWERCASE) combinedChars += CHARS.LOWERCASE;
if (minReqs.UPPERCASE) combinedChars += CHARS.UPPERCASE;
if (minReqs.NUMBERS) combinedChars += CHARS.NUMBERS;
if (minReqs.SPECIAL) combinedChars += CHARS.SPECIAL;
// Remove exclusion characters from the combined set
let effectiveKeyspaceChars = combinedChars;
if (exclude) {
for (const char of exclude) {
effectiveKeyspaceChars = effectiveKeyspaceChars.split(char).join('');
}
}
if (effectiveKeyspaceChars.length === 0) {
throw new Error("No characters left to generate passwords. Check your requirements and exclusions.");
}
const effectiveKeyspace = effectiveKeyspaceChars.length;
getElement('wp-passgen-keyspace').textContent = effectiveKeyspace;
// 2. Generation Loop
generatedResults = [];
for (let i = 0; i < count; i++) {
const password = generatePassword(length, effectiveKeyspaceChars, minReqs, exclude);
const entropy = calculateEntropy(length, effectiveKeyspace);
const crackSeconds = Math.pow(2, entropy) / CRACK_SPEED_PER_SECOND;
generatedResults.push({
id: i + 1,
password: password,
entropy: entropy,
crackTime: formatCrackTime(crackSeconds)
});
}
// 3. Render Results & UX
renderTable(generatedResults);
drawChart(generatedResults);
resultsArea.style.display = 'flex';
getElement('wp-passgen-download').disabled = false;
getElement('wp-passgen-share').disabled = false;
// Smooth scroll to results
smoothScrollTo(resultsArea);
} catch (error) {
msgArea.textContent = `Error: ${error.message}`;
msgArea.style.display = 'block';
resultsArea.style.display = 'none';
getElement('wp-passgen-download').disabled = true;
getElement('wp-passgen-share').disabled = true;
console.error("Calculation Error:", error);
}
}
// --- EXPORT/SHARE LOGIC / CLIPBOARD ---
function showCopyFeedback(element, text) {
// Handles feedback for the main Download/Share buttons and the small table copy buttons.
const originalText = element.textContent;
element.textContent = text;
// If it's a small table copy button, also show the sibling feedback span
if (element.classList.contains('wp-passgen-action-btn')) {
const feedbackElement = element.nextElementSibling; // The feedback span is the next sibling
feedbackElement.textContent = 'Copied!';
feedbackElement.style.display = 'inline';
setTimeout(() => {
feedbackElement.style.display = 'none';
}, 1500);
}
// Revert the button text for all buttons
setTimeout(() => {
element.textContent = originalText;
}, 1500);
}
function copyToClipboard(text, element) {
// Use document.execCommand('copy') as a robust fallback for clipboard in iFrames
try {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed'; // Prevents scrolling to bottom
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showCopyFeedback(element, 'Copied!');
} catch (err) {
console.error('Fallback copy failed:', err);
getElement('wp-passgen-message-area').textContent = 'Failed to copy password. Clipboard access denied.';
getElement('wp-passgen-message-area').style.display = 'block';
}
}
function downloadCSV() {
if (generatedResults.length === 0) return;
const headers = ["#", "Password", "Entropy (bits)", "Time to Crack"];
const rows = generatedResults.map(r => [
r.id,
`"${r.password}"`, // Enclose password in quotes to handle special chars
r.entropy.toFixed(2),
r.crackTime.replace(/,/g, '') // Remove commas for clean CSV
]);
let csvContent = headers.join(",") + "\n" + rows.map(e => e.join(",")).join("\n");
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", "passwords_breakdown.csv");
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showCopyFeedback(getElement('wp-passgen-download'), 'Downloaded!');
}
function shareResults() {
const msgArea = getElement('wp-passgen-message-area');
if (navigator.share) {
let shareText = "Generated Password Summary:\n";
shareText += `Average Entropy: ${getElement('wp-passgen-avg-entropy').textContent}\n`;
shareText += `Keyspace: ${getElement('wp-passgen-keyspace').textContent}\n`;
shareText += `\nPasswords Generated:\n`;
shareText += generatedResults.slice(0, 5).map(r => `[${r.id}] ${r.password} (${r.entropy.toFixed(1)} bits)`).join('\n');
shareText += generatedResults.length > 5 ? `\n...and ${generatedResults.length - 5} more.` : '';
navigator.share({
title: 'Advanced Password Generation Results',
text: shareText
}).then(() => {
showCopyFeedback(getElement('wp-passgen-share'), 'Shared!');
}).catch((error) => {
msgArea.textContent = 'Share failed. Try copying manually.';
msgArea.style.display = 'block';
});
} else {
msgArea.textContent = 'Share feature not supported on this browser. Use the Download button instead.';
msgArea.style.display = 'block';
showCopyFeedback(getElement('wp-passgen-share'), 'Not Supported!');
}
}
// --- INITIALIZATION (Show Sample Results on First Load) ---
window.onload = function() {
console.log("Tool Initialized. Generating sample results...");
handleCalculation();
};