Skip to content

Commit 9a9b20b

Browse files
author
Ezeudoh Tochukwu
committed
Added some docs on hashing and encryption
1 parent 119ed74 commit 9a9b20b

File tree

10 files changed

+180
-63
lines changed

10 files changed

+180
-63
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# **Encryption and Hashing**
2+
3+
**Encryption** is the method of transforming data. This process changes the original information, referred to as plaintext,
4+
into an alternative form called ciphertext.
5+
The goal is to ensure that only authorized parties possess the capability to decrypt ciphertext back into plaintext and access the original information.
6+
Encryption doesn't inherently prevent interference but rather restricts the intelligible content from potential interceptors.
7+
Encryption is a bidirectional operation; what's encrypted can be decrypted using the correct key.
8+
9+
**Hashing** is the process of converting a given key into another value.
10+
A hash function is employed to create this new value following a mathematical algorithm.
11+
After hashing is applied, it should be practically impossible to reverse the process and derive the original
12+
input from the output.
13+
14+
## **Encryption**
15+
In Python, the [`cryptography`](https://pypi.org/project/cryptography/) library provides a user-friendly way to implement encryption.
16+
One common encryption scheme is Fernet, which offers symmetric key encryption.
17+
18+
For example,
19+
```python
20+
from cryptography.fernet import Fernet
21+
22+
# Generate a random encryption key
23+
key = Fernet.generate_key()
24+
25+
# Create a Fernet cipher object with the key
26+
cipher_suite = Fernet(key)
27+
28+
# Text to be encrypted
29+
plaintext = b"Hello, this is a secret message!"
30+
31+
# Encrypt the plaintext
32+
cipher_text = cipher_suite.encrypt(plaintext)
33+
34+
# Decrypt the ciphertext
35+
decrypted_text = cipher_suite.decrypt(cipher_text)
36+
37+
# Convert bytes to string for printing
38+
original_message = decrypted_text.decode("utf-8")
39+
40+
print("Original Message: ", plaintext)
41+
print("Encrypted Message: ", cipher_text)
42+
print("Decrypted Message: ", original_message)
43+
44+
```
45+
The provided Python example demonstrates this process, securing a message with encryption and then decrypting it using the same key.
46+
It's crucial to manage encryption keys securely in real applications to maintain the confidentiality and integrity of your data.
47+
48+
## **Hashing**
49+
For hashing, Ellar works with [passlib](https://pypi.org/project/passlib/) and [hashlib](https://docs.python.org/3/library/hashlib.html)
50+
to create a wrapper around some hashing algorithms listed below,
51+
52+
- **PBKDF2Hasher**: `pbkdf2_sha256` hashing algorithm wrapper
53+
- **PBKDF2SHA1Hasher**: `pbkdf2_sha1` hashing algorithm wrapper
54+
- **Argon2Hasher**: `argon2` hashing algorithm wrapper
55+
- **BCryptSHA256Hasher**: `bcrypt_sha256` hashing algorithm wrapper
56+
- **BCryptHasher**: `bcrypt` hashing algorithm wrapper
57+
- **ScryptHasher**: `scrypt` hashing algorithm wrapper
58+
- **MD5Hasher**: `md5` hashing algorithm wrapper
59+
60+
## **Password Hashing**
61+
Ellar provides two important utility functions: `make_password` for password hashing
62+
and `check_password` for password validation. Both of these functions are available in the
63+
`ellar.core.security.hashers` package.
64+
65+
```python
66+
def make_password(
67+
password: str|bytes,
68+
algorithm: str = "pbkdf2_sha256",
69+
salt: str|None = None,
70+
) -> str:
71+
pass
72+
73+
74+
def check_password(
75+
password: str|bytes,
76+
encoded: str,
77+
setter: Callable[..., Any]|None = None,
78+
preferred_algorithm: str = "pbkdf2_sha256",
79+
) -> bool:
80+
pass
81+
```
82+
83+
The `make_password` function takes plain text and generates a hashed result based on the provided hash algorithm.
84+
In the code snippet above, the default algorithm for `make_password` is `pbkdf2_sha256`.
85+
86+
The [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) algorithm with a SHA256 hash is a password stretching mechanism recommended by [NIST](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-132.pdf).
87+
This should be sufficient for most users as it is quite secure and requires massive amounts of computing time to break.
88+
89+
!!! note
90+
All hashing wrappers are registered as `key-value` pairs and can be accessed by the algorithm names
91+
using the get_hasher utility function in the `ellar.core.security.hashers` package.
92+
93+
For an example,
94+
```python
95+
from ellar.core.security.hashers import make_password
96+
97+
## Using pbkdf2_sha256 - PBKDF2Hasher
98+
password_hash = make_password('mypassword1234', algorithm="pbkdf2_sha256", salt='seasalt')
99+
print(password_hash)
100+
# pbkdf2_sha256$870000$seasalt$XE8bb8u57rxvyv2SThRFtMg9mzJLff2wjm3J8kGgFVI=
101+
102+
## Using bcrypt_sha256 - BCryptSHA256Hasher
103+
password_hash = make_password('mypassword1234', algorithm="bcrypt_sha256", salt='20AmWL1wKJZAHPiI1HEk4k')
104+
print(password_hash)
105+
# bcrypt_sha256$$2b$12$20AmWL1wKJZAHPiI1HEk4eZuAlMGHkK1rw4oou26bnwGmAE8F0JGK
106+
```
107+
108+
On the other hand, you can check or validate a password using the `check_password` function.
109+
110+
```python
111+
from ellar.core.security.hashers import check_password
112+
113+
hash_secret = "bcrypt_sha256$$2b$12$20AmWL1wKJZAHPiI1HEk4eZuAlMGHkK1rw4oou26bnwGmAE8F0JGK"
114+
assert check_password('mypassword1234', hash_secret) # True
115+
```

ellar/core/security/hashers/__init__.py

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
import typing as t
22

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
3+
from .argon2 import Argon2Hasher
4+
from .base import BaseHasher, EncodingType, get_random_string, must_update_salt
5+
from .bcrypt import BCryptHasher, BCryptSHA256Hasher
6+
from .md5 import MD5Hasher
7+
from .pbkdf import PBKDF2Hasher, PBKDF2SHA1Hasher
8+
from .scrypt import ScryptHasher
99

1010
# This will never be a valid encoded hash
1111
_UNUSABLE_PASSWORD_PREFIX = "!"
1212
_UNUSABLE_PASSWORD_SUFFIX_LENGTH = (
1313
40 # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
1414
)
15-
__HASHERS_DICT: t.Dict[str, t.Type["BasePasswordHasher"]] = {}
15+
__HASHERS_DICT: t.Dict[str, t.Type["BaseHasher"]] = {}
1616

1717

18-
def add_hasher(*hashers: t.Type["BasePasswordHasher"]) -> None:
18+
def add_hasher(*hashers: t.Type["BaseHasher"]) -> None:
1919
for hasher in hashers:
2020
__HASHERS_DICT.update({hasher.algorithm: hasher})
2121

2222

23-
def get_hasher(algorithm: str = "pbkdf2_sha256") -> "BasePasswordHasher":
23+
def get_hasher(algorithm: str = "pbkdf2_sha256") -> "BaseHasher":
2424
try:
2525
hasher_type = __HASHERS_DICT[algorithm]
2626
return hasher_type()
@@ -31,7 +31,7 @@ def get_hasher(algorithm: str = "pbkdf2_sha256") -> "BasePasswordHasher":
3131
) from kex
3232

3333

34-
def identify_hasher(encoded: str) -> "BasePasswordHasher":
34+
def identify_hasher(encoded: str) -> "BaseHasher":
3535
possible_hashers = [v for k, v in __HASHERS_DICT.items() if v.identity(encoded)]
3636
if possible_hashers:
3737
return possible_hashers[0]()
@@ -83,8 +83,8 @@ def check_password(
8383
Return a boolean of whether the raw password matches the three
8484
part encoded digest.
8585
86-
If setter is specified, it'll be called when you need to
87-
regenerate the password.
86+
If setter is specified, it'll be called if a password hash needs to be updated
87+
or regenerated based on the `preferred_algorithm`
8888
"""
8989

9090
if password is None or not is_password_usable(encoded):
@@ -107,23 +107,24 @@ def check_password(
107107

108108

109109
add_hasher(
110-
PBKDF2PasswordHasher,
111-
PBKDF2SHA1PasswordHasher,
112-
Argon2PasswordHasher,
113-
BCryptSHA256PasswordHasher,
114-
BCryptPasswordHasher,
115-
ScryptPasswordHasher,
116-
MD5PasswordHasher,
110+
PBKDF2Hasher,
111+
PBKDF2SHA1Hasher,
112+
Argon2Hasher,
113+
BCryptSHA256Hasher,
114+
BCryptHasher,
115+
ScryptHasher,
116+
MD5Hasher,
117117
)
118118

119119
__all__ = [
120-
"PBKDF2PasswordHasher",
121-
"PBKDF2SHA1PasswordHasher",
122-
"Argon2PasswordHasher",
123-
"BCryptSHA256PasswordHasher",
124-
"BCryptPasswordHasher",
125-
"ScryptPasswordHasher",
126-
"MD5PasswordHasher",
120+
"BaseHasher",
121+
"PBKDF2Hasher",
122+
"PBKDF2SHA1Hasher",
123+
"Argon2Hasher",
124+
"BCryptSHA256Hasher",
125+
"BCryptHasher",
126+
"ScryptHasher",
127+
"MD5Hasher",
127128
"must_update_salt",
128129
"make_password",
129130
"check_password",

ellar/core/security/hashers/argon2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
from passlib.hash import argon2
44

5-
from .base import BasePasswordHasher, EncodingSalt, EncodingType, must_update_salt
5+
from .base import BaseHasher, EncodingSalt, EncodingType, must_update_salt
66

77

8-
class Argon2PasswordHasher(BasePasswordHasher):
8+
class Argon2Hasher(BaseHasher):
99
"""
1010
Secure password hashing using the argon2 algorithm.
1111

ellar/core/security/hashers/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
RANDOM_STRING_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
1515

16-
__HASHERS_DICT: t.Dict[str, t.Type["BasePasswordHasher"]] = {}
16+
__HASHERS_DICT: t.Dict[str, t.Type["BaseHasher"]] = {}
1717
EncodingType = t.Union[str, bytes]
1818
EncodingSalt = t.Optional[t.Union[str, bytes]]
1919

@@ -39,7 +39,7 @@ def get_random_string(length: int, allowed_chars: str = RANDOM_STRING_CHARS) ->
3939
) # pragma no cover
4040

4141

42-
class BasePasswordHasher(ABC):
42+
class BaseHasher(ABC):
4343
hasher: t.Union[t.Type[uh.GenericHandler], t.Any]
4444
algorithm: str
4545
salt_entropy: int = 128

ellar/core/security/hashers/bcrypt.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from passlib.hash import django_bcrypt, django_bcrypt_sha256
22

3-
from .base import BasePasswordHasher
3+
from .base import BaseHasher
44

55

6-
class BCryptSHA256PasswordHasher(BasePasswordHasher):
6+
class BCryptSHA256Hasher(BaseHasher):
77
"""
88
Secure password hashing using the bcrypt algorithm (recommended)
99
@@ -38,7 +38,7 @@ def must_update(self, encoded: str) -> bool:
3838
return decoded["work_factor"] != self.rounds # type:ignore[no-any-return]
3939

4040

41-
class BCryptPasswordHasher(BCryptSHA256PasswordHasher):
41+
class BCryptHasher(BCryptSHA256Hasher):
4242
"""
4343
Secure password hashing using the bcrypt algorithm
4444

ellar/core/security/hashers/md5.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
from passlib.hash import md5_crypt
44

5-
from .base import BasePasswordHasher, EncodingSalt, EncodingType, must_update_salt
5+
from .base import BaseHasher, EncodingSalt, EncodingType, must_update_salt
66

77

8-
class MD5PasswordHasher(BasePasswordHasher):
8+
class MD5Hasher(BaseHasher):
99
"""
1010
The Salted MD5 password hashing algorithm (not recommended)
1111
"""

ellar/core/security/hashers/pbkdf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from passlib.hash import django_pbkdf2_sha1, django_pbkdf2_sha256
22

3-
from .base import BasePasswordHasher, must_update_salt
3+
from .base import BaseHasher, must_update_salt
44

55

6-
class PBKDF2PasswordHasher(BasePasswordHasher):
6+
class PBKDF2Hasher(BaseHasher):
77
"""
88
Handles PBKDF2 passwords
99
"""
@@ -31,7 +31,7 @@ def must_update(self, encoded: str) -> bool:
3131
return (decoded["iterations"] != self.iterations) or update_salt
3232

3333

34-
class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
34+
class PBKDF2SHA1Hasher(PBKDF2Hasher):
3535
"""
3636
Alternate PBKDF2 hasher which uses SHA1, the default PRF
3737
recommended by PKCS #5. This is compatible with other

ellar/core/security/hashers/scrypt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import secrets
44
import typing as t
55

6-
from .base import BasePasswordHasher, EncodingSalt, EncodingType
6+
from .base import BaseHasher, EncodingSalt, EncodingType
77

88

9-
class ScryptPasswordHasher(BasePasswordHasher):
9+
class ScryptHasher(BaseHasher):
1010
"""
1111
Secure password hashing using the Scrypt algorithm.
1212
"""

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ nav:
110110
- Security:
111111
- Authentication: security/authentication.md
112112
- Authorization: security/authorization.md
113+
- Encryption and Hashing: security/encryption_and_hashing.md
113114
- CSRF and CORS: security/csrf.md
114115
- Sessions: security/sessions.md
115116
- Rate Limiting: security/throttling.md

0 commit comments

Comments
 (0)