Search code examples
pythoncryptographysha256hmacmessage-authentication-code

Problem with calculating HMAC of a string with python


I am trying to write a program that calculates hmac of a string. I am allowed to use hashlib library but not hmac. Here is my code:

from hashlib import sha256

def string_to_decimal(string):
    res = ''
    for char in string:
        res += hex(ord(char))[2:]
    return int(res, 16)

def decimal_to_string(number):
    hex_number = hex(number)[2:]
    hex_number = hex_number[::-1]
    res = ''
    for i in range(0, len(hex_number), 2):
        if i + 1 < len(hex_number):
            res += chr(int(hex_number[i + 1] + hex_number[i], 16))
        else:
            res += chr(int(hex_number[i], 16))

    return res[::-1]

def calculate_ipad_opad(block_size, ipad, opad):
    res_ipad = 0
    res_opad = 0
    for i in range(block_size // 8):
        res_ipad = (res_ipad * 256) + ipad
        res_opad = (res_opad * 256) + opad
    return res_ipad, res_opad

def integer_to_bytes(number):
    res = list()
    while number > 0:
        res.append(number % 256)
        number //= 256
    
    return bytes(res[::-1])

block_size = 512

msg = 'The quick brown fox jumps over the lazy dog'
key = 'key'

ipad = 54
opad = 92

block_ipad, block_opad = calculate_ipad_opad(block_size, ipad, opad)

key_decimal = string_to_decimal(key)
si = key_decimal ^ block_ipad
so = key_decimal ^ block_opad

a = sha256(integer_to_bytes(so) + sha256(integer_to_bytes(si) + msg.encode()).digest())

print(a.digest())

I know the key length is not more than the block size. I used Wikipedia to write the code. But it doesn't work properly. Could you please help me with that???

EDIT: I expected the output of the code to be f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8. But the output is 3d6243123b984bcc17cb96eb61c2b47d27545c3a9119b623be7932e846bf0643.


Solution

  • The key must be padded with 0x00 values on the right side up to the block size of the digest (if the key size is smaller than the block size, which is the case here). You can achieve this by e.g. adding in string_to_decimal() just before the return:

    res = res.ljust(64 * 2, "0")
    

    With this change, the expected result is provided.

    For an implementation, it is better to use a more detailed specification of the algorithm, e.g. FIPS PUB 198-1 instead of the highly abbreviated description in Wikipedia. Furthermore, you are on the safe side if official test vectors are applied.


    The conversion to decimal numbers is actually not necessary, i.e. it is possible to work directly with bytes like objects, which simplifies the implementation considerably:

    from hashlib import sha256
    
    msg = 'The quick brown fox jumps over the lazy dog'
    key = 'key'
    
    block_size_bytes = 64
    
    block_opad_bytes = b'\x5c' * block_size_bytes
    block_ipad_bytes = b'\x36' * block_size_bytes
    key_bytes = key.encode().ljust(block_size_bytes, b'\0')
    
    si_bytes = bytes([a ^ b for (a, b) in zip(block_ipad_bytes, key_bytes)]) 
    so_bytes = bytes([a ^ b for (a, b) in zip(block_opad_bytes, key_bytes)]) 
    
    a = sha256(so_bytes + sha256(si_bytes + msg.encode()).digest())
    
    print(a.digest().hex()) # f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8