Mercurial > libervia-backend
comparison libervia/backend/core/patches.py @ 4237:a1e7e82a8921
core: implement SCRAM-SHA auth algorithm:
Twisted auth mechanism are outdated, and as a result, Libervia was not supporting the
mandatory SCRAM-SHA auth mechanism. This patch implements it for SCRAM-SHA-1,
SCRAM-SHA-256 and SCRAM-SHA-512 variants.
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 08 Apr 2024 12:29:40 +0200 |
parents | 4b842c1fb686 |
children | c14e904eee13 |
comparison
equal
deleted
inserted
replaced
4236:f59e9421a650 | 4237:a1e7e82a8921 |
---|---|
1 import base64 | |
1 import copy | 2 import copy |
2 from twisted.words.protocols.jabber import xmlstream, sasl, client as tclient, jid | 3 import secrets |
4 | |
5 from cryptography.hazmat.backends import default_backend | |
6 from cryptography.hazmat.primitives import hashes, hmac | |
7 from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC | |
8 from twisted.words.protocols.jabber import ( | |
9 client as tclient, | |
10 jid, | |
11 sasl, | |
12 sasl_mechanisms, | |
13 xmlstream, | |
14 ) | |
3 from wokkel import client | 15 from wokkel import client |
16 from zope.interface import implementer | |
17 | |
4 from libervia.backend.core.constants import Const as C | 18 from libervia.backend.core.constants import Const as C |
5 from libervia.backend.core.log import getLogger | 19 from libervia.backend.core.log import getLogger |
6 | 20 |
7 log = getLogger(__name__) | 21 log = getLogger(__name__) |
8 | 22 |
11 (until merged upstream). | 25 (until merged upstream). |
12 Second part add a trigger point to send and onElement method of XmlStream | 26 Second part add a trigger point to send and onElement method of XmlStream |
13 """ | 27 """ |
14 | 28 |
15 | 29 |
30 # SCRAM-SHA implementation | |
31 | |
32 | |
33 @implementer(sasl_mechanisms.ISASLMechanism) | |
34 class ScramSha: | |
35 """Implements the SCRAM-SHA SASL authentication mechanism. | |
36 | |
37 This mechanism is defined in RFC 5802. | |
38 """ | |
39 | |
40 ALLOWED_ALGORITHMS = ("SHA-1", "SHA-256", "SHA-512") | |
41 backend = default_backend() | |
42 | |
43 def __init__(self, username: str, password: str, algorithm: str) -> None: | |
44 """Initialize SCRAM-SHA mechanism with user credentials. | |
45 | |
46 @param username: The user's username. | |
47 @param password: The user's password. | |
48 """ | |
49 if algorithm not in self.ALLOWED_ALGORITHMS: | |
50 raise ValueError(f"Invalid algorithm: {algorithm!r}") | |
51 | |
52 self.username = username | |
53 self.password = password | |
54 self.algorithm = getattr(hashes, algorithm.replace("-", "", 1))() | |
55 self.name = f"SCRAM-{algorithm}" | |
56 self.client_nonce = base64.b64encode(secrets.token_bytes(24)).decode() | |
57 self.server_nonce = None | |
58 self.salted_password = None | |
59 | |
60 def digest(self, data: bytes) -> bytes: | |
61 hasher = hashes.Hash(self.algorithm) | |
62 hasher.update(data) | |
63 return hasher.finalize() | |
64 | |
65 def _hmac(self, key: bytes, msg: bytes) -> bytes: | |
66 """Compute HMAC-SHA""" | |
67 h = hmac.HMAC(key, self.algorithm, backend=self.backend) | |
68 h.update(msg) | |
69 return h.finalize() | |
70 | |
71 def _hi(self, password: str, salt: bytes, iterations: int) -> bytes: | |
72 kdf = PBKDF2HMAC( | |
73 algorithm=self.algorithm, | |
74 length=self.algorithm.digest_size, | |
75 salt=salt, | |
76 iterations=iterations, | |
77 backend=default_backend(), | |
78 ) | |
79 return kdf.derive(password.encode()) | |
80 | |
81 def getInitialResponse(self) -> bytes: | |
82 """Builds the initial client response message.""" | |
83 return f"n,,n={self.username},r={self.client_nonce}".encode() | |
84 | |
85 def getResponse(self, challenge: bytes) -> bytes: | |
86 """SCRAM-SHA authentication final step. Building proof of having the password. | |
87 | |
88 @param challenge: Challenge string from the server. | |
89 @return: Client proof. | |
90 """ | |
91 challenge_parts = dict(item.split("=") for item in challenge.decode().split(",")) | |
92 self.server_nonce = challenge_parts["r"] | |
93 salt = base64.b64decode(challenge_parts["s"]) | |
94 iterations = int(challenge_parts["i"]) | |
95 self.salted_password = self._hi(self.password, salt, iterations) | |
96 | |
97 client_key = self._hmac(self.salted_password, b"Client Key") | |
98 stored_key = self.digest(client_key) | |
99 auth_message = ( | |
100 f"n={self.username},r={self.client_nonce},{challenge.decode()},c=biws," | |
101 f"r={self.server_nonce}" | |
102 ).encode() | |
103 client_signature = self._hmac(stored_key, auth_message) | |
104 client_proof = bytes(a ^ b for a, b in zip(client_key, client_signature)) | |
105 client_final_message = ( | |
106 f"c=biws,r={self.server_nonce},p={base64.b64encode(client_proof).decode()}" | |
107 ) | |
108 return client_final_message.encode() | |
109 | |
110 | |
111 class SASLInitiatingInitializer(sasl.SASLInitiatingInitializer): | |
112 | |
113 def setMechanism(self): | |
114 """ | |
115 Select and setup authentication mechanism. | |
116 | |
117 Uses the authenticator's C{jid} and C{password} attribute for the | |
118 authentication credentials. If no supported SASL mechanisms are | |
119 advertized by the receiving party, a failing deferred is returned with | |
120 a L{SASLNoAcceptableMechanism} exception. | |
121 """ | |
122 | |
123 jid = self.xmlstream.authenticator.jid | |
124 password = self.xmlstream.authenticator.password | |
125 | |
126 mechanisms = sasl.get_mechanisms(self.xmlstream) | |
127 if jid.user is not None: | |
128 if "SCRAM-SHA-512" in mechanisms: | |
129 self.mechanism = ScramSha(jid.user, password, algorithm="SHA-512") | |
130 elif "SCRAM-SHA-256" in mechanisms: | |
131 self.mechanism = ScramSha(jid.user, password, algorithm="SHA-256") | |
132 elif "SCRAM-SHA-1" in mechanisms: | |
133 self.mechanism = ScramSha(jid.user, password, algorithm="SHA-1") | |
134 # FIXME: PLAIN should probably be disabled. | |
135 elif "PLAIN" in mechanisms: | |
136 self.mechanism = sasl_mechanisms.Plain(None, jid.user, password) | |
137 else: | |
138 raise sasl.SASLNoAcceptableMechanism() | |
139 else: | |
140 if "ANONYMOUS" in mechanisms: | |
141 self.mechanism = sasl_mechanisms.Anonymous() | |
142 else: | |
143 raise sasl.SASLNoAcceptableMechanism() | |
144 | |
145 | |
16 ## certificate validation patches | 146 ## certificate validation patches |
17 | 147 |
18 | 148 |
19 class XMPPClient(client.XMPPClient): | 149 class XMPPClient(client.XMPPClient): |
20 | 150 |
21 def __init__(self, jid, password, host=None, port=5222, | 151 def __init__( |
22 tls_required=True, configurationForTLS=None): | 152 self, |
153 jid, | |
154 password, | |
155 host=None, | |
156 port=5222, | |
157 tls_required=True, | |
158 configurationForTLS=None, | |
159 ): | |
23 self.jid = jid | 160 self.jid = jid |
24 self.domain = jid.host.encode('idna') | 161 self.domain = jid.host.encode("idna") |
25 self.host = host | 162 self.host = host |
26 self.port = port | 163 self.port = port |
27 | 164 |
28 factory = HybridClientFactory( | 165 factory = HybridClientFactory( |
29 jid, password, tls_required=tls_required, | 166 jid, |
30 configurationForTLS=configurationForTLS) | 167 password, |
168 tls_required=tls_required, | |
169 configurationForTLS=configurationForTLS, | |
170 ) | |
31 | 171 |
32 client.StreamManager.__init__(self, factory) | 172 client.StreamManager.__init__(self, factory) |
33 | 173 |
34 | 174 |
35 def HybridClientFactory(jid, password, tls_required=True, configurationForTLS=None): | 175 def HybridClientFactory(jid, password, tls_required=True, configurationForTLS=None): |
50 | 190 |
51 def associateWithStream(self, xs): | 191 def associateWithStream(self, xs): |
52 xmlstream.ConnectAuthenticator.associateWithStream(self, xs) | 192 xmlstream.ConnectAuthenticator.associateWithStream(self, xs) |
53 | 193 |
54 tlsInit = xmlstream.TLSInitiatingInitializer( | 194 tlsInit = xmlstream.TLSInitiatingInitializer( |
55 xs, required=self.tls_required, configurationForTLS=self.configurationForTLS) | 195 xs, required=self.tls_required, configurationForTLS=self.configurationForTLS |
56 xs.initializers = [client.client.CheckVersionInitializer(xs), | 196 ) |
57 tlsInit, | 197 xs.initializers = [ |
58 CheckAuthInitializer(xs, self.res_binding)] | 198 client.client.CheckVersionInitializer(xs), |
199 tlsInit, | |
200 CheckAuthInitializer(xs, self.res_binding), | |
201 ] | |
59 | 202 |
60 | 203 |
61 # XmlStream triggers | 204 # XmlStream triggers |
62 | 205 |
63 | 206 |
109 | 252 |
110 def initialize(self): | 253 def initialize(self): |
111 # XXX: modification of client.CheckAuthInitializer which has optional | 254 # XXX: modification of client.CheckAuthInitializer which has optional |
112 # resource binding, and which doesn't do deprecated | 255 # resource binding, and which doesn't do deprecated |
113 # SessionInitializer | 256 # SessionInitializer |
114 if (sasl.NS_XMPP_SASL, 'mechanisms') in self.xmlstream.features: | 257 if (sasl.NS_XMPP_SASL, "mechanisms") in self.xmlstream.features: |
115 inits = [(sasl.SASLInitiatingInitializer, True)] | 258 inits = [(SASLInitiatingInitializer, True)] |
116 if self.res_binding: | 259 if self.res_binding: |
117 inits.append((tclient.BindInitializer, True)), | 260 inits.append((tclient.BindInitializer, True)), |
118 | 261 |
119 for initClass, required in inits: | 262 for initClass, required in inits: |
120 init = initClass(self.xmlstream) | 263 init = initClass(self.xmlstream) |
121 init.required = required | 264 init.required = required |
122 self.xmlstream.initializers.append(init) | 265 self.xmlstream.initializers.append(init) |
123 elif (tclient.NS_IQ_AUTH_FEATURE, 'auth') in self.xmlstream.features: | 266 elif (tclient.NS_IQ_AUTH_FEATURE, "auth") in self.xmlstream.features: |
124 self.xmlstream.initializers.append( | 267 self.xmlstream.initializers.append(tclient.IQAuthInitializer(self.xmlstream)) |
125 tclient.IQAuthInitializer(self.xmlstream)) | |
126 else: | 268 else: |
127 raise Exception("No available authentication method found") | 269 raise Exception("No available authentication method found") |
128 | 270 |
129 | 271 |
130 # jid fix | 272 # jid fix |
273 | |
131 | 274 |
132 def internJID(jidstring): | 275 def internJID(jidstring): |
133 """ | 276 """ |
134 Return interned JID. | 277 Return interned JID. |
135 | 278 |