diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..fc9a638 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,319 @@ +# This file is autogenerated by maturin v1.12.3 +# To update, run +# +# maturin generate-ci github +# +name: CI + +on: + push: + branches: + - main + tags: + - '*' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + - runner: ubuntu-22.04 + target: s390x + - runner: ubuntu-22.04 + target: ppc64le + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v5 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: dist + + linux-pypy: + runs-on: ubuntu-22.04 + strategy: + matrix: + target: + - x86_64 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: 'pypy-3.11' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist --interpreter pypy3.11 + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: 2_34 + container: off + - name: Upload wheels + uses: actions/upload-artifact@v5 + with: + name: wheels-linux-pypy-${{ matrix.target }} + path: dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-22.04 + target: x86_64 + - runner: ubuntu-22.04 + target: x86 + - runner: ubuntu-22.04 + target: aarch64 + - runner: ubuntu-22.04 + target: armv7 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: musllinux_1_2 + - name: Upload wheels + uses: actions/upload-artifact@v5 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: dist + + windows: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: windows-latest + target: x64 + python_arch: x64 + - runner: windows-latest + target: x86 + python_arch: x86 + - runner: windows-11-arm + target: aarch64 + python_arch: arm64 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + architecture: ${{ matrix.platform.python_arch }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + - name: Upload wheels + uses: actions/upload-artifact@v5 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: dist + + macos: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: macos-15-intel + target: x86_64 + - runner: macos-latest + target: aarch64 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + + android: + runs-on: ubuntu-22.04 + strategy: + matrix: + target: + - aarch64-linux-android + - armv7-linux-androideabi + - x86_64-linux-android + python-version: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + steps: + - uses: actions/checkout@v6 + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r27d + local-cache: true + - name: Prepare PYO3_CROSS_LIB_DIR + run: | + NDK="$ANDROID_NDK_ROOT" + TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64" + ANDROID_API=24 + PYVER="${{ matrix.python-version }}" + MOCK_DIR="${{ github.workspace }}/android_python_${{ matrix.target }}_${{ matrix.python-version }}" + mkdir -p "$MOCK_DIR" + + case "${{ matrix.target }}" in + aarch64-linux-android) CLANG_TARGET="aarch64-linux-android${ANDROID_API}" ;; + armv7-linux-androideabi) CLANG_TARGET="armv7a-linux-androideabi${ANDROID_API}" ;; + x86_64-linux-android) CLANG_TARGET="x86_64-linux-android${ANDROID_API}" ;; + esac + + # NDK r27d removed generic clang/ld.lld symlinks; restore them so + # maturin-action's target-prefixed wrappers (${TARGET}-clang) can work. + CLANG=$(ls "$TOOLCHAIN/bin/clang-"[0-9]* 2>/dev/null | head -1) + if [ -z "$CLANG" ]; then + CLANG="$TOOLCHAIN/bin/clang" + if [ ! -f "$CLANG" ]; then + echo "ERROR: no clang found in $TOOLCHAIN/bin/" + ls -la "$TOOLCHAIN/bin/" | head -30 + exit 1 + fi + else + CLANG_BASE=$(basename "$CLANG") + ln -sf "$CLANG_BASE" "$TOOLCHAIN/bin/clang" + ln -sf "$CLANG_BASE" "$TOOLCHAIN/bin/clang++" + fi + ln -sf lld "$TOOLCHAIN/bin/ld.lld" + "$CLANG" --target="$CLANG_TARGET" -shared \ + -Wl,-soname,"libpython${PYVER}.so" \ + -o "$MOCK_DIR/libpython${PYVER}.so" -xc /dev/null + # pyo3 also links -lpython3 (versionless); provide it as a symlink + ln -sf "libpython${PYVER}.so" "$MOCK_DIR/libpython3.so" + + case "${{ matrix.target }}" in + aarch64-linux-android) LIBDL_ARCH="aarch64-linux-android" ;; + armv7-linux-androideabi) LIBDL_ARCH="arm-linux-androideabi" ;; + x86_64-linux-android) LIBDL_ARCH="x86_64-linux-android" ;; + esac + LIBDL=$(find "$TOOLCHAIN/sysroot/usr/lib/$LIBDL_ARCH" -name libdl.so | head -1) + [ -n "$LIBDL" ] && cp "$LIBDL" "$MOCK_DIR/" + + SYSCONFIG_NAME="_sysconfigdata__linux_${{ matrix.target }}" + cat > "$MOCK_DIR/${SYSCONFIG_NAME}.py" << EOF + build_time_vars = { + 'abiflags': '', + 'ABIFLAGS': '', + 'SO': '.so', + 'SOABI': 'cpython-${PYVER}', + 'EXT_SUFFIX': '.so', + 'LIBDIR': '$MOCK_DIR', + 'LDLIBRARY': 'libpython${PYVER}.so', + 'Py_ENABLE_SHARED': 1, + 'CC': 'clang', + 'CXX': 'clang++', + 'VERSION': '${PYVER}', + 'LDSHARED': 'clang -shared', + 'LDFLAGS': '-L$MOCK_DIR', + 'CCSHARED': '-fPIC', + 'GNULD': 'yes', + 'Py_DEBUG': 0, + 'WITH_PYMALLOC': 1, + } + EOF + + echo "PYO3_CROSS_LIB_DIR=$MOCK_DIR" >> "$GITHUB_ENV" + echo "PYO3_CROSS_PYTHON_VERSION=$PYVER" >> "$GITHUB_ENV" + echo "ANDROID_API_LEVEL=$ANDROID_API" >> "$GITHUB_ENV" + + RFLAGS_VAR="CARGO_TARGET_$(echo "${{ matrix.target }}" | tr '[:lower:]-' '[:upper:]_')_RUSTFLAGS" + echo "$RFLAGS_VAR=-C link-arg=-Wl,--no-as-needed -C link-arg=-lpython${PYVER} -C link-arg=-Wl,--unresolved-symbols=ignore-all" >> "$GITHUB_ENV" + env: + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + manylinux: off + env: + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + - name: Upload wheels + uses: actions/upload-artifact@v5 + with: + name: wheels-android-${{ matrix.target }}-${{ matrix.python-version }} + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v5 + with: + name: wheels-sdist + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} + needs: [linux, linux-pypy, musllinux, windows, macos, android, sdist] + permissions: + # Use to sign the release artifacts + id-token: write + # Used to upload release artifacts + contents: write + # Used to generate artifact attestation + attestations: write + steps: + - uses: actions/download-artifact@v6 + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v3 + with: + subject-path: 'wheels-*/*' + - name: Install uv + if: ${{ startsWith(github.ref, 'refs/tags/') }} + uses: astral-sh/setup-uv@v7 + - name: Publish to PyPI + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: uv publish 'wheels-*/*' + env: + UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..afc1e8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.venv/ +target/ +__pycache__/ +.vscode/ +*.so +*.pyi +.pytest_cache/ + +# Opencode +.opencode/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a44532b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,89 @@ +# Changelog + +## v1.0.5 + +- CI: add Android wheel builds (aarch64-linux-android, armv7-linux-androideabi, x86_64-linux-android × Python 3.8–3.14) — proper Termux support via PEP 738 android platform tags +- CI: remove redundant termux job (built manylinux aarch64/armv7, already covered by linux job; not Termux-compatible) + +## v1.0.3 + +- CI: add PyPy 3.11 setup step to restore PyPy wheel builds + +## v1.0.2 + +- chore: bump version to v1.0.2 + +## v1.0.1 + +Fix PyPI classifier for Unlicense (use `License :: Public Domain`). + +## v1.0.0 + +First stable release. Public domain (Unlicense). + +### Features +- Kyber-512, Kyber-768, Kyber-1024 post-quantum KEM +- Object-oriented Python API (`Kyber512`, `Kyber768`, `Kyber1024` classes) +- Static methods for encapsulate/decapsulate without a keypair instance +- Deterministic key generation from 64-byte seed +- Serialization: hex, base64, dict for both `Keypair` and `EncapsulationResult` +- Tuple unpacking for `Keypair` → `(pk, sk)` and `EncapsulationResult` → `(ct, ss)` +- Input validation with clear `KyberError` messages + +### Security +- Constant-time arithmetic to mitigate timing side-channels +- Corrected Kyber-512 noise distribution (ETA1=3) +- NIST FIPS-203 compliant Kyber-1024 compression parameters (du=11, dv=5) +- Secret material zeroed on drop via `zeroize` crate +- Safe Keccak squeeze (no buffer overruns) +- Hardened NTT butterfly bounds + +### Performance +| Variant | Keypair | Encapsulate | Decapsulate | +|---------|---------|-------------|-------------| +| Kyber-512 | 0.44 ms | 0.58 ms | 0.71 ms | +| Kyber-768 | 0.76 ms | 0.90 ms | 1.08 ms | +| Kyber-1024 | 1.08 ms | 1.35 ms | 1.55 ms | + +## v0.2.0 + +- Merge security hardening and deterministic key feature branch +- Memory zeroing and cryptographic primitive hardening +- Deterministic key generation and serialization utilities + +## v0.1.7 + +- Version bump (pre-security-hardening) + +## v0.1.6 + +- New decapsulate feature +- Stress tests and performance graphs + +## v0.1.5 + +- Error handling section in README + +## v0.1.4 + +- Better Python file structure +- More verbose error messages +- Additional panic tests and error handling + +## v0.1.3 + +- LSP syntax support (`.pyi` stubs) + +## v0.1.2 + +- Python 3.8+ support via `abi3` +- Termux (aarch64, armv7) wheel builds + +## v0.1.1 + +- Python 3.8+ support with `abi3` + +## v0.1.0 + +- Initial release to PyPI +- Basic Kyber-768 support diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9f5ef5a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,277 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pykyber" +version = "0.1.7" +dependencies = [ + "pyo3", + "rand", + "rand_core", + "zeroize", +] + +[[package]] +name = "pyo3" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" +dependencies = [ + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6f88410 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pykyber" +version = "1.0.5" +edition = "2021" + +[lib] +name = "pykyber" +crate-type = ["lib", "cdylib"] + +[dependencies] +pyo3 = { version = "0.27.0", features = ["extension-module", "abi3-py38"] } +zeroize = { version = "1.7", features = ["derive"] } +rand = "0.8" +rand_core = "0.6" + +[dev-dependencies] +rand = "0.8" + +[profile.dev] +opt-level = 0 + +[profile.release] +opt-level = 3 diff --git a/LICENSE b/LICENSE index c5b4af4..fdddb29 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,24 @@ -MIT License +This is free and unencumbered software released into the public domain. -Copyright (c) 2026 Ziki +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +For more information, please refer to diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bcb3b2d --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ + + +dev-build: clean dev-build-pyi + + +dev-build-package: + maturin develop + +dev-build-pyi: dev-build-package + pyo3-stubgen pykyber._pykyber ./python/ + +clean: + rm ./python/pykyber/*.so + rm ./python/pykyber/_pykyber.pyi + rm -rf ./target/ + +benchmark: + .venv/bin/python performance-tests/benchmark.py \ No newline at end of file diff --git a/README.md b/README.md index dd71838..1158e5f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,147 @@ -# pykyber -This is a python package that allows easy use for Kyber. +# PyKyber + +A Python library for Kyber post-quantum key encapsulation, implemented in Rust. + +## Installation + +```bash +pip install pykyber +``` + +## Quick Start + +```python +import pykyber + +# Generate a keypair (Alice) +alice_keypair = pykyber.Kyber768() + +# Encapsulate - create shared secret (Bob) +bob_result = pykyber.Kyber768.encapsulate(alice_keypair.public_key) + +# Decapsulate - recover shared secret (Alice) +shared_secret = alice_keypair.decapsulate(bob_result.ciphertext) + +# Both parties now share the same secret +print(f"Match: {bob_result.shared_secret == shared_secret}") +``` + +## API Usage + +```python +import pykyber + +# Create a keypair - instant generation on class instantiation +keypair = pykyber.Kyber512() # ~AES-128 security +keypair = pykyber.Kyber768() # ~AES-192 security +keypair = pykyber.Kyber1024() # ~AES-256 security + +# Deterministic key generation from a 64-byte seed +seed = bytes(range(64)) +keypair = pykyber.Kyber768(seed=seed) + +# Access raw key bytes +public_key = keypair.public_key # bytes +secret_key = keypair.secret_key # bytes + +# Or unpack directly as tuple +public_key, secret_key = keypair # same as above + +# Encapsulate - create ciphertext and shared secret +result = keypair.encapsulate() +# result.ciphertext - bytes to send to receiver +# result.shared_secret - 32 bytes shared secret + +# Serialization and Utilities +hex_pk = keypair.public_key_hex # Get hex representation +b64_sk = keypair.secret_key_b64 # Get base64 representation +data = keypair.to_dict() # Get dictionary of hex keys + +# Encapsulation results also support serialization +ct_hex = result.ciphertext_hex +ss_b64 = result.shared_secret_b64 + +# Or unpack directly as tuple +ciphertext, shared_secret = keypair.encapsulate() # same as above + +# Decapsulate - recover shared secret from ciphertext +shared_secret = keypair.decapsulate(result.ciphertext) + +# Static methods - use without creating a keypair instance + +# Encapsulate with just a public key +result = pykyber.Kyber768.encapsulate(public_key) + +# Decapsulate with just ciphertext and secret key +shared_secret = pykyber.Kyber768.decapsulate(ciphertext, secret_key) + +# Load a Keypair object from existing keys +keypair = pykyber.Kyber768.from_keys(public_key, secret_key) +``` + +## Key Sizes + +| Variant | Public Key | Secret Key | Ciphertext | Shared Secret | +|-----------|------------|------------|------------|---------------| +| Kyber-512 | 800 bytes | 1632 bytes | 768 bytes | 32 bytes | +| Kyber-768 | 1184 bytes | 2400 bytes | 1088 bytes | 32 bytes | +| Kyber-1024| 1568 bytes | 3168 bytes | 1568 bytes | 32 bytes | + + +## Error Handling + +All operations raise `KyberError` when invalid input is provided: + +```python +import pykyber + +try: + pykyber.Kyber768.encapsulate(b"too_short") +except pykyber.KyberError as e: + print(e) +# Output: Invalid public key for Kyber768.encapsulate: expected 1184 bytes, got 10. +# Ensure you're using the correct Kyber variant (Kyber512=800, Kyber768=1184, Kyber1024=1568). +``` + +Common error cases: +- **Invalid public key size**: Wrong number of bytes for the Kyber variant +- **Invalid ciphertext size**: Wrong number of bytes when decapsulating +- **Invalid secret key size**: Wrong number of bytes for the Kyber variant +- **Invalid seed size**: Seed must be exactly 64 bytes for deterministic generation + + +## Performance + +Performance benchmarks (100 iterations each): + +| Variant | Keypair | Encapsulate | Decapsulate | +|---------|---------|-------------|-------------| +| Kyber512 | 0.44 ms (2,262/s) | 0.58 ms (1,721/s) | 0.71 ms (1,404/s) | +| Kyber768 | 0.76 ms (1,312/s) | 0.90 ms (1,111/s) | 1.08 ms (930/s) | +| Kyber1024 | 1.08 ms (928/s) | 1.35 ms (743/s) | 1.55 ms (645/s) | + +Run benchmarks and generate graphs: + +```bash +make benchmark +``` + +Generated graphs: + +### Performance Comparison +![Performance Comparison](performance-tests/performance_comparison.png) + +### Operation Breakdown +![Operation Breakdown](performance-tests/operation_breakdown.png) + +### Scalability +![Scalability](performance-tests/scalability.png) + +### Throughput +![Throughput](performance-tests/throughput.png) + + +## License + +This is free and unencumbered software released into the public domain. +See [LICENSE](LICENSE) or . diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..5a8a1cb --- /dev/null +++ b/docs/api.md @@ -0,0 +1,140 @@ +# pykyber API Reference + +## Classes + +### `Kyber512`, `Kyber768`, `Kyber1024` + +Each variant has the same interface with variant-specific key sizes. + +#### Class constants + +| Constant | Kyber512 | Kyber768 | Kyber1024 | +|----------|----------|----------|-----------| +| `PUBLIC_KEY_SIZE` | 800 | 1184 | 1568 | +| `SECRET_KEY_SIZE` | 1632 | 2400 | 3168 | +| `CIPHERTEXT_SIZE` | 768 | 1088 | 1568 | +| `SHARED_SECRET_SIZE` | 32 | 32 | 32 | + +#### `__new__(seed: Optional[bytes] = None) -> Keypair` + +Generate a new keypair. If `seed` is provided, it must be exactly 64 bytes for deterministic key generation. + +```python +keypair = pykyber.Kyber768() +keypair = pykyber.Kyber768(seed=bytes(range(64))) +``` + +#### `encapsulate(public_key: bytes) -> EncapsulationResult` *(static)* + +Encapsulate a shared secret using a public key without creating a keypair instance. + +```python +result = pykyber.Kyber768.encapsulate(public_key) +``` + +#### `decapsulate(ciphertext: bytes, secret_key: bytes) -> bytes` *(static)* + +Decapsulate a shared secret using ciphertext and secret key without a keypair instance. + +```python +ss = pykyber.Kyber768.decapsulate(ciphertext, secret_key) +``` + +#### `from_keys(public_key: bytes, secret_key: bytes) -> Keypair` *(classmethod)* + +Create a `Keypair` from existing key bytes. + +```python +keypair = pykyber.Kyber768.from_keys(pk, sk) +``` + +--- + +### `Keypair` + +Returned by `Kyber*()` instantiation and `from_keys()`. + +#### Properties + +- `public_key` — raw public key bytes +- `secret_key` — raw secret key bytes +- `public_key_hex` — hex string +- `secret_key_hex` — hex string +- `public_key_b64` — base64 string +- `secret_key_b64` — base64 string + +#### Methods + +- `encapsulate() -> EncapsulationResult` — encapsulate using own public key +- `decapsulate(ciphertext: bytes) -> bytes` — decapsulate using own secret key +- `to_dict() -> dict` — `{"public_key": ..., "secret_key": ...}` (hex) + +#### Tuple unpacking + +```python +public_key, secret_key = keypair # __iter__ yields (pk, sk) +``` + +--- + +### `EncapsulationResult` + +Returned by `encapsulate()` and static `Kyber*.encapsulate()`. + +#### Properties + +- `ciphertext` — raw ciphertext bytes +- `shared_secret` — raw shared secret bytes (32 bytes) +- `ciphertext_hex` — hex string +- `shared_secret_hex` — hex string +- `ciphertext_b64` — base64 string +- `shared_secret_b64` — base64 string + +#### Methods + +- `to_dict() -> dict` — `{"ciphertext": ..., "shared_secret": ...}` (hex) + +#### Tuple unpacking + +```python +ciphertext, shared_secret = result # __iter__ yields (ct, ss) +``` + +--- + +## Exceptions + +### `KyberError` + +Subclass of `Exception`. Raised on: +- Invalid public key size +- Invalid ciphertext size +- Invalid secret key size +- Invalid seed size (not 64 bytes) + +```python +try: + pykyber.Kyber768.encapsulate(b"too_short") +except pykyber.KyberError as e: + print(e) +``` + +--- + +## Low-level functions + +These are variant-generic (Kyber-768): + +| Function | Returns | Description | +|----------|---------|-------------| +| `_generate_keypair(seed)` | `(pk, sk)` | Kyber-768 keypair | +| `_encapsulate(pk)` | `(ct, ss)` | Kyber-768 encapsulate | +| `_decapsulate(ct, sk)` | `ss` | Kyber-768 decapsulate | + +Variant-specific low-level functions: + +| Function | Kyber-512 | Kyber-768 | Kyber-1024 | +|----------|-----------|-----------|------------| +| keypair | `_keypair_512(seed)` | `_keypair_768(seed)` | `_keypair_1024(seed)` | +| encapsulate | `_encapsulate_512(pk)` | `_encapsulate_768(pk)` | `_encapsulate_1024(pk)` | +| decapsulate | `_decapsulate_512(ct, sk)` | `_decapsulate_768(ct, sk)` | `_decapsulate_1024(ct, sk)` | diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..945c3a8 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,9 @@ +# pykyber docs + +- [API Reference](api.md) — full class and function documentation + +## Quick links + +- [README](../README.md) — installation and quick start +- [CHANGELOG](../CHANGELOG.md) — release history +- [LICENSE](../LICENSE) — public domain (Unlicense) diff --git a/performance-tests/benchmark.py b/performance-tests/benchmark.py new file mode 100644 index 0000000..e50c5b2 --- /dev/null +++ b/performance-tests/benchmark.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +"""Performance benchmarking and graphing script for pykyber. + +This script runs the performance tests from python/tests/test_kyber.py +and generates graphs from the results. +""" + +import subprocess +import re +import matplotlib.pyplot as plt + + +def run_performance_tests(): + """Run pytest performance tests and parse results.""" + result = subprocess.run( + ['.venv/bin/python', '-m', 'pytest', + 'python/tests/test_kyber.py', + '-v', '-k', 'performance', + '--tb=no', '-q', '-s'], + capture_output=True, + text=True + ) + + output = result.stdout + result.stderr + + results = { + 'Kyber512': {}, + 'Kyber768': {}, + 'Kyber1024': {} + } + + pattern = r'Kyber(\d+)\s+(keypair generation|encapsulation|decapsulation|full key exchange.*?):\s+([\d.]+)\s+ms/op' + + for match in re.finditer(pattern, output): + variant = f"Kyber{match.group(1)}" + operation = match.group(2).strip() + time_ms = float(match.group(3)) + + if 'keypair generation' in operation: + results[variant]['keypair'] = time_ms + elif 'encapsulation' in operation: + results[variant]['encapsulate'] = time_ms + elif 'decapsulation' in operation: + results[variant]['decapsulate'] = time_ms + elif 'class API' in operation: + results[variant]['class_api'] = time_ms + elif 'static methods' in operation: + results[variant]['static_methods'] = time_ms + + return results + + +def create_comparison_chart(results): + """Create a comparison chart across all variants and operations.""" + variants = ['Kyber512', 'Kyber768', 'Kyber1024'] + + x = range(len(variants)) + width = 0.25 + + fig, ax = plt.subplots(figsize=(12, 6)) + + keypair_times = [results[v]['keypair'] for v in variants] + encapsulate_times = [results[v]['encapsulate'] for v in variants] + decapsulate_times = [results[v]['decapsulate'] for v in variants] + + bars1 = ax.bar([i - width for i in x], keypair_times, width, label='Keypair Generation', color='#2ecc71') + bars2 = ax.bar(x, encapsulate_times, width, label='Encapsulation', color='#3498db') + bars3 = ax.bar([i + width for i in x], decapsulate_times, width, label='Decapsulation', color='#e74c3c') + + ax.set_xlabel('Kyber Variant', fontsize=12) + ax.set_ylabel('Time (ms)', fontsize=12) + ax.set_title('PyKyber Performance Comparison', fontsize=14, fontweight='bold') + ax.set_xticks(x) + ax.set_xticklabels(variants) + ax.legend() + ax.grid(axis='y', alpha=0.3) + + for bars in [bars1, bars2, bars3]: + for bar in bars: + height = bar.get_height() + ax.annotate(f'{height:.2f}', + xy=(bar.get_x() + bar.get_width() / 2, height), + xytext=(0, 3), + textcoords="offset points", + ha='center', va='bottom', fontsize=8) + + plt.tight_layout() + plt.savefig('performance-tests/performance_comparison.png', dpi=150) + print("Saved: performance-tests/performance_comparison.png") + plt.close() + + +def create_operation_breakdown(results): + """Create individual charts for each operation.""" + variants = ['Kyber512', 'Kyber768', 'Kyber1024'] + colors = ['#2ecc71', '#3498db', '#e74c3c'] + + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + + operations = ['keypair', 'encapsulate', 'decapsulate'] + titles = ['Keypair Generation', 'Encapsulation', 'Decapsulation'] + + for idx, (op, title) in enumerate(zip(operations, titles)): + times = [results[v][op] for v in variants] + bars = axes[idx].bar(variants, times, color=colors) + axes[idx].set_title(title, fontsize=12, fontweight='bold') + axes[idx].set_ylabel('Time (ms)') + axes[idx].grid(axis='y', alpha=0.3) + + for bar in bars: + height = bar.get_height() + axes[idx].annotate(f'{height:.2f}', + xy=(bar.get_x() + bar.get_width() / 2, height), + xytext=(0, 3), + textcoords="offset points", + ha='center', va='bottom', fontsize=10) + + plt.tight_layout() + plt.savefig('performance-tests/operation_breakdown.png', dpi=150) + print("Saved: performance-tests/operation_breakdown.png") + plt.close() + + +def create_scalability_chart(results): + """Create a chart showing how performance scales with security level.""" + variants = ['Kyber512', 'Kyber768', 'Kyber1024'] + security_levels = ['~AES-128', '~AES-192', '~AES-256'] + + keypair_times = [results[v]['keypair'] for v in variants] + encapsulate_times = [results[v]['encapsulate'] for v in variants] + decapsulate_times = [results[v]['decapsulate'] for v in variants] + + fig, ax = plt.subplots(figsize=(10, 6)) + + x = range(len(variants)) + + ax.plot(x, keypair_times, 'o-', linewidth=2, markersize=8, label='Keypair Generation', color='#2ecc71') + ax.plot(x, encapsulate_times, 's-', linewidth=2, markersize=8, label='Encapsulation', color='#3498db') + ax.plot(x, decapsulate_times, '^-', linewidth=2, markersize=8, label='Decapsulation', color='#e74c3c') + + ax.set_xticks(x) + ax.set_xticklabels([f'{v}\n({s})' for v, s in zip(variants, security_levels)]) + ax.set_xlabel('Kyber Variant (Security Level)', fontsize=12) + ax.set_ylabel('Time (ms)', fontsize=12) + ax.set_title('PyKyber Performance Scalability', fontsize=14, fontweight='bold') + ax.legend() + ax.grid(alpha=0.3) + + for i, variant in enumerate(variants): + for j, (data, label) in enumerate([(keypair_times, 'keypair'), + (encapsulate_times, 'encapsulate'), + (decapsulate_times, 'decapsulate')]): + ax.annotate(f'{data[i]:.2f}', + (i, data[i]), + textcoords="offset points", + xytext=(0, 10), + ha='center', fontsize=8) + + plt.tight_layout() + plt.savefig('performance-tests/scalability.png', dpi=150) + print("Saved: performance-tests/scalability.png") + plt.close() + + +def create_throughput_chart(results): + """Create a throughput chart showing operations per second.""" + variants = ['Kyber512', 'Kyber768', 'Kyber1024'] + + keypair_ops = [1000 / results[v]['keypair'] for v in variants] + encapsulate_ops = [1000 / results[v]['encapsulate'] for v in variants] + decapsulate_ops = [1000 / results[v]['decapsulate'] for v in variants] + + fig, ax = plt.subplots(figsize=(10, 6)) + + x = range(len(variants)) + width = 0.25 + + bars1 = ax.bar([i - width for i in x], keypair_ops, width, label='Keypair Generation', color='#2ecc71') + bars2 = ax.bar(x, encapsulate_ops, width, label='Encapsulation', color='#3498db') + bars3 = ax.bar([i + width for i in x], decapsulate_ops, width, label='Decapsulation', color='#e74c3c') + + ax.set_xlabel('Kyber Variant', fontsize=12) + ax.set_ylabel('Operations per Second', fontsize=12) + ax.set_title('PyKyber Throughput (Operations per Second)', fontsize=14, fontweight='bold') + ax.set_xticks(x) + ax.set_xticklabels(variants) + ax.legend() + ax.grid(axis='y', alpha=0.3) + + for bars in [bars1, bars2, bars3]: + for bar in bars: + height = bar.get_height() + ax.annotate(f'{int(height)}', + xy=(bar.get_x() + bar.get_width() / 2, height), + xytext=(0, 3), + textcoords="offset points", + ha='center', va='bottom', fontsize=8) + + plt.tight_layout() + plt.savefig('performance-tests/throughput.png', dpi=150) + print("Saved: performance-tests/throughput.png") + plt.close() + + +def print_summary(results): + """Print a summary table of results.""" + print("\n" + "=" * 60) + print("PyKyber Performance Summary (100 iterations each)") + print("=" * 60) + print(f"{'Variant':<12} {'Keypair (ms)':<15} {'Encapsulate (ms)':<18} {'Decapsulate (ms)':<18}") + print("-" * 60) + for variant in ['Kyber512', 'Kyber768', 'Kyber1024']: + r = results[variant] + print(f"{variant:<12} {r['keypair']:<15.3f} {r['encapsulate']:<18.3f} {r['decapsulate']:<18.3f}") + print("=" * 60) + + print("\nThroughput (operations/second):") + print("-" * 60) + print(f"{'Variant':<12} {'Keypair/s':<15} {'Encapsulate/s':<18} {'Decapsulate/s':<18}") + print("-" * 60) + for variant in ['Kyber512', 'Kyber768', 'Kyber1024']: + r = results[variant] + kp_ops = int(1000 / r['keypair']) + enc_ops = int(1000 / r['encapsulate']) + dec_ops = int(1000 / r['decapsulate']) + print(f"{variant:<12} {kp_ops:<15} {enc_ops:<18} {dec_ops:<18}") + print("=" * 60) + + +if __name__ == '__main__': + print("Running performance tests from python/tests/test_kyber.py...") + results = run_performance_tests() + print_summary(results) + create_comparison_chart(results) + create_operation_breakdown(results) + create_scalability_chart(results) + create_throughput_chart(results) + print("\nAll graphs saved in performance-tests/") diff --git a/performance-tests/operation_breakdown.png b/performance-tests/operation_breakdown.png new file mode 100644 index 0000000..88644a7 Binary files /dev/null and b/performance-tests/operation_breakdown.png differ diff --git a/performance-tests/performance_comparison.png b/performance-tests/performance_comparison.png new file mode 100644 index 0000000..b2a299a Binary files /dev/null and b/performance-tests/performance_comparison.png differ diff --git a/performance-tests/scalability.png b/performance-tests/scalability.png new file mode 100644 index 0000000..890aa51 Binary files /dev/null and b/performance-tests/scalability.png differ diff --git a/performance-tests/throughput.png b/performance-tests/throughput.png new file mode 100644 index 0000000..1ec261f Binary files /dev/null and b/performance-tests/throughput.png differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2cc3c87 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["maturin>=1.12,<2.0"] +build-backend = "maturin" + +[project] +name = "pykyber" +version = "1.0.5" +description = "Kyber post-quantum key encapsulation in Rust" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "Unlicense"} +authors = [ + {name = "Yoav Haimov", email = "haimovyoav@gmail.com"} +] +keywords = ["kyber", "post-quantum", "cryptography", "pqc", "key-encapsulation"] +classifiers = [ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Rust", + "License :: Public Domain", + "Operating System :: OS Independent", + "Topic :: Security :: Cryptography", +] + +[project.urls] +Homepage = "https://github.com/Zikithezikit/pykyber" +Documentation = "https://github.com/Zikithezikit/pykyber/tree/v1.0.5/docs" +Repository = "https://github.com/Zikithezikit/pykyber" +Issues = "https://github.com/Zikithezikit/pykyber/issues" + +[tool.maturin] +python-source = "python" +module-name = "pykyber._pykyber" diff --git a/python/pykyber/__init__.py b/python/pykyber/__init__.py new file mode 100644 index 0000000..5e5e15a --- /dev/null +++ b/python/pykyber/__init__.py @@ -0,0 +1,32 @@ +"""Kyber post-quantum key encapsulation.""" + + + + + +from ._kyber import ( + Kyber768, + Kyber512, + Kyber1024, + KyberError, + _validate_encapsulate, + _validate_decapsulate, + _validate_keypair, +) + +__all__ = ["Kyber512", "Kyber768", "Kyber1024", "KyberError"] + +from . import _pykyber + +_generate_keypair = _validate_keypair(_pykyber._generate_keypair) +_encapsulate = _validate_encapsulate(_pykyber._encapsulate, 1184, "_generate_keypair") +_decapsulate = _validate_decapsulate(_pykyber._decapsulate, 1088, "_generate_keypair") +_keypair_512 = _validate_keypair(_pykyber._keypair_512) +_keypair_768 = _validate_keypair(_pykyber._keypair_768) +_keypair_1024 = _validate_keypair(_pykyber._keypair_1024) +_encapsulate_512 = _validate_encapsulate(_pykyber._encapsulate_512, Kyber512.PUBLIC_KEY_SIZE, "_encapsulate_512") +_encapsulate_768 = _validate_encapsulate(_pykyber._encapsulate_768, Kyber768.PUBLIC_KEY_SIZE, "_encapsulate_768") +_encapsulate_1024 = _validate_encapsulate(_pykyber._encapsulate_1024, Kyber1024.PUBLIC_KEY_SIZE, "_encapsulate_1024") +_decapsulate_512 = _validate_decapsulate(_pykyber._decapsulate_512, Kyber512.CIPHERTEXT_SIZE, "_decapsulate_512") +_decapsulate_768 = _validate_decapsulate(_pykyber._decapsulate_768, Kyber768.CIPHERTEXT_SIZE, "_decapsulate_768") +_decapsulate_1024 = _validate_decapsulate(_pykyber._decapsulate_1024, Kyber1024.CIPHERTEXT_SIZE, "_decapsulate_1024") diff --git a/python/pykyber/_error.py b/python/pykyber/_error.py new file mode 100644 index 0000000..997c1da --- /dev/null +++ b/python/pykyber/_error.py @@ -0,0 +1,6 @@ +"""Kyber error exception.""" + + +class KyberError(Exception): + """Kyber error exception - raised when invalid input is provided.""" + pass diff --git a/python/pykyber/_kyber.py b/python/pykyber/_kyber.py new file mode 100644 index 0000000..877a06d --- /dev/null +++ b/python/pykyber/_kyber.py @@ -0,0 +1,121 @@ +"""Kyber post-quantum key encapsulation.""" + +from typing import Optional +from ._error import KyberError +from ._types import EncapsulationResult, Keypair +from ._validation import _validate_encapsulate, _validate_decapsulate, _validate_keypair, _validate_pk + + +class Kyber512: + """Kyber-512 key encapsulation (security ~AES-128).""" + + PUBLIC_KEY_SIZE = 800 + SECRET_KEY_SIZE = 1632 + CIPHERTEXT_SIZE = 768 + SHARED_SECRET_SIZE = 32 + + def __new__(cls, seed: Optional[bytes] = None) -> Keypair: + """Generate a new key pair, optionally from a 64-byte seed.""" + from . import _pykyber + pk, sk = _validate_keypair(_pykyber._keypair_512)(seed) + return Keypair(sk, pk, + _validate_encapsulate(_pykyber._encapsulate_512, Kyber512.PUBLIC_KEY_SIZE, "Kyber512"), + _validate_decapsulate(_pykyber._decapsulate_512, Kyber512.CIPHERTEXT_SIZE, "Kyber512")) + + @staticmethod + def encapsulate(public_key: bytes) -> EncapsulationResult: + """Encapsulate a shared secret using a public key (no keypair needed).""" + from . import _pykyber + return EncapsulationResult(*_validate_encapsulate(_pykyber._encapsulate_512, Kyber512.PUBLIC_KEY_SIZE, "Kyber512.encapsulate")(public_key)) + + @staticmethod + def decapsulate(ciphertext: bytes, secret_key: bytes) -> bytes: + """Decapsulate a shared secret using ciphertext and secret key (no keypair needed).""" + from . import _pykyber + return _validate_decapsulate(_pykyber._decapsulate_512, Kyber512.CIPHERTEXT_SIZE, "Kyber512.decapsulate")(ciphertext, secret_key) + + @classmethod + def from_keys(cls, public_key: bytes, secret_key: bytes) -> Keypair: + """Create a Keypair object from existing public and secret keys.""" + from . import _pykyber + _validate_pk(public_key, cls.PUBLIC_KEY_SIZE, f"{cls.__name__}.from_keys") + return Keypair(secret_key, public_key, + _validate_encapsulate(_pykyber._encapsulate_512, cls.PUBLIC_KEY_SIZE, cls.__name__), + _validate_decapsulate(_pykyber._decapsulate_512, cls.CIPHERTEXT_SIZE, cls.__name__)) + + +class Kyber768: + """Kyber-768 key encapsulation (security ~AES-192).""" + + PUBLIC_KEY_SIZE = 1184 + SECRET_KEY_SIZE = 2400 + CIPHERTEXT_SIZE = 1088 + SHARED_SECRET_SIZE = 32 + + def __new__(cls, seed: Optional[bytes] = None) -> Keypair: + """Generate a new key pair, optionally from a 64-byte seed.""" + from . import _pykyber + pk, sk = _validate_keypair(_pykyber._keypair_768)(seed) + return Keypair(sk, pk, + _validate_encapsulate(_pykyber._encapsulate_768, Kyber768.PUBLIC_KEY_SIZE, "Kyber768"), + _validate_decapsulate(_pykyber._decapsulate_768, Kyber768.CIPHERTEXT_SIZE, "Kyber768")) + + @staticmethod + def encapsulate(public_key: bytes) -> EncapsulationResult: + """Encapsulate a shared secret using a public key (no keypair needed).""" + from . import _pykyber + return EncapsulationResult(*_validate_encapsulate(_pykyber._encapsulate_768, Kyber768.PUBLIC_KEY_SIZE, "Kyber768.encapsulate")(public_key)) + + @staticmethod + def decapsulate(ciphertext: bytes, secret_key: bytes) -> bytes: + """Decapsulate a shared secret using ciphertext and secret key (no keypair needed).""" + from . import _pykyber + return _validate_decapsulate(_pykyber._decapsulate_768, Kyber768.CIPHERTEXT_SIZE, "Kyber768.decapsulate")(ciphertext, secret_key) + + @classmethod + def from_keys(cls, public_key: bytes, secret_key: bytes) -> Keypair: + """Create a Keypair object from existing public and secret keys.""" + from . import _pykyber + _validate_pk(public_key, cls.PUBLIC_KEY_SIZE, f"{cls.__name__}.from_keys") + return Keypair(secret_key, public_key, + _validate_encapsulate(_pykyber._encapsulate_768, cls.PUBLIC_KEY_SIZE, cls.__name__), + _validate_decapsulate(_pykyber._decapsulate_768, cls.CIPHERTEXT_SIZE, cls.__name__)) + + +class Kyber1024: + """Kyber-1024 key encapsulation (security ~AES-256).""" + + PUBLIC_KEY_SIZE = 1568 + SECRET_KEY_SIZE = 3168 + CIPHERTEXT_SIZE = 1568 + SHARED_SECRET_SIZE = 32 + + def __new__(cls, seed: Optional[bytes] = None) -> Keypair: + """Generate a new key pair, optionally from a 64-byte seed.""" + from . import _pykyber + pk, sk = _validate_keypair(_pykyber._keypair_1024)(seed) + return Keypair(sk, pk, + _validate_encapsulate(_pykyber._encapsulate_1024, Kyber1024.PUBLIC_KEY_SIZE, "Kyber1024"), + _validate_decapsulate(_pykyber._decapsulate_1024, Kyber1024.CIPHERTEXT_SIZE, "Kyber1024")) + + @staticmethod + def encapsulate(public_key: bytes) -> EncapsulationResult: + """Encapsulate a shared secret using a public key (no keypair needed).""" + from . import _pykyber + return EncapsulationResult(*_validate_encapsulate(_pykyber._encapsulate_1024, Kyber1024.PUBLIC_KEY_SIZE, "Kyber1024.encapsulate")(public_key)) + + @staticmethod + def decapsulate(ciphertext: bytes, secret_key: bytes) -> bytes: + """Decapsulate a shared secret using ciphertext and secret key (no keypair needed).""" + from . import _pykyber + return _validate_decapsulate(_pykyber._decapsulate_1024, Kyber1024.CIPHERTEXT_SIZE, "Kyber1024.decapsulate")(ciphertext, secret_key) + + @classmethod + def from_keys(cls, public_key: bytes, secret_key: bytes) -> Keypair: + """Create a Keypair object from existing public and secret keys.""" + from . import _pykyber + _validate_pk(public_key, cls.PUBLIC_KEY_SIZE, f"{cls.__name__}.from_keys") + return Keypair(secret_key, public_key, + _validate_encapsulate(_pykyber._encapsulate_1024, cls.PUBLIC_KEY_SIZE, cls.__name__), + _validate_decapsulate(_pykyber._decapsulate_1024, cls.CIPHERTEXT_SIZE, cls.__name__)) + diff --git a/python/pykyber/_types.py b/python/pykyber/_types.py new file mode 100644 index 0000000..6cbfc3c --- /dev/null +++ b/python/pykyber/_types.py @@ -0,0 +1,112 @@ +"""Kyber types and data structures.""" + +import base64 +from dataclasses import dataclass +from typing import Tuple, Callable, Dict, Any + + +@dataclass(frozen=True) +class EncapsulationResult: + """Result of a Kyber encapsulation.""" + + ciphertext: bytes + shared_secret: bytes + + @property + def ciphertext_hex(self) -> str: + """Return the ciphertext in hex format.""" + return self.ciphertext.hex() + + @property + def shared_secret_hex(self) -> str: + """Return the shared secret in hex format.""" + return self.shared_secret.hex() + + @property + def ciphertext_b64(self) -> str: + """Return the ciphertext in base64 format.""" + return base64.b64encode(self.ciphertext).decode("utf-8") + + @property + def shared_secret_b64(self) -> str: + """Return the shared secret in base64 format.""" + return base64.b64encode(self.shared_secret).decode("utf-8") + + def to_dict(self) -> Dict[str, Any]: + """Convert result to a dictionary.""" + return { + "ciphertext": self.ciphertext_hex, + "shared_secret": self.shared_secret_hex, + } + + def __repr__(self) -> str: + return ( + f"EncapsulationResult(" + f"ciphertext={len(self.ciphertext)} bytes, " + f"shared_secret={len(self.shared_secret)} bytes)" + ) + + def __iter__(self): + yield self.ciphertext + yield self.shared_secret + + +@dataclass +class Keypair: + """Keypair containing private key material with decapsulate method.""" + _secret_key: bytes + _public_key: bytes + _encapsulate_fn: Callable[[bytes], Tuple[bytes, bytes]] + _decapsulate_fn: Callable[[bytes, bytes], bytes] + + def encapsulate(self) -> EncapsulationResult: + """Encapsulate a shared secret using the public key.""" + return EncapsulationResult(*self._encapsulate_fn(self._public_key)) + + def decapsulate(self, ciphertext: bytes) -> bytes: + """Decapsulate a shared secret using ciphertext.""" + return self._decapsulate_fn(ciphertext, self._secret_key) + + @property + def secret_key(self) -> bytes: + """Return the raw secret key bytes.""" + return self._secret_key + + @property + def public_key(self) -> bytes: + """Return the raw public key bytes.""" + return self._public_key + + @property + def public_key_hex(self) -> str: + """Return the public key in hex format.""" + return self._public_key.hex() + + @property + def secret_key_hex(self) -> str: + """Return the secret key in hex format.""" + return self._secret_key.hex() + + @property + def public_key_b64(self) -> str: + """Return the public key in base64 format.""" + return base64.b64encode(self._public_key).decode("utf-8") + + @property + def secret_key_b64(self) -> str: + """Return the secret key in base64 format.""" + return base64.b64encode(self._secret_key).decode("utf-8") + + def to_dict(self) -> Dict[str, Any]: + """Convert keypair to a dictionary (using hex encoding).""" + return { + "public_key": self.public_key_hex, + "secret_key": self.secret_key_hex, + } + + def __repr__(self) -> str: + return f"Keypair(secret_key={len(self._secret_key)} bytes, public_key={len(self._public_key)} bytes)" + + def __iter__(self): + yield self._public_key + yield self._secret_key diff --git a/python/pykyber/_validation.py b/python/pykyber/_validation.py new file mode 100644 index 0000000..2b078b9 --- /dev/null +++ b/python/pykyber/_validation.py @@ -0,0 +1,67 @@ +"""Kyber validation helpers.""" + +from typing import Tuple, Callable, Optional + +from ._error import KyberError + + +def _validate_pk(pk: bytes, expected_size: int, method_name: str) -> None: + """Validate public key length with a clear error message.""" + actual_size = len(pk) + if actual_size != expected_size: + raise KyberError( + f"Invalid public key for {method_name}: expected {expected_size} bytes, got {actual_size}. " + f"Ensure you're using the correct Kyber variant (Kyber512={800}, Kyber768={1184}, Kyber1024={1568})." + ) + + +def _validate_seed(seed: Optional[bytes], expected_size: int = 64) -> None: + """Validate seed length.""" + if seed is not None: + actual_size = len(seed) + if actual_size != expected_size: + raise KyberError( + f"Invalid seed size: expected {expected_size} bytes, got {actual_size}." + ) + + +def _validate_encapsulate(fn: Callable[[bytes], Tuple[bytes, bytes]], expected_pk_size: int, method_name: str) -> Callable[[bytes], Tuple[bytes, bytes]]: + """Validate public key and call Rust encapsulate function.""" + def wrapper(pk: bytes) -> Tuple[bytes, bytes]: + _validate_pk(pk, expected_pk_size, method_name) + try: + return fn(pk) + except Exception as e: + raise KyberError(str(e)) from None + return wrapper + + +def _validate_ct(ct: bytes, expected_size: int, method_name: str) -> None: + """Validate ciphertext length with a clear error message.""" + actual_size = len(ct) + if actual_size != expected_size: + raise KyberError( + f"Invalid ciphertext for {method_name}: expected {expected_size} bytes, got {actual_size}." + ) + + +def _validate_decapsulate(fn: Callable[[bytes, bytes], bytes], expected_ct_size: int, method_name: str) -> Callable[[bytes, bytes], bytes]: + """Validate ciphertext and call Rust decapsulate function.""" + def wrapper(ct: bytes, sk: bytes) -> bytes: + _validate_ct(ct, expected_ct_size, method_name) + try: + return fn(ct, sk) + except Exception as e: + raise KyberError(str(e)) from None + return wrapper + + +def _validate_keypair(fn: Callable[[Optional[bytes]], Tuple[bytes, bytes]]) -> Callable[[Optional[bytes]], Tuple[bytes, bytes]]: + """Wrap a Rust keypair function to raise KyberError on failure.""" + def wrapper(seed: Optional[bytes] = None) -> Tuple[bytes, bytes]: + _validate_seed(seed) + try: + return fn(seed) + except Exception as e: + raise KyberError(str(e)) from None + return wrapper diff --git a/python/pykyber/py.typed b/python/pykyber/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/test_features.py b/python/tests/test_features.py new file mode 100644 index 0000000..434af63 --- /dev/null +++ b/python/tests/test_features.py @@ -0,0 +1,144 @@ +import pykyber +import pytest +import base64 + + +def test_deterministic_keypair(): + """Test that the same seed produces the same keypair.""" + seed = bytes(range(64)) + + # Kyber-512 + k512_1 = pykyber.Kyber512(seed=seed) + k512_2 = pykyber.Kyber512(seed=seed) + assert k512_1.public_key == k512_2.public_key + assert k512_1.secret_key == k512_2.secret_key + + # Kyber-768 + k768_1 = pykyber.Kyber768(seed=seed) + k768_2 = pykyber.Kyber768(seed=seed) + assert k768_1.public_key == k768_2.public_key + assert k768_1.secret_key == k768_2.secret_key + + # Kyber-1024 + k1024_1 = pykyber.Kyber1024(seed=seed) + k1024_2 = pykyber.Kyber1024(seed=seed) + assert k1024_1.public_key == k1024_2.public_key + assert k1024_1.secret_key == k1024_2.secret_key + + +def test_invalid_seed_size(): + """Test that invalid seed size raises KyberError.""" + seed = b"too short" + with pytest.raises(pykyber.KyberError, match="Invalid seed size"): + pykyber.Kyber768(seed=seed) + + +def test_serialization_utilities(): + """Test the hex, base64 and dict serialization properties.""" + k768 = pykyber.Kyber768() + + # Keypair serialization + assert k768.public_key_hex == k768.public_key.hex() + assert k768.secret_key_hex == k768.secret_key.hex() + assert k768.public_key_b64 == base64.b64encode(k768.public_key).decode("utf-8") + assert k768.secret_key_b64 == base64.b64encode(k768.secret_key).decode("utf-8") + + d = k768.to_dict() + assert d["public_key"] == k768.public_key_hex + assert d["secret_key"] == k768.secret_key_hex + + # EncapsulationResult serialization + res = k768.encapsulate() + assert res.ciphertext_hex == res.ciphertext.hex() + assert res.shared_secret_hex == res.shared_secret.hex() + assert res.ciphertext_b64 == base64.b64encode(res.ciphertext).decode("utf-8") + assert res.shared_secret_b64 == base64.b64encode(res.shared_secret).decode("utf-8") + + rd = res.to_dict() + assert rd["ciphertext"] == res.ciphertext_hex + assert rd["shared_secret"] == res.shared_secret_hex + + +def test_from_keys(): + """Test creating a Keypair from existing keys.""" + k768 = pykyber.Kyber768() + pk = k768.public_key + sk = k768.secret_key + + k768_new = pykyber.Kyber768.from_keys(pk, sk) + assert k768_new.public_key == pk + assert k768_new.secret_key == sk + + # Verify it can still decapsulate + res = k768.encapsulate() + ss1 = k768.decapsulate(res.ciphertext) + ss2 = k768_new.decapsulate(res.ciphertext) + assert ss1 == ss2 + + +def test_kyber1024_fips203_parameters(): + """Verify that Kyber-1024 uses FIPS-203 (NIST) compliant sizes.""" + # FIPS 203 specifies du=11, dv=5 for ML-KEM-1024 + # Ciphertext size = k*32*du + 32*dv = 4*32*11 + 32*5 = 1408 + 160 = 1568 bytes + k1024 = pykyber.Kyber1024() + result = k1024.encapsulate() + assert len(result.ciphertext) == 1568 + assert len(k1024.public_key) == 1568 # 4*384 + 32 = 1536 + 32 = 1568 + + # Verify that we can successfully decapsulate with these sizes + ss = k1024.decapsulate(result.ciphertext) + assert len(ss) == 32 + + +def test_deterministic_domain_separation(): + """Test that the same seed produces different keys for different variants.""" + seed = bytes(range(64)) + + k512 = pykyber.Kyber512(seed=seed) + k768 = pykyber.Kyber768(seed=seed) + k1024 = pykyber.Kyber1024(seed=seed) + + # Public keys should all be different + pks = [k512.public_key, k768.public_key, k1024.public_key] + assert len(set(pks)) == 3 + + # Secret keys should all be different + sks = [k512.secret_key, k768.secret_key, k1024.secret_key] + assert len(set(sks)) == 3 + + +def test_keypair_unpacking(): + """Test tuple unpacking of Keypair.""" + pk, sk = pykyber.Kyber768() + assert isinstance(pk, bytes) + assert isinstance(sk, bytes) + assert len(pk) == 1184 + assert len(sk) == 2400 + + +def test_encapsulation_result_unpacking(): + """Test tuple unpacking of EncapsulationResult.""" + keypair = pykyber.Kyber768() + ct, ss = keypair.encapsulate() + assert isinstance(ct, bytes) + assert isinstance(ss, bytes) + assert len(ct) == 1088 + assert len(ss) == 32 + + +def test_encapsulation_result_hex_b64(): + """Test hex and base64 properties of EncapsulationResult.""" + keypair = pykyber.Kyber768() + res = keypair.encapsulate() + + assert res.ciphertext_hex == res.ciphertext.hex() + assert res.shared_secret_hex == res.shared_secret.hex() + + # Simple check for base64 (should be longer than hex) + assert len(res.ciphertext_b64) > len(res.ciphertext_hex) / 2 + assert len(res.shared_secret_b64) == 44 # 32 bytes -> 44 chars in b64 + + # Test to_dict + d = res.to_dict() + assert d["ciphertext"] == res.ciphertext_hex + assert d["shared_secret"] == res.shared_secret_hex diff --git a/python/tests/test_kyber.py b/python/tests/test_kyber.py new file mode 100644 index 0000000..1b787d3 --- /dev/null +++ b/python/tests/test_kyber.py @@ -0,0 +1,1572 @@ +import pytest +import pykyber + + +# Class-based API tests + +def test_kyber768_new_api(): + """Test new Kyber768 API: keypair = Kyber768(); result = keypair.encapsulate()""" + keypair = pykyber.Kyber768() + + result = keypair.encapsulate() + shared_secret2 = keypair.decapsulate(result.ciphertext) + + assert len(keypair.public_key) == 1184 + assert len(keypair.secret_key) == 2400 + assert len(result.ciphertext) == 1088 + assert len(result.shared_secret) == 32 + assert result.shared_secret == shared_secret2 + + +def test_kyber512_new_api(): + """Test new Kyber512 API.""" + keypair = pykyber.Kyber512() + + result = keypair.encapsulate() + shared_secret2 = keypair.decapsulate(result.ciphertext) + + assert len(keypair.public_key) == 800 + assert len(keypair.secret_key) == 1632 + assert len(result.ciphertext) == 768 + assert len(result.shared_secret) == 32 + assert result.shared_secret == shared_secret2 + + +def test_kyber1024_new_api(): + """Test new Kyber1024 API.""" + keypair = pykyber.Kyber1024() + + result = keypair.encapsulate() + shared_secret2 = keypair.decapsulate(result.ciphertext) + + assert len(keypair.public_key) == 1568 + assert len(keypair.secret_key) == 3168 + assert len(result.ciphertext) == 1568 + assert len(result.shared_secret) == 32 + assert result.shared_secret == shared_secret2 + + +def test_keypair_repr(): + """Test __repr__ methods.""" + keypair = pykyber.Kyber768() + r = repr(keypair) + assert 'Keypair' in r + assert 'public_key' in r + assert 'secret_key' in r + + result = keypair.encapsulate() + r = repr(result) + assert 'EncapsulationResult' in r + + +def test_keypair_generation(): + """Test that keypair generation produces valid keys.""" + pk, sk = pykyber._generate_keypair() + assert len(pk) == 1184 + assert len(sk) == 2400 + + +def test_encapsulate_decapsulate(): + """Test that encapsulate and decapsulate work correctly.""" + pk, sk = pykyber._generate_keypair() + ct, ss = pykyber._encapsulate(pk) + assert len(ct) == 1088 + assert len(ss) == 32 + ss2 = pykyber._decapsulate(ct, sk) + assert ss == ss2 + + +def test_invalid_public_key_length(): + """Test that invalid public key length raises an error.""" + with pytest.raises(Exception): + pykyber._encapsulate(b"short") + + +def test_invalid_ciphertext_length(): + """Test that invalid ciphertext length raises an error.""" + sk = bytes(2400) + with pytest.raises(Exception): + pykyber._decapsulate(b"short", sk) + + +def test_invalid_secret_key_length(): + """Test that invalid secret key length raises an error.""" + ct = bytes(1088) + with pytest.raises(Exception): + pykyber._decapsulate(ct, b"short") + + +def test_different_keypairs_produce_different_secrets(): + """Test that different keypairs produce different shared secrets.""" + pk1, sk1 = pykyber._generate_keypair() + pk2, sk2 = pykyber._generate_keypair() + + ct1, ss1 = pykyber._encapsulate(pk1) + ct2, ss2 = pykyber._encapsulate(pk2) + + ss1_dec = pykyber._decapsulate(ct1, sk1) + ss2_dec = pykyber._decapsulate(ct2, sk2) + + assert ss1 == ss1_dec + assert ss2 == ss2_dec + assert ss1 != ss2 + + +def test_wrong_secret_key_fails(): + """Test that using wrong secret key produces different result.""" + pk, sk = pykyber._generate_keypair() + _, wrong_sk = pykyber._generate_keypair() + + ct, ss = pykyber._encapsulate(pk) + ss_wrong = pykyber._decapsulate(ct, wrong_sk) + + assert ss != ss_wrong + + +def test_keypair_512(): + """Test keypair generation for Kyber-512.""" + pk, sk = pykyber._keypair_512() + assert len(pk) == 800 + assert len(sk) == 1632 + + +def test_keypair_768(): + """Test keypair generation for Kyber-768.""" + pk, sk = pykyber._keypair_768() + assert len(pk) == 1184 + assert len(sk) == 2400 + + +def test_keypair_1024(): + """Test keypair generation for Kyber-1024.""" + pk, sk = pykyber._keypair_1024() + assert len(pk) == 1568 + assert len(sk) == 3168 + + +def test_deterministic_decapsulation(): + """Test that decapsulation is deterministic given the same ciphertext.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + + results = [pykyber._decapsulate(ct, sk) for _ in range(5)] + assert len(set(results)) == 1 + + +def test_ciphertext_randomness(): + """Test that ciphertexts are random.""" + pk, sk = pykyber._generate_keypair() + ciphertexts = [pykyber._encapsulate(pk)[0] for _ in range(5)] + assert len(set(ciphertexts)) > 1 + + +def test_empty_public_key(): + """Test that empty public key raises error.""" + with pytest.raises(Exception): + pykyber._encapsulate(b"") + + +def test_empty_ciphertext(): + """Test that empty ciphertext raises error.""" + pk, sk = pykyber._generate_keypair() + with pytest.raises(Exception): + pykyber._decapsulate(b"", sk) + + +def test_empty_secret_key(): + """Test that empty secret key raises error.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + with pytest.raises(Exception): + pykyber._decapsulate(ct, b"") + + +def test_boundary_public_key_length(): + """Test with public key that's 1 byte too short.""" + pk, sk = pykyber._generate_keypair() + short_pk = pk[:-1] + with pytest.raises(Exception): + pykyber._encapsulate(short_pk) + + +def test_boundary_secret_key_length(): + """Test with secret key that's 1 byte too short.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + short_sk = sk[:-1] + with pytest.raises(Exception): + pykyber._decapsulate(ct, short_sk) + + +def test_boundary_ciphertext_length(): + """Test with ciphertext that's 1 byte too short.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + short_ct = ct[:-1] + with pytest.raises(Exception): + pykyber._decapsulate(short_ct, sk) + + +def test_all_zero_keypair(): + """Test that generated keys are not all zeros.""" + pk, sk = pykyber._generate_keypair() + assert any(b != 0 for b in pk) + assert any(b != 0 for b in sk) + + +def test_shared_secret_not_all_zeros(): + """Test that shared secrets are not all zeros.""" + pk, sk = pykyber._generate_keypair() + ct, ss = pykyber._encapsulate(pk) + assert any(b != 0 for b in ss) + ss2 = pykyber._decapsulate(ct, sk) + assert any(b != 0 for b in ss2) + + +def test_multiple_encapsulations(): + """Test multiple encapsulations with same keypair.""" + pk, sk = pykyber._generate_keypair() + + results = [] + for _ in range(10): + ct, ss = pykyber._encapsulate(pk) + ss_dec = pykyber._decapsulate(ct, sk) + results.append((ct, ss, ss_dec)) + + # All should decapsulate correctly + for ct, ss, ss_dec in results: + assert ss == ss_dec + + # Ciphertexts should mostly be different (random) + ciphertexts = [r[0] for r in results] + assert len(set(ciphertexts)) > 1 + + +def test_keypair_768_function(): + """Test that keypair_768 function works.""" + pk, sk = pykyber._keypair_768() + ct, ss = pykyber._encapsulate(pk) + ss2 = pykyber._decapsulate(ct, sk) + assert ss == ss2 + + +def test_keypair_512_function(): + """Test that keypair_512 function works.""" + pk, sk = pykyber._keypair_512() + ct, ss = pykyber._encapsulate_512(pk) + ss2 = pykyber._decapsulate_512(ct, sk) + assert ss == ss2 + + +def test_keypair_1024_function(): + """Test that keypair_1024 function works.""" + pk, sk = pykyber._keypair_1024() + ct, ss = pykyber._encapsulate_1024(pk) + ss2 = pykyber._decapsulate_1024(ct, sk) + assert ss == ss2 + + +def test_encapsulate_512(): + """Test encapsulate_512 function.""" + pk, sk = pykyber._keypair_512() + ct, ss = pykyber._encapsulate_512(pk) + ss2 = pykyber._decapsulate_512(ct, sk) + assert ss == ss2 + + +def test_encapsulate_768(): + """Test encapsulate_768 function.""" + pk, sk = pykyber._keypair_768() + ct, ss = pykyber._encapsulate_768(pk) + ss2 = pykyber._decapsulate_768(ct, sk) + assert ss == ss2 + + +def test_encapsulate_1024(): + """Test encapsulate_1024 function.""" + pk, sk = pykyber._keypair_1024() + ct, ss = pykyber._encapsulate_1024(pk) + ss2 = pykyber._decapsulate_1024(ct, sk) + assert ss == ss2 + + +def test_decapsulate_512(): + """Test decapsulate_512 function.""" + pk, sk = pykyber._keypair_512() + ct, ss = pykyber._encapsulate_512(pk) + ss2 = pykyber._decapsulate_512(ct, sk) + assert ss == ss2 + + +def test_decapsulate_768(): + """Test decapsulate_768 function.""" + pk, sk = pykyber._keypair_768() + ct, ss = pykyber._encapsulate(pk) + ss2 = pykyber._decapsulate_768(ct, sk) + assert ss == ss2 + + +def test_decapsulate_1024(): + """Test decapsulate_1024 function.""" + pk, sk = pykyber._keypair_1024() + ct, ss = pykyber._encapsulate_1024(pk) + ss2 = pykyber._decapsulate_1024(ct, sk) + assert ss == ss2 + + +def test_same_ciphertext_decapsulation(): + """Test that same ciphertext always decapsulates to same secret.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + + results = [pykyber._decapsulate(ct, sk) for _ in range(10)] + assert all(r == results[0] for r in results) + + +def test_interoperability_different_calls(): + """Test that different function variants work together.""" + pk, sk = pykyber._generate_keypair() + + # Mix and match + ct1, ss1 = pykyber._encapsulate(pk) + ss1_dec = pykyber._decapsulate(ct1, sk) + assert ss1 == ss1_dec + + ct2, ss2 = pykyber._encapsulate_768(pk) + ss2_dec = pykyber._decapsulate_768(ct2, sk) + assert ss2 == ss2_dec + + +def test_zero_keypair_not_rejected(): + """Test that zero public key is not rejected (produces weak output).""" + zero_pk = bytes(800) + # Kyber doesn't reject zero keys - it will produce output (but weak) + ct, ss = pykyber._encapsulate_512(zero_pk) + assert len(ct) == 768 + assert len(ss) == 32 + + +def test_very_long_input_rejection(): + """Test that too long inputs are handled.""" + pk, sk = pykyber._generate_keypair() + long_pk = pk + b"extra" + with pytest.raises(Exception): + pykyber._encapsulate(long_pk) + + +# Key exchange tests (Alice and Bob) + +def test_key_exchange_kyber512(): + """Test Alice and Bob can exchange keys using Kyber512.""" + # Alice generates a keypair + alice_keypair = pykyber.Kyber512() + alice_public_key = alice_keypair.public_key + + # Bob encapsulates using Alice's public key (no keypair needed) + result = pykyber.Kyber512.encapsulate(alice_public_key) + bob_shared_secret = result.shared_secret + ciphertext = result.ciphertext + + # Alice decapsulates using her keypair + alice_shared_secret = alice_keypair.decapsulate(ciphertext) + + # Both should have the same shared secret + assert alice_shared_secret == bob_shared_secret + + +def test_key_exchange_kyber768(): + """Test Alice and Bob can exchange keys using Kyber768.""" + # Alice generates a keypair + alice_keypair = pykyber.Kyber768() + alice_public_key = alice_keypair.public_key + + # Bob encapsulates using Alice's public key (no keypair needed) + result = pykyber.Kyber768.encapsulate(alice_public_key) + bob_shared_secret = result.shared_secret + ciphertext = result.ciphertext + + # Alice decapsulates using her keypair + alice_shared_secret = alice_keypair.decapsulate(ciphertext) + + # Both should have the same shared secret + assert alice_shared_secret == bob_shared_secret + + +def test_key_exchange_kyber1024(): + """Test Alice and Bob can exchange keys using Kyber1024.""" + # Alice generates a keypair + alice_keypair = pykyber.Kyber1024() + alice_public_key = alice_keypair.public_key + + # Bob encapsulates using Alice's public key (no keypair needed) + result = pykyber.Kyber1024.encapsulate(alice_public_key) + bob_shared_secret = result.shared_secret + ciphertext = result.ciphertext + + # Alice decapsulates using her keypair + alice_shared_secret = alice_keypair.decapsulate(ciphertext) + + # Both should have the same shared secret + assert alice_shared_secret == bob_shared_secret + + +def test_encapsulate_static_method_kyber512(): + """Test Kyber512.encapsulate() static method works.""" + # Generate a keypair to get a public key + keypair = pykyber.Kyber512() + pk = keypair.public_key + + # Use static method to encapsulate + result = pykyber.Kyber512.encapsulate(pk) + + assert isinstance(result, pykyber._kyber.EncapsulationResult) + assert len(result.ciphertext) == 768 + assert len(result.shared_secret) == 32 + + # Can decapsulate with keypair + ss = keypair.decapsulate(result.ciphertext) + assert ss == result.shared_secret + + +def test_encapsulate_static_method_kyber768(): + """Test Kyber768.encapsulate() static method works.""" + keypair = pykyber.Kyber768() + pk = keypair.public_key + + result = pykyber.Kyber768.encapsulate(pk) + + assert isinstance(result, pykyber._kyber.EncapsulationResult) + assert len(result.ciphertext) == 1088 + assert len(result.shared_secret) == 32 + + ss = keypair.decapsulate(result.ciphertext) + assert ss == result.shared_secret + + +def test_encapsulate_static_method_kyber1024(): + """Test Kyber1024.encapsulate() static method works.""" + keypair = pykyber.Kyber1024() + pk = keypair.public_key + + result = pykyber.Kyber1024.encapsulate(pk) + + assert isinstance(result, pykyber._kyber.EncapsulationResult) + assert len(result.ciphertext) == 1568 + assert len(result.shared_secret) == 32 + + ss = keypair.decapsulate(result.ciphertext) + assert ss == result.shared_secret + + +# Error handling tests - comprehensive + +def test_kyber_error_can_be_imported(): + """Test that KyberError can be imported.""" + assert pykyber.KyberError is not None + assert issubclass(pykyber.KyberError, Exception) + + +def test_invalid_public_key_length_raises_error(): + """Test that invalid public key length raises KyberError.""" + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._encapsulate(b"short") + assert "Invalid public key" in str(exc_info.value) + + +def test_invalid_ciphertext_length_raises_error(): + """Test that invalid ciphertext length raises KyberError.""" + sk = bytes(2400) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate(b"short", sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_invalid_secret_key_length_raises_error(): + """Test that invalid secret key length raises KyberError.""" + ct = bytes(1088) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate(ct, b"short") + assert "Invalid input for 'sk'" in str(exc_info.value) + + +def test_empty_public_key_raises_error(): + """Test that empty public key raises KyberError.""" + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._encapsulate(b"") + assert "Invalid public key" in str(exc_info.value) + + +def test_empty_ciphertext_raises_error(): + """Test that empty ciphertext raises KyberError.""" + pk, sk = pykyber._generate_keypair() + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate(b"", sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_empty_secret_key_raises_error(): + """Test that empty secret key raises KyberError.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate(ct, b"") + assert "Invalid input for 'sk'" in str(exc_info.value) + + +def test_very_long_input_raises_error(): + """Test that too long inputs raise KyberError.""" + pk, sk = pykyber._generate_keypair() + long_pk = pk + b"extra" + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._encapsulate(long_pk) + assert "Invalid public key" in str(exc_info.value) + + +def test_kyber512_invalid_public_key_length(): + """Test Kyber512 with invalid public key length.""" + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._encapsulate_512(b"short") + assert "Invalid public key" in str(exc_info.value) + + +def test_kyber512_invalid_ciphertext_length(): + """Test Kyber512 with invalid ciphertext length.""" + sk = bytes(1632) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate_512(b"short", sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_kyber512_invalid_secret_key_length(): + """Test Kyber512 with invalid secret key length.""" + ct = bytes(768) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate_512(ct, b"short") + assert "Invalid input for 'sk'" in str(exc_info.value) + + +def test_kyber1024_invalid_public_key_length(): + """Test Kyber1024 with invalid public key length.""" + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._encapsulate_1024(b"short") + assert "Invalid public key" in str(exc_info.value) + + +def test_kyber1024_invalid_ciphertext_length(): + """Test Kyber1024 with invalid ciphertext length.""" + sk = bytes(3168) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate_1024(b"short", sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_kyber1024_invalid_secret_key_length(): + """Test Kyber1024 with invalid secret key length.""" + ct = bytes(1568) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate_1024(ct, b"short") + assert "Invalid input for 'sk'" in str(exc_info.value) + + +def test_boundary_public_key_length_512(): + """Test with Kyber512 public key that's 1 byte too short.""" + pk, _ = pykyber._keypair_512() + short_pk = pk[:-1] + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._encapsulate_512(short_pk) + assert "Invalid public key" in str(exc_info.value) + assert "800" in str(exc_info.value) + assert "799" in str(exc_info.value) + + +def test_boundary_public_key_length_768(): + """Test with Kyber768 public key that's 1 byte too short.""" + pk, _ = pykyber._keypair_768() + short_pk = pk[:-1] + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._encapsulate(short_pk) + assert "Invalid public key" in str(exc_info.value) + assert "1184" in str(exc_info.value) + assert "1183" in str(exc_info.value) + + +def test_boundary_public_key_length_1024(): + """Test with Kyber1024 public key that's 1 byte too short.""" + pk, _ = pykyber._keypair_1024() + short_pk = pk[:-1] + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._encapsulate_1024(short_pk) + assert "Invalid public key" in str(exc_info.value) + assert "1568" in str(exc_info.value) + assert "1567" in str(exc_info.value) + + +def test_boundary_secret_key_length_512(): + """Test with Kyber512 secret key that's 1 byte too short.""" + _, sk = pykyber._keypair_512() + ct, _ = pykyber._encapsulate_512(sk[:800]) + short_sk = sk[:-1] + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate_512(ct, short_sk) + assert "Invalid input for 'sk'" in str(exc_info.value) + + +def test_boundary_secret_key_length_768(): + """Test with Kyber768 secret key that's 1 byte too short.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + short_sk = sk[:-1] + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate(ct, short_sk) + assert "Invalid input for 'sk'" in str(exc_info.value) + + +def test_boundary_secret_key_length_1024(): + """Test with Kyber1024 secret key that's 1 byte too short.""" + pk, sk = pykyber._keypair_1024() + ct, _ = pykyber._encapsulate_1024(pk) + short_sk = sk[:-1] + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate_1024(ct, short_sk) + assert "Invalid input for 'sk'" in str(exc_info.value) + + +def test_boundary_ciphertext_length_512(): + """Test with Kyber512 ciphertext that's 1 byte too short.""" + pk, sk = pykyber._keypair_512() + ct, _ = pykyber._encapsulate_512(pk) + short_ct = ct[:-1] + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate_512(short_ct, sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_boundary_ciphertext_length_768(): + """Test with Kyber768 ciphertext that's 1 byte too short.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + short_ct = ct[:-1] + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate(short_ct, sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_boundary_ciphertext_length_1024(): + """Test with Kyber1024 ciphertext that's 1 byte too short.""" + pk, sk = pykyber._keypair_1024() + ct, _ = pykyber._encapsulate_1024(pk) + short_ct = ct[:-1] + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate_1024(short_ct, sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_error_can_be_caught_as_exception(): + """Test that KyberError can be caught as generic Exception.""" + try: + pykyber._encapsulate(b"short") + assert False, "Should have raised" + except Exception as e: + assert isinstance(e, pykyber.KyberError) + + +def test_error_message_contains_useful_info(): + """Test that error messages contain parameter name and sizes.""" + pk, sk = pykyber._generate_keypair() + + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._encapsulate(b"x" * 100) + msg = str(exc_info.value) + assert "public key" in msg + assert "expected" in msg or "bytes" in msg + + ct, _ = pykyber._encapsulate(pk) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate(ct, b"x" * 100) + msg = str(exc_info.value) + assert "sk" in msg + + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber._decapsulate(b"x" * 100, sk) + msg = str(exc_info.value) + assert "ct" in msg + + +def test_wrong_key_type_raises_error(): + """Test that passing wrong types raises appropriate error.""" + with pytest.raises((pykyber.KyberError, TypeError)): + pykyber._encapsulate(12345) + + with pytest.raises((pykyber.KyberError, TypeError)): + pykyber._decapsulate(None, None) + + +def test_all_variants_of_error(): + """Test that different error variants produce appropriate messages.""" + with pytest.raises(pykyber.KyberError): + pykyber._encapsulate(b"") + + with pytest.raises(pykyber.KyberError): + pk, _ = pykyber._generate_keypair() + pykyber._decapsulate(pk, b"") + + +def test_multiple_error_types_in_sequence(): + """Test catching different errors in sequence.""" + errors = [] + + try: + pykyber._encapsulate(b"x") + except pykyber.KyberError: + errors.append("invalid_pk") + + pk, sk = pykyber._generate_keypair() + try: + pykyber._decapsulate(b"x", sk) + except pykyber.KyberError: + errors.append("invalid_ct") + + try: + pykyber._decapsulate(pk, b"x") + except pykyber.KyberError: + errors.append("invalid_sk") + + assert len(errors) == 3 + assert "invalid_pk" in errors + assert "invalid_ct" in errors + assert "invalid_sk" in errors + + +def test_error_is_subclass_of_exception(): + """Test that KyberError is properly a subclass of Exception.""" + assert issubclass(pykyber.KyberError, Exception) + e = pykyber.KyberError("test") + assert isinstance(e, Exception) + assert str(e) == "test" + + +def test_class_api_error_handling_kyber512(): + """Test error handling in Kyber512 class API.""" + keypair = pykyber.Kyber512() + + with pytest.raises(pykyber.KyberError): + pykyber.Kyber512.encapsulate(b"short") + + with pytest.raises(pykyber.KyberError): + keypair.decapsulate(b"short") + + +def test_class_api_error_handling_kyber768(): + """Test error handling in Kyber768 class API.""" + keypair = pykyber.Kyber768() + + with pytest.raises(pykyber.KyberError): + pykyber.Kyber768.encapsulate(b"short") + + with pytest.raises(pykyber.KyberError): + keypair.decapsulate(b"short") + + +def test_class_api_error_handling_kyber1024(): + """Test error handling in Kyber1024 class API.""" + keypair = pykyber.Kyber1024() + + with pytest.raises(pykyber.KyberError): + pykyber.Kyber1024.encapsulate(b"short") + + with pytest.raises(pykyber.KyberError): + keypair.decapsulate(b"short") + + +def test_error_with_none_values(): + """Test that None values raise appropriate errors.""" + with pytest.raises((pykyber.KyberError, TypeError)): + pykyber._encapsulate(None) + + with pytest.raises((pykyber.KyberError, TypeError)): + pykyber._decapsulate(None, b"x" * 2400) + + with pytest.raises((pykyber.KyberError, TypeError)): + pykyber._decapsulate(b"x" * 1088, None) + + +def test_error_with_list_input(): + """Test that list input raises appropriate error.""" + with pytest.raises((pykyber.KyberError, TypeError)): + pykyber._encapsulate([1, 2, 3]) + + with pytest.raises((pykyber.KyberError, TypeError)): + pykyber._decapsulate([1, 2, 3], b"x" * 2400) + + +def test_error_with_integer_input(): + """Test that integer input raises appropriate error.""" + with pytest.raises((pykyber.KyberError, TypeError)): + pykyber._encapsulate(12345) + + with pytest.raises((pykyber.KyberError, TypeError)): + pykyber._decapsulate(12345, b"x" * 2400) + + +def test_keypair_functions_error_handling(): + """Test that keypair functions still work correctly.""" + pk, sk = pykyber._keypair_512() + assert len(pk) == 800 + assert len(sk) == 1632 + + pk, sk = pykyber._keypair_768() + assert len(pk) == 1184 + assert len(sk) == 2400 + + pk, sk = pykyber._keypair_1024() + assert len(pk) == 1568 + assert len(sk) == 3168 + + +def test_user_can_handle_error_gracefully(): + """Test that a user can handle the error gracefully.""" + def safe_encapsulate(public_key): + """Example of user handling the error.""" + try: + return pykyber._encapsulate(public_key) + except pykyber.KyberError as e: + return f"Error: {e}" + except Exception as e: + return f"Unexpected error: {e}" + + result = safe_encapsulate(b"short") + assert "Error:" in result + assert "Invalid" in result + + pk, _ = pykyber._generate_keypair() + result = safe_encapsulate(pk) + ct, ss = result + assert len(ct) == 1088 + assert len(ss) == 32 + + +# Additional panic prevention tests + +def test_all_zero_public_key_512(): + """Test encapsulation with all-zero public key (Kyber512).""" + zero_pk = bytes(800) + ct, ss = pykyber._encapsulate_512(zero_pk) + assert len(ct) == 768 + assert len(ss) == 32 + + +def test_all_zero_public_key_768(): + """Test encapsulation with all-zero public key (Kyber768).""" + zero_pk = bytes(1184) + ct, ss = pykyber._encapsulate(zero_pk) + assert len(ct) == 1088 + assert len(ss) == 32 + + +def test_all_zero_public_key_1024(): + """Test encapsulation with all-zero public key (Kyber1024).""" + zero_pk = bytes(1568) + ct, ss = pykyber._encapsulate_1024(zero_pk) + assert len(ct) == 1568 + assert len(ss) == 32 + + +def test_all_ones_public_key_512(): + """Test encapsulation with all-ones public key (Kyber512).""" + ones_pk = bytes([255] * 800) + ct, ss = pykyber._encapsulate_512(ones_pk) + assert len(ct) == 768 + assert len(ss) == 32 + + +def test_all_ones_public_key_768(): + """Test encapsulation with all-ones public key (Kyber768).""" + ones_pk = bytes([255] * 1184) + ct, ss = pykyber._encapsulate(ones_pk) + assert len(ct) == 1088 + assert len(ss) == 32 + + +def test_all_ones_public_key_1024(): + """Test encapsulation with all-ones public key (Kyber1024).""" + ones_pk = bytes([255] * 1568) + ct, ss = pykyber._encapsulate_1024(ones_pk) + assert len(ct) == 1568 + assert len(ss) == 32 + + +def test_alternating_bits_public_key(): + """Test encapsulation with alternating bits public key.""" + alternating_pk = bytes([i % 2 for i in range(1184)]) + ct, ss = pykyber._encapsulate(alternating_pk) + assert len(ct) == 1088 + assert len(ss) == 32 + + +def test_decapsulate_with_all_zero_ciphertext_512(): + """Test decapsulation with all-zero ciphertext (Kyber512).""" + pk, sk = pykyber._keypair_512() + zero_ct = bytes(768) + ss = pykyber._decapsulate_512(zero_ct, sk) + assert len(ss) == 32 + + +def test_decapsulate_with_all_zero_ciphertext_768(): + """Test decapsulation with all-zero ciphertext (Kyber768).""" + pk, sk = pykyber._generate_keypair() + zero_ct = bytes(1088) + ss = pykyber._decapsulate(zero_ct, sk) + assert len(ss) == 32 + + +def test_decapsulate_with_all_zero_ciphertext_1024(): + """Test decapsulation with all-zero ciphertext (Kyber1024).""" + pk, sk = pykyber._keypair_1024() + zero_ct = bytes(1568) + ss = pykyber._decapsulate_1024(zero_ct, sk) + assert len(ss) == 32 + + +def test_decapsulate_with_all_ones_ciphertext(): + """Test decapsulation with all-ones ciphertext.""" + pk, sk = pykyber._generate_keypair() + ones_ct = bytes([255] * 1088) + ss = pykyber._decapsulate(ones_ct, sk) + assert len(ss) == 32 + + +def test_wrong_variant_ciphertext_decapsulate_512_with_768_ct(): + """Test using 768 ciphertext with 512 decapsulate - should fail gracefully.""" + pk_768, sk_768 = pykyber._generate_keypair() + pk_512, sk_512 = pykyber._keypair_512() + ct_768, _ = pykyber._encapsulate(pk_768) + with pytest.raises(pykyber.KyberError): + pykyber._decapsulate_512(ct_768, sk_512) + + +def test_wrong_variant_ciphertext_decapsulate_1024_with_768_ct(): + """Test using 768 ciphertext with 1024 decapsulate - should fail gracefully.""" + pk_768, sk_768 = pykyber._generate_keypair() + pk_1024, sk_1024 = pykyber._keypair_1024() + ct_768, _ = pykyber._encapsulate(pk_768) + with pytest.raises(pykyber.KyberError): + pykyber._decapsulate_1024(ct_768, sk_1024) + + +def test_wrong_variant_ciphertext_decapsulate_768_with_512_ct(): + """Test using 512 ciphertext with 768 decapsulate - should fail gracefully.""" + pk_512, sk_512 = pykyber._keypair_512() + pk_768, sk_768 = pykyber._generate_keypair() + ct_512, _ = pykyber._encapsulate_512(pk_512) + with pytest.raises(pykyber.KyberError): + pykyber._decapsulate(ct_512, sk_768) + + +def test_mismatched_keypair_ciphertext(): + """Test decapsulating ciphertext from different keypair.""" + keypair1 = pykyber.Kyber768() + keypair2 = pykyber.Kyber768() + result = keypair1.encapsulate() + ss_dec = keypair2.decapsulate(result.ciphertext) + assert ss_dec != result.shared_secret + + +def test_repeated_keypair_generation(): + """Test multiple rapid keypair generations don't panic.""" + for _ in range(100): + pk, sk = pykyber._generate_keypair() + assert len(pk) == 1184 + assert len(sk) == 2400 + + +def test_repeated_encapsulation(): + """Test multiple rapid encapsulations don't panic.""" + pk, _ = pykyber._generate_keypair() + for _ in range(100): + ct, ss = pykyber._encapsulate(pk) + assert len(ct) == 1088 + assert len(ss) == 32 + + +def test_repeated_decapsulation(): + """Test multiple rapid decapsulations don't panic.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + for _ in range(100): + ss = pykyber._decapsulate(ct, sk) + assert len(ss) == 32 + + +def test_very_large_valid_size_public_key(): + """Test with a public key that's much larger than expected - should fail gracefully.""" + huge_pk = bytes(10000) + with pytest.raises(pykyber.KyberError): + pykyber._encapsulate(huge_pk) + + +def test_very_large_valid_size_secret_key(): + """Test with a secret key that's much larger than expected - should fail gracefully.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + huge_sk = sk + bytes(10000) + with pytest.raises(pykyber.KyberError): + pykyber._decapsulate(ct, huge_sk) + + +def test_very_large_valid_size_ciphertext(): + """Test with a ciphertext that's much larger than expected - should fail gracefully.""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + huge_ct = ct + bytes(10000) + with pytest.raises(pykyber.KyberError): + pykyber._decapsulate(huge_ct, sk) + + +def test_boundary_minus_one_ciphertext_512(): + """Test with ciphertext that's 1 byte less than minimum (Kyber512).""" + pk, sk = pykyber._keypair_512() + ct, _ = pykyber._encapsulate_512(pk) + short_ct = ct[:-1] + with pytest.raises(pykyber.KyberError): + pykyber._decapsulate_512(short_ct, sk) + + +def test_boundary_minus_one_ciphertext_768(): + """Test with ciphertext that's 1 byte less than minimum (Kyber768).""" + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + short_ct = ct[:-1] + with pytest.raises(pykyber.KyberError): + pykyber._decapsulate(short_ct, sk) + + +def test_boundary_minus_one_ciphertext_1024(): + """Test with ciphertext that's 1 byte less than minimum (Kyber1024).""" + pk, sk = pykyber._keypair_1024() + ct, _ = pykyber._encapsulate_1024(pk) + short_ct = ct[:-1] + with pytest.raises(pykyber.KyberError): + pykyber._decapsulate_1024(short_ct, sk) + + +def test_max_uint8_values_secret_key(): + """Test decapsulation with max uint8 values in secret key - should not panic.""" + pk, sk = pykyber._generate_keypair() + max_sk = bytes([255] * len(sk)) + ct, _ = pykyber._encapsulate(pk) + ss = pykyber._decapsulate(ct, max_sk) + assert len(ss) == 32 + + +def test_max_uint8_values_ciphertext(): + """Test decapsulation with max uint8 values in ciphertext - should not panic.""" + pk, sk = pykyber._generate_keypair() + max_ct = bytes([255] * 1088) + ss = pykyber._decapsulate(max_ct, sk) + assert len(ss) == 32 + + +def test_interleaved_key_generation_and_encapsulation(): + """Test interleaved key generation and encapsulation.""" + for i in range(50): + if i % 3 == 0: + pk, _ = pykyber._keypair_512() + ct, ss = pykyber._encapsulate_512(pk) + elif i % 3 == 1: + pk, _ = pykyber._generate_keypair() + ct, ss = pykyber._encapsulate(pk) + else: + pk, _ = pykyber._keypair_1024() + ct, ss = pykyber._encapsulate_1024(pk) + assert len(ct) > 0 + assert len(ss) == 32 + + +def test_encapsulate_with_known_vector_512(): + """Test encapsulation produces expected output format.""" + pk = bytes(800) + ct, ss = pykyber._encapsulate_512(pk) + assert isinstance(ct, bytes) + assert isinstance(ss, bytes) + assert len(ct) == 768 + assert len(ss) == 32 + + +def test_keypair_with_specific_seed_issue(): + """Test that various seed patterns don't cause panic.""" + seeds = [ + bytes(32), + bytes([0] * 32), + bytes([255] * 32), + bytes([i for i in range(32)]), + bytes([31 - i for i in range(32)]), + ] + for seed in seeds: + pk, sk = pykyber._generate_keypair() + assert len(pk) == 1184 + assert len(sk) == 2400 + + +def test_concurrent_error_handling(): + """Test that error handling works correctly in sequence.""" + error_inputs = [ + (b"", b""), + (b"x", b"x" * 2400), + (b"x" * 1088, b"x"), + (b"x" * 100, b"x" * 100), + ] + for pk_test, sk_test in error_inputs: + try: + pykyber._encapsulate(pk_test) + except pykyber.KyberError: + pass + except Exception as e: + if "panicked" not in str(e).lower(): + raise AssertionError(f"Unexpected non-panic error: {e}") + + pk, sk = pykyber._generate_keypair() + ct, _ = pykyber._encapsulate(pk) + for ct_test, sk_test in error_inputs: + try: + pykyber._decapsulate(ct_test, sk_test) + except pykyber.KyberError: + pass + except Exception as e: + if "panicked" not in str(e).lower(): + raise AssertionError(f"Unexpected non-panic error: {e}") + + +# Static decapsulate method tests + +def test_static_decapsulate_kyber512(): + """Test static decapsulate method for Kyber512.""" + pk, sk = pykyber._keypair_512() + ct, ss = pykyber._encapsulate_512(pk) + ss_dec = pykyber.Kyber512.decapsulate(ct, sk) + assert ss == ss_dec + + +def test_static_decapsulate_kyber768(): + """Test static decapsulate method for Kyber768.""" + pk, sk = pykyber._keypair_768() + ct, ss = pykyber._encapsulate_768(pk) + ss_dec = pykyber.Kyber768.decapsulate(ct, sk) + assert ss == ss_dec + + +def test_static_decapsulate_kyber1024(): + """Test static decapsulate method for Kyber1024.""" + pk, sk = pykyber._keypair_1024() + ct, ss = pykyber._encapsulate_1024(pk) + ss_dec = pykyber.Kyber1024.decapsulate(ct, sk) + assert ss == ss_dec + + +def test_static_decapsulate_vs_instance_decapsulate_kyber512(): + """Test that static and instance decapsulate produce same result (Kyber512).""" + keypair = pykyber.Kyber512() + result = keypair.encapsulate() + ss_instance = keypair.decapsulate(result.ciphertext) + ss_static = pykyber.Kyber512.decapsulate(result.ciphertext, keypair.secret_key) + assert ss_instance == ss_static + + +def test_static_decapsulate_vs_instance_decapsulate_kyber768(): + """Test that static and instance decapsulate produce same result (Kyber768).""" + keypair = pykyber.Kyber768() + result = keypair.encapsulate() + ss_instance = keypair.decapsulate(result.ciphertext) + ss_static = pykyber.Kyber768.decapsulate(result.ciphertext, keypair.secret_key) + assert ss_instance == ss_static + + +def test_static_decapsulate_vs_instance_decapsulate_kyber1024(): + """Test that static and instance decapsulate produce same result (Kyber1024).""" + keypair = pykyber.Kyber1024() + result = keypair.encapsulate() + ss_instance = keypair.decapsulate(result.ciphertext) + ss_static = pykyber.Kyber1024.decapsulate(result.ciphertext, keypair.secret_key) + assert ss_instance == ss_static + + +def test_static_decapsulate_invalid_ciphertext_length_kyber512(): + """Test static decapsulate with invalid ciphertext length (Kyber512).""" + sk = bytes(1632) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber.Kyber512.decapsulate(b"short", sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_static_decapsulate_invalid_secret_key_length_kyber512(): + """Test static decapsulate with invalid secret key length (Kyber512).""" + ct = bytes(768) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber.Kyber512.decapsulate(ct, b"short") + assert "Invalid input for 'sk'" in str(exc_info.value) + + +def test_static_decapsulate_invalid_ciphertext_length_kyber768(): + """Test static decapsulate with invalid ciphertext length (Kyber768).""" + sk = bytes(2400) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber.Kyber768.decapsulate(b"short", sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_static_decapsulate_invalid_secret_key_length_kyber768(): + """Test static decapsulate with invalid secret key length (Kyber768).""" + ct = bytes(1088) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber.Kyber768.decapsulate(ct, b"short") + assert "Invalid input for 'sk'" in str(exc_info.value) + + +def test_static_decapsulate_invalid_ciphertext_length_kyber1024(): + """Test static decapsulate with invalid ciphertext length (Kyber1024).""" + sk = bytes(3168) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber.Kyber1024.decapsulate(b"short", sk) + assert "Invalid ciphertext" in str(exc_info.value) + + +def test_static_decapsulate_invalid_secret_key_length_kyber1024(): + """Test static decapsulate with invalid secret key length (Kyber1024).""" + ct = bytes(1568) + with pytest.raises(pykyber.KyberError) as exc_info: + pykyber.Kyber1024.decapsulate(ct, b"short") + assert "Invalid input for 'sk'" in str(exc_info.value) + + +# Stress tests + +def test_stress_kyber512_many_operations(): + """Stress test: many keypair generations and encapsulations (Kyber512).""" + for _ in range(1000): + pk, sk = pykyber._keypair_512() + ct, ss = pykyber._encapsulate_512(pk) + ss_dec = pykyber._decapsulate_512(ct, sk) + assert ss == ss_dec + + +def test_stress_kyber768_many_operations(): + """Stress test: many keypair generations and encapsulations (Kyber768).""" + for _ in range(1000): + pk, sk = pykyber._keypair_768() + ct, ss = pykyber._encapsulate_768(pk) + ss_dec = pykyber._decapsulate_768(ct, sk) + assert ss == ss_dec + + +def test_stress_kyber1024_many_operations(): + """Stress test: many keypair generations and encapsulations (Kyber1024).""" + for _ in range(1000): + pk, sk = pykyber._keypair_1024() + ct, ss = pykyber._encapsulate_1024(pk) + ss_dec = pykyber._decapsulate_1024(ct, sk) + assert ss == ss_dec + + +def test_stress_class_api_kyber512(): + """Stress test: class API with many operations (Kyber512).""" + for _ in range(1000): + keypair = pykyber.Kyber512() + result = keypair.encapsulate() + ss = keypair.decapsulate(result.ciphertext) + assert result.shared_secret == ss + + +def test_stress_class_api_kyber768(): + """Stress test: class API with many operations (Kyber768).""" + for _ in range(1000): + keypair = pykyber.Kyber768() + result = keypair.encapsulate() + ss = keypair.decapsulate(result.ciphertext) + assert result.shared_secret == ss + + +def test_stress_class_api_kyber1024(): + """Stress test: class API with many operations (Kyber1024).""" + for _ in range(1000): + keypair = pykyber.Kyber1024() + result = keypair.encapsulate() + ss = keypair.decapsulate(result.ciphertext) + assert result.shared_secret == ss + + +def test_stress_static_methods_kyber512(): + """Stress test: static methods without keypair (Kyber512).""" + for _ in range(1000): + pk, sk = pykyber._keypair_512() + result = pykyber.Kyber512.encapsulate(pk) + ss = pykyber.Kyber512.decapsulate(result.ciphertext, sk) + assert result.shared_secret == ss + + +def test_stress_static_methods_kyber768(): + """Stress test: static methods without keypair (Kyber768).""" + for _ in range(1000): + pk, sk = pykyber._keypair_768() + result = pykyber.Kyber768.encapsulate(pk) + ss = pykyber.Kyber768.decapsulate(result.ciphertext, sk) + assert result.shared_secret == ss + + +def test_stress_static_methods_kyber1024(): + """Stress test: static methods without keypair (Kyber1024).""" + for _ in range(1000): + pk, sk = pykyber._keypair_1024() + result = pykyber.Kyber1024.encapsulate(pk) + ss = pykyber.Kyber1024.decapsulate(result.ciphertext, sk) + assert result.shared_secret == ss + + +def test_stress_mixed_variants(): + """Stress test: mixed variants in sequence.""" + for _ in range(333): + pk_512, sk_512 = pykyber._keypair_512() + ct_512, ss_512 = pykyber._encapsulate_512(pk_512) + ss_512_dec = pykyber._decapsulate_512(ct_512, sk_512) + assert ss_512 == ss_512_dec + + pk_768, sk_768 = pykyber._keypair_768() + ct_768, ss_768 = pykyber._encapsulate_768(pk_768) + ss_768_dec = pykyber._decapsulate_768(ct_768, sk_768) + assert ss_768 == ss_768_dec + + pk_1024, sk_1024 = pykyber._keypair_1024() + ct_1024, ss_1024 = pykyber._encapsulate_1024(pk_1024) + ss_1024_dec = pykyber._decapsulate_1024(ct_1024, sk_1024) + assert ss_1024 == ss_1024_dec + + +# Performance tests + +def test_performance_kyber512_keypair_generation(): + """Performance test: measure time for 100 keypair generations (Kyber512).""" + import time + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pykyber._keypair_512() + elapsed = time.perf_counter() - start + + print(f"\nKyber512 keypair generation: {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 # Should complete 100 iterations in under 5 seconds + + +def test_performance_kyber768_keypair_generation(): + """Performance test: measure time for 100 keypair generations (Kyber768).""" + import time + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pykyber._keypair_768() + elapsed = time.perf_counter() - start + + print(f"\nKyber768 keypair generation: {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_performance_kyber1024_keypair_generation(): + """Performance test: measure time for 100 keypair generations (Kyber1024).""" + import time + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pykyber._keypair_1024() + elapsed = time.perf_counter() - start + + print(f"\nKyber1024 keypair generation: {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_performance_kyber512_encapsulation(): + """Performance test: measure time for 100 encapsulations (Kyber512).""" + import time + pk, _ = pykyber._keypair_512() + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pykyber._encapsulate_512(pk) + elapsed = time.perf_counter() - start + + print(f"\nKyber512 encapsulation: {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_performance_kyber768_encapsulation(): + """Performance test: measure time for 100 encapsulations (Kyber768).""" + import time + pk, _ = pykyber._keypair_768() + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pykyber._encapsulate_768(pk) + elapsed = time.perf_counter() - start + + print(f"\nKyber768 encapsulation: {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_performance_kyber1024_encapsulation(): + """Performance test: measure time for 100 encapsulations (Kyber1024).""" + import time + pk, _ = pykyber._keypair_1024() + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pykyber._encapsulate_1024(pk) + elapsed = time.perf_counter() - start + + print(f"\nKyber1024 encapsulation: {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_performance_kyber512_decapsulation(): + """Performance test: measure time for 100 decapsulations (Kyber512).""" + import time + pk, sk = pykyber._keypair_512() + ct, _ = pykyber._encapsulate_512(pk) + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pykyber._decapsulate_512(ct, sk) + elapsed = time.perf_counter() - start + + print(f"\nKyber512 decapsulation: {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_performance_kyber768_decapsulation(): + """Performance test: measure time for 100 decapsulations (Kyber768).""" + import time + pk, sk = pykyber._keypair_768() + ct, _ = pykyber._encapsulate_768(pk) + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pykyber._decapsulate_768(ct, sk) + elapsed = time.perf_counter() - start + + print(f"\nKyber768 decapsulation: {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_performance_kyber1024_decapsulation(): + """Performance test: measure time for 100 decapsulations (Kyber1024).""" + import time + pk, sk = pykyber._keypair_1024() + ct, _ = pykyber._encapsulate_1024(pk) + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pykyber._decapsulate_1024(ct, sk) + elapsed = time.perf_counter() - start + + print(f"\nKyber1024 decapsulation: {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_performance_class_api_kyber768(): + """Performance test: class API full key exchange (Kyber768).""" + import time + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + keypair = pykyber.Kyber768() + result = keypair.encapsulate() + keypair.decapsulate(result.ciphertext) + elapsed = time.perf_counter() - start + + print(f"\nKyber768 full key exchange (class API): {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_performance_static_methods_kyber768(): + """Performance test: static methods full key exchange (Kyber768).""" + import time + iterations = 100 + + start = time.perf_counter() + for _ in range(iterations): + pk, sk = pykyber._keypair_768() + result = pykyber.Kyber768.encapsulate(pk) + pykyber.Kyber768.decapsulate(result.ciphertext, sk) + elapsed = time.perf_counter() - start + + print(f"\nKyber768 full key exchange (static methods): {elapsed/iterations*1000:.3f} ms/op ({iterations} iterations)") + assert elapsed < 5.0 + + +def test_keypair_iter(): + """Test Keypair can be unpacked as tuple (public_key, secret_key).""" + keypair = pykyber.Kyber768() + pk, sk = keypair + + assert isinstance(pk, bytes) + assert isinstance(sk, bytes) + assert len(pk) == 1184 + assert len(sk) == 2400 + assert pk == keypair.public_key + assert sk == keypair.secret_key + + +def test_encapsulation_result_iter(): + """Test EncapsulationResult can be unpacked as tuple (ciphertext, shared_secret).""" + keypair = pykyber.Kyber768() + result = keypair.encapsulate() + ct, ss = result + + assert isinstance(ct, bytes) + assert isinstance(ss, bytes) + assert len(ct) == 1088 + assert len(ss) == 32 + assert ct == result.ciphertext + assert ss == result.shared_secret + + +def test_keypair_iter_all_variants(): + """Test Keypair __iter__ for all Kyber variants.""" + for cls, expected_pk_len, expected_sk_len in [ + (pykyber.Kyber512, 800, 1632), + (pykyber.Kyber768, 1184, 2400), + (pykyber.Kyber1024, 1568, 3168), + ]: + keypair = cls() + pk, sk = keypair + assert len(pk) == expected_pk_len + assert len(sk) == expected_sk_len + + +def test_encapsulation_result_iter_all_variants(): + """Test EncapsulationResult __iter__ for all Kyber variants.""" + for cls, expected_ct_len in [ + (pykyber.Kyber512, 768), + (pykyber.Kyber768, 1088), + (pykyber.Kyber1024, 1568), + ]: + keypair = cls() + result = keypair.encapsulate() + ct, ss = result + assert len(ct) == expected_ct_len + assert len(ss) == 32 + + +def test_keypair_as_tuple_in_function(): + """Test Keypair can be passed directly to functions expecting tuple.""" + pk, sk = pykyber.Kyber768() + + ciphertext, shared_secret = pykyber.Kyber768.encapsulate(pk) + decrypted = pykyber.Kyber768.decapsulate(ciphertext, sk) + + assert shared_secret == decrypted diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4c1eabb --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +# No runtime dependencies - pure Rust extension diff --git a/src/kyber/api.rs b/src/kyber/api.rs new file mode 100644 index 0000000..2cc0997 --- /dev/null +++ b/src/kyber/api.rs @@ -0,0 +1,114 @@ +use super::error::KyberError; +use super::kem::{crypto_kem_dec, crypto_kem_enc, crypto_kem_keypair}; +use super::params::*; +use rand_core::{CryptoRng, RngCore}; + +pub fn keypair(rng: &mut R) -> Result +where + R: RngCore + CryptoRng, +{ + let mut public = [0u8; KYBER_PUBLICKEYBYTES]; + let mut secret = [0u8; KYBER_SECRETKEYBYTES]; + crypto_kem_keypair(&mut public, &mut secret, rng, None)?; + Ok(Keypair { public, secret }) +} + +pub fn encapsulate(pk: &[u8], rng: &mut R) -> Encapsulated +where + R: CryptoRng + RngCore, +{ + if pk.len() != KYBER_PUBLICKEYBYTES { + return Err(KyberError::invalid_input( + "pk", + KYBER_PUBLICKEYBYTES, + pk.len(), + )); + } + let mut ct = [0u8; KYBER_CIPHERTEXTBYTES]; + let mut ss = [0u8; KYBER_SSBYTES]; + crypto_kem_enc(&mut ct, &mut ss, pk, rng, None)?; + Ok((ct, ss)) +} + +pub fn decapsulate(ct: &[u8], sk: &[u8]) -> Decapsulated { + if ct.len() != KYBER_CIPHERTEXTBYTES { + return Err(KyberError::invalid_input( + "ct", + KYBER_CIPHERTEXTBYTES, + ct.len(), + )); + } + if sk.len() != KYBER_SECRETKEYBYTES { + return Err(KyberError::invalid_input( + "sk", + KYBER_SECRETKEYBYTES, + sk.len(), + )); + } + let mut ss = [0u8; KYBER_SSBYTES]; + crypto_kem_dec(&mut ss, ct, sk)?; + Ok(ss) +} + +use zeroize::Zeroize; + +#[derive(Clone, Debug, Eq, PartialEq, Zeroize)] +#[zeroize(drop)] +pub struct Keypair { + pub public: PublicKey, + pub secret: SecretKey, +} + +impl Keypair { + pub fn generate(rng: &mut R) -> Result { + keypair(rng) + } +} + +struct DummyRng {} +impl CryptoRng for DummyRng {} +impl RngCore for DummyRng { + fn next_u32(&mut self) -> u32 { + panic!() + } + fn next_u64(&mut self) -> u64 { + panic!() + } + fn try_fill_bytes(&mut self, _dest: &mut [u8]) -> Result<(), rand_core::Error> { + panic!() + } + fn fill_bytes(&mut self, _dest: &mut [u8]) { + panic!() + } +} + +pub fn derive(seed: &[u8]) -> Result { + let mut public = [0u8; KYBER_PUBLICKEYBYTES]; + let mut secret = [0u8; KYBER_SECRETKEYBYTES]; + let mut _rng = DummyRng {}; + if seed.len() != 64 { + return Err(KyberError::invalid_input("seed", 64, seed.len())); + } + crypto_kem_keypair( + &mut public, + &mut secret, + &mut _rng, + Some((&seed[..32], &seed[32..])), + )?; + Ok(Keypair { public, secret }) +} + +pub fn public(sk: &[u8]) -> PublicKey { + let mut pk = [0u8; KYBER_INDCPA_PUBLICKEYBYTES]; + pk.copy_from_slice( + &sk[KYBER_INDCPA_SECRETKEYBYTES..KYBER_INDCPA_SECRETKEYBYTES + KYBER_INDCPA_PUBLICKEYBYTES], + ); + pk +} + +pub type PublicKey = [u8; KYBER_PUBLICKEYBYTES]; +pub type SecretKey = [u8; KYBER_SECRETKEYBYTES]; +pub type Encapsulated = Result<(Ciphertext, SharedSecret), KyberError>; +pub type Decapsulated = Result; +pub type Ciphertext = [u8; KYBER_CIPHERTEXTBYTES]; +pub type SharedSecret = [u8; KYBER_SSBYTES]; diff --git a/src/kyber/cbd.rs b/src/kyber/cbd.rs new file mode 100644 index 0000000..b820b5a --- /dev/null +++ b/src/kyber/cbd.rs @@ -0,0 +1,58 @@ +use super::params::KYBER_N; +use super::poly::Poly; + +fn load32_littleendian(x: &[u8]) -> u32 { + let mut r = x[0] as u32; + r |= (x[1] as u32) << 8; + r |= (x[2] as u32) << 16; + r |= (x[3] as u32) << 24; + r +} + +fn load24_littleendian(x: &[u8]) -> u32 { + let mut r = x[0] as u32; + r |= (x[1] as u32) << 8; + r |= (x[2] as u32) << 16; + r +} + +pub fn cbd2(r: &mut Poly, buf: &[u8]) { + let (mut d, mut t, mut a, mut b); + for i in 0..(KYBER_N / 8) { + t = load32_littleendian(&buf[4 * i..]); + d = t & 0x55555555; + d += (t >> 1) & 0x55555555; + for j in 0..8 { + a = ((d >> (4 * j)) & 0x3) as i16; + b = ((d >> (4 * j + 2)) & 0x3) as i16; + r.coeffs[8 * i + j] = a - b; + } + } +} + +pub fn cbd3(r: &mut Poly, buf: &[u8]) { + let (mut d, mut t, mut a, mut b); + for i in 0..(KYBER_N / 4) { + t = load24_littleendian(&buf[3 * i..]); + d = t & 0x00249249; + d += (t >> 1) & 0x00249249; + d += (t >> 2) & 0x00249249; + for j in 0..4 { + a = ((d >> (6 * j)) & 0x7) as i16; + b = ((d >> (6 * j + 3)) & 0x7) as i16; + r.coeffs[4 * i + j] = a - b; + } + } +} + +pub fn poly_cbd_eta1(r: &mut Poly, buf: &[u8]) { + cbd2(r, buf) +} + +pub fn poly_cbd_eta1_512(r: &mut Poly, buf: &[u8]) { + cbd3(r, buf) +} + +pub fn poly_cbd_eta2(r: &mut Poly, buf: &[u8]) { + cbd2(r, buf) +} diff --git a/src/kyber/error.rs b/src/kyber/error.rs new file mode 100644 index 0000000..2dba3e8 --- /dev/null +++ b/src/kyber/error.rs @@ -0,0 +1,45 @@ +#[derive(Debug, PartialEq)] +pub enum KyberError { + InvalidInput { + param: &'static str, + expected: usize, + actual: usize, + }, + Decapsulation, + RandomBytesGeneration, +} + +impl KyberError { + pub fn invalid_input(param: &'static str, expected: usize, actual: usize) -> Self { + KyberError::InvalidInput { + param, + expected, + actual, + } + } +} + +impl core::fmt::Display for KyberError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match *self { + KyberError::InvalidInput { + param, + expected, + actual, + } => { + write!( + f, + "Invalid input for '{}': expected {} bytes, got {}", + param, expected, actual + ) + } + KyberError::Decapsulation => write!( + f, + "Decapsulation Failure: unable to obtain shared secret from ciphertext" + ), + KyberError::RandomBytesGeneration => write!(f, "Random bytes generation failed"), + } + } +} + +impl std::error::Error for KyberError {} diff --git a/src/kyber/fips202.rs b/src/kyber/fips202.rs new file mode 100644 index 0000000..f064ce0 --- /dev/null +++ b/src/kyber/fips202.rs @@ -0,0 +1,466 @@ +#![allow(clippy::needless_range_loop, dead_code)] + +pub const SHAKE128_RATE: usize = 168; +const SHAKE256_RATE: usize = 136; +const SHA3_256_RATE: usize = 136; +const SHA3_512_RATE: usize = 72; +const NROUNDS: usize = 24; + +use zeroize::Zeroize; + +#[derive(Clone, Zeroize)] +#[zeroize(drop)] +pub struct KeccakState { + pub s: [u64; 25], + pub pos: usize, +} + +impl KeccakState { + pub fn new() -> Self { + KeccakState { + s: [0u64; 25], + pos: 0usize, + } + } + + pub fn reset(&mut self) { + self.s = [0u64; 25]; + self.pos = 0; + } +} + +/// Rotate left operation for 64-bit values. +/// Rotates `value` left by `offset` bits. +fn rotate_left(value: u64, offset: u64) -> u64 { + (value << offset) | (value >> (64 - offset)) +} + +/// Load 8 bytes from little-endian byte array into a 64-bit unsigned integer. +pub fn load_u64(bytes: &[u8]) -> u64 { + let mut result = 0u64; + for i in 0..8 { + result |= (bytes[i] as u64) << (8 * i); + } + result +} + +/// Store a 64-bit unsigned integer into little-endian byte array. +pub fn store_u64(bytes: &mut [u8], mut value: u64) { + for byte in bytes.iter_mut().take(8) { + *byte = value as u8; + value >>= 8; + } +} + +const KECCAKF_ROUNDCONSTANTS: [u64; NROUNDS] = [ + 0x0000000000000001, + 0x0000000000008082, + 0x800000000000808a, + 0x8000000080008000, + 0x000000000000808b, + 0x0000000080000001, + 0x8000000080008081, + 0x8000000000008009, + 0x000000000000008a, + 0x0000000000000088, + 0x0000000080008009, + 0x000000008000000a, + 0x000000008000808b, + 0x800000000000008b, + 0x8000000000008089, + 0x8000000000008003, + 0x8000000000008002, + 0x8000000000000080, + 0x000000000000800a, + 0x800000008000000a, + 0x8000000080008081, + 0x8000000000008080, + 0x0000000080000001, + 0x8000000080008008, +]; + +pub fn keccakf1600_statepermute(state: &mut [u64]) { + let mut aba = state[0]; + let mut abe = state[1]; + let mut abi = state[2]; + let mut abo = state[3]; + let mut abu = state[4]; + let mut aga = state[5]; + let mut age = state[6]; + let mut agi = state[7]; + let mut ago = state[8]; + let mut agu = state[9]; + let mut aka = state[10]; + let mut ake = state[11]; + let mut aki = state[12]; + let mut ako = state[13]; + let mut aku = state[14]; + let mut ama = state[15]; + let mut ame = state[16]; + let mut ami = state[17]; + let mut amo = state[18]; + let mut amu = state[19]; + let mut asa = state[20]; + let mut ase = state[21]; + let mut asi = state[22]; + let mut aso = state[23]; + let mut asu = state[24]; + + for round in (0..NROUNDS).step_by(2) { + let mut bca = aba ^ aga ^ aka ^ ama ^ asa; + let mut bce = abe ^ age ^ ake ^ ame ^ ase; + let mut bci = abi ^ agi ^ aki ^ ami ^ asi; + let mut bco = abo ^ ago ^ ako ^ amo ^ aso; + let mut bcu = abu ^ agu ^ aku ^ amu ^ asu; + + let mut da = bcu ^ rotate_left(bce, 1); + let mut de = bca ^ rotate_left(bci, 1); + let mut di = bce ^ rotate_left(bco, 1); + let mut d_o = bci ^ rotate_left(bcu, 1); + let mut du = bco ^ rotate_left(bca, 1); + + aba ^= da; + bca = aba; + age ^= de; + bce = rotate_left(age, 44); + aki ^= di; + bci = rotate_left(aki, 43); + amo ^= d_o; + bco = rotate_left(amo, 21); + asu ^= du; + bcu = rotate_left(asu, 14); + let mut eba = bca ^ ((!bce) & bci); + eba ^= KECCAKF_ROUNDCONSTANTS[round]; + let mut ebe = bce ^ ((!bci) & bco); + let mut ebi = bci ^ ((!bco) & bcu); + let mut ebo = bco ^ ((!bcu) & bca); + let mut ebu = bcu ^ ((!bca) & bce); + + abo ^= d_o; + bca = rotate_left(abo, 28); + agu ^= du; + bce = rotate_left(agu, 20); + aka ^= da; + bci = rotate_left(aka, 3); + ame ^= de; + bco = rotate_left(ame, 45); + asi ^= di; + bcu = rotate_left(asi, 61); + let mut ega = bca ^ ((!bce) & bci); + let mut ege = bce ^ ((!bci) & bco); + let mut egi = bci ^ ((!bco) & bcu); + let mut ego = bco ^ ((!bcu) & bca); + let mut egu = bcu ^ ((!bca) & bce); + + abe ^= de; + bca = rotate_left(abe, 1); + agi ^= di; + bce = rotate_left(agi, 6); + ako ^= d_o; + bci = rotate_left(ako, 25); + amu ^= du; + bco = rotate_left(amu, 8); + asa ^= da; + bcu = rotate_left(asa, 18); + let mut eka = bca ^ ((!bce) & bci); + let mut eke = bce ^ ((!bci) & bco); + let mut eki = bci ^ ((!bco) & bcu); + let mut eko = bco ^ ((!bcu) & bca); + let mut eku = bcu ^ ((!bca) & bce); + + abu ^= du; + bca = rotate_left(abu, 27); + aga ^= da; + bce = rotate_left(aga, 36); + ake ^= de; + bci = rotate_left(ake, 10); + ami ^= di; + bco = rotate_left(ami, 15); + aso ^= d_o; + bcu = rotate_left(aso, 56); + let mut ema = bca ^ ((!bce) & bci); + let mut eme = bce ^ ((!bci) & bco); + let mut emi = bci ^ ((!bco) & bcu); + let mut emo = bco ^ ((!bcu) & bca); + let mut emu = bcu ^ ((!bca) & bce); + + abi ^= di; + bca = rotate_left(abi, 62); + ago ^= d_o; + bce = rotate_left(ago, 55); + aku ^= du; + bci = rotate_left(aku, 39); + ama ^= da; + bco = rotate_left(ama, 41); + ase ^= de; + bcu = rotate_left(ase, 2); + let mut esa = bca ^ ((!bce) & bci); + let mut ese = bce ^ ((!bci) & bco); + let mut esi = bci ^ ((!bco) & bcu); + let mut eso = bco ^ ((!bcu) & bca); + let mut esu = bcu ^ ((!bca) & bce); + + bca = eba ^ ega ^ eka ^ ema ^ esa; + bce = ebe ^ ege ^ eke ^ eme ^ ese; + bci = ebi ^ egi ^ eki ^ emi ^ esi; + bco = ebo ^ ego ^ eko ^ emo ^ eso; + bcu = ebu ^ egu ^ eku ^ emu ^ esu; + + da = bcu ^ rotate_left(bce, 1); + de = bca ^ rotate_left(bci, 1); + di = bce ^ rotate_left(bco, 1); + d_o = bci ^ rotate_left(bcu, 1); + du = bco ^ rotate_left(bca, 1); + + eba ^= da; + bca = eba; + ege ^= de; + bce = rotate_left(ege, 44); + eki ^= di; + bci = rotate_left(eki, 43); + emo ^= d_o; + bco = rotate_left(emo, 21); + esu ^= du; + bcu = rotate_left(esu, 14); + aba = bca ^ ((!bce) & bci); + aba ^= KECCAKF_ROUNDCONSTANTS[round + 1]; + abe = bce ^ ((!bci) & bco); + abi = bci ^ ((!bco) & bcu); + abo = bco ^ ((!bcu) & bca); + abu = bcu ^ ((!bca) & bce); + + ebo ^= d_o; + bca = rotate_left(ebo, 28); + egu ^= du; + bce = rotate_left(egu, 20); + eka ^= da; + bci = rotate_left(eka, 3); + eme ^= de; + bco = rotate_left(eme, 45); + esi ^= di; + bcu = rotate_left(esi, 61); + aga = bca ^ ((!bce) & bci); + age = bce ^ ((!bci) & bco); + agi = bci ^ ((!bco) & bcu); + ago = bco ^ ((!bcu) & bca); + agu = bcu ^ ((!bca) & bce); + + ebe ^= de; + bca = rotate_left(ebe, 1); + egi ^= di; + bce = rotate_left(egi, 6); + eko ^= d_o; + bci = rotate_left(eko, 25); + emu ^= du; + bco = rotate_left(emu, 8); + esa ^= da; + bcu = rotate_left(esa, 18); + aka = bca ^ ((!bce) & bci); + ake = bce ^ ((!bci) & bco); + aki = bci ^ ((!bco) & bcu); + ako = bco ^ ((!bcu) & bca); + aku = bcu ^ ((!bca) & bce); + + ebu ^= du; + bca = rotate_left(ebu, 27); + ega ^= da; + bce = rotate_left(ega, 36); + eke ^= de; + bci = rotate_left(eke, 10); + emi ^= di; + bco = rotate_left(emi, 15); + eso ^= d_o; + bcu = rotate_left(eso, 56); + ama = bca ^ ((!bce) & bci); + ame = bce ^ ((!bci) & bco); + ami = bci ^ ((!bco) & bcu); + amo = bco ^ ((!bcu) & bca); + amu = bcu ^ ((!bca) & bce); + + ebi ^= di; + bca = rotate_left(ebi, 62); + ego ^= d_o; + bce = rotate_left(ego, 55); + eku ^= du; + bci = rotate_left(eku, 39); + ema ^= da; + bco = rotate_left(ema, 41); + ese ^= de; + bcu = rotate_left(ese, 2); + asa = bca ^ ((!bce) & bci); + ase = bce ^ ((!bci) & bco); + asi = bci ^ ((!bco) & bcu); + aso = bco ^ ((!bcu) & bca); + asu = bcu ^ ((!bca) & bce); + } + + state[0] = aba; + state[1] = abe; + state[2] = abi; + state[3] = abo; + state[4] = abu; + state[5] = aga; + state[6] = age; + state[7] = agi; + state[8] = ago; + state[9] = agu; + state[10] = aka; + state[11] = ake; + state[12] = aki; + state[13] = ako; + state[14] = aku; + state[15] = ama; + state[16] = ame; + state[17] = ami; + state[18] = amo; + state[19] = amu; + state[20] = asa; + state[21] = ase; + state[22] = asi; + state[23] = aso; + state[24] = asu; +} + +fn keccak_squeezeblocks(h: &mut [u8], mut nblocks: usize, s: &mut [u64], r: usize) { + let mut idx = 0usize; + while nblocks > 0 { + keccakf1600_statepermute(s); + for i in 0..r / 8 { + store_u64(&mut h[idx + 8 * i..], s[i]) + } + idx += r; + nblocks -= 1; + } +} + +pub fn shake128_squeezeblocks(out: &mut [u8], nblocks: usize, state: &mut KeccakState) { + keccak_squeezeblocks(out, nblocks, &mut state.s, SHAKE128_RATE); +} + +pub fn shake256(out: &mut [u8], mut outlen: usize, input: &[u8], inlen: usize) { + let mut state = KeccakState::new(); + let mut idx = 0; + shake256_absorb_once(&mut state, input, inlen); + let nblocks = outlen / SHAKE256_RATE; + shake256_squeezeblocks(&mut out[idx..], nblocks, &mut state); + outlen -= nblocks * SHAKE256_RATE; + idx += nblocks * SHAKE256_RATE; + shake256_squeeze(&mut out[idx..], outlen, &mut state); +} + +pub fn sha3_256(h: &mut [u8], input: &[u8], inlen: usize) { + let mut s = [0u64; 25]; + keccak_absorb_once(&mut s, SHA3_256_RATE, input, inlen, 0x06); + keccakf1600_statepermute(&mut s); + for i in 0..4 { + store_u64(&mut h[8 * i..], s[i]); + } +} + +pub fn sha3_512(h: &mut [u8], input: &[u8], inlen: usize) { + let mut s = [0u64; 25]; + keccak_absorb_once(&mut s, SHA3_512_RATE, input, inlen, 0x06); + keccakf1600_statepermute(&mut s); + for i in 0..8 { + store_u64(&mut h[8 * i..], s[i]); + } +} + +fn keccak_finalize(s: &mut [u64], pos: usize, r: usize, p: u8) { + s[pos / 8] ^= (p as u64) << 8 * (pos % 8); + s[r / 8 - 1] ^= 1u64 << 63; +} + +pub fn keccak_absorb_once(s: &mut [u64], r: usize, input: &[u8], mut inlen: usize, p: u8) { + s.fill(0); + + let mut idx = 0usize; + while inlen >= r { + for i in 0..(r / 8) { + s[i] ^= load_u64(&input[idx + 8 * i..]); + } + idx += r; + inlen -= r; + keccakf1600_statepermute(s); + } + + for i in 0..inlen { + s[i / 8] ^= (input[idx + i] as u64) << 8 * (i % 8); + } + s[inlen / 8] ^= (p as u64) << 8 * (inlen % 8); + s[(r - 1) / 8] ^= 1u64 << 63; +} + +fn keccak_squeeze( + out: &mut [u8], + mut outlen: usize, + s: &mut [u64], + mut pos: usize, + r: usize, +) -> usize { + let mut idx = 0; + while outlen > 0 { + if pos == r { + keccakf1600_statepermute(s); + pos = 0 + } + let mut i = pos; + while i < r && outlen > 0 { + if outlen >= 8 && i % 8 == 0 && i + 8 <= r { + store_u64(&mut out[idx..], s[i / 8]); + idx += 8; + outlen -= 8; + i += 8; + } else { + out[idx] = (s[i / 8] >> (8 * (i % 8))) as u8; + idx += 1; + outlen -= 1; + i += 1; + } + } + pos = i; + } + pos +} + +pub fn shake128_absorb_once(state: &mut KeccakState, input: &[u8], inlen: usize) { + keccak_absorb_once(&mut state.s, SHAKE128_RATE, input, inlen, 0x1F); + state.pos = SHAKE128_RATE; +} + +fn shake256_init(state: &mut KeccakState) { + state.reset(); +} + +fn shake256_finalize(state: &mut KeccakState) { + keccak_finalize(&mut state.s, state.pos, SHAKE256_RATE, 0x1F); + state.pos = SHAKE256_RATE; +} + +fn shake256_squeeze(out: &mut [u8], outlen: usize, state: &mut KeccakState) { + state.pos = keccak_squeeze(out, outlen, &mut state.s, state.pos, SHAKE256_RATE); +} + +fn shake256_absorb_once(state: &mut KeccakState, input: &[u8], inlen: usize) { + keccak_absorb_once(&mut state.s, SHAKE256_RATE, input, inlen, 0x1F); + state.pos = SHAKE256_RATE; +} + +fn shake256_squeezeblocks(out: &mut [u8], nblocks: usize, state: &mut KeccakState) { + keccak_squeezeblocks(out, nblocks, &mut state.s, SHAKE256_RATE); +} + +fn shake128(out: &mut [u8], mut outlen: usize, input: &[u8], inlen: usize) { + let mut state = KeccakState::new(); + let mut idx = 0; + shake128_absorb_once(&mut state, input, inlen); + let nblocks = outlen / SHAKE128_RATE; + shake128_squeezeblocks(&mut out[idx..], nblocks, &mut state); + outlen -= nblocks * SHAKE128_RATE; + idx += nblocks * SHAKE128_RATE; + shake128_squeeze(&mut out[idx..], outlen, &mut state); +} + +fn shake128_squeeze(out: &mut [u8], outlen: usize, state: &mut KeccakState) { + state.pos = keccak_squeeze(out, outlen, &mut state.s, state.pos, SHAKE128_RATE); +} diff --git a/src/kyber/indcpa.rs b/src/kyber/indcpa.rs new file mode 100644 index 0000000..be3748a --- /dev/null +++ b/src/kyber/indcpa.rs @@ -0,0 +1,586 @@ +use super::error::KyberError; +use super::params::*; +use super::poly::*; +use super::polyvec::*; +use super::symmetric::{ + hash_sha3_512, xof_absorb_bytes, xof_squeeze_blocks, XofState, XOF_BLOCKBYTES, +}; +use rand_core::{CryptoRng, RngCore}; +use zeroize::Zeroize; + +fn pack_pk(r: &mut [u8], pk: &mut Polyvec, seed: &[u8]) { + const END: usize = KYBER_SYMBYTES + KYBER_POLYVECBYTES; + polyvec_tobytes(r, pk); + r[KYBER_POLYVECBYTES..END].copy_from_slice(&seed[..KYBER_SYMBYTES]); +} + +fn unpack_pk(pk: &mut Polyvec, seed: &mut [u8], packedpk: &[u8]) { + const END: usize = KYBER_SYMBYTES + KYBER_POLYVECBYTES; + polyvec_frombytes(pk, packedpk); + seed[..KYBER_SYMBYTES].copy_from_slice(&packedpk[KYBER_POLYVECBYTES..END]); +} + +fn pack_sk(r: &mut [u8], sk: &mut Polyvec) { + polyvec_tobytes(r, sk); +} + +fn unpack_sk(sk: &mut Polyvec, packedsk: &[u8]) { + polyvec_frombytes(sk, packedsk); +} + +fn pack_ciphertext(r: &mut [u8], b: &mut Polyvec, v: Poly) { + polyvec_compress(r, b.clone()); + poly_compress(&mut r[KYBER_POLYVECCOMPRESSEDBYTES..], v); +} + +fn unpack_ciphertext(b: &mut Polyvec, v: &mut Poly, c: &[u8]) { + polyvec_decompress(b, c); + poly_decompress(v, &c[KYBER_POLYVECCOMPRESSEDBYTES..]); +} + +fn rej_uniform(r: &mut [i16], len: usize, buf: &[u8], buflen: usize) -> usize { + let (mut ctr, mut pos) = (0usize, 0usize); + let (mut val0, mut val1); + + while ctr < len && pos + 3 <= buflen { + val0 = ((buf[pos + 0] >> 0) as u16 | (buf[pos + 1] as u16) << 8) & 0xFFF; + val1 = ((buf[pos + 1] >> 4) as u16 | (buf[pos + 2] as u16) << 4) & 0xFFF; + pos += 3; + + if val0 < KYBER_Q as u16 { + r[ctr] = val0 as i16; + ctr += 1; + } + if ctr < len && val1 < KYBER_Q as u16 { + r[ctr] = val1 as i16; + ctr += 1; + } + } + ctr +} + +fn gen_a(a: &mut [Polyvec], b: &[u8]) { + gen_matrix(a, b, false); +} + +fn gen_at(a: &mut [Polyvec], b: &[u8]) { + gen_matrix(a, b, true); +} + +fn gen_matrix(a: &mut [Polyvec], seed: &[u8], transposed: bool) { + let mut ctr; + const GEN_MATRIX_NBLOCKS: usize = + (12 * KYBER_N / 8 * (1 << 12) / KYBER_Q + XOF_BLOCKBYTES) / XOF_BLOCKBYTES; + let mut buf = [0u8; GEN_MATRIX_NBLOCKS * XOF_BLOCKBYTES + 2]; + let mut buflen: usize; + let mut off: usize; + let mut state = XofState::new(); + + for i in 0..KYBER_K { + for j in 0..KYBER_K { + if transposed { + xof_absorb_bytes(&mut state, seed, i as u8, j as u8); + } else { + xof_absorb_bytes(&mut state, seed, j as u8, i as u8); + } + xof_squeeze_blocks(&mut buf, GEN_MATRIX_NBLOCKS, &mut state); + buflen = GEN_MATRIX_NBLOCKS * XOF_BLOCKBYTES; + ctr = rej_uniform(&mut a[i].vec[j].coeffs, KYBER_N, &buf, buflen); + + while ctr < KYBER_N { + off = buflen % 3; + for k in 0..off { + buf[k] = buf[buflen - off + k]; + } + xof_squeeze_blocks(&mut buf[off..], 1, &mut state); + buflen = off + XOF_BLOCKBYTES; + ctr += rej_uniform(&mut a[i].vec[j].coeffs[ctr..], KYBER_N - ctr, &buf, buflen); + } + } + } +} + +pub fn indcpa_keypair( + pk: &mut [u8], + sk: &mut [u8], + _seed: Option<(&[u8], &[u8])>, + _rng: &mut R, +) -> Result<(), KyberError> +where + R: CryptoRng + RngCore, +{ + let mut a: [Polyvec; KYBER_K] = core::array::from_fn(|_| Polyvec::new()); + let (mut e, mut pkpv, mut skpv) = (Polyvec::new(), Polyvec::new(), Polyvec::new()); + let mut nonce = 0u8; + let mut buf = [0u8; 2 * KYBER_SYMBYTES]; + let mut randbuf = [0u8; 2 * KYBER_SYMBYTES]; + + if let Some(s) = _seed { + randbuf[..KYBER_SYMBYTES].copy_from_slice(&s.0); + } else { + _rng.fill_bytes(&mut randbuf[..KYBER_SYMBYTES]); + } + + hash_sha3_512(&mut buf, &randbuf, KYBER_SYMBYTES); + + let (publicseed, noiseseed) = buf.split_at(KYBER_SYMBYTES); + gen_a(&mut a, publicseed); + + for i in 0..KYBER_K { + poly_getnoise_eta1(&mut skpv.vec[i], noiseseed, nonce); + nonce += 1; + } + for i in 0..KYBER_K { + poly_getnoise_eta1(&mut e.vec[i], noiseseed, nonce); + nonce += 1; + } + + polyvec_ntt(&mut skpv); + polyvec_ntt(&mut e); + + for i in 0..KYBER_K { + polyvec_basemul_acc_montgomery(&mut pkpv.vec[i], &a[i], &skpv); + poly_tomont(&mut pkpv.vec[i]); + } + polyvec_add(&mut pkpv, &e); + polyvec_reduce(&mut pkpv); + + pack_sk(sk, &mut skpv); + pack_pk(pk, &mut pkpv, publicseed); + + a.zeroize(); + buf.zeroize(); + randbuf.zeroize(); + Ok(()) +} + +pub fn indcpa_enc(c: &mut [u8], m: &[u8], pk: &[u8], coins: &[u8]) { + let mut at: [Polyvec; KYBER_K] = core::array::from_fn(|_| Polyvec::new()); + let (mut sp, mut pkpv, mut ep, mut b) = ( + Polyvec::new(), + Polyvec::new(), + Polyvec::new(), + Polyvec::new(), + ); + let (mut v, mut k, mut epp) = (Poly::new(), Poly::new(), Poly::new()); + let mut seed = [0u8; KYBER_SYMBYTES]; + let mut nonce = 0u8; + + unpack_pk(&mut pkpv, &mut seed, pk); + poly_frommsg(&mut k, m); + gen_at(&mut at, &seed); + + for i in 0..KYBER_K { + poly_getnoise_eta1(&mut sp.vec[i], coins, nonce); + nonce += 1; + } + for i in 0..KYBER_K { + poly_getnoise_eta2(&mut ep.vec[i], coins, nonce); + nonce += 1; + } + poly_getnoise_eta2(&mut epp, coins, nonce); + + polyvec_ntt(&mut sp); + + for i in 0..KYBER_K { + polyvec_basemul_acc_montgomery(&mut b.vec[i], &at[i], &sp); + } + + polyvec_basemul_acc_montgomery(&mut v, &pkpv, &sp); + polyvec_invntt_tomont(&mut b); + poly_invntt_tomont(&mut v); + + polyvec_add(&mut b, &ep); + poly_add(&mut v, &epp); + poly_add(&mut v, &k); + polyvec_reduce(&mut b); + poly_reduce(&mut v); + + pack_ciphertext(c, &mut b, v.clone()); + seed.zeroize(); +} + +pub fn indcpa_dec(m: &mut [u8], c: &[u8], sk: &[u8]) { + let (mut b, mut skpv) = (Polyvec::new(), Polyvec::new()); + let (mut v, mut mp) = (Poly::new(), Poly::new()); + + unpack_ciphertext(&mut b, &mut v, c); + unpack_sk(&mut skpv, sk); + + polyvec_ntt(&mut b); + polyvec_basemul_acc_montgomery(&mut mp, &skpv, &b); + poly_invntt_tomont(&mut mp); + + poly_sub(&mut mp, &v); + poly_reduce(&mut mp); + + poly_tomsg(m, mp); +} + +fn pack_pk_512(r: &mut [u8], pk: &mut Polyvec512, seed: &[u8]) { + const END: usize = KYBER_SYMBYTES + KYBER_512_POLYVECBYTES; + polyvec_tobytes_512(r, pk); + r[KYBER_512_POLYVECBYTES..END].copy_from_slice(&seed[..KYBER_SYMBYTES]); +} + +fn unpack_pk_512(pk: &mut Polyvec512, seed: &mut [u8], packedpk: &[u8]) { + const END: usize = KYBER_SYMBYTES + KYBER_512_POLYVECBYTES; + polyvec_frombytes_512(pk, packedpk); + seed[..KYBER_SYMBYTES].copy_from_slice(&packedpk[KYBER_512_POLYVECBYTES..END]); +} + +fn pack_ciphertext_512(r: &mut [u8], b: &mut Polyvec512, v: Poly) { + polyvec_compress_512(r, b.clone()); + poly_compress(&mut r[KYBER_512_POLYVECCOMPRESSEDBYTES..], v); +} + +fn unpack_ciphertext_512(b: &mut Polyvec512, v: &mut Poly, c: &[u8]) { + polyvec_decompress_512(b, c); + poly_decompress(v, &c[KYBER_512_POLYVECCOMPRESSEDBYTES..]); +} + +fn gen_matrix_512(a: &mut [Polyvec512], seed: &[u8], transposed: bool) { + let mut ctr; + const K: usize = KYBER_512_K; + const GEN_MATRIX_NBLOCKS: usize = + (12 * KYBER_N / 8 * (1 << 12) / KYBER_Q + XOF_BLOCKBYTES) / XOF_BLOCKBYTES; + let mut buf = [0u8; GEN_MATRIX_NBLOCKS * XOF_BLOCKBYTES + 2]; + let mut buflen: usize; + let mut off: usize; + let mut state = XofState::new(); + + for i in 0..K { + for j in 0..K { + if transposed { + xof_absorb_bytes(&mut state, seed, i as u8, j as u8); + } else { + xof_absorb_bytes(&mut state, seed, j as u8, i as u8); + } + xof_squeeze_blocks(&mut buf, GEN_MATRIX_NBLOCKS, &mut state); + buflen = GEN_MATRIX_NBLOCKS * XOF_BLOCKBYTES; + ctr = rej_uniform(&mut a[i].vec[j].coeffs, KYBER_N, &buf, buflen); + + while ctr < KYBER_N { + off = buflen % 3; + for k in 0..off { + buf[k] = buf[buflen - off + k]; + } + xof_squeeze_blocks(&mut buf[off..], 1, &mut state); + buflen = off + XOF_BLOCKBYTES; + ctr += rej_uniform(&mut a[i].vec[j].coeffs[ctr..], KYBER_N - ctr, &buf, buflen); + } + } + } +} + +fn gen_a_512(a: &mut [Polyvec512], b: &[u8]) { + gen_matrix_512(a, b, false); +} + +fn gen_at_512(a: &mut [Polyvec512], b: &[u8]) { + gen_matrix_512(a, b, true); +} + +pub fn indcpa_keypair_512( + pk: &mut [u8], + sk: &mut [u8], + _seed: Option<(&[u8], &[u8])>, + _rng: &mut R, +) -> Result<(), KyberError> +where + R: CryptoRng + RngCore, +{ + const K: usize = KYBER_512_K; + let mut a: [Polyvec512; K] = core::array::from_fn(|_| Polyvec512::new()); + let (mut e, mut pkpv, mut skpv) = (Polyvec512::new(), Polyvec512::new(), Polyvec512::new()); + let mut nonce = 0u8; + let mut buf = [0u8; 2 * KYBER_SYMBYTES]; + let mut randbuf = [0u8; 2 * KYBER_SYMBYTES]; + + if let Some(s) = _seed { + randbuf[..KYBER_SYMBYTES].copy_from_slice(&s.0); + } else { + _rng.fill_bytes(&mut randbuf[..KYBER_SYMBYTES]); + } + + hash_sha3_512(&mut buf, &randbuf, KYBER_SYMBYTES); + + let (publicseed, noiseseed) = buf.split_at(KYBER_SYMBYTES); + gen_a_512(&mut a, publicseed); + + for i in 0..K { + poly_getnoise_eta1_512(&mut skpv.vec[i], noiseseed, nonce); + nonce += 1; + } + for i in 0..K { + poly_getnoise_eta1_512(&mut e.vec[i], noiseseed, nonce); + nonce += 1; + } + + polyvec_ntt_512(&mut skpv); + polyvec_ntt_512(&mut e); + + for i in 0..K { + polyvec_basemul_acc_montgomery_512(&mut pkpv.vec[i], &a[i], &skpv); + poly_tomont(&mut pkpv.vec[i]); + } + polyvec_add_512(&mut pkpv, &e); + polyvec_reduce_512(&mut pkpv); + + polyvec_tobytes_512(sk, &skpv); + pack_pk_512(pk, &mut pkpv, publicseed); + + a.zeroize(); + buf.zeroize(); + randbuf.zeroize(); + Ok(()) +} + +pub fn indcpa_enc_512(c: &mut [u8], m: &[u8], pk: &[u8], coins: &[u8]) { + const K: usize = KYBER_512_K; + let mut at: [Polyvec512; K] = core::array::from_fn(|_| Polyvec512::new()); + let (mut sp, mut pkpv, mut ep, mut b) = ( + Polyvec512::new(), + Polyvec512::new(), + Polyvec512::new(), + Polyvec512::new(), + ); + let (mut v, mut k, mut epp) = (Poly::new(), Poly::new(), Poly::new()); + let mut seed = [0u8; KYBER_SYMBYTES]; + let mut nonce = 0u8; + + unpack_pk_512(&mut pkpv, &mut seed, pk); + poly_frommsg(&mut k, m); + gen_at_512(&mut at, &seed); + + for i in 0..K { + poly_getnoise_eta1_512(&mut sp.vec[i], coins, nonce); + nonce += 1; + } + for i in 0..K { + poly_getnoise_eta2_512(&mut ep.vec[i], coins, nonce); + nonce += 1; + } + poly_getnoise_eta2_512(&mut epp, coins, nonce); + + polyvec_ntt_512(&mut sp); + + for i in 0..K { + polyvec_basemul_acc_montgomery_512(&mut b.vec[i], &at[i], &sp); + } + + polyvec_basemul_acc_montgomery_512(&mut v, &pkpv, &sp); + polyvec_invntt_tomont_512(&mut b); + poly_invntt_tomont(&mut v); + + polyvec_add_512(&mut b, &ep); + poly_add(&mut v, &epp); + poly_add(&mut v, &k); + polyvec_reduce_512(&mut b); + poly_reduce(&mut v); + + pack_ciphertext_512(c, &mut b, v.clone()); + at.zeroize(); + seed.zeroize(); +} + +pub fn indcpa_dec_512(m: &mut [u8], c: &[u8], sk: &[u8]) { + let (mut b, mut skpv) = (Polyvec512::new(), Polyvec512::new()); + let (mut v, mut mp) = (Poly::new(), Poly::new()); + + unpack_ciphertext_512(&mut b, &mut v, c); + polyvec_frombytes_512(&mut skpv, sk); + + polyvec_ntt_512(&mut b); + polyvec_basemul_acc_montgomery_512(&mut mp, &skpv, &b); + poly_invntt_tomont(&mut mp); + + poly_sub(&mut mp, &v); + poly_reduce(&mut mp); + + poly_tomsg(m, mp); +} + +fn pack_pk_1024(r: &mut [u8], pk: &mut Polyvec1024, seed: &[u8]) { + const END: usize = KYBER_SYMBYTES + KYBER_1024_POLYVECBYTES; + polyvec_tobytes_1024(r, pk); + r[KYBER_1024_POLYVECBYTES..END].copy_from_slice(&seed[..KYBER_SYMBYTES]); +} + +fn unpack_pk_1024(pk: &mut Polyvec1024, seed: &mut [u8], packedpk: &[u8]) { + const END: usize = KYBER_SYMBYTES + KYBER_1024_POLYVECBYTES; + polyvec_frombytes_1024(pk, packedpk); + seed[..KYBER_SYMBYTES].copy_from_slice(&packedpk[KYBER_1024_POLYVECBYTES..END]); +} + +fn pack_ciphertext_1024(r: &mut [u8], b: &mut Polyvec1024, v: Poly) { + polyvec_compress_1024(r, b.clone()); + poly_compress_1024(&mut r[KYBER_1024_POLYVECCOMPRESSEDBYTES..], v); +} + +fn unpack_ciphertext_1024(b: &mut Polyvec1024, v: &mut Poly, c: &[u8]) { + polyvec_decompress_1024(b, c); + poly_decompress_1024(v, &c[KYBER_1024_POLYVECCOMPRESSEDBYTES..]); +} + +fn gen_matrix_1024(a: &mut [Polyvec1024], seed: &[u8], transposed: bool) { + let mut ctr; + const K: usize = KYBER_1024_K; + const GEN_MATRIX_NBLOCKS: usize = + (12 * KYBER_N / 8 * (1 << 12) / KYBER_Q + XOF_BLOCKBYTES) / XOF_BLOCKBYTES; + let mut buf = [0u8; GEN_MATRIX_NBLOCKS * XOF_BLOCKBYTES + 2]; + let mut buflen: usize; + let mut off: usize; + let mut state = XofState::new(); + + for i in 0..K { + for j in 0..K { + if transposed { + xof_absorb_bytes(&mut state, seed, i as u8, j as u8); + } else { + xof_absorb_bytes(&mut state, seed, j as u8, i as u8); + } + xof_squeeze_blocks(&mut buf, GEN_MATRIX_NBLOCKS, &mut state); + buflen = GEN_MATRIX_NBLOCKS * XOF_BLOCKBYTES; + ctr = rej_uniform(&mut a[i].vec[j].coeffs, KYBER_N, &buf, buflen); + + while ctr < KYBER_N { + off = buflen % 3; + for k in 0..off { + buf[k] = buf[buflen - off + k]; + } + xof_squeeze_blocks(&mut buf[off..], 1, &mut state); + buflen = off + XOF_BLOCKBYTES; + ctr += rej_uniform(&mut a[i].vec[j].coeffs[ctr..], KYBER_N - ctr, &buf, buflen); + } + } + } +} + +fn gen_a_1024(a: &mut [Polyvec1024], b: &[u8]) { + gen_matrix_1024(a, b, false); +} + +fn gen_at_1024(a: &mut [Polyvec1024], b: &[u8]) { + gen_matrix_1024(a, b, true); +} + +pub fn indcpa_keypair_1024( + pk: &mut [u8], + sk: &mut [u8], + _seed: Option<(&[u8], &[u8])>, + _rng: &mut R, +) -> Result<(), KyberError> +where + R: CryptoRng + RngCore, +{ + const K: usize = KYBER_1024_K; + let mut a: [Polyvec1024; K] = core::array::from_fn(|_| Polyvec1024::new()); + let (mut e, mut pkpv, mut skpv) = (Polyvec1024::new(), Polyvec1024::new(), Polyvec1024::new()); + let mut nonce = 0u8; + let mut buf = [0u8; 2 * KYBER_SYMBYTES]; + let mut randbuf = [0u8; 2 * KYBER_SYMBYTES]; + + if let Some(s) = _seed { + randbuf[..KYBER_SYMBYTES].copy_from_slice(&s.0); + } else { + _rng.fill_bytes(&mut randbuf[..KYBER_SYMBYTES]); + } + + hash_sha3_512(&mut buf, &randbuf, KYBER_SYMBYTES); + + let (publicseed, noiseseed) = buf.split_at(KYBER_SYMBYTES); + gen_a_1024(&mut a, publicseed); + + for i in 0..K { + poly_getnoise_eta1(&mut skpv.vec[i], noiseseed, nonce); + nonce += 1; + } + for i in 0..K { + poly_getnoise_eta1(&mut e.vec[i], noiseseed, nonce); + nonce += 1; + } + + polyvec_ntt_1024(&mut skpv); + polyvec_ntt_1024(&mut e); + + for i in 0..K { + polyvec_basemul_acc_montgomery_1024(&mut pkpv.vec[i], &a[i], &skpv); + poly_tomont(&mut pkpv.vec[i]); + } + polyvec_add_1024(&mut pkpv, &e); + polyvec_reduce_1024(&mut pkpv); + + polyvec_tobytes_1024(sk, &skpv); + pack_pk_1024(pk, &mut pkpv, publicseed); + + a.zeroize(); + buf.zeroize(); + randbuf.zeroize(); + Ok(()) +} + +pub fn indcpa_enc_1024(c: &mut [u8], m: &[u8], pk: &[u8], coins: &[u8]) { + const K: usize = KYBER_1024_K; + let mut at: [Polyvec1024; K] = core::array::from_fn(|_| Polyvec1024::new()); + let (mut sp, mut pkpv, mut ep, mut b) = ( + Polyvec1024::new(), + Polyvec1024::new(), + Polyvec1024::new(), + Polyvec1024::new(), + ); + let (mut v, mut k, mut epp) = (Poly::new(), Poly::new(), Poly::new()); + let mut seed = [0u8; KYBER_SYMBYTES]; + let mut nonce = 0u8; + + unpack_pk_1024(&mut pkpv, &mut seed, pk); + poly_frommsg(&mut k, m); + gen_at_1024(&mut at, &seed); + + for i in 0..K { + poly_getnoise_eta1(&mut sp.vec[i], coins, nonce); + nonce += 1; + } + for i in 0..K { + poly_getnoise_eta2(&mut ep.vec[i], coins, nonce); + nonce += 1; + } + poly_getnoise_eta2(&mut epp, coins, nonce); + + polyvec_ntt_1024(&mut sp); + + for i in 0..K { + polyvec_basemul_acc_montgomery_1024(&mut b.vec[i], &at[i], &sp); + } + + polyvec_basemul_acc_montgomery_1024(&mut v, &pkpv, &sp); + polyvec_invntt_tomont_1024(&mut b); + poly_invntt_tomont(&mut v); + + polyvec_add_1024(&mut b, &ep); + poly_add(&mut v, &epp); + poly_add(&mut v, &k); + polyvec_reduce_1024(&mut b); + poly_reduce(&mut v); + + pack_ciphertext_1024(c, &mut b, v.clone()); + at.zeroize(); + seed.zeroize(); +} + +pub fn indcpa_dec_1024(m: &mut [u8], c: &[u8], sk: &[u8]) { + let (mut b, mut skpv) = (Polyvec1024::new(), Polyvec1024::new()); + let (mut v, mut mp) = (Poly::new(), Poly::new()); + + unpack_ciphertext_1024(&mut b, &mut v, c); + polyvec_frombytes_1024(&mut skpv, sk); + + polyvec_ntt_1024(&mut b); + polyvec_basemul_acc_montgomery_1024(&mut mp, &skpv, &b); + poly_invntt_tomont(&mut mp); + + poly_sub(&mut mp, &v); + poly_reduce(&mut mp); + + poly_tomsg(m, mp); +} diff --git a/src/kyber/kem.rs b/src/kyber/kem.rs new file mode 100644 index 0000000..cbcfcd2 --- /dev/null +++ b/src/kyber/kem.rs @@ -0,0 +1,333 @@ +use super::error::KyberError; +use super::indcpa::{ + indcpa_dec, indcpa_dec_1024, indcpa_dec_512, indcpa_enc, indcpa_enc_1024, indcpa_enc_512, + indcpa_keypair, indcpa_keypair_1024, indcpa_keypair_512, +}; +use super::params::*; +use super::symmetric::{hash_sha3_256, hash_sha3_512, shake256_kdf}; +use super::verify::{cmov, verify}; +use rand_core::{CryptoRng, RngCore}; +use zeroize::Zeroize; + +pub fn crypto_kem_keypair( + pk: &mut [u8], + sk: &mut [u8], + _rng: &mut R, + _seed: Option<(&[u8], &[u8])>, +) -> Result<(), KyberError> +where + R: RngCore + CryptoRng, +{ + const PK_START: usize = KYBER_SECRETKEYBYTES - (2 * KYBER_SYMBYTES); + const SK_START: usize = KYBER_SECRETKEYBYTES - KYBER_SYMBYTES; + const END: usize = KYBER_INDCPA_PUBLICKEYBYTES + KYBER_INDCPA_SECRETKEYBYTES; + + indcpa_keypair(pk, sk, _seed, _rng)?; + + sk[KYBER_INDCPA_SECRETKEYBYTES..END].copy_from_slice(&pk[..KYBER_INDCPA_PUBLICKEYBYTES]); + hash_sha3_256(&mut sk[PK_START..], pk, KYBER_PUBLICKEYBYTES); + + if let Some(s) = _seed { + sk[SK_START..].copy_from_slice(&s.1) + } else { + _rng.fill_bytes(&mut sk[SK_START..SK_START + KYBER_SYMBYTES]); + } + Ok(()) +} + +pub fn crypto_kem_enc( + ct: &mut [u8], + ss: &mut [u8], + pk: &[u8], + _rng: &mut R, + _seed: Option<&[u8]>, +) -> Result<(), KyberError> +where + R: RngCore + CryptoRng, +{ + let mut kr = [0u8; 2 * KYBER_SYMBYTES]; + let mut buf = [0u8; 2 * KYBER_SYMBYTES]; + let mut randbuf = [0u8; 2 * KYBER_SYMBYTES]; + + if let Some(s) = _seed { + randbuf[..KYBER_SYMBYTES].copy_from_slice(&s); + } else { + _rng.fill_bytes(&mut randbuf); + } + + hash_sha3_256(&mut buf, &randbuf, KYBER_SYMBYTES); + hash_sha3_256(&mut buf[KYBER_SYMBYTES..], pk, KYBER_PUBLICKEYBYTES); + hash_sha3_512(&mut kr, &buf, 2 * KYBER_SYMBYTES); + + indcpa_enc(ct, &buf, pk, &kr[KYBER_SYMBYTES..]); + + hash_sha3_256(&mut kr[KYBER_SYMBYTES..], ct, KYBER_CIPHERTEXTBYTES); + shake256_kdf(ss, &kr, 2 * KYBER_SYMBYTES); + + kr.zeroize(); + buf.zeroize(); + randbuf.zeroize(); + Ok(()) +} + +pub fn crypto_kem_dec(ss: &mut [u8], ct: &[u8], sk: &[u8]) -> Result<(), KyberError> { + if ct.len() != KYBER_CIPHERTEXTBYTES { + return Err(KyberError::invalid_input( + "ct", + KYBER_CIPHERTEXTBYTES, + ct.len(), + )); + } + if sk.len() != KYBER_SECRETKEYBYTES { + return Err(KyberError::invalid_input( + "sk", + KYBER_SECRETKEYBYTES, + sk.len(), + )); + } + if ss.len() != KYBER_SSBYTES { + return Err(KyberError::invalid_input("ss", KYBER_SSBYTES, ss.len())); + } + + let mut buf = [0u8; 2 * KYBER_SYMBYTES]; + let mut kr = [0u8; 2 * KYBER_SYMBYTES]; + let mut cmp = [0u8; KYBER_CIPHERTEXTBYTES]; + let mut pk = [0u8; KYBER_INDCPA_PUBLICKEYBYTES]; + + pk.copy_from_slice(&sk[KYBER_INDCPA_SECRETKEYBYTES..][..KYBER_INDCPA_PUBLICKEYBYTES]); + + indcpa_dec(&mut buf, ct, sk); + + const START: usize = KYBER_SECRETKEYBYTES - 2 * KYBER_SYMBYTES; + const END: usize = KYBER_SECRETKEYBYTES - KYBER_SYMBYTES; + buf[KYBER_SYMBYTES..].copy_from_slice(&sk[START..END]); + hash_sha3_512(&mut kr, &buf, 2 * KYBER_SYMBYTES); + + indcpa_enc(&mut cmp, &buf, &pk, &kr[KYBER_SYMBYTES..]); + let fail = verify(ct, &cmp, KYBER_CIPHERTEXTBYTES); + hash_sha3_256(&mut kr[KYBER_SYMBYTES..], ct, KYBER_CIPHERTEXTBYTES); + cmov(&mut kr, &sk[END..], KYBER_SYMBYTES, fail); + shake256_kdf(ss, &kr, 2 * KYBER_SYMBYTES); + + buf.zeroize(); + kr.zeroize(); + cmp.zeroize(); + pk.zeroize(); + Ok(()) +} + +pub fn crypto_kem_keypair_512( + pk: &mut [u8], + sk: &mut [u8], + _rng: &mut R, + _seed: Option<(&[u8], &[u8])>, +) -> Result<(), KyberError> +where + R: RngCore + CryptoRng, +{ + const PK_START: usize = KYBER_512_SECRETKEYBYTES - (2 * KYBER_SYMBYTES); + const SK_START: usize = KYBER_512_SECRETKEYBYTES - KYBER_SYMBYTES; + const END: usize = KYBER_512_INDCPA_PUBLICKEYBYTES + KYBER_512_INDCPA_SECRETKEYBYTES; + + indcpa_keypair_512(pk, sk, _seed, _rng)?; + + sk[KYBER_512_INDCPA_SECRETKEYBYTES..END] + .copy_from_slice(&pk[..KYBER_512_INDCPA_PUBLICKEYBYTES]); + hash_sha3_256(&mut sk[PK_START..], pk, KYBER_512_PUBLICKEYBYTES); + + if let Some(s) = _seed { + sk[SK_START..].copy_from_slice(&s.1) + } else { + _rng.fill_bytes(&mut sk[SK_START..SK_START + KYBER_SYMBYTES]); + } + Ok(()) +} + +pub fn crypto_kem_enc_512( + ct: &mut [u8], + ss: &mut [u8], + pk: &[u8], + _rng: &mut R, + _seed: Option<&[u8]>, +) -> Result<(), KyberError> +where + R: RngCore + CryptoRng, +{ + let mut kr = [0u8; 2 * KYBER_SYMBYTES]; + let mut buf = [0u8; 2 * KYBER_SYMBYTES]; + let mut randbuf = [0u8; 2 * KYBER_SYMBYTES]; + + if let Some(s) = _seed { + randbuf[..KYBER_SYMBYTES].copy_from_slice(&s); + } else { + _rng.fill_bytes(&mut randbuf); + } + + hash_sha3_256(&mut buf, &randbuf, KYBER_SYMBYTES); + hash_sha3_256(&mut buf[KYBER_SYMBYTES..], pk, KYBER_512_PUBLICKEYBYTES); + hash_sha3_512(&mut kr, &buf, 2 * KYBER_SYMBYTES); + + indcpa_enc_512(ct, &buf, pk, &kr[KYBER_SYMBYTES..]); + + hash_sha3_256(&mut kr[KYBER_SYMBYTES..], ct, KYBER_512_CIPHERTEXTBYTES); + shake256_kdf(ss, &kr, 2 * KYBER_SYMBYTES); + + kr.zeroize(); + buf.zeroize(); + randbuf.zeroize(); + Ok(()) +} + +pub fn crypto_kem_dec_512(ss: &mut [u8], ct: &[u8], sk: &[u8]) -> Result<(), KyberError> { + if ct.len() != KYBER_512_CIPHERTEXTBYTES { + return Err(KyberError::invalid_input( + "ct", + KYBER_512_CIPHERTEXTBYTES, + ct.len(), + )); + } + if sk.len() != KYBER_512_SECRETKEYBYTES { + return Err(KyberError::invalid_input( + "sk", + KYBER_512_SECRETKEYBYTES, + sk.len(), + )); + } + if ss.len() != KYBER_SSBYTES { + return Err(KyberError::invalid_input("ss", KYBER_SSBYTES, ss.len())); + } + + let mut buf = [0u8; 2 * KYBER_SYMBYTES]; + let mut kr = [0u8; 2 * KYBER_SYMBYTES]; + let mut cmp = [0u8; KYBER_512_CIPHERTEXTBYTES]; + let mut pk = [0u8; KYBER_512_INDCPA_PUBLICKEYBYTES]; + + pk.copy_from_slice(&sk[KYBER_512_INDCPA_SECRETKEYBYTES..][..KYBER_512_INDCPA_PUBLICKEYBYTES]); + + indcpa_dec_512(&mut buf, ct, sk); + + const START: usize = KYBER_512_SECRETKEYBYTES - 2 * KYBER_SYMBYTES; + const END: usize = KYBER_512_SECRETKEYBYTES - KYBER_SYMBYTES; + buf[KYBER_SYMBYTES..].copy_from_slice(&sk[START..END]); + hash_sha3_512(&mut kr, &buf, 2 * KYBER_SYMBYTES); + + indcpa_enc_512(&mut cmp, &buf, &pk, &kr[KYBER_SYMBYTES..]); + let fail = verify(ct, &cmp, KYBER_512_CIPHERTEXTBYTES); + hash_sha3_256(&mut kr[KYBER_SYMBYTES..], ct, KYBER_512_CIPHERTEXTBYTES); + cmov(&mut kr, &sk[END..], KYBER_SYMBYTES, fail); + shake256_kdf(ss, &kr, 2 * KYBER_SYMBYTES); + + buf.zeroize(); + kr.zeroize(); + cmp.zeroize(); + pk.zeroize(); + Ok(()) +} + +pub fn crypto_kem_keypair_1024( + pk: &mut [u8], + sk: &mut [u8], + _rng: &mut R, + _seed: Option<(&[u8], &[u8])>, +) -> Result<(), KyberError> +where + R: RngCore + CryptoRng, +{ + const PK_START: usize = KYBER_1024_SECRETKEYBYTES - (2 * KYBER_SYMBYTES); + const SK_START: usize = KYBER_1024_SECRETKEYBYTES - KYBER_SYMBYTES; + const END: usize = KYBER_1024_INDCPA_PUBLICKEYBYTES + KYBER_1024_INDCPA_SECRETKEYBYTES; + + indcpa_keypair_1024(pk, sk, _seed, _rng)?; + + sk[KYBER_1024_INDCPA_SECRETKEYBYTES..END] + .copy_from_slice(&pk[..KYBER_1024_INDCPA_PUBLICKEYBYTES]); + hash_sha3_256(&mut sk[PK_START..], pk, KYBER_1024_PUBLICKEYBYTES); + + if let Some(s) = _seed { + sk[SK_START..].copy_from_slice(&s.1) + } else { + _rng.fill_bytes(&mut sk[SK_START..SK_START + KYBER_SYMBYTES]); + } + Ok(()) +} + +pub fn crypto_kem_enc_1024( + ct: &mut [u8], + ss: &mut [u8], + pk: &[u8], + _rng: &mut R, + _seed: Option<&[u8]>, +) -> Result<(), KyberError> +where + R: RngCore + CryptoRng, +{ + let mut kr = [0u8; 2 * KYBER_SYMBYTES]; + let mut buf = [0u8; 2 * KYBER_SYMBYTES]; + let mut randbuf = [0u8; 2 * KYBER_SYMBYTES]; + + if let Some(s) = _seed { + randbuf[..KYBER_SYMBYTES].copy_from_slice(&s); + } else { + _rng.fill_bytes(&mut randbuf); + } + + hash_sha3_256(&mut buf, &randbuf, KYBER_SYMBYTES); + hash_sha3_256(&mut buf[KYBER_SYMBYTES..], pk, KYBER_1024_PUBLICKEYBYTES); + hash_sha3_512(&mut kr, &buf, 2 * KYBER_SYMBYTES); + + indcpa_enc_1024(ct, &buf, pk, &kr[KYBER_SYMBYTES..]); + + hash_sha3_256(&mut kr[KYBER_SYMBYTES..], ct, KYBER_1024_CIPHERTEXTBYTES); + shake256_kdf(ss, &kr, 2 * KYBER_SYMBYTES); + + kr.zeroize(); + buf.zeroize(); + randbuf.zeroize(); + Ok(()) +} + +pub fn crypto_kem_dec_1024(ss: &mut [u8], ct: &[u8], sk: &[u8]) -> Result<(), KyberError> { + if ct.len() != KYBER_1024_CIPHERTEXTBYTES { + return Err(KyberError::invalid_input( + "ct", + KYBER_1024_CIPHERTEXTBYTES, + ct.len(), + )); + } + if sk.len() != KYBER_1024_SECRETKEYBYTES { + return Err(KyberError::invalid_input( + "sk", + KYBER_1024_SECRETKEYBYTES, + sk.len(), + )); + } + if ss.len() != KYBER_SSBYTES { + return Err(KyberError::invalid_input("ss", KYBER_SSBYTES, ss.len())); + } + + let mut buf = [0u8; 2 * KYBER_SYMBYTES]; + let mut kr = [0u8; 2 * KYBER_SYMBYTES]; + let mut cmp = [0u8; KYBER_1024_CIPHERTEXTBYTES]; + let mut pk = [0u8; KYBER_1024_INDCPA_PUBLICKEYBYTES]; + + pk.copy_from_slice(&sk[KYBER_1024_INDCPA_SECRETKEYBYTES..][..KYBER_1024_INDCPA_PUBLICKEYBYTES]); + + indcpa_dec_1024(&mut buf, ct, sk); + + const START: usize = KYBER_1024_SECRETKEYBYTES - 2 * KYBER_SYMBYTES; + const END: usize = KYBER_1024_SECRETKEYBYTES - KYBER_SYMBYTES; + buf[KYBER_SYMBYTES..].copy_from_slice(&sk[START..END]); + hash_sha3_512(&mut kr, &buf, 2 * KYBER_SYMBYTES); + + indcpa_enc_1024(&mut cmp, &buf, &pk, &kr[KYBER_SYMBYTES..]); + let fail = verify(ct, &cmp, KYBER_1024_CIPHERTEXTBYTES); + hash_sha3_256(&mut kr[KYBER_SYMBYTES..], ct, KYBER_1024_CIPHERTEXTBYTES); + cmov(&mut kr, &sk[END..], KYBER_SYMBYTES, fail); + shake256_kdf(ss, &kr, 2 * KYBER_SYMBYTES); + + buf.zeroize(); + kr.zeroize(); + cmp.zeroize(); + pk.zeroize(); + Ok(()) +} diff --git a/src/kyber/mod.rs b/src/kyber/mod.rs new file mode 100644 index 0000000..e133a29 --- /dev/null +++ b/src/kyber/mod.rs @@ -0,0 +1,35 @@ +pub mod api; +pub mod cbd; +pub mod error; +pub mod fips202; +pub mod indcpa; +pub mod kem; +pub mod ntt; +pub mod params; +pub mod poly; +pub mod polyvec; +pub mod reduce; +pub mod symmetric; +pub mod verify; + +pub use api::*; +pub use error::KyberError; +pub use indcpa::{ + indcpa_dec, indcpa_dec_1024, indcpa_dec_512, indcpa_enc, indcpa_enc_1024, indcpa_enc_512, + indcpa_keypair, indcpa_keypair_1024, indcpa_keypair_512, +}; +pub use kem::{ + crypto_kem_dec, crypto_kem_dec_1024, crypto_kem_dec_512, crypto_kem_enc, crypto_kem_enc_1024, + crypto_kem_enc_512, crypto_kem_keypair, crypto_kem_keypair_1024, crypto_kem_keypair_512, +}; +pub use params::*; +pub use polyvec::{ + polyvec_add, polyvec_add_1024, polyvec_add_512, polyvec_basemul_acc_montgomery, + polyvec_basemul_acc_montgomery_1024, polyvec_basemul_acc_montgomery_512, polyvec_compress, + polyvec_compress_1024, polyvec_compress_512, polyvec_decompress, polyvec_decompress_1024, + polyvec_decompress_512, polyvec_frombytes, polyvec_frombytes_1024, polyvec_frombytes_512, + polyvec_invntt_tomont, polyvec_invntt_tomont_1024, polyvec_invntt_tomont_512, polyvec_ntt, + polyvec_ntt_1024, polyvec_ntt_512, polyvec_reduce, polyvec_reduce_1024, polyvec_reduce_512, + polyvec_tobytes, polyvec_tobytes_1024, polyvec_tobytes_512, Polyvec, Polyvec1024, Polyvec512, +}; +pub use rand_core::{CryptoRng, RngCore}; diff --git a/src/kyber/ntt.rs b/src/kyber/ntt.rs new file mode 100644 index 0000000..42228a9 --- /dev/null +++ b/src/kyber/ntt.rs @@ -0,0 +1,90 @@ +use super::reduce::*; + +/// Twiddle factors (primitive roots of unity) used in the Number Theoretic Transform. +/// These are Montgomery-friendly constants precomputed for the Kyber polynomial ring. +pub const ZETAS: [i16; 128] = [ + -1044, -758, -359, -1517, 1493, 1422, 287, 202, -171, 622, 1577, 182, 962, -1202, -1474, 1468, + 573, -1325, 264, 383, -829, 1458, -1602, -130, -681, 1017, 732, 608, -1542, 411, -205, -1571, + 1223, 652, -552, 1015, -1293, 1491, -282, -1544, 516, -8, -320, -666, -1618, -1162, 126, 1469, + -853, -90, -271, 830, 107, -1421, -247, -951, -398, 961, -1508, -725, 448, -1065, 677, -1275, + -1103, 430, 555, 843, -1251, 871, 1550, 105, 422, 587, 177, -235, -291, -460, 1574, 1653, -246, + 778, 1159, -147, -777, 1483, -602, 1119, -1590, 644, -872, 349, 418, 329, -156, -75, 817, 1097, + 603, 610, 1322, -1285, -1465, 384, -1215, -136, 1218, -1335, -874, 220, -1187, -1659, -1185, + -1530, -1278, 794, -1510, -854, -870, 478, -108, -308, 996, 991, 958, -1460, 1522, 1628, +]; + +/// Field multiplication in the Kyber polynomial ring (q = 3329). +/// Uses Montgomery multiplication to compute a * b mod q. +pub fn field_multiply(a: i16, b: i16) -> i16 { + montgomery_reduce(a as i32 * b as i32) +} + +/// Forward Number Theoretic Transform (NTT) on a polynomial. +/// Transforms coefficients from standard to NTT domain using iterative Cooley-Tukey algorithm. +/// Input is in-place on the coefficient array. +pub fn ntt_forward(coeffs: &mut [i16]) { + let mut j; + let mut zeta_index = 1usize; + let mut layer_length = 128; + let (mut temp, mut zeta); + + while layer_length >= 2 { + let mut start = 0; + while start < 256 { + zeta = ZETAS[zeta_index]; + zeta_index += 1; + j = start; + while j < (start + layer_length) { + temp = field_multiply(zeta, coeffs[j + layer_length]); + coeffs[j + layer_length] = (coeffs[j] as i32 - temp as i32) as i16; + coeffs[j] = (coeffs[j] as i32 + temp as i32) as i16; + j += 1; + } + start = j + layer_length; + } + layer_length >>= 1; + } +} + +/// Inverse Number Theuristic Transform (INTT) on a polynomial. +/// Transforms coefficients from NTT domain back to standard representation. +/// Uses inverse butterflies with Barrett reduction and final scaling factor. +pub fn ntt_inverse(coeffs: &mut [i16]) { + let mut j; + let mut zeta_index = 127usize; + let mut layer_length = 2; + let (mut temp, mut zeta); + const F: i16 = 1441; + while layer_length <= 128 { + let mut start = 0; + while start < 256 { + zeta = ZETAS[zeta_index]; + zeta_index -= 1; + j = start; + while j < (start + layer_length) { + temp = coeffs[j]; + coeffs[j] = barrett_reduce(temp as i32 + coeffs[j + layer_length] as i32); + coeffs[j + layer_length] = (coeffs[j + layer_length] as i32 - temp as i32) as i16; + coeffs[j + layer_length] = field_multiply(zeta, coeffs[j + layer_length]); + j += 1 + } + start = j + layer_length; + } + layer_length <<= 1; + } + for j in 0..256 { + coeffs[j] = field_multiply(coeffs[j], F); + } +} + +/// Base multiplication for two polynomials in the NTT domain. +/// Computes point-wise multiplication of polynomials a and b, +/// using the provided twiddle factor zeta for the butterfly operation. +pub fn base_multiply(result: &mut [i16], a: &[i16], b: &[i16], zeta: i16) { + result[0] = field_multiply(a[1], b[1]); + result[0] = field_multiply(result[0], zeta); + result[0] += field_multiply(a[0], b[0]); + + result[1] = field_multiply(a[0], b[1]); + result[1] += field_multiply(a[1], b[0]); +} diff --git a/src/kyber/params.rs b/src/kyber/params.rs new file mode 100644 index 0000000..77ccc44 --- /dev/null +++ b/src/kyber/params.rs @@ -0,0 +1,70 @@ +pub const KYBER_K: usize = 3; + +pub const KYBER_N: usize = 256; +pub const KYBER_Q: usize = 3329; + +pub const KYBER_ETA1: usize = 2; +pub const KYBER_ETA2: usize = 2; + +pub const KYBER_SYMBYTES: usize = 32; + +pub const KYBER_SSBYTES: usize = 32; + +pub const KYBER_POLYBYTES: usize = 384; +pub const KYBER_POLYVECBYTES: usize = KYBER_K * KYBER_POLYBYTES; + +pub const KYBER_POLYCOMPRESSEDBYTES: usize = 128; +pub const KYBER_POLYVECCOMPRESSEDBYTES: usize = KYBER_K * 320; + +pub const KYBER_INDCPA_PUBLICKEYBYTES: usize = KYBER_POLYVECBYTES + KYBER_SYMBYTES; +pub const KYBER_INDCPA_SECRETKEYBYTES: usize = KYBER_POLYVECBYTES; +pub const KYBER_INDCPA_BYTES: usize = KYBER_POLYVECCOMPRESSEDBYTES + KYBER_POLYCOMPRESSEDBYTES; + +pub const KYBER_PUBLICKEYBYTES: usize = KYBER_INDCPA_PUBLICKEYBYTES; +pub const KYBER_SECRETKEYBYTES: usize = + KYBER_INDCPA_SECRETKEYBYTES + KYBER_INDCPA_PUBLICKEYBYTES + 2 * KYBER_SYMBYTES; +pub const KYBER_CIPHERTEXTBYTES: usize = KYBER_INDCPA_BYTES; + +// Kyber-512 constants (k=2, du=10, dv=4, eta1=3, eta2=2) +pub const KYBER_512_K: usize = 2; +pub const KYBER_512_ETA1: usize = 3; +pub const KYBER_512_ETA2: usize = 2; +pub const KYBER_512_POLYVECBYTES: usize = KYBER_512_K * KYBER_POLYBYTES; +pub const KYBER_512_POLYVECCOMPRESSEDBYTES: usize = KYBER_512_K * 320; +pub const KYBER_512_POLYCOMPRESSEDBYTES: usize = 128; +pub const KYBER_512_INDCPA_PUBLICKEYBYTES: usize = KYBER_512_POLYVECBYTES + KYBER_SYMBYTES; +pub const KYBER_512_INDCPA_SECRETKEYBYTES: usize = KYBER_512_POLYVECBYTES; +pub const KYBER_512_INDCPA_BYTES: usize = + KYBER_512_POLYVECCOMPRESSEDBYTES + KYBER_512_POLYCOMPRESSEDBYTES; +pub const KYBER_512_PUBLICKEYBYTES: usize = KYBER_512_INDCPA_PUBLICKEYBYTES; +pub const KYBER_512_SECRETKEYBYTES: usize = + KYBER_512_INDCPA_SECRETKEYBYTES + KYBER_512_INDCPA_PUBLICKEYBYTES + 2 * KYBER_SYMBYTES; +pub const KYBER_512_CIPHERTEXTBYTES: usize = KYBER_512_INDCPA_BYTES; + +// Kyber-768 constants (k=3, du=10, dv=4) +pub const KYBER_768_K: usize = 3; +pub const KYBER_768_POLYVECBYTES: usize = KYBER_768_K * KYBER_POLYBYTES; +pub const KYBER_768_POLYVECCOMPRESSEDBYTES: usize = KYBER_768_K * 320; +pub const KYBER_768_POLYCOMPRESSEDBYTES: usize = 128; +pub const KYBER_768_INDCPA_PUBLICKEYBYTES: usize = KYBER_768_POLYVECBYTES + KYBER_SYMBYTES; +pub const KYBER_768_INDCPA_SECRETKEYBYTES: usize = KYBER_768_POLYVECBYTES; +pub const KYBER_768_INDCPA_BYTES: usize = + KYBER_768_POLYVECCOMPRESSEDBYTES + KYBER_768_POLYCOMPRESSEDBYTES; +pub const KYBER_768_PUBLICKEYBYTES: usize = KYBER_768_INDCPA_PUBLICKEYBYTES; +pub const KYBER_768_SECRETKEYBYTES: usize = + KYBER_768_INDCPA_SECRETKEYBYTES + KYBER_768_INDCPA_PUBLICKEYBYTES + 2 * KYBER_SYMBYTES; +pub const KYBER_768_CIPHERTEXTBYTES: usize = KYBER_768_INDCPA_BYTES; + +// Kyber-1024 constants (k=4, du=11, dv=5) +pub const KYBER_1024_K: usize = 4; +pub const KYBER_1024_POLYVECBYTES: usize = KYBER_1024_K * KYBER_POLYBYTES; +pub const KYBER_1024_POLYVECCOMPRESSEDBYTES: usize = KYBER_1024_K * 352; +pub const KYBER_1024_POLYCOMPRESSEDBYTES: usize = 160; +pub const KYBER_1024_INDCPA_PUBLICKEYBYTES: usize = KYBER_1024_POLYVECBYTES + KYBER_SYMBYTES; +pub const KYBER_1024_INDCPA_SECRETKEYBYTES: usize = KYBER_1024_POLYVECBYTES; +pub const KYBER_1024_INDCPA_BYTES: usize = + KYBER_1024_POLYVECCOMPRESSEDBYTES + KYBER_1024_POLYCOMPRESSEDBYTES; +pub const KYBER_1024_PUBLICKEYBYTES: usize = KYBER_1024_INDCPA_PUBLICKEYBYTES; +pub const KYBER_1024_SECRETKEYBYTES: usize = + KYBER_1024_INDCPA_SECRETKEYBYTES + KYBER_1024_INDCPA_PUBLICKEYBYTES + 2 * KYBER_SYMBYTES; +pub const KYBER_1024_CIPHERTEXTBYTES: usize = KYBER_1024_INDCPA_BYTES; diff --git a/src/kyber/poly.rs b/src/kyber/poly.rs new file mode 100644 index 0000000..669a08e --- /dev/null +++ b/src/kyber/poly.rs @@ -0,0 +1,227 @@ +use super::cbd::poly_cbd_eta1; +use super::cbd::poly_cbd_eta1_512; +use super::cbd::poly_cbd_eta2; +use super::ntt::{base_multiply, ntt_forward, ntt_inverse, ZETAS}; +use super::params::*; +use super::reduce::{barrett_reduce, montgomery_reduce}; +use super::symmetric::shake256_prf_expand; + +use zeroize::Zeroize; + +#[derive(Clone, Zeroize)] +#[zeroize(drop)] +pub struct Poly { + pub coeffs: [i16; KYBER_N], +} + +impl Default for Poly { + fn default() -> Self { + Poly { + coeffs: [0i16; KYBER_N], + } + } +} + +impl Poly { + pub fn new() -> Self { + Self::default() + } +} + +pub fn poly_compress(r: &mut [u8], a: Poly) { + let mut t = [0u8; 8]; + let mut k = 0usize; + let mut u: i16; + + for i in 0..KYBER_N / 8 { + for j in 0..8 { + u = a.coeffs[8 * i + j]; + u += (u >> 15) & KYBER_Q as i16; + let d = (((u as u32) << 4) + 1664) as u64; + t[j] = (((d * 10321345) >> 35) & 15) as u8; + } + r[k] = t[0] | (t[1] << 4); + r[k + 1] = t[2] | (t[3] << 4); + r[k + 2] = t[4] | (t[5] << 4); + r[k + 3] = t[6] | (t[7] << 4); + k += 4; + } +} + +pub fn poly_compress_1024(r: &mut [u8], a: Poly) { + let mut t = [0u8; 8]; + let mut k = 0usize; + let mut u: i16; + + for i in 0..KYBER_N / 8 { + for j in 0..8 { + u = a.coeffs[8 * i + j]; + u += (u >> 15) & KYBER_Q as i16; + let d = (((u as u32) << 5) + 1664) as u64; + t[j] = (((d * 10321345) >> 35) & 31) as u8; + } + r[k] = t[0] | (t[1] << 5); + r[k + 1] = (t[1] >> 3) | (t[2] << 2) | (t[3] << 7); + r[k + 2] = (t[3] >> 1) | (t[4] << 4); + r[k + 3] = (t[4] >> 4) | (t[5] << 1) | (t[6] << 6); + r[k + 4] = (t[6] >> 2) | (t[7] << 3); + k += 5; + } +} + +pub fn poly_decompress(r: &mut Poly, a: &[u8]) { + let mut idx = 0usize; + for i in 0..KYBER_N / 2 { + r.coeffs[2 * i + 0] = ((((a[idx] & 15) as usize * KYBER_Q) + 8) >> 4) as i16; + r.coeffs[2 * i + 1] = ((((a[idx] >> 4) as usize * KYBER_Q) + 8) >> 4) as i16; + idx += 1; + } +} + +pub fn poly_decompress_1024(r: &mut Poly, a: &[u8]) { + let mut idx = 0usize; + for i in 0..KYBER_N / 8 { + r.coeffs[8 * i + 0] = ((((a[idx + 0] & 31) as usize * KYBER_Q) + 16) >> 5) as i16; + r.coeffs[8 * i + 1] = + (((((a[idx + 0] >> 5) | ((a[idx + 1] & 3) << 3)) as usize * KYBER_Q) + 16) >> 5) as i16; + r.coeffs[8 * i + 2] = (((((a[idx + 1] >> 2) & 31) as usize * KYBER_Q) + 16) >> 5) as i16; + r.coeffs[8 * i + 3] = + (((((a[idx + 1] >> 7) | ((a[idx + 2] & 15) << 1)) as usize * KYBER_Q) + 16) >> 5) + as i16; + r.coeffs[8 * i + 4] = + (((((a[idx + 2] >> 4) | ((a[idx + 3] & 1) << 4)) as usize * KYBER_Q) + 16) >> 5) as i16; + r.coeffs[8 * i + 5] = (((((a[idx + 3] >> 1) & 31) as usize * KYBER_Q) + 16) >> 5) as i16; + r.coeffs[8 * i + 6] = + (((((a[idx + 3] >> 6) | ((a[idx + 4] & 7) << 2)) as usize * KYBER_Q) + 16) >> 5) as i16; + r.coeffs[8 * i + 7] = ((((a[idx + 4] >> 3) as usize * KYBER_Q) + 16) >> 5) as i16; + idx += 5; + } +} + +pub fn poly_tobytes(r: &mut [u8], a: Poly) { + let (mut t0, mut t1); + + for i in 0..(KYBER_N / 2) { + t0 = a.coeffs[2 * i]; + t0 += (t0 >> 15) & KYBER_Q as i16; + t1 = a.coeffs[2 * i + 1]; + t1 += (t1 >> 15) & KYBER_Q as i16; + r[3 * i + 0] = (t0 >> 0) as u8; + r[3 * i + 1] = ((t0 >> 8) | (t1 << 4)) as u8; + r[3 * i + 2] = (t1 >> 4) as u8; + } +} + +pub fn poly_frombytes(r: &mut Poly, a: &[u8]) { + for i in 0..(KYBER_N / 2) { + r.coeffs[2 * i + 0] = + ((a[3 * i + 0] >> 0) as u16 | ((a[3 * i + 1] as u16) << 8) & 0xFFF) as i16; + r.coeffs[2 * i + 1] = + ((a[3 * i + 1] >> 4) as u16 | ((a[3 * i + 2] as u16) << 4) & 0xFFF) as i16; + } +} + +pub fn poly_getnoise_eta1(r: &mut Poly, seed: &[u8], nonce: u8) { + const LENGTH: usize = KYBER_ETA1 * KYBER_N / 4; + let mut buf = [0u8; LENGTH]; + shake256_prf_expand(&mut buf, LENGTH, seed, nonce); + poly_cbd_eta1(r, &buf); +} + +pub fn poly_getnoise_eta1_512(r: &mut Poly, seed: &[u8], nonce: u8) { + const LENGTH: usize = KYBER_512_ETA1 * KYBER_N / 4; + let mut buf = [0u8; LENGTH]; + shake256_prf_expand(&mut buf, LENGTH, seed, nonce); + poly_cbd_eta1_512(r, &buf); +} + +pub fn poly_getnoise_eta2(r: &mut Poly, seed: &[u8], nonce: u8) { + const LENGTH: usize = KYBER_ETA2 * KYBER_N / 4; + let mut buf = [0u8; LENGTH]; + shake256_prf_expand(&mut buf, LENGTH, seed, nonce); + poly_cbd_eta2(r, &buf); +} + +pub fn poly_getnoise_eta2_512(r: &mut Poly, seed: &[u8], nonce: u8) { + const LENGTH: usize = KYBER_512_ETA2 * KYBER_N / 4; + let mut buf = [0u8; LENGTH]; + shake256_prf_expand(&mut buf, LENGTH, seed, nonce); + poly_cbd_eta2(r, &buf); +} + +pub fn poly_ntt(r: &mut Poly) { + ntt_forward(&mut r.coeffs); + poly_reduce(r); +} + +pub fn poly_invntt_tomont(r: &mut Poly) { + ntt_inverse(&mut r.coeffs); +} + +pub fn poly_base_multiply(r: &mut Poly, a: &Poly, b: &Poly) { + for i in 0..(KYBER_N / 4) { + base_multiply( + &mut r.coeffs[4 * i..], + &a.coeffs[4 * i..], + &b.coeffs[4 * i..], + ZETAS[64 + i], + ); + base_multiply( + &mut r.coeffs[4 * i + 2..], + &a.coeffs[4 * i + 2..], + &b.coeffs[4 * i + 2..], + -(ZETAS[64 + i]), + ); + } +} + +pub fn poly_tomont(r: &mut Poly) { + let f = ((1u64 << 32) % KYBER_Q as u64) as i16; + for i in 0..KYBER_N { + let a = r.coeffs[i] as i32 * f as i32; + r.coeffs[i] = montgomery_reduce(a); + } +} + +pub fn poly_reduce(r: &mut Poly) { + for i in 0..KYBER_N { + r.coeffs[i] = barrett_reduce(r.coeffs[i] as i32); + } +} + +pub fn poly_add(r: &mut Poly, b: &Poly) { + for i in 0..KYBER_N { + r.coeffs[i] += b.coeffs[i]; + } +} + +pub fn poly_sub(r: &mut Poly, a: &Poly) { + for i in 0..KYBER_N { + r.coeffs[i] = a.coeffs[i] - r.coeffs[i]; + } +} + +pub fn poly_frommsg(r: &mut Poly, msg: &[u8]) { + let mut mask; + for i in 0..KYBER_N / 8 { + for j in 0..8 { + mask = ((msg[i] as u16 >> j) & 1).wrapping_neg(); + r.coeffs[8 * i + j] = (mask & ((KYBER_Q + 1) / 2) as u16) as i16; + } + } +} + +pub fn poly_tomsg(msg: &mut [u8], a: Poly) { + let mut t; + + for i in 0..KYBER_N / 8 { + msg[i] = 0; + for j in 0..8 { + t = a.coeffs[8 * i + j]; + t += (t >> 15) & KYBER_Q as i16; + let d = (((t as u32) << 1) + 1664) as u64; + t = (((d * 10321345) >> 35) & 1) as i16; + msg[i] |= (t << j) as u8; + } + } +} diff --git a/src/kyber/polyvec.rs b/src/kyber/polyvec.rs new file mode 100644 index 0000000..92712d7 --- /dev/null +++ b/src/kyber/polyvec.rs @@ -0,0 +1,371 @@ +#![allow(clippy::precedence)] +use super::params::*; +use super::poly::*; + +use zeroize::Zeroize; + +#[derive(Clone, Zeroize)] +#[zeroize(drop)] +pub struct Polyvec512 { + pub vec: [Poly; KYBER_512_K], +} + +impl Polyvec512 { + pub fn new() -> Self { + Polyvec512 { + vec: core::array::from_fn(|_| Poly::new()), + } + } +} + +impl Default for Polyvec512 { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Zeroize)] +#[zeroize(drop)] +pub struct Polyvec768 { + pub vec: [Poly; KYBER_768_K], +} + +impl Polyvec768 { + pub fn new() -> Self { + Polyvec768 { + vec: core::array::from_fn(|_| Poly::new()), + } + } +} + +impl Default for Polyvec768 { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Zeroize)] +#[zeroize(drop)] +pub struct Polyvec1024 { + pub vec: [Poly; KYBER_1024_K], +} + +impl Polyvec1024 { + pub fn new() -> Self { + Polyvec1024 { + vec: core::array::from_fn(|_| Poly::new()), + } + } +} + +impl Default for Polyvec1024 { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Zeroize)] +#[zeroize(drop)] +pub struct Polyvec { + pub vec: [Poly; KYBER_K], +} + +impl Polyvec { + pub fn new() -> Self { + Polyvec { + vec: core::array::from_fn(|_| Poly::new()), + } + } +} + +impl Default for Polyvec { + fn default() -> Self { + Self::new() + } +} + +pub fn polyvec_compress_512(r: &mut [u8], a: Polyvec512) { + let mut t = [0u16; 4]; + let mut idx = 0usize; + const K: usize = KYBER_512_K; + for i in 0..K { + for j in 0..KYBER_N / 4 { + for k in 0..4 { + t[k] = a.vec[i].coeffs[4 * j + k] as u16; + t[k] = t[k].wrapping_add((((t[k] as i16) >> 15) & KYBER_Q as i16) as u16); + let d = (((t[k] as u32) << 10) + 1664) as u64; + t[k] = (((d * 10321345) >> 35) & 0x3ff) as u16; + } + r[idx + 0] = (t[0] >> 0) as u8; + r[idx + 1] = ((t[0] >> 8) | (t[1] << 2)) as u8; + r[idx + 2] = ((t[1] >> 6) | (t[2] << 4)) as u8; + r[idx + 3] = ((t[2] >> 4) | (t[3] << 6)) as u8; + r[idx + 4] = (t[3] >> 2) as u8; + idx += 5; + } + } +} + +pub fn polyvec_decompress_512(r: &mut Polyvec512, a: &[u8]) { + let mut idx = 0usize; + let mut t = [0u16; 4]; + const K: usize = KYBER_512_K; + for i in 0..K { + for j in 0..KYBER_N / 4 { + t[0] = (a[idx + 0] as u16) | (a[idx + 1] as u16) << 8; + t[1] = (a[idx + 1] >> 2) as u16 | (a[idx + 2] as u16) << 6; + t[2] = (a[idx + 2] >> 4) as u16 | (a[idx + 3] as u16) << 4; + t[3] = (a[idx + 3] >> 6) as u16 | (a[idx + 4] as u16) << 2; + idx += 5; + + for k in 0..4 { + r.vec[i].coeffs[4 * j + k] = + ((((t[k] as u32) & 0x3FF) * KYBER_Q as u32 + 512) >> 10) as i16; + } + } + } +} + +pub fn polyvec_tobytes_512(r: &mut [u8], a: &Polyvec512) { + const K: usize = KYBER_512_K; + for i in 0..K { + poly_tobytes(&mut r[i * KYBER_POLYBYTES..], a.vec[i].clone()); + } +} + +pub fn polyvec_frombytes_512(r: &mut Polyvec512, a: &[u8]) { + const K: usize = KYBER_512_K; + for i in 0..K { + poly_frombytes(&mut r.vec[i], &a[i * KYBER_POLYBYTES..]); + } +} + +pub fn polyvec_ntt_512(r: &mut Polyvec512) { + const K: usize = KYBER_512_K; + for i in 0..K { + poly_ntt(&mut r.vec[i]); + } +} + +pub fn polyvec_invntt_tomont_512(r: &mut Polyvec512) { + const K: usize = KYBER_512_K; + for i in 0..K { + poly_invntt_tomont(&mut r.vec[i]); + } +} + +pub fn polyvec_basemul_acc_montgomery_512(r: &mut Poly, a: &Polyvec512, b: &Polyvec512) { + const K: usize = KYBER_512_K; + let mut t = Poly::new(); + poly_base_multiply(r, &a.vec[0], &b.vec[0]); + for i in 1..K { + poly_base_multiply(&mut t, &a.vec[i], &b.vec[i]); + poly_add(r, &t); + } + poly_reduce(r); +} + +pub fn polyvec_reduce_512(r: &mut Polyvec512) { + const K: usize = KYBER_512_K; + for i in 0..K { + poly_reduce(&mut r.vec[i]); + } +} + +pub fn polyvec_add_512(r: &mut Polyvec512, b: &Polyvec512) { + const K: usize = KYBER_512_K; + for i in 0..K { + poly_add(&mut r.vec[i], &b.vec[i]); + } +} + +pub fn polyvec_compress_1024(r: &mut [u8], a: Polyvec1024) { + let mut t = [0u16; 8]; + let mut idx = 0usize; + const K: usize = KYBER_1024_K; + for i in 0..K { + for j in 0..KYBER_N / 8 { + for k in 0..8 { + t[k] = a.vec[i].coeffs[8 * j + k] as u16; + t[k] = t[k].wrapping_add((((t[k] as i16) >> 15) & KYBER_Q as i16) as u16); + let d = (((t[k] as u32) << 11) + 1664) as u64; + t[k] = (((d * 10321345) >> 35) & 0x7ff) as u16; + } + r[idx + 0] = (t[0] >> 0) as u8; + r[idx + 1] = ((t[0] >> 8) | (t[1] << 3)) as u8; + r[idx + 2] = ((t[1] >> 5) | (t[2] << 6)) as u8; + r[idx + 3] = (t[2] >> 2) as u8; + r[idx + 4] = ((t[2] >> 10) | (t[3] << 1)) as u8; + r[idx + 5] = ((t[3] >> 7) | (t[4] << 4)) as u8; + r[idx + 6] = ((t[4] >> 4) | (t[5] << 7)) as u8; + r[idx + 7] = (t[5] >> 1) as u8; + r[idx + 8] = ((t[5] >> 9) | (t[6] << 2)) as u8; + r[idx + 9] = ((t[6] >> 6) | (t[7] << 5)) as u8; + r[idx + 10] = (t[7] >> 3) as u8; + idx += 11; + } + } +} + +pub fn polyvec_decompress_1024(r: &mut Polyvec1024, a: &[u8]) { + let mut idx = 0usize; + let mut t = [0u16; 8]; + const K: usize = KYBER_1024_K; + for i in 0..K { + for j in 0..KYBER_N / 8 { + t[0] = (a[idx + 0] as u16) | (a[idx + 1] as u16) << 8; + t[1] = (a[idx + 1] >> 3) as u16 | (a[idx + 2] as u16) << 5; + t[2] = (a[idx + 2] >> 6) as u16 | (a[idx + 3] as u16) << 2 | (a[idx + 4] as u16) << 10; + t[3] = (a[idx + 4] >> 1) as u16 | (a[idx + 5] as u16) << 7; + t[4] = (a[idx + 5] >> 4) as u16 | (a[idx + 6] as u16) << 4; + t[5] = (a[idx + 6] >> 7) as u16 | (a[idx + 7] as u16) << 1 | (a[idx + 8] as u16) << 9; + t[6] = (a[idx + 8] >> 2) as u16 | (a[idx + 9] as u16) << 6; + t[7] = (a[idx + 9] >> 5) as u16 | (a[idx + 10] as u16) << 3; + idx += 11; + + for k in 0..8 { + r.vec[i].coeffs[8 * j + k] = + ((((t[k] as u32) & 0x7FF) * KYBER_Q as u32 + 1024) >> 11) as i16; + } + } + } +} + +pub fn polyvec_tobytes_1024(r: &mut [u8], a: &Polyvec1024) { + const K: usize = KYBER_1024_K; + for i in 0..K { + poly_tobytes(&mut r[i * KYBER_POLYBYTES..], a.vec[i].clone()); + } +} + +pub fn polyvec_frombytes_1024(r: &mut Polyvec1024, a: &[u8]) { + const K: usize = KYBER_1024_K; + for i in 0..K { + poly_frombytes(&mut r.vec[i], &a[i * KYBER_POLYBYTES..]); + } +} + +pub fn polyvec_ntt_1024(r: &mut Polyvec1024) { + const K: usize = KYBER_1024_K; + for i in 0..K { + poly_ntt(&mut r.vec[i]); + } +} + +pub fn polyvec_invntt_tomont_1024(r: &mut Polyvec1024) { + const K: usize = KYBER_1024_K; + for i in 0..K { + poly_invntt_tomont(&mut r.vec[i]); + } +} + +pub fn polyvec_basemul_acc_montgomery_1024(r: &mut Poly, a: &Polyvec1024, b: &Polyvec1024) { + const K: usize = KYBER_1024_K; + let mut t = Poly::new(); + poly_base_multiply(r, &a.vec[0], &b.vec[0]); + for i in 1..K { + poly_base_multiply(&mut t, &a.vec[i], &b.vec[i]); + poly_add(r, &t); + } + poly_reduce(r); +} + +pub fn polyvec_reduce_1024(r: &mut Polyvec1024) { + const K: usize = KYBER_1024_K; + for i in 0..K { + poly_reduce(&mut r.vec[i]); + } +} + +pub fn polyvec_add_1024(r: &mut Polyvec1024, b: &Polyvec1024) { + const K: usize = KYBER_1024_K; + for i in 0..K { + poly_add(&mut r.vec[i], &b.vec[i]); + } +} + +pub fn polyvec_compress(r: &mut [u8], a: Polyvec) { + let mut t = [0u16; 4]; + let mut idx = 0usize; + for i in 0..KYBER_K { + for j in 0..KYBER_N / 4 { + for k in 0..4 { + t[k] = a.vec[i].coeffs[4 * j + k] as u16; + t[k] = t[k].wrapping_add((((t[k] as i16) >> 15) & KYBER_Q as i16) as u16); + let d = (((t[k] as u32) << 10) + 1664) as u64; + t[k] = (((d * 10321345) >> 35) & 0x3ff) as u16; + } + r[idx + 0] = (t[0] >> 0) as u8; + r[idx + 1] = ((t[0] >> 8) | (t[1] << 2)) as u8; + r[idx + 2] = ((t[1] >> 6) | (t[2] << 4)) as u8; + r[idx + 3] = ((t[2] >> 4) | (t[3] << 6)) as u8; + r[idx + 4] = (t[3] >> 2) as u8; + idx += 5; + } + } +} + +pub fn polyvec_decompress(r: &mut Polyvec, a: &[u8]) { + let mut idx = 0usize; + let mut t = [0u16; 4]; + for i in 0..KYBER_K { + for j in 0..KYBER_N / 4 { + t[0] = (a[idx + 0] as u16) | (a[idx + 1] as u16) << 8; + t[1] = (a[idx + 1] >> 2) as u16 | (a[idx + 2] as u16) << 6; + t[2] = (a[idx + 2] >> 4) as u16 | (a[idx + 3] as u16) << 4; + t[3] = (a[idx + 3] >> 6) as u16 | (a[idx + 4] as u16) << 2; + idx += 5; + + for k in 0..4 { + r.vec[i].coeffs[4 * j + k] = + ((((t[k] as u32) & 0x3FF) * KYBER_Q as u32 + 512) >> 10) as i16; + } + } + } +} + +pub fn polyvec_tobytes(r: &mut [u8], a: &Polyvec) { + for i in 0..KYBER_K { + poly_tobytes(&mut r[i * KYBER_POLYBYTES..], a.vec[i].clone()); + } +} + +pub fn polyvec_frombytes(r: &mut Polyvec, a: &[u8]) { + for i in 0..KYBER_K { + poly_frombytes(&mut r.vec[i], &a[i * KYBER_POLYBYTES..]); + } +} + +pub fn polyvec_ntt(r: &mut Polyvec) { + for i in 0..KYBER_K { + poly_ntt(&mut r.vec[i]); + } +} + +pub fn polyvec_invntt_tomont(r: &mut Polyvec) { + for i in 0..KYBER_K { + poly_invntt_tomont(&mut r.vec[i]); + } +} + +pub fn polyvec_basemul_acc_montgomery(r: &mut Poly, a: &Polyvec, b: &Polyvec) { + let mut t = Poly::new(); + poly_base_multiply(r, &a.vec[0], &b.vec[0]); + for i in 1..KYBER_K { + poly_base_multiply(&mut t, &a.vec[i], &b.vec[i]); + poly_add(r, &t); + } + poly_reduce(r); +} + +pub fn polyvec_reduce(r: &mut Polyvec) { + for i in 0..KYBER_K { + poly_reduce(&mut r.vec[i]); + } +} + +pub fn polyvec_add(r: &mut Polyvec, b: &Polyvec) { + for i in 0..KYBER_K { + poly_add(&mut r.vec[i], &b.vec[i]); + } +} diff --git a/src/kyber/reduce.rs b/src/kyber/reduce.rs new file mode 100644 index 0000000..239261b --- /dev/null +++ b/src/kyber/reduce.rs @@ -0,0 +1,20 @@ +use super::params::*; + +const QINV: i32 = 62209; + +pub fn montgomery_reduce(a: i32) -> i16 { + let ua = a.wrapping_mul(QINV) as i16; + let u = ua as i32; + let mut t = u * KYBER_Q as i32; + t = a - t; + t >>= 16; + t as i16 +} + +pub fn barrett_reduce(a: i32) -> i16 { + let v = ((1u32 << 26) / KYBER_Q as u32 + 1) as i32; + let mut t = v * a + (1 << 25); + t >>= 26; + t *= KYBER_Q as i32; + (a - t) as i16 +} diff --git a/src/kyber/symmetric.rs b/src/kyber/symmetric.rs new file mode 100644 index 0000000..3daabba --- /dev/null +++ b/src/kyber/symmetric.rs @@ -0,0 +1,76 @@ +#![allow(dead_code)] + +use zeroize::Zeroize; + +pub use super::fips202::{ + sha3_256, sha3_512, shake128_absorb_once, shake256, KeccakState, SHAKE128_RATE, +}; + +pub const AES256CTR_BLOCKBYTES: usize = 64; + +pub const XOF_BLOCKBYTES: usize = SHAKE128_RATE; + +pub type XofState = KeccakState; + +/// SHA3-256 hash function (H) used in Kyber for hashing messages +/// and other cryptographic operations requiring a 256-bit hash. +pub fn hash_sha3_256(out: &mut [u8], input: &[u8], inlen: usize) { + sha3_256(out, input, inlen); +} + +/// SHA3-512 hash function (G) used in Kyber for generating +/// pseudorandom bytes from a seed. +pub fn hash_sha3_512(out: &mut [u8], input: &[u8], inlen: usize) { + sha3_512(out, input, inlen); +} + +/// XOF (Extendable Output Function) absorb operation for SHAKE128. +/// Absorbs input bytes into the XOF state with coordinate parameters. +pub fn xof_absorb_bytes(state: &mut XofState, input: &[u8], x_coord: u8, y_coord: u8) { + kyber_shake128_absorb(state, input, x_coord, y_coord); +} + +/// XOF squeeze operation that outputs blocks of SHAKE128. +pub fn xof_squeeze_blocks(out: &mut [u8], outblocks: usize, state: &mut XofState) { + kyber_shake128_squeezeblocks(out, outblocks, state); +} + +/// PRF (Pseudorandom Function) using SHAKE256 for expanding a key +/// and nonce into arbitrary-length output. +/// Used in Kyber for generating random bytes from a seed and nonce. +pub fn shake256_prf_expand(out: &mut [u8], outbytes: usize, key: &[u8], nonce: u8) { + shake256_prf(out, outbytes, key, nonce); +} + +/// KDF (Key Derivation Function) using SHAKE256 to derive +/// a 32-byte output from input bytes. +/// Used in Kyber for key expansion. +pub fn shake256_kdf(out: &mut [u8], input: &[u8], inlen: usize) { + shake256(out, 32, input, inlen); +} + +/// Internal: Absorbs input into SHAKE128 XOF with extended seed (coordinates). +/// Combines the input seed with x and y coordinates for domain separation. +fn kyber_shake128_absorb(state: &mut KeccakState, input: &[u8], x_coord: u8, y_coord: u8) { + let mut extseed = [0u8; 34]; + extseed[..32].copy_from_slice(input); + extseed[32] = x_coord; + extseed[33] = y_coord; + shake128_absorb_once(state, &extseed, 34); + extseed.zeroize(); +} + +/// Internal: Squeezes output blocks from SHAKE128 XOF state. +fn kyber_shake128_squeezeblocks(output: &mut [u8], nblocks: usize, state: &mut KeccakState) { + super::fips202::shake128_squeezeblocks(output, nblocks, state); +} + +/// Internal: SHA implementation that extends a key and nonce +/// into arbitrary-length output using SHAKE256. +fn shake256_prf(output: &mut [u8], outlen: usize, key: &[u8], nonce: u8) { + let mut extkey = [0u8; 33]; + extkey[..32].copy_from_slice(key); + extkey[32] = nonce; + shake256(output, outlen, &extkey, 33); + extkey.zeroize(); +} diff --git a/src/kyber/verify.rs b/src/kyber/verify.rs new file mode 100644 index 0000000..ef9f7fd --- /dev/null +++ b/src/kyber/verify.rs @@ -0,0 +1,15 @@ +pub fn verify(a: &[u8], b: &[u8], len: usize) -> u8 { + let mut r = 0u64; + for i in 0..len { + r |= (a[i] ^ b[i]) as u64; + } + r = r.wrapping_neg() >> 63; + r as u8 +} + +pub fn cmov(r: &mut [u8], x: &[u8], len: usize, mut b: u8) { + b = b.wrapping_neg(); + for i in 0..len { + r[i] ^= b & (x[i] ^ r[i]); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0d8274d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,214 @@ +pub mod kyber; + +use kyber::{ + crypto_kem_dec_1024, crypto_kem_dec_512, crypto_kem_enc_1024, crypto_kem_enc_512, + crypto_kem_keypair_1024, crypto_kem_keypair_512, decapsulate, encapsulate, error::KyberError, + keypair, KYBER_1024_CIPHERTEXTBYTES, KYBER_1024_PUBLICKEYBYTES, KYBER_1024_SECRETKEYBYTES, + KYBER_512_CIPHERTEXTBYTES, KYBER_512_PUBLICKEYBYTES, KYBER_512_SECRETKEYBYTES, +}; +use pyo3::exceptions::PyException; +use pyo3::prelude::*; +use rand::thread_rng; + +fn make_kyber_error(msg: &str) -> PyErr { + PyException::new_err(msg.to_string()) +} + +#[pyfunction] +fn generate_keypair(seed: Option<&[u8]>) -> PyResult<(Vec, Vec)> { + if let Some(s) = seed { + if s.len() != 64 { + return Err(make_kyber_error("Seed must be 64 bytes")); + } + kyber::derive(s) + .map(|keys| (keys.public.to_vec(), keys.secret.to_vec())) + .map_err(|e| make_kyber_error(&e.to_string())) + } else { + let mut rng = thread_rng(); + keypair(&mut rng) + .map(|keys| (keys.public.to_vec(), keys.secret.to_vec())) + .map_err(|e| make_kyber_error(&e.to_string())) + } +} + +#[pyfunction] +fn generate_keypair_512(seed: Option<&[u8]>) -> PyResult<(Vec, Vec)> { + let mut rng = thread_rng(); + let mut pk = vec![0u8; KYBER_512_PUBLICKEYBYTES]; + let mut sk = vec![0u8; KYBER_512_SECRETKEYBYTES]; + let s_opt = if let Some(s) = seed { + if s.len() != 64 { + return Err(make_kyber_error("Seed must be 64 bytes")); + } + Some((&s[..32], &s[32..])) + } else { + None + }; + crypto_kem_keypair_512(&mut pk, &mut sk, &mut rng, s_opt) + .map(|_| (pk, sk)) + .map_err(|e| make_kyber_error(&e.to_string())) +} + +#[pyfunction] +fn generate_keypair_1024(seed: Option<&[u8]>) -> PyResult<(Vec, Vec)> { + let mut rng = thread_rng(); + let mut pk = vec![0u8; KYBER_1024_PUBLICKEYBYTES]; + let mut sk = vec![0u8; KYBER_1024_SECRETKEYBYTES]; + let s_opt = if let Some(s) = seed { + if s.len() != 64 { + return Err(make_kyber_error("Seed must be 64 bytes")); + } + Some((&s[..32], &s[32..])) + } else { + None + }; + crypto_kem_keypair_1024(&mut pk, &mut sk, &mut rng, s_opt) + .map(|_| (pk, sk)) + .map_err(|e| make_kyber_error(&e.to_string())) +} + +#[pyfunction] +fn encapsulate_key(pk: &[u8]) -> PyResult<(Vec, Vec)> { + let mut rng = thread_rng(); + encapsulate(pk, &mut rng) + .map(|(ct, ss)| (ct.to_vec(), ss.to_vec())) + .map_err(|e| make_kyber_error(&e.to_string())) +} + +#[pyfunction] +fn decapsulate_key(ct: &[u8], sk: &[u8]) -> PyResult> { + decapsulate(ct, sk) + .map(|ss| ss.to_vec()) + .map_err(|e| make_kyber_error(&e.to_string())) +} + +#[pyfunction] +fn encapsulate_key_512(pk: &[u8]) -> PyResult<(Vec, Vec)> { + if pk.len() != KYBER_512_PUBLICKEYBYTES { + return Err(make_kyber_error( + &KyberError::invalid_input("pk", KYBER_512_PUBLICKEYBYTES, pk.len()).to_string(), + )); + } + let mut rng = thread_rng(); + let mut ct = vec![0u8; KYBER_512_CIPHERTEXTBYTES]; + let mut ss = vec![0u8; 32]; + crypto_kem_enc_512(&mut ct, &mut ss, pk, &mut rng, None) + .map(|_| (ct, ss)) + .map_err(|e| make_kyber_error(&e.to_string())) +} + +#[pyfunction] +fn encapsulate_key_1024(pk: &[u8]) -> PyResult<(Vec, Vec)> { + if pk.len() != KYBER_1024_PUBLICKEYBYTES { + return Err(make_kyber_error( + &KyberError::invalid_input("pk", KYBER_1024_PUBLICKEYBYTES, pk.len()).to_string(), + )); + } + let mut rng = thread_rng(); + let mut ct = vec![0u8; KYBER_1024_CIPHERTEXTBYTES]; + let mut ss = vec![0u8; 32]; + crypto_kem_enc_1024(&mut ct, &mut ss, pk, &mut rng, None) + .map(|_| (ct, ss)) + .map_err(|e| make_kyber_error(&e.to_string())) +} + +#[pyfunction] +fn decapsulate_key_512(ct: &[u8], sk: &[u8]) -> PyResult> { + if ct.len() != KYBER_512_CIPHERTEXTBYTES { + return Err(make_kyber_error( + &KyberError::invalid_input("ct", KYBER_512_CIPHERTEXTBYTES, ct.len()).to_string(), + )); + } + if sk.len() != KYBER_512_SECRETKEYBYTES { + return Err(make_kyber_error( + &KyberError::invalid_input("sk", KYBER_512_SECRETKEYBYTES, sk.len()).to_string(), + )); + } + let mut ss = vec![0u8; 32]; + crypto_kem_dec_512(&mut ss, ct, sk) + .map(|_| ss) + .map_err(|e| make_kyber_error(&e.to_string())) +} + +#[pyfunction] +fn decapsulate_key_1024(ct: &[u8], sk: &[u8]) -> PyResult> { + if ct.len() != KYBER_1024_CIPHERTEXTBYTES { + return Err(make_kyber_error( + &KyberError::invalid_input("ct", KYBER_1024_CIPHERTEXTBYTES, ct.len()).to_string(), + )); + } + if sk.len() != KYBER_1024_SECRETKEYBYTES { + return Err(make_kyber_error( + &KyberError::invalid_input("sk", KYBER_1024_SECRETKEYBYTES, sk.len()).to_string(), + )); + } + let mut ss = vec![0u8; 32]; + crypto_kem_dec_1024(&mut ss, ct, sk) + .map(|_| ss) + .map_err(|e| make_kyber_error(&e.to_string())) +} + +#[pymodule] +mod _pykyber { + use pyo3::prelude::*; + + #[pyfunction] + pub fn _generate_keypair(seed: Option<&[u8]>) -> PyResult<(Vec, Vec)> { + super::generate_keypair(seed) + } + + #[pyfunction] + pub fn _encapsulate(pk: &[u8]) -> PyResult<(Vec, Vec)> { + super::encapsulate_key(pk) + } + + #[pyfunction] + pub fn _decapsulate(ct: &[u8], sk: &[u8]) -> PyResult> { + super::decapsulate_key(ct, sk) + } + + #[pyfunction] + pub fn _keypair_512(seed: Option<&[u8]>) -> PyResult<(Vec, Vec)> { + super::generate_keypair_512(seed) + } + + #[pyfunction] + pub fn _keypair_768(seed: Option<&[u8]>) -> PyResult<(Vec, Vec)> { + super::generate_keypair(seed) + } + + #[pyfunction] + pub fn _keypair_1024(seed: Option<&[u8]>) -> PyResult<(Vec, Vec)> { + super::generate_keypair_1024(seed) + } + + #[pyfunction] + pub fn _encapsulate_512(pk: &[u8]) -> PyResult<(Vec, Vec)> { + super::encapsulate_key_512(pk) + } + + #[pyfunction] + pub fn _encapsulate_768(pk: &[u8]) -> PyResult<(Vec, Vec)> { + super::encapsulate_key(pk) + } + + #[pyfunction] + pub fn _encapsulate_1024(pk: &[u8]) -> PyResult<(Vec, Vec)> { + super::encapsulate_key_1024(pk) + } + + #[pyfunction] + pub fn _decapsulate_512(ct: &[u8], sk: &[u8]) -> PyResult> { + super::decapsulate_key_512(ct, sk) + } + + #[pyfunction] + pub fn _decapsulate_768(ct: &[u8], sk: &[u8]) -> PyResult> { + super::decapsulate_key(ct, sk) + } + + #[pyfunction] + pub fn _decapsulate_1024(ct: &[u8], sk: &[u8]) -> PyResult> { + super::decapsulate_key_1024(ct, sk) + } +} diff --git a/tests/api_kem.rs b/tests/api_kem.rs new file mode 100644 index 0000000..508419b --- /dev/null +++ b/tests/api_kem.rs @@ -0,0 +1,225 @@ +//! API tests for Kyber KEM - equivalent to src/lib.rs tests + +use pykyber::kyber::api::{decapsulate, derive, encapsulate, keypair, public, Keypair}; +use pykyber::kyber::params::*; +use rand::thread_rng; + +#[test] +fn test_keypair_generation() { + let mut rng = thread_rng(); + let result = keypair(&mut rng); + assert!(result.is_ok()); + let keys = result.unwrap(); + assert_eq!(keys.public.len(), KYBER_PUBLICKEYBYTES); + assert_eq!(keys.secret.len(), KYBER_SECRETKEYBYTES); +} + +#[test] +fn test_encapsulate_decapsulate() { + let mut rng = thread_rng(); + let keys = keypair(&mut rng).unwrap(); + + let (ct, ss) = encapsulate(&keys.public, &mut thread_rng()).unwrap(); + assert_eq!(ct.len(), KYBER_CIPHERTEXTBYTES); + assert_eq!(ss.len(), KYBER_SSBYTES); + + let ss2 = decapsulate(&ct, &keys.secret).unwrap(); + assert_eq!(ss, ss2); +} + +#[test] +fn test_invalid_public_key_length() { + let result = encapsulate(b"short", &mut thread_rng()); + assert!(result.is_err()); +} + +#[test] +fn test_invalid_ciphertext_length() { + let result = decapsulate(b"short", &[0u8; KYBER_SECRETKEYBYTES]); + assert!(result.is_err()); +} + +#[test] +fn test_invalid_secret_key_length() { + let result = decapsulate(&[0u8; KYBER_CIPHERTEXTBYTES], b"short"); + assert!(result.is_err()); +} + +#[test] +fn test_different_keypairs_produce_different_secrets() { + let keys1 = keypair(&mut thread_rng()).unwrap(); + let keys2 = keypair(&mut thread_rng()).unwrap(); + + let (ct1, ss1) = encapsulate(&keys1.public, &mut thread_rng()).unwrap(); + let (ct2, ss2) = encapsulate(&keys2.public, &mut thread_rng()).unwrap(); + + let ss1_dec = decapsulate(&ct1, &keys1.secret).unwrap(); + let ss2_dec = decapsulate(&ct2, &keys2.secret).unwrap(); + + assert_eq!(ss1, ss1_dec); + assert_eq!(ss2, ss2_dec); + assert_ne!(ss1, ss2); +} + +#[test] +fn test_wrong_secret_key_fails() { + let keys = keypair(&mut thread_rng()).unwrap(); + let wrong_keys = keypair(&mut thread_rng()).unwrap(); + + let (ct, ss) = encapsulate(&keys.public, &mut thread_rng()).unwrap(); + let ss_wrong = decapsulate(&ct, &wrong_keys.secret).unwrap(); + + assert_ne!(ss, ss_wrong); +} + +#[test] +fn test_keypair_struct() { + let keys = keypair(&mut thread_rng()).unwrap(); + assert_eq!(keys.public.len(), KYBER_PUBLICKEYBYTES); + assert_eq!(keys.secret.len(), KYBER_SECRETKEYBYTES); +} + +#[test] +fn test_keypair_generate_method() { + let keys = Keypair::generate(&mut thread_rng()).unwrap(); + assert_eq!(keys.public.len(), KYBER_PUBLICKEYBYTES); + assert_eq!(keys.secret.len(), KYBER_SECRETKEYBYTES); +} + +#[test] +fn test_derive_keypair() { + let seed = [0u8; 64]; + let result = derive(&seed); + assert!(result.is_ok()); + let keys = result.unwrap(); + assert_eq!(keys.public.len(), KYBER_PUBLICKEYBYTES); + assert_eq!(keys.secret.len(), KYBER_SECRETKEYBYTES); +} + +#[test] +fn test_derive_invalid_seed_length() { + let seed = [0u8; 32]; + let result = derive(&seed); + assert!(result.is_err()); +} + +#[test] +fn test_public_key_extraction() { + let keys = keypair(&mut thread_rng()).unwrap(); + let extracted_pub = public(&keys.secret); + assert_eq!(extracted_pub.len(), KYBER_INDCPA_PUBLICKEYBYTES); +} + +#[test] +fn test_empty_public_key() { + let result = encapsulate(&[], &mut thread_rng()); + assert!(result.is_err()); +} + +#[test] +fn test_empty_ciphertext() { + let keys = keypair(&mut thread_rng()).unwrap(); + let result = decapsulate(&[], &keys.secret); + assert!(result.is_err()); +} + +#[test] +fn test_empty_secret_key() { + let keys = keypair(&mut thread_rng()).unwrap(); + let (ct, _) = encapsulate(&keys.public, &mut thread_rng()).unwrap(); + let result = decapsulate(&ct, &[]); + assert!(result.is_err()); +} + +#[test] +fn test_multiple_encapsulations_same_pk() { + let keys = keypair(&mut thread_rng()).unwrap(); + + let mut ciphertexts = Vec::new(); + let mut secrets = Vec::new(); + + for _ in 0..5 { + let (ct, ss) = encapsulate(&keys.public, &mut thread_rng()).unwrap(); + ciphertexts.push(ct); + secrets.push(ss); + } + + let mut all_different = false; + for i in 0..ciphertexts.len() { + for j in 0..i { + if ciphertexts[i] != ciphertexts[j] { + all_different = true; + } + } + } + assert!(all_different); + + for (ct, ss) in ciphertexts.iter().zip(secrets.iter()) { + let decapsulated = decapsulate(ct, &keys.secret).unwrap(); + assert_eq!(*ss, decapsulated); + } +} + +#[test] +fn test_deterministic_decapsulation() { + let keys = keypair(&mut thread_rng()).unwrap(); + let (ct, _) = encapsulate(&keys.public, &mut thread_rng()).unwrap(); + + let ss1 = decapsulate(&ct, &keys.secret).unwrap(); + let ss2 = decapsulate(&ct, &keys.secret).unwrap(); + let ss3 = decapsulate(&ct, &keys.secret).unwrap(); + + assert_eq!(ss1, ss2); + assert_eq!(ss2, ss3); +} + +#[test] +fn test_all_zero_keypair() { + let mut rng = thread_rng(); + let keys = keypair(&mut rng).unwrap(); + + assert!(keys.public.iter().any(|&x| x != 0)); + assert!(keys.secret.iter().any(|&x| x != 0)); +} + +#[test] +fn test_shared_secret_all_zeros() { + let keys = keypair(&mut thread_rng()).unwrap(); + let (ct, ss) = encapsulate(&keys.public, &mut thread_rng()).unwrap(); + + assert!(ss.iter().any(|&x| x != 0)); + + let ss2 = decapsulate(&ct, &keys.secret).unwrap(); + assert!(ss2.iter().any(|&x| x != 0)); +} + +#[test] +fn test_boundary_public_key_length() { + let keys = keypair(&mut thread_rng()).unwrap(); + let mut short_pk = keys.public.to_vec(); + short_pk.pop(); + + let result = encapsulate(&short_pk, &mut thread_rng()); + assert!(result.is_err()); +} + +#[test] +fn test_boundary_secret_key_length() { + let keys = keypair(&mut thread_rng()).unwrap(); + let (ct, _) = encapsulate(&keys.public, &mut thread_rng()).unwrap(); + + let mut short_sk = keys.secret.to_vec(); + short_sk.pop(); + + let result = decapsulate(&ct, &short_sk); + assert!(result.is_err()); +} + +#[test] +fn test_boundary_ciphertext_length() { + let keys = keypair(&mut thread_rng()).unwrap(); + let short_ct = vec![0u8; KYBER_CIPHERTEXTBYTES - 1]; + + let result = decapsulate(&short_ct, &keys.secret); + assert!(result.is_err()); +} diff --git a/tests/integration_kem.rs b/tests/integration_kem.rs new file mode 100644 index 0000000..543b067 --- /dev/null +++ b/tests/integration_kem.rs @@ -0,0 +1,192 @@ +//! Integration tests for Kyber KEM + +use pykyber::kyber::api::{decapsulate, encapsulate, keypair}; +use pykyber::kyber::crypto_kem_dec_1024; +use pykyber::kyber::crypto_kem_dec_512; +use pykyber::kyber::crypto_kem_enc_1024; +use pykyber::kyber::crypto_kem_enc_512; +use pykyber::kyber::crypto_kem_keypair_1024; +use pykyber::kyber::crypto_kem_keypair_512; +use pykyber::kyber::params::*; +use rand::thread_rng; + +#[test] +fn test_kyber_512_keypair_sizes() { + let mut rng = thread_rng(); + let mut pk = vec![0u8; KYBER_512_PUBLICKEYBYTES]; + let mut sk = vec![0u8; KYBER_512_SECRETKEYBYTES]; + crypto_kem_keypair_512(&mut pk, &mut sk, &mut rng, None).unwrap(); + + assert_eq!(pk.len(), KYBER_512_PUBLICKEYBYTES); + assert_eq!(sk.len(), KYBER_512_SECRETKEYBYTES); +} + +#[test] +fn test_kyber_768_keypair_sizes() { + let mut rng = thread_rng(); + let keys = keypair(&mut rng).unwrap(); + + assert_eq!(keys.public.len(), KYBER_768_PUBLICKEYBYTES); + assert_eq!(keys.secret.len(), KYBER_768_SECRETKEYBYTES); +} + +#[test] +fn test_kyber_1024_keypair_sizes() { + let mut rng = thread_rng(); + let mut pk = vec![0u8; KYBER_1024_PUBLICKEYBYTES]; + let mut sk = vec![0u8; KYBER_1024_SECRETKEYBYTES]; + crypto_kem_keypair_1024(&mut pk, &mut sk, &mut rng, None).unwrap(); + + assert_eq!(pk.len(), KYBER_1024_PUBLICKEYBYTES); + assert_eq!(sk.len(), KYBER_1024_SECRETKEYBYTES); +} + +#[test] +fn test_kyber_512_encapsulate_decapsulate() { + let mut rng = thread_rng(); + let mut pk = vec![0u8; KYBER_512_PUBLICKEYBYTES]; + let mut sk = vec![0u8; KYBER_512_SECRETKEYBYTES]; + crypto_kem_keypair_512(&mut pk, &mut sk, &mut rng, None).unwrap(); + + let mut ct = vec![0u8; KYBER_512_CIPHERTEXTBYTES]; + let mut ss = vec![0u8; KYBER_SSBYTES]; + crypto_kem_enc_512(&mut ct, &mut ss, &pk, &mut rng, None).unwrap(); + + assert_eq!(ct.len(), KYBER_512_CIPHERTEXTBYTES); + assert_eq!(ss.len(), KYBER_SSBYTES); + + let mut ss2 = vec![0u8; KYBER_SSBYTES]; + crypto_kem_dec_512(&mut ss2, &ct, &sk).unwrap(); + assert_eq!(ss, ss2); +} + +#[test] +fn test_kyber_768_encapsulate_decapsulate() { + let mut rng = thread_rng(); + let keys = keypair(&mut rng).unwrap(); + + let (ct, ss) = encapsulate(&keys.public, &mut rng).unwrap(); + assert_eq!(ct.len(), KYBER_768_CIPHERTEXTBYTES); + assert_eq!(ss.len(), KYBER_SSBYTES); + + let ss2 = decapsulate(&ct, &keys.secret).unwrap(); + assert_eq!(ss.to_vec(), ss2.to_vec()); +} + +#[test] +fn test_kyber_1024_encapsulate_decapsulate() { + let mut rng = thread_rng(); + let mut pk = vec![0u8; KYBER_1024_PUBLICKEYBYTES]; + let mut sk = vec![0u8; KYBER_1024_SECRETKEYBYTES]; + crypto_kem_keypair_1024(&mut pk, &mut sk, &mut rng, None).unwrap(); + + let mut ct = vec![0u8; KYBER_1024_CIPHERTEXTBYTES]; + let mut ss = vec![0u8; KYBER_SSBYTES]; + crypto_kem_enc_1024(&mut ct, &mut ss, &pk, &mut rng, None).unwrap(); + + assert_eq!(ct.len(), KYBER_1024_CIPHERTEXTBYTES); + assert_eq!(ss.len(), KYBER_SSBYTES); + + let mut ss2 = vec![0u8; KYBER_SSBYTES]; + crypto_kem_dec_1024(&mut ss2, &ct, &sk).unwrap(); + assert_eq!(ss, ss2); +} + +#[test] +fn test_different_keypairs_produce_different_secrets() { + let mut rng = thread_rng(); + let keys1 = keypair(&mut rng).unwrap(); + let keys2 = keypair(&mut rng).unwrap(); + + let (ct1, ss1) = encapsulate(&keys1.public, &mut rng).unwrap(); + let (ct2, ss2) = encapsulate(&keys2.public, &mut rng).unwrap(); + + let ss1_dec = decapsulate(&ct1, &keys1.secret).unwrap(); + let ss2_dec = decapsulate(&ct2, &keys2.secret).unwrap(); + + assert_eq!(ss1.to_vec(), ss1_dec.to_vec()); + assert_eq!(ss2.to_vec(), ss2_dec.to_vec()); + assert_ne!(ss1.to_vec(), ss2.to_vec()); +} + +#[test] +fn test_wrong_key_fails() { + let mut rng = thread_rng(); + let keys = keypair(&mut rng).unwrap(); + let wrong_keys = keypair(&mut rng).unwrap(); + + let (ct, ss) = encapsulate(&keys.public, &mut rng).unwrap(); + let ss_wrong = decapsulate(&ct, &wrong_keys.secret).unwrap(); + + assert_ne!(ss.to_vec(), ss_wrong.to_vec()); +} + +#[test] +fn test_decapsulation_is_deterministic() { + let mut rng = thread_rng(); + let keys = keypair(&mut rng).unwrap(); + + let (ct, _) = encapsulate(&keys.public, &mut rng).unwrap(); + + for _ in 0..5 { + let ss = decapsulate(&ct, &keys.secret).unwrap(); + assert_eq!( + ss.to_vec(), + decapsulate(&ct, &keys.secret).unwrap().to_vec(), + "decapsulation should be deterministic" + ); + } +} + +#[test] +fn test_invalid_public_key_length() { + let result = encapsulate(b"short", &mut thread_rng()); + assert!(result.is_err()); +} + +#[test] +fn test_invalid_ciphertext_length() { + let keys = keypair(&mut thread_rng()).unwrap(); + let result = decapsulate(b"short", &keys.secret); + assert!(result.is_err()); +} + +#[test] +fn test_invalid_secret_key_length() { + let keys = keypair(&mut thread_rng()).unwrap(); + let (ct, _) = encapsulate(&keys.public, &mut thread_rng()).unwrap(); + let result = decapsulate(&ct, b"short"); + assert!(result.is_err()); +} + +#[test] +fn test_empty_public_key() { + let result = encapsulate(&[], &mut thread_rng()); + assert!(result.is_err()); +} + +#[test] +fn test_empty_ciphertext() { + let keys = keypair(&mut thread_rng()).unwrap(); + let result = decapsulate(&[], &keys.secret); + assert!(result.is_err()); +} + +#[test] +fn test_keypair_not_all_zeros() { + let keys = keypair(&mut thread_rng()).unwrap(); + assert!(keys.public.iter().any(|&x| x != 0)); + assert!(keys.secret.iter().any(|&x| x != 0)); +} + +#[test] +fn test_shared_secret_not_all_zeros() { + let mut rng = thread_rng(); + let keys = keypair(&mut rng).unwrap(); + let (ct, ss) = encapsulate(&keys.public, &mut rng).unwrap(); + + assert!(ss.iter().any(|&x| x != 0)); + + let ss2 = decapsulate(&ct, &keys.secret).unwrap(); + assert!(ss2.iter().any(|&x| x != 0)); +} diff --git a/tests/unit_params.rs b/tests/unit_params.rs new file mode 100644 index 0000000..ee39958 --- /dev/null +++ b/tests/unit_params.rs @@ -0,0 +1,55 @@ +//! Unit tests for Kyber parameters + +use pykyber::kyber::params::*; + +#[test] +fn test_kyber_512_params() { + assert_eq!(KYBER_512_K, 2); + assert_eq!(KYBER_512_POLYVECBYTES, 2 * 384); + assert_eq!(KYBER_512_POLYVECCOMPRESSEDBYTES, 2 * 320); + assert_eq!(KYBER_512_INDCPA_PUBLICKEYBYTES, 2 * 384 + 32); + assert_eq!(KYBER_512_PUBLICKEYBYTES, 800); + assert_eq!(KYBER_512_SECRETKEYBYTES, 2 * 384 + (2 * 384 + 32) + 2 * 32); + assert_eq!(KYBER_512_CIPHERTEXTBYTES, 2 * 320 + 128); +} + +#[test] +fn test_kyber_768_params() { + assert_eq!(KYBER_768_K, 3); + assert_eq!(KYBER_768_POLYVECBYTES, 3 * 384); + assert_eq!(KYBER_768_POLYVECCOMPRESSEDBYTES, 3 * 320); + assert_eq!(KYBER_768_INDCPA_PUBLICKEYBYTES, 3 * 384 + 32); + assert_eq!(KYBER_768_PUBLICKEYBYTES, 1184); + assert_eq!(KYBER_768_SECRETKEYBYTES, 3 * 384 + (3 * 384 + 32) + 2 * 32); + assert_eq!(KYBER_768_CIPHERTEXTBYTES, 3 * 320 + 128); +} + +#[test] +fn test_kyber_1024_params() { + assert_eq!(KYBER_1024_K, 4); + assert_eq!(KYBER_1024_POLYVECBYTES, 4 * 384); + assert_eq!(KYBER_1024_POLYVECCOMPRESSEDBYTES, 4 * 320); + assert_eq!(KYBER_1024_INDCPA_PUBLICKEYBYTES, 4 * 384 + 32); + assert_eq!(KYBER_1024_PUBLICKEYBYTES, 1568); + assert_eq!(KYBER_1024_SECRETKEYBYTES, 4 * 384 + (4 * 384 + 32) + 2 * 32); + assert_eq!(KYBER_1024_CIPHERTEXTBYTES, 4 * 320 + 128); +} + +#[test] +fn test_common_params() { + assert_eq!(KYBER_N, 256); + assert_eq!(KYBER_Q, 3329); + assert_eq!(KYBER_ETA1, 2); + assert_eq!(KYBER_ETA2, 2); + assert_eq!(KYBER_SYMBYTES, 32); + assert_eq!(KYBER_SSBYTES, 32); + assert_eq!(KYBER_POLYBYTES, 384); + assert_eq!(KYBER_POLYCOMPRESSEDBYTES, 128); +} + +#[test] +fn test_default_kyber_k() { + assert_eq!(KYBER_K, 3); + assert_eq!(KYBER_POLYVECBYTES, KYBER_K * KYBER_POLYBYTES); + assert_eq!(KYBER_PUBLICKEYBYTES, KYBER_INDCPA_PUBLICKEYBYTES); +}