#!/usr/bin/env python

"""
Copyright (c) 2006-2026 sqlmap developers (https://sqlmap.org)
See the file 'LICENSE' for copying permission

Password-hashing primitives (lib/utils/hash.py) used by the dictionary-attack
cracker (-? / --passwords). These are pure functions; correctness here is what
makes a cracked password actually match the target hash.

The generic hashes are cross-checked against the stdlib hashlib (an INDEPENDENT
oracle, not just a regression against sqlmap's own output). The DBMS-specific
algorithms (MySQL/MSSQL/Oracle/Postgres) are pinned to known vectors, and
hashRecognition's classification is exercised as a table.
"""

import hashlib
import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _testutils import bootstrap
bootstrap()

from lib.utils import hash as H
from lib.core.enums import HASH


class TestGenericVsHashlib(unittest.TestCase):
    """Independent oracle: sqlmap's generic hashes must equal stdlib hashlib."""

    PW = "testpass"

    def test_md5(self):
        self.assertEqual(H.md5_generic_passwd(self.PW), hashlib.md5(b"testpass").hexdigest())

    def test_sha1(self):
        self.assertEqual(H.sha1_generic_passwd(self.PW), hashlib.sha1(b"testpass").hexdigest())

    def test_sha224(self):
        self.assertEqual(H.sha224_generic_passwd(self.PW), hashlib.sha224(b"testpass").hexdigest())

    def test_sha256(self):
        self.assertEqual(H.sha256_generic_passwd(self.PW), hashlib.sha256(b"testpass").hexdigest())

    def test_sha384(self):
        self.assertEqual(H.sha384_generic_passwd(self.PW), hashlib.sha384(b"testpass").hexdigest())

    def test_sha512(self):
        self.assertEqual(H.sha512_generic_passwd(self.PW), hashlib.sha512(b"testpass").hexdigest())


class TestUppercase(unittest.TestCase):
    def test_uppercase_flag(self):
        self.assertEqual(H.md5_generic_passwd("testpass", uppercase=True),
                         hashlib.md5(b"testpass").hexdigest().upper())

    def test_lowercase_default(self):
        out = H.md5_generic_passwd("testpass", uppercase=False)
        self.assertEqual(out, out.lower())


class TestDbmsSpecificVectors(unittest.TestCase):
    """Known vectors for the DBMS-native algorithms (mirrors the docstrings)."""

    def test_mysql(self):
        self.assertEqual(H.mysql_passwd("testpass", uppercase=True),
                         "*00E247AC5F9AF26AE0194B41E1E769DEE1429A29")

    def test_mysql_old(self):
        self.assertEqual(H.mysql_old_passwd("testpass", uppercase=True), "7DCDA0D57290B453")

    def test_postgres(self):
        self.assertEqual(H.postgres_passwd("testpass", "testuser", uppercase=False),
                         "md599e5ea7a6f7c3269995cba3927fd0093")

    def test_mssql(self):
        self.assertEqual(H.mssql_passwd("testpass", salt="4086ceb6", uppercase=False),
                         "0x01004086ceb60c90646a8ab9889fe3ed8e5c150b5460ece8425a")

    def test_oracle(self):
        self.assertEqual(H.oracle_passwd("SHAlala", salt="1B7B5F82B7235E9E182C", uppercase=True),
                         "S:2BFCFDF5895014EE9BB2B9BA067B01E0389BB5711B7B5F82B7235E9E182C")

    def test_oracle_old(self):
        self.assertEqual(H.oracle_old_passwd("tiger", "scott", uppercase=True), "F894844C34402B67")


class TestCryptGenericBytesInput(unittest.TestCase):
    """
    The dictionary-attack workers (_bruteProcessVariantA/B in lib/utils/hash.py) always pass
    'password' as bytes (getBytes(word)), regardless of which hash function gets dispatched. Every
    other function here hashes via hashlib (bytes-friendly), but crypt_generic_passwd forwards
    straight to the stdlib crypt.crypt(), which requires str - raising 'TypeError: crypt() argument
    1 must be str, not bytes' on Linux/macOS (reported via a user's cracked-hash session).
    """

    def test_bytes_password_does_not_raise(self):
        self.assertEqual(H.crypt_generic_passwd(password=b"rasmuslerdorf", salt="rl", uppercase=False), "rl.3StKT.4T8M")

    def test_str_password_unaffected(self):
        self.assertEqual(H.crypt_generic_passwd(password="rasmuslerdorf", salt="rl", uppercase=False), "rl.3StKT.4T8M")


class TestMssqlUnicodePassword(unittest.TestCase):
    """MSSQL hashes the password as UCS-2/UTF-16LE. A per-char 'utf-8 + NUL' approximation is only
    correct for ASCII, so non-ASCII passwords (cafe, etc.) hashed WRONG and were uncrackable. The
    2012+ (SHA-512) ground truth is a live PWDENCRYPT(N'caf'+NCHAR(233)) from Azure SQL Edge."""

    CAFE = u"caf\xe9"

    def test_mssql_new_matches_live_pwdencrypt(self):
        real = ("0x0200a0d961e49fc45ec4922793c4f0b278587e977b281c10871a30a6e620ab0c24c"
                "dce517f208252d6e5ca608d958c89aff5c69061cc6c788854e3e0788cb2510e227481990d")
        self.assertEqual(H.mssql_new_passwd(self.CAFE, salt="a0d961e4", uppercase=False), real)

    def test_mssql_matches_documented_algorithm(self):
        # 2005/2008: 0x0100 + salt + SHA1(UTF16LE(password) + salt)
        salt = "4086ceb6"
        expected = "0x0100%s%s" % (salt, hashlib.sha1(self.CAFE.encode("utf-16-le") + bytearray.fromhex(salt)).hexdigest())
        self.assertEqual(H.mssql_passwd(self.CAFE, salt=salt, uppercase=False), expected)

    def test_ascii_unchanged(self):
        # the fix must leave the ASCII path identical (still the documented UTF-16LE form)
        salt = "4086ceb6"
        expected = "0x0100%s%s" % (salt, hashlib.sha1(u"testpass".encode("utf-16-le") + bytearray.fromhex(salt)).hexdigest())
        self.assertEqual(H.mssql_passwd("testpass", salt=salt, uppercase=False), expected)


class TestHashRecognition(unittest.TestCase):
    def test_md5_generic(self):
        self.assertEqual(H.hashRecognition("179ad45c6ce2cb97cf1029e212046e81"), HASH.MD5_GENERIC)

    def test_sha1_generic(self):
        self.assertEqual(H.hashRecognition("206c80413b9a96c1312cc346b7d2517b84463edd"), HASH.SHA1_GENERIC)

    def test_mysql(self):
        self.assertEqual(H.hashRecognition("*00E247AC5F9AF26AE0194B41E1E769DEE1429A29"), HASH.MYSQL)

    def test_crypt_generic(self):
        # Traditional DES crypt(3) (hashcat -m 1500); mixed-case is required by the heuristic
        self.assertEqual(H.hashRecognition("rl.3StKT.4T8M"), HASH.CRYPT_GENERIC)

    def test_crypt_generic_single_case_is_none(self):
        # All-lower/all-upper 13-char values are too greedy to be trusted as crypt
        self.assertIsNone(H.hashRecognition("abcdefghijklm"))
        self.assertIsNone(H.hashRecognition("ABCDEFGHIJKLM"))

    def test_junk_is_none(self):
        self.assertIsNone(H.hashRecognition("foobar"))


if __name__ == "__main__":
    unittest.main(verbosity=2)
