mirror of
https://github.com/ansible-collections/community.mysql.git
synced 2025-07-19 11:20:23 -07:00
Add salt parameter to hash generation for sha256 plugins (#631)
* add salt parameter to hash generation for sha256 plugin * technomax review modification * no general user test for salt
This commit is contained in:
parent
50e7413b88
commit
0bc3e3d848
6 changed files with 251 additions and 7 deletions
125
plugins/module_utils/implementations/mysql/hash.py
Normal file
125
plugins/module_utils/implementations/mysql/hash.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
Generate MySQL sha256 compatible plugins hash for a given password and salt
|
||||
|
||||
based on
|
||||
* https://www.akkadia.org/drepper/SHA-crypt.txt
|
||||
* https://crypto.stackexchange.com/questions/77427/whats-the-algorithm-behind-mysqls-sha256-password-hashing-scheme/111174#111174
|
||||
* https://github.com/hashcat/hashcat/blob/master/tools/test_modules/m07400.pm
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import hashlib
|
||||
|
||||
|
||||
def _to64(v, n):
|
||||
"""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 = ""
|
||||
while n > 0:
|
||||
n -= 1
|
||||
result += i64[v & 0x3F]
|
||||
v >>= 6
|
||||
return result
|
||||
|
||||
|
||||
def _hashlib_sha256(data):
|
||||
"""Return SHA-256 digest from hashlib ."""
|
||||
return hashlib.sha256(data).digest()
|
||||
|
||||
|
||||
def _sha256_digest(key, salt, loops):
|
||||
"""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 = 32
|
||||
bytes_key = key.encode()
|
||||
bytes_salt = 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_sha256_password_hash(password, salt):
|
||||
"""Return a MySQL compatible caching_sha2_password hash in raw format."""
|
||||
if len(salt) != 20:
|
||||
raise ValueError("Salt must be 20 characters long.")
|
||||
|
||||
count = 5
|
||||
iteration = 1000 * count
|
||||
|
||||
digest = _sha256_digest(password, salt, iteration)
|
||||
return "$A${0:>03}${1}{2}".format(count, salt, digest)
|
||||
|
||||
|
||||
def mysql_sha256_password_hash_hex(password, salt):
|
||||
"""Return a MySQL compatible caching_sha2_password hash in hex format."""
|
||||
return mysql_sha256_password_hash(password, salt).encode().hex().upper()
|
|
@ -1,4 +1,6 @@
|
|||
from __future__ import (absolute_import, division, print_function)
|
||||
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
# This code is part of Ansible, but is an independent component.
|
||||
|
@ -19,6 +21,10 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
|
|||
mysql_driver,
|
||||
get_server_implementation,
|
||||
)
|
||||
from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql.hash import (
|
||||
mysql_sha256_password_hash,
|
||||
mysql_sha256_password_hash_hex,
|
||||
)
|
||||
|
||||
|
||||
class InvalidPrivsError(Exception):
|
||||
|
@ -135,7 +141,7 @@ def get_existing_authentication(cursor, user, host):
|
|||
|
||||
|
||||
def user_add(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string, new_priv,
|
||||
plugin, plugin_hash_string, plugin_auth_string, salt, new_priv,
|
||||
attributes, tls_requires, reuse_existing_password, module,
|
||||
password_expire, password_expire_interval):
|
||||
# If attributes are set, perform a sanity check to ensure server supports user attributes before creating user
|
||||
|
@ -181,6 +187,12 @@ def user_add(cursor, user, host, host_all, password, encrypted,
|
|||
# Mysql and MariaDB differ in naming pam plugin and Syntax to set it
|
||||
if plugin == 'pam': # Used by MariaDB which requires the USING keyword, not BY
|
||||
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s USING %s", (user, host, plugin, plugin_auth_string)
|
||||
elif salt:
|
||||
if plugin in ['caching_sha2_password', 'sha256_password']:
|
||||
generated_hash_string = mysql_sha256_password_hash_hex(password=plugin_auth_string, salt=salt)
|
||||
else:
|
||||
module.fail_json(msg="salt not handled for %s authentication plugin" % plugin)
|
||||
query_with_args = ("CREATE USER %s@%s IDENTIFIED WITH %s AS 0x" + generated_hash_string), (user, host, plugin)
|
||||
else:
|
||||
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)
|
||||
elif plugin:
|
||||
|
@ -221,7 +233,7 @@ def is_hash(password):
|
|||
|
||||
|
||||
def user_mod(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string, new_priv,
|
||||
plugin, plugin_hash_string, plugin_auth_string, salt, new_priv,
|
||||
append_privs, subtract_privs, attributes, tls_requires, module,
|
||||
password_expire, password_expire_interval, role=False, maria_role=False):
|
||||
changed = False
|
||||
|
@ -342,7 +354,11 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
if plugin_hash_string and current_plugin[1] != plugin_hash_string:
|
||||
update = True
|
||||
|
||||
if plugin_auth_string and current_plugin[1] != plugin_auth_string:
|
||||
if salt:
|
||||
if plugin in ['caching_sha2_password', 'sha256_password']:
|
||||
if current_plugin[1] != mysql_sha256_password_hash(password=plugin_auth_string, salt=salt):
|
||||
update = True
|
||||
elif plugin_auth_string and current_plugin[1] != plugin_auth_string:
|
||||
# this case can cause more updates than expected,
|
||||
# as plugin can hash auth_string in any way it wants
|
||||
# and there's no way to figure it out for
|
||||
|
@ -356,6 +372,12 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
|
|||
# Mysql and MariaDB differ in naming pam plugin and syntax to set it
|
||||
if plugin in ('pam', 'ed25519'):
|
||||
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s USING %s", (user, host, plugin, plugin_auth_string)
|
||||
elif salt:
|
||||
if plugin in ['caching_sha2_password', 'sha256_password']:
|
||||
generated_hash_string = mysql_sha256_password_hash_hex(password=plugin_auth_string, salt=salt)
|
||||
else:
|
||||
module.fail_json(msg="salt not handled for %s authentication plugin" % plugin)
|
||||
query_with_args = ("ALTER USER %s@%s IDENTIFIED WITH %s AS 0x" + generated_hash_string), (user, host, plugin)
|
||||
else:
|
||||
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)
|
||||
else:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue