Search code examples
pythonrustpyo3maturin

How to work with a type from a Rust made library in Python


I am making a Rust based library for Python which implements different cryptographic protocols, and I'm having trouble working with EphemeralSecret, as I keep getting the error log:

"no method named to_bytes found for struct EphemeralSecret in the current scope
method not found in EphemeralSecret"

This is being used to create a Rust lib to use it in Python so I can implement different cryptographic protocols and I need to work with the created keys in my Python side of things. I understand I maybe shouldn´t need to have a privateKey serialized for security reasons, but this is my case, so how do I "pass" it? Or should I maybe convert it in another way to work with it and be able to pass it around? I also have the trouble when converting it "back" from python to rust as there is no conversion back to it from bytes. The trouble is in the following section:

// Elliptic Curve Diffie-Hellman
#[pyfunction]
fn generate_ecdh_key() -> PyResult<(Vec<u8>, Vec<u8>)> {
    let private_key = EphemeralSecret::random_from_rng(OsRng);
    let public_key = PublicKey::from(&private_key);
    
    Ok((private_key.to_bytes().to_vec(), public_key.as_bytes().to_vec()))
}

#[pyfunction]
fn derive_ecdh_shared_key(private_key_bytes: Vec<u8>, server_public_key_bytes: Vec<u8>) -> PyResult<Vec<u8>> {
    let private_key = EphemeralSecret::from(private_key_bytes.as_slice().try_into().unwrap());
    let server_public_key = PublicKey::from(server_public_key_bytes.as_slice().try_into().unwrap());

    let shared_secret = private_key.diffie_hellman(&server_public_key);
    Ok(shared_secret.as_bytes().to_vec())
}

It is part of the complete code:

use pyo3::prelude::*;
use pyo3::wrap_pyfunction;
use x25519_dalek::{EphemeralSecret, PublicKey};
use rand::Rng;
use rand::rngs::OsRng;
use num_bigint::{BigUint, RandBigInt};
use rsa::{RsaPrivateKey, RsaPublicKey, Pkcs1v15Encrypt};
use rsa::pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPrivateKey, EncodeRsaPublicKey};
use rsa::pkcs8::LineEnding;

// Diffie-Hellman
#[pyfunction]
fn generate_dh_key(p: &str, g: &str) -> PyResult<(String, String)> {
    let p = BigUint::parse_bytes(p.as_bytes(), 10).ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid p"))?;
    let g = BigUint::parse_bytes(g.as_bytes(), 10).ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid g"))?;
    
    let mut rng = OsRng;
    let private_key = rng.gen_biguint_below(&p);
    let public_key = g.modpow(&private_key, &p);

    Ok((private_key.to_str_radix(10), public_key.to_str_radix(10)))
}

#[pyfunction]
fn derive_dh_shared_key(private_key: &str, server_public_key: &str, p: &str) -> PyResult<String> {
    let p = BigUint::parse_bytes(p.as_bytes(), 10).ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid p"))?;
    let private_key = BigUint::parse_bytes(private_key.as_bytes(), 10).ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid private_key"))?;
    let server_public_key = BigUint::parse_bytes(server_public_key.as_bytes(), 10).ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid server_public_key"))?;

    let shared_secret = server_public_key.modpow(&private_key, &p);
    Ok(shared_secret.to_str_radix(10))
}

// Elliptic Curve Diffie-Hellman
#[pyfunction]
fn generate_ecdh_key() -> PyResult<(Vec<u8>, Vec<u8>)> {
    let private_key = EphemeralSecret::random_from_rng(OsRng);
    let public_key = PublicKey::from(&private_key);
    
    Ok((private_key.to_bytes().to_vec(), public_key.as_bytes().to_vec()))
}

#[pyfunction]
fn derive_ecdh_shared_key(private_key_bytes: Vec<u8>, server_public_key_bytes: Vec<u8>) -> PyResult<Vec<u8>> {
    let private_key = EphemeralSecret::from(private_key_bytes.as_slice().try_into().unwrap());
    let server_public_key = PublicKey::from(server_public_key_bytes.as_slice().try_into().unwrap());

    let shared_secret = private_key.diffie_hellman(&server_public_key);
    Ok(shared_secret.as_bytes().to_vec())
}

// RSA Functions
#[pyfunction]
fn generate_rsa_key() -> (String, String) {
    let mut rng = OsRng;
    let bits = 2048;

    let private_key = RsaPrivateKey::new(&mut rng, bits).unwrap();
    let public_key = RsaPublicKey::from(&private_key);

    let private_pem = private_key.to_pkcs1_pem(LineEnding::LF).unwrap();
    let public_pem = public_key.to_pkcs1_pem(LineEnding::LF).unwrap();

    (private_pem.to_string(), public_pem)
}

