I got locked out of the GST portal for the third time because my password "didn't meet requirements."
No explanation. Just "Invalid password."
After wasting 30 minutes trying different passwords, I decided to build a tool that would tell me exactly what was wrong with my password for different government portals.
Result: PasswordChecker.in - Built in 2 weekends with vanilla JavaScript. No React. No Vue. No frameworks.
Here's how I did it.
Why No Frameworks?
Speed. I wanted the tool to be:
- Fast to load (< 50KB total)
- Fast to build (2 weekends)
- Fast to run (real-time password checking)
Adding React would add 130KB+ before I wrote a single line of code. For a single-page tool, that's overkill.
The Tech Stack
- Frontend: Vanilla JavaScript + Tailwind CSS
- Styling: Tailwind CDN (for rapid prototyping)
- API: Have I Been Pwned (k-Anonymity model)
- Hosting: Vercel
- Source Control: GitHub
Total bundle size: ~48KB (including Tailwind)
Feature #1: Real-Time Password Strength Checker
The Requirements
Different Indian government portals have different password requirements:
const portalRequirements = {
uidai: {
minLength: 8,
requiresUppercase: true,
requiresLowercase: true,
requiresNumber: true,
requiresSpecial: true,
},
gstn: {
minLength: 10,
requiresUppercase: true,
requiresLowercase: true,
requiresNumber: true,
requiresSpecial: true,
maxLength: 15,
},
incomeTax: {
minLength: 12,
requiresUppercase: true,
requiresLowercase: true,
requiresNumber: true,
requiresSpecial: true,
maxLength: 14,
},
};
The Core Logic
function checkPasswordStrength(password) {
const checks = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password),
special: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password),
};
// Calculate score (0-100)
let score = 0;
if (checks.length) score += 20;
if (checks.uppercase) score += 20;
if (checks.lowercase) score += 20;
if (checks.number) score += 20;
if (checks.special) score += 20;
// Additional points for length
if (password.length >= 12) score += 10;
if (password.length >= 16) score += 10;
return {
score: Math.min(score, 100),
checks,
strength: getStrengthLabel(score),
};
}
function getStrengthLabel(score) {
if (score >= 80) return 'Very Strong';
if (score >= 60) return 'Strong';
if (score >= 40) return 'Moderate';
if (score >= 20) return 'Weak';
return 'Very Weak';
}
Real-Time Updates
const passwordInput = document.getElementById('password');
const strengthMeter = document.getElementById('strength-meter');
passwordInput.addEventListener('input', (e) => {
const password = e.target.value;
const result = checkPasswordStrength(password);
// Update UI
updateStrengthMeter(result);
updateChecklist(result.checks);
checkPortalCompliance(password);
});
Feature #2: Password Generator
Users needed passwords that were:
Strong (meets requirements)
Memorable (not random gibberish)
Portal-specific (works for UIDAI, GST, etc.)
The Memorable Password Algorithm
Instead of generating xK9!mP2@, generate Quick7Tiger$42
function generateReadablePassword(requirements) {
const adjectives = ['Quick', 'Brave', 'Smart', 'Cool', 'Swift'];
const nouns = ['Tiger', 'Eagle', 'Wolf', 'Lion', 'Hawk'];
const specials = ['@', '#', '$', '%', '&', '*'];
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
const number1 = Math.floor(Math.random() * 9) + 1;
const noun = nouns[Math.floor(Math.random() * nouns.length)];
const special = specials[Math.floor(Math.random() * specials.length)];
const number2 = Math.floor(Math.random() * 90) + 10;
let password = `${adjective}${number1}${noun}${special}${number2}`;
// Adjust for portal requirements
if (requirements.minLength > password.length) {
password += Math.random().toString(36).substring(2, requirements.minLength - password.length + 2);
}
return password;
}
Output: Passwords like Smart5Wolf@73 - easy to remember, meets all requirements.
Feature #3: Data Breach Checker
This was the most complex feature. I needed to check if a password appeared in data breaches without sending the password to any server.
The k-Anonymity Model
Have I Been Pwned (HIBP) uses k-Anonymity:
- Hash the password (SHA-1)
- Send only the first 5 characters of the hash
- Server returns all breached passwords matching those 5 chars
- Client checks if full hash is in the list
async function checkPasswordBreach(password) {
// Hash password
const hash = await sha1(password);
const prefix = hash.substring(0, 5);
const suffix = hash.substring(5).toUpperCase();
// Query HIBP API (only send first 5 chars)
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const data = await response.text();
// Check if full hash is in response
const hashes = data.split('\n');
for (let line of hashes) {
const [hashSuffix, count] = line.split(':');
if (hashSuffix === suffix) {
return {
found: true,
count: parseInt(count),
};
}
}
return { found: false };
}
// SHA-1 implementation
async function sha1(str) {
const buffer = new TextEncoder().encode(str);
const hash = await crypto.subtle.digest('SHA-1', buffer);
const hashArray = Array.from(new Uint8Array(hash));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
Privacy: Your password never leaves your browser. Only the first 5 characters of its hash are sent.
Feature #4: Multi-Portal Compatibility Test
Test one password against 10 portals simultaneously.
function testPortalCompatibility(password) {
const portals = [
{ name: 'UIDAI', requirements: portalRequirements.uidai },
{ name: 'GSTN', requirements: portalRequirements.gstn },
{ name: 'Income Tax', requirements: portalRequirements.incomeTax },
// ... more portals
];
return portals.map(portal => ({
name: portal.name,
compatible: checkRequirements(password, portal.requirements),
issues: getIssues(password, portal.requirements),
}));
}
function checkRequirements(password, requirements) {
if (password.length < requirements.minLength) return false;
if (requirements.maxLength && password.length > requirements.maxLength) return false;
if (requirements.requiresUppercase && !/[A-Z]/.test(password)) return false;
if (requirements.requiresLowercase && !/[a-z]/.test(password)) return false;
if (requirements.requiresNumber && !/[0-9]/.test(password)) return false;
if (requirements.requiresSpecial && !/[!@#$%^&*]/.test(password)) return false;
return true;
}
Deployment
Local Development:
# No build step!
python -m http.server 8000
# Visit localhost:8000
Production:
git push origin main
# Vercel auto-deploys
No webpack. No babel. No build process.
Performance Optimizations
- Debounced Input Don't check password on every keystroke:
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
const debouncedCheck = debounce((password) => {
checkPasswordStrength(password);
}, 300);
passwordInput.addEventListener('input', (e) => {
debouncedCheck(e.target.value);
});
- Lazy Load Breach Check Only check breaches when user clicks "Check for Breaches":
// Don't auto-check (expensive API call)
breachCheckButton.addEventListener('click', async () => {
showLoader();
const result = await checkPasswordBreach(password);
hideLoader();
displayResult(result);
});
- Cache API Responses
const breachCache = new Map();
async function checkPasswordBreach(password) {
const hash = await sha1(password);
const prefix = hash.substring(0, 5);
// Check cache first
if (breachCache.has(prefix)) {
return checkHash(breachCache.get(prefix), hash);
}
// Fetch and cache
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
const data = await response.text();
breachCache.set(prefix, data);
return checkHash(data, hash);
}
Lessons Learned
1. Vanilla JS is Fast Enough
For most tools, you don't need a framework. Vanilla JS is:
- Faster to load
- Easier to debug
- Fewer dependencies
- More portable
2. Real-Time Feedback is Critical
Users type fast. Your UI must keep up. Debouncing is your friend.
3. Privacy Matters
For a password tool, client-side only was non-negotiable. No backend. No database. No tracking.
Users can inspect the source and verify nothing is sent to any server (except the HIBP API with anonymized data).
4. Government Standards are Inconsistent
Every portal has different requirements. No standardization. This makes users' lives harder—and creates opportunities for tools like mine.
What's Next
Planned Features:
- Browser extension (auto-fill government portals)
- Bulk password checking (CSV upload)
- API for developers
- Dark mode
Tech Debt:
- Add proper testing (currently manually tested)
- Consider Web Components for reusability
- Add service worker for offline support
Try It Yourself
Live: passwordchecker.in
Source: (Will open-source if there's interest—comment below!)
Conclusion
You don't need React to build useful tools. Sometimes vanilla JavaScript is the right choice.
Key Takeaways:
- Keep it simple
- Client-side for privacy
- Real-time feedback
- Focus on UX
Built in 2 weekends. 500+ users in the first week. Zero frameworks.
Questions? Ask in the comments!
Top comments (0)