Using Openssl in Rust
openssl rust cryptographyThis post talks about using OpenSSL in rust. As it is the first blog post in a series, we will cover only following stuff
- Using OpenSSL APIs to generate an RSA key-pair.
- Encrypting and decrypting the data using those keys.
What we are not going to do is
- get into the specifics of RSA algorithm.
- get into the lower level implementation details of RSA in OpenSSL.
Prerequisites
Before we start
-
What is RSA?
It’s an asymmetric encryption algorithm, which means; you get a key key-pair (public key and private key), using these two keys you perform the encryption and decryption. During communication, The public key is shared to the other party while the private key is kept secret.
You can perform both encryption and decryption with both the public and private keys. Meaning, data encrypted with the public key can be decrypted with the private key and the data encrypted with the private key can be decrypted with the public key.
-
Where is it used?
Mostly during the initial authentication or key exchange between two parties.
Using openssl in a project
To use OpenSSL in a rust project, you would need to add it to the Cargo.toml
.
For example,
[package]
name = "open_ssl_demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
openssl = "0.10.68"
Now, let’s use it to generate a key-pair
use openssl::rsa::Rsa;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// generate the key-pair
let rsa = Rsa::generate(4096)?;
// get the public key
let rsa_public_key = &rsa.public_key_to_pem()?;
// get the private key
let rsa_private_key = &rsa.private_key_to_pem()?;
//convert the key data (bytes) to string
let rsa_public_key_str = String::from_utf8(rsa_public_key.to_vec())?;
let rsa_private_key_str = String::from_utf8(rsa_private_key.to_vec())?;
println!("rsa public key {}", rsa_public_key_str);
println!("rsa priate key {}", rsa_private_key_str);
Ok(())
}
Here we created a key pair of 4096
bits, parsed the keys in PEM
format and printed those keys.
To run this
$ cargo run --release
Finished release [optimized] target(s) in 0.01s
Running `target/release/open_ssl_demo`
rsa public key -----BEGIN PUBLIC KEY-----
<your public key data>
-----END PUBLIC KEY-----
rsa priate key -----BEGIN RSA PRIVATE KEY-----
<your private key data>
-----END RSA PRIVATE KEY-----
Now we have generated the keys, let’s use them for encryption and decryption.
The API is simple, you take the plain text and encrypt it with the public key. Then you take this encrypted text/cipher text and decrypt it with the private key.
While doing this we need to use appropriate Padding.
For RSA, we need to use PKCS1padding.
(If you are interested, the latest RFC for PKCS1
is RFC8017).
For example
let input_plaintext = "this is plain text";
println!("plain text: {}", input_plaintext);
// allocate the buffer for the encrypted data
plain_text_decrypted = vec![0; rsa.size() as usize];
// allocate the buffer for the decrypted data
let mut encrypted_data = vec![0; rsa.size() as usize];
// encrypt the data using public key
// use the PKCS1 padding
rsa.public_encrypt(
&input_plaintext.as_bytes(),
&mut encrypted_data,
Padding::PKCS1,
)?;
println!("encrypted data: {:?}", encrypted_data);
// decrypt the cipher text using private key
// use the PKCS1 padding
rsa.private_decrypt(&encrypted_data, &mut plain_text_decrypted, Padding::PKCS1)?;
println!(
"decrypted data: {}",
String::from_utf8(plain_text_decrypted)?
);
Similarly you can take the plain text and encrypt it with the private key and then decrypt the cipher text with the public key.
let input_plaintext = "this is plain text";
println!("plain text: {}", input_plaintext);
// allocate the buffer for the encrypted data
plain_text_decrypted = vec![0; rsa.size() as usize];
// allocate the buffer for the decrypted data
let mut encrypted_data = vec![0; rsa.size() as usize];
// encrypt the data using private key
// use the PKCS1 padding
rsa.private_encrypt(
&input_plaintext.as_bytes(),
&mut encrypted_data,
Padding::PKCS1,
)?;
println!("encrypted data: {:?}", encrypted_data);
// decrypt the cipher text using public key
// use the PKCS1 padding
rsa.public_decrypt(&encrypted_data, &mut plain_text_decrypted, Padding::PKCS1)?;
println!(
"decrypted data: {}",
String::from_utf8(plain_text_decrypted)?
);
The complete code is
use openssl::rsa::{Padding, Rsa};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// generate the key-pair
let rsa = Rsa::generate(4096)?;
// get the public key
let rsa_public_key = &rsa.public_key_to_pem()?;
// get the private key
let rsa_private_key = &rsa.private_key_to_pem()?;
//convert the key data (bytes) to string
let rsa_public_key_str = String::from_utf8(rsa_public_key.to_vec())?;
let rsa_private_key_str = String::from_utf8(rsa_private_key.to_vec())?;
println!("rsa public key {}", rsa_public_key_str);
println!("rsa priate key {}", rsa_private_key_str);
let input_plaintext = "this is plain text";
println!("plain text: {}", input_plaintext);
// allocate the buffer for the encrypted data
let mut plain_text_decrypted = vec![0; rsa.size() as usize];
// allocate the buffer for the decrypted data
let mut encrypted_data = vec![0; rsa.size() as usize];
// encrypt the data using public key
// use the PKCS1 padding
rsa.public_encrypt(
&input_plaintext.as_bytes(),
&mut encrypted_data,
Padding::PKCS1,
)?;
println!("encrypted data: {:?}", encrypted_data);
// decrypt the cipher text using private key
// use the PKCS1 padding
rsa.private_decrypt(&encrypted_data, &mut plain_text_decrypted, Padding::PKCS1)?;
println!(
"decrypted data: {}",
String::from_utf8(plain_text_decrypted)?
);
let input_plaintext = "this is plain text";
println!("plain text: {}", input_plaintext);
// allocate the buffer for the encrypted data
let mut plain_text_decrypted = vec![0; rsa.size() as usize];
// allocate the buffer for the decrypted data
let mut encrypted_data = vec![0; rsa.size() as usize];
// encrypt the data using private key
// use the PKCS1 padding
rsa.private_encrypt(
&input_plaintext.as_bytes(),
&mut encrypted_data,
Padding::PKCS1,
)?;
println!("encrypted data: {:?}", encrypted_data);
// decrypt the cipher text using public key
// use the PKCS1 padding
rsa.public_decrypt(&encrypted_data, &mut plain_text_decrypted, Padding::PKCS1)?;
println!(
"decrypted data: {}",
String::from_utf8(plain_text_decrypted)?
);
Ok(())
}
In the code, buffers allocated for encrypted and decrypted data are of the same size as the generated RSA key. This is done for a couple of reasons, When encrypting or decrypting data using RSA, it’s important to ensure that the input data is smaller than the key size, otherwise it can lead to issues like
- It can open up new attack vectors, check out the padding oracle attacks
- Although RSA is more secure than other symmetric key encryption algorithms, it’s more compute intensive, if the input data size is bigger than the key size, it can lead to performance and scaling issues.
- Also the rust API that we used, will raise an exception when the input data is bigger than the key size.
To encrypt the larger data than your key size, we will be talking about encrypting the data using AES in the next post.