Skip to content

Commit b1032ef

Browse files
author
¨eadwinCode¨
committed
Added more encryption libraries
1 parent aad6e9e commit b1032ef

File tree

14 files changed

+955
-138
lines changed

14 files changed

+955
-138
lines changed

docs/security/csrf.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ class Development(BaseConfig):
2929
]
3030

3131
```
32+
33+
## **CORS**
34+
Cross-origin resource sharing (CORS) is a mechanism that allows resources to be requested from another domain.
35+
Under the hood, Ellar registers CORS Middleware and provides CORS options in application for CORS customization.
36+
See how to configure **CORS** [here](../overview/middleware.md#corsmiddleware)

ellar/core/security/__init__.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +0,0 @@
1-
from .hashers.base import (
2-
BasePasswordHasher,
3-
PBKDF2PasswordHasher,
4-
PBKDF2SHA1PasswordHasher,
5-
)
6-
7-
__all__ = [
8-
"BasePasswordHasher",
9-
"PBKDF2PasswordHasher",
10-
"PBKDF2SHA1PasswordHasher",
11-
]

ellar/core/security/constants.py

Whitespace-only changes.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import typing as t
2+
3+
from .argon2 import Argon2PasswordHasher
4+
from .base import BasePasswordHasher, EncodingType, get_random_string, must_update_salt
5+
from .bcrypt import BCryptPasswordHasher, BCryptSHA256PasswordHasher
6+
from .md5 import MD5PasswordHasher
7+
from .pbkdf import PBKDF2PasswordHasher, PBKDF2SHA1PasswordHasher
8+
from .scrypt import ScryptPasswordHasher
9+
10+
# This will never be a valid encoded hash
11+
_UNUSABLE_PASSWORD_PREFIX = "!"
12+
_UNUSABLE_PASSWORD_SUFFIX_LENGTH = (
13+
40 # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
14+
)
15+
__HASHERS_DICT: t.Dict[str, t.Type["BasePasswordHasher"]] = {}
16+
17+
18+
def add_hasher(*hashers: t.Type["BasePasswordHasher"]) -> None:
19+
for hasher in hashers:
20+
__HASHERS_DICT.update({hasher.algorithm: hasher})
21+
22+
23+
def get_hasher(algorithm: str = "pbkdf2_sha256") -> "BasePasswordHasher":
24+
try:
25+
hasher_type = __HASHERS_DICT[algorithm]
26+
return hasher_type()
27+
except KeyError as kex:
28+
raise ValueError(
29+
f"Unknown password hashing algorithm '{algorithm}'. "
30+
f"Please use `add_hasher` in `ellar.core.security.hashers` package to add implementation for '{algorithm}'"
31+
) from kex
32+
33+
34+
def identify_hasher(encoded: str) -> "BasePasswordHasher":
35+
possible_hashers = [v for k, v in __HASHERS_DICT.items() if v.identity(encoded)]
36+
if possible_hashers:
37+
return possible_hashers[0]()
38+
raise ValueError("Unable to identify Hasher")
39+
40+
41+
def is_password_usable(encoded: t.Optional[str]) -> bool:
42+
"""
43+
Return True if this password wasn't generated by
44+
User.set_unusable_password(), i.e. make_password(None).
45+
"""
46+
return encoded is None or not encoded.startswith(_UNUSABLE_PASSWORD_PREFIX)
47+
48+
49+
def make_password(
50+
password: t.Optional[EncodingType],
51+
algorithm: str = "pbkdf2_sha256",
52+
salt: t.Optional[str] = None,
53+
) -> str:
54+
"""
55+
Turn a plain-text password into a hash for database storage
56+
57+
Same as encode() but generate a new random salt. If password is None then
58+
return a concatenation of UNUSABLE_PASSWORD_PREFIX and a random string,
59+
which disallows logins. Additional random string reduces chances of gaining
60+
access to staff or superuser accounts. See ticket #20079 for more info.
61+
"""
62+
if password is None:
63+
return _UNUSABLE_PASSWORD_PREFIX + get_random_string(
64+
_UNUSABLE_PASSWORD_SUFFIX_LENGTH
65+
)
66+
if not isinstance(password, (bytes, str)):
67+
raise TypeError(
68+
"Password must be a string or bytes, got %s." % type(password).__qualname__
69+
)
70+
71+
hasher = get_hasher(algorithm)
72+
# Passlib includes salt in almost every hash
73+
return hasher.encode(password, salt=salt)
74+
75+
76+
def check_password(
77+
password: EncodingType,
78+
encoded: str,
79+
setter: t.Optional[t.Callable[..., t.Any]] = None,
80+
preferred_algorithm: str = "pbkdf2_sha256",
81+
) -> bool:
82+
"""
83+
Return a boolean of whether the raw password matches the three
84+
part encoded digest.
85+
86+
If setter is specified, it'll be called when you need to
87+
regenerate the password.
88+
"""
89+
90+
if password is None or not is_password_usable(encoded):
91+
return False
92+
93+
preferred = get_hasher(preferred_algorithm)
94+
try:
95+
hasher = identify_hasher(encoded)
96+
except ValueError:
97+
# encoded is gibberish or uses a hasher that's no longer installed.
98+
return False
99+
100+
hasher_changed = hasher.algorithm != preferred.algorithm
101+
must_update: bool = hasher_changed or preferred.must_update(encoded)
102+
is_correct: bool = hasher.verify(password, encoded)
103+
104+
if setter and is_correct and must_update:
105+
setter(password)
106+
return is_correct
107+
108+
109+
add_hasher(
110+
PBKDF2PasswordHasher,
111+
PBKDF2SHA1PasswordHasher,
112+
Argon2PasswordHasher,
113+
BCryptSHA256PasswordHasher,
114+
BCryptPasswordHasher,
115+
ScryptPasswordHasher,
116+
MD5PasswordHasher,
117+
)
118+
119+
__all__ = [
120+
"PBKDF2PasswordHasher",
121+
"PBKDF2SHA1PasswordHasher",
122+
"Argon2PasswordHasher",
123+
"BCryptSHA256PasswordHasher",
124+
"BCryptPasswordHasher",
125+
"ScryptPasswordHasher",
126+
"MD5PasswordHasher",
127+
"must_update_salt",
128+
"make_password",
129+
"check_password",
130+
"is_password_usable",
131+
"get_hasher",
132+
"identify_hasher",
133+
"add_hasher",
134+
]

ellar/core/security/hashers/argon2.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import typing as t
2+
3+
from passlib.hash import argon2
4+
5+
from .base import BasePasswordHasher, EncodingSalt, EncodingType, must_update_salt
6+
7+
8+
class Argon2PasswordHasher(BasePasswordHasher):
9+
"""
10+
Secure password hashing using the argon2 algorithm.
11+
12+
This is the winner of the Password Hashing Competition 2013-2015
13+
(https://password-hashing.net). It requires the argon2-cffi library which
14+
depends on native C code and might cause portability issues.
15+
"""
16+
17+
algorithm = "argon2"
18+
hasher = argon2
19+
20+
time_cost = 2
21+
memory_cost = argon2.memory_cost
22+
parallelism = argon2.parallelism
23+
24+
def _get_using_kwargs(self) -> dict:
25+
return {
26+
"time_cost": self.time_cost,
27+
"memory_cost": self.memory_cost,
28+
"parallelism": self.parallelism,
29+
}
30+
31+
def encode(
32+
self, password: EncodingType, salt: EncodingSalt = None
33+
) -> t.Union[str, t.Any]:
34+
salt = bytes(salt, "utf-8") if salt else salt # type:ignore[arg-type]
35+
return super().encode(password, salt)
36+
37+
def decode(self, encoded: str) -> dict:
38+
argon_2 = t.cast(t.Any, self.hasher.from_string(encoded))
39+
return {
40+
"algorithm": self.algorithm,
41+
"memory_cost": argon_2.memory_cost,
42+
"parallelism": argon_2.parallelism,
43+
"salt": argon_2.salt,
44+
"time_cost": argon_2.rounds,
45+
"hash": argon_2.data,
46+
}
47+
48+
def must_update(self, encoded: str) -> bool:
49+
decoded = self.decode(encoded)
50+
51+
update_salt = must_update_salt(decoded["salt"], self.salt_entropy)
52+
if update_salt:
53+
return update_salt
54+
55+
if decoded["time_cost"] != self.time_cost:
56+
return True
57+
return self.hasher.needs_update(encoded) # type:ignore[no-any-return]

0 commit comments

Comments
 (0)