Python ChaCha20-Poly1305 Code Example (Online Runner)

Python ChaCha20-Poly1305 examples with key/nonce/AAD encoding and ciphertext+tag handling.

Online calculator: use the site ChaCha20-Poly1305 text tool.

Note: This snippet requires locally installed dependencies and will not run in the online runner.

Calculation method

ChaCha20-Poly1305 uses a 32-byte key, 12-byte nonce, optional AAD, and outputs ciphertext plus a 16-byte tag. The online tool concatenates ciphertext + tag; this helper mirrors that behavior.

Install the dependency first: pip install pycryptodome.

Implementation notes

  • Package: pycryptodome provides Crypto.Cipher.ChaCha20_Poly1305.
  • Implementation: ciphertext and the 16-byte tag are concatenated to match the tool output. The nonce is validated to be exactly 12 bytes.
  • Notes: never reuse a nonce with the same key. AAD must match on decrypt; verification fails with an exception if the tag is wrong.
python
from __future__ import annotations

import base64
from pathlib import Path
from typing import Literal

from Crypto.Cipher import ChaCha20_Poly1305

KeyEncoding = Literal["utf8", "hex"]
CipherEncoding = Literal["hex", "base64"]

KEY_SIZE = 32
NONCE_SIZE = 12
TAG_SIZE = 16


def _decode_value(value: str, encoding: KeyEncoding) -> bytes:
    return value.encode("utf-8") if encoding == "utf8" else bytes.fromhex(value)


def _encode_ciphertext(data: bytes, encoding: CipherEncoding) -> str:
    return data.hex() if encoding == "hex" else base64.b64encode(data).decode("ascii")


def _decode_ciphertext(value: str, encoding: CipherEncoding) -> bytes:
    return bytes.fromhex(value) if encoding == "hex" else base64.b64decode(value)


def _normalize_key(key: bytes) -> bytes:
    return key[:KEY_SIZE].ljust(KEY_SIZE, b"\x00")


def chacha20_poly1305_encrypt(
    plaintext: bytes,
    key: str,
    nonce: str,
    *,
    key_encoding: KeyEncoding = "hex",
    nonce_encoding: KeyEncoding = "hex",
    aad: str | None = None,
    aad_encoding: KeyEncoding = "utf8",
    output_encoding: CipherEncoding = "base64",
) -> str:
    key_bytes = _normalize_key(_decode_value(key, key_encoding))
    nonce_bytes = _decode_value(nonce, nonce_encoding)
    if len(nonce_bytes) != NONCE_SIZE:
        raise ValueError("Nonce must be 12 bytes")
    cipher = ChaCha20_Poly1305.new(key=key_bytes, nonce=nonce_bytes)
    if aad:
        cipher.update(_decode_value(aad, aad_encoding))
    ciphertext, tag = cipher.encrypt_and_digest(plaintext)
    combined = ciphertext + tag
    return _encode_ciphertext(combined, output_encoding)


def chacha20_poly1305_decrypt(
    combined: str,
    key: str,
    nonce: str,
    *,
    key_encoding: KeyEncoding = "hex",
    nonce_encoding: KeyEncoding = "hex",
    aad: str | None = None,
    aad_encoding: KeyEncoding = "utf8",
    input_encoding: CipherEncoding = "base64",
) -> bytes:
    key_bytes = _normalize_key(_decode_value(key, key_encoding))
    nonce_bytes = _decode_value(nonce, nonce_encoding)
    data = _decode_ciphertext(combined, input_encoding)
    if len(data) < TAG_SIZE:
        raise ValueError("Ciphertext must include the Poly1305 tag")
    ciphertext, tag = data[:-TAG_SIZE], data[-TAG_SIZE:]
    cipher = ChaCha20_Poly1305.new(key=key_bytes, nonce=nonce_bytes)
    if aad:
        cipher.update(_decode_value(aad, aad_encoding))
    return cipher.decrypt_and_verify(ciphertext, tag)


def chacha20_poly1305_file(path: Path, key: str, nonce: str, *, key_encoding: KeyEncoding = "hex", nonce_encoding: KeyEncoding = "hex", aad: str | None = None, aad_encoding: KeyEncoding = "utf8", output_encoding: CipherEncoding = "base64") -> str:
    return chacha20_poly1305_encrypt(path.read_bytes(), key, nonce, key_encoding=key_encoding, nonce_encoding=nonce_encoding, aad=aad, aad_encoding=aad_encoding, output_encoding=output_encoding)

# Example usage
key = "00" * 32
nonce = "01" * 12

payload = chacha20_poly1305_encrypt(
    b"hello",
    key,
    nonce,
    key_encoding="hex",
    nonce_encoding="hex",
    aad="context",
    aad_encoding="utf8",
    output_encoding="hex",
)
print(payload)

plain = chacha20_poly1305_decrypt(
    payload,
    key,
    nonce,
    key_encoding="hex",
    nonce_encoding="hex",
    aad="context",
    aad_encoding="utf8",
    input_encoding="hex",
)
print(plain.decode("utf-8"))

