UNPKG

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
<!DOCTYPE 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>&copy; 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>