#[pyfunction]
fn rsa_encrypt(public_key_pem: &str, message: &str) -> Vec<u8> {
    let public_key = RsaPublicKey::from_pkcs1_pem(public_key_pem).unwrap();
    let mut rng = OsRng;
    public_key.encrypt(&mut rng, Pkcs1v15Encrypt, message.as_bytes()).unwrap()
}

#[pyfunction]
fn rsa_decrypt(private_key_pem: &str, encrypted_data: Vec<u8>) -> String {
    let private_key = RsaPrivateKey::from_pkcs1_pem(private_key_pem).unwrap();
    let decrypted_data = private_key.decrypt(Pkcs1v15Encrypt, &encrypted_data).unwrap();
    String::from_utf8(decrypted_data).unwrap()
}

// Swoosh NIKE key generation and exchange
#[pyfunction]
fn swoosh_generate_keys(parameters: (usize, usize, usize)) -> PyResult<(Vec<i8>, Vec<i8>)> {
    let (q, _d, n) = parameters;
    let mut rng = rand::thread_rng();
    let a: Vec<i8> = (0..n * n).map(|_| rng.gen_range(0..q) as i8).collect();
    let s: Vec<i8> = (0..n).map(|_| rng.gen_range(-1..=1)).collect();
    let e: Vec<i8> = (0..n).map(|_| rng.gen_range(-1..=1)).collect();
    let public_key: Vec<i8> = a.chunks(n).zip(&s).map(|(row, &s)| (row.iter().sum::<i8>() + e[s as usize]) % q as i8).collect();
    Ok((s, public_key))
}

#[pyfunction]
fn swoosh_derive_shared_key(private_key: Vec<i8>, public_key: Vec<i8>, q: usize) -> PyResult<Vec<i8>> {
    let shared_key: Vec<i8> = private_key.iter().zip(&public_key).map(|(&s, &p)| (s * p) % q as i8).collect();
    Ok(shared_key)
}

#[pymodule]
fn shadowCrypt(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(generate_dh_key, m)?)?;
    m.add_function(wrap_pyfunction!(derive_dh_shared_key, m)?)?;
    m.add_function(wrap_pyfunction!(generate_ecdh_key, m)?)?;
    m.add_function(wrap_pyfunction!(derive_ecdh_shared_key, m)?)?;
    m.add_function(wrap_pyfunction!(generate_rsa_key, m)?)?;
    m.add_function(wrap_pyfunction!(rsa_encrypt, m)?)?;
    m.add_function(wrap_pyfunction!(rsa_decrypt, m)?)?;
    m.add_function(wrap_pyfunction!(swoosh_generate_keys, m)?)?;
    m.add_function(wrap_pyfunction!(swoosh_derive_shared_key, m)?)?;
    Ok(())
}


