From 57f923c40f2c25c4c7a58c04aa868502d67019a0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:43:04 +0000 Subject: [PATCH] Fix deterministic AES encryption in crypto.rs by replacing the zero-nonce with a cryptographically secure random 12-byte nonce using rand::Rng. Update aes_encrypt and aes_decrypt to prepend/extract the nonce respectively. Add tests to ensure identical plaintexts produce different ciphertexts. Co-authored-by: Tcode-Motion <188012755+Tcode-Motion@users.noreply.github.com> --- stdlib/src/crypto.rs | 29 ++++++++++++++++++++++++----- stdlib/tests/stdlib_tests.rs | 27 ++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/stdlib/src/crypto.rs b/stdlib/src/crypto.rs index 7b8edbb4..0bd4ebf1 100644 --- a/stdlib/src/crypto.rs +++ b/stdlib/src/crypto.rs @@ -6,6 +6,7 @@ use aes_gcm::{ use bcrypt; use sha2::Digest; use std::collections::HashMap; +use rand::Rng; use std::rc::Rc; use techscript_runtime::{error::RuntimeError, error::RuntimeErrorKind, value::RuntimeValue}; @@ -39,8 +40,10 @@ impl StdlibRegistry { ) })?; - // Nonce is 12-byte zero nonce for simple FFI compatibility - let nonce = Nonce::from_slice(&[0u8; 12]); + // Generate a random 12-byte nonce for secure encryption + let mut nonce_bytes = [0u8; 12]; + rand::thread_rng().fill(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); let ciphertext = cipher.encrypt(nonce, text.as_bytes()).map_err(|e| { RuntimeError::new( @@ -53,8 +56,12 @@ impl StdlibRegistry { ) })?; + // Prepend nonce to ciphertext + let mut combined = nonce_bytes.to_vec(); + combined.extend_from_slice(&ciphertext); + // Hex encode ciphertext - let hex_ciphertext = ciphertext + let hex_ciphertext = combined .iter() .map(|b| format!("{:02x}", b)) .collect::(); @@ -105,10 +112,22 @@ impl StdlibRegistry { ) })?; - let nonce = Nonce::from_slice(&[0u8; 12]); + if ciphertext.len() < 12 { + return Err(RuntimeError::new( + RuntimeErrorKind::InvalidOperation( + "Ciphertext too short (missing nonce)".to_string(), + ), + None, + None, + )); + } + + let nonce_bytes = &ciphertext[0..12]; + let actual_ciphertext = &ciphertext[12..]; + let nonce = Nonce::from_slice(nonce_bytes); let plaintext_bytes = - cipher.decrypt(nonce, ciphertext.as_slice()).map_err(|e| { + cipher.decrypt(nonce, actual_ciphertext).map_err(|e| { RuntimeError::new( RuntimeErrorKind::InvalidOperation(format!( "AES decryption error: {}", diff --git a/stdlib/tests/stdlib_tests.rs b/stdlib/tests/stdlib_tests.rs index 7d530343..f7df969f 100644 --- a/stdlib/tests/stdlib_tests.rs +++ b/stdlib/tests/stdlib_tests.rs @@ -893,13 +893,30 @@ fn test_crypto_hash_and_compression() { let key = RuntimeValue::Str("my_secret_key_123".to_string()); let plain = RuntimeValue::Str("hello crypto world".to_string()); - let encrypted = aes_enc - .call(&mut ctx_unprivileged, vec![key.clone(), plain]) + let encrypted1 = aes_enc + .call(&mut ctx_unprivileged, vec![key.clone(), plain.clone()]) .unwrap(); - let decrypted = aes_dec - .call(&mut ctx_unprivileged, vec![key, encrypted]) + + let encrypted2 = aes_enc + .call(&mut ctx_unprivileged, vec![key.clone(), plain.clone()]) + .unwrap(); + + // Verify deterministic encryption is fixed (random nonce is working) + assert_ne!( + encrypted1.as_string().unwrap(), + encrypted2.as_string().unwrap(), + "Encrypting the same plaintext with the same key should produce different ciphertexts" + ); + + let decrypted1 = aes_dec + .call(&mut ctx_unprivileged, vec![key.clone(), encrypted1]) + .unwrap(); + assert_eq!(decrypted1.as_string(), Some("hello crypto world")); + + let decrypted2 = aes_dec + .call(&mut ctx_unprivileged, vec![key.clone(), encrypted2]) .unwrap(); - assert_eq!(decrypted.as_string(), Some("hello crypto world")); + assert_eq!(decrypted2.as_string(), Some("hello crypto world")); let bcrypt_hash = crypto.exports.get("bcrypt_hash").unwrap(); let bcrypt_verify = crypto.exports.get("bcrypt_verify").unwrap();