def _encrypt_via_chacha20_poly1305(plaintext: bytes,
                                   key: bytes,
                                   aad: bytes = b"header") -> dict:
    """Encrypt a bytestring with the stream cipher ChaCha20.

    Additional cleartext data can be provided so that the
    generated mac tag also verifies its integrity.

    :param plaintext: the bytes to cipher
    :param key: 32 bytes long cryptographic key
    :param aad: optional "additional authenticated data"

    :return: dict with fields "ciphertext", "tag", "nonce" and "header" as bytestrings"""
    _check_symmetric_key_length_bytes(len(key))
    cipher = ChaCha20_Poly1305.new(key=key)
    cipher.update(aad)
    ciphertext, tag = cipher.encrypt_and_digest(plaintext)
    nonce = cipher.nonce
    encryption = {
        "ciphertext": ciphertext,
        "tag": tag,
        "nonce": nonce,
        "aad": aad
    }
    return encryption
def _decrypt_via_aes_eax(cipherdict: dict, key: bytes) -> bytes:
    """Decrypt a bytestring using AES (EAX mode).

    :param cipherdict: dict with fields "ciphertext", "tag" and "nonce" as bytestrings
    :param key: the cryptographic key used to decipher

    :return: the decrypted bytestring"""
    _check_symmetric_key_length_bytes(len(key))
    decipher = AES.new(key, AES.MODE_EAX, nonce=cipherdict["nonce"])
    plaintext = decipher.decrypt(cipherdict["ciphertext"])
    decipher.verify(cipherdict["tag"])
    return plaintext
def _decrypt_via_aes_cbc(cipherdict: dict, key: bytes) -> bytes:
    """Decrypt a bytestring using AES (CBC mode).

    :param cipherdict: dict with fields "iv" and "ciphertext" as bytestrings
    :param key: the cryptographic key used to decipher

    :return: the decrypted bytestring"""
    _check_symmetric_key_length_bytes(len(key))
    iv = cipherdict["iv"]
    ciphertext = cipherdict["ciphertext"]
    decipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = unpad(decipher.decrypt(ciphertext), block_size=AES.block_size)
    return plaintext
def _encrypt_via_aes_eax(plaintext: bytes, key: bytes) -> dict:
    """Encrypt a bytestring using AES (EAX mode).

    :param plaintext: the bytes to cipher
    :param key: AES cryptographic key. It must be 16, 24 or 32 bytes long
        (respectively for *AES-128*, *AES-192* or *AES-256*).

    :return: dict with fields "ciphertext", "tag" and "nonce" as bytestrings"""
    _check_symmetric_key_length_bytes(len(key))
    cipher = AES.new(key, AES.MODE_EAX)
    nonce = cipher.nonce
    ciphertext, tag = cipher.encrypt_and_digest(plaintext)
    cipherdict = {"ciphertext": ciphertext, "tag": tag, "nonce": nonce}
    return cipherdict
def _encrypt_via_aes_cbc(plaintext: bytes, key: bytes) -> dict:
    """Encrypt a bytestring using AES (CBC mode).

    :param plaintext: the bytes to cipher
    :param key: AES cryptographic key. It must be 16, 24 or 32 bytes long
        (respectively for *AES-128*, *AES-192* or *AES-256*).

    :return: dict with fields "iv" and "ciphertext" as bytestrings"""
    _check_symmetric_key_length_bytes(len(key))
    iv = get_random_bytes(AES.block_size)
    cipher = AES.new(key, AES.MODE_CBC, iv)
    ciphertext = cipher.encrypt(pad(plaintext, block_size=AES.block_size))
    cipherdict = {"iv": iv, "ciphertext": ciphertext}
    return cipherdict
def _decrypt_via_chacha20_poly1305(cipherdict: dict, key: bytes) -> bytes:
    """Decrypt a bytestring with the stream cipher ChaCha20.

    :param cipherdict: dict with fields "ciphertext", "tag", "nonce" and "header" as bytestrings
    :param key: the cryptographic key used to decipher

    :return: the decrypted bytestring"""
    _check_symmetric_key_length_bytes(len(key))
    decipher = ChaCha20_Poly1305.new(key=key, nonce=cipherdict["nonce"])
    decipher.update(cipherdict["aad"])
    plaintext = decipher.decrypt_and_verify(
        ciphertext=cipherdict["ciphertext"],
        received_mac_tag=cipherdict["tag"])
    return plaintext