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 = []
|
ikm-management = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
aes-gcm = { version = "0.10.3", default-features = false, features = ["std", "aes"] }
|
||||||
base64ct = { version = "1.6.0", default-features = false, features = ["std"] }
|
base64ct = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||||
blake3 = { version = "1.5.0", default-features = false }
|
blake3 = { version = "1.5.0", default-features = false }
|
||||||
chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["std"] }
|
chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["std"] }
|
||||||
getrandom = { version = "0.2.12", default-features = false }
|
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 }
|
thiserror = { version = "1.0.57", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -94,16 +94,23 @@ mod tests {
|
||||||
ctx
|
ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_ikm_lst() -> InputKeyMaterialList {
|
fn get_ikm_lst_chacha20poly1305_blake3() -> InputKeyMaterialList {
|
||||||
InputKeyMaterialList::import(
|
InputKeyMaterialList::import(
|
||||||
"AQAAAA:AQAAAAEAAAC_vYEw1ujVG5i-CtoPYSzik_6xaAq59odjPm5ij01-e6zz4mUAAAAALJGBiwAAAAAA",
|
"AQAAAA:AQAAAAEAAAC_vYEw1ujVG5i-CtoPYSzik_6xaAq59odjPm5ij01-e6zz4mUAAAAALJGBiwAAAAAA",
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_ikm_lst_aes128gcm_sha256() -> InputKeyMaterialList {
|
||||||
|
InputKeyMaterialList::import(
|
||||||
|
"AQAAAA:AQAAAAIAAAA2lXqTSduZ22J0LiwEhmENjB6pLo0GVKvAQYocJcAAp1f8_2UAAAAAuzDPeAAAAAAA",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn encrypt_decrypt_no_context() {
|
fn encrypt_decrypt_no_context_chacha20poly1305_blake3() {
|
||||||
let lst = get_ikm_lst();
|
let lst = get_ikm_lst_chacha20poly1305_blake3();
|
||||||
let key_ctx = get_static_empty_key_ctx();
|
let key_ctx = get_static_empty_key_ctx();
|
||||||
let data_ctx = DataContext::from([]);
|
let data_ctx = DataContext::from([]);
|
||||||
let cb = CipherBox::new(&lst);
|
let cb = CipherBox::new(&lst);
|
||||||
|
@ -123,8 +130,29 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn encrypt_decrypt_with_static_context() {
|
fn encrypt_decrypt_no_context_aes128gcm_sha256() {
|
||||||
let lst = get_ikm_lst();
|
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 key_ctx = get_static_key_ctx();
|
||||||
let data_ctx = DataContext::from(TEST_DATA_CTX);
|
let data_ctx = DataContext::from(TEST_DATA_CTX);
|
||||||
let cb = CipherBox::new(&lst);
|
let cb = CipherBox::new(&lst);
|
||||||
|
@ -144,8 +172,29 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn encrypt_decrypt_with_context() {
|
fn encrypt_decrypt_with_static_context_aes128gcm_sha256() {
|
||||||
let lst = get_ikm_lst();
|
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 key_ctx = KeyContext::from(TEST_KEY_CTX);
|
||||||
let data_ctx = DataContext::from(TEST_DATA_CTX);
|
let data_ctx = DataContext::from(TEST_DATA_CTX);
|
||||||
let cb = CipherBox::new(&lst);
|
let cb = CipherBox::new(&lst);
|
||||||
|
@ -164,6 +213,27 @@ mod tests {
|
||||||
assert_eq!(plaintext, TEST_DATA);
|
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]
|
#[test]
|
||||||
fn decrypt_invalid_ciphertext() {
|
fn decrypt_invalid_ciphertext() {
|
||||||
let tests = &[
|
let tests = &[
|
||||||
|
@ -176,7 +246,7 @@ mod tests {
|
||||||
("AQAAAA:W-nzcGkPU6eWj_JjjqLpQk6WSe_CIUPF:we_HR8yD3XnQ9aaJlZFvqPitnDlQHexw4QPaYaOTzpHSWNW86QQrLRRZOg", "missing time period"),
|
("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 key_ctx = KeyContext::from(TEST_KEY_CTX);
|
||||||
let data_ctx = DataContext::from(TEST_DATA_CTX);
|
let data_ctx = DataContext::from(TEST_DATA_CTX);
|
||||||
let cb = CipherBox::new(&lst);
|
let cb = CipherBox::new(&lst);
|
||||||
|
@ -194,7 +264,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_context() {
|
fn invalid_context() {
|
||||||
let lst = get_ikm_lst();
|
let lst = get_ikm_lst_chacha20poly1305_blake3();
|
||||||
let key_ctx = KeyContext::from(TEST_KEY_CTX);
|
let key_ctx = KeyContext::from(TEST_KEY_CTX);
|
||||||
let data_ctx = DataContext::from(TEST_DATA_CTX);
|
let data_ctx = DataContext::from(TEST_DATA_CTX);
|
||||||
let cb = CipherBox::new(&lst);
|
let cb = CipherBox::new(&lst);
|
||||||
|
|
|
@ -8,6 +8,8 @@ pub enum Error {
|
||||||
IkmNoneAvailable,
|
IkmNoneAvailable,
|
||||||
#[error("ikm error: {0}: input key material not found")]
|
#[error("ikm error: {0}: input key material not found")]
|
||||||
IkmNotFound(crate::ikm::IkmId),
|
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}")]
|
#[error("parsing error: invalid base64-urlsafe-nopadding data: {0}")]
|
||||||
ParsingBase64Error(base64ct::Error),
|
ParsingBase64Error(base64ct::Error),
|
||||||
#[error("parsing error: encoded data: empty nonce")]
|
#[error("parsing error: encoded data: empty nonce")]
|
||||||
|
|
19
src/ikm.rs
19
src/ikm.rs
|
@ -427,7 +427,7 @@ mod encryption {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_latest_ikm() {
|
fn get_latest_ikm_xchacha20poly1305_blake3() {
|
||||||
let mut lst = InputKeyMaterialList::new();
|
let mut lst = InputKeyMaterialList::new();
|
||||||
let _ = lst.add_ikm();
|
let _ = lst.add_ikm();
|
||||||
let _ = lst.add_ikm();
|
let _ = lst.add_ikm();
|
||||||
|
@ -443,6 +443,23 @@ mod encryption {
|
||||||
assert_eq!(latest_ikm.content.len(), 32);
|
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]
|
#[test]
|
||||||
fn get_latest_ikm_empty() {
|
fn get_latest_ikm_empty() {
|
||||||
let lst = InputKeyMaterialList::new();
|
let lst = InputKeyMaterialList::new();
|
||||||
|
|
|
@ -6,9 +6,13 @@ use crate::error::Result;
|
||||||
use crate::kdf::KdfFunction;
|
use crate::kdf::KdfFunction;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
|
#[cfg(feature = "encryption")]
|
||||||
|
mod aes;
|
||||||
#[cfg(feature = "encryption")]
|
#[cfg(feature = "encryption")]
|
||||||
mod blake3;
|
mod blake3;
|
||||||
#[cfg(feature = "encryption")]
|
#[cfg(feature = "encryption")]
|
||||||
|
mod sha2;
|
||||||
|
#[cfg(feature = "encryption")]
|
||||||
mod xchacha20poly1305;
|
mod xchacha20poly1305;
|
||||||
|
|
||||||
#[cfg(feature = "encryption")]
|
#[cfg(feature = "encryption")]
|
||||||
|
@ -22,12 +26,14 @@ pub(crate) type SchemeSerializeType = u32;
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub enum Scheme {
|
pub enum Scheme {
|
||||||
XChaCha20Poly1305WithBlake3 = 1,
|
XChaCha20Poly1305WithBlake3 = 1,
|
||||||
|
Aes128GcmWithSha256 = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scheme {
|
impl Scheme {
|
||||||
pub(crate) fn get_ikm_size(&self) -> usize {
|
pub(crate) fn get_ikm_size(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Scheme::XChaCha20Poly1305WithBlake3 => 32,
|
Scheme::XChaCha20Poly1305WithBlake3 => 32,
|
||||||
|
Scheme::Aes128GcmWithSha256 => 32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +43,7 @@ impl Scheme {
|
||||||
pub(crate) fn get_kdf(&self) -> Box<KdfFunction> {
|
pub(crate) fn get_kdf(&self) -> Box<KdfFunction> {
|
||||||
match self {
|
match self {
|
||||||
Scheme::XChaCha20Poly1305WithBlake3 => Box::new(blake3::blake3_derive),
|
Scheme::XChaCha20Poly1305WithBlake3 => Box::new(blake3::blake3_derive),
|
||||||
|
Scheme::Aes128GcmWithSha256 => Box::new(sha2::sha256_derive),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +52,7 @@ impl Scheme {
|
||||||
Scheme::XChaCha20Poly1305WithBlake3 => {
|
Scheme::XChaCha20Poly1305WithBlake3 => {
|
||||||
Box::new(xchacha20poly1305::xchacha20poly1305_decrypt)
|
Box::new(xchacha20poly1305::xchacha20poly1305_decrypt)
|
||||||
}
|
}
|
||||||
|
Scheme::Aes128GcmWithSha256 => Box::new(aes::aes128gcm_decrypt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +61,7 @@ impl Scheme {
|
||||||
Scheme::XChaCha20Poly1305WithBlake3 => {
|
Scheme::XChaCha20Poly1305WithBlake3 => {
|
||||||
Box::new(xchacha20poly1305::xchacha20poly1305_encrypt)
|
Box::new(xchacha20poly1305::xchacha20poly1305_encrypt)
|
||||||
}
|
}
|
||||||
|
Scheme::Aes128GcmWithSha256 => Box::new(aes::aes128gcm_encrypt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +70,7 @@ impl Scheme {
|
||||||
Scheme::XChaCha20Poly1305WithBlake3 => {
|
Scheme::XChaCha20Poly1305WithBlake3 => {
|
||||||
Box::new(xchacha20poly1305::xchacha20poly1305_gen_nonce)
|
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> {
|
fn try_from(value: SchemeSerializeType) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
1 => Ok(Scheme::XChaCha20Poly1305WithBlake3),
|
1 => Ok(Scheme::XChaCha20Poly1305WithBlake3),
|
||||||
|
2 => Ok(Scheme::Aes128GcmWithSha256),
|
||||||
_ => Err(Error::ParsingSchemeUnknownScheme(value)),
|
_ => 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