8
\$\begingroup\$

I wanted to better understand regular expressions, however, just learning regular expressions would be extremely boring. So I created a simple little program that will verify a hash by the length and output the most likely and least likely hash type. How it works, it will grab the argv from the user containing the hash, run it through a regex until it finds a match, if it finds a match, it will output all the hashes from most likely to least likely. If it does not find a match it will output that no match was found (this script is apart of a bigger tool, if you would like to see the whole tool to see how it works together see here):

import re
import sys

HASH_TYPE_REGEX = {
    re.compile(r"^[a-f0-9]{32}(:.+)?$", re.IGNORECASE):  ["MD5", "MD4", "MD2", "Double MD5",
                                                          "LM", "RIPEMD-128", "Haval-128",
                                                          "Tiger-128", "Skein-256(128)", "Skein-512(128",
                                                          "Lotus Notes/Domino 5", "Skype", "ZipMonster",
                                                          "PrestaShop"],
    re.compile(r"^[a-f0-9]{64}(:.+)?$", re.IGNORECASE):  ["SHA-256", "RIPEMD-256", "SHA3-256", "Haval-256",
                                                          "GOST R 34.11-94", "GOST CryptoPro S-Box",
                                                          "Skein-256", "Skein-512(256)", "Ventrilo"],
    re.compile(r"^[a-f0-9]{128}(:.+)?$", re.IGNORECASE): ["SHA-512", "Whirlpool", "Salsa10",
                                                          "Salsa20", "SHA3-512", "Skein-512",
                                                          "Skein-1024(512)"],
    re.compile(r"^[a-f0-9]{56}$", re.IGNORECASE):        ["SHA-224", "Haval-224", "SHA3-224",
                                                          "Skein-256(224)", "Skein-512(224)"],
    re.compile(r"^[a-f0-9]{40}(:.+)?$", re.IGNORECASE):  ["SHA-1", "Double SHA-1", "RIPEMD-160",
                                                          "Haval-160", "Tiger-160", "HAS-160",
                                                          "LinkedIn", "Skein-256(160)", "Skein-512(160)",
                                                          "MangoWeb Enhanced CMS"],
    re.compile(r"^[a-f0-9]{96}$", re.IGNORECASE):        ["SHA-384", "SHA3-384", "Skein-512(384)",
                                                          "Skein-1024(384)"],
    re.compile(r"^[a-f0-9]{16}$", re.IGNORECASE):        ["MySQL323", "DES(Oracle)", "Half MD5",
                                                          "Oracle 7-10g", "FNV-164", "CRC-64"],
    re.compile(r"^\*[a-f0-9]{40}$", re.IGNORECASE):      ["MySQL5.x", "MySQL4.1"],
    re.compile(r"^[a-f0-9]{48}$", re.IGNORECASE):        ["Haval-192", "Tiger-192", "SHA-1(Oracle)",
                                                          "XSHA (v10.4 - v10.6)"]
}


class HashChecker(object):

    def __init__(self, check_hash):
        self.hash = check_hash
        self.found = False

    def obtain_hash_type(self):
        for algorithm in HASH_TYPE_REGEX:
            if algorithm.match(self.hash):
                self.found = True
                self.enumerate_hash_types(HASH_TYPE_REGEX[algorithm])
        if self.found is False:
            error_message = "Unable to verify hash type "
            error_message += "for hash: '{}'. This could mean ".format(self.hash)
            error_message += "that this is not a valid hash, or that "
            error_message += "this hash is not supported by Pybelt "
            error_message += "yet. If you feel this should be supported "
            error_message += "make an issue regarding this hash."
            print(error_message)
            return

    @staticmethod
    def enumerate_hash_types(items):
        print("{} possible hash types found..".format(len(items)))
        count = 0
        for item in items:
            count += 1
            if count <= 3:
                print("\033[92m[*] Most likely possible hash type: {}\033[0m".format(item))
                if count == 3:
                    print("")
            else:
                print("\033[33m[*] Least likely possible hash type: {}\033[0m".format(item))


if __name__ == '__main__':
    print("Analyzing hash: {}".format(sys.argv[1]))
    HashChecker(str(sys.argv[1])).obtain_hash_type()

I would like some critique on what I've done, main points that I would like to focus on (feel free to critique everything):

  • Is this a reliable way to identify a hash?
  • Are there better ways to create hash identification?
  • Is there a way I could update the regular expressions to be more accurate?
  • Is this the correct use of a class in Python?
\$\endgroup\$
5
\$\begingroup\$

Main

