def bytes_to_hex(data):
    return data.hex()


def hex_to_bytes(hex_string):
    return bytes.fromhex(hex_string)


def pad_data(data, block_size=32):
    padding_len = block_size - (len(data) % block_size)
    if padding_len == 0:
        padding_len = block_size
    padding = bytes([padding_len] * padding_len)
    return data + padding


def unpad_data(data):
    if len(data) == 0:
        raise ValueError("empty data")
    padding_len = data[-1]
    if padding_len > len(data) or padding_len == 0:
        raise ValueError("invalid padding")
    for i in range(1, padding_len + 1):
        if data[-i] != padding_len:
            raise ValueError("invalid padding")
    return data[:-padding_len]


def xor_bytes(a, b):
    if len(a) != len(b):
        raise ValueError("byte arrays must have equal length")
    return bytes(x ^ y for x, y in zip(a, b))


def rotate_left_byte(val, shift):
    shift %= 8
    return ((val << shift) | (val >> (8 - shift))) & 0xff


def rotate_right_byte(val, shift):
    shift %= 8
    return ((val >> shift) | (val << (8 - shift))) & 0xff


def split_blocks(data, block_size=32):
    if len(data) % block_size != 0:
        raise ValueError(f"data length must be multiple of {block_size}")
    return [data[i:i + block_size] for i in range(0, len(data), block_size)]


def join_blocks(blocks):
    return b"".join(blocks)


def constant_time_compare(a, b):
    if len(a) != len(b):
        return False
    result = 0
    for x, y in zip(a, b):
        result |= x ^ y
    return result == 0
