ChaCha20 vector creation

This page documents the code that was used to generate the vectors to test the counter overflow behavior in ChaCha20 as well as code used to verify them against another implementation.

Creation

The following Python script was run to generate the vector files.

# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

import binascii
import struct

from cryptography.hazmat.primitives import ciphers
from cryptography.hazmat.primitives.ciphers import algorithms

_N_BLOCKS = [1, 1.5, 2, 2.5, 3]
_INITIAL_COUNTERS = [2**32 - 1, 2**64 - 1]


def _build_vectors():
    count = 0
    output = []
    key = "0" * 64
    nonce = "0" * 16
    for blocks in _N_BLOCKS:
        plaintext = binascii.unhexlify("0" * int(128 * blocks))
        for counter in _INITIAL_COUNTERS:
            full_nonce = struct.pack("<Q", counter) + binascii.unhexlify(nonce)
            cipher = ciphers.Cipher(
                algorithms.ChaCha20(binascii.unhexlify(key), full_nonce),
                None,
            )
            encryptor = cipher.encryptor()
            output.append(f"\nCOUNT = {count}")
            count += 1
            output.append(f"KEY = {key}")
            output.append(f"NONCE = {nonce}")
            output.append(f"INITIAL_BLOCK_COUNTER = {counter}")
            output.append(f"PLAINTEXT = {binascii.hexlify(plaintext)}")
            output.append(
                f"CIPHERTEXT = {binascii.hexlify(encryptor.update(plaintext))}"
            )
    return "\n".join(output)


def _write_file(data, filename):
    with open(filename, "w") as f:
        f.write(data)


if __name__ == "__main__":
    _write_file(_build_vectors(), "counter-overflow.txt")

Download link: generate_chacha20_overflow.py

Verification

The following Python script was used to verify the vectors. The counter overflow is handled manually to avoid relying on the same code that generated the vectors.

# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

import binascii
import math
import struct
from pathlib import Path

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from tests.utils import load_nist_vectors

BLOCK_SIZE = 64
MAX_COUNTER = 2**64 - 1


def encrypt(
    key: bytes, nonce: bytes, initial_block_counter: int, plaintext: bytes
) -> bytes:
    full_nonce = struct.pack("<Q", initial_block_counter) + nonce
    encryptor = Cipher(
        algorithms.ChaCha20(key, full_nonce), mode=None
    ).encryptor()

    plaintext_len_blocks = math.ceil(len(plaintext) / BLOCK_SIZE)
    blocks_until_overflow = MAX_COUNTER - initial_block_counter + 1

    if plaintext_len_blocks <= blocks_until_overflow:
        return binascii.hexlify(encryptor.update(plaintext))
    else:
        bytes_until_overflow = min(blocks_until_overflow * 64, len(plaintext))
        first_batch = binascii.hexlify(
            encryptor.update(plaintext[:bytes_until_overflow])
        )
        # We manually handle the overflow by resetting the counter to zero once
        # we surpass MAX_COUNTER blocks. This way we can check the vectors are
        # correct without relying on the same logic that generated them.
        full_nonce = struct.pack("<Q", 0) + nonce
        encryptor = Cipher(
            algorithms.ChaCha20(key, full_nonce), mode=None
        ).encryptor()
        second_batch = binascii.hexlify(
            encryptor.update(plaintext[bytes_until_overflow:])
        )
        return first_batch + second_batch


def verify_vectors(filename: Path):
    with open(filename) as f:
        vector_file = f.read().splitlines()

    vectors = load_nist_vectors(vector_file)
    for vector in vectors:
        key = binascii.unhexlify(vector["key"])
        nonce = binascii.unhexlify(vector["nonce"])
        ibc = int(vector["initial_block_counter"])
        pt = binascii.unhexlify(vector["plaintext"])

        computed_ct = encrypt(key, nonce, ibc, pt)

        assert computed_ct == vector["ciphertext"]


overflow_path = Path(
    "vectors/cryptography_vectors/ciphers/ChaCha20/counter-overflow.txt"
)
verify_vectors(overflow_path)

Download link: verify_chacha20_overflow.py