You've written the code actually using your class behind the if __name__ ... test which is very good.

A few minor details can be improved though:

  • it would make your code easier to update and to understand if you were to store sys.argv[1] in a variable with a relevant name.

  • elements from sys.argv are already string so there is no point in calling str.

You don't need a class

You class has 2 (real) methods, one of which is init. This is the typical useless class Jack Diederich describes in his excellent talk "Stop writing classes". This could simply be a function.

Then your code would look like :

def obtain_hash_type(check_hash):
    found = False
    for algorithm in HASH_TYPE_REGEX:
        if algorithm.match(check_hash):
            found = True
            enumerate_hash_types(HASH_TYPE_REGEX[algorithm])
    if found is False:
        error_message = "Unable to verify hash type "
        error_message += "for hash: '{}'. This could mean ".format(check_hash)
        error_message += "that this is not a valid hash, or that "
        error_message += "this hash is not supported by Pybelt "
        error_message += "yet. If you feel this should be supported "
        error_message += "make an issue regarding this hash."
        print(error_message)
        return

def enumerate_hash_types(items):
    print("{} possible hash types found..".format(len(items)))
    count = 0
    for item in items:
        count += 1
        if count <= 3:
            print("\033[92m[*] Most likely possible hash type: {}\033[0m".format(item))
            if count == 3:
                print("")
        else:
            print("\033[33m[*] Least likely possible hash type: {}\033[0m".format(item))


if __name__ == '__main__':
    check_hash = "f34d2680ed88c51e95b0032087e0403a" # or sys.argv[1]
    print("Analyzing hash: {}".format(check_hash))
    obtain_hash_type(check_hash)

(It's probably a good idea to rename arguments and functions but I'll let you deal with this).

Useless return

Your return is useless.

Do not use is to check booleans

It is a bit more idiomatic to write if not found instead of if found is False.

Avoid string concatenations

Instead of using += again and again on your string, you can simply define a literal string on multiple lines. Also, if you do it directly in your call to print, the parenthesis will help you:

    print("Unable to verify hash type "
          "for hash: '{}'. This could mean "
          "that this is not a valid hash, or that "
          "this hash is not supported by Pybelt "
          "yet. If you feel this should be supported "
          "make an issue regarding this hash.".format(check_hash))

Iterate over a dictionnary key and values

You can use the items() or iteritems() to iterate over keys and values in the same time:

for algorithm, items in HASH_TYPE_REGEX.items():
    if algorithm.match(check_hash):
        found = True
        enumerate_hash_types(items)

Single source of information / magic numbers

In your enumerate_hash_types, the magic number 3 appears twice. Because the function is so short, it is obvious that if you change the first, you have to change the other one but it is a good practice to use a local variable/argument/global constant to store this value so that one does not need to change it in multiple places (and potentially forget a few of them).

Conclusion

I have to go, I might continue this review but at the moment, the code looks like:

def obtain_hash_type(check_hash):
    found = False
    for algorithm, items in HASH_TYPE_REGEX.items():
        if algorithm.match(check_hash):
            found = True
            enumerate_hash_types(items)
    if not found:
        print("Unable to verify hash type "
              "for hash: '{}'. This could mean "
              "that this is not a valid hash, or that "
              "this hash is not supported by Pybelt "
              "yet. If you feel this should be supported "
              "make an issue regarding this hash.".format(check_hash))

def enumerate_hash_types(items, nb_likeliest=3):
    print("{} possible hash types found..".format(len(items)))
    count = 0
    for item in items:
        count += 1
        if count <= nb_likeliest:
            print("\033[92m[*] Most likely possible hash type: {}\033[0m".format(item))
            if count == nb_likeliest:
                print("")
        else:
            print("\033[33m[*] Least likely possible hash type: {}\033[0m".format(item))


if __name__ == '__main__':
    check_hash = "f34d2680ed88c51e95b0032087e0403a" # or sys.argv[1]
    print("Analyzing hash: {}".format(check_hash))
    obtain_hash_type(check_hash)

Enumerate

As pointed out in comments, you could (and should) use enumerate to keep track of the current index during an iteration. In your case, because it makes sense to start from index 1, you can use the start=1 parameter.

def enumerate_hash_types(items, nb_likeliest=3):
    print("{} possible hash types found..".format(len(items)))
    for count, item in enumerate(items, start=1):
        if count <= nb_likeliest:
            print("\033[92m[*] Most likely possible hash type: {}\033[0m".format(item))
            if count == nb_likeliest:
                print("")
        else:
            print("\033[33m[*] Least likely possible hash type: {}\033[0m".format(item))

