aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/asyncpg/protocol/scram.pyx
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/asyncpg/protocol/scram.pyx
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/asyncpg/protocol/scram.pyx')
-rw-r--r--.venv/lib/python3.12/site-packages/asyncpg/protocol/scram.pyx341
1 files changed, 341 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/asyncpg/protocol/scram.pyx b/.venv/lib/python3.12/site-packages/asyncpg/protocol/scram.pyx
new file mode 100644
index 00000000..9b485aee
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/asyncpg/protocol/scram.pyx
@@ -0,0 +1,341 @@
+# Copyright (C) 2016-present the asyncpg authors and contributors
+# <see AUTHORS file>
+#
+# This module is part of asyncpg and is released under
+# the Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0
+
+
+import base64
+import hashlib
+import hmac
+import re
+import secrets
+import stringprep
+import unicodedata
+
+
+@cython.final
+cdef class SCRAMAuthentication:
+ """Contains the protocol for generating and a SCRAM hashed password.
+
+ Since PostgreSQL 10, the option to hash passwords using the SCRAM-SHA-256
+ method was added. This module follows the defined protocol, which can be
+ referenced from here:
+
+ https://www.postgresql.org/docs/current/sasl-authentication.html#SASL-SCRAM-SHA-256
+
+ libpq references the following RFCs that it uses for implementation:
+
+ * RFC 5802
+ * RFC 5803
+ * RFC 7677
+
+ The protocol works as such:
+
+ - A client connets to the server. The server requests the client to begin
+ SASL authentication using SCRAM and presents a client with the methods it
+ supports. At present, those are SCRAM-SHA-256, and, on servers that are
+ built with OpenSSL and
+ are PG11+, SCRAM-SHA-256-PLUS (which supports channel binding, more on that
+ below)
+
+ - The client sends a "first message" to the server, where it chooses which
+ method to authenticate with, and sends, along with the method, an indication
+ of channel binding (we disable for now), a nonce, and the username.
+ (Technically, PostgreSQL ignores the username as it already has it from the
+ initical connection, but we add it for completeness)
+
+ - The server responds with a "first message" in which it extends the nonce,
+ as well as a password salt and the number of iterations to hash the password
+ with. The client validates that the new nonce contains the first part of the
+ client's original nonce
+
+ - The client generates a salted password, but does not sent this up to the
+ server. Instead, the client follows the SCRAM algorithm (RFC5802) to
+ generate a proof. This proof is sent aspart of a client "final message" to
+ the server for it to validate.
+
+ - The server validates the proof. If it is valid, the server sends a
+ verification code for the client to verify that the server came to the same
+ proof the client did. PostgreSQL immediately sends an AuthenticationOK
+ response right after a valid negotiation. If the password the client
+ provided was invalid, then authentication fails.
+
+ (The beauty of this is that the salted password is never transmitted over
+ the wire!)
+
+ PostgreSQL 11 added support for the channel binding (i.e.
+ SCRAM-SHA-256-PLUS) but to do some ongoing discussion, there is a conscious
+ decision by several driver authors to not support it as of yet. As such, the
+ channel binding parameter is hard-coded to "n" for now, but can be updated
+ to support other channel binding methos in the future
+ """
+ AUTHENTICATION_METHODS = [b"SCRAM-SHA-256"]
+ DEFAULT_CLIENT_NONCE_BYTES = 24
+ DIGEST = hashlib.sha256
+ REQUIREMENTS_CLIENT_FINAL_MESSAGE = ['client_channel_binding',
+ 'server_nonce']
+ REQUIREMENTS_CLIENT_PROOF = ['password_iterations', 'password_salt',
+ 'server_first_message', 'server_nonce']
+ SASLPREP_PROHIBITED = (
+ stringprep.in_table_a1, # PostgreSQL treats this as prohibited
+ stringprep.in_table_c12,
+ stringprep.in_table_c21_c22,
+ stringprep.in_table_c3,
+ stringprep.in_table_c4,
+ stringprep.in_table_c5,
+ stringprep.in_table_c6,
+ stringprep.in_table_c7,
+ stringprep.in_table_c8,
+ stringprep.in_table_c9,
+ )
+
+ def __cinit__(self, bytes authentication_method):
+ self.authentication_method = authentication_method
+ self.authorization_message = None
+ # channel binding is turned off for the time being
+ self.client_channel_binding = b"n,,"
+ self.client_first_message_bare = None
+ self.client_nonce = None
+ self.client_proof = None
+ self.password_salt = None
+ # self.password_iterations = None
+ self.server_first_message = None
+ self.server_key = None
+ self.server_nonce = None
+
+ cdef create_client_first_message(self, str username):
+ """Create the initial client message for SCRAM authentication"""
+ cdef:
+ bytes msg
+ bytes client_first_message
+
+ self.client_nonce = \
+ self._generate_client_nonce(self.DEFAULT_CLIENT_NONCE_BYTES)
+ # set the client first message bare here, as it's used in a later step
+ self.client_first_message_bare = b"n=" + username.encode("utf-8") + \
+ b",r=" + self.client_nonce
+ # put together the full message here
+ msg = bytes()
+ msg += self.authentication_method + b"\0"
+ client_first_message = self.client_channel_binding + \
+ self.client_first_message_bare
+ msg += (len(client_first_message)).to_bytes(4, byteorder='big') + \
+ client_first_message
+ return msg
+
+ cdef create_client_final_message(self, str password):
+ """Create the final client message as part of SCRAM authentication"""
+ cdef:
+ bytes msg
+
+ if any([getattr(self, val) is None for val in
+ self.REQUIREMENTS_CLIENT_FINAL_MESSAGE]):
+ raise Exception(
+ "you need values from server to generate a client proof")
+
+ # normalize the password using the SASLprep algorithm in RFC 4013
+ password = self._normalize_password(password)
+
+ # generate the client proof
+ self.client_proof = self._generate_client_proof(password=password)
+ msg = bytes()
+ msg += b"c=" + base64.b64encode(self.client_channel_binding) + \
+ b",r=" + self.server_nonce + \
+ b",p=" + base64.b64encode(self.client_proof)
+ return msg
+
+ cdef parse_server_first_message(self, bytes server_response):
+ """Parse the response from the first message from the server"""
+ self.server_first_message = server_response
+ try:
+ self.server_nonce = re.search(b'r=([^,]+),',
+ self.server_first_message).group(1)
+ except IndexError:
+ raise Exception("could not get nonce")
+ if not self.server_nonce.startswith(self.client_nonce):
+ raise Exception("invalid nonce")
+ try:
+ self.password_salt = re.search(b',s=([^,]+),',
+ self.server_first_message).group(1)
+ except IndexError:
+ raise Exception("could not get salt")
+ try:
+ self.password_iterations = int(re.search(b',i=(\d+),?',
+ self.server_first_message).group(1))
+ except (IndexError, TypeError, ValueError):
+ raise Exception("could not get iterations")
+
+ cdef verify_server_final_message(self, bytes server_final_message):
+ """Verify the final message from the server"""
+ cdef:
+ bytes server_signature
+
+ try:
+ server_signature = re.search(b'v=([^,]+)',
+ server_final_message).group(1)
+ except IndexError:
+ raise Exception("could not get server signature")
+
+ verify_server_signature = hmac.new(self.server_key.digest(),
+ self.authorization_message, self.DIGEST)
+ # validate the server signature against the verifier
+ return server_signature == base64.b64encode(
+ verify_server_signature.digest())
+
+ cdef _bytes_xor(self, bytes a, bytes b):
+ """XOR two bytestrings together"""
+ return bytes(a_i ^ b_i for a_i, b_i in zip(a, b))
+
+ cdef _generate_client_nonce(self, int num_bytes):
+ cdef:
+ bytes token
+
+ token = secrets.token_bytes(num_bytes)
+
+ return base64.b64encode(token)
+
+ cdef _generate_client_proof(self, str password):
+ """need to ensure a server response exists, i.e. """
+ cdef:
+ bytes salted_password
+
+ if any([getattr(self, val) is None for val in
+ self.REQUIREMENTS_CLIENT_PROOF]):
+ raise Exception(
+ "you need values from server to generate a client proof")
+ # generate a salt password
+ salted_password = self._generate_salted_password(password,
+ self.password_salt, self.password_iterations)
+ # client key is derived from the salted password
+ client_key = hmac.new(salted_password, b"Client Key", self.DIGEST)
+ # this allows us to compute the stored key that is residing on the server
+ stored_key = self.DIGEST(client_key.digest())
+ # as well as compute the server key
+ self.server_key = hmac.new(salted_password, b"Server Key", self.DIGEST)
+ # build the authorization message that will be used in the
+ # client signature
+ # the "c=" portion is for the channel binding, but this is not
+ # presently implemented
+ self.authorization_message = self.client_first_message_bare + b"," + \
+ self.server_first_message + b",c=" + \
+ base64.b64encode(self.client_channel_binding) + \
+ b",r=" + self.server_nonce
+ # sign!
+ client_signature = hmac.new(stored_key.digest(),
+ self.authorization_message, self.DIGEST)
+ # and the proof
+ return self._bytes_xor(client_key.digest(), client_signature.digest())
+
+ cdef _generate_salted_password(self, str password, bytes salt, int iterations):
+ """This follows the "Hi" algorithm specified in RFC5802"""
+ cdef:
+ bytes p
+ bytes s
+ bytes u
+
+ # convert the password to a binary string - UTF8 is safe for SASL
+ # (though there are SASLPrep rules)
+ p = password.encode("utf8")
+ # the salt needs to be base64 decoded -- full binary must be used
+ s = base64.b64decode(salt)
+ # the initial signature is the salt with a terminator of a 32-bit string
+ # ending in 1
+ ui = hmac.new(p, s + b'\x00\x00\x00\x01', self.DIGEST)
+ # grab the initial digest
+ u = ui.digest()
+ # for X number of iterations, recompute the HMAC signature against the
+ # password and the latest iteration of the hash, and XOR it with the
+ # previous version
+ for x in range(iterations - 1):
+ ui = hmac.new(p, ui.digest(), hashlib.sha256)
+ # this is a fancy way of XORing two byte strings together
+ u = self._bytes_xor(u, ui.digest())
+ return u
+
+ cdef _normalize_password(self, str original_password):
+ """Normalize the password using the SASLprep from RFC4013"""
+ cdef:
+ str normalized_password
+
+ # Note: Per the PostgreSQL documentation, PostgreSWL does not require
+ # UTF-8 to be used for the password, but will perform SASLprep on the
+ # password regardless.
+ # If the password is not valid UTF-8, PostgreSQL will then **not** use
+ # SASLprep processing.
+ # If the password fails SASLprep, the password should still be sent
+ # See: https://www.postgresql.org/docs/current/sasl-authentication.html
+ # and
+ # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/common/saslprep.c
+ # using the `pg_saslprep` function
+ normalized_password = original_password
+ # if the original password is an ASCII string or fails to encode as a
+ # UTF-8 string, then no further action is needed
+ try:
+ original_password.encode("ascii")
+ except UnicodeEncodeError:
+ pass
+ else:
+ return original_password
+
+ # Step 1 of SASLPrep: Map. Per the algorithm, we map non-ascii space
+ # characters to ASCII spaces (\x20 or \u0020, but we will use ' ') and
+ # commonly mapped to nothing characters are removed
+ # Table C.1.2 -- non-ASCII spaces
+ # Table B.1 -- "Commonly mapped to nothing"
+ normalized_password = u"".join(
+ ' ' if stringprep.in_table_c12(c) else c
+ for c in tuple(normalized_password) if not stringprep.in_table_b1(c)
+ )
+
+ # If at this point the password is empty, PostgreSQL uses the original
+ # password
+ if not normalized_password:
+ return original_password
+
+ # Step 2 of SASLPrep: Normalize. Normalize the password using the
+ # Unicode normalization algorithm to NFKC form
+ normalized_password = unicodedata.normalize('NFKC', normalized_password)
+
+ # If the password is not empty, PostgreSQL uses the original password
+ if not normalized_password:
+ return original_password
+
+ normalized_password_tuple = tuple(normalized_password)
+
+ # Step 3 of SASLPrep: Prohobited characters. If PostgreSQL detects any
+ # of the prohibited characters in SASLPrep, it will use the original
+ # password
+ # We also include "unassigned code points" in the prohibited character
+ # category as PostgreSQL does the same
+ for c in normalized_password_tuple:
+ if any(
+ in_prohibited_table(c)
+ for in_prohibited_table in self.SASLPREP_PROHIBITED
+ ):
+ return original_password
+
+ # Step 4 of SASLPrep: Bi-directional characters. PostgreSQL follows the
+ # rules for bi-directional characters laid on in RFC3454 Sec. 6 which
+ # are:
+ # 1. Characters in RFC 3454 Sec 5.8 are prohibited (C.8)
+ # 2. If a string contains a RandALCat character, it cannot containy any
+ # LCat character
+ # 3. If the string contains any RandALCat character, an RandALCat
+ # character must be the first and last character of the string
+ # RandALCat characters are found in table D.1, whereas LCat are in D.2
+ if any(stringprep.in_table_d1(c) for c in normalized_password_tuple):
+ # if the first character or the last character are not in D.1,
+ # return the original password
+ if not (stringprep.in_table_d1(normalized_password_tuple[0]) and
+ stringprep.in_table_d1(normalized_password_tuple[-1])):
+ return original_password
+
+ # if any characters are in D.2, use the original password
+ if any(
+ stringprep.in_table_d2(c) for c in normalized_password_tuple
+ ):
+ return original_password
+
+ # return the normalized password
+ return normalized_password