From ca86747862e653ef23c7d308d1866db42215bbd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolphe=20Br=C3=A9ard?= Date: Sun, 24 Mar 2024 12:16:54 +0100 Subject: [PATCH] Add support for AES128-GCM using HKDF-SHA256 to derive keys --- Cargo.toml | 3 ++ src/cipher_box.rs | 88 +++++++++++++++++++++++++++++++++++++++++----- src/error.rs | 2 ++ src/ikm.rs | 19 +++++++++- src/scheme.rs | 11 ++++++ src/scheme/aes.rs | 66 ++++++++++++++++++++++++++++++++++ src/scheme/sha2.rs | 23 ++++++++++++ 7 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 src/scheme/aes.rs create mode 100644 src/scheme/sha2.rs diff --git a/Cargo.toml b/Cargo.toml index 7b195db..5409e7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,13 @@ encryption = [] ikm-management = [] [dependencies] +aes-gcm = { version = "0.10.3", default-features = false, features = ["std", "aes"] } base64ct = { version = "1.6.0", default-features = false, features = ["std"] } blake3 = { version = "1.5.0", default-features = false } chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["std"] } getrandom = { version = "0.2.12", default-features = false } +hkdf = { version = "0.12.4", default-features = false, features = ["std"] } +sha2 = { version = "0.10.8", default-features = false, features = ["std"] } thiserror = { version = "1.0.57", default-features = false } [dev-dependencies] diff --git a/src/cipher_box.rs b/src/cipher_box.rs index f17f743..c15b6d4 100644 --- a/src/cipher_box.rs +++ b/src/cipher_box.rs @@ -94,16 +94,23 @@ mod tests { ctx } - fn get_ikm_lst() -> InputKeyMaterialList { + fn get_ikm_lst_chacha20poly1305_blake3() -> InputKeyMaterialList { InputKeyMaterialList::import( "AQAAAA:AQAAAAEAAAC_vYEw1ujVG5i-CtoPYSzik_6xaAq59odjPm5ij01-e6zz4mUAAAAALJGBiwAAAAAA", ) .unwrap() } + fn get_ikm_lst_aes128gcm_sha256() -> InputKeyMaterialList { + InputKeyMaterialList::import( + "AQAAAA:AQAAAAIAAAA2lXqTSduZ22J0LiwEhmENjB6pLo0GVKvAQYocJcAAp1f8_2UAAAAAuzDPeAAAAAAA", + ) + .unwrap() + } + #[test] - fn encrypt_decrypt_no_context() { - let lst = get_ikm_lst(); + fn encrypt_decrypt_no_context_chacha20poly1305_blake3() { + let lst = get_ikm_lst_chacha20poly1305_blake3(); let key_ctx = get_static_empty_key_ctx(); let data_ctx = DataContext::from([]); let cb = CipherBox::new(&lst); @@ -123,8 +130,29 @@ mod tests { } #[test] - fn encrypt_decrypt_with_static_context() { - let lst = get_ikm_lst(); + fn encrypt_decrypt_no_context_aes128gcm_sha256() { + let lst = get_ikm_lst_aes128gcm_sha256(); + let key_ctx = get_static_empty_key_ctx(); + let data_ctx = DataContext::from([]); + let cb = CipherBox::new(&lst); + + // Encrypt + let res = cb.encrypt(&key_ctx, &data_ctx, TEST_DATA); + assert!(res.is_ok(), "res: {res:?}"); + let ciphertext = res.unwrap(); + assert!(ciphertext.starts_with("AQAAAA:")); + assert_eq!(ciphertext.len(), 82); + + // Decrypt + let res = cb.decrypt(&key_ctx, &data_ctx, &ciphertext); + assert!(res.is_ok(), "res: {res:?}"); + let plaintext = res.unwrap(); + assert_eq!(plaintext, TEST_DATA); + } + + #[test] + fn encrypt_decrypt_with_static_context_chacha20poly1305_blake3() { + let lst = get_ikm_lst_chacha20poly1305_blake3(); let key_ctx = get_static_key_ctx(); let data_ctx = DataContext::from(TEST_DATA_CTX); let cb = CipherBox::new(&lst); @@ -144,8 +172,29 @@ mod tests { } #[test] - fn encrypt_decrypt_with_context() { - let lst = get_ikm_lst(); + fn encrypt_decrypt_with_static_context_aes128gcm_sha256() { + let lst = get_ikm_lst_aes128gcm_sha256(); + let key_ctx = get_static_key_ctx(); + let data_ctx = DataContext::from(TEST_DATA_CTX); + let cb = CipherBox::new(&lst); + + // Encrypt + let res = cb.encrypt(&key_ctx, &data_ctx, TEST_DATA); + assert!(res.is_ok(), "res: {res:?}"); + let ciphertext = res.unwrap(); + assert!(ciphertext.starts_with("AQAAAA:")); + assert_eq!(ciphertext.len(), 82); + + // Decrypt + let res = cb.decrypt(&key_ctx, &data_ctx, &ciphertext); + assert!(res.is_ok(), "res: {res:?}"); + let plaintext = res.unwrap(); + assert_eq!(plaintext, TEST_DATA); + } + + #[test] + fn encrypt_decrypt_with_context_chacha20poly1305_blake3() { + let lst = get_ikm_lst_chacha20poly1305_blake3(); let key_ctx = KeyContext::from(TEST_KEY_CTX); let data_ctx = DataContext::from(TEST_DATA_CTX); let cb = CipherBox::new(&lst); @@ -164,6 +213,27 @@ mod tests { assert_eq!(plaintext, TEST_DATA); } + #[test] + fn encrypt_decrypt_with_context_aes128gcm_sha256() { + let lst = get_ikm_lst_aes128gcm_sha256(); + let key_ctx = KeyContext::from(TEST_KEY_CTX); + let data_ctx = DataContext::from(TEST_DATA_CTX); + let cb = CipherBox::new(&lst); + + // Encrypt + let res = cb.encrypt(&key_ctx, &data_ctx, TEST_DATA); + assert!(res.is_ok(), "res: {res:?}"); + let ciphertext = res.unwrap(); + assert!(ciphertext.starts_with("AQAAAA:")); + assert_eq!(ciphertext.len(), 94); + + // Decrypt + let res = cb.decrypt(&key_ctx, &data_ctx, &ciphertext); + assert!(res.is_ok(), "res: {res:?}"); + let plaintext = res.unwrap(); + assert_eq!(plaintext, TEST_DATA); + } + #[test] fn decrypt_invalid_ciphertext() { let tests = &[ @@ -176,7 +246,7 @@ mod tests { ("AQAAAA:W-nzcGkPU6eWj_JjjqLpQk6WSe_CIUPF:we_HR8yD3XnQ9aaJlZFvqPitnDlQHexw4QPaYaOTzpHSWNW86QQrLRRZOg", "missing time period"), ]; - let lst = get_ikm_lst(); + let lst = get_ikm_lst_chacha20poly1305_blake3(); let key_ctx = KeyContext::from(TEST_KEY_CTX); let data_ctx = DataContext::from(TEST_DATA_CTX); let cb = CipherBox::new(&lst); @@ -194,7 +264,7 @@ mod tests { #[test] fn invalid_context() { - let lst = get_ikm_lst(); + let lst = get_ikm_lst_chacha20poly1305_blake3(); let key_ctx = KeyContext::from(TEST_KEY_CTX); let data_ctx = DataContext::from(TEST_DATA_CTX); let cb = CipherBox::new(&lst); diff --git a/src/error.rs b/src/error.rs index 3151807..c96cd77 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,6 +8,8 @@ pub enum Error { IkmNoneAvailable, #[error("ikm error: {0}: input key material not found")] IkmNotFound(crate::ikm::IkmId), + #[error("encoded data: invalid nonce size: got {1} instead of {0}")] + InvalidNonceSize(usize, usize), #[error("parsing error: invalid base64-urlsafe-nopadding data: {0}")] ParsingBase64Error(base64ct::Error), #[error("parsing error: encoded data: empty nonce")] diff --git a/src/ikm.rs b/src/ikm.rs index 3ae2a11..07861ec 100644 --- a/src/ikm.rs +++ b/src/ikm.rs @@ -427,7 +427,7 @@ mod encryption { use super::*; #[test] - fn get_latest_ikm() { + fn get_latest_ikm_xchacha20poly1305_blake3() { let mut lst = InputKeyMaterialList::new(); let _ = lst.add_ikm(); let _ = lst.add_ikm(); @@ -443,6 +443,23 @@ mod encryption { assert_eq!(latest_ikm.content.len(), 32); } + #[test] + fn get_latest_ikm_aes128gcm_sha256() { + let mut lst = InputKeyMaterialList::new(); + let _ = lst.add_ikm(); + let _ = lst.add_ikm(); + let _ = lst.add_custom_ikm( + Scheme::Aes128GcmWithSha256, + Duration::from_secs(crate::DEFAULT_IKM_DURATION), + ); + let res = lst.get_latest_ikm(); + assert!(res.is_ok(), "res: {res:?}"); + let latest_ikm = res.unwrap(); + assert_eq!(latest_ikm.id, 3); + assert_eq!(latest_ikm.scheme, Scheme::Aes128GcmWithSha256); + assert_eq!(latest_ikm.content.len(), 32); + } + #[test] fn get_latest_ikm_empty() { let lst = InputKeyMaterialList::new(); diff --git a/src/scheme.rs b/src/scheme.rs index 73462ca..7df5976 100644 --- a/src/scheme.rs +++ b/src/scheme.rs @@ -6,9 +6,13 @@ use crate::error::Result; use crate::kdf::KdfFunction; use crate::Error; +#[cfg(feature = "encryption")] +mod aes; #[cfg(feature = "encryption")] mod blake3; #[cfg(feature = "encryption")] +mod sha2; +#[cfg(feature = "encryption")] mod xchacha20poly1305; #[cfg(feature = "encryption")] @@ -22,12 +26,14 @@ pub(crate) type SchemeSerializeType = u32; #[derive(Copy, Clone, Debug, PartialEq)] pub enum Scheme { XChaCha20Poly1305WithBlake3 = 1, + Aes128GcmWithSha256 = 2, } impl Scheme { pub(crate) fn get_ikm_size(&self) -> usize { match self { Scheme::XChaCha20Poly1305WithBlake3 => 32, + Scheme::Aes128GcmWithSha256 => 32, } } } @@ -37,6 +43,7 @@ impl Scheme { pub(crate) fn get_kdf(&self) -> Box { match self { Scheme::XChaCha20Poly1305WithBlake3 => Box::new(blake3::blake3_derive), + Scheme::Aes128GcmWithSha256 => Box::new(sha2::sha256_derive), } } @@ -45,6 +52,7 @@ impl Scheme { Scheme::XChaCha20Poly1305WithBlake3 => { Box::new(xchacha20poly1305::xchacha20poly1305_decrypt) } + Scheme::Aes128GcmWithSha256 => Box::new(aes::aes128gcm_decrypt), } } @@ -53,6 +61,7 @@ impl Scheme { Scheme::XChaCha20Poly1305WithBlake3 => { Box::new(xchacha20poly1305::xchacha20poly1305_encrypt) } + Scheme::Aes128GcmWithSha256 => Box::new(aes::aes128gcm_encrypt), } } @@ -61,6 +70,7 @@ impl Scheme { Scheme::XChaCha20Poly1305WithBlake3 => { Box::new(xchacha20poly1305::xchacha20poly1305_gen_nonce) } + Scheme::Aes128GcmWithSha256 => Box::new(aes::aes128gcm_gen_nonce), } } } @@ -71,6 +81,7 @@ impl TryFrom for Scheme { fn try_from(value: SchemeSerializeType) -> Result { match value { 1 => Ok(Scheme::XChaCha20Poly1305WithBlake3), + 2 => Ok(Scheme::Aes128GcmWithSha256), _ => Err(Error::ParsingSchemeUnknownScheme(value)), } } diff --git a/src/scheme/aes.rs b/src/scheme/aes.rs new file mode 100644 index 0000000..f3d90bf --- /dev/null +++ b/src/scheme/aes.rs @@ -0,0 +1,66 @@ +use crate::encrypted_data::EncryptedData; +use crate::error::{Error, Result}; +use aes_gcm::aead::{Aead, KeyInit, Payload}; +use aes_gcm::{Aes128Gcm, Key, Nonce}; + +// 96 bits (12 bytes) +const NONCE_SIZE: usize = 12; + +pub(crate) fn aes128gcm_gen_nonce() -> Result> { + let mut nonce: [u8; NONCE_SIZE] = [0; NONCE_SIZE]; + getrandom::getrandom(&mut nonce)?; + Ok(nonce.to_vec()) +} + +pub(crate) fn aes128gcm_encrypt( + key: &[u8], + nonce: &[u8], + data: &[u8], + aad: &str, +) -> Result { + // Adapt the key and nonce + let key = Key::::from_slice(key); + let nonce = Nonce::from_slice(&nonce[0..NONCE_SIZE]); + + // Prepare the payload + let payload = Payload { + msg: data, + aad: aad.as_bytes(), + }; + + // Encrypt the payload + let cipher = Aes128Gcm::new(key); + let ciphertext = cipher.encrypt(nonce, payload)?; + + // Return the result + Ok(EncryptedData { + nonce: nonce.to_vec(), + ciphertext, + }) +} + +pub(crate) fn aes128gcm_decrypt( + key: &[u8], + encrypted_data: &EncryptedData, + aad: &str, +) -> Result> { + // Adapt the key and nonce + let key = Key::::from_slice(key); + if encrypted_data.nonce.len() != NONCE_SIZE { + return Err(Error::InvalidNonceSize( + NONCE_SIZE, + encrypted_data.nonce.len(), + )); + } + let nonce = Nonce::from_slice(&encrypted_data.nonce[0..NONCE_SIZE]); + + // Prepare the payload + let payload = Payload { + msg: &encrypted_data.ciphertext, + aad: aad.as_bytes(), + }; + + // Decrypt the payload and return + let cipher = Aes128Gcm::new(key); + Ok(cipher.decrypt(nonce, payload)?) +} diff --git a/src/scheme/sha2.rs b/src/scheme/sha2.rs new file mode 100644 index 0000000..4a8c55b --- /dev/null +++ b/src/scheme/sha2.rs @@ -0,0 +1,23 @@ +use hkdf::Hkdf; +use sha2::Sha256; + +pub(crate) fn sha256_derive(context: &str, ikm: &[u8]) -> Vec { + let mut buff = [0u8; 16]; + let hkdf = Hkdf::::new(None, ikm); + hkdf.expand(context.as_bytes(), &mut buff).unwrap(); + buff.to_vec() +} + +#[cfg(test)] +mod tests { + #[test] + fn sha256_derive() { + assert_eq!( + super::sha256_derive("this is a context", b"7b47db8f365e5b602fd956d35985e9e1"), + vec![ + 0xad, 0xf2, 0xcd, 0x3a, 0x52, 0xfd, 0xf6, 0xad, 0x12, 0xce, 0xdd, 0x9a, 0x4d, 0x9e, + 0xcd, 0x4b, + ] + ); + } +}