Sillo provides a comprehensive password hashing system powered by passlib, with support for multiple algorithms and automatic scheme detection.
Overview
Section titled “Overview”The hashing system provides:
- Multiple algorithms - bcrypt, argon2, scrypt, pbkdf2
- Automatic detection - Verify passwords regardless of which algorithm was used
- Optional dependencies - Install only the algorithms you need
- Best-practice defaults - Secure settings out of the box
- Password utilities - Validation, strength checking, comparison
- User integration - Seamless integration with
sillo.users.User
Installation
Section titled “Installation”Passlib is included as a core dependency. Install algorithm support as needed:
# Bcrypt (default, lightweight, widely adopted)uv add bcrypt
# Argon2 (most secure, memory-hard, recommended)uv add argon2-cffi
# Scrypt (memory-hard, GPU-resistant)uv add scrypt
# PBKDF2 (built-in to Python, no extra dependency needed)
# Install all algorithmsuv add "sillo[hashing-all]"Quick Start
Section titled “Quick Start”Basic Hashing and Verification
Section titled “Basic Hashing and Verification”The most common pattern:
from sillo.hashing import hash_password, verify_password
# Hash a passwordhashed = hash_password("my_secure_password")
# Verify a password (auto-detects algorithm)if verify_password("my_secure_password", hashed): print("Password is correct!")With Sillo User Model
Section titled “With Sillo User Model”from sillo.users import User
# Create user with passworduser = User(username="john", email="john@example.com")user.set_password("my_secure_password")await user.save()
# Verify passwordif user.check_password("my_secure_password"): print("Login successful!")Detailed API Reference
Section titled “Detailed API Reference”Core Hashing Functions
Section titled “Core Hashing Functions”hash_password(password: str, scheme: str = "bcrypt") -> str
Section titled “hash_password(password: str, scheme: str = "bcrypt") -> str”Hash a password using the specified algorithm.
Parameters:
password(str): The plaintext password to hashscheme(str, optional): Hashing algorithm to use. Default is “bcrypt”- Valid schemes:
"bcrypt","argon2","scrypt","pbkdf2_sha256","pbkdf2_sha512"
- Valid schemes:
Returns:
- Hashed password string with algorithm prefix, salt, and parameters encoded
Raises:
InvalidSchemeError: If the requested scheme is not installed
Examples:
from sillo.hashing import hash_password
# Use default (bcrypt)hashed = hash_password("password")
# Use argon2 (most secure)hashed = hash_password("password", scheme="argon2")
# Use scrypthashed = hash_password("password", scheme="scrypt")
# Use PBKDF2hashed = hash_password("password", scheme="pbkdf2_sha256")Hash Formats:
- Bcrypt:
$2b$12$R9h7cIPz0gi.URNNX3kh2O... - Argon2:
$argon2id$v=19$m=65536,t=2,p=4$... - Scrypt:
scrypt$base64salt$base64hash - PBKDF2:
pbkdf2$sha256$600000$base64salt$base64hash
verify_password(password: str, hashed: str) -> bool
Section titled “verify_password(password: str, hashed: str) -> bool”Verify a plaintext password against a hash. Automatically detects which algorithm was used.
Parameters:
password(str): The plaintext password to verifyhashed(str): The previously hashed password
Returns:
Trueif password matches,Falseotherwise- Returns
False(safe) if hash is invalid, rather than raising an exception
Examples:
from sillo.hashing import hash_password, verify_password
# Hash with bcryptbcrypt_hash = hash_password("password")
# Verify (auto-detects bcrypt)verify_password("password", bcrypt_hash) # True
# Hash with argon2argon2_hash = hash_password("password", scheme="argon2")
# Verify (auto-detects argon2)verify_password("password", argon2_hash) # True
# Both work even though they use different algorithms!verify_password("wrong", bcrypt_hash) # Falseverify_password("wrong", argon2_hash) # Falseneeds_update(hashed: str) -> bool
Section titled “needs_update(hashed: str) -> bool”Check if a hash should be regenerated according to passlib’s policy.
Parameters:
hashed(str): The previously hashed password
Returns:
Trueif the hash is using deprecated settings or schemes,Falseotherwise
Examples:
from sillo.hashing import needs_update
if needs_update(user.password_hash): # Hash is using old settings, should regenerate user.set_password(plaintext_password) await user.save()needs_rehash(hashed: str, rounds: int = 12) -> bool
Section titled “needs_rehash(hashed: str, rounds: int = 12) -> bool”Check if a hash should be regenerated with stronger settings.
Parameters:
hashed(str): The previously hashed passwordrounds(int): Minimum acceptable bcrypt rounds (default 12)
Returns:
Trueif hash should be regenerated,Falseotherwise
Details:
- For bcrypt hashes: Checks if the cost factor (rounds) is below the specified minimum
- For other schemes: Uses passlib’s
needs_update()logic - Returns
Truefor invalid hashes or no hash
Examples:
from sillo.hashing import needs_rehash, hash_password
# Weak hash with only 4 rounds (old, insecure)weak = hash_password("password") # By default uses 12 rounds
if needs_rehash(weak, rounds=12): # Need to rehash strong = hash_password(plaintext)Use in Login Flow:
@app.post("/login")async def login(request, response, username: str, password: str): user = await User.get_or_none(username=username)
if user and user.check_password(password): # Optionally rehash old passwords to new algorithm if needs_rehash(user.password_hash): user.set_password(password) await user.save()
return response.json({"token": generate_jwt_token(user)})
return response.json({"error": "Invalid credentials"}, status_code=401)set_default_scheme(scheme: str) -> None
Section titled “set_default_scheme(scheme: str) -> None”Set the default hashing algorithm for the application. All future hash_password() calls without an explicit scheme will use this.
Parameters:
scheme(str): The scheme name to use by default
Raises:
InvalidSchemeError: If the scheme is not installed
Examples:
from sillo.hashing import set_default_scheme, hash_password
# Set argon2 as defaultset_default_scheme("argon2")
# Now these use argon2hash1 = hash_password("password")hash2 = hash_password("password")
# Both start with $argon2...get_available_schemes_list() -> list[str]
Section titled “get_available_schemes_list() -> list[str]”Get the list of currently available hashing schemes.
Returns:
- List of scheme names (e.g.,
["bcrypt", "argon2", "pbkdf2_sha256"])
Examples:
from sillo.hashing import get_available_schemes_list
schemes = get_available_schemes_list()print(f"Available: {schemes}")
# Conditionally use better algorithmif "argon2" in schemes: set_default_scheme("argon2")Password Utilities
Section titled “Password Utilities”is_password_usable(encoded: str) -> bool
Section titled “is_password_usable(encoded: str) -> bool”Check if a password hash is usable (not marked as unusable/disabled).
Parameters:
encoded(str): The hashed password to check
Returns:
Trueif password is usable,Falseif it’s marked as unusable
Details:
- Unusable passwords start with
!(theUNUSABLE_PASSWORD_PREFIX) - Used for disabled accounts, unset passwords, etc.
Examples:
from sillo.hashing import is_password_usable, make_unusable_password
# Disable a user's passwordunusable = make_unusable_password()user.password_hash = unusableawait user.save()
# Later, check if password is usableif is_password_usable(user.password_hash): # Can log in with password passelse: # Account is disabled passmake_unusable_password() -> str
Section titled “make_unusable_password() -> str”Create an unusable/disabled password marker.
Returns:
- A string that will fail password verification
Details:
- Used for accounts that shouldn’t authenticate with a password
- E.g., disabled accounts, SSO-only accounts, invite-only flows
Examples:
from sillo.hashing import make_unusable_password
# Create an invited user without passworduser = User(username="john")user.password_hash = make_unusable_password()await user.save()
# Later, user sets passworduser.set_password("password") # Now usableawait user.save()validate_password(password: str, user=None, min_length: int = 8) -> list[str]
Section titled “validate_password(password: str, user=None, min_length: int = 8) -> list[str]”Validate password against common strength requirements.
Parameters:
password(str): The plaintext password to validateuser(optional): User object (unused, for API compatibility)min_length(int): Minimum password length (default 8)
Returns:
- List of error messages (empty list if valid)
Validation Rules:
- At least
min_lengthcharacters - At least one uppercase letter (A-Z)
- At least one lowercase letter (a-z)
- At least one digit (0-9)
- At least one special character (!@#$%^&*(),.?”:{}|<>)
Examples:
from sillo.hashing import validate_password
# Valid passworderrors = validate_password("MyPass123!")print(errors) # []
# Too shorterrors = validate_password("Pass1!")print(errors) # ["Password must be at least 8 characters."]
# Missing uppercaseerrors = validate_password("mypass123!")print(errors) # ["Password must contain at least one uppercase letter."]
# Use in registration@app.post("/register")async def register(request, response, password: str = Query(...)): errors = validate_password(password) if errors: return response.json({"errors": errors}, status_code=400)
user = User(username="john") user.set_password(password) await user.save() return response.json({"user_id": user.id}, status_code=201)password_strength(password: str) -> dict
Section titled “password_strength(password: str) -> dict”Analyze password strength and provide feedback.
Parameters:
password(str): The plaintext password to analyze
Returns:
- Dictionary with:
score(int): 0-6 pointsstrength(str): “weak”, “medium”, or “strong”feedback(list): Suggestions for improvement
Scoring:
- +2 points: At least 12 characters
- +1 point: 8-11 characters
- +1 point: Contains uppercase letters
- +1 point: Contains lowercase letters
- +1 point: Contains digits
- +1 point: Contains special characters
Strength Classification:
- 5-6 points: “strong”
- 3-4 points: “medium”
- 0-2 points: “weak”
Examples:
from sillo.hashing import password_strength
result = password_strength("MySecurePass123!@#")print(result)# {# "score": 6,# "strength": "strong",# "feedback": []# }
result = password_strength("weak")print(result)# {# "score": 1,# "strength": "weak",# "feedback": ["Too short", "Low character diversity"]# }
# Use for real-time feedback@app.post("/check-password-strength")async def check_strength(request, response, password: str = Query(...)): result = password_strength(password) return response.json(result)constant_time_compare(val1: str, val2: str) -> bool
Section titled “constant_time_compare(val1: str, val2: str) -> bool”Compare two values in constant time (timing-safe comparison).
Parameters:
val1(str): First value to compareval2(str): Second value to compare
Returns:
Trueif values match,Falseotherwise
Details:
- Uses Python’s
secrets.compare_digest()under the hood - Prevents timing attacks by comparing all characters, not stopping early
- Already used internally by
verify_password()
Examples:
from sillo.hashing import constant_time_compare
# Comparing tokens or secrets (not passwords - use verify_password for that)user_token = "abc123..."provided_token = "abc123..."
if constant_time_compare(user_token, provided_token): print("Token matches!")Constants
Section titled “Constants”UNUSABLE_PASSWORD_PREFIX
Section titled “UNUSABLE_PASSWORD_PREFIX”The prefix used to mark passwords as unusable.
from sillo.hashing import UNUSABLE_PASSWORD_PREFIX
# Default value: "!"# Unusable passwords start with this prefixUNUSABLE_PASSWORD_SUFFIX_LENGTH
Section titled “UNUSABLE_PASSWORD_SUFFIX_LENGTH”The length of the random suffix appended to the prefix.
from sillo.hashing import UNUSABLE_PASSWORD_SUFFIX_LENGTH
# Default value: 40Algorithm Comparison
Section titled “Algorithm Comparison”Bcrypt
Section titled “Bcrypt”Best For: General purpose, compatibility, wide adoption
hashed = hash_password("password", scheme="bcrypt")# $2b$12$R9h7cIPz0gi.URNNX3kh2O...Characteristics:
- Time-based security (intentionally slow)
- Limited to 72-character passwords
- Proven and battle-tested (since 2006)
- Default choice for compatibility
- ~100ms per hash with 12 rounds
Installation: uv add bcrypt
Pros:
- Widely adopted across Python ecosystem
- Battle-tested and proven secure
- Simple and reliable
- Good default choice
Cons:
- No memory hardness (can be parallelized on GPUs)
- Character length limitation (72 bytes)
- Slower than necessary for modern hardware
When to Use:
- Existing applications (backward compatible)
- Need maximum compatibility
- When unsure which to choose
Argon2 (Recommended)
Section titled “Argon2 (Recommended)”Best For: New applications, high security requirements
hashed = hash_password("password", scheme="argon2")# $argon2id$v=19$m=65536,t=2,p=4$...Characteristics:
- Memory-hard (resistant to GPU/ASIC attacks)
- Time-based security
- Won PHC (Password Hashing Competition) in 2015
- No character length limitations
- ~50ms per hash with default settings
Installation: uv add argon2-cffi
Pros:
- Most secure modern algorithm
- Resistant to GPU and ASIC attacks
- No character length limits
- Customizable time/memory/parallelism parameters
- Future-proof
Cons:
- Requires external library
- Uses more memory (~64MB default)
- Slightly slower than bcrypt
When to Use:
- New applications
- High-security requirements
- You want the best modern algorithm
- Willing to accept slight performance overhead
Scrypt
Section titled “Scrypt”Best For: Memory-hard requirements, GPU-resistant hashing
hashed = hash_password("password", scheme="scrypt")# scrypt$base64salt$base64hashCharacteristics:
- Memory-hard and GPU-resistant
- Time-based security
- Proven design (since 2009)
- Good performance/security tradeoff
- ~30ms per hash with default settings
Installation: uv add scrypt
Pros:
- GPU-resistant (memory-hard)
- Fast and efficient
- Proven security
- Good middle ground
Cons:
- Less widely adopted than bcrypt
- Requires external library
- Fewer implementation options
When to Use:
- Need GPU-resistance
- Want lighter memory usage than argon2
- Performance is a concern
PBKDF2
Section titled “PBKDF2”Best For: Lightweight applications, no extra dependencies
hashed = hash_password("password", scheme="pbkdf2_sha256")# pbkdf2$sha256$600000$base64salt$base64hashCharacteristics:
- Key derivation function (not designed for passwords, but usable)
- Time-based security (600,000 iterations)
- Built into Python (no external dependency)
- Fast (~5ms per hash)
- NIST-approved
Installation: None (built-in)
Pros:
- No external dependency
- Lightweight
- NIST approved
- Fast
- Simple implementation
Cons:
- Not memory-hard (can be parallelized)
- Iteration count must increase over time
- Not designed specifically for passwords
When to Use:
- Embedded systems with dependency constraints
- Lightweight applications
- No extra dependencies acceptable
- Performance critical
Quick Decision Matrix
Section titled “Quick Decision Matrix”| Requirement | Algorithm | Reason |
|---|---|---|
| New app | Argon2 | Most secure, future-proof |
| Best compatibility | Bcrypt | Industry standard |
| GPU-resistant | Scrypt | Memory-hard |
| No dependencies | PBKDF2 | Built-in |
| Not sure | Bcrypt | Safe default |
| Highest security | Argon2 | Resistant to all attacks |
| Performance priority | PBKDF2 | Fastest |
Usage Examples
Section titled “Usage Examples”User Registration
Section titled “User Registration”from sillo import silloApp, Queryfrom sillo.hashing import validate_passwordfrom sillo.users import User
app = silloApp()
@app.post("/register")async def register( request, response, username: str = Query(...), email: str = Query(...), password: str = Query(...),): """Register a new user."""
# Validate password strength errors = validate_password(password) if errors: return response.json( {"errors": errors}, status_code=400, )
# Check if user exists existing = await User.get_or_none(email=email) if existing: return response.json( {"error": "Email already registered"}, status_code=400, )
# Create user with hashed password user = User(username=username, email=email) user.set_password(password) # Hashes automatically await user.save()
return response.json( {"user_id": user.id, "username": username}, status_code=201, )User Login with Progressive Rehashing
Section titled “User Login with Progressive Rehashing”from sillo.hashing import needs_rehash, get_available_schemes_list, set_default_scheme
# Setup: use argon2 if availableif "argon2" in get_available_schemes_list(): set_default_scheme("argon2")
@app.post("/login")async def login( request, response, email: str = Query(...), password: str = Query(...),): """Login user and optionally rehash old passwords."""
user = await User.get_or_none(email=email)
# Check password if not user or not user.check_password(password): return response.json( {"error": "Invalid email or password"}, status_code=401, )
# Progressive rehashing: upgrade old hashes to new algorithm if needs_rehash(user.password_hash): user.set_password(password) # Rehashes with new algorithm await user.save()
# Generate JWT token token = generate_jwt_token(user) return response.json({"token": token})Change Password
Section titled “Change Password”@app.post("/change-password")async def change_password( request, response, old_password: str = Query(...), new_password: str = Query(...), user_id: str = Query(...), # From JWT): """Change user password."""
user = await User.get_or_none(id=user_id) if not user: return response.json({"error": "User not found"}, status_code=404)
# Verify old password if not user.check_password(old_password): return response.json( {"error": "Current password is incorrect"}, status_code=401, )
# Validate new password errors = validate_password(new_password) if errors: return response.json({"errors": errors}, status_code=400)
# Check it's different if old_password == new_password: return response.json( {"error": "New password must be different"}, status_code=400, )
# Update password user.set_password(new_password) await user.save()
return response.json({"message": "Password updated"})Password Reset
Section titled “Password Reset”from sillo.hashing import make_unusable_password
@app.post("/request-password-reset")async def request_reset(request, response, email: str = Query(...)): """Request password reset."""
user = await User.get_or_none(email=email) if not user: # Don't reveal if user exists return response.json({"message": "If email exists, reset link sent"})
# Generate reset token (JWT or other) reset_token = generate_reset_token(user)
# Save token (in cache or database) await cache.set(f"reset:{reset_token}", user.id, ttl=3600)
# Send email with reset link await send_password_reset_email(user.email, reset_token)
return response.json({"message": "Reset link sent to email"})
@app.post("/reset-password")async def reset_password( request, response, token: str = Query(...), new_password: str = Query(...),): """Complete password reset."""
# Verify token user_id = await cache.get(f"reset:{token}") if not user_id: return response.json( {"error": "Reset link expired or invalid"}, status_code=400, )
# Validate new password errors = validate_password(new_password) if errors: return response.json({"errors": errors}, status_code=400)
# Update password user = await User.get(id=user_id) user.set_password(new_password) await user.save()
# Invalidate token await cache.delete(f"reset:{token}")
return response.json({"message": "Password reset successfully"})Admin User Creation
Section titled “Admin User Creation”from sillo.users import UserManager
@app.post("/create-admin")async def create_admin( request, response, username: str = Query(...), email: str = Query(...), password: str = Query(...),): """Create admin user (validates password)."""
# Use UserManager for validation try: admin = await User.objects.create_superuser( username=username, email=email, password=password, ) return response.json({ "admin_id": admin.id, "username": admin.username, }, status_code=201) except ValueError as e: # Password validation failed return response.json({"error": str(e)}, status_code=400)Configuration
Section titled “Configuration”Setting Default Algorithm
Section titled “Setting Default Algorithm”from sillo.hashing import set_default_scheme
# In your app startupset_default_scheme("argon2")
# All subsequent hash_password() calls use argon2Checking Available Algorithms
Section titled “Checking Available Algorithms”from sillo.hashing import get_available_schemes_list
schemes = get_available_schemes_list()print(f"Installed: {schemes}")
# Use this to enable features conditionallyif "argon2" in schemes: use_argon2 = TrueAdvanced: Custom Password Strength Requirements
Section titled “Advanced: Custom Password Strength Requirements”from sillo.hashing import validate_password
def my_validate_password(password: str) -> list[str]: """Custom validation with stricter requirements."""
errors = []
# Use default validation errors.extend(validate_password(password, min_length=12))
# Add custom rules if password.count(password[0]) > len(password) * 0.3: errors.append("Password has too many repeated characters")
return errors
# Use in registration@app.post("/register")async def register(request, response, password: str = Query(...)): errors = my_validate_password(password) if errors: return response.json({"errors": errors}, status_code=400) # ... continue registrationBest Practices
Section titled “Best Practices”Security
Section titled “Security”- Always hash passwords - Never store plaintext
- Use argon2 for new apps - Most secure modern algorithm
- Verify on login - Don’t skip the verification step
- Use HTTPS - Always encrypt in transit
- Rate limit logins - Protect against brute-force
- Enforce complexity - Use
validate_password() - Progressive rehashing - Upgrade old hashes on login
- Keep algorithms updated - Install new packages as they’re released
Performance
Section titled “Performance”- Hash only passwords - Don’t hash other data
- Cache user lookups - Don’t query DB per hash check
- Rehash asynchronously - Don’t make login slower
- Use defaults - They’re tuned for balance
- Monitor login times - Alert if times increase unexpectedly
Maintenance
Section titled “Maintenance”- Test password reset - Ensure the flow works
- Log auth failures - For security monitoring
- Document requirements - Tell users password rules
- Plan migrations - How to upgrade algorithms
- Backup user data - In case of emergency reset
Q: Which algorithm should I use? A: Use argon2 for new applications. Use bcrypt if you need maximum compatibility.
Q: Can I change algorithms?
A: Yes! Use needs_rehash() to detect old hashes and upgrade them on login.
Q: What about password length limits? A: Bcrypt has a 72-byte limit. Argon2 and scrypt don’t. PBKDF2 doesn’t.
Q: Is PBKDF2 secure? A: Yes, but argon2 and scrypt are better due to memory hardness.
Q: Why does argon2 take so long? A: Intentionally slow to resist brute-force attacks. This is a feature.
Q: Can I use multiple algorithms?
A: Yes! verify_password() auto-detects. Mix algorithms freely.
Q: How do I handle older hashes?
A: Use progressive rehashing: detect with needs_rehash(), rehash on next login.
Q: What if a user forgets their password?
A: Use make_unusable_password() temporarily, send reset link, have them set new password.
Q: Is timing attack protection important?
A: Yes, use constant_time_compare() for secrets. Already used in verify_password().
Q: How often should I rehash? A: Gradually as users log in. Don’t force immediate rehashing.
Migration Guide
Section titled “Migration Guide”From Other Frameworks
Section titled “From Other Frameworks”Django:
# Old Django codefrom django.contrib.auth.hashers import make_password, check_password
# New Sillo codefrom sillo.hashing import hash_password, verify_password
# Compatible API!hashed = hash_password("password")verify_password("password", hashed)Flask-Security:
# Old Flask-Security codefrom flask_security import hash_password, verify_password
# New Sillo codefrom sillo.hashing import hash_password, verify_password
# Same names and behavior!From Bcrypt Only
Section titled “From Bcrypt Only”# Old code (bcrypt only)import bcrypt
hash = bcrypt.hashpw(b"password", bcrypt.gensalt())
# New code (multi-algorithm)from sillo.hashing import hash_password, verify_password
hash = hash_password("password") # Still bcrypt by default
# Opt in to argon2 when readyset_default_scheme("argon2")hash = hash_password("password") # Now argon2Troubleshooting
Section titled “Troubleshooting”Problem: InvalidSchemeError: Scheme 'argon2' is not available
Solution: Install argon2-cffi
uv add argon2-cffiProblem: Password verification always fails
Check:
- Is
verify_password()being used? (notcheck_password()on wrong hash) - Is the hash valid? (not corrupted or truncated)
- Are there leading/trailing whitespace characters?
# Debugfrom sillo.hashing import verify_password
print(f"Hash: '{user.password_hash}'")print(f"Length: {len(user.password_hash)}")print(f"Verifies: {verify_password('password', user.password_hash)}")Problem: Login is slow
Causes:
- Hash computation is slow by design (use case: password is correct, need to verify)
- Database query is slow (add indexing)
- Rehashing on every login (use
needs_rehash()to avoid unnecessary work)
Problem: verify_password() returns False but password is correct
Check:
- Is there a typo in the password?
- Did you copy the hash correctly (no truncation)?
- Is the hash corrupted (invalid characters)?
- Did the hash algorithm change (e.g., plaintext stored)?
See Also
Section titled “See Also”- User Authentication - Complete auth guide
- User Model - sillo.users documentation
- Permissions - Access control
- Sessions - Session management
- Security - Security best practices