From e063648ecfe9b876138e987c98179867010d8c35 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 29 Nov 2023 21:56:55 +0000 Subject: [PATCH] Fixes, tests passing. --- randquik/cha.py | 6 ++--- src/cha4ssse3.h | 16 +++++++----- src/cha8avx2.h | 10 +++++--- src/chacha20.h | 2 +- tests/test_chacha.py | 61 +++++++++++++++++++------------------------- 5 files changed, 45 insertions(+), 50 deletions(-) diff --git a/randquik/cha.py b/randquik/cha.py index 6b60722..88f16a7 100644 --- a/randquik/cha.py +++ b/randquik/cha.py @@ -70,7 +70,7 @@ def _processBuffer(out): class Cha: - def __init__(self, key: bytes | Any, iv: bytes | Any, *, rounds=8): + def __init__(self, key: bytes | Any, iv: bytes | Any, *, rounds=20): """Construct a generator that holds its internal state, moving forward on each call.""" key, iv = _processKeys(key, iv) self.ctx = ffi.new("cha_ctx*") @@ -91,7 +91,7 @@ def generate_into( key: bytes | Any, iv: bytes | Any = bytes(16), *, - rounds=8, + rounds=20, ): """Fill in random bytes into an existing array (buffer interface)""" key, iv = _processKeys(key, iv) @@ -100,7 +100,7 @@ def generate_into( return out -def generate(outlen: int, key: bytes | Any, iv: bytes | Any = bytes(16), *, rounds=8): +def generate(outlen: int, key: bytes | Any, iv: bytes | Any = bytes(16), *, rounds=20): """Return a bytearray of random bytes""" assert outlen >= 0 return generate_into(bytearray(outlen), key, iv, rounds=rounds) diff --git a/src/cha4ssse3.h b/src/cha4ssse3.h index 48d1641..19e9970 100644 --- a/src/cha4ssse3.h +++ b/src/cha4ssse3.h @@ -4,7 +4,7 @@ #elif defined(__aarch64__) #include "sse2neon.h" #endif - +#include // clang-format off #define VEC4_ROT(A, IMM) \ @@ -45,12 +45,12 @@ _mm_storeu_si128((__m128i*)(OUT + 192), x[D]); \ } -#define COUNTER_INCREMENT(a, b, c, d) \ +#define COUNTER_INCREMENT(addv) \ { \ - __m128i addv = _mm_set_epi32(d, c, b, a); \ + __m128i carry = orig[12]; \ orig[12] = _mm_add_epi32(orig[12], addv); \ - addv = _mm_srli_epi32(_mm_cmplt_epi32(orig[12], addv), 31); \ - orig[13] = _mm_add_epi32(orig[13], addv); \ + carry = _mm_srli_epi32(_mm_and_si128(_mm_xor_si128(orig[12], carry), carry), 31); \ + orig[13] = _mm_add_epi32(orig[13], carry); \ } static inline uint64_t @@ -63,7 +63,9 @@ _cha_4block(uint8_t* buf, size_t bufsize, uint32_t state[16], unsigned rounds) { // Load state to vectors, duplicate four times, only different counters __m128i orig[16]; for (unsigned i = 0; i < 16; ++i) orig[i] = _mm_set1_epi32(state[i]); - COUNTER_INCREMENT(0, 1, 2, 3); + __m128i addv = _mm_set_epi32(3, 2, 1, 0); + COUNTER_INCREMENT(addv); + addv = _mm_set1_epi32(4); const unsigned batches = bufsize / 256; for (unsigned b = batches; b-->0;) { __m128i x[16]; @@ -85,7 +87,7 @@ _cha_4block(uint8_t* buf, size_t bufsize, uint32_t state[16], unsigned rounds) { ONEQUAD(4, 5, 6, 7, buf + 16); ONEQUAD(8, 9, 10, 11, buf + 32); ONEQUAD(12, 13, 14, 15, buf + 48); - COUNTER_INCREMENT(4, 4, 4, 4); + COUNTER_INCREMENT(addv); buf += 256; } // Store counter diff --git a/src/cha8avx2.h b/src/cha8avx2.h index c9c6270..7b3636e 100644 --- a/src/cha8avx2.h +++ b/src/cha8avx2.h @@ -64,10 +64,12 @@ _mm256_storeu_si256((__m256i*)(c + 448), _mm256_permute2x128_si256(x[D], x[D2], 0x31)); \ } -#define COUNTER_INCREMENT(addv) \ - { \ - orig[12] = _mm256_add_epi32(orig[12], addv); \ - orig[13] = _mm256_add_epi32(orig[13], _mm256_srli_epi32(_mm256_cmpgt_epi32(addv, orig[12]), 31)); \ +#define COUNTER_INCREMENT(addv) \ + { \ + __m256i carry = orig[12]; \ + orig[12] = _mm256_add_epi32(orig[12], addv); \ + carry = _mm256_srli_epi32(_mm256_and_si256(_mm256_xor_si256(orig[12], carry), carry), 31); \ + orig[13] = _mm256_add_epi32(orig[13], carry); \ } static inline uint64_t diff --git a/src/chacha20.h b/src/chacha20.h index 7f01800..ea1db7d 100644 --- a/src/chacha20.h +++ b/src/chacha20.h @@ -86,7 +86,7 @@ void cha_seek_blocks(cha_ctx* ctx, int64_t offset) { /// @param ctx ChaCha context /// @param out output buffer /// @param outlen output buffer length -static inline void cha_update(cha_ctx* ctx, uint8_t* out, uint64_t outlen) { +void cha_update(cha_ctx* ctx, uint8_t* out, uint64_t outlen) { // The included header will mess with these variables uint8_t* end = out + outlen; if (ctx->offset) { diff --git a/tests/test_chacha.py b/tests/test_chacha.py index 0af824f..8e82258 100644 --- a/tests/test_chacha.py +++ b/tests/test_chacha.py @@ -1,44 +1,12 @@ from secrets import randbelow, token_bytes -import numpy as np +import pytest from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers.algorithms import ChaCha20 -from scipy.stats import chisquare from randquik import cha -def test_cha_generate_statistical(): - """Requests in multiple of ChaCha20 block size 64 bytes""" - ROUNDS = 100 - min_p = 1 - 0.99 ** (1 / ROUNDS) # 99 % confidence over the entire test - print(min_p) - - for round in range(ROUNDS): - # First round both zero, use 8-byte nonce as round counter - key = bytes(32) - nonce = round.to_bytes(8, "little") - iv = bytes(8) + nonce # Cryptography module requires IV (counter, nonce) - # Varying sizes to test internal processing that occurs in 64 bit blocks - # and with SIMD implementations also 256 or 512 bytes at a time. - N = 10000 + randbelow(10000) - ct0, ct1 = bytearray(N), bytearray(N) - Cipher(ChaCha20(key, iv), None, None).encryptor().update_into(bytes(N), ct0) - cha.generate(ct1, key, nonce) - assert len(ct0) == len(ct1) - assert ct0.hex() == ct1.hex() - assert ct1.count(0) - - # Zeroing out 50 bytes fail the test in ~30 rounds - # ct1[:50] = bytes(50) - - # Test that all byte values are equivalently common (despite zero inputs) - observed = np.bincount(ct1, minlength=256) - expected = np.full(256, N / 256) - chi2, p_value = chisquare(observed, expected) - assert p_value >= min_p, f"{round=} {N=}" - - def test_cipherstreams_fullblocks(): """Requests in multiple of ChaCha20 block size 64 bytes""" key = token_bytes(32) @@ -51,7 +19,30 @@ def test_cipherstreams_fullblocks(): ct0 = c0.update(bytes(N)) ct1 = c1(bytearray(N)) assert len(ct0) == len(ct1) - assert ct0.hex() == ct1.hex() + assert ct0.hex() == ct1.hex(), f"{i=} {N=}" + + +@pytest.mark.parametrize( + "counter", + [ + b"\x00\x00\x00\x00\x00\x00\x00\x00", + b"\xFF\xFF\xFF\xFF\x00\x00\x00\x00", + b"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + b"\xFF\xFF\xFF\x7F\xFF\xFF\xFF\xFF", + b"\x00\x00\x00\x80\xFF\xFF\xFF\xFF", + ], +) +def test_counter_wrap(counter): + """Tests carry handling of counter increments""" + key = bytes(32) + iv = counter + b"--------" + c0 = Cipher(ChaCha20(key, iv), None, None).encryptor() + c1 = cha.Cha(key, iv) + N = 128 + ct0 = c0.update(bytes(N)) + ct1 = c1(bytearray(N)) + assert len(ct0) == len(ct1) + assert ct0.hex() == ct1.hex() def test_cipherstreams_partial_updates(): @@ -66,7 +57,7 @@ def test_cipherstreams_partial_updates(): ct0 = c0.update(bytes(N)) ct1 = c1(bytearray(N)) assert len(ct0) == len(ct1) - assert ct0.hex() == ct1.hex() + assert ct0.hex() == ct1.hex(), f"{i=} {N=}" def test_cipherstreams_32leftover():