Solution

  • EphemeralSecret does not have a to_bytes method as Ephemeral Secrets cannot be serialized to Vec as mentioned in the docs.

    This type is identical to the StaticSecret type, except that the EphemeralSecret::diffie_hellman method consumes and then wipes the secret key, and there are no serialization methods defined

    To convert to Vec<u8> you can use StaticSecret which has a StaticSecret::to_bytes method and can be used multiple times as well as serialized.

    So the final code would be something like

    use num_bigint::{BigUint, RandBigInt};
    use pyo3::prelude::*;
    use pyo3::wrap_pyfunction;
    use rand::rngs::OsRng;
    use rand::Rng;
    use rsa::pkcs1::{
        DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPrivateKey, EncodeRsaPublicKey,
    };
    use rsa::pkcs8::LineEnding;
    use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};
    use x25519_dalek::{PublicKey, StaticSecret};
    
    // Diffie-Hellman
    #[pyfunction]
    fn generate_dh_key(p: &str, g: &str) -> PyResult<(String, String)> {
        let p = BigUint::parse_bytes(p.as_bytes(), 10)
            .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid p"))?;
        let g = BigUint::parse_bytes(g.as_bytes(), 10)
            .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid g"))?;
    
        let mut rng = OsRng;
        let private_key = rng.gen_biguint_below(&p);
        let public_key = g.modpow(&private_key, &p);
    
        Ok((private_key.to_str_radix(10), public_key.to_str_radix(10)))
    }
    
    #[pyfunction]
    fn derive_dh_shared_key(private_key: &str, server_public_key: &str, p: &str) -> PyResult<String> {
        let p = BigUint::parse_bytes(p.as_bytes(), 10)
            .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid p"))?;
        let private_key = BigUint::parse_bytes(private_key.as_bytes(), 10)
            .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid private_key"))?;
        let server_public_key = BigUint::parse_bytes(server_public_key.as_bytes(), 10)
            .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid server_public_key"))?;
    
        let shared_secret = server_public_key.modpow(&private_key, &p);
        Ok(shared_secret.to_str_radix(10))
    }
    
    // Elliptic Curve Diffie-Hellman
    #[pyfunction]
    fn generate_ecdh_key() -> PyResult<(Vec<u8>, Vec<u8>)> {
        let private_key = StaticSecret::random_from_rng(OsRng);
        let public_key = PublicKey::from(&private_key);
    
        Ok((
            private_key.to_bytes().to_vec(),
            public_key.as_bytes().to_vec(),
        ))
    }
    
    #[pyfunction]
    fn derive_ecdh_shared_key(
        private_key_bytes: Vec<u8>,
        server_public_key_bytes: Vec<u8>,
    ) -> PyResult<Vec<u8>> {
        let private_key = StaticSecret::from(private_key_bytes.as_slice().try_into().unwrap());
        let server_public_key = PublicKey::from(server_public_key_bytes.as_slice().try_into().unwrap());
    
        let shared_secret = private_key.diffie_hellman(&server_public_key);
        Ok(shared_secret.as_bytes().to_vec())
    }
    
    // RSA Functions
    #[pyfunction]
    fn generate_rsa_key() -> (String, String) {
        let mut rng = OsRng;
        let bits = 2048;
    
        let private_key = RsaPrivateKey::new(&mut rng, bits).unwrap();
        let public_key = RsaPublicKey::from(&private_key);
    
        let private_pem = private_key.to_pkcs1_pem(LineEnding::LF).unwrap();
        let public_pem = public_key.to_pkcs1_pem(LineEnding::LF).unwrap();
    
        (private_pem.to_string(), public_pem)
    }
    
    #[pyfunction]
    fn rsa_encrypt(public_key_pem: &str, message: &str) -> Vec<u8> {
        let public_key = RsaPublicKey::from_pkcs1_pem(public_key_pem).unwrap();
        let mut rng = OsRng;
        public_key
            .encrypt(&mut rng, Pkcs1v15Encrypt, message.as_bytes())
            .unwrap()
    }
    
    #[pyfunction]
    fn rsa_decrypt(private_key_pem: &str, encrypted_data: Vec<u8>) -> String {
        let private_key = RsaPrivateKey::from_pkcs1_pem(private_key_pem).unwrap();
        let decrypted_data = private_key
            .decrypt(Pkcs1v15Encrypt, &encrypted_data)
            .unwrap();
        String::from_utf8(decrypted_data).unwrap()
    }
    
    // Swoosh NIKE key generation and exchange
    #[pyfunction]
    fn swoosh_generate_keys(parameters: (usize, usize, usize)) -> PyResult<(Vec<i8>, Vec<i8>)> {
        let (q, _d, n) = parameters;
        let mut rng = rand::thread_rng();
        let a: Vec<i8> = (0..n * n).map(|_| rng.gen_range(0..q) as i8).collect();
        let s: Vec<i8> = (0..n).map(|_| rng.gen_range(-1..=1)).collect();
        let e: Vec<i8> = (0..n).map(|_| rng.gen_range(-1..=1)).collect();
        let public_key: Vec<i8> = a
            .chunks(n)
            .zip(&s)
            .map(|(row, &s)| (row.iter().sum::<i8>() + e[s as usize]) % q as i8)
            .collect();
        Ok((s, public_key))
    }
    
    #[pyfunction]
    fn swoosh_derive_shared_key(
        private_key: Vec<i8>,
        public_key: Vec<i8>,
        q: usize,
    ) -> PyResult<Vec<i8>> {
        let shared_key: Vec<i8> = private_key
            .iter()
            .zip(&public_key)
            .map(|(&s, &p)| (s * p) % q as i8)
            .collect();
        Ok(shared_key)
    }
    
    #[pymodule]
    fn shadow_crypt(m: &Bound<'_, PyModule>) -> PyResult<()> {
        m.add_function(wrap_pyfunction!(generate_dh_key, m)?)?;
        m.add_function(wrap_pyfunction!(derive_dh_shared_key, m)?)?;
        m.add_function(wrap_pyfunction!(generate_ecdh_key, m)?)?;
        m.add_function(wrap_pyfunction!(derive_ecdh_shared_key, m)?)?;
        m.add_function(wrap_pyfunction!(generate_rsa_key, m)?)?;
        m.add_function(wrap_pyfunction!(rsa_encrypt, m)?)?;
        m.add_function(wrap_pyfunction!(rsa_decrypt, m)?)?;
        m.add_function(wrap_pyfunction!(swoosh_generate_keys, m)?)?;
        m.add_function(wrap_pyfunction!(swoosh_derive_shared_key, m)?)?;
        Ok(())
    }
    

    Note: The Cargo.toml file needs to be modified as follows:

    x25519_dalek = { version = "2.0.1", features = ["static_secrets"] }