Add support for AES128-GCM using HKDF-SHA256 to derive keys
This commit is contained in:
parent
56db45cbad
commit
ca86747862
7 changed files with 202 additions and 10 deletions
|
@ -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]
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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")]
|
||||
|
|
19
src/ikm.rs
19
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();
|
||||
|
|
|
@ -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
66
src/scheme/aes.rs
Normal 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
23
src/scheme/sha2.rs
Normal 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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue