diff --git a/Cargo.lock b/Cargo.lock index d857092..99d1c82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,21 @@ dependencies = [ "subtle", ] +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -158,9 +173,10 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cas-lib" -version = "0.2.84" +version = "0.2.85" dependencies = [ "aes-gcm", + "aes-gcm-siv", "argon2", "ascon-aead", "bcrypt", diff --git a/Cargo.toml b/Cargo.toml index acfc03a..0084d1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cas-lib" -version = "0.2.84" +version = "0.2.85" edition = "2021" description = "A function wrapper layer for RustCrypto and Dalek-Cryptography. Intended to be used in FFI situations with a global heap deallactor at the top level project." license = "Apache-2.0" @@ -13,6 +13,7 @@ crate-type = ["lib"] [dependencies] aes-gcm = "0.10.3" +aes-gcm-siv = "0.11.1" argon2 = "0.5.2" bcrypt = "0.18.0" blake2 = "0.10.6" diff --git a/src/lib.rs b/src/lib.rs index 4525d03..4118e98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ pub mod key_exchange { pub mod symmetric { pub mod aes; + pub mod aes_gcm_siv; pub mod chacha20poly1305; pub mod cas_symmetric_encryption; } diff --git a/src/symmetric/aes_gcm_siv.rs b/src/symmetric/aes_gcm_siv.rs new file mode 100644 index 0000000..9d41125 --- /dev/null +++ b/src/symmetric/aes_gcm_siv.rs @@ -0,0 +1,154 @@ +use aes_gcm_siv::{ + aead::{Aead, KeyInit}, + Aes128GcmSiv, Aes256GcmSiv, Key, Nonce, +}; +use hkdf::Hkdf; +use rand::{rngs::OsRng, RngCore}; +use sha2::Sha256; + +use crate::error::{CasError, CasResult}; + +use super::cas_symmetric_encryption::{CASAES128SIVEncryption, CASAES256SIVEncryption}; + +const AES_SIV_NONCE_LEN: usize = 12; +const AES128_KEY_LEN: usize = 16; +const AES256_KEY_LEN: usize = 32; + +pub struct CASAES128SIV; +pub struct CASAES256SIV; + +impl CASAES256SIVEncryption for CASAES256SIV { + + /// Generates an AES-256-GCM-SIV key from a vector. + /// Returns an error if the input is not 32 bytes long. + fn key_from_vec(key_slice: Vec) -> CasResult> { + if key_slice.len() != AES256_KEY_LEN { + return Err(CasError::InvalidKey); + } + let key = Key::::from_slice(key_slice.as_slice()); + Ok(key.to_vec()) + } + + /// Generates an AES-256-GCM-SIV 32-byte key + fn generate_key() -> Vec { + let mut os_rng = OsRng; + Aes256GcmSiv::generate_key(&mut os_rng).to_vec() + } + + /// Encrypts with AES-256-GCM-SIV taking an aes_key and nonce + fn encrypt_plaintext(aes_key: Vec, nonce: Vec, plaintext: Vec) -> CasResult> { + if aes_key.len() != AES256_KEY_LEN { + return Err(CasError::InvalidKey); + } + if nonce.len() != AES_SIV_NONCE_LEN { + return Err(CasError::InvalidNonce); + } + let key = Key::::from_slice(aes_key.as_slice()); + let cipher = Aes256GcmSiv::new(key); + let nonce = Nonce::from_slice(nonce.as_slice()); + cipher + .encrypt(nonce, plaintext.as_ref()) + .map_err(|_| CasError::EncryptionFailed) + } + + /// Decrypts with AES-256-GCM-SIV taking an aes_key and nonce + fn decrypt_ciphertext(aes_key: Vec, nonce: Vec, ciphertext: Vec) -> CasResult> { + if aes_key.len() != AES256_KEY_LEN { + return Err(CasError::InvalidKey); + } + if nonce.len() != AES_SIV_NONCE_LEN { + return Err(CasError::InvalidNonce); + } + let key = Key::::from_slice(aes_key.as_slice()); + let cipher = Aes256GcmSiv::new(key); + let nonce = Nonce::from_slice(nonce.as_slice()); + cipher + .decrypt(nonce, ciphertext.as_ref()) + .map_err(|_| CasError::DecryptionFailed) + } + + /// Creates an AES-256-GCM-SIV 32-byte key from an X25519 shared secret + fn key_from_x25519_shared_secret(shared_secret: Vec) -> CasResult> { + let hk = Hkdf::::new(None, &shared_secret); + let mut aes_key: Box<[u8; 32]> = Box::new([0u8; 32]); + hk.expand(b"aes key", &mut *aes_key) + .map_err(|_| CasError::KeyGenerationFailed)?; + Ok(aes_key.to_vec()) + } + + /// Generates an AES-GCM-SIV nonce + fn generate_nonce() -> Vec { + let mut os_rng = OsRng; + let mut nonce = [0u8; AES_SIV_NONCE_LEN]; + os_rng.fill_bytes(&mut nonce); + nonce.to_vec() + } +} + +impl CASAES128SIVEncryption for CASAES128SIV { + + /// Generates an AES-128-GCM-SIV key from a vector. + /// Returns an error if the input is not 16 bytes long. + fn key_from_vec(key_slice: Vec) -> CasResult> { + if key_slice.len() != AES128_KEY_LEN { + return Err(CasError::InvalidKey); + } + let key = Key::::from_slice(key_slice.as_slice()); + Ok(key.to_vec()) + } + + /// Generates an AES-128-GCM-SIV 16-byte key + fn generate_key() -> Vec { + let mut os_rng = OsRng; + Aes128GcmSiv::generate_key(&mut os_rng).to_vec() + } + + /// Encrypts with AES-128-GCM-SIV taking an aes_key and nonce + fn encrypt_plaintext(aes_key: Vec, nonce: Vec, plaintext: Vec) -> CasResult> { + if aes_key.len() != AES128_KEY_LEN { + return Err(CasError::InvalidKey); + } + if nonce.len() != AES_SIV_NONCE_LEN { + return Err(CasError::InvalidNonce); + } + let key = Key::::from_slice(aes_key.as_slice()); + let cipher = Aes128GcmSiv::new(key); + let nonce = Nonce::from_slice(nonce.as_slice()); + cipher + .encrypt(nonce, plaintext.as_ref()) + .map_err(|_| CasError::EncryptionFailed) + } + + /// Decrypts with AES-128-GCM-SIV taking an aes_key and nonce + fn decrypt_ciphertext(aes_key: Vec, nonce: Vec, ciphertext: Vec) -> CasResult> { + if aes_key.len() != AES128_KEY_LEN { + return Err(CasError::InvalidKey); + } + if nonce.len() != AES_SIV_NONCE_LEN { + return Err(CasError::InvalidNonce); + } + let key = Key::::from_slice(aes_key.as_slice()); + let cipher = Aes128GcmSiv::new(key); + let nonce = Nonce::from_slice(nonce.as_slice()); + cipher + .decrypt(nonce, ciphertext.as_ref()) + .map_err(|_| CasError::DecryptionFailed) + } + + /// Creates an AES-128-GCM-SIV 16-byte key from an X25519 shared secret + fn key_from_x25519_shared_secret(shared_secret: Vec) -> CasResult> { + let hk = Hkdf::::new(None, &shared_secret); + let mut aes_key = Box::new([0u8; 16]); + hk.expand(b"aes key", &mut *aes_key) + .map_err(|_| CasError::KeyGenerationFailed)?; + Ok(aes_key.to_vec()) + } + + /// Generates an AES-GCM-SIV nonce + fn generate_nonce() -> Vec { + let mut os_rng = OsRng; + let mut nonce = [0u8; AES_SIV_NONCE_LEN]; + os_rng.fill_bytes(&mut nonce); + nonce.to_vec() + } +} diff --git a/src/symmetric/cas_symmetric_encryption.rs b/src/symmetric/cas_symmetric_encryption.rs index 50262d4..b2d75ab 100644 --- a/src/symmetric/cas_symmetric_encryption.rs +++ b/src/symmetric/cas_symmetric_encryption.rs @@ -18,6 +18,24 @@ pub trait CASAES128Encryption { fn key_from_vec(key_slice: Vec) -> CasResult>; } +pub trait CASAES256SIVEncryption { + fn generate_key() -> Vec; + fn encrypt_plaintext(aes_key: Vec, nonce: Vec, plaintext: Vec) -> CasResult>; + fn decrypt_ciphertext(aes_key: Vec, nonce: Vec, ciphertext: Vec) -> CasResult>; + fn key_from_x25519_shared_secret(shared_secret: Vec) -> CasResult>; + fn generate_nonce() -> Vec; + fn key_from_vec(key_slice: Vec) -> CasResult>; +} + +pub trait CASAES128SIVEncryption { + fn generate_key() -> Vec; + fn encrypt_plaintext(aes_key: Vec, nonce: Vec, plaintext: Vec) -> CasResult>; + fn decrypt_ciphertext(aes_key: Vec, nonce: Vec, ciphertext: Vec) -> CasResult>; + fn key_from_x25519_shared_secret(shared_secret: Vec) -> CasResult>; + fn generate_nonce() -> Vec; + fn key_from_vec(key_slice: Vec) -> CasResult>; +} + pub trait Chacha20Poly1305Encryption { fn generate_key() -> Vec; fn encrypt_plaintext(aes_key: Vec, nonce: Vec, plaintext: Vec) -> CasResult>; diff --git a/tests/aes_gcm_siv.rs b/tests/aes_gcm_siv.rs new file mode 100644 index 0000000..b522081 --- /dev/null +++ b/tests/aes_gcm_siv.rs @@ -0,0 +1,293 @@ +use std::{fs::File, io::Write, path::Path}; + +use cas_lib::symmetric::{ + aes_gcm_siv::{CASAES128SIV, CASAES256SIV}, + cas_symmetric_encryption::{CASAES128SIVEncryption, CASAES256SIVEncryption}, +}; + +mod common; + +#[cfg(test)] +mod aes_gcm_siv { + use crate::common::temp_output_path; + use cas_lib::key_exchange::{cas_key_exchange::CASKeyExchange, x25519::X25519}; + + use super::*; + + fn decode_hex(hex: &str) -> Vec { + assert_eq!(hex.len() % 2, 0, "hex input must have an even length"); + + hex.as_bytes() + .chunks(2) + .map(|chunk| { + let byte = std::str::from_utf8(chunk).unwrap(); + u8::from_str_radix(byte, 16).unwrap() + }) + .collect() + } + + /// Known-answer test vectors from RFC 8452 Appendix C. + /// Each result is the ciphertext with the 16-byte authentication tag appended, + /// which matches the output of `encrypt_plaintext`. All cases use empty AAD. + struct SivKatCase { + key: &'static str, + nonce: &'static str, + plaintext: &'static str, + result: &'static str, + } + + // RFC 8452 Appendix C.1 — AES-128-GCM-SIV + const AES128_GCM_SIV_KAT: &[SivKatCase] = &[ + SivKatCase { + key: "01000000000000000000000000000000", + nonce: "030000000000000000000000", + plaintext: "", + result: "dc20e2d83f25705bb49e439eca56de25", + }, + SivKatCase { + key: "01000000000000000000000000000000", + nonce: "030000000000000000000000", + plaintext: "0100000000000000", + result: "b5d839330ac7b786578782fff6013b815b287c22493a364c", + }, + SivKatCase { + key: "01000000000000000000000000000000", + nonce: "030000000000000000000000", + plaintext: "010000000000000000000000", + result: "7323ea61d05932260047d942a4978db357391a0bc4fdec8b0d106639", + }, + SivKatCase { + key: "01000000000000000000000000000000", + nonce: "030000000000000000000000", + plaintext: "01000000000000000000000000000000", + result: "743f7c8077ab25f8624e2e948579cf77303aaf90f6fe21199c6068577437a0c4", + }, + SivKatCase { + key: "01000000000000000000000000000000", + nonce: "030000000000000000000000", + plaintext: "0100000000000000000000000000000002000000000000000000000000000000", + result: "84e07e62ba83a6585417245d7ec413a9fe427d6315c09b57ce45f2e3936a94451a8e45dcd4578c667cd86847bf6155ff", + }, + ]; + + // RFC 8452 Appendix C.2 — AES-256-GCM-SIV + const AES256_GCM_SIV_KAT: &[SivKatCase] = &[ + SivKatCase { + key: "0100000000000000000000000000000000000000000000000000000000000000", + nonce: "030000000000000000000000", + plaintext: "", + result: "07f5f4169bbf55a8400cd47ea6fd400f", + }, + SivKatCase { + key: "0100000000000000000000000000000000000000000000000000000000000000", + nonce: "030000000000000000000000", + plaintext: "0100000000000000", + result: "c2ef328e5c71c83b843122130f7364b761e0b97427e3df28", + }, + SivKatCase { + key: "0100000000000000000000000000000000000000000000000000000000000000", + nonce: "030000000000000000000000", + plaintext: "010000000000000000000000", + result: "9aab2aeb3faa0a34aea8e2b18ca50da9ae6559e48fd10f6e5c9ca17e", + }, + SivKatCase { + key: "0100000000000000000000000000000000000000000000000000000000000000", + nonce: "030000000000000000000000", + plaintext: "01000000000000000000000000000000", + result: "85a01b63025ba19b7fd3ddfc033b3e76c9eac6fa700942702e90862383c6c366", + }, + ]; + + #[test] + fn test_aes_256_gcm_siv_round_trip() { + let path = Path::new("tests/test.docx"); + let file_bytes: Vec = std::fs::read(path).unwrap(); + let nonce = ::generate_nonce(); + let key = ::generate_key(); + let encrypted_bytes = ::encrypt_plaintext( + key.clone(), + nonce.clone(), + file_bytes.clone(), + ) + .unwrap(); + let mut file = File::create(temp_output_path("encrypted.docx")).unwrap(); + file.write_all(&encrypted_bytes).unwrap(); + + let decrypted_bytes = ::decrypt_ciphertext( + key, + nonce, + encrypted_bytes, + ) + .unwrap(); + let mut file = File::create(temp_output_path("decrypted.docx")).unwrap(); + file.write_all(&decrypted_bytes).unwrap(); + assert_eq!(file_bytes, decrypted_bytes); + } + + #[test] + fn test_aes_128_gcm_siv_round_trip() { + let path = Path::new("tests/test.docx"); + let file_bytes: Vec = std::fs::read(path).unwrap(); + let nonce = ::generate_nonce(); + let key = ::generate_key(); + let encrypted_bytes = ::encrypt_plaintext( + key.clone(), + nonce.clone(), + file_bytes.clone(), + ) + .unwrap(); + let mut file = File::create(temp_output_path("encrypted.docx")).unwrap(); + file.write_all(&encrypted_bytes).unwrap(); + + let decrypted_bytes = ::decrypt_ciphertext( + key, + nonce, + encrypted_bytes, + ) + .unwrap(); + let mut file = File::create(temp_output_path("decrypted.docx")).unwrap(); + file.write_all(&decrypted_bytes).unwrap(); + assert_eq!(file_bytes, decrypted_bytes); + } + + #[test] + fn test_aes_128_gcm_siv_rfc8452_vectors() { + for case in AES128_GCM_SIV_KAT { + let key = decode_hex(case.key); + let nonce = decode_hex(case.nonce); + let plaintext = decode_hex(case.plaintext); + let expected = decode_hex(case.result); + + let encrypted = ::encrypt_plaintext( + key.clone(), + nonce.clone(), + plaintext.clone(), + ) + .unwrap(); + assert_eq!(encrypted, expected, "encrypt mismatch for pt = {}", case.plaintext); + + let decrypted = ::decrypt_ciphertext( + key, + nonce, + expected, + ) + .unwrap(); + assert_eq!(decrypted, plaintext, "decrypt mismatch for pt = {}", case.plaintext); + } + } + + #[test] + fn test_aes_256_gcm_siv_rfc8452_vectors() { + for case in AES256_GCM_SIV_KAT { + let key = decode_hex(case.key); + let nonce = decode_hex(case.nonce); + let plaintext = decode_hex(case.plaintext); + let expected = decode_hex(case.result); + + let encrypted = ::encrypt_plaintext( + key.clone(), + nonce.clone(), + plaintext.clone(), + ) + .unwrap(); + assert_eq!(encrypted, expected, "encrypt mismatch for pt = {}", case.plaintext); + + let decrypted = ::decrypt_ciphertext( + key, + nonce, + expected, + ) + .unwrap(); + assert_eq!(decrypted, plaintext, "decrypt mismatch for pt = {}", case.plaintext); + } + } + + #[test] + fn test_aes_256_gcm_siv_tampered_ciphertext_fails() { + let key = ::generate_key(); + let nonce = ::generate_nonce(); + let mut encrypted = ::encrypt_plaintext( + key.clone(), + nonce.clone(), + b"top secret message".to_vec(), + ) + .unwrap(); + + // Flip a bit so the authentication tag no longer verifies. + encrypted[0] ^= 0x01; + + let result = + ::decrypt_ciphertext(key, nonce, encrypted); + assert!(result.is_err()); + } + + #[test] + fn test_aes_128_gcm_siv_invalid_key_and_nonce() { + let valid_key = ::generate_key(); + let valid_nonce = ::generate_nonce(); + + // Wrong key length. + assert!(::encrypt_plaintext( + vec![0u8; 8], + valid_nonce.clone(), + b"data".to_vec() + ) + .is_err()); + + // Wrong nonce length. + assert!(::encrypt_plaintext( + valid_key, + vec![0u8; 8], + b"data".to_vec() + ) + .is_err()); + } + + #[test] + fn test_aes_256_gcm_siv_key_from_vec() { + let key = ::generate_key(); + assert_eq!( + ::key_from_vec(key.clone()).unwrap(), + key + ); + assert!(::key_from_vec(vec![0u8; 31]).is_err()); + } + + #[test] + fn test_aes_256_gcm_siv_x25519_diffie_hellman() { + let bob = ::generate_secret_and_public_key(); + let alice = ::generate_secret_and_public_key(); + + let bob_shared_secret = + ::diffie_hellman(bob.secret_key, alice.public_key).unwrap(); + let alice_shared_secret = + ::diffie_hellman(alice.secret_key, bob.public_key).unwrap(); + + let nonce = ::generate_nonce(); + let path = Path::new("tests/test.docx"); + let file_bytes: Vec = std::fs::read(path).unwrap(); + + let alice_key = ::key_from_x25519_shared_secret( + bob_shared_secret, + ) + .unwrap(); + let encrypted_bytes = ::encrypt_plaintext( + alice_key, + nonce.clone(), + file_bytes.clone(), + ) + .unwrap(); + + let bob_key = ::key_from_x25519_shared_secret( + alice_shared_secret, + ) + .unwrap(); + let decrypted_bytes = ::decrypt_ciphertext( + bob_key, + nonce, + encrypted_bytes, + ) + .unwrap(); + assert_eq!(file_bytes, decrypted_bytes); + } +}