-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Encrypt data with secret key #2803
base: master
Are you sure you want to change the base?
Changes from 11 commits
ab6c711
b33f56a
4bd058b
ffea9d4
8278683
ac98764
e08f9f9
4163474
4d81f62
3c67b97
bf5d1cd
5296e3f
bba0d6c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,28 @@ | ||
use std::fmt; | ||
|
||
use chacha20poly1305::{ | ||
aead::{generic_array::typenum::Unsigned, Aead, AeadCore, KeyInit, OsRng}, | ||
XChaCha20Poly1305, XNonce | ||
}; | ||
use hkdf::Hkdf; | ||
use sha2::Sha256; | ||
use cookie::Key; | ||
use base64::{engine::general_purpose::URL_SAFE, Engine as _}; | ||
use serde::{de, ser, Deserialize, Serialize}; | ||
|
||
use crate::request::{Outcome, Request, FromRequest}; | ||
|
||
#[derive(Debug)] | ||
pub enum Error { | ||
KeyLengthError, | ||
NonceFillError, | ||
EncryptionError, | ||
DecryptionError, | ||
EncryptedDataLengthError, | ||
Base64DecodeError, | ||
HexDecodeError, | ||
} | ||
|
||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | ||
enum Kind { | ||
Zero, | ||
|
@@ -80,6 +98,38 @@ pub struct SecretKey { | |
provided: bool, | ||
} | ||
|
||
/// A struct representing encrypted data. | ||
/// | ||
/// The `Cipher` struct encapsulates encrypted data and provides various | ||
/// utility methods for encoding and decoding this data in different formats | ||
/// such as bytes, hexadecimal, and base64. | ||
/// | ||
/// # Examples | ||
/// | ||
/// Creating a `Cipher` from bytes: | ||
/// ``` | ||
/// let data = b"some encrypted data"; | ||
/// let cipher = Cipher::from_bytes(data); | ||
/// ``` | ||
/// | ||
/// Converting a `Cipher` to a hexadecimal string: | ||
/// ``` | ||
/// let hex = cipher.to_hex(); | ||
/// ``` | ||
/// | ||
/// Creating a `Cipher` from a base64 string: | ||
/// ``` | ||
/// let base64_str = "c29tZSBlbmNyeXB0ZWQgZGF0YQ=="; | ||
/// let cipher = Cipher::from_base64(base64_str).unwrap(); | ||
/// ``` | ||
/// | ||
/// Converting a `Cipher` back to bytes: | ||
/// ``` | ||
/// let bytes = cipher.as_bytes(); | ||
Comment on lines
+111
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Many of these doc-tests fail. Use |
||
/// ``` | ||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
pub struct Cipher(Vec<u8>); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To make Cipher more useful, it could implement There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly, we could implement |
||
|
||
impl SecretKey { | ||
/// Returns a secret key that is all zeroes. | ||
pub(crate) fn zero() -> SecretKey { | ||
|
@@ -178,6 +228,70 @@ impl SecretKey { | |
{ | ||
ser.serialize_bytes(&[0; 32][..]) | ||
} | ||
|
||
fn cipher(&self, nonce: &[u8]) -> Result<XChaCha20Poly1305, Error> { | ||
let (mut prk, hk) = Hkdf::<Sha256>::extract(Some(nonce), self.key.encryption()); | ||
hk.expand(b"secret_key_data_encryption", &mut prk).map_err(|_| Error::KeyLengthError)?; | ||
|
||
Ok(XChaCha20Poly1305::new(&prk)) | ||
} | ||
|
||
/// Encrypts the given data. | ||
/// Generates a random nonce for each encryption to ensure uniqueness. | ||
/// Returns the Vec<u8> of the concatenated nonce and ciphertext. | ||
/// | ||
/// # Example | ||
/// ```rust | ||
/// use rocket::config::SecretKey; | ||
/// | ||
/// let plaintext = "I like turtles".as_bytes(); | ||
/// let secret_key = SecretKey::generate().unwrap(); | ||
/// | ||
/// let cipher = secret_key.encrypt(&plaintext).unwrap(); | ||
/// let decrypted = secret_key.decrypt(&cipher).unwrap(); | ||
/// | ||
/// assert_eq!(plaintext, decrypted); | ||
/// ``` | ||
pub fn encrypt<T: AsRef<[u8]>>(&self, value: T) -> Result<Cipher, Error> { | ||
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); | ||
let cipher = self.cipher(&nonce)?; | ||
|
||
let ciphertext = cipher | ||
.encrypt(&nonce, value.as_ref()) | ||
.map_err(|_| Error::EncryptionError)?; | ||
|
||
// Prepare a vector to hold the nonce and ciphertext | ||
let mut encrypted_data = Vec::with_capacity(nonce.len() + ciphertext.len()); | ||
encrypted_data.extend_from_slice(nonce.as_slice()); | ||
encrypted_data.extend_from_slice(&ciphertext); | ||
|
||
Ok(Cipher(encrypted_data)) | ||
} | ||
|
||
/// Decrypts the given encrypted data, encapsulated in a Cipher wrapper. | ||
/// Extracts the nonce from the data and uses it for decryption. | ||
/// Returns the decrypted Vec<u8>. | ||
pub fn decrypt(&self, encrypted: &Cipher) -> Result<Vec<u8>, Error> { | ||
let encrypted = encrypted.as_bytes(); | ||
|
||
// Check if the length of decoded data is at least the length of the nonce | ||
let nonce_len = <XChaCha20Poly1305 as AeadCore>::NonceSize::USIZE; | ||
if encrypted.len() <= nonce_len { | ||
return Err(Error::EncryptedDataLengthError); | ||
} | ||
Comment on lines
+279
to
+281
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should not need to be checked here. Rather, it should be checked when |
||
|
||
// Split the decoded data into nonce and ciphertext | ||
let (nonce, ciphertext) = encrypted.split_at(nonce_len); | ||
let nonce = XNonce::from_slice(nonce); | ||
|
||
let cipher = self.cipher(nonce)?; | ||
|
||
// Decrypt the ciphertext using the nonce | ||
let decrypted = cipher.decrypt(nonce, ciphertext) | ||
.map_err(|_| Error::DecryptionError)?; | ||
|
||
Ok(decrypted) | ||
} | ||
} | ||
|
||
impl PartialEq for SecretKey { | ||
|
@@ -269,3 +383,53 @@ impl fmt::Debug for SecretKey { | |
<Self as fmt::Display>::fmt(self, f) | ||
} | ||
} | ||
|
||
impl Cipher { | ||
/// Create a `Cipher` from its raw bytes representation. | ||
pub fn from_bytes(bytes: &[u8]) -> Self { | ||
Cipher(bytes.to_vec()) | ||
} | ||
|
||
/// Create a `Cipher` from a vector of bytes. | ||
pub fn from_vec(vec: Vec<u8>) -> Self { | ||
Cipher(vec) | ||
} | ||
|
||
/// Create a `Cipher` from a hex string. | ||
pub fn from_hex(hex: &str) -> Result<Self, Error> { | ||
let decoded = hex::decode(hex).map_err(|_| Error::HexDecodeError)?; | ||
Ok(Cipher(decoded)) | ||
} | ||
|
||
/// Create a `Cipher` from a base64 string. | ||
pub fn from_base64(base64: &str) -> Result<Self, Error> { | ||
let decoded = URL_SAFE.decode(base64).map_err(|_| Error::Base64DecodeError)?; | ||
Ok(Cipher(decoded)) | ||
} | ||
Comment on lines
+388
to
+408
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of these methods should validate that the length is longer than the Nonce, and that the length (minus the Nonce) is a multiple of the block size. Ideally, the only error that should be caught when decrypting the value is that the value was not created by encrypting with the same secret key. |
||
|
||
/// Returns the bytes contained in the `Cipher`. | ||
pub fn as_bytes(&self) -> &[u8] { | ||
&self.0 | ||
} | ||
|
||
/// Consumes the `Cipher` and returns the contained bytes as a vector. | ||
pub fn into_vec(self) -> Vec<u8> { | ||
self.0 | ||
} | ||
|
||
/// Returns the hex representation of the bytes contained in the `Cipher`. | ||
pub fn to_hex(&self) -> String { | ||
hex::encode(&self.0) | ||
} | ||
|
||
/// Returns the base64 representation of the bytes contained in the `Cipher`. | ||
pub fn to_base64(&self) -> String { | ||
URL_SAFE.encode(&self.0) | ||
} | ||
} | ||
|
||
impl fmt::Display for Cipher { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "{}", self.to_base64()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
#![cfg(feature = "secrets")] | ||
#![deny(warnings)] | ||
|
||
#[cfg(test)] | ||
mod cookies_private_tests { | ||
use rocket::config::{SecretKey, Cipher}; | ||
|
||
#[test] | ||
fn cipher_conversions() { | ||
let secret_key = SecretKey::generate().unwrap(); | ||
|
||
let plaintext = "I like turtles"; | ||
let cipher = secret_key.encrypt(plaintext).unwrap(); | ||
|
||
assert_eq!(cipher, Cipher::from_bytes(&cipher.as_bytes())); | ||
assert_eq!(cipher, Cipher::from_vec(cipher.clone().into_vec())); | ||
assert_eq!(cipher, Cipher::from_hex(&cipher.to_hex()).unwrap()); | ||
assert_eq!(cipher, Cipher::from_base64(&cipher.to_base64()).unwrap()); | ||
} | ||
|
||
#[test] | ||
fn encrypt_decrypt() { | ||
let secret_key = SecretKey::generate().unwrap(); | ||
|
||
// encrypt byte array | ||
let msg = "very-secret-message".as_bytes(); | ||
let encrypted = secret_key.encrypt(&msg).unwrap(); | ||
let decrypted = secret_key.decrypt(&encrypted).unwrap(); | ||
assert_eq!(msg, decrypted); | ||
|
||
// encrypt String | ||
let msg = "very-secret-message".to_string(); | ||
let encrypted = secret_key.encrypt(&msg).unwrap(); | ||
let decrypted = secret_key.decrypt(&encrypted).unwrap(); | ||
assert_eq!(msg.as_bytes(), decrypted); | ||
} | ||
|
||
#[test] | ||
fn encrypt_with_wrong_key() { | ||
let msg = "very-secret-message".as_bytes(); | ||
|
||
let secret_key = SecretKey::generate().unwrap(); | ||
let encrypted = secret_key.encrypt(msg).unwrap(); | ||
|
||
let another_secret_key = SecretKey::generate().unwrap(); | ||
let result = another_secret_key.decrypt(&encrypted); | ||
assert!(result.is_err()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,8 +17,8 @@ members = [ | |
"testing", | ||
"tls", | ||
"upgrade", | ||
|
||
"pastebin", | ||
"todo", | ||
"chat", | ||
"private-data", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,2 @@ | ||
[default] | ||
secret_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg=" | ||
template_dir = "templates" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
[package] | ||
name = "private-data" | ||
version = "0.0.0" | ||
workspace = "../" | ||
edition = "2021" | ||
publish = false | ||
|
||
[dependencies] | ||
rocket = { path = "../../core/lib", features = ["secrets"] } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
#[macro_use] | ||
extern crate rocket; | ||
|
||
use rocket::config::Cipher; | ||
use rocket::{Config, State}; | ||
use rocket::fairing::AdHoc; | ||
use rocket::response::status; | ||
use rocket::http::Status; | ||
|
||
#[cfg(test)] mod tests; | ||
|
||
#[get("/encrypt/<msg>")] | ||
fn encrypt_endpoint(msg: &str, config: &State<Config>) -> Result<String, status::Custom<String>> { | ||
let secret_key = config.secret_key.clone(); | ||
|
||
let encrypted_msg = secret_key | ||
.encrypt(msg) | ||
.map(|cipher| cipher.to_base64()) | ||
.map_err(|_| { | ||
status::Custom(Status::InternalServerError, "Failed to encrypt message".to_string()) | ||
})?; | ||
|
||
info!("received message for encrypt: '{}'", msg); | ||
info!("encrypted msg: '{}'", encrypted_msg); | ||
|
||
Ok(encrypted_msg) | ||
} | ||
|
||
#[get("/decrypt/<msg>")] | ||
fn decrypt_endpoint(msg: &str, config: &State<Config>) -> Result<String, status::Custom<String>> { | ||
let secret_key = config.secret_key.clone(); | ||
|
||
let cipher = Cipher::from_base64(msg).map_err(|_| { | ||
status::Custom(Status::BadRequest, "Failed to decode base64".to_string()) | ||
})?; | ||
|
||
let decrypted = secret_key.decrypt(&cipher).map_err(|_| { | ||
status::Custom(Status::InternalServerError, "Failed to decrypt message".to_string()) | ||
})?; | ||
|
||
let decrypted_msg = String::from_utf8(decrypted).map_err(|_| { | ||
status::Custom(Status::InternalServerError, | ||
"Failed to convert decrypted message to UTF-8".to_string()) | ||
})?; | ||
|
||
info!("received message for decrypt: '{}'", msg); | ||
info!("decrypted msg: '{}'", decrypted_msg); | ||
|
||
Ok(decrypted_msg) | ||
} | ||
|
||
#[launch] | ||
fn rocket() -> _ { | ||
rocket::build() | ||
.mount("/", routes![encrypt_endpoint, decrypt_endpoint]) | ||
.attach(AdHoc::config::<Config>()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
use rocket::{config::SecretKey, local::blocking::Client}; | ||
|
||
#[test] | ||
fn encrypt_decrypt() { | ||
let secret_key = SecretKey::generate().unwrap(); | ||
let msg = "very-secret-message".as_bytes(); | ||
|
||
let encrypted = secret_key.encrypt(msg).unwrap(); | ||
let decrypted = secret_key.decrypt(&encrypted).unwrap(); | ||
|
||
assert_eq!(msg, decrypted); | ||
} | ||
|
||
#[test] | ||
fn encrypt_decrypt_api() { | ||
let client = Client::tracked(super::rocket()).unwrap(); | ||
let msg = "some-secret-message"; | ||
|
||
let encrypted = client.get(format!("/encrypt/{}", msg)).dispatch().into_string().unwrap(); | ||
let decrypted = client.get(format!("/decrypt/{}", encrypted)).dispatch().into_string().unwrap(); | ||
|
||
assert_eq!(msg, decrypted); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these should probably be feature-gated behind the
secrets
feature.