File encryption example

python
from pathlib import Path
import base64
import tempfile
from typing import Literal

from Crypto.Cipher import ChaCha20_Poly1305

KeyEncoding = Literal["utf8", "hex"]
CipherEncoding = Literal["hex", "base64"]

KEY_SIZE = 32
NONCE_SIZE = 12
TAG_SIZE = 16


def _decode_value(value: str, encoding: KeyEncoding) -> bytes:
    return value.encode("utf-8") if encoding == "utf8" else bytes.fromhex(value)


def _encode_ciphertext(data: bytes, encoding: CipherEncoding) -> str:
    return data.hex() if encoding == "hex" else base64.b64encode(data).decode("ascii")


def _normalize_key(key: bytes) -> bytes:
    return key[:KEY_SIZE].ljust(KEY_SIZE, b"\x00")


def chacha20_poly1305_file(
    path: Path,
    key: str,
    nonce: str,
    *,
    key_encoding: KeyEncoding = "hex",
    nonce_encoding: KeyEncoding = "hex",
    output_encoding: CipherEncoding = "base64",
) -> str:
    key_bytes = _normalize_key(_decode_value(key, key_encoding))
    nonce_bytes = _decode_value(nonce, nonce_encoding)
    if len(nonce_bytes) != NONCE_SIZE:
        raise ValueError("Nonce must be 12 bytes")
    cipher = ChaCha20_Poly1305.new(key=key_bytes, nonce=nonce_bytes)
    ciphertext, tag = cipher.encrypt_and_digest(path.read_bytes())
    return _encode_ciphertext(ciphertext + tag, output_encoding)


with tempfile.TemporaryDirectory() as temp_dir:
    sample_path = Path(temp_dir) / "sample.bin"
    sample_path.write_bytes(b"hello")
    encrypted = chacha20_poly1305_file(
        sample_path,
        key="00" * 32,
        nonce="01" * 12,
        key_encoding="hex",
        nonce_encoding="hex",
        output_encoding="hex",
    )
    print(encrypted)

Complete script (implementation + tests)

python
import base64
from typing import Literal

from Crypto.Cipher import ChaCha20_Poly1305

KeyEncoding = Literal["utf8", "hex"]
CipherEncoding = Literal["hex", "base64"]

KEY_SIZE = 32
NONCE_SIZE = 12
TAG_SIZE = 16


def _decode_value(value: str, encoding: KeyEncoding) -> bytes:
    return value.encode("utf-8") if encoding == "utf8" else bytes.fromhex(value)


def _encode_ciphertext(data: bytes, encoding: CipherEncoding) -> str:
    return data.hex() if encoding == "hex" else base64.b64encode(data).decode("ascii")


def _decode_ciphertext(value: str, encoding: CipherEncoding) -> bytes:
    return bytes.fromhex(value) if encoding == "hex" else base64.b64decode(value)


def _normalize_key(key: bytes) -> bytes:
    return key[:KEY_SIZE].ljust(KEY_SIZE, b"\x00")


def chacha20_poly1305_encrypt(
    plaintext: bytes,
    key: str,
    nonce: str,
    *,
    key_encoding: KeyEncoding = "hex",
    nonce_encoding: KeyEncoding = "hex",
    output_encoding: CipherEncoding = "base64",
) -> str:
    key_bytes = _normalize_key(_decode_value(key, key_encoding))
    nonce_bytes = _decode_value(nonce, nonce_encoding)
    cipher = ChaCha20_Poly1305.new(key=key_bytes, nonce=nonce_bytes)
    ciphertext, tag = cipher.encrypt_and_digest(plaintext)
    return _encode_ciphertext(ciphertext + tag, output_encoding)


def chacha20_poly1305_decrypt(
    combined: str,
    key: str,
    nonce: str,
    *,
    key_encoding: KeyEncoding = "hex",
    nonce_encoding: KeyEncoding = "hex",
    input_encoding: CipherEncoding = "base64",
) -> bytes:
    key_bytes = _normalize_key(_decode_value(key, key_encoding))
    nonce_bytes = _decode_value(nonce, nonce_encoding)
    data = _decode_ciphertext(combined, input_encoding)
    ciphertext, tag = data[:-TAG_SIZE], data[-TAG_SIZE:]
    cipher = ChaCha20_Poly1305.new(key=key_bytes, nonce=nonce_bytes)
    return cipher.decrypt_and_verify(ciphertext, tag)

def run_tests() -> None:
    key = "00" * 32
    nonce = "01" * 12
    combined = chacha20_poly1305_encrypt(b"hello", key, nonce, key_encoding="hex", nonce_encoding="hex", output_encoding="hex")
    recovered = chacha20_poly1305_decrypt(combined, key, nonce, key_encoding="hex", nonce_encoding="hex", input_encoding="hex")
    assert recovered == b"hello"
    print("ChaCha20-Poly1305 tests passed")


if __name__ == "__main__":
    run_tests()