diff --git a/Cargo.lock b/Cargo.lock index 154bd36..665644f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "bumpalo" version = "3.19.0" @@ -99,24 +105,39 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmov" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" + +[[package]] +name = "cpubits" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" + [[package]] name = "criterion" -version = "0.7.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", + "is-terminal", "itertools", "num-traits", + "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", + "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -124,9 +145,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", @@ -163,6 +184,28 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b6a7421484856c90cb2e996b91068d608539bb4e6f0a111b16d70678824d09" +dependencies = [ + "cpubits", + "ctutils", + "num-traits", + "rand_core 0.10.0", + "serdect", +] + +[[package]] +name = "ctutils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4" +dependencies = [ + "cmov", +] + [[package]] name = "either" version = "1.15.0" @@ -191,11 +234,28 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + [[package]] name = "itertools" -version = "0.13.0" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] @@ -294,7 +354,9 @@ name = "okuchi" version = "0.1.0" dependencies = [ "criterion", + "crypto-bigint", "num-bigint-dig", + "num-integer", "num-traits", "rand", "thiserror", @@ -376,7 +438,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -386,7 +448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -398,6 +460,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rayon" version = "1.11.0" @@ -511,6 +579,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serdect" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "smallvec" version = "1.15.1" diff --git a/Cargo.toml b/Cargo.toml index aedc8a4..744d099 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,35 @@ [package] name = "okuchi" version = "0.1.0" -edition = "2024" +edition = "2021" authors = ["Nelson Dominguez "] description = "Okamoto-Uchiyama Cryptosystem Implementation" license = "MIT OR Apache-2.0" readme = "README.md" - [dependencies] -num-bigint-dig = { version = "0.8", features = ["zeroize", "prime", "rand", "u64_digit"] } +num-bigint-dig = { version = "0.8", features = [ + "zeroize", + "prime", + "rand", + "u64_digit", +] } num-traits = "0.2" +num-integer = "0.1" rand = "0.8" -zeroize = { version = "1.6", features = ["derive"] } -thiserror = "2.0" +zeroize = { version = "1.8", features = ["derive"] } +thiserror = "2" +# Constant-time modular arithmetic for the decryption hot path (SECURITY-2). +# BoxedUint provides heap-allocated variable-width integers with CT guarantees. +crypto-bigint = { version = "0.7", features = ["alloc"] } [dev-dependencies] -criterion = "0.7" +criterion = "0.5" [[bench]] name = "okuchi" harness = false - [profile.release] opt-level = 3 lto = true diff --git a/README.md b/README.md index 41147d1..b20bd8d 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,99 @@ # Okuchi -A pure Rust implementation of the **Okamoto–Uchiyama** cryptosystem - a probabilistic public-key scheme whose security relies on the hardness of factoring and discrete logarithms modulo a composite number. +A pure-Rust implementation of the **Okamoto-Uchiyama (OU)** cryptosystem: a +probabilistic public-key encryption scheme with additive homomorphism, whose +security relies on the hardness of factoring `n = p²q` and computing discrete +logarithms modulo `p²`. **Okuchi** is a portmanteau of **Ok**amoto and **Uchi**yama. +--- + ## ⚠️ Important Notice -The project is evolving and should be treated as a **work in progress**. -Breaking changes, redesigns, or API removals may occur without notice. +This implementation is **experimental**, **incomplete**, and **not audited** by +any external security professionals. It may contain defects, conceptual +mistakes, side-channel vulnerabilities, insecure parameter choices, or other +issues that could compromise confidentiality, integrity, or availability of +data. + +The project is a **work in progress**. Breaking changes, API removals, or +redesigns may occur without notice. -This implementation is (still) **experimental**, **incomplete**, and **not audited** by any external security professionals. -It may contain defects, conceptual mistakes, side-channel vulnerabilities, insecure parameter choices, or other issues that could compromise confidentiality, integrity, or availability of data. +**Do not use Okuchi in production systems, high-risk environments, or anywhere +security or correctness is critical.** -**Do not use Okuchi in production systems, high-risk environments, or anywhere security or correctness is critical.** +If you choose to use this code despite these warnings, you do so entirely at +your own risk. No guarantees, explicit or implied, are made regarding +correctness, security, or fitness for any purpose. The author(s) assume no +liability for any damages or consequences resulting from use or misuse of this +software. -If you choose to use this code despite these warnings, **you do so entirely at your own risk**. No guarantees - explicit or implied - are made regarding performance, correctness, security, or fitness for any purpose. -The author(s) **assume no liability** for any damages, losses, or consequences resulting from the use, misuse, or inability to use this software. +--- ## Goals -- Provide a correct, readable and safe Rust implementation of the **OU** cryptosystem +- Provide a correct, readable, and safe Rust implementation of the OU + cryptosystem - Serve as a reference for learning and experimentation -- Maintain minimal dependencies and clear internal structure +- Maintain minimal dependencies and a clear internal structure + +This project does **not** aim to be a hardened or production-quality +cryptographic library. + +--- + +## Features + +- **Probabilistic encryption**: two encryptions of the same plaintext produce + distinct ciphertexts +- **Additive homomorphism**: `E(m1) * E(m2) mod n` decrypts to + `(m1 + m2) mod p`, without decrypting either operand +- **Stream API**: encrypt and decrypt arbitrary-length byte sequences via + automatic block splitting and reassembly +- **Validated plaintext type**: [`Plaintext`] enforces the OU plaintext bound + at construction and provides checked addition for safe homomorphic + accumulation +- **Zeroize on drop**: secret key material (`p`, `q`, derived constants) is + zeroed when the key is dropped -This project **does not** aim to be a hardened or production-quality cryptographic library. +--- + +## Security Notes + +- The minimum enforced key size is **512 bits** (testing only). Use **2048 + bits or larger** for any non-trivial use. +- The modular exponentiation `c^(p-1) mod p²` in decryption is routed through + `crypto-bigint` Montgomery form for constant-time guarantees. All other + big-integer operations (`num-bigint-dig`) are **variable-time** and may leak + information about secret values through timing. +- No formal audit has been performed. Do not deploy in adversarial environments. + +--- ## Usage -As mentioned above, until the library matures and receives proper review, usage should be limited to: +Intended use cases: -- academic experiments -- prototyping -- security research -- code reading and learning +- Academic experiments +- Prototyping +- Security research +- Code reading and learning **Production use is strongly discouraged.** -### Example +### Encrypt and Decrypt -```rs +```rust use okuchi::{KeyPair, Okuchi}; -let keypair = KeyPair::new(2048).expect("key generation failed"); -let pub_key = keypair.pub_key(); +let keypair = KeyPair::new(2048).expect("key generation failed"); +let pub_key = keypair.pub_key(); let priv_key = keypair.priv_key(); let message = "hello world 🌍"; -// Encrypt (stream API) +// Encrypt arbitrary-length data via the stream API let packed = Okuchi::encrypt_stream(pub_key, message).unwrap(); // Decrypt @@ -56,7 +102,3 @@ let decrypted = String::from_utf8(decrypted_bytes).unwrap(); assert_eq!(message, decrypted); ``` - -## References - -- Okamoto, T., Uchiyama, S. (1998). _A New Public-Key Cryptosystem as Secure as Factoring._ diff --git a/src/ciphertext.rs b/src/ciphertext.rs index aef8f3b..f7b054c 100644 --- a/src/ciphertext.rs +++ b/src/ciphertext.rs @@ -1,64 +1,47 @@ // Copyright 2025 Nelson Dominguez // SPDX-License-Identifier: MIT OR Apache-2.0 -use std::ops::{Deref, Mul}; - use num_bigint_dig::BigUint; +use crate::{Error, Result}; + +/// An Okamoto-Uchiyama ciphertext element in `ℤ_n`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Ciphertext { value: BigUint, } impl Ciphertext { - pub fn new(value: BigUint) -> Self { + /// Construct from a raw `BigUint`. + pub(crate) fn new(value: BigUint) -> Self { Self { value } } + /// Construct a [`Ciphertext`] from a big-endian byte slice. + /// + /// # Errors + /// + /// Returns [`Error::InvalidCiphertext`] if the decoded value is `>= n`. + /// + /// # Notes + /// + /// Leading zero bytes are accepted and ignored by `BigUint::from_bytes_be`. + /// An empty slice decodes to zero, which is a valid ciphertext value. + pub fn from_bytes(bytes: &[u8], n: &BigUint) -> Result { + let value = BigUint::from_bytes_be(bytes); + if &value >= n { + return Err(Error::InvalidCiphertext); + } + Ok(Self { value }) + } + + /// Returns a reference to the raw ciphertext integer. pub fn value(&self) -> &BigUint { &self.value } + /// Serialize the ciphertext to big-endian bytes. pub fn to_bytes(&self) -> Vec { self.value.to_bytes_be() } } - -impl Deref for Ciphertext { - type Target = BigUint; - - fn deref(&self) -> &Self::Target { - &self.value - } -} - -use std::convert::From; - -impl From for Ciphertext -where - T: AsRef<[u8]>, -{ - fn from(data: T) -> Self { - let bytes = data.as_ref(); - Self { - value: BigUint::from_bytes_be(bytes), - } - } -} - -// Homomorphic multiplication: E(m₁) · E(m₂) = E(m₁ + m₂) -impl Mul for &Ciphertext { - type Output = Ciphertext; - - fn mul(self, rhs: Self) -> Ciphertext { - Ciphertext::new(&self.value * &rhs.value) - } -} - -impl Mul for Ciphertext { - type Output = Ciphertext; - - fn mul(self, rhs: Self) -> Ciphertext { - Ciphertext::new(self.value * rhs.value) - } -} diff --git a/src/error.rs b/src/error.rs index 1d6137f..c3c9979 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,32 +1,41 @@ // Copyright 2025 Nelson Dominguez // SPDX-License-Identifier: MIT OR Apache-2.0 +/// Crate-wide result type alias. pub type Result = std::result::Result; -/// Errors that can occur during cryptographic operations. +/// Error variants produced by OU cryptographic operations #[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] pub enum Error { + /// The requested key size is below the minimum safe threshold. #[error("Invalid key size: must be at least {min} bits, got {actual}")] InvalidKeySize { min: usize, actual: usize }, + /// The ciphertext value is out of range or the packed byte format is malformed. #[error("Ciphertext is invalid or corrupted")] InvalidCiphertext, + /// The plaintext integer exceeds the conservative OU plaintext bound. #[error("Plaintext exceeds maximum allowed value")] PlaintextTooLarge, + /// Prime generation or key assembly failed. #[error("Key generation failed: {0}")] KeyGenerationFailed(String), + /// Reserved for arithmetic overflow conditions. #[error("Arithmetic overflow detected")] ArithmeticOverflow, + /// The supplied public key parameters are structurally invalid. #[error("Invalid public key")] InvalidPublicKey, + /// The supplied private key parameters are structurally invalid. #[error("Invalid private key")] InvalidPrivateKey, - #[error("decryption failed: {0}")] + /// A stream decryption or packed-format operation failed. + #[error("Decryption failed: {0}")] DecryptionFailed(String), } diff --git a/src/key.rs b/src/key.rs index 4c58b13..fd637f5 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,31 +1,40 @@ // Copyright 2025 Nelson Dominguez // SPDX-License-Identifier: MIT OR Apache-2.0 -use crate::{Error, Result}; - -use num_bigint_dig::{BigUint, RandBigInt}; +use num_bigint_dig::{BigUint, ModInverse, RandBigInt}; +use num_integer::Integer; use num_traits::{One, Zero}; use rand::rngs::OsRng; use zeroize::{Zeroize, ZeroizeOnDrop}; +use crate::util::l_function; +use crate::{Error, Result}; + +/// Okamoto-Uchiyama public key `(n, g, h)`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PublicKey { + // `n = p²q` where `p` and `q` are distinct large primes n: BigUint, + // `g ∈ ℤ_n*` with `g^(p-1) ≢ 1 (mod p²)` (OU generator condition) g: BigUint, + // `h = g^n mod n`, has order `p` in `ℤ_n*` h: BigUint, bit_length: usize, } impl PublicKey { + /// Construct a [`PublicKey`] from raw parameters. + /// + /// # Errors + /// + /// Returns [`Error::InvalidPublicKey`] if any parameter is zero or if `g >= n` or `h >= n`. pub fn new(n: BigUint, g: BigUint, h: BigUint, bit_length: usize) -> Result { if n.is_zero() || g.is_zero() || h.is_zero() { return Err(Error::InvalidPublicKey); } - if g >= n || h >= n { return Err(Error::InvalidPublicKey); } - Ok(Self { n, g, @@ -34,81 +43,143 @@ impl PublicKey { }) } + /// The modulus `n = p²q`. #[inline] pub fn n(&self) -> &BigUint { &self.n } + /// The OU generator `g`, satisfying `g^(p-1) ≢ 1 (mod p²)` and `gcd(g, n) = 1`. #[inline] pub fn g(&self) -> &BigUint { &self.g } + /// The randomization base `h = g^n mod n`, which has order `p` in `ℤ_n*`. + /// + /// Used as the blinding factor base during encryption: `h^r mod n`. #[inline] pub fn h(&self) -> &BigUint { &self.h } + /// The nominal bit length of `n` as supplied at construction. #[inline] pub fn bit_length(&self) -> usize { self.bit_length } + + /// Conservative upper bound on the plaintext bit length. + /// [`Error::PlaintextTooLarge`]. + #[inline] + pub fn plaintext_bound_bits(&self) -> usize { + (self.bit_length / 3).saturating_sub(1) + } } -/// Private key with automatic secure erasure. +/// Okamoto-Uchiyama private key. +/// +/// Holds the secret prime factorization of `n` and precomputed decryption +/// constants derived from it. +/// +/// # Security /// -/// The `Zeroize` and `ZeroizeOnDrop` traits ensure that p, q, and the -/// precomputed value are wiped from memory when this struct is dropped. -/// `num-bigint-dig` implements `Zeroize` for `BigUint`, which recursively -/// zeroes the underlying heap-allocated digit vectors. +/// All secret fields (`p`, `q`, `p_squared`, `l_gp_inv`) are zeroed on drop +/// via [`ZeroizeOnDrop`]. #[derive(PartialEq, Eq, Zeroize, ZeroizeOnDrop, Clone)] pub struct PrivateKey { #[zeroize(skip)] public_key: PublicKey, - /// Prime factor p where n = p²q - pub(crate) p: BigUint, + // Secret prime `p` where `n = p²q`. + p: BigUint, - /// Prime factor q - pub(crate) q: BigUint, + // Secret prime `q`. + q: BigUint, - /// Cached g^(p-1) mod p² for faster decryption - pub(crate) g_p_precomputed: BigUint, + /// Cached `p²`, used as the modulus for the decryption exponentiation. + p_squared: BigUint, + + // Precomputed `L(g^(p-1) mod p²)^(-1) mod p`. + // + // Eliminates a modular inverse from every decryption call. Treated as + // secret key material because it is derived from `g` and `p`. + l_gp_inv: BigUint, } impl PrivateKey { - pub fn new(public_key: PublicKey, p: BigUint, q: BigUint) -> Result { + /// Construct a [`PrivateKey`] and precompute decryption constants. + /// + /// # Errors + /// + /// Returns [`Error::InvalidPrivateKey`] if: + /// - `p` or `q` is zero + /// - `p²q != n` + /// - `L(g^(p-1) mod p²)` has no inverse mod `p`, which means `g` does not + /// satisfy the OU generator condition and decryption would be undefined + /// + /// # Notes + /// + /// The `modpow` used here (`num-bigint-dig`) is variable-time. This call + /// occurs only at key construction, not during decryption. + pub fn new(public_key: PublicKey, p: BigUint, q: BigUint) -> Result { if p.is_zero() || q.is_zero() { return Err(Error::InvalidPrivateKey); } - let p_squared = &p * &p; - let computed_n = &p_squared * &q; - - if computed_n != *public_key.n() { + if &p_squared * &q != *public_key.n() { return Err(Error::InvalidPrivateKey); } - // Precompute g^(p-1) mod p² once during key creation let p_minus_1 = &p - BigUint::one(); - let g_p_precomputed = public_key.g().modpow(&p_minus_1, &p_squared); + let g_p = public_key.g().modpow(&p_minus_1, &p_squared); + + // g^(p-1) ≢ 1 (mod p²) was enforced at generator selection; L(g_p) ≠ 0. + let l_gp = l_function(&g_p, &p); + let l_gp_inv = l_gp + .mod_inverse(&p) + .ok_or(Error::InvalidPrivateKey)? + .to_biguint() + .ok_or(Error::InvalidPrivateKey)?; Ok(Self { public_key, p, q, - g_p_precomputed, + p_squared, + l_gp_inv, }) } + /// Returns the public key associated with this private key. #[inline] pub fn pub_key(&self) -> &PublicKey { &self.public_key } + + /// Secret prime `p`. + #[inline] + pub(crate) fn p(&self) -> &BigUint { + &self.p + } + + /// Cached value of `p²`. + #[inline] + pub(crate) fn p_squared(&self) -> &BigUint { + &self.p_squared + } + + /// Precomputed `L(g^(p-1) mod p²)^(-1) mod p`. + #[inline] + pub(crate) fn l_gp_inv(&self) -> &BigUint { + &self.l_gp_inv + } } -const MIN_BIT_LENGTH: usize = 512; +/// Minimum key size enforced by [`KeyPair::new`]. +const MIN_BIT_LENGTH: usize = 2048; +/// A matched Okamoto-Uchiyama key pair `(PublicKey, PrivateKey)`. #[derive(PartialEq, Eq, Zeroize, ZeroizeOnDrop)] pub struct KeyPair { #[zeroize(skip)] @@ -117,25 +188,24 @@ pub struct KeyPair { } impl KeyPair { - /// Generates a new Okamoto-Uchiyama keypair. - /// - /// ## Security Parameters - /// - /// - `bit_length`: Total security parameter (≥ 512 bits, recommend 2048+) + const BITS_2048: usize = 2048; + const BITS_3072: usize = 3072; + const BITS_4096: usize = 4096; + + /// Generate a fresh Okamoto-Uchiyama key pair. /// - /// The primes p and q are chosen such that |n| ≈ bit_length: - /// - |p| ≈ bit_length / 3 - /// - |q| ≈ bit_length - 2|p| + /// # Errors /// - /// Both p and q are safe primes (p = 2p' + 1 where p' is prime) to - /// maximize factorization difficulty. + /// Returns [`Error::InvalidKeySize`] if `bit_length < 2048`. /// - /// ## Generator Selection + /// Returns [`Error::KeyGenerationFailed`] if: + /// - Safe prime generation exceeds 2048 retries for either prime + /// - The generated primes `p` and `q` are equal (negligible probability) /// - /// The generator g is chosen uniformly from [2, n-1] subject to: - /// g^(p-1) ≢ 1 (mod p²) + /// # Notes /// - /// This ensures the discrete log problem in the p-subgroup is hard. + /// Generator selection uses rejection sampling over `ℤ_n*` and terminates + /// in expected constant iterations. Entropy is sourced from [`OsRng`]. pub fn new(bit_length: usize) -> Result { if bit_length < MIN_BIT_LENGTH { return Err(Error::InvalidKeySize { @@ -145,16 +215,13 @@ impl KeyPair { } let mut rng = OsRng; - - // Bit distribution: n = p²q implies |n| = 2|p| + |q| let p_bits = bit_length / 3; let q_bits = bit_length - (2 * p_bits); - let p = crate::util::generate_safe_prime(p_bits); - let q = crate::util::generate_safe_prime(q_bits); + let p = crate::util::generate_prime(p_bits)?; + let q = crate::util::generate_prime(q_bits)?; if p == q { - // astronomically unlikely, but we must guarantee distinct p,q primes return Err(Error::KeyGenerationFailed("Primes must be distinct".into())); } @@ -162,18 +229,20 @@ impl KeyPair { let n = &p_squared * &q; let p_minus_1 = &p - BigUint::one(); - // find a valid generator let g = loop { let candidate = rng.gen_biguint_range(&BigUint::from(2u32), &n); - // Core requirement: g^(p-1) ≢ 1 (mod p²) - let check = candidate.modpow(&p_minus_1, &p_squared); - if check != BigUint::one() { + // core OU generator requirement: + // g^(p-1) ≢ 1 (mod p²) + // gcd(g, n) = 1, where g in ℤ_n* + if candidate.modpow(&p_minus_1, &p_squared) != BigUint::one() + && candidate.gcd(&n).is_one() + { break candidate; } }; - // compute h = g^n mod n + // h = g^n mod n let h = g.modpow(&n, &n); let public_key = PublicKey::new(n, g, h, bit_length)?; @@ -185,12 +254,28 @@ impl KeyPair { }) } + /// Generate a 2048 bits sized Okamoto-Uchiyama key pair. + pub fn new_2048() -> Result { + Self::new(Self::BITS_2048) + } + + /// Generate a 3072 bits sized Okamoto-Uchiyama key pair. + pub fn new_3072() -> Result { + Self::new(Self::BITS_3072) + } + + /// Generate a 4096 bits sized Okamoto-Uchiyama key pair. + pub fn new_4096() -> Result { + Self::new(Self::BITS_4096) + } + + /// Returns a reference to the public key. #[inline] pub fn pub_key(&self) -> &PublicKey { &self.pub_key } - #[allow(unused)] + /// Returns a reference to the private key. #[inline] pub fn priv_key(&self) -> &PrivateKey { &self.priv_key diff --git a/src/lib.rs b/src/lib.rs index 60bb2ac..02c89f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,37 +3,72 @@ //! # Okamoto-Uchiyama Cryptosystem //! -//! Probabilistic encryption scheme with additive homomorphism, based on the -//! hardness of factoring n = p²q and computing discrete logs mod p². +//! A pure Rust implementation of the Okamoto-Uchiyama (OU) probabilistic +//! public-key encryption scheme with additive homomorphism. //! -//! Reference: [Okamoto & Uchiyama (1998), EUROCRYPT](https://link.springer.com/chapter/10.1007/BFb0054135) +//! Security relies on the hardness of factoring `n = p²q` and computing +//! discrete logarithms modulo `p²`. //! -//! ## Security +//! Reference: +//! Okamoto & Uchiyama, "A New Public-Key Cryptosystem as Secure as Factoring", EUROCRYPT 1998. //! -//! The scheme is secure under the p-subgroup assumption. The private key -//! (p, q) is automatically zeroized on drop via the `zeroize` crate. +//! ## Quick Start //! -//! ## Example +//! ```rust +//! use okuchi::{KeyPair, Okuchi}; +//! +//! let keypair = KeyPair::new_2048().unwrap(); +//! +//! let ciphertext = Okuchi::encrypt(keypair.pub_key(), b"hello").unwrap(); +//! let plaintext = Okuchi::decrypt(keypair.priv_key(), &ciphertext).unwrap(); +//! +//! assert_eq!(plaintext, b"hello"); +//! ``` +//! +//! ## Homomorphic Addition +//! +//! Multiplying two ciphertexts mod `n` decrypts to the sum of their plaintexts +//! mod `p`: //! -//! ```rust,no_run +//! ```rust //! use okuchi::{KeyPair, Okuchi}; -//! use num_bigint_dig::BigUint; //! -//! let keypair = KeyPair::new(2048).expect("key generation failed"); -//! let message = "hello world"; +//! let keypair = KeyPair::new_2048().unwrap(); +//! let pub_key = keypair.pub_key(); //! -//! let ciphertext = Okuchi::encrypt(keypair.pub_key(), &message).expect("encryption failed"); -//! let decrypted = Okuchi::decrypt(keypair.priv_key(), &ciphertext).expect("decryption failed"); -//! assert_eq!(message.as_bytes(), decrypted); +//! let c1 = Okuchi::encrypt(pub_key, 10u32.to_be_bytes()).unwrap(); +//! let c2 = Okuchi::encrypt(pub_key, 20u32.to_be_bytes()).unwrap(); +//! let sum = Okuchi::homomorphic_add(pub_key, &c1, &c2); +//! // Decrypts to 30 //! ``` +//! +//! ## Security Notice +//! +//! Decryption uses `num-bigint-dig` for `modpow`, `mod_inverse`, and integer +//! division over secret values. These operations are **not** constant-time. +//! Timing side-channels are possible in adversarial environments. +//! +//! The sensitive modular exponentiation in the decrypt path (`c^(p-1) mod p²`) +//! is routed through `crypto-bigint` Montgomery form for constant-time +//! guarantees. All other big-integer operations remain variable-time. +//! +//! Do not use this crate in contexts where timing oracles are a threat model +//! concern without a full audit of the remaining variable-time paths. +//! +//! ## Key Size +//! +//! The minimum enforced key size is 2048 bits (`KeyPair::new` rejects smaller +//! values). For production use, at least 2048 bits or larger is recommended. mod ciphertext; mod error; mod key; mod okuchi; +mod plaintext; mod util; -pub use ciphertext::*; -pub use error::*; -pub use key::*; -pub use okuchi::*; +pub use ciphertext::Ciphertext; +pub use error::{Error, Result}; +pub use key::{KeyPair, PrivateKey, PublicKey}; +pub use okuchi::Okuchi; +pub use plaintext::Plaintext; diff --git a/src/okuchi.rs b/src/okuchi.rs index 753f707..4e22b02 100644 --- a/src/okuchi.rs +++ b/src/okuchi.rs @@ -1,37 +1,68 @@ // Copyright 2025 Nelson Dominguez // SPDX-License-Identifier: MIT OR Apache-2.0 -use num_bigint_dig::{BigUint, ModInverse, RandBigInt}; -use num_traits::One; -use num_traits::Zero; +use num_bigint_dig::{BigUint, RandBigInt}; +use num_integer::Integer; +use num_traits::{One, Zero}; use rand::rngs::OsRng; use crate::ciphertext::Ciphertext; use crate::error::{Error, Result}; use crate::key::{PrivateKey, PublicKey}; +/// Entry point for all Okamoto-Uchiyama cryptographic operations. +/// +/// All methods are stateless. The key material required for each operation +/// is passed explicitly on every call. pub struct Okuchi; impl Okuchi { - /// Encrypts a plaintext under the Okamoto-Uchiyama scheme. + /// Encrypt a plaintext byte slice under the Okamoto-Uchiyama scheme. /// - /// ## Plaintext Space + /// Computes `c = g^m * h^r mod n` where `m` is the big-endian integer + /// encoded by `plaintext` and `r` is a fresh random value in `[1, n)` + /// with `gcd(r, n) = 1`. /// - /// Technically m ∈ ℤ_p, but we enforce m < n for implementation simplicity. - /// The effective message space is therefore min(p, n), which is p in practice. + /// Encryption is probabilistic: two calls with identical inputs produce + /// distinct ciphertexts with overwhelming probability. + /// + /// # Requirements + /// + /// - `plaintext`, interpreted as a big-endian integer, must satisfy + /// `m.bits() <= pub_key.plaintext_bound_bits()` + /// - The public key must have been produced by [`KeyPair::new`](crate::KeyPair::new) + /// or satisfy the OU parameter invariants documented on [`PublicKey`] + /// + /// # Errors + /// + /// Returns [`Error::PlaintextTooLarge`] if the plaintext exceeds the + /// conservative bound `2^(n_bits/3 - 1) - 1`. + /// + /// # Security + /// + /// `r` is sampled from [`OsRng`] and rejection-sampled until + /// `gcd(r, n) = 1`. pub fn encrypt>(pub_key: &PublicKey, plaintext: P) -> Result { let n = pub_key.n(); + let plaintext = BigUint::from_bytes_be(plaintext.as_ref()); - let bytes = plaintext.as_ref(); - let plaintext = BigUint::from_bytes_be(bytes); - if &plaintext >= n { + // Enforce m < p. p is secret; use conservative bound: p ≥ 2^(p_bits-1). + let max_bits = pub_key.plaintext_bound_bits(); + if plaintext.bits() > max_bits { return Err(Error::PlaintextTooLarge); } let mut rng = OsRng; - let r = rng.gen_biguint_range(&BigUint::one(), n); - // c = g^m · h^r mod n + // OU requires gcd(r, n) = 1. With n = p²q the failure probability is + // ≈ 1/p + 1/q. This is negligible but the protocol is undefined otherwise. + let r = loop { + let candidate = rng.gen_biguint_range(&BigUint::one(), n); + if candidate.gcd(n).is_one() { + break candidate; + } + }; + let gm = pub_key.g().modpow(&plaintext, n); let hr = pub_key.h().modpow(&r, n); let c = (gm * hr) % n; @@ -39,211 +70,292 @@ impl Okuchi { Ok(Ciphertext::new(c)) } - /// Encrypt an arbitrary-length byte sequence. Returns a packed byte vector - /// containing version, block count and each ciphertext's length-prefixed bytes. + /// Homomorphically add two ciphertexts. /// - /// Packed format: - /// [u8 version=1][u32 BE block_count] - /// for each block: - /// [u32 BE len][len bytes ciphertext] + /// # Security + /// + /// Addition wraps silently mod `p` with no error or indication. If the + /// sum of the underlying plaintexts meets or exceeds `p`, the decrypted + /// result is incorrect. Use [`Plaintext::checked_add`](crate::Plaintext::checked_add) + /// before encryption to detect overflow against the conservative public bound. + pub fn homomorphic_add(pub_key: &PublicKey, a: &Ciphertext, b: &Ciphertext) -> Ciphertext { + // Computes `E(m1) * E(m2) mod n`, which decrypts to `(m1 + m2) mod p`. + Ciphertext::new((a.value() * b.value()) % pub_key.n()) + } + + /// Encrypt an arbitrary-length byte sequence as a packed multi-block stream. + /// + /// The output is a self-describing binary format suitable for [`decrypt_stream`](Okuchi::decrypt_stream). + /// + /// # Packed Format + /// + /// ```text + /// [u32 BE block_count][u32 BE original_data_len] + /// for each block: [u32 BE ciphertext_len][ciphertext bytes] + /// ``` + /// + /// `original_data_len` is stored so `decrypt_stream` can restore leading-zero + /// bytes that `BigUint::to_bytes_be` drops. A value of `0` is a sentinel + /// meaning "lengths unknown; skip per-block padding" and is written only by + /// [`homomorphic_add_packed`](Okuchi::homomorphic_add_packed). + /// + /// # Errors + /// + /// Propagates [`Error::PlaintextTooLarge`] from [`encrypt`](Okuchi::encrypt) + /// if a block exceeds the plaintext bound, which should not occur under + /// correct block-size computation. pub fn encrypt_stream>(pub_key: &PublicKey, data: P) -> Result> { let bytes = data.as_ref(); + let original_len = bytes.len() as u32; let max_block = Self::max_plaintext_bytes(pub_key); - let mut blocks: Vec> = Vec::new(); + let block_count = if bytes.is_empty() { + 0usize + } else { + bytes.len().div_ceil(max_block) + }; + let est_ct_bytes = pub_key.n().bits() / 8 + 1; + let mut out = Vec::with_capacity(8 + block_count * (4 + est_ct_bytes)); + + out.extend_from_slice(&(block_count as u32).to_be_bytes()); + out.extend_from_slice(&original_len.to_be_bytes()); + let mut i = 0usize; while i < bytes.len() { let end = std::cmp::min(i + max_block, bytes.len()); - let block = &bytes[i..end]; - let c = Self::encrypt(pub_key, block)?; - blocks.push(c.to_bytes()); + let c = Self::encrypt(pub_key, &bytes[i..end])?; + let c_bytes = c.to_bytes(); + out.extend_from_slice(&(c_bytes.len() as u32).to_be_bytes()); + out.extend_from_slice(&c_bytes); i = end; } - // If data is empty, encrypt a single zero-block so decrypt can distinguish empty vs missing. - if bytes.is_empty() { - let c = Self::encrypt(pub_key, [])?; - blocks.push(c.to_bytes()); - } - - // serialize - let mut out = Vec::new(); - out.push(1u8); // version - let cnt = blocks.len() as u32; - out.extend_from_slice(&cnt.to_be_bytes()); - for b in blocks { - let len = b.len() as u32; - out.extend_from_slice(&len.to_be_bytes()); - out.extend_from_slice(&b); - } - Ok(out) } - /// Decrypt a single Ciphertext to raw bytes. + /// Encrypt a pre-validated [`Plaintext`](crate::Plaintext). + /// + /// Skips the bit-length check performed by [`encrypt`](Okuchi::encrypt) + /// because [`Plaintext`](crate::Plaintext) construction already enforces the bound. + /// + /// # Errors + /// + /// Propagates any error from the underlying [`encrypt`](Okuchi::encrypt) call. + pub fn encrypt_plaintext( + pub_key: &PublicKey, + pt: &crate::plaintext::Plaintext, + ) -> Result { + Self::encrypt(pub_key, pt.to_bytes_be()) + } + + /// Decrypt a single [`Ciphertext`] to raw big-endian bytes. + /// + /// # Errors + /// + /// Returns [`Error::InvalidCiphertext`] if `ciphertext.value() >= n`. + /// + /// # Security + /// + /// The modular exponentiation `c^(p-1) mod p²` is performed via using + /// `crypto-bigint` Montgomery form, providing constant-time guarantees + /// for that step. The subsequent `L` function computation and modular + /// multiplication use `num-bigint-dig` and are variable-time. pub fn decrypt(priv_key: &PrivateKey, ciphertext: &Ciphertext) -> Result> { - let p = &priv_key.p; - let p_squared = p * p; + let p = priv_key.p(); + let p_squared = priv_key.p_squared(); let c = ciphertext.value(); if c >= priv_key.pub_key().n() { return Err(Error::InvalidCiphertext); } + // c^(p-1) mod p²: constant-time via crypto-bigint let p_minus_1 = p - BigUint::one(); - let cp = c.modpow(&p_minus_1, &p_squared); + let cp = crate::util::ct_modpow(c, &p_minus_1, p_squared); let l_cp = crate::util::l_function(&cp, p); - let l_gp = crate::util::l_function(&priv_key.g_p_precomputed, p); - let l_gp_inv = l_gp - .mod_inverse(p) - .ok_or(Error::DecryptionFailed("Modular inverse failed".into()))? - .to_biguint() - .ok_or(Error::DecryptionFailed("Inverse negative".into()))?; + // m = L(c^(p-1) mod p²) * L(g^(p-1) mod p²)^(-1) mod p, where L(x) = (x - 1) / p + // l_gp_inv precomputed in PrivateKey::new + let m = (l_cp * priv_key.l_gp_inv()) % p; - let m = (l_cp * l_gp_inv) % p; - - // special-case zero -> empty vec (so empty plaintext roundtrips correctly) let bytes = if m.is_zero() { Vec::new() } else { m.to_bytes_be() }; - Ok(bytes) } - /// Decrypt a packed stream produced by `encrypt_stream`. + /// Decrypt a packed stream produced by [`encrypt_stream`](Okuchi::encrypt_stream). + /// + /// If `original_data_len == 0` (the sentinel written by + /// [`homomorphic_add_packed`](Okuchi::homomorphic_add_packed)), per-block + /// padding is skipped and raw decrypted bytes are concatenated directly. + /// + /// # Errors + /// + /// Returns [`Error::DecryptionFailed`] if: + /// - The packed buffer is shorter than 8 bytes + /// - `block_count` exceeds the 4M-block limit + /// - Any block length field extends beyond the buffer boundary /// - /// Returns reassembled plaintext bytes. + /// Returns [`Error::InvalidCiphertext`] if any deserialized ciphertext value is `>= n`. pub fn decrypt_stream>(priv_key: &PrivateKey, packed: B) -> Result> { let bytes = packed.as_ref(); - // parse header - if bytes.len() < 5 { + // Header: block_count(4) + original_data_len(4) = 8 bytes + if bytes.len() < 8 { return Err(Error::DecryptionFailed("Packed data too short".into())); } - let version = bytes[0]; - if version != 1u8 { - return Err(Error::DecryptionFailed("Unsupported packed version".into())); + + let block_count = u32::from_be_bytes(bytes[0..4].try_into().unwrap()) as usize; + let original_data_len = u32::from_be_bytes(bytes[4..8].try_into().unwrap()) as usize; + + const MAX_BLOCKS: usize = 1 << 22; // 4M blocks + if block_count > MAX_BLOCKS { + return Err(Error::DecryptionFailed(format!( + "block count {block_count} exceeds limit {MAX_BLOCKS}" + ))); } - let mut offset = 1usize; - let block_count = { - let mut buf = [0u8; 4]; - buf.copy_from_slice(&bytes[offset..offset + 4]); - offset += 4; - u32::from_be_bytes(buf) as usize + + if block_count == 0 { + return Ok(Vec::new()); + } + + let max_block = Self::max_plaintext_bytes(priv_key.pub_key()); + let do_padding = original_data_len > 0; + let last_block_len = if do_padding { + original_data_len.saturating_sub((block_count - 1) * max_block) + } else { + 0 }; - let mut result: Vec = Vec::new(); + let mut offset = 8usize; + let mut result = Vec::with_capacity(original_data_len.max(block_count)); - for _ in 0..block_count { + for idx in 0..block_count { if offset + 4 > bytes.len() { return Err(Error::DecryptionFailed("Truncated packed data".into())); } - let mut len_buf = [0u8; 4]; - len_buf.copy_from_slice(&bytes[offset..offset + 4]); + let c_len = u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize; offset += 4; - let len = u32::from_be_bytes(len_buf) as usize; - if offset + len > bytes.len() { + if offset + c_len > bytes.len() { return Err(Error::DecryptionFailed("Truncated ciphertext block".into())); } - let c_bytes = &bytes[offset..offset + len]; - offset += len; + let c = Ciphertext::from_bytes(&bytes[offset..offset + c_len], priv_key.pub_key().n())?; + offset += c_len; + + let plain = Self::decrypt(priv_key, &c)?; + + if do_padding { + // Restore leading zeros stripped by BigUint::to_bytes_be + let expected = if idx == block_count - 1 { + last_block_len + } else { + max_block + }; + let pad = expected.saturating_sub(plain.len()); + result.extend(std::iter::repeat_n(0u8, pad)); + } - // reconstruct Ciphertext and decrypt block - let c = Ciphertext::from(c_bytes); - let block_plain = Self::decrypt(priv_key, &c)?; - result.extend_from_slice(&block_plain); + result.extend_from_slice(&plain); } Ok(result) } - /// Homomorphically add two packed ciphertext streams (produced by `encrypt_stream`). + /// Homomorphically add two packed streams block-by-block. + /// + /// # Errors + /// + /// Returns [`Error::InvalidCiphertext`] if either buffer is shorter than 8 bytes. /// - /// Both packed inputs must use the same block count and version. Returns a new - /// packed stream with each block = (c1_block * c2_block) mod n. - #[allow(unused)] - fn homomorphic_add_packed( + /// Returns [`Error::DecryptionFailed`] if the two streams have different `block_count` values. + /// + /// # Security + /// + /// Addition wraps silently mod `p` per block. + /// See [`homomorphic_add`](Okuchi::homomorphic_add) for the overflow caveat. + pub fn homomorphic_add_packed( pub_key: &PublicKey, packed_a: &[u8], packed_b: &[u8], ) -> Result> { - // Parse both headers quickly (reuse format from encrypt_stream) - if packed_a.len() < 5 || packed_b.len() < 5 { - return Err(Error::DecryptionFailed("Packed input too short".into())); - } - if packed_a[0] != 1 || packed_b[0] != 1 { - return Err(Error::DecryptionFailed("Unsupported packed version".into())); + if packed_a.len() < 8 || packed_b.len() < 8 { + return Err(Error::InvalidCiphertext); } - let a_cnt = u32::from_be_bytes(packed_a[1..5].try_into().unwrap()) as usize; - let b_cnt = u32::from_be_bytes(packed_b[1..5].try_into().unwrap()) as usize; + // Both streams must have equal `block_count` values. Each pair of + // corresponding blocks is combined as `E(m1_i) * E(m2_i) mod n`, + // which decrypts to `(m1_i + m2_i) mod p`. + let a_cnt = u32::from_be_bytes(packed_a[0..4].try_into().unwrap()) as usize; + let b_cnt = u32::from_be_bytes(packed_b[0..4].try_into().unwrap()) as usize; if a_cnt != b_cnt { return Err(Error::DecryptionFailed("Block count mismatch".into())); } - // helper to iterate blocks - fn iter_blocks(packed: &[u8]) -> Result>> { - let mut out = Vec::new(); - let mut off = 1usize + 4usize; - let cnt = u32::from_be_bytes(packed[1..5].try_into().unwrap()) as usize; - for _ in 0..cnt { - if off + 4 > packed.len() { - return Err(Error::DecryptionFailed("Truncated packed data".into())); - } - let len = u32::from_be_bytes(packed[off..off + 4].try_into().unwrap()) as usize; - off += 4; - if off + len > packed.len() { - return Err(Error::DecryptionFailed("Truncated packed data".into())); - } - out.push(packed[off..off + len].to_vec()); - off += len; - } - Ok(out) - } - - let blocks_a = iter_blocks(packed_a)?; - let blocks_b = iter_blocks(packed_b)?; - - let mut out_blocks: Vec> = Vec::with_capacity(a_cnt); + let blocks_a = Self::parse_blocks(packed_a)?; + let blocks_b = Self::parse_blocks(packed_b)?; let n = pub_key.n(); - for (a_bytes, b_bytes) in blocks_a.into_iter().zip(blocks_b.into_iter()) { - let a_c = Ciphertext::from(&a_bytes); - let b_c = Ciphertext::from(&b_bytes); - // multiply ciphertexts (homomorphic addition) - let prod = Ciphertext::new((&a_c * &b_c).value().clone() % n); - out_blocks.push(prod.to_bytes()); + let est_ct_bytes = n.bits() / 8 + 1; + let mut out = Vec::with_capacity(8 + a_cnt * (4 + est_ct_bytes)); + out.extend_from_slice(&(a_cnt as u32).to_be_bytes()); + out.extend_from_slice(&0u32.to_be_bytes()); // original_data_len = 0 sentinel + + for (a_bytes, b_bytes) in blocks_a.into_iter().zip(blocks_b) { + let a_c = Ciphertext::from_bytes(&a_bytes, n)?; + let b_c = Ciphertext::from_bytes(&b_bytes, n)?; + // E(m1) * E(m2) mod n = E(m1 + m2 mod p) + let prod = Ciphertext::new((a_c.value() * b_c.value()) % n); + let prod_bytes = prod.to_bytes(); + out.extend_from_slice(&(prod_bytes.len() as u32).to_be_bytes()); + out.extend_from_slice(&prod_bytes); } - // serialize packed format - let mut out = Vec::new(); - out.push(1u8); - let cnt_u32 = out_blocks.len() as u32; - out.extend_from_slice(&cnt_u32.to_be_bytes()); - for b in out_blocks { - out.extend_from_slice(&(b.len() as u32).to_be_bytes()); - out.extend_from_slice(&b); - } + Ok(out) + } + /// Extract raw ciphertext byte blocks from a packed stream. + /// + /// Reads `block_count` from the 4-byte header and parses each block's + /// length-prefixed ciphertext bytes. The 8-byte header (block count + + /// original data length) is skipped entirely. + /// + /// # Errors + /// + /// Returns [`Error::DecryptionFailed`] if any length field extends beyond + /// the buffer boundary. + fn parse_blocks(packed: &[u8]) -> Result>> { + let cnt = u32::from_be_bytes(packed[0..4].try_into().unwrap()) as usize; + let mut out = Vec::with_capacity(cnt); + let mut off = 8usize; // skip 8-byte header + for _ in 0..cnt { + if off + 4 > packed.len() { + return Err(Error::DecryptionFailed("Truncated packed data".into())); + } + let len = u32::from_be_bytes(packed[off..off + 4].try_into().unwrap()) as usize; + off += 4; + if off + len > packed.len() { + return Err(Error::DecryptionFailed("Truncated ciphertext block".into())); + } + out.push(packed[off..off + len].to_vec()); + off += len; + } Ok(out) } - /// Conservative estimate of maximum plaintext bytes per block. + /// Compute the maximum plaintext block size in bytes for a given public key. /// - /// OU's plaintext space is modulo `p`, but public key exposes `n = p^2 q`. - /// If p,q are chosen similar size then bits(p) ≈ bits(n)/3. We use that - /// conservative estimate to compute a safe byte-length per block. + /// Derived as `floor((n_bits/3 - 1) / 8)`, clamped to a minimum of 1. + /// This is the byte-level counterpart of [`PublicKey::plaintext_bound_bits`] and determines + /// the block boundaries used by [`encrypt_stream`](Okuchi::encrypt_stream) and + /// [`decrypt_stream`](Okuchi::decrypt_stream). fn max_plaintext_bytes(pub_key: &PublicKey) -> usize { let n_bits = pub_key.n().bits(); - // conservative estimate of p bits let p_bits = n_bits / 3; - // ensure at least 1 byte available let bytes = p_bits.saturating_sub(1) / 8; - // require at least 1 byte std::cmp::max(1, bytes) } } @@ -252,227 +364,153 @@ impl Okuchi { mod tests { use super::*; use crate::key::KeyPair; - use num_traits::Zero; + const TEST_KEY_SIZE: usize = 2048; + #[test] - fn encrypt_decrypt_utf8_emoji() { - let keypair = KeyPair::new(512).unwrap(); - let pub_key = keypair.pub_key(); - let priv_key = keypair.priv_key(); + fn encrypt_decrypt_roundtrip() { + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); + let message = "hello world!"; + let ciphertext = Okuchi::encrypt(keypair.pub_key(), message).unwrap(); + let decrypted = Okuchi::decrypt(keypair.priv_key(), &ciphertext).unwrap(); + assert_eq!(message.as_bytes(), decrypted); + } + #[test] + fn encrypt_decrypt_utf8_emoji() { + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); let msg = "Testing OU encryption"; - - // Use packed stream API for arbitrary UTF-8 - let packed = Okuchi::encrypt_stream(pub_key, msg).unwrap(); - let decrypted_bytes = Okuchi::decrypt_stream(priv_key, &packed).unwrap(); - - let decrypted = String::from_utf8(decrypted_bytes).expect("decrypted not valid UTF-8"); - assert_eq!(decrypted, msg); + let packed = Okuchi::encrypt_stream(keypair.pub_key(), msg).unwrap(); + let decrypted_bytes = Okuchi::decrypt_stream(keypair.priv_key(), &packed).unwrap(); + assert_eq!(String::from_utf8(decrypted_bytes).unwrap(), msg); } #[test] fn encrypt_decrypt_long_text() { - let keypair = KeyPair::new(512).unwrap(); - let pub_key = keypair.pub_key(); - let priv_key = keypair.priv_key(); - - // make a long message that will definitely exceed a single block + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); let msg = "Rust cryptography long text test".repeat(50); + let packed = Okuchi::encrypt_stream(keypair.pub_key(), &msg).unwrap(); + let decrypted_bytes = Okuchi::decrypt_stream(keypair.priv_key(), &packed).unwrap(); + assert_eq!(String::from_utf8(decrypted_bytes).unwrap(), msg); + } - let packed = Okuchi::encrypt_stream(pub_key, &msg).unwrap(); - let decrypted_bytes = Okuchi::decrypt_stream(priv_key, &packed).unwrap(); + #[test] + fn encrypt_decrypt_empty_string() { + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); + let packed = Okuchi::encrypt_stream(keypair.pub_key(), b"").unwrap(); + let decrypted = Okuchi::decrypt_stream(keypair.priv_key(), &packed).unwrap(); + assert!(decrypted.is_empty()); + } - let decrypted = String::from_utf8(decrypted_bytes).expect("decrypted not valid UTF-8"); - assert_eq!(decrypted, msg); + #[test] + fn leading_zero_bytes_preserved() { + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); + let max_block = { + let n_bits = keypair.pub_key().n().bits(); + std::cmp::max(1, ((n_bits / 3).saturating_sub(1)) / 8) + }; + let mut data = vec![0u8; max_block]; + data[max_block - 1] = 0xFF; + let packed = Okuchi::encrypt_stream(keypair.pub_key(), &data).unwrap(); + let decrypted = Okuchi::decrypt_stream(keypair.priv_key(), &packed).unwrap(); + assert_eq!(decrypted, data); } #[test] fn homomorphic_addition() { - let keypair = KeyPair::new(512).unwrap(); + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); let pub_key = keypair.pub_key(); let priv_key = keypair.priv_key(); - // small integers that fit in one block - // test homomorphic addition via packed streams let m1 = BigUint::from(50u32); let m2 = BigUint::from(25u32); let packed1 = Okuchi::encrypt_stream(pub_key, m1.to_bytes_be()).unwrap(); let packed2 = Okuchi::encrypt_stream(pub_key, m2.to_bytes_be()).unwrap(); - - // homomorphically add the two packed streams (block by block) let packed_sum = Okuchi::homomorphic_add_packed(pub_key, &packed1, &packed2).unwrap(); - - let decrypted_sum_bytes = Okuchi::decrypt_stream(priv_key, &packed_sum).unwrap(); - let decrypted_sum_bn = BigUint::from_bytes_be(&decrypted_sum_bytes); - - let expected = &m1 + &m2; - assert_eq!(decrypted_sum_bn, expected); + let decrypted_bytes = Okuchi::decrypt_stream(priv_key, &packed_sum).unwrap(); + assert_eq!(BigUint::from_bytes_be(&decrypted_bytes), &m1 + &m2); } #[test] - fn keygen_consistency() { - let keypair = KeyPair::new(512).unwrap(); + fn homomorphic_add_direct() { + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); let pub_key = keypair.pub_key(); let priv_key = keypair.priv_key(); - assert_eq!(priv_key.pub_key(), pub_key); - assert!(!pub_key.n().is_zero()); - assert!(!pub_key.g().is_zero()); - assert!(!pub_key.h().is_zero()); - - // n should be roughly the right size - assert!(pub_key.n().bits() >= 504); - } - - #[test] - fn encrypt_decrypt_roundtrip() { - let keypair = KeyPair::new(512).unwrap(); - let message = "hello world!"; - - let ciphertext = Okuchi::encrypt(keypair.pub_key(), message).unwrap(); - let decrypted = Okuchi::decrypt(keypair.priv_key(), &ciphertext).unwrap(); - - assert_eq!(message.as_bytes(), decrypted); - } - - #[test] - fn encrypt_decrypt_utf8_ascii() { - let keypair = KeyPair::new(512).unwrap(); - let msg = "Hello from Japan"; - - let ciphertext = Okuchi::encrypt(keypair.pub_key(), msg).unwrap(); - let decrypted_bytes = Okuchi::decrypt(keypair.priv_key(), &ciphertext).unwrap(); - - let decrypted = String::from_utf8(decrypted_bytes).unwrap(); - assert_eq!(decrypted, msg); - } - - #[test] - fn encrypt_decrypt_utf8_japanese() { - let keypair = KeyPair::new(512).unwrap(); - let msg = "こんにちは世界"; // Japanese UTF-8 - - let ciphertext = Okuchi::encrypt(keypair.pub_key(), msg).unwrap(); - let decrypted_bytes = Okuchi::decrypt(keypair.priv_key(), &ciphertext).unwrap(); - - let decrypted = String::from_utf8(decrypted_bytes).unwrap(); - assert_eq!(decrypted, msg); - } + let m1 = BigUint::from(10u32); + let m2 = BigUint::from(20u32); + let m3 = BigUint::from(30u32); - #[test] - fn encrypt_decrypt_empty_string() { - let keypair = KeyPair::new(512).unwrap(); - let msg = ""; + let c1 = Okuchi::encrypt(pub_key, m1.to_bytes_be()).unwrap(); + let c2 = Okuchi::encrypt(pub_key, m2.to_bytes_be()).unwrap(); + let c3 = Okuchi::encrypt(pub_key, m3.to_bytes_be()).unwrap(); - let ciphertext = Okuchi::encrypt(keypair.pub_key(), msg).unwrap(); - let decrypted_bytes = Okuchi::decrypt(keypair.priv_key(), &ciphertext).unwrap(); + let c12 = Okuchi::homomorphic_add(pub_key, &c1, &c2); + let c123 = Okuchi::homomorphic_add(pub_key, &c12, &c3); - let decrypted = String::from_utf8(decrypted_bytes).unwrap(); - assert_eq!(decrypted, msg); + let decrypted = Okuchi::decrypt(priv_key, &c123).unwrap(); + let expected = (&m1 + &m2 + &m3) % priv_key.p(); + assert_eq!(expected.to_bytes_be(), decrypted); } #[test] fn probabilistic_encryption() { - let keypair = KeyPair::new(512).unwrap(); + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); let message = "Hello world"; - let c1 = Okuchi::encrypt(keypair.pub_key(), message).unwrap(); let c2 = Okuchi::encrypt(keypair.pub_key(), message).unwrap(); - - // different random r values MUST produce different ciphertexts assert_ne!(c1.value(), c2.value()); } #[test] fn plaintext_too_large() { - let keypair = KeyPair::new(512).unwrap(); - let too_large = keypair.pub_key().n() + BigUint::one(); - - let result = Okuchi::encrypt(keypair.pub_key(), too_large.to_bytes_be()); - assert!(matches!(result, Err(Error::PlaintextTooLarge))); + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); + let max_bits = keypair.pub_key().plaintext_bound_bits(); + let too_large = BigUint::one() << (max_bits + 1); + assert!(matches!( + Okuchi::encrypt(keypair.pub_key(), too_large.to_bytes_be()), + Err(Error::PlaintextTooLarge) + )); } #[test] fn invalid_ciphertext() { - let keypair = KeyPair::new(512).unwrap(); + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); let bad_val = keypair.pub_key().n() + BigUint::one(); let bad_cipher = Ciphertext::new(bad_val); - - let result = Okuchi::decrypt(keypair.priv_key(), &bad_cipher); - assert!(matches!(result, Err(Error::InvalidCiphertext))); - } - - #[test] - fn ciphertext_serialization() { - let val = BigUint::from(0xDEADBEEFu64); - let c = Ciphertext::new(val.clone()); - - let bytes = c.to_bytes(); - let c_restored = Ciphertext::from(&bytes); - - assert_eq!(c, c_restored); - assert_eq!(c_restored.value(), &val); + assert!(matches!( + Okuchi::decrypt(keypair.priv_key(), &bad_cipher), + Err(Error::InvalidCiphertext) + )); } #[test] fn zero_message() { - let keypair = KeyPair::new(512).unwrap(); + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); let message = BigUint::zero(); - let c = Okuchi::encrypt(keypair.pub_key(), message.to_bytes_be()).unwrap(); let decrypted = Okuchi::decrypt(keypair.priv_key(), &c).unwrap(); - assert_eq!(message, BigUint::from_bytes_be(&decrypted)); } - #[test] - fn homomorphic_triple_addition() { - let keypair = KeyPair::new(512).unwrap(); - let pub_key = keypair.pub_key(); - let priv_key = keypair.priv_key(); - - let m1 = BigUint::from(10u32); - let m2 = BigUint::from(20u32); - let m3 = BigUint::from(30u32); - - let c1 = Okuchi::encrypt(pub_key, m1.to_bytes_be()).unwrap(); - let c2 = Okuchi::encrypt(pub_key, m2.to_bytes_be()).unwrap(); - let c3 = Okuchi::encrypt(pub_key, m3.to_bytes_be()).unwrap(); - - let c_prod = &(&c1 * &c2) * &c3; - let c_final = Ciphertext::new(c_prod.value() % pub_key.n()); - - let decrypted = Okuchi::decrypt(priv_key, &c_final).unwrap(); - let expected = (&m1 + &m2 + &m3) % &priv_key.p; - - assert_eq!(expected.to_bytes_be(), decrypted); - } - #[test] fn key_structure_validation() { - let keypair = KeyPair::new(512).unwrap(); + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); let priv_key = keypair.priv_key(); - - // verify n = p²q - let p_squared = &priv_key.p * &priv_key.p; - let n = &p_squared * &priv_key.q; - - assert_eq!(&n, priv_key.pub_key().n()); + assert_eq!(priv_key.p_squared(), &(priv_key.p() * priv_key.p())); + assert!(!priv_key.pub_key().n().is_zero()); } #[test] fn max_safe_plaintext() { - let keypair = KeyPair::new(512).unwrap(); - let priv_key = keypair.priv_key(); - - // max safe value is p - 1 - let max_safe = &priv_key.p - BigUint::one(); + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); + let max_bits = keypair.pub_key().plaintext_bound_bits(); + let max_safe = (BigUint::one() << max_bits) - BigUint::one(); let max_safe_bytes = max_safe.to_bytes_be(); let ciphertext = Okuchi::encrypt(keypair.pub_key(), &max_safe_bytes).unwrap(); let decrypted = Okuchi::decrypt(keypair.priv_key(), &ciphertext).unwrap(); - assert_eq!(max_safe_bytes, decrypted); } } diff --git a/src/plaintext.rs b/src/plaintext.rs new file mode 100644 index 0000000..6c3bd96 --- /dev/null +++ b/src/plaintext.rs @@ -0,0 +1,152 @@ +// Copyright 2025 Nelson Dominguez +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use num_bigint_dig::BigUint; +use num_traits::Zero; + +use crate::error::{Error, Result}; +use crate::key::PublicKey; + +/// A validated plaintext value in the OU plaintext space `ℤ_p`. +/// +/// ## Homomorphic Accumulation +/// +/// OU homomorphic addition computes `(m1 + m2) mod p` in the ciphertext domain. +/// If the true sum meets or exceeds the secret `p`, the result wraps silently +/// with no error at decryption time. [`Plaintext::checked_add`] detects overflow +/// against the conservative public bound before encryption. +/// +/// Use [`checked_add`](Plaintext::checked_add) whenever accumulating values +/// that will later be combined homomorphically. Note that passing the bound +/// check does not guarantee the sum is below the actual `p`; it only +/// guarantees it is below `2^(p_bits - 1)`, a strict subset of `ℤ_p`. +/// +/// ```rust,no_run +/// use okuchi::{KeyPair, Okuchi, Plaintext}; +/// use num_bigint_dig::BigUint; +/// +/// let keypair = KeyPair::new(2048).unwrap(); +/// let pub_key = keypair.pub_key(); +/// +/// let a = Plaintext::new(BigUint::from(100u32), pub_key).unwrap(); +/// let b = Plaintext::new(BigUint::from(200u32), pub_key).unwrap(); +/// let sum = a.checked_add(b, pub_key).unwrap(); // Err if sum exceeds bound +/// +/// let ct = Okuchi::encrypt_plaintext(pub_key, &sum).unwrap(); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Plaintext(BigUint); + +impl Plaintext { + /// Construct a [`Plaintext`], validating that `value` fits within the conservative OU plaintext bound. + /// + /// # Errors + /// + /// Returns [`Error::PlaintextTooLarge`] if `value.bits() > pub_key.plaintext_bound_bits()`. + pub fn new(value: impl Into, pub_key: &PublicKey) -> Result { + let value = value.into(); + let max_bits = pub_key.plaintext_bound_bits(); + if value.bits() > max_bits { + return Err(Error::PlaintextTooLarge); + } + Ok(Self(value)) + } + + /// Construct a [`Plaintext`] from a big-endian byte slice. + /// + /// # Errors + /// + /// Returns [`Error::PlaintextTooLarge`] if the decoded value exceeds the bound. + pub fn from_bytes_be(bytes: &[u8], pub_key: &PublicKey) -> Result<Plaintext> { + Self::new(BigUint::from_bytes_be(bytes), pub_key) + } + + /// Checked addition in the plaintext domain. + /// + /// Returns a new [`Plaintext`] containing `self + other` if the result + /// satisfies the plaintext bound. Consumes both operands. + /// + /// # Errors + /// + /// Returns [`Error::PlaintextTooLarge`] if `self + other` exceeds + /// `2^(p_bits - 1) - 1`. + /// + /// # Notes + /// + /// A sum that passes this check may still equal or exceed the secret `p` + /// if the actual `p` is close to `2^(p_bits - 1)`. For values well below + /// the bound this risk is negligible. The check is conservative, not exact. + pub fn checked_add(self, other: Plaintext, pub_key: &PublicKey) -> Result<Plaintext> { + Plaintext::new(self.0 + other.0, pub_key) + } + + /// Returns a reference to the inner plaintext integer. + pub fn value(&self) -> &BigUint { + &self.0 + } + + /// Whether the plaintext value is zero. + pub fn is_zero(&self) -> bool { + self.0.is_zero() + } + + /// Encode the plaintext as big-endian bytes. + /// + /// The output is suitable for direct use as input to [`Okuchi::encrypt`](crate::Okuchi::encrypt). + /// `BigUint::to_bytes_be` strips leading zero bytes; a zero value returns an empty `Vec`. + pub fn to_bytes_be(&self) -> Vec<u8> { + self.0.to_bytes_be() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::KeyPair; + use num_traits::One; + + const TEST_KEY_SIZE: usize = 2048; + + #[test] + fn plaintext_bounds_enforced() { + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); + let pub_key = keypair.pub_key(); + let max_bits = pub_key.plaintext_bound_bits(); + + // Exactly at the bound: ok + let max_val = (BigUint::one() << max_bits) - BigUint::one(); + assert!(Plaintext::new(max_val.clone(), pub_key).is_ok()); + + // One bit over: rejected + let over = max_val + BigUint::one(); + assert!(matches!( + Plaintext::new(over, pub_key), + Err(Error::PlaintextTooLarge) + )); + } + + #[test] + fn checked_add_rejects_overflow() { + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); + let pub_key = keypair.pub_key(); + let max_bits = pub_key.plaintext_bound_bits(); + + // Two values that individually fit but sum to overflow + let half = BigUint::one() << (max_bits - 1); + let a = Plaintext::new(half.clone(), pub_key).unwrap(); + let b = Plaintext::new(half.clone(), pub_key).unwrap(); + // half + half = 2^(max_bits-1) * 2 = 2^max_bits which is one bit over + let result = a.checked_add(b, pub_key); + assert!(matches!(result, Err(Error::PlaintextTooLarge))); + } + + #[test] + fn checked_add_within_bound() { + let keypair = KeyPair::new(TEST_KEY_SIZE).unwrap(); + let pub_key = keypair.pub_key(); + let a = Plaintext::new(BigUint::from(10u32), pub_key).unwrap(); + let b = Plaintext::new(BigUint::from(20u32), pub_key).unwrap(); + let sum = a.checked_add(b, pub_key).unwrap(); + assert_eq!(sum.value(), &BigUint::from(30u32)); + } +} diff --git a/src/util.rs b/src/util.rs index cc4ac87..da4a08a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,36 +1,103 @@ // Copyright 2025 Nelson Dominguez // SPDX-License-Identifier: MIT OR Apache-2.0 -use num_bigint_dig::{BigUint, RandPrime, prime::probably_prime}; +//! Internal cryptographic utilities. +//! +//! All items in this module are crate-private. None are part of the public API. + +use crypto_bigint::modular::{BoxedMontyForm, BoxedMontyParams}; +use crypto_bigint::{BoxedUint, Odd}; +use num_bigint_dig::{BigUint, RandPrime}; use num_traits::One; use rand::rngs::OsRng; -/// L(x) = (x - 1) / p +use crate::{Error, Result}; + +/// Computes the OU `L` function: `L(x) = (x - 1) / p`. +/// +/// Well-defined only when `x ≡ 1 (mod p)`, which holds when `x = c^(p-1) mod p²` +/// by Fermat's little theorem. Integer division is exact in this case; no rounding +/// occurs. /// -/// This function appears in the decryption algorithm. It's well-defined -/// because x ≡ 1 (mod p) when x = c^(p-1) mod p². +/// # Requirements +/// +/// - `x` must satisfy `x ≡ 1 (mod p)`; otherwise the division is inexact and +/// the result is cryptographically meaningless +/// - `p` must be non-zero; a zero `p` causes a panic in `BigUint` division #[inline] pub fn l_function(x: &BigUint, p: &BigUint) -> BigUint { (x - BigUint::one()) / p } -/// Generates a safe prime p where p = 2p' + 1 and p' is also prime. +/// Constant-time modular exponentiation: `base^exp mod modulus`. /// -/// Safe primes provide additional security margin against certain -/// factorization attacks by ensuring the group order has a large -/// prime factor. -pub fn generate_safe_prime(bits: usize) -> BigUint { - let mut rng = OsRng; - loop { - // random prime p' of size bits-1 - let p_prime = rng.gen_prime(bits - 1); +/// Implemented via `crypto-bigint` Montgomery form, which provides +/// constant-time guarantees for the exponentiation itself. Used in +/// [`Okuchi::decrypt`] for the sensitive operation `c^(p-1) mod p²` +/// to mitigate timing side-channels over the secret exponent `p - 1`. +/// +/// # Requirements +/// +/// - `modulus` must be odd; `p²` is always odd for any prime `p` +/// - `exp` and `base` may be any non-negative integers; `base` is +/// reduced mod `modulus` internally before conversion +/// +/// # Notes +/// +/// Bit precision is rounded up to the next multiple of 64 (one limb) to +/// satisfy `crypto-bigint`'s alignment requirement. This does not affect +/// the result. +/// +/// The conversion from `num-bigint-dig` to `crypto-bigint` types is +/// variable-time. Only the Montgomery exponentiation itself is constant-time. +pub fn ct_modpow(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { + // Montgomery form requires base < modulus; c < n but c may be >= p² + let base = base % modulus; - // compute p = 2p' + 1 - let p = (&p_prime << 1) + BigUint::one(); + // Precision: round modulus bit-length up to a multiple of 64 (limb size) + let raw_bits = modulus.bits() as u32; + let bits = raw_bits.div_ceil(64) * 64; + let bits = bits.max(64); - // check if p is prime (Miller-Rabin with 20 rounds) - if probably_prime(&p, 20) { - return p; - } + let base_b = to_boxed(&base, bits); + let exp_b = to_boxed(exp, bits); + let mod_b = to_boxed(modulus, bits); + + let odd_mod = Odd::new(mod_b).expect("p² is always odd"); + let params = BoxedMontyParams::new(odd_mod); + let base_m = BoxedMontyForm::new(base_b, &params); + let result = base_m.pow(&exp_b); + + BigUint::from_bytes_be(result.retrieve().to_be_bytes().as_ref()) +} + +/// Encode `n` as a big-endian [`BoxedUint`] with exactly `bits` bits of precision. +/// +/// # Requirements +/// +/// - `bits` must be a positive multiple of 64 +/// - `n.bits() <= bits`; callers must pre-reduce `n` to ensure this +/// +/// Padding is applied on the left (big-endian) to reach `bits / 8` bytes. +/// If `n` requires more bytes than `bits / 8`, the most-significant bytes +/// of `n` are silently truncated. Callers are responsible for preventing this. +fn to_boxed(n: &BigUint, bits: u32) -> BoxedUint { + let byte_len = (bits / 8) as usize; + let src = n.to_bytes_be(); + let mut padded = vec![0u8; byte_len]; + // src.len() <= byte_len after pre-reduction + let copy_len = src.len().min(byte_len); + padded[byte_len - copy_len..].copy_from_slice(&src[src.len() - copy_len..]); + BoxedUint::from_be_slice(&padded, bits).expect("padded length matches declared bit precision") +} + +/// Generate a prime of the requested bit length. +pub fn generate_prime(bits: usize) -> Result<BigUint> { + if bits < 2 { + return Err(Error::KeyGenerationFailed( + "prime bit length must be >= 2".into(), + )); } + let mut rng = OsRng; + Ok(rng.gen_prime(bits)) // gen_prime uses Miller-Rabin internally }