From eb0877c30ba39d6f05dd263e7e0aead0f1bc9e02 Mon Sep 17 00:00:00 2001 From: tn3w Date: Sat, 24 Aug 2024 17:57:56 +0200 Subject: [PATCH] Version 1.0.0 --- .github/build.yml | 32 ++++++++++ .gitignore | 154 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 ++++ Dockerfile | 20 ++++++ README.md | 159 +++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 6 ++ src/lib.rs | 148 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 .github/build.yml create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 pyproject.toml create mode 100644 src/lib.rs diff --git a/.github/build.yml b/.github/build.yml new file mode 100644 index 0000000..bc0cd57 --- /dev/null +++ b/.github/build.yml @@ -0,0 +1,32 @@ +name: Build Wheels + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8, 3.9, 3.10] + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + pip install maturin cibuildwheel + + - name: Build wheels + run: | + if [ ${{ matrix.os }} == 'ubuntu-latest' ]; then + cibuildwheel --output-dir wheelhouse + fi + env: + CIBW_BUILD: "cp36-abi3-* cp37-abi3-* cp38-abi3-* cp39-abi3-* cp310-abi3-*" diff --git a/.gitignore b/.gitignore index d01bd1a..3392716 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,160 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + # RustRover # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e00b502 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "py_ntru" +version = "1.0.0" +edition = "2021" + +[dependencies] +rand = "0.8" +aes-gcm = "0.9" +ntrust-native = { version = "1.0", features = ["ntruhrss701"] } +pyo3 = { version = "0.15", features = ["extension-module"] } + +[lib] +crate-type = ["cdylib"] # Required for creating a Python extension module \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bc4e97f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.11-slim + +# Install Rust and Cargo +RUN apt-get update && apt-get install -y \ + curl \ + build-essential \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + && export PATH="$PATH:/root/.cargo/bin" + +# Install maturin and cibuildwheel +RUN pip install maturin cibuildwheel + +# Set the working directory +WORKDIR /app + +# Copy the project files into the container +COPY . . + +# Run cibuildwheel to build wheels +CMD ["cibuildwheel", "--output-dir", "/wheelhouse"] diff --git a/README.md b/README.md index acc544a..03b2018 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,159 @@ # py_ntru -This is an implementation of the Rust NTRU library through a Python wrapper to improve the availability and speed of post quantum algorithms for Python. + +Welcome to the `py_ntru` documentation! This library provides a Python wrapper around the Rust NTRU library, enabling the use of post-quantum cryptographic algorithms with enhanced performance and availability. The following comprehensive guide will walk you through the installation, building, and usage processes. + +## 🚀 Installation + +### Prerequisites + +Before you begin, ensure you have the following installed on your system: +- **Python 3.7+**: The latest version of Python 3 is recommended. You can download it from the [official Python website](https://www.python.org/downloads/). +- **Rust**: Required for compiling the Rust code. You can install it via [rustup](https://rustup.rs/). + +### Virtualenv Setup + +Using a virtual environment helps manage dependencies and avoid conflicts. Follow these steps to set it up: + +1. **Create a Virtual Environment** + + Run the following command to create a virtual environment in the current directory: + + ```bash + python -m venv .venv + ``` + +2. **Activate the Virtual Environment** + + - On **Linux/macOS**, activate the virtual environment with: + + ```bash + source .venv/bin/activate + ``` + + - On **Windows**, use: + + ```bash + .venv\Scripts\activate + ``` + +### Installing Dependencies + +1. **Install Maturin** + + Maturin is a tool for building and publishing Rust-based Python packages. Install it using pip: + + ```bash + pip install maturin + ``` + +2. **Clone the Repository** + + Clone the `py_ntru` repository from GitHub to your local machine: + + ```bash + git clone https://github.com/tn3w/py_ntru.git + ``` + +3. **Navigate to the Repository Directory** + + Change your working directory to the `py_ntru` directory: + + ```bash + cd py_ntru + ``` + +4. **Build the Python Package** + + Use Maturin to compile the Rust code and create a Python extension wheel. The `--release` flag ensures that the code is optimized for performance: + + ```bash + maturin build --release + ``` + +5. **Locate the Wheel File** + + After building, navigate to the directory where the wheel file is located: + + ```bash + cd target/wheels + ``` + +6. **Install the Wheel File** + + Install the wheel file using pip. Replace `` and `` with the actual version and build number from the wheel file: + + ```bash + pip install py_ntru--.whl + ``` + +## 🛞 Building Wheels Using Docker + +If you prefer to build the package in a Docker container, follow these steps: + +1. **Build the Docker Image** + + Use the provided Dockerfile to build the Docker image. This image contains all the necessary dependencies for building `py_ntru`: + + ```bash + sudo docker build -t py_ntru_builder . + ``` + +2. **Run the Docker Container** + + Start a container from the image and mount your local repository directory: + + ```bash + sudo docker run --rm -it -v $(pwd):/workspace py_ntru_builder + ``` + + Inside the Docker container, navigate to the `/workspace` directory and build the package as described in the [Building the Python Package](#build-the-python-package) section. + +## 📝 Usage + +Once `py_ntru` is installed, you can use it in your Python scripts. Here is a basic example of how to use the library: + +```python +import py_ntru + +# Generate a private key +private_key = py_ntru.generate_private_key() +print(f"Private Key: {private_key}") + +# Generate a public key based on the private key +public_key = py_ntru.generate_public_key_based_on_private_key(private_key) +print(f"Public Key: {public_key}") + +# Message to encrypt +message = b"Hello!" +print(f"Original Message: {message}") + +# Encrypt the message using the public key +ciphertext = py_ntru.encrypt(public_key, message) +print(f"Ciphertext: {ciphertext}") + +# Decrypt the ciphertext using the private key +decrypted_message = py_ntru.decrypt(private_key, ciphertext) +print(f"Decrypted Message: {decrypted_message}") + +``` + +## 🛠 Troubleshooting + +If you encounter any issues during installation or usage, consider the following: + +- **Ensure Compatibility**: Check that your versions of Python, Rust, and Maturin are compatible with `py_ntru`. +- **Check Dependencies**: Verify that all necessary dependencies are installed and up-to-date. +- **Consult Logs**: Review error messages and logs for clues on what might be going wrong. + +For additional support, you can open an issue on the [GitHub repository](https://github.com/tn3w/py_ntru/issues) or consult the community forums. + +## 📚 Contributing + +Contributions to `py_ntru` are welcome! If you'd like to contribute, please follow these guidelines: + +1. **Fork the Repository**: Create a personal fork of the `py_ntru` repository. +2. **Create a Branch**: Make a new branch for your changes. +3. **Make Changes**: Implement your changes and ensure they adhere to the project's coding standards. +4. **Submit a Pull Request**: Push your branch to your fork and submit a pull request to the main repository. + +Thank you for using `py_ntru` and contributing to the advancement of post-quantum cryptography! diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..573249c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = ["maturin"] +build-backend = "maturin" + +[tool.maturin] +name = "py_ntru" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d8ea4b2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,148 @@ +use pyo3::prelude::*; +use pyo3::types::PyBytes; +use ntrust_native::{AesState, crypto_kem_dec, crypto_kem_enc, crypto_kem_keypair}; +use ntrust_native::{CRYPTO_BYTES, CRYPTO_CIPHERTEXTBYTES, CRYPTO_PUBLICKEYBYTES, CRYPTO_SECRETKEYBYTES}; +use aes_gcm::{Aes256Gcm, Key, Nonce}; +use aes_gcm::aead::{Aead, NewAead}; +use rand::Rng; +use std::convert::TryInto; + + +#[pyfunction] +/// Generates a private key for the NTRU scheme. +/// +/// Returns: +/// bytes: The generated private key as a byte sequence. +fn generate_private_key(py: Python) -> PyResult> { + let mut rng = AesState::new(); + let mut sk = [0u8; CRYPTO_SECRETKEYBYTES]; + let mut pk = [0u8; CRYPTO_PUBLICKEYBYTES]; + + crypto_kem_keypair(&mut pk, &mut sk, &mut rng) + .map_err(|e| PyErr::new::(format!("{:?}", e)))?; + + Ok(PyBytes::new(py, &sk).into()) +} + + +#[pyfunction] +/// Generates a public key based on a given private key. +/// +/// Args: +/// private_key (bytes): The private key as a byte sequence. +/// +/// Returns: +/// bytes: The corresponding public key as a byte sequence. +fn generate_public_key_based_on_private_key(py: Python, private_key: &PyBytes) -> PyResult> { + let mut rng = AesState::new(); + let mut pk = [0u8; CRYPTO_PUBLICKEYBYTES]; + let private_key_bytes = private_key.as_bytes(); + + if private_key_bytes.len() != CRYPTO_SECRETKEYBYTES { + return Err(PyErr::new::("Invalid private key length")); + } + + // Create a new array for the private key + let mut sk = [0u8; CRYPTO_SECRETKEYBYTES]; + sk.copy_from_slice(private_key_bytes); + + crypto_kem_keypair(&mut pk, &mut sk, &mut rng) + .map_err(|e| PyErr::new::(format!("{:?}", e)))?; + + Ok(PyBytes::new(py, &pk).into()) +} + + +#[pyfunction] +/// Encrypts a message using the public key. +/// +/// Args: +/// public_key (bytes): The public key as a byte sequence. +/// message (bytes): The message to be encrypted as a byte sequence. +/// +/// Returns: +/// bytes: The ciphertext, which includes the KEM ciphertext, nonce, and AES ciphertext. +fn encrypt(py: Python, public_key: &PyBytes, message: &PyBytes) -> PyResult> { + let mut rng = AesState::new(); + let mut ct = [0u8; CRYPTO_CIPHERTEXTBYTES]; + let mut ss = [0u8; CRYPTO_BYTES]; + + let public_key_bytes = public_key.as_bytes(); + if public_key_bytes.len() != CRYPTO_PUBLICKEYBYTES { + return Err(PyErr::new::("Invalid public key length")); + } + + crypto_kem_enc(&mut ct, &mut ss, public_key_bytes.try_into().map_err(|_| PyErr::new::("Invalid public key length"))?, &mut rng) + .map_err(|e| PyErr::new::(format!("{:?}", e)))?; + + let key = Key::from_slice(&ss); + let cipher = Aes256Gcm::new(key); + + // Generate a random nonce + let mut nonce_bytes = [0u8; 12]; + rand::thread_rng().fill(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher.encrypt(nonce, message.as_bytes()) + .map_err(|e| PyErr::new::(format!("{:?}", e)))?; + + // Combine the KEM ciphertext, nonce, and the AES ciphertext + let mut combined = Vec::with_capacity(CRYPTO_CIPHERTEXTBYTES + nonce_bytes.len() + ciphertext.len()); + combined.extend_from_slice(&ct); + combined.extend_from_slice(&nonce_bytes); + combined.extend_from_slice(&ciphertext); + + Ok(PyBytes::new(py, &combined).into()) +} + + +#[pyfunction] +/// Decrypts a ciphertext using the private key. +/// +/// Args: +/// private_key (bytes): The private key as a byte sequence. +/// ciphertext (bytes): The ciphertext as a byte sequence, which includes the KEM ciphertext, nonce, and AES ciphertext. +/// +/// Returns: +/// bytes: The decrypted message. +fn decrypt(py: Python, private_key: &PyBytes, ciphertext: &PyBytes) -> PyResult> { + let private_key_bytes = private_key.as_bytes(); + if private_key_bytes.len() != CRYPTO_SECRETKEYBYTES { + return Err(PyErr::new::("Invalid private key length")); + } + + let ciphertext_bytes = ciphertext.as_bytes(); + if ciphertext_bytes.len() < CRYPTO_CIPHERTEXTBYTES + 12 { + return Err(PyErr::new::("Invalid ciphertext length")); + } + + let (kem_ct, rest) = ciphertext_bytes.split_at(CRYPTO_CIPHERTEXTBYTES); + let nonce = Nonce::from_slice(&rest[..12]); + let aes_ct = &rest[12..]; + + let mut ss = [0u8; CRYPTO_BYTES]; + let kem_ct_array: &[u8; CRYPTO_CIPHERTEXTBYTES] = kem_ct.try_into().map_err(|_| PyErr::new::("Invalid KEM ciphertext length"))?; + let private_key_array: &[u8; CRYPTO_SECRETKEYBYTES] = private_key_bytes.try_into().map_err(|_| PyErr::new::("Invalid private key length"))?; + + crypto_kem_dec(&mut ss, kem_ct_array, private_key_array) + .map_err(|e| PyErr::new::(format!("{:?}", e)))?; + + let key = Key::from_slice(&ss); + let cipher = Aes256Gcm::new(key); + + let plaintext = cipher.decrypt(nonce, aes_ct) + .map_err(|e| PyErr::new::(format!("{:?}", e)))?; + + Ok(PyBytes::new(py, &plaintext).into()) +} + + +#[pymodule] +/// A Python module that wraps the NTRU post-quantum encryption scheme using Rust. +fn py_ntru(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(generate_private_key, m)?)?; + m.add_function(wrap_pyfunction!(generate_public_key_based_on_private_key, m)?)?; + m.add_function(wrap_pyfunction!(encrypt, m)?)?; + m.add_function(wrap_pyfunction!(decrypt, m)?)?; + Ok(()) +}