From e28fb8ad9060b57bee7d18939b1bc0759f6b7392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 16 Jan 2024 14:31:19 +0100 Subject: [PATCH] Switch "crypto" implementation to Python This simplifies the build process and avoids needing to ship a vendor'd OpenSSL. --- .github/workflows/tests.yml | 15 +- scripts/fetch-vendor.json | 3 - scripts/fetch-vendor.py | 64 ------ setup.py | 10 - src/aioquic/_crypto.c | 416 ------------------------------------ src/aioquic/_crypto.py | 123 +++++++++++ src/aioquic/_crypto.pyi | 17 -- src/aioquic/quic/crypto.py | 3 +- 8 files changed, 126 insertions(+), 525 deletions(-) delete mode 100644 scripts/fetch-vendor.json delete mode 100644 scripts/fetch-vendor.py delete mode 100644 src/aioquic/_crypto.c create mode 100644 src/aioquic/_crypto.py delete mode 100644 src/aioquic/_crypto.pyi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 52231b585..111f158c8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,16 +65,6 @@ jobs: if: matrix.os == 'macos-latest' run: | sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off - echo "AIOQUIC_SKIP_TESTS=chacha20" >> $GITHUB_ENV - echo "CFLAGS=-I/usr/local/opt/openssl/include" >> $GITHUB_ENV - echo "LDFLAGS=-L/usr/local/opt/openssl/lib" >> $GITHUB_ENV - - name: Install OpenSSL - if: matrix.os == 'windows-latest' - run: | - choco install openssl --no-progress - echo "INCLUDE=C:\Progra~1\OpenSSL\include" >> $GITHUB_ENV - echo "LIB=C:\Progra~1\OpenSSL\lib" >> $GITHUB_ENV - shell: bash - name: Run tests run: | python -m pip install -U pip setuptools wheel @@ -134,10 +124,7 @@ jobs: - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} - CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py C:\cibw\vendor - CIBW_ENVIRONMENT: AIOQUIC_SKIP_TESTS=ipv6,loss CFLAGS=-I/tmp/vendor/include LDFLAGS=-L/tmp/vendor/lib - CIBW_ENVIRONMENT_WINDOWS: AIOQUIC_SKIP_TESTS=ipv6,loss INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib + CIBW_ENVIRONMENT: AIOQUIC_SKIP_TESTS=ipv6,loss CIBW_SKIP: cp37-* pp37-* *-musllinux* CIBW_TEST_COMMAND: python -m unittest discover -t {project} -s {project}/tests # there are no wheels for cryptography on these platforms diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json deleted file mode 100644 index 1ae2063b3..000000000 --- a/scripts/fetch-vendor.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "urls": ["https://github.com/aiortc/aioquic-openssl/releases/download/3.2.0-1/openssl-{platform}.tar.gz"] -} diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py deleted file mode 100644 index fe7b91c49..000000000 --- a/scripts/fetch-vendor.py +++ /dev/null @@ -1,64 +0,0 @@ -import argparse -import json -import logging -import os -import platform -import shutil -import struct -import subprocess - - -def get_platform(): - system = platform.system() - machine = platform.machine() - if system == "Linux": - return f"manylinux_{machine}" - elif system == "Darwin": - # cibuildwheel sets ARCHFLAGS: - # https://github.com/pypa/cibuildwheel/blob/5255155bc57eb6224354356df648dc42e31a0028/cibuildwheel/macos.py#L207-L220 - if "ARCHFLAGS" in os.environ: - machine = os.environ["ARCHFLAGS"].split()[1] - return f"macosx_{machine}" - elif system == "Windows": - if struct.calcsize("P") * 8 == 64: - return "win_amd64" - else: - return "win32" - else: - raise Exception(f"Unsupported system {system}") - - -parser = argparse.ArgumentParser(description="Fetch and extract tarballs") -parser.add_argument("destination_dir") -parser.add_argument("--cache-dir", default="tarballs") -parser.add_argument("--config-file", default=os.path.splitext(__file__)[0] + ".json") -args = parser.parse_args() -logging.basicConfig(level=logging.INFO) - -# read config file -with open(args.config_file, "r") as fp: - config = json.load(fp) - -# create fresh destination directory -logging.info("Creating directory %s" % args.destination_dir) -if os.path.exists(args.destination_dir): - shutil.rmtree(args.destination_dir) -os.makedirs(args.destination_dir) - -for url_template in config["urls"]: - tarball_url = url_template.replace("{platform}", get_platform()) - - # download tarball - tarball_name = tarball_url.split("/")[-1] - tarball_file = os.path.join(args.cache_dir, tarball_name) - if not os.path.exists(tarball_file): - logging.info("Downloading %s" % tarball_url) - if not os.path.exists(args.cache_dir): - os.mkdir(args.cache_dir) - subprocess.check_call( - ["curl", "--location", "--output", tarball_file, "--silent", tarball_url] - ) - - # extract tarball - logging.info("Extracting %s" % tarball_name) - subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) diff --git a/setup.py b/setup.py index 399fb954f..c3bf95800 100644 --- a/setup.py +++ b/setup.py @@ -5,10 +5,8 @@ if sys.platform == "win32": extra_compile_args = [] - libraries = ["libcrypto", "advapi32", "crypt32", "gdi32", "user32", "ws2_32"] else: extra_compile_args = ["-std=c99"] - libraries = ["crypto"] class bdist_wheel_abi3(bdist_wheel): @@ -30,14 +28,6 @@ def get_tag(self): define_macros=[("Py_LIMITED_API", "0x03080000")], py_limited_api=True, ), - setuptools.Extension( - "aioquic._crypto", - extra_compile_args=extra_compile_args, - libraries=libraries, - sources=["src/aioquic/_crypto.c"], - define_macros=[("Py_LIMITED_API", "0x03080000")], - py_limited_api=True, - ), ], cmdclass={"bdist_wheel": bdist_wheel_abi3}, ) diff --git a/src/aioquic/_crypto.c b/src/aioquic/_crypto.c deleted file mode 100644 index b03c38db3..000000000 --- a/src/aioquic/_crypto.c +++ /dev/null @@ -1,416 +0,0 @@ -#define PY_SSIZE_T_CLEAN - -#include -#include -#include - -#define MODULE_NAME "aioquic._crypto" - -#define AEAD_KEY_LENGTH_MAX 32 -#define AEAD_NONCE_LENGTH 12 -#define AEAD_TAG_LENGTH 16 - -#define PACKET_LENGTH_MAX 1500 -#define PACKET_NUMBER_LENGTH_MAX 4 -#define SAMPLE_LENGTH 16 - -#define CHECK_RESULT(expr) \ - if (!(expr)) { \ - ERR_clear_error(); \ - PyErr_SetString(CryptoError, "OpenSSL call failed"); \ - return NULL; \ - } - -#define CHECK_RESULT_CTOR(expr) \ - if (!(expr)) { \ - ERR_clear_error(); \ - PyErr_SetString(CryptoError, "OpenSSL call failed"); \ - return -1; \ - } - -static PyObject *CryptoError; - -/* AEAD */ - -typedef struct { - PyObject_HEAD - EVP_CIPHER_CTX *decrypt_ctx; - EVP_CIPHER_CTX *encrypt_ctx; - unsigned char buffer[PACKET_LENGTH_MAX]; - unsigned char key[AEAD_KEY_LENGTH_MAX]; - unsigned char iv[AEAD_NONCE_LENGTH]; - unsigned char nonce[AEAD_NONCE_LENGTH]; -} AEADObject; - -static PyObject *AEADType; - -static EVP_CIPHER_CTX * -create_ctx(const EVP_CIPHER *cipher, int key_length, int operation) -{ - EVP_CIPHER_CTX *ctx; - int res; - - ctx = EVP_CIPHER_CTX_new(); - CHECK_RESULT(ctx != 0); - - res = EVP_CipherInit_ex(ctx, cipher, NULL, NULL, NULL, operation); - CHECK_RESULT(res != 0); - - res = EVP_CIPHER_CTX_set_key_length(ctx, key_length); - CHECK_RESULT(res != 0); - - res = EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_CCM_SET_IVLEN, AEAD_NONCE_LENGTH, NULL); - CHECK_RESULT(res != 0); - - return ctx; -} - -static int -AEAD_init(AEADObject *self, PyObject *args, PyObject *kwargs) -{ - const char *cipher_name; - const unsigned char *key, *iv; - Py_ssize_t cipher_name_len, key_len, iv_len; - - if (!PyArg_ParseTuple(args, "y#y#y#", &cipher_name, &cipher_name_len, &key, &key_len, &iv, &iv_len)) - return -1; - - const EVP_CIPHER *evp_cipher = EVP_get_cipherbyname(cipher_name); - if (evp_cipher == 0) { - PyErr_Format(CryptoError, "Invalid cipher name: %s", cipher_name); - return -1; - } - if (key_len > AEAD_KEY_LENGTH_MAX) { - PyErr_SetString(CryptoError, "Invalid key length"); - return -1; - } - if (iv_len > AEAD_NONCE_LENGTH) { - PyErr_SetString(CryptoError, "Invalid iv length"); - return -1; - } - - memcpy(self->key, key, key_len); - memcpy(self->iv, iv, iv_len); - - self->decrypt_ctx = create_ctx(evp_cipher, key_len, 0); - CHECK_RESULT_CTOR(self->decrypt_ctx != 0); - - self->encrypt_ctx = create_ctx(evp_cipher, key_len, 1); - CHECK_RESULT_CTOR(self->encrypt_ctx != 0); - - return 0; -} - -static void -AEAD_dealloc(AEADObject *self) -{ - EVP_CIPHER_CTX_free(self->decrypt_ctx); - EVP_CIPHER_CTX_free(self->encrypt_ctx); - PyTypeObject *tp = Py_TYPE(self); - freefunc free = PyType_GetSlot(tp, Py_tp_free); - free(self); - Py_DECREF(tp); -} - -static PyObject* -AEAD_decrypt(AEADObject *self, PyObject *args) -{ - const unsigned char *data, *associated; - Py_ssize_t data_len, associated_len; - int outlen, outlen2, res; - uint64_t pn; - - if (!PyArg_ParseTuple(args, "y#y#K", &data, &data_len, &associated, &associated_len, &pn)) - return NULL; - - if (data_len < AEAD_TAG_LENGTH || data_len > PACKET_LENGTH_MAX) { - PyErr_SetString(CryptoError, "Invalid payload length"); - return NULL; - } - - memcpy(self->nonce, self->iv, AEAD_NONCE_LENGTH); - for (int i = 0; i < 8; ++i) { - self->nonce[AEAD_NONCE_LENGTH - 1 - i] ^= (uint8_t)(pn >> 8 * i); - } - - res = EVP_CIPHER_CTX_ctrl(self->decrypt_ctx, EVP_CTRL_CCM_SET_TAG, AEAD_TAG_LENGTH, (void*)(data + (data_len - AEAD_TAG_LENGTH))); - CHECK_RESULT(res != 0); - - res = EVP_CipherInit_ex(self->decrypt_ctx, NULL, NULL, self->key, self->nonce, 0); - CHECK_RESULT(res != 0); - - res = EVP_CipherUpdate(self->decrypt_ctx, NULL, &outlen, associated, associated_len); - CHECK_RESULT(res != 0); - - res = EVP_CipherUpdate(self->decrypt_ctx, self->buffer, &outlen, data, data_len - AEAD_TAG_LENGTH); - CHECK_RESULT(res != 0); - - res = EVP_CipherFinal_ex(self->decrypt_ctx, NULL, &outlen2); - if (res == 0) { - PyErr_SetString(CryptoError, "Payload decryption failed"); - return NULL; - } - - return PyBytes_FromStringAndSize((const char*)self->buffer, outlen); -} - -static PyObject* -AEAD_encrypt(AEADObject *self, PyObject *args) -{ - const unsigned char *data, *associated; - Py_ssize_t data_len, associated_len; - int outlen, outlen2, res; - uint64_t pn; - - if (!PyArg_ParseTuple(args, "y#y#K", &data, &data_len, &associated, &associated_len, &pn)) - return NULL; - - if (data_len > PACKET_LENGTH_MAX) { - PyErr_SetString(CryptoError, "Invalid payload length"); - return NULL; - } - - memcpy(self->nonce, self->iv, AEAD_NONCE_LENGTH); - for (int i = 0; i < 8; ++i) { - self->nonce[AEAD_NONCE_LENGTH - 1 - i] ^= (uint8_t)(pn >> 8 * i); - } - - res = EVP_CipherInit_ex(self->encrypt_ctx, NULL, NULL, self->key, self->nonce, 1); - CHECK_RESULT(res != 0); - - res = EVP_CipherUpdate(self->encrypt_ctx, NULL, &outlen, associated, associated_len); - CHECK_RESULT(res != 0); - - res = EVP_CipherUpdate(self->encrypt_ctx, self->buffer, &outlen, data, data_len); - CHECK_RESULT(res != 0); - - res = EVP_CipherFinal_ex(self->encrypt_ctx, NULL, &outlen2); - CHECK_RESULT(res != 0 && outlen2 == 0); - - res = EVP_CIPHER_CTX_ctrl(self->encrypt_ctx, EVP_CTRL_CCM_GET_TAG, AEAD_TAG_LENGTH, self->buffer + outlen); - CHECK_RESULT(res != 0); - - return PyBytes_FromStringAndSize((const char*)self->buffer, outlen + AEAD_TAG_LENGTH); -} - -static PyMethodDef AEAD_methods[] = { - {"decrypt", (PyCFunction)AEAD_decrypt, METH_VARARGS, ""}, - {"encrypt", (PyCFunction)AEAD_encrypt, METH_VARARGS, ""}, - - {NULL} -}; - -static PyType_Slot AEADType_slots[] = { - {Py_tp_dealloc, AEAD_dealloc}, - {Py_tp_methods, AEAD_methods}, - {Py_tp_doc, "AEAD objects"}, - {Py_tp_init, AEAD_init}, - {0, 0}, -}; - -static PyType_Spec AEADType_spec = { - MODULE_NAME ".AEADType", - sizeof(AEADObject), - 0, - Py_TPFLAGS_DEFAULT, - AEADType_slots -}; - -/* HeaderProtection */ - -typedef struct { - PyObject_HEAD - EVP_CIPHER_CTX *ctx; - int is_chacha20; - unsigned char buffer[PACKET_LENGTH_MAX]; - unsigned char mask[31]; - unsigned char zero[5]; -} HeaderProtectionObject; - -static PyObject *HeaderProtectionType; - -static int -HeaderProtection_init(HeaderProtectionObject *self, PyObject *args, PyObject *kwargs) -{ - const char *cipher_name; - const unsigned char *key; - Py_ssize_t cipher_name_len, key_len; - int res; - - if (!PyArg_ParseTuple(args, "y#y#", &cipher_name, &cipher_name_len, &key, &key_len)) - return -1; - - const EVP_CIPHER *evp_cipher = EVP_get_cipherbyname(cipher_name); - if (evp_cipher == 0) { - PyErr_Format(CryptoError, "Invalid cipher name: %s", cipher_name); - return -1; - } - - memset(self->mask, 0, sizeof(self->mask)); - memset(self->zero, 0, sizeof(self->zero)); - self->is_chacha20 = cipher_name_len == 8 && memcmp(cipher_name, "chacha20", 8) == 0; - - self->ctx = EVP_CIPHER_CTX_new(); - CHECK_RESULT_CTOR(self->ctx != 0); - - res = EVP_CipherInit_ex(self->ctx, evp_cipher, NULL, NULL, NULL, 1); - CHECK_RESULT_CTOR(res != 0); - - res = EVP_CIPHER_CTX_set_key_length(self->ctx, key_len); - CHECK_RESULT_CTOR(res != 0); - - res = EVP_CipherInit_ex(self->ctx, NULL, NULL, key, NULL, 1); - CHECK_RESULT_CTOR(res != 0); - - return 0; -} - -static void -HeaderProtection_dealloc(HeaderProtectionObject *self) -{ - EVP_CIPHER_CTX_free(self->ctx); - PyTypeObject *tp = Py_TYPE(self); - freefunc free = PyType_GetSlot(tp, Py_tp_free); - free(self); - Py_DECREF(tp); -} - -static int HeaderProtection_mask(HeaderProtectionObject *self, const unsigned char* sample) -{ - int outlen; - if (self->is_chacha20) { - return EVP_CipherInit_ex(self->ctx, NULL, NULL, NULL, sample, 1) && - EVP_CipherUpdate(self->ctx, self->mask, &outlen, self->zero, sizeof(self->zero)); - } else { - return EVP_CipherUpdate(self->ctx, self->mask, &outlen, sample, SAMPLE_LENGTH); - } -} - -static PyObject* -HeaderProtection_apply(HeaderProtectionObject *self, PyObject *args) -{ - const unsigned char *header, *payload; - Py_ssize_t header_len, payload_len; - int res; - - if (!PyArg_ParseTuple(args, "y#y#", &header, &header_len, &payload, &payload_len)) - return NULL; - - int pn_length = (header[0] & 0x03) + 1; - int pn_offset = header_len - pn_length; - - res = HeaderProtection_mask(self, payload + PACKET_NUMBER_LENGTH_MAX - pn_length); - CHECK_RESULT(res != 0); - - memcpy(self->buffer, header, header_len); - memcpy(self->buffer + header_len, payload, payload_len); - - if (self->buffer[0] & 0x80) { - self->buffer[0] ^= self->mask[0] & 0x0F; - } else { - self->buffer[0] ^= self->mask[0] & 0x1F; - } - - for (int i = 0; i < pn_length; ++i) { - self->buffer[pn_offset + i] ^= self->mask[1 + i]; - } - - return PyBytes_FromStringAndSize((const char*)self->buffer, header_len + payload_len); -} - -static PyObject* -HeaderProtection_remove(HeaderProtectionObject *self, PyObject *args) -{ - const unsigned char *packet; - Py_ssize_t packet_len; - int pn_offset, res; - - if (!PyArg_ParseTuple(args, "y#I", &packet, &packet_len, &pn_offset)) - return NULL; - - res = HeaderProtection_mask(self, packet + pn_offset + PACKET_NUMBER_LENGTH_MAX); - CHECK_RESULT(res != 0); - - memcpy(self->buffer, packet, pn_offset + PACKET_NUMBER_LENGTH_MAX); - - if (self->buffer[0] & 0x80) { - self->buffer[0] ^= self->mask[0] & 0x0F; - } else { - self->buffer[0] ^= self->mask[0] & 0x1F; - } - - int pn_length = (self->buffer[0] & 0x03) + 1; - uint32_t pn_truncated = 0; - for (int i = 0; i < pn_length; ++i) { - self->buffer[pn_offset + i] ^= self->mask[1 + i]; - pn_truncated = self->buffer[pn_offset + i] | (pn_truncated << 8); - } - - return Py_BuildValue("y#i", self->buffer, pn_offset + pn_length, pn_truncated); -} - -static PyMethodDef HeaderProtection_methods[] = { - {"apply", (PyCFunction)HeaderProtection_apply, METH_VARARGS, ""}, - {"remove", (PyCFunction)HeaderProtection_remove, METH_VARARGS, ""}, - {NULL} -}; - -static PyType_Slot HeaderProtectionType_slots[] = { - {Py_tp_dealloc, HeaderProtection_dealloc}, - {Py_tp_methods, HeaderProtection_methods}, - {Py_tp_doc, "HeaderProtection objects"}, - {Py_tp_init, HeaderProtection_init}, - {0, 0}, -}; - -static PyType_Spec HeaderProtectionType_spec = { - MODULE_NAME ".HeaderProtectionType", - sizeof(HeaderProtectionObject), - 0, - Py_TPFLAGS_DEFAULT, - HeaderProtectionType_slots -}; - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - MODULE_NAME, /* m_name */ - "Cryptography utilities.", /* m_doc */ - -1, /* m_size */ - NULL, /* m_methods */ - NULL, /* m_reload */ - NULL, /* m_traverse */ - NULL, /* m_clear */ - NULL, /* m_free */ -}; - -PyMODINIT_FUNC -PyInit__crypto(void) -{ - PyObject* m; - - m = PyModule_Create(&moduledef); - if (m == NULL) - return NULL; - - CryptoError = PyErr_NewException(MODULE_NAME ".CryptoError", PyExc_ValueError, NULL); - Py_INCREF(CryptoError); - PyModule_AddObject(m, "CryptoError", CryptoError); - - AEADType = PyType_FromSpec(&AEADType_spec); - if (AEADType == NULL) - return NULL; - PyModule_AddObject(m, "AEAD", AEADType); - - HeaderProtectionType = PyType_FromSpec(&HeaderProtectionType_spec); - if (HeaderProtectionType == NULL) - return NULL; - PyModule_AddObject(m, "HeaderProtection", HeaderProtectionType); - - // ensure required ciphers are initialised - EVP_add_cipher(EVP_aes_128_ecb()); - EVP_add_cipher(EVP_aes_128_gcm()); - EVP_add_cipher(EVP_aes_256_ecb()); - EVP_add_cipher(EVP_aes_256_gcm()); - - return m; -} diff --git a/src/aioquic/_crypto.py b/src/aioquic/_crypto.py new file mode 100644 index 000000000..c3832659b --- /dev/null +++ b/src/aioquic/_crypto.py @@ -0,0 +1,123 @@ +import struct +from typing import Tuple, Union + +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives.ciphers import ( + Cipher, + aead, + algorithms, + modes, +) + +AEAD_NONCE_LENGTH = 12 +AEAD_TAG_LENGTH = 16 + +CHACHA20_ZEROS = bytes(5) +PACKET_NUMBER_LENGTH_MAX = 4 +SAMPLE_LENGTH = 16 + + +class CryptoError(ValueError): + pass + + +class AEAD: + _aead: Union[aead.AESGCM, aead.ChaCha20Poly1305] + + def __init__(self, cipher_name: bytes, key: bytes, iv: bytes): + assert cipher_name in (b"aes-128-gcm", b"aes-256-gcm", b"chacha20-poly1305") + assert len(iv) == AEAD_NONCE_LENGTH + + if cipher_name == b"chacha20-poly1305": + self._aead = aead.ChaCha20Poly1305(key) + else: + self._aead = aead.AESGCM(key) + self._iv = iv + + def decrypt(self, data: bytes, associated_data: bytes, packet_number: int) -> bytes: + try: + return self._aead.decrypt( + self._nonce(packet_number), + data, + associated_data, + ) + except InvalidTag as exc: + raise CryptoError(str(exc)) + + def encrypt(self, data: bytes, associated_data: bytes, packet_number: int) -> bytes: + return self._aead.encrypt( + self._nonce(packet_number), + data, + associated_data, + ) + + def _nonce(self, packet_number: int) -> bytes: + return self._iv[0:4] + struct.pack( + ">Q", struct.unpack(">Q", self._iv[4:12])[0] ^ packet_number + ) + + +class HeaderProtection: + def __init__(self, cipher_name: bytes, key: bytes): + assert cipher_name in (b"aes-128-ecb", b"aes-256-ecb", b"chacha20") + + if cipher_name == b"chacha20": + self._encryptor = None + else: + self._encryptor = Cipher( + algorithm=algorithms.AES(key), + mode=modes.ECB(), + ).encryptor() + + self._key = key + + def apply(self, plain_header: bytes, protected_payload: bytes) -> bytes: + pn_length = (plain_header[0] & 0x03) + 1 + pn_offset = len(plain_header) - pn_length + + sample_offset = PACKET_NUMBER_LENGTH_MAX - pn_length + mask = self._mask( + protected_payload[sample_offset : sample_offset + SAMPLE_LENGTH] + ) + + buffer = bytearray(plain_header + protected_payload) + if buffer[0] & 0x80: + buffer[0] ^= mask[0] & 0x0F + else: + buffer[0] ^= mask[0] & 0x1F + + for i in range(pn_length): + buffer[pn_offset + i] ^= mask[1 + i] + + return bytes(buffer) + + def remove(self, packet: bytes, pn_offset: int) -> Tuple[bytes, int]: + sample_offset = pn_offset + PACKET_NUMBER_LENGTH_MAX + mask = self._mask(packet[sample_offset : sample_offset + SAMPLE_LENGTH]) + + buffer = bytearray(packet) + if buffer[0] & 0x80: + buffer[0] ^= mask[0] & 0x0F + else: + buffer[0] ^= mask[0] & 0x1F + + pn_length = (buffer[0] & 0x03) + 1 + pn_truncated = 0 + for i in range(pn_length): + buffer[pn_offset + i] ^= mask[1 + i] + pn_truncated = buffer[pn_offset + i] | (pn_truncated << 8) + + return bytes(buffer[: pn_offset + pn_length]), pn_truncated + + def _mask(self, sample: bytes) -> bytes: + if self._encryptor is None: + return ( + Cipher( + algorithm=algorithms.ChaCha20(self._key, sample), + mode=None, + ) + .encryptor() + .update(CHACHA20_ZEROS) + ) + else: + return self._encryptor.update(sample) diff --git a/src/aioquic/_crypto.pyi b/src/aioquic/_crypto.pyi deleted file mode 100644 index 32c5230d9..000000000 --- a/src/aioquic/_crypto.pyi +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Tuple - -class AEAD: - def __init__(self, cipher_name: bytes, key: bytes, iv: bytes): ... - def decrypt( - self, data: bytes, associated_data: bytes, packet_number: int - ) -> bytes: ... - def encrypt( - self, data: bytes, associated_data: bytes, packet_number: int - ) -> bytes: ... - -class CryptoError(ValueError): ... - -class HeaderProtection: - def __init__(self, cipher_name: bytes, key: bytes): ... - def apply(self, plain_header: bytes, protected_payload: bytes) -> bytes: ... - def remove(self, packet: bytes, encrypted_offset: int) -> Tuple[bytes, int]: ... diff --git a/src/aioquic/quic/crypto.py b/src/aioquic/quic/crypto.py index 013c69fc8..a9e97a678 100644 --- a/src/aioquic/quic/crypto.py +++ b/src/aioquic/quic/crypto.py @@ -13,6 +13,7 @@ INITIAL_CIPHER_SUITE = CipherSuite.AES_128_GCM_SHA256 INITIAL_SALT_DRAFT_29 = binascii.unhexlify("afbfec289993d24c9e9786f19c6111e04390a899") INITIAL_SALT_VERSION_1 = binascii.unhexlify("38762cf7f55934b34d179ae6a4c80cadccbb7f0a") +IV_SIZE = 12 SAMPLE_SIZE = 16 @@ -40,7 +41,7 @@ def derive_key_iv_hp( key_size = 16 return ( hkdf_expand_label(algorithm, secret, b"quic key", b"", key_size), - hkdf_expand_label(algorithm, secret, b"quic iv", b"", 12), + hkdf_expand_label(algorithm, secret, b"quic iv", b"", IV_SIZE), hkdf_expand_label(algorithm, secret, b"quic hp", b"", key_size), )