Small function

It may be a good idea to write a wrapper around re.compile so that you do not need to rewrite re.IGNORECASE so many times.

It may also be nice to have it add the ^ and $ automatically.

You'd get something like:

def compile_re(string):
    return re.compile(r"^" + string + r"$", re.IGNORECASE)

HASH_TYPE_REGEX = {
    compile_re(r"[a-f0-9]{32}(:.+)?"):  ["MD5", "MD4", "MD2", "Double MD5", "LM", "RIPEMD-128", "Haval-128", "Tiger-128", "Skein-256(128)", "Skein-512(128", "Lotus Notes/Domino 5", "Skype", "ZipMonster", "PrestaShop"],
    compile_re(r"[a-f0-9]{64}(:.+)?"):  ["SHA-256", "RIPEMD-256", "SHA3-256", "Haval-256", "GOST R 34.11-94", "GOST CryptoPro S-Box", "Skein-256", "Skein-512(256)", "Ventrilo"],
    compile_re(r"[a-f0-9]{128}(:.+)?"): ["SHA-512", "Whirlpool", "Salsa10", "Salsa20", "SHA3-512", "Skein-512", "Skein-1024(512)"],
    compile_re(r"[a-f0-9]{56}"):        ["SHA-224", "Haval-224", "SHA3-224", "Skein-256(224)", "Skein-512(224)"],
    compile_re(r"[a-f0-9]{40}(:.+)?"):  ["SHA-1", "Double SHA-1", "RIPEMD-160", "Haval-160", "Tiger-160", "HAS-160", "LinkedIn", "Skein-256(160)", "Skein-512(160)", "MangoWeb Enhanced CMS"],
    compile_re(r"[a-f0-9]{96}"):        ["SHA-384", "SHA3-384", "Skein-512(384)", "Skein-1024(384)"],
    compile_re(r"[a-f0-9]{16}"):        ["MySQL323", "DES(Oracle)", "Half MD5", "Oracle 7-10g", "FNV-164", "CRC-64"],
    compile_re(r"\*[a-f0-9]{40}"):      ["MySQL5.x", "MySQL4.1"],
    compile_re(r"[a-f0-9]{48}"):        ["Haval-192", "Tiger-192", "SHA-1(Oracle)", "XSHA (v10.4 - v10.6)"]
}

Now, you could go crazier and use the fact that all your regexps look the same to define a function to build them.

You'd get something like:

def build_hash_re(hex_len, prefix=r"", suffix=r""):
    string = r"^%s[a-f0-9]{%d}%s$" % (prefix, hex_len, suffix)
    return re.compile(string, re.IGNORECASE)


HASH_TYPE_REGEX = {
    build_hash_re(32, suffix=r"(:.+)?"):  ["MD5", "MD4", "MD2", "Double MD5", "LM", "RIPEMD-128", "Haval-128", "Tiger-128", "
    build_hash_re(64, suffix=r"(:.+)?"):  ["SHA-256", "RIPEMD-256", "SHA3-256", "Haval-256", "GOST R 34.11-94", "GOST CryptoP
    build_hash_re(128, suffix=r"(:.+)?"): ["SHA-512", "Whirlpool", "Salsa10", "Salsa20", "SHA3-512", "Skein-512", "Skein-1024
    build_hash_re(56):                    ["SHA-224", "Haval-224", "SHA3-224", "Skein-256(224)", "Skein-512(224)"],
    build_hash_re(40, suffix=r"(:.+)?"):  ["SHA-1", "Double SHA-1", "RIPEMD-160", "Haval-160", "Tiger-160", "HAS-160", "Linke
    build_hash_re(96):                    ["SHA-384", "SHA3-384", "Skein-512(384)", "Skein-1024(384)"],
    build_hash_re(16):                    ["MySQL323", "DES(Oracle)", "Half MD5", "Oracle 7-10g", "FNV-164", "CRC-64"],
    build_hash_re(40, prefix=r"\*"):      ["MySQL5.x", "MySQL4.1"],
    build_hash_re(48):                    ["Haval-192", "Tiger-192", "SHA-1(Oracle)", "XSHA (v10.4 - v10.6)"]
}
\$\endgroup\$
  • 2
    \$\begingroup\$ You could use enumerate instead of using and explicitly incrementing count. \$\endgroup\$ – Graipher Feb 10 '17 at 17:32
  • \$\begingroup\$ I watched that whole thing, great info and very valid points, thank you \$\endgroup\$ – secxit Feb 10 '17 at 18:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.