Conversation
the10thWiz
left a comment
There was a problem hiding this comment.
@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.
the10thWiz
left a comment
There was a problem hiding this comment.
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.
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.
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.
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.
Great point! Should we do the same for cookie-rs?
There was a problem hiding this comment.
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.
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
| /// | ||
| /// assert_eq!(decrypted, plaintext); | ||
| /// ``` | ||
| pub fn encrypt<T: AsRef<[u8]>>(&self, value: T) -> Result<Vec<u8>, Error> { |
There was a problem hiding this comment.
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.
It would be nice to provide a variant that allows the caller to provide associated data.
There was a problem hiding this comment.
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
| .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.
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. |
the10thWiz
left a comment
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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? |
|
I apologize for my absence. Please allow me to take a deep breath, update the branch, and finish with this PR. |
Implements encryption and decryption functions for arbitrary textual data, as discussed in #477.
encryptanddecrypttoSecretKey.