Skip to content
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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

va-an
Copy link
Contributor

@va-an va-an commented Jun 3, 2024

Implements encryption and decryption functions for arbitrary textual data, as discussed in #477.

  • Added functions encrypt and decrypt to SecretKey.
  • AAD is not used since we don't have metadata like cookie names.
  • Functions for encryption/decryption are based on implementations from cookie-rs.
  • Added example with suggested usage.

Copy link
Collaborator

@the10thWiz the10thWiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SergioBenitez can weigh in, but I think this is pretty close to being ready for merge. There are a few specific points in the API I noted.

core/lib/src/config/secret_key.rs Outdated Show resolved Hide resolved
core/lib/src/config/secret_key.rs Outdated Show resolved Hide resolved
core/lib/src/config/secret_key.rs Outdated Show resolved Hide resolved
examples/private-data/Rocket.toml Outdated Show resolved Hide resolved
examples/private-data/src/main.rs Outdated Show resolved Hide resolved
@va-an va-an requested a review from the10thWiz June 4, 2024 18:41
Copy link
Collaborator

@the10thWiz the10thWiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm almost ready to approve these changes. @SergioBenitez should take a look, I think the implementation is solid.

core/lib/src/config/secret_key.rs Outdated Show resolved Hide resolved
Comment on lines 215 to 218
let key: [u8; KEY_LEN] = self.key
.encryption()
.try_into()
.map_err(|_| Error::KeyLengthError)?;
Copy link
Member

@SergioBenitez SergioBenitez Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stores the key on the stack, which shouldn't be necessary. Furthermore, it reuses the encryption() key for a different operation, which we shouldn't do. Instead we should use a KDF (say HKDF) to generate a new key for this purpose using a unique info string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added encryption key generation using HKDF, so the original key is no longer stored on the stack.
I'm not sure I understand about enсryption() - do we need to use a different method to obtain the secret key for KDF?

.map_err(|_| Error::KeyLengthError)?;

// Create a new AES-256-GCM instance with the provided key
let aead = Aes256Gcm::new(GenericArray::from_slice(&key));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we likely want to use a more a nonce-resistant algorithm here, either AES-GCM-SIV or XChaCha20Poly1305.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point! Should we do the same for cookie-rs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to use XChaCha20Poly1305 because the crate chacha20poly1305 has received a security audit, and the crate aes_gcm_siv has not been audited yet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good. cookie-rs has slightly different requirements. It may be worthwhile to change our algorithm there too, at least to aes-gcm-siv but the trade-off is different there. It's something I'm considering, but it's orthogonal to this PR.

Comment on lines 224 to 227
let mut nonce = [0u8; NONCE_LEN];
let mut rng = rand::thread_rng();
rng.try_fill_bytes(&mut nonce).map_err(|_| Error::NonceFillError)?;
let nonce = Nonce::from_slice(&nonce);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that you can use the generate_nonce() method to generate a properly sized nonce.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

///
/// assert_eq!(decrypted, plaintext);
/// ```
pub fn encrypt<T: AsRef<[u8]>>(&self, value: T) -> Result<Vec<u8>, Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A return type of Vec<u8> is not particularly useful. It would be nice to return something that encapsulates the Vec<u8> with convenient methods, like blake3::Hash. For example, something lIke:

struct Cipher(Vec<u8>);

impl Cipher {
    fn from_bytes(&[u8]) -> Result<Self>;
    fn from_vec(Vec<u8>) -> Result<Self>;
    fn from_hex(&str) -> Result<Self>;
    fn from_base64(&str) -> Result<Self>;

    fn as_bytes(&self) -> &[u8];
    fn into_vec(self) -> Vec<u8>;
    fn to_hex(&self) -> String;
    fn to_base64(&self) -> String;
}

///
/// assert_eq!(decrypted, plaintext);
/// ```
pub fn encrypt<T: AsRef<[u8]>>(&self, value: T) -> Result<Vec<u8>, Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to provide a variant that allows the caller to provide associated data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can implement it in different ways.

  • change the current method by adding an optional argument for AAD:
pub fn encrypt<T: AsRef<[u8]>, A: AsRef<[u8]>>(
    &self,
    value: T,
    aad: Option<A>
) -> Result<Cipher, Error> {
  • or add separate method with additional argument (and accordingly a similar method
    for decrypt):
pub fn encrypt_with_aad<T: AsRef<[u8]>, A: AsRef<[u8]>>(
    &self,
    value: T,
    aad: A
) -> Result<Cipher, Error> {

I prefer the second way. Despite requiring more code, it might be clearer for the user.
Which one do you like more?

/// Decrypts the given encrypted data.
/// Extracts the nonce from the data and uses it for decryption.
/// Returns the decrypted Vec<u8>.
pub fn decrypt<T: AsRef<[u8]>>(&self, encrypted: T) -> Result<Vec<u8>, Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would then take a Cipher, which is pre-validated.

};
use cookie::Key;
use serde::{de, ser, Deserialize, Serialize};

use crate::request::{Outcome, Request, FromRequest};

const NONCE_LEN: usize = 12;
const NONCE_LEN: usize = 24; // 192-bit
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this anymore. There's NonceSize.

};
use cookie::Key;
use serde::{de, ser, Deserialize, Serialize};

use crate::request::{Outcome, Request, FromRequest};

const NONCE_LEN: usize = 12;
const NONCE_LEN: usize = 24; // 192-bit
const KEY_LEN: usize = 32;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this. We should be able to get it from the Key and ConstArrayLen.

.map_err(|_| Error::KeyLengthError)?;

// Create a new AES-256-GCM instance with the provided key
let aead = Aes256Gcm::new(GenericArray::from_slice(&key));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good. cookie-rs has slightly different requirements. It may be worthwhile to change our algorithm there too, at least to aes-gcm-siv but the trade-off is different there. It's something I'm considering, but it's orthogonal to this PR.

@va-an va-an force-pushed the encrypt-data-with-secret-key branch from 79adcb1 to 4d81f62 Compare June 8, 2024 18:22
@va-an
Copy link
Contributor Author

va-an commented Jun 8, 2024

Rebased on master.

@the10thWiz
Copy link
Collaborator

There has been an issue with CI on Windows, so you will need to merge the latest commit from master before CI will pass.

@va-an
Copy link
Contributor Author

va-an commented Jun 11, 2024

There has been an issue with CI on Windows, so you will need to merge the latest commit from master before CI will pass.

Understood, I will do it after I finish making changes based on the comments.
Thanks!

@va-an va-an requested a review from SergioBenitez June 26, 2024 07:50
Copy link
Collaborator

@the10thWiz the10thWiz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is getting closer to a complete PR, but there are still a number of changes that need to be made.

Comment on lines 77 to 81
chacha20poly1305 = "0.10.1"
hkdf = "0.12.4"
sha2 = "0.10.8"
base64 = "0.22.1"
hex = "0.4.3"
Copy link
Collaborator

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.

Comment on lines +279 to +281
if encrypted.len() <= nonce_len {
return Err(Error::EncryptedDataLengthError);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 Cipher is constructed.

Comment on lines +388 to +408
/// 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))
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Comment on lines +111 to +128
/// 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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many of these doc-tests fail. Use cargo test --doc to run them locally.

/// let bytes = cipher.as_bytes();
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cipher(Vec<u8>);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make Cipher more useful, it could implement FromForm, FromParam, FromSegment, and FromData. They should either check the format (which may not be possible, since hex could be re-interpreted as base-64), or always use one specific encoding (and provide a method like encode that encodes it using the same encoding).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, we could implement Serialize and Deserialize if the serde feature is enabled.

@va-an
Copy link
Contributor Author

va-an commented Jul 7, 2024

@the10thWiz Thanks for the thorough review!
I merged the changes from master that you mentioned and moved the dependencies to the "secrets" feature.
I will address the remaining comments a bit later.

@SergioBenitez
Copy link
Member

@va-an Checking in. Any update?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants