Add support for AES128-GCM using HKDF-SHA256 to derive keys

This commit is contained in:
Rodolphe Bréard 2024-03-24 12:16:54 +01:00
parent 56db45cbad
commit ca86747862
7 changed files with 202 additions and 10 deletions

View file

@ -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]

View file

@ -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);

View file

@ -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")]

View file

@ -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();

View file

@ -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<KdfFunction> {
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<SchemeSerializeType> for Scheme {
fn try_from(value: SchemeSerializeType) -> Result<Self, Self::Error> {
match value {
1 => Ok(Scheme::XChaCha20Poly1305WithBlake3),
2 => Ok(Scheme::Aes128GcmWithSha256),
_ => Err(Error::ParsingSchemeUnknownScheme(value)),
}
}

66
src/scheme/aes.rs Normal file
View file

@ -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<Vec<u8>> {
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<EncryptedData> {
// Adapt the key and nonce
let key = Key::<Aes128Gcm>::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<Vec<u8>> {
// Adapt the key and nonce
let key = Key::<Aes128Gcm>::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)?)
}

23
src/scheme/sha2.rs Normal file
View file

@ -0,0 +1,23 @@
use hkdf::Hkdf;
use sha2::Sha256;
pub(crate) fn sha256_derive(context: &str, ikm: &[u8]) -> Vec<u8> {
let mut buff = [0u8; 16];
let hkdf = Hkdf::<Sha256>::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,
]
);
}
}