-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
mysql_user
: can't get plugin_hash_string
working with caching_sha2_password
#621
Comments
Hi @Aohzan and thank you for your well documented issue. Regarding tests using For the non binary characters you are right. The collection need a way to pass them to MySQL and currently the documentation and the tests are lacking. As I only work with MariaDB, I can't fix those issues myself. But I'll be happy to help with the tests and reviewing a PR. |
Thanks, I will make a PR as soon as I find a solution to generate a working hash 😁 |
Great, let me know when you start working on this. Because I can't fix the TASK [test_mysql_info : Mysql_info users_info | Prepare tests users for MySQL 8+] ***
failed: [testhost] (item={'name': 'users_info_caching_sha2', 'priv': {'*.*': 'ALL'}, 'plugin_hash_string': '$A$005$61j/uF%Qb4-=O2xkeO82u2HNkF.lxDq0liO4U3xqi7bDUCbWM6HayRXWn1', 'plugin': 'caching_sha2_password'}) => {"ansible_loop_var": "item", "changed": false, "item": {"name": "users_info_caching_sha2", "plugin": "caching_sha2_password", "plugin_hash_string": "$A$005$61j/uF%Qb4-=O2xkeO82u2HNkF.lxDq0liO4U3xqi7bDUCbWM6HayRXWn1", "priv": {"*.*": "ALL"}}, "msg": "(1827, \"The password hash doesn't have the expected format.\")"} |
fyi, I opened a bug report to MySQL https://bugs.mysql.com/bug.php?id=114610&thanks=4 |
We made a python script (based on this) to generate a MySQL hash compatible with caching_sha2_password plugin, what is the best way to integrate it in this module ? Maybe a filter will be the best solution, but we need a way for give a hash in hex format to """Generate MySQL caching_sha2_password hash for a given password and salt."""
import hashlib
def to64(v: int, n: int) -> str:
"""Convert a 32-bit integer to a base-64 string"""
i64 = (
[".", "/"]
+ [chr(x) for x in range(48, 58)]
+ [chr(x) for x in range(65, 91)]
+ [chr(x) for x in range(97, 123)]
)
result: str = ""
while n > 0:
n -= 1
result += i64[v & 0x3F]
v >>= 6
return result
def hashlib_sha256(data: bytes) -> bytes:
"""Return SHA-256 digest from hashlib ."""
return hashlib.sha256(data).digest()
def sha256_digest(bits: int, key: str, salt: str, loops: int) -> str:
"""Return a SHA-256 digest of the concatenation of the key, the salt, and the key, repeated as necessary."""
num_bytes: bytes = bits // 8
bytes_key: bytes = key.encode()
bytes_salt: bytes = salt.encode()
b = hashlib_sha256(bytes_key + bytes_salt + bytes_key)
tmp = bytes_key + bytes_salt
for i in range(len(bytes_key), 0, -num_bytes):
tmp += b if i > num_bytes else b[:i]
i = len(bytes_key)
while i > 0:
tmp += b if (i & 1) != 0 else bytes_key
i >>= 1
a = hashlib_sha256(tmp)
tmp = b""
for i in range(len(bytes_key)):
tmp += bytes_key
dp = hashlib_sha256(tmp)
p = b""
for i in range(len(bytes_key), 0, -num_bytes):
p += dp if i > num_bytes else dp[:i]
tmp = b""
til = 16 + a[0]
for i in range(til):
tmp += bytes_salt
ds = hashlib_sha256(tmp)
s = b""
for i in range(len(bytes_salt), 0, -num_bytes):
s += ds if i > num_bytes else ds[:i]
c = a
for i in range(loops):
tmp = p if (i & 1) else c
if i % 3:
tmp += s
if i % 7:
tmp += p
tmp += c if (i & 1) else p
c = hashlib_sha256(tmp)
inc1, inc2, mod, end = (10, 21, 30, 0) if bits == 256 else (21, 22, 63, 21)
i = 0
tmp = ""
while True:
tmp += to64(
(c[i] << 16) | (c[(i + inc1) % mod] << 8) | c[(i + inc1 * 2) % mod], 4
)
i = (i + inc2) % mod
if i == end:
break
tmp += to64((c[31] << 8) | c[30], 3) if bits == 256 else to64(c[63], 2)
return tmp
def mysql_caching_sha2_password_hex(password: str, salt: str) -> str:
"""Return a MySQL compatible caching_sha2_password hash in hex format."""
assert len(salt) == 20, "Salt must be 20 characters long."
count = 5
iteration = 1000 * count
digest = sha256_digest(256, password, salt, iteration)
return f"$A${count:>03}${salt}{digest}".encode().hex().upper()
mysql_hash = mysql_caching_sha2_password_hex("testpassword", "TDwqdanU82d0yNtvaabb")
print(mysql_hash)
print(
f"ALTER USER `mysql`@`%` IDENTIFIED WITH 'caching_sha2_password' AS 0x{mysql_hash};"
) |
That is quite amazing job you did there @Aohzan to replicate the MySQL behavior! Do you think that adding the above method as a filter is enough? Or do we need to add some new options like 'salt' to the module for caching_sha2_password to fully works? We need to be 100% sure that your method will always produce the same hash as MySQL 8+ expects. Do you know if there has been any changes on the those authentication plugins in MySQL 8.1, 8.2 and 8.3? |
Yes I agree 😞
Option 1:
Option 2:
Maybe an other option ?
I will test it to be sure, but I think it works in the same way |
I prefer Option 2. The C source is not very simple: https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/auth/sha2_password.cc Regarding your offered solution, it has a bit too much variables with one or two letters to my taste. It makes it hard to understand what is going on. For instance, what are the variables |
it's the name of digest used in the algorithm: https://www.akkadia.org/drepper/SHA-crypt.txt |
there is the updated script """Generate MySQL caching_sha2_password hash for a given password and salt."""
import hashlib
def to64(v: int, n: int) -> str:
"""Convert a 32-bit integer to a base-64 string"""
i64 = (
[".", "/"]
+ [chr(x) for x in range(48, 58)]
+ [chr(x) for x in range(65, 91)]
+ [chr(x) for x in range(97, 123)]
)
result: str = ""
while n > 0:
n -= 1
result += i64[v & 0x3F]
v >>= 6
return result
def hashlib_sha256(data: bytes) -> bytes:
"""Return SHA-256 digest from hashlib ."""
return hashlib.sha256(data).digest()
def sha256_digest(key: str, salt: str, loops: int) -> str:
"""Return a SHA-256 digest of the concatenation of the key, the salt, and the key, repeated as necessary."""
# https://www.akkadia.org/drepper/SHA-crypt.txt
num_bytes: bytes = 32
bytes_key: bytes = key.encode()
bytes_salt: bytes = salt.encode()
digest_b = hashlib_sha256(bytes_key + bytes_salt + bytes_key)
tmp = bytes_key + bytes_salt
for i in range(len(bytes_key), 0, -num_bytes):
tmp += digest_b if i > num_bytes else digest_b[:i]
i = len(bytes_key)
while i > 0:
tmp += digest_b if (i & 1) != 0 else bytes_key
i >>= 1
digest_a = hashlib_sha256(tmp)
tmp = b""
for i in range(len(bytes_key)):
tmp += bytes_key
digest_dp = hashlib_sha256(tmp)
byte_sequence_p = b""
for i in range(len(bytes_key), 0, -num_bytes):
byte_sequence_p += digest_dp if i > num_bytes else digest_dp[:i]
tmp = b""
til = 16 + digest_a[0]
for i in range(til):
tmp += bytes_salt
digest_ds = hashlib_sha256(tmp)
byte_sequence_s = b""
for i in range(len(bytes_salt), 0, -num_bytes):
byte_sequence_s += digest_ds if i > num_bytes else digest_ds[:i]
digest_c = digest_a
for i in range(loops):
tmp = byte_sequence_p if (i & 1) else digest_c
if i % 3:
tmp += byte_sequence_s
if i % 7:
tmp += byte_sequence_p
tmp += digest_c if (i & 1) else byte_sequence_p
digest_c = hashlib_sha256(tmp)
inc1, inc2, mod, end = (10, 21, 30, 0)
i = 0
tmp = ""
while True:
tmp += to64(
(digest_c[i] << 16) | (digest_c[(i + inc1) % mod] << 8) | digest_c[(i + inc1 * 2) % mod], 4
)
i = (i + inc2) % mod
if i == end:
break
tmp += to64((digest_c[31] << 8) | digest_c[30], 3)
return tmp
def mysql_caching_sha2_password_hex(password: str, salt: str) -> str:
"""Return a MySQL compatible caching_sha2_password hash in hex format."""
assert len(salt) == 20, "Salt must be 20 characters long."
count = 5
iteration = 1000 * count
digest = sha256_digest(password, salt, iteration)
return f"$A${count:>03}${salt}{digest}".encode().hex().upper()
mysql_hash = mysql_caching_sha2_password_hex("testpassword", "TDwqdanU82d0yNtvaabb")
print(mysql_hash)
print(
f"ALTER USER `testuser1`@`%` IDENTIFIED WITH 'caching_sha2_password' AS 0x{mysql_hash};"
) |
Thanks for the revision of your code @Aohzan. It's better now. I'm not saying that I understand everything thought ;) I must be too tired... caching_sha2_password: But when I grab the user info using I used this tests from my in progress PR : https://github.com/ansible-collections/community.mysql/pull/629/files#diff-0a20e475385b180622c5b82eba4351e27488c54255c6ca7cd3435abf82a2b51f First good news is that the salt is optional, right? |
I managed to find the successful test in the logs: https://github.com/ansible-collections/community.mysql/actions/runs/8663542726/job/23757899256#step:11:4183 |
From what I understood. https://dev.mysql.com/doc/refman/8.3/en/create-user.html
so when using the "literal string"
Here A much more realistic approach is to pass
I am not sure I get what you mean.
|
@tchernomax thanks for taking the time to clarify things for me.
I'm not sure I got this right. |
Yes we're starting to work on it |
SUMMARY
Hello,
I use
caching_sha2_password
plugin for my users on MySQL 8 servers.At beginning, I just use
but I had idempotence issue because, each time the password was marked as changed because the hash generated by MySQL was different.
So I tried to use
plugin_hash_string
instead ofplugin_auth_string
to provide directly the hash generated from ansible to avoid idempotence issue. Likebut I have many problems:
plugin_hash_string
instead ofplugin_auth_string
plugin_hash_string = base64.b64decode(plugin_hash_base64).decode("utf-8")
I found this on MySQL source code, with the hash like
$A$005$salthash
(salf length 20, hash length 43) but cannot log with it. Maybe a correct example can be added to the ansible documentation ?ISSUE TYPE
COMPONENT NAME
mysql_user
ANSIBLE VERSION
COLLECTION VERSION
CONFIGURATION
OS / ENVIRONMENT
Debian 12
STEPS TO REPRODUCE
example of hash
EXPECTED RESULTS
Changed hash, can connect with password before hash, and idempotence ok
ACTUAL RESULTS
Change hash, cannot connect with password
or
Hash not accepted by MySQL
The text was updated successfully, but these errors were encountered: