check-pwnedpasswords
Version:
A simple Node.js module that checks against the https://haveibeenpwned.com database through its https://api.pwnedpasswords.com API.
274 lines (239 loc) • 18.2 kB
HTML
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Check Pwned Passwords - Secure Password Checker</title>
<link rel="icon" type="image/png" href="favicon.png">
<meta name="description" content="A secure, client-side password checker using the k-Anonymity model from Have I Been Pwned.">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://jzhobes.github.io/check-pwnedpasswords/">
<meta property="og:title" content="Check Pwned Passwords - Secure Password Checker">
<meta property="og:description" content="A secure, client-side password checker using the k-Anonymity model from Have I Been Pwned.">
<meta property="og:image" content="social-preview.png">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://jzhobes.github.io/check-pwnedpasswords/">
<meta property="twitter:title" content="Check Pwned Passwords - Secure Password Checker">
<meta property="twitter:description" content="A secure, client-side password checker using the k-Anonymity model from Have I Been Pwned.">
<meta property="twitter:image" content="social-preview.png">
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
colors: {
// Matching the CSS variables from comparison.html
bg: {
body: { light: '#f8fafc', dark: '#0f172a' },
card: { light: '#ffffff', dark: '#1e293b' },
},
primary: {
DEFAULT: '#2563eb', // blue-600
dark: '#3b82f6', // blue-500
hover: '#1d4ed8', // blue-700
'dark-hover': '#60a5fa', // blue-400
}
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0', transform: 'translateY(4px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
}
}
}
}
}
</script>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body class="bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-50 min-h-screen flex flex-col font-sans antialiased selection:bg-blue-500 selection:text-white transition-colors duration-300">
<!-- Header -->
<header class="w-full bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 py-12 mb-12 transition-colors duration-300 relative">
<div class="max-w-4xl mx-auto px-6 text-center relative">
<!-- Theme Toggle -->
<button id="themeToggle" class="absolute top-0 right-6 -mt-4 p-2 rounded-full border border-slate-200 dark:border-slate-700 text-slate-500 hover:bg-slate-50 dark:hover:bg-slate-700 dark:text-slate-400 transition-all" aria-label="Toggle theme">
<!-- Sun Icon -->
<svg id="sunIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 hidden dark:block">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
<!-- Moon Icon -->
<svg id="moonIcon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 block dark:hidden">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
</svg>
</button>
<h1 class="text-4xl font-bold tracking-tight mb-4 text-slate-900 dark:text-white">Check Pwned Passwords</h1>
<p class="text-slate-500 dark:text-slate-400 text-lg mb-6">
A secure, client-side password checker using k-Anonymity.
</p>
<a href="https://github.com/jzhobes/check-pwnedpasswords" target="_blank" class="inline-flex items-center gap-2 bg-slate-800 hover:bg-slate-900 dark:bg-slate-100 dark:hover:bg-white text-white dark:text-slate-900 px-5 py-2.5 rounded-md font-medium text-sm transition-colors">
<svg height="20" width="20" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path>
</svg>
View on GitHub
</a>
</div>
</header>
<!-- Main Content -->
<main class="flex-1 w-full max-w-4xl mx-auto px-6 pb-16">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Checker Card -->
<div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-8 shadow-sm transition-colors duration-300">
<h2 class="text-xl font-semibold mb-6 flex items-center gap-2 pb-4 border-b border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-slate-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 01-1.043 3.296 3.745 3.745 0 01-3.296 1.043A3.745 3.745 0 0112 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 01-3.296-1.043 3.746 3.746 0 01-1.043-3.296A3.745 3.745 0 013 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 011.043-3.296 3.746 3.746 0 013.296-1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0112 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 013.296 1.043 3.746 3.746 0 011.043 3.296A3.745 3.745 0 0121 12z" />
</svg>
Password Check
</h2>
<form id="checkForm" class="space-y-5">
<div>
<label for="password" class="block text-sm font-medium text-slate-900 dark:text-slate-200 mb-2">Password</label>
<div class="relative">
<input type="password" id="password" placeholder="Enter password to check..." required autocomplete="off" class="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-md py-2.5 px-3 text-slate-900 dark:text-slate-100 placeholder-slate-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none transition-all duration-200 pr-10">
<button type="button" id="toggleBtn" aria-label="Toggle password visibility" class="absolute right-0 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors p-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
</div>
</div>
<button type="submit" id="submitBtn" class="w-full bg-primary hover:bg-primary-hover dark:bg-primary-dark dark:hover:bg-primary-dark-hover text-white font-medium py-3 rounded-md transition-colors duration-150 disabled:opacity-70 disabled:cursor-not-allowed flex items-center justify-center gap-2 text-[0.95rem]">
Check Password
</button>
</form>
<div id="result" class="hidden mt-6 animate-fade-in">
<!-- Results will appear here -->
</div>
</div>
<!-- Info Card -->
<div class="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-8 shadow-sm transition-colors duration-300">
<h2 class="text-xl font-semibold mb-6 flex items-center gap-2 pb-4 border-b border-slate-200 dark:border-slate-700 text-slate-900 dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-slate-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg>
How it Works
</h2>
<div class="space-y-4 text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
<p>
This tool uses the <strong>k-Anonymity</strong> model to securely check your password against the <a href="https://haveibeenpwned.com/" target="_blank" class="text-blue-600 dark:text-blue-400 hover:underline">Have I Been Pwned</a> database.
</p>
<ul class="list-disc pl-5 space-y-2">
<li>Your password is hashed locally using <strong>SHA-1</strong>.</li>
<li>Only the first <strong>5 characters</strong> of the hash are sent to the API.</li>
<li>The API returns a list of all breached hashes starting with those 5 characters.</li>
<li>The full comparison happens <strong>locally</strong> in your browser.</li>
</ul>
<p class="pt-2 font-medium text-slate-900 dark:text-slate-200">
Your full password and hash never leave this device.
</p>
</div>
<div class="mt-8 bg-slate-900 dark:bg-black rounded-md p-4 border border-slate-200 dark:border-slate-700 flex items-center justify-between font-mono text-sm text-slate-200">
<div class="flex gap-2">
<span class="text-slate-500">$</span>
<span>npm install</span>
<span class="text-blue-400">check-pwnedpasswords</span>
</div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="w-full bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700 py-10 transition-colors duration-300">
<div class="max-w-4xl mx-auto px-6 text-center text-sm text-slate-500 dark:text-slate-400">
<p>© 2025 check-pwnedpasswords. Licensed under MIT. Maintained by <a href="https://github.com/jzhobes" class="text-slate-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 font-medium transition-colors">John Ho</a>.</p>
</div>
</footer>
<script>
// SHA-1 implementation using Web Crypto API
async function sha1(str) {
const buffer = new TextEncoder().encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-1', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase();
}
const form = document.getElementById('checkForm');
const passwordInput = document.getElementById('password');
const toggleBtn = document.getElementById('toggleBtn');
const submitBtn = document.getElementById('submitBtn');
const resultDiv = document.getElementById('result');
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
// Theme Toggle Logic
themeToggle.addEventListener('click', () => {
html.classList.toggle('dark');
localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
});
// Initialize Theme
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
const eyeIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>`;
const eyeSlashIcon = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" /></svg>`;
// Toggle Password Visibility
toggleBtn.addEventListener('click', () => {
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
passwordInput.setAttribute('type', type);
toggleBtn.innerHTML = type === 'password' ? eyeIcon : eyeSlashIcon;
});
// Handle Form Submit
form.addEventListener('submit', async (e) => {
e.preventDefault();
const password = passwordInput.value;
if (!password) return;
// Reset UI
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="opacity-75">Checking...</span>';
resultDiv.classList.add('hidden');
resultDiv.className = 'hidden mt-6 animate-fade-in'; // Reset classes
try {
// 1. Hash the password locally
const hash = await sha1(password);
const prefix = hash.slice(0, 5);
const suffix = hash.slice(5);
// 2. Fetch from API (k-Anonymity)
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
if (!response.ok) throw new Error('API request failed');
const text = await response.text();
// 3. Check for match locally
const match = text.split(/\r?\n/).find(line => line.startsWith(suffix));
resultDiv.classList.remove('hidden');
if (match) {
const count = parseInt(match.split(':')[1], 10);
resultDiv.innerHTML = `
<div class="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-900/30 rounded-md text-center">
<p class="text-red-700 dark:text-red-400 font-semibold text-lg mb-1">Breach Detected</p>
<p class="text-slate-600 dark:text-slate-400 text-sm">Seen <span class="text-slate-900 dark:text-slate-200 font-mono font-medium">${count.toLocaleString()}</span> times.</p>
</div>
`;
} else {
resultDiv.innerHTML = `
<div class="p-4 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-900/30 rounded-md text-center">
<p class="text-emerald-700 dark:text-emerald-400 font-semibold text-lg mb-1">No Breach Found</p>
<p class="text-slate-600 dark:text-slate-400 text-sm">This password appears safe.</p>
</div>
`;
}
} catch (error) {
resultDiv.classList.remove('hidden');
resultDiv.innerHTML = `<p class="text-center text-red-600 dark:text-red-400 text-sm bg-red-50 dark:bg-red-900/20 p-3 rounded-md border border-red-200 dark:border-red-900/30">Error: ${error.message}</p>`;
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Check Password';
}
});
</script>
</body>
</html>