-
-
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?
Conversation
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.
@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.
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.
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
let key: [u8; KEY_LEN] = self.key | ||
.encryption() | ||
.try_into() | ||
.map_err(|_| Error::KeyLengthError)?; |
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.
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.
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.
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?
core/lib/src/config/secret_key.rs
Outdated
.map_err(|_| Error::KeyLengthError)?; | ||
|
||
// Create a new AES-256-GCM instance with the provided key | ||
let aead = Aes256Gcm::new(GenericArray::from_slice(&key)); |
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.
I think we likely want to use a more a nonce-resistant algorithm here, either AES-GCM-SIV or XChaCha20Poly1305.
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.
Great point! Should we do the same for cookie-rs?
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.
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.
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.
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.
core/lib/src/config/secret_key.rs
Outdated
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); |
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.
Note that you can use the generate_nonce()
method to generate a properly sized nonce.
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.
Done!
core/lib/src/config/secret_key.rs
Outdated
/// | ||
/// assert_eq!(decrypted, plaintext); | ||
/// ``` | ||
pub fn encrypt<T: AsRef<[u8]>>(&self, value: T) -> Result<Vec<u8>, Error> { |
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.
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;
}
core/lib/src/config/secret_key.rs
Outdated
/// | ||
/// assert_eq!(decrypted, plaintext); | ||
/// ``` | ||
pub fn encrypt<T: AsRef<[u8]>>(&self, value: T) -> Result<Vec<u8>, Error> { |
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.
It would be nice to provide a variant that allows the caller to provide associated data.
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.
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
fordecrypt
):
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?
core/lib/src/config/secret_key.rs
Outdated
/// 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> { |
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.
This would then take a Cipher
, which is pre-validated.
core/lib/src/config/secret_key.rs
Outdated
}; | ||
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 |
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.
I don't think we need this anymore. There's NonceSize
.
core/lib/src/config/secret_key.rs
Outdated
}; | ||
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; |
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.
Same with this. We should be able to get it from the Key
and ConstArrayLen
.
core/lib/src/config/secret_key.rs
Outdated
.map_err(|_| Error::KeyLengthError)?; | ||
|
||
// Create a new AES-256-GCM instance with the provided key | ||
let aead = Aes256Gcm::new(GenericArray::from_slice(&key)); |
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.
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.
79adcb1
to
4d81f62
Compare
Rebased on master. |
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. |
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.
I think is getting closer to a complete PR, but there are still a number of changes that need to be made.
core/lib/Cargo.toml
Outdated
chacha20poly1305 = "0.10.1" | ||
hkdf = "0.12.4" | ||
sha2 = "0.10.8" | ||
base64 = "0.22.1" | ||
hex = "0.4.3" |
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.
if encrypted.len() <= nonce_len { | ||
return Err(Error::EncryptedDataLengthError); | ||
} |
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.
This should not need to be checked here. Rather, it should be checked when Cipher
is constructed.
/// 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)) | ||
} |
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 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.
/// 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(); |
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.
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>); |
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.
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).
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.
Similarly, we could implement Serialize
and Deserialize
if the serde
feature is enabled.
@the10thWiz Thanks for the thorough review! |
@va-an Checking in. Any update? |
Implements encryption and decryption functions for arbitrary textual data, as discussed in #477.
encrypt
anddecrypt
toSecretKey
.