comparison libervia/backend/memory/crypto.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/memory/crypto.py@524856bd7b19
children 0d7bb4df2343
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3 # SAT: a jabber client
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
5 # Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from os import urandom
21 from base64 import b64encode, b64decode
22 from cryptography.hazmat.primitives import hashes
23 from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
24 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
25 from cryptography.hazmat.backends import default_backend
26
27
28 crypto_backend = default_backend()
29
30
31 class BlockCipher:
32
33 BLOCK_SIZE = 16
34 MAX_KEY_SIZE = 32
35 IV_SIZE = BLOCK_SIZE # initialization vector size, 16 bits
36
37 @staticmethod
38 def encrypt(key, text, leave_empty=True):
39 """Encrypt a message.
40
41 Based on http://stackoverflow.com/a/12525165
42
43 @param key (unicode): the encryption key
44 @param text (unicode): the text to encrypt
45 @param leave_empty (bool): if True, empty text will be returned "as is"
46 @return (D(str)): base-64 encoded encrypted message
47 """
48 if leave_empty and text == "":
49 return ""
50 iv = BlockCipher.get_random_key()
51 key = key.encode()
52 key = (
53 key[: BlockCipher.MAX_KEY_SIZE]
54 if len(key) >= BlockCipher.MAX_KEY_SIZE
55 else BlockCipher.pad(key)
56 )
57
58 cipher = Cipher(algorithms.AES(key), modes.CFB8(iv), backend=crypto_backend)
59 encryptor = cipher.encryptor()
60 encrypted = encryptor.update(BlockCipher.pad(text.encode())) + encryptor.finalize()
61 return b64encode(iv + encrypted).decode()
62
63 @staticmethod
64 def decrypt(key, ciphertext, leave_empty=True):
65 """Decrypt a message.
66
67 Based on http://stackoverflow.com/a/12525165
68
69 @param key (unicode): the decryption key
70 @param ciphertext (base-64 encoded str): the text to decrypt
71 @param leave_empty (bool): if True, empty ciphertext will be returned "as is"
72 @return: Deferred: str or None if the password could not be decrypted
73 """
74 if leave_empty and ciphertext == "":
75 return ""
76 ciphertext = b64decode(ciphertext)
77 iv, ciphertext = (
78 ciphertext[: BlockCipher.IV_SIZE],
79 ciphertext[BlockCipher.IV_SIZE :],
80 )
81 key = key.encode()
82 key = (
83 key[: BlockCipher.MAX_KEY_SIZE]
84 if len(key) >= BlockCipher.MAX_KEY_SIZE
85 else BlockCipher.pad(key)
86 )
87
88 cipher = Cipher(algorithms.AES(key), modes.CFB8(iv), backend=crypto_backend)
89 decryptor = cipher.decryptor()
90 decrypted = decryptor.update(ciphertext) + decryptor.finalize()
91 return BlockCipher.unpad(decrypted)
92
93 @staticmethod
94 def get_random_key(size=None, base64=False):
95 """Return a random key suitable for block cipher encryption.
96
97 Note: a good value for the key length is to make it as long as the block size.
98
99 @param size: key length in bytes, positive or null (default: BlockCipher.IV_SIZE)
100 @param base64: if True, encode the result to base-64
101 @return: str (eventually base-64 encoded)
102 """
103 if size is None or size < 0:
104 size = BlockCipher.IV_SIZE
105 key = urandom(size)
106 return b64encode(key) if base64 else key
107
108 @staticmethod
109 def pad(s):
110 """Method from http://stackoverflow.com/a/12525165"""
111 bs = BlockCipher.BLOCK_SIZE
112 return s + (bs - len(s) % bs) * (chr(bs - len(s) % bs)).encode()
113
114 @staticmethod
115 def unpad(s):
116 """Method from http://stackoverflow.com/a/12525165"""
117 s = s.decode()
118 return s[0 : -ord(s[-1])]
119
120
121 class PasswordHasher:
122
123 SALT_LEN = 16 # 128 bits
124
125 @staticmethod
126 def hash(password, salt=None, leave_empty=True):
127 """Hash a password.
128
129 @param password (str): the password to hash
130 @param salt (base-64 encoded str): if not None, use the given salt instead of a random value
131 @param leave_empty (bool): if True, empty password will be returned "as is"
132 @return: Deferred: base-64 encoded str
133 """
134 if leave_empty and password == "":
135 return ""
136 salt = (
137 b64decode(salt)[: PasswordHasher.SALT_LEN]
138 if salt
139 else urandom(PasswordHasher.SALT_LEN)
140 )
141
142 # we use PyCrypto's PBKDF2 arguments while porting to crytography, to stay
143 # compatible with existing installations. But this is temporary and we need
144 # to update them to more secure values.
145 kdf = PBKDF2HMAC(
146 # FIXME: SHA1() is not secure, it is used here for historical reasons
147 # and must be changed as soon as possible
148 algorithm=hashes.SHA1(),
149 length=16,
150 salt=salt,
151 iterations=1000,
152 backend=crypto_backend
153 )
154 key = kdf.derive(password.encode())
155 return b64encode(salt + key).decode()
156
157 @staticmethod
158 def verify(attempt, pwd_hash):
159 """Verify a password attempt.
160
161 @param attempt (str): the attempt to check
162 @param pwd_hash (str): the hash of the password
163 @return: Deferred: boolean
164 """
165 assert isinstance(attempt, str)
166 assert isinstance(pwd_hash, str)
167 leave_empty = pwd_hash == ""
168 attempt_hash = PasswordHasher.hash(attempt, pwd_hash, leave_empty)
169 assert isinstance(attempt_hash, str)
170 return attempt_hash == pwd_hash