diff --git a/Cargo.toml b/Cargo.toml index 89cd406..b3100d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,5 +20,6 @@ i-understand-and-accept-the-risks = [] [dependencies] 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 } thiserror = { version = "1.0.57", default-features = false } diff --git a/src/canonicalization.rs b/src/canonicalization.rs index 01c4649..b28984a 100644 --- a/src/canonicalization.rs +++ b/src/canonicalization.rs @@ -3,17 +3,22 @@ use base64ct::{Base64UrlUnpadded, Encoding}; const CANONICALIZATION_BUFFER_SIZE: usize = 1024; const CANONICALIZATION_SEPARATOR: &str = ":"; -pub(crate) fn canonicalize(key_context: &[&str]) -> String { - match key_context.len() { +#[inline] +pub(crate) fn join_canonicalized_str(s1: &str, s2: &str) -> String { + format!("{s1}{CANONICALIZATION_SEPARATOR}{s2}") +} + +pub(crate) fn canonicalize(context: &[impl AsRef<[u8]>]) -> String { + match context.len() { 0 => String::new(), - 1 => key_context[0].to_string(), + 1 => Base64UrlUnpadded::encode_string(context[0].as_ref()), _ => { let mut ret = String::with_capacity(CANONICALIZATION_BUFFER_SIZE); - for (i, ctx_elem) in key_context.iter().enumerate() { + for (i, ctx_elem) in context.iter().enumerate() { if i != 0 { ret += CANONICALIZATION_SEPARATOR; } - ret += &Base64UrlUnpadded::encode_string(ctx_elem.as_bytes()); + ret += &Base64UrlUnpadded::encode_string(ctx_elem.as_ref()); } ret } @@ -24,16 +29,18 @@ pub(crate) fn canonicalize(key_context: &[&str]) -> String { mod tests { use super::*; + const EMPTY_CTX: &[[u8; 0]] = &[]; + #[test] fn canonicalize_empty() { - let canon = canonicalize(&[]); + let canon = canonicalize(EMPTY_CTX); assert_eq!(canon, String::new()); } #[test] fn canonicalize_one() { let canon = canonicalize(&["test"]); - assert_eq!(&canon, "test"); + assert_eq!(&canon, "dGVzdA"); } #[test] @@ -41,4 +48,17 @@ mod tests { let canon = canonicalize(&["test", "bis", "ter", ""]); assert_eq!(&canon, "dGVzdA:Ymlz:dGVy:"); } + + #[test] + fn test_join_canonicalized_empty() { + assert_eq!(join_canonicalized_str("", ""), ":"); + } + + #[test] + fn test_join_canonicalized_with_data() { + assert_eq!( + join_canonicalized_str("QWO7RGDt:f-JmDPvU", "_Sfx61Fp"), + "QWO7RGDt:f-JmDPvU:_Sfx61Fp" + ); + } } diff --git a/src/encryption.rs b/src/encryption.rs index de5a904..21bcd6a 100644 --- a/src/encryption.rs +++ b/src/encryption.rs @@ -1,5 +1,15 @@ +use crate::canonicalization::{canonicalize, join_canonicalized_str}; use crate::kdf::derive_key; -use crate::{Error, InputKeyMaterialList}; +use crate::{storage, Error, InputKeyMaterialList}; +use chacha20poly1305::aead::{Aead, KeyInit, Payload}; +use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; + +pub(crate) type EncryptionFunction = dyn Fn(&[u8], &[u8], &str) -> Result; + +pub(crate) struct EncryptedData { + pub(crate) nonce: Vec, + pub(crate) ciphertext: Vec, +} pub fn encrypt( ikml: &InputKeyMaterialList, @@ -7,9 +17,51 @@ pub fn encrypt( data: impl AsRef<[u8]>, data_context: &[impl AsRef<[u8]>], ) -> Result { + // Derive the key let ikm = ikml.get_latest_ikm()?; let key = derive_key(ikm, key_context); - unimplemented!("encrypt"); + + // Generate the AAD + let key_context_canon = canonicalize(key_context); + let data_context_canon = canonicalize(data_context); + let aad = join_canonicalized_str(&key_context_canon, &data_context_canon); + + // Encrypt + let encryption_function = ikm.scheme.get_encryption(); + let encrypted_data = encryption_function(&key, data.as_ref(), &aad)?; + + // Encode + Ok(storage::encode(ikm.id, &encrypted_data)) +} + +pub(crate) fn xchacha20poly1305_encrypt( + key: &[u8], + data: &[u8], + aad: &str, +) -> Result { + // Adapt the key + let key = Key::from_slice(key); + + // Generate a nonce + let mut nonce: [u8; 24] = [0; 24]; + getrandom::getrandom(&mut nonce)?; + let nonce = XNonce::from_slice(&nonce); + + // Prepare the payload + let payload = Payload { + msg: data, + aad: aad.as_bytes(), + }; + + // Encrypt the payload + let cipher = XChaCha20Poly1305::new(key); + let ciphertext = cipher.encrypt(nonce, payload)?; + + // Return the result + Ok(EncryptedData { + nonce: nonce.to_vec(), + ciphertext, + }) } pub fn decrypt( @@ -20,3 +72,44 @@ pub fn decrypt( ) -> Result, Error> { unimplemented!("decrypt"); } + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_DATA: &[u8] = b"Lorem ipsum dolor sit amet."; + const TEST_KEY_CTX: &[&str] = &["db_name", "table_name", "column_name"]; + const TEST_DATA_CTX: &[&str] = &["018db876-3d9d-79af-9460-55d17da991d8"]; + const EMPTY_DATA_CTX: &[[u8; 0]] = &[]; + + fn get_ikm_lst() -> InputKeyMaterialList { + InputKeyMaterialList::import( + "AQAAAAEAAAABAAAANGFtbdYEN0s7dzCfMm7dYeQWD64GdmuKsYSiKwppAhmkz81lAAAAACQDr2cAAAAAAA", + ) + .unwrap() + } + + #[test] + fn encrypt_decrypt_no_context() { + let lst = get_ikm_lst(); + let res = encrypt(&lst, &[], TEST_DATA, EMPTY_DATA_CTX); + assert!(res.is_ok()); + let ciphertext = res.unwrap(); + assert!(ciphertext.starts_with("AQAAAA:")); + assert_eq!(ciphertext.len(), 98); + + // TODO: decrypt + } + + #[test] + fn encrypt_decrypt_with_context() { + let lst = get_ikm_lst(); + let res = encrypt(&lst, TEST_KEY_CTX, TEST_DATA, TEST_DATA_CTX); + assert!(res.is_ok()); + let ciphertext = res.unwrap(); + assert!(ciphertext.starts_with("AQAAAA:")); + assert_eq!(ciphertext.len(), 98); + + // TODO: decrypt + } +} diff --git a/src/error.rs b/src/error.rs index d9ae575..0e00aef 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,6 +2,8 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum Error { + #[error("cipher error: {0}")] + ChaCha20Poly1305Error(chacha20poly1305::Error), #[error("ikm error: no input key material available")] IkmNoneAvailable, #[error("ikm error: {0}: input key material not found")] @@ -26,6 +28,12 @@ impl From for Error { } } +impl From for Error { + fn from(error: chacha20poly1305::Error) -> Self { + Error::ChaCha20Poly1305Error(error) + } +} + impl From for Error { fn from(error: getrandom::Error) -> Self { Error::RandomSourceError(error) diff --git a/src/lib.rs b/src/lib.rs index 1c5cbec..69bd7b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,8 @@ mod ikm; mod kdf; #[cfg(any(feature = "encryption", feature = "ikm-management"))] mod scheme; +#[cfg(feature = "encryption")] +mod storage; #[cfg(feature = "encryption")] pub use encryption::{decrypt, encrypt}; diff --git a/src/scheme.rs b/src/scheme.rs index f0eaefc..b541551 100644 --- a/src/scheme.rs +++ b/src/scheme.rs @@ -1,3 +1,4 @@ +use crate::encryption::EncryptionFunction; use crate::kdf::KdfFunction; use crate::Error; @@ -12,6 +13,14 @@ impl Scheme { Scheme::XChaCha20Poly1305WithBlake3 => Box::new(crate::kdf::blake3_derive), } } + + pub(crate) fn get_encryption(&self) -> Box { + match self { + Scheme::XChaCha20Poly1305WithBlake3 => { + Box::new(crate::encryption::xchacha20poly1305_encrypt) + } + } + } } impl TryFrom for Scheme { diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..19874fe --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,42 @@ +use crate::encryption::EncryptedData; +use crate::Error; +use base64ct::{Base64UrlUnpadded, Encoding}; + +const STORAGE_SEPARATOR: &str = ":"; + +#[inline] +fn encode_data(data: &[u8]) -> String { + Base64UrlUnpadded::encode_string(data) +} + +pub(crate) fn encode(ikm_id: u32, encrypted_data: &EncryptedData) -> String { + let mut ret = String::new(); + ret += &encode_data(&ikm_id.to_le_bytes()); + ret += STORAGE_SEPARATOR; + ret += &encode_data(&encrypted_data.nonce); + ret += STORAGE_SEPARATOR; + ret += &encode_data(&encrypted_data.ciphertext); + ret +} + +#[cfg(test)] +mod tests { + use crate::storage::EncryptedData; + + #[test] + fn encode() { + let data = EncryptedData { + nonce: vec![ + 0x6b, 0x94, 0xa9, 0x8c, 0x0a, 0x2a, 0x86, 0xfb, 0x88, 0xf6, 0x7d, 0xc6, 0x3e, 0x10, + 0xca, 0xba, 0x8b, 0x6a, 0xa0, 0xb6, 0xdf, 0xef, 0xf1, 0x5b, + ], + ciphertext: vec![ + 0x4c, 0x8d, 0xb8, 0x5a, 0xbf, 0xe0, 0xf9, 0x95, 0x7b, 0xfd, 0x7d, 0x68, 0x1e, 0xa5, + 0x4a, 0x6a, 0x4f, 0x62, 0x46, 0x54, 0x12, 0x9d, 0xe6, 0x15, 0x38, 0xc5, 0x81, 0xfb, + 0x72, 0xe9, 0xfa, 0x11, 0x47, 0x29, 0xfc, 0x5f, 0x9d, 0x8f, 0xb3, 0x47, 0xf6, 0xcd, + ], + }; + let s = super::encode(42, &data); + assert_eq!(&s, "KgAAAA:a5SpjAoqhvuI9n3GPhDKuotqoLbf7_Fb:TI24Wr_g-ZV7_X1oHqVKak9iRlQSneYVOMWB-3Lp-hFHKfxfnY-zR_bN"); + } +}