forked from invoke-ai/InvokeAI
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpassword_utils.py
More file actions
113 lines (84 loc) · 3.65 KB
/
password_utils.py
File metadata and controls
113 lines (84 loc) · 3.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""Password hashing and validation utilities."""
from typing import Literal, cast
from passlib.context import CryptContext
# Configure bcrypt context - set truncate_error=False to allow passwords >72 bytes
# without raising an error. They will be automatically truncated by bcrypt to 72 bytes.
pwd_context = CryptContext(
schemes=["bcrypt"],
deprecated="auto",
bcrypt__truncate_error=False,
)
def hash_password(password: str) -> str:
"""Hash a password using bcrypt.
bcrypt has a maximum password length of 72 bytes. Longer passwords
are automatically truncated to comply with this limit.
Args:
password: The plain text password to hash
Returns:
The hashed password
"""
# bcrypt has a 72 byte limit - encode and truncate if necessary
password_bytes = password.encode("utf-8")
if len(password_bytes) > 72:
# Truncate to 72 bytes and decode back, dropping incomplete UTF-8 sequences
password = password_bytes[:72].decode("utf-8", errors="ignore")
return cast(str, pwd_context.hash(password))
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against a hash.
bcrypt has a maximum password length of 72 bytes. Longer passwords
are automatically truncated to match hash_password behavior.
Args:
plain_password: The plain text password to verify
hashed_password: The hashed password to verify against
Returns:
True if the password matches the hash, False otherwise
"""
try:
# bcrypt has a 72 byte limit - encode and truncate if necessary to match hash_password
password_bytes = plain_password.encode("utf-8")
if len(password_bytes) > 72:
# Truncate to 72 bytes and decode back, dropping incomplete UTF-8 sequences
plain_password = password_bytes[:72].decode("utf-8", errors="ignore")
return cast(bool, pwd_context.verify(plain_password, hashed_password))
except Exception:
# Invalid hash format or other error - return False
return False
def validate_password_strength(password: str) -> tuple[bool, str]:
"""Validate password meets minimum security requirements.
Password requirements:
- At least 8 characters long
- Contains at least one uppercase letter
- Contains at least one lowercase letter
- Contains at least one digit
Args:
password: The password to validate
Returns:
A tuple of (is_valid, error_message). If valid, error_message is empty.
"""
if len(password) < 8:
return False, "Password must be at least 8 characters long"
has_upper = any(c.isupper() for c in password)
has_lower = any(c.islower() for c in password)
has_digit = any(c.isdigit() for c in password)
if not (has_upper and has_lower and has_digit):
return False, "Password must contain uppercase, lowercase, and numbers"
return True, ""
def get_password_strength(password: str) -> Literal["weak", "moderate", "strong"]:
"""Determine the strength of a password.
Strength levels:
- weak: less than 8 characters
- moderate: 8+ characters but missing at least one of uppercase, lowercase, or digit
- strong: 8+ characters with uppercase, lowercase, and digit
Args:
password: The password to evaluate
Returns:
One of "weak", "moderate", or "strong"
"""
if len(password) < 8:
return "weak"
has_upper = any(c.isupper() for c in password)
has_lower = any(c.islower() for c in password)
has_digit = any(c.isdigit() for c in password)
if not (has_upper and has_lower and has_digit):
return "moderate"
return "strong"