Using HMAC with AES modes that do NOT require padding I'm trying to use HMAC with AES modes that do not require any sort of padding.
Although I am aware that modes like AES-GCM and AES-EAX already provide
authentication, I want to know if HMAC is suitable with the modes that do not
provide any authentication. This is what I tried:
The code
from Cryptodome.Cipher import AES
import hmac


class DecryptionError(Exception):
    pass


class AESNonAEAD:
    def __init__(self, is_encrypting, key, mode, iv_or_nonce, digestmod='sha256'):
        self._is_encrypting = is_encrypting

        # Use KDF to derive key material from given key:
        # length of crp == length of key
        # length of authkey == (digest size of hashalgo for HMAC // 8)
        crpkey, authkey = derive_key_material(key, digestmod)

        self._auth = hmac.new(authkey, digestmod=digestmod)
        self._cipher = AES.new(key, mode, iv_or_nonce)
        if is_encrypting:
            self._update = self._cipher.encrypt
        else:
            self._update = self._cipher.decrypt

        self._auth.update(iv_or_nonce)

        self._updated = False
        self._len_ct = 0
        self._len_aad = 0

    def authenticate(self, data):
        if self._updated:
            raise ValueError('update has already been called')
        self._auth.update(data)
        self._len_aad += len(data)

    def _pad_aad(self):
        # pad to a multiple of 16 bytes
        if self._len_aad &amp; 0x0F:
            self._auth.update(bytes(16 - (self._len_aad &amp; 0x0F)))

    def update(self):
        if not self._updated:
            self._pad_aad()
            self._updated = True

        if not self._is_encrypting:
            self._auth.update(data)

        res = self._update(data)
        self._len_ct += len(res)

        if self._is_encrypting:
            self._auth.update(res)
        return res

    def finalize(self, tag=None):
        if not self._is_encrypting and tag is None:
            raise ValueError('tag is required')

        # pad to a multiple of 16 bytes
        if self._len_ct &amp; 0x0F:
           self._auth.update(bytes(16 - (self._len_ct &amp; 0x0F)))
        # include length of aad and ciphertext
        self._auth.update(self._len_aad.to_bytes(8, 'little'))
        self._auth.update(self._len_ct.to_bytes(8, 'little'))

        if self._is_encrypting:
            return
        if not hmac.compare_digest(tag, self._auth.digest()):
            raise DecryptionError

    def calcuate_tag(self):
        if self._is_encrypting:
            return self._hmac.digest()


Usage
Encryption
from Cryptodome.Cipher import AES

key = os.urandom(32)
iv = os.urandom(16)
enc = AESNonAEAD(True, key, AES.MODE_CFB, iv)  # or MODE_CTR

enc.authenticate(b'yes this')
encdata = enc.update(b'no not this')
enc.finalize()
tag = enc.calculate_tag()


Decryption
dec = AESNonAEAD(False, key, AES.MODE_CFB, iv)  # or MODE_CTR

dec.authenticate(b'yes this')
decdata = dec.update(encdata)


Questions:

Is this implementation acceptable?
Is this the proper way of using HMAC? Is it necessary to pad the HMAC for non padded cipher modes?
Can this very same scheme be used with Camellia cipher?
If I understand correctly, your input to HMAC is the ciphertext $c$, padded with null bytes (to a multiple of 16 bytes):
    # pad to a multiple of 16 bytes
    if self._len_ct &amp; 0x0F:
       self._auth.update(bytes(16 - (self._len_ct &amp; 0x0F)))

As a concrete example, suppose the ciphertext is $c=$ deadbeef00, then you will compute the HMAC tag as $t = $ HMAC(deadbeef0000...00), 16 bytes. But this is also a valid HMAC tag for different ciphertexts $c'=$ deadbeef as well as $c' =$ deadbeef000000 etc. I hope you can see why this breaks the authenticated encryption security property.
I'm not sure why you are padding/encoding the HMAC input in this way, but my guess is you are trying to &quot;serialize&quot; both the ciphertext and associated data into a single string that is given to HMAC.  This is a common pitfall for authentication: you want to authenticate a pair of things $(x,y)$, and so you authenticate the string $x \| y$. But many pairs $(x',y')$ get mapped to the same string  $x\|y$ (think of cases where the length of $x$, $y$ are variable)! In order to properly authenticate, you need an unambiguous encoding of the pair $(x,y)$ into a string. One good way would be to somehow include the length of $x$ as part of this encoding.
edit: also you never seem to use self._cipher?