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:
pycryptodomeprovidesCrypto.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()