NIST Password Guidelines

NIST Password Guidelines

The National Institute of Standards and Technology (NIST) revolutionized password security guidance with Special Publication 800-63B, abandoning outdated complexity requirements in favor of evidence-based recommendations. These guidelines, while not legally binding for private organizations, represent best practices that many regulations reference and courts consider when evaluating reasonable security measures.

NIST's key recommendations include minimum 8-character passwords (with longer requirements for higher security), checking against compromised password lists, removing arbitrary complexity rules, eliminating mandatory password rotation, and supporting all printable ASCII and Unicode characters. These guidelines acknowledge that user-hostile requirements often decrease security by encouraging poor password choices or insecure workarounds.

import requests
import hashlib
from typing import Set, Tuple, Optional
import sqlite3
from bloom_filter2 import BloomFilter

class NISTCompliantPasswordChecker:
    """NIST SP 800-63B compliant password checking"""
    
    def __init__(self, min_length: int = 8, 
                 check_breach_list: bool = True,
                 allow_common_passwords: bool = False):
        self.min_length = min_length
        self.check_breach_list = check_breach_list
        self.allow_common_passwords = allow_common_passwords
        
        # Initialize breach checking
        if self.check_breach_list:
            self.breach_checker = BreachedPasswordChecker()
        
        # Load common passwords list
        if not self.allow_common_passwords:
            self.common_passwords = self._load_common_passwords()
    
    def check_password(self, password: str, 
                      username: Optional[str] = None) -> Tuple[bool, List[str]]:
        """Check password against NIST guidelines"""
        
        issues = []
        
        # NIST: Minimum length requirement (8 chars for most uses)
        if len(password) < self.min_length:
            issues.append(f"Password must be at least {self.min_length} characters")
        
        # NIST: Maximum length at least 64 characters
        if len(password) > 256:  # Generous maximum
            issues.append("Password exceeds maximum length")
        
        # NIST: Support all printable ASCII and Unicode
        # (No character restrictions)
        
        # NIST: Check against breach lists
        if self.check_breach_list:
            if self.breach_checker.is_breached(password):
                issues.append("Password found in breach database")
        
        # NIST: Check against common passwords
        if not self.allow_common_passwords:
            if password.lower() in self.common_passwords:
                issues.append("Password is too common")
        
        # NIST: Context-specific checks
        if username:
            if username.lower() in password.lower():
                issues.append("Password contains username")
        
        # NIST: No composition rules required
        # (No complexity requirements like special chars, etc.)
        
        return len(issues) == 0, issues
    
    def _load_common_passwords(self) -> Set[str]:
        """Load list of common passwords"""
        
        # In production, load from comprehensive list
        # This is a minimal example
        common = {
            'password', '123456', 'password123', 'admin', 'letmein',
            'welcome', 'monkey', '1234567890', 'qwerty', 'abc123',
            'Password1', 'password1', '123456789', 'welcome123',
            '12345678', '12345', '1234567', 'sunshine', 'iloveyou'
        }
        
        return {pwd.lower() for pwd in common}

class BreachedPasswordChecker:
    """Check passwords against breach databases"""
    
    def __init__(self, use_bloom_filter: bool = True):
        self.use_bloom_filter = use_bloom_filter
        
        if use_bloom_filter:
            # Bloom filter for memory-efficient checking
            # In production, pre-populate with breach data
            self.bloom = BloomFilter(
                max_elements=1000000000,  # 1 billion passwords
                error_rate=0.001          # 0.1% false positive rate
            )
            self._populate_bloom_filter()
    
    def is_breached(self, password: str) -> bool:
        """Check if password appears in breach databases"""
        
        # Option 1: Check against local bloom filter
        if self.use_bloom_filter:
            return password in self.bloom
        
        # Option 2: Use Have I Been Pwned API (k-anonymity)
        return self._check_hibp_api(password)
    
    def _check_hibp_api(self, password: str) -> bool:
        """Check password against Have I Been Pwned API"""
        
        # Hash password with SHA-1 (HIBP requirement)
        sha1_hash = hashlib.sha1(password.encode()).hexdigest().upper()
        prefix = sha1_hash[:5]
        suffix = sha1_hash[5:]
        
        try:
            # Query API with k-anonymity
            response = requests.get(
                f'https://api.pwnedpasswords.com/range/{prefix}',
                headers={'User-Agent': 'NIST-Compliant-Checker'}
            )
            
            if response.status_code == 200:
                # Check if our suffix appears in results
                hashes = response.text.splitlines()
                for line in hashes:
                    hash_suffix, count = line.split(':')
                    if hash_suffix == suffix:
                        return True
            
            return False
            
        except Exception as e:
            # Log error but don't block password creation
            logging.error(f"HIBP API check failed: {e}")
            return False
    
    def _populate_bloom_filter(self):
        """Populate bloom filter with known breached passwords"""
        
        # In production, load from breach databases
        # This is a demonstration
        breached_samples = [
            'password', '123456', 'password123', 'admin', 'letmein'
        ]
        
        for pwd in breached_samples:
            self.bloom.add(pwd)

class ModernPasswordPolicy:
    """NIST SP 800-63B based password policy"""
    
    def __init__(self):
        self.checker = NISTCompliantPasswordChecker()
        
    def generate_policy_text(self) -> str:
        """Generate user-friendly policy based on NIST guidelines"""
        
        return """
Password Requirements:
• Minimum 8 characters (12+ recommended)
• Maximum 256 characters
• All characters allowed (letters, numbers, symbols, spaces)
• No specific character types required
• Cannot be a commonly used password
• Cannot contain your username
• Cannot be a previously breached password

Tips for Strong Passwords:
• Use a passphrase: "coffee sunrise mountain bicycle"
• Make it memorable but unique to you
• Consider using a password manager
• Longer is stronger than complex

What We DON'T Require:
✗ Special characters (though you can use them)
✗ Regular password changes (unless compromised)
✗ Security questions
✗ Password hints
"""
    
    def validate_password_change(self, old_password: str, 
                               new_password: str) -> Tuple[bool, List[str]]:
        """Validate password change per NIST guidelines"""
        
        issues = []
        
        # Check basic requirements
        valid, basic_issues = self.checker.check_password(new_password)
        issues.extend(basic_issues)
        
        # NIST: Don't require regular rotation
        # Only check if passwords are different
        if old_password == new_password:
            issues.append("New password must be different from current password")
        
        # NIST: No arbitrary lifetime restrictions
        # Password changes should be event-driven (breach, compromise, etc.)
        
        return len(issues) == 0, issues

NIST guidelines emphasize usability alongside security. Arbitrary complexity requirements and forced rotation often lead to predictable patterns (Password1!, Password2!, etc.) that decrease security. Instead, length provides the primary defense against brute force attacks, while breach checking prevents known-compromised passwords. This approach acknowledges that security measures must work with human behavior, not against it.