diff --git a/util/RustSdk/rust/build.rs b/util/RustSdk/rust/build.rs index 0905afc22d..2eeedbbebd 100644 --- a/util/RustSdk/rust/build.rs +++ b/util/RustSdk/rust/build.rs @@ -1,6 +1,7 @@ fn main() { csbindgen::Builder::default() .input_extern_file("src/lib.rs") + .input_extern_file("src/cipher.rs") .csharp_dll_name("libsdk") .csharp_namespace("Bit.RustSDK") .csharp_class_accessibility("public") diff --git a/util/RustSdk/rust/src/cipher.rs b/util/RustSdk/rust/src/cipher.rs new file mode 100644 index 0000000000..e54255f3e6 --- /dev/null +++ b/util/RustSdk/rust/src/cipher.rs @@ -0,0 +1,435 @@ +//! Cipher encryption and decryption functions for the Seeder. +//! +//! This module provides FFI functions for encrypting and decrypting Bitwarden ciphers +//! using the Rust SDK's cryptographic primitives. + +use std::ffi::{c_char, CStr, CString}; + +use base64::{engine::general_purpose::STANDARD, Engine}; + +use bitwarden_core::key_management::KeyIds; +use bitwarden_crypto::{ + BitwardenLegacyKeyBytes, CompositeEncryptable, Decryptable, KeyEncryptable, KeyStore, + SymmetricCryptoKey, +}; +use bitwarden_vault::{Cipher, CipherView}; + +/// Create an error JSON response and return it as a C string pointer. +fn error_response(message: &str) -> *const c_char { + let error_json = serde_json::json!({ "error": message }).to_string(); + CString::new(error_json).unwrap().into_raw() +} + +/// Encrypt a CipherView with a symmetric key, returning an encrypted Cipher as JSON. +/// +/// # Arguments +/// * `cipher_view_json` - JSON string representing a CipherView (camelCase format) +/// * `symmetric_key_b64` - Base64-encoded symmetric key (64 bytes for AES-256-CBC-HMAC-SHA256) +/// +/// # Returns +/// JSON string representing the encrypted Cipher +/// +/// # Safety +/// Both pointers must be valid null-terminated strings. +#[no_mangle] +pub unsafe extern "C" fn encrypt_cipher( + cipher_view_json: *const c_char, + symmetric_key_b64: *const c_char, +) -> *const c_char { + let cipher_view_json = match CStr::from_ptr(cipher_view_json).to_str() { + Ok(s) => s, + Err(_) => return error_response("Invalid UTF-8 in cipher_view_json"), + }; + + let key_b64 = match CStr::from_ptr(symmetric_key_b64).to_str() { + Ok(s) => s, + Err(_) => return error_response("Invalid UTF-8 in symmetric_key_b64"), + }; + + let cipher_view: CipherView = match serde_json::from_str(cipher_view_json) { + Ok(v) => v, + Err(_) => return error_response("Failed to parse CipherView JSON"), + }; + + let key_bytes = match STANDARD.decode(key_b64) { + Ok(b) => b, + Err(_) => return error_response("Failed to decode base64 key"), + }; + + let key = + match SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_bytes.as_slice())) { + Ok(k) => k, + Err(_) => { + return error_response( + "Failed to create symmetric key: invalid key format or length", + ) + } + }; + + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let key_id = ctx.add_local_symmetric_key(key); + + let cipher = match cipher_view.encrypt_composite(&mut ctx, key_id) { + Ok(c) => c, + Err(_) => return error_response("Failed to encrypt cipher: encryption operation failed"), + }; + + match serde_json::to_string(&cipher) { + Ok(json) => CString::new(json).unwrap().into_raw(), + Err(_) => error_response("Failed to serialize encrypted cipher"), + } +} + +/// Decrypt an encrypted Cipher with a symmetric key, returning a CipherView as JSON. +/// +/// # Arguments +/// * `cipher_json` - JSON string representing an encrypted Cipher +/// * `symmetric_key_b64` - Base64-encoded symmetric key (64 bytes for AES-256-CBC-HMAC-SHA256) +/// +/// # Returns +/// JSON string representing the decrypted CipherView +/// +/// # Safety +/// Both pointers must be valid null-terminated strings. +#[no_mangle] +pub unsafe extern "C" fn decrypt_cipher( + cipher_json: *const c_char, + symmetric_key_b64: *const c_char, +) -> *const c_char { + let cipher_json = match CStr::from_ptr(cipher_json).to_str() { + Ok(s) => s, + Err(_) => return error_response("Invalid UTF-8 in cipher_json"), + }; + + let key_b64 = match CStr::from_ptr(symmetric_key_b64).to_str() { + Ok(s) => s, + Err(_) => return error_response("Invalid UTF-8 in symmetric_key_b64"), + }; + + let cipher: Cipher = match serde_json::from_str(cipher_json) { + Ok(c) => c, + Err(_) => return error_response("Failed to parse Cipher JSON"), + }; + + let key_bytes = match STANDARD.decode(key_b64) { + Ok(b) => b, + Err(_) => return error_response("Failed to decode base64 key"), + }; + + let key = + match SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_bytes.as_slice())) { + Ok(k) => k, + Err(_) => { + return error_response( + "Failed to create symmetric key: invalid key format or length", + ) + } + }; + + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + let key_id = ctx.add_local_symmetric_key(key); + + let cipher_view: CipherView = match cipher.decrypt(&mut ctx, key_id) { + Ok(v) => v, + Err(_) => return error_response("Failed to decrypt cipher: decryption operation failed"), + }; + + match serde_json::to_string(&cipher_view) { + Ok(json) => CString::new(json).unwrap().into_raw(), + Err(_) => error_response("Failed to serialize decrypted cipher"), + } +} + +/// Encrypt a plaintext string with a symmetric key, returning an EncString. +/// +/// # Arguments +/// * `plaintext` - The plaintext string to encrypt +/// * `symmetric_key_b64` - Base64-encoded symmetric key (64 bytes for AES-256-CBC-HMAC-SHA256) +/// +/// # Returns +/// EncString in format "2.{iv}|{data}|{mac}" +/// +/// # Safety +/// Both pointers must be valid null-terminated strings. +#[no_mangle] +pub unsafe extern "C" fn encrypt_string( + plaintext: *const c_char, + symmetric_key_b64: *const c_char, +) -> *const c_char { + let plaintext = match CStr::from_ptr(plaintext).to_str() { + Ok(s) => s, + Err(_) => return error_response("Invalid UTF-8 in plaintext"), + }; + + let key_b64 = match CStr::from_ptr(symmetric_key_b64).to_str() { + Ok(s) => s, + Err(_) => return error_response("Invalid UTF-8 in symmetric_key_b64"), + }; + + let key_bytes = match STANDARD.decode(key_b64) { + Ok(b) => b, + Err(_) => return error_response("Failed to decode base64 key"), + }; + + let key = + match SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_bytes.as_slice())) { + Ok(k) => k, + Err(_) => { + return error_response( + "Failed to create symmetric key: invalid key format or length", + ) + } + }; + + let encrypted = match plaintext.to_string().encrypt_with_key(&key) { + Ok(e) => e, + Err(_) => return error_response("Failed to encrypt string"), + }; + + CString::new(encrypted.to_string()).unwrap().into_raw() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{free_c_string, generate_organization_keys}; + use bitwarden_vault::{CipherType, LoginView}; + + fn create_test_cipher_view() -> CipherView { + CipherView { + id: None, + organization_id: None, + folder_id: None, + collection_ids: vec![], + key: None, + name: "Test Login".to_string(), + notes: Some("Secret notes".to_string()), + r#type: CipherType::Login, + login: Some(LoginView { + username: Some("testuser@example.com".to_string()), + password: Some("SuperSecretP@ssw0rd!".to_string()), + password_revision_date: None, + uris: None, + totp: None, + autofill_on_page_load: None, + fido2_credentials: None, + }), + identity: None, + card: None, + secure_note: None, + ssh_key: None, + favorite: false, + reprompt: bitwarden_vault::CipherRepromptType::None, + organization_use_totp: false, + edit: true, + permissions: None, + view_password: true, + local_data: None, + attachments: None, + fields: None, + password_history: None, + creation_date: "2025-01-01T00:00:00Z".parse().unwrap(), + deleted_date: None, + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + archived_date: None, + } + } + + fn call_encrypt_cipher(cipher_json: &str, key_b64: &str) -> String { + let cipher_cstr = CString::new(cipher_json).unwrap(); + let key_cstr = CString::new(key_b64).unwrap(); + + let result_ptr = unsafe { encrypt_cipher(cipher_cstr.as_ptr(), key_cstr.as_ptr()) }; + let result_cstr = unsafe { CStr::from_ptr(result_ptr) }; + let result = result_cstr.to_str().unwrap().to_owned(); + unsafe { free_c_string(result_ptr as *mut c_char) }; + + result + } + + fn make_test_key_b64() -> String { + SymmetricCryptoKey::make_aes256_cbc_hmac_key() + .to_base64() + .into() + } + + #[test] + fn encrypt_cipher_produces_encrypted_fields() { + let key_b64 = make_test_key_b64(); + let cipher_view = create_test_cipher_view(); + let cipher_json = serde_json::to_string(&cipher_view).unwrap(); + + let encrypted_json = call_encrypt_cipher(&cipher_json, &key_b64); + + assert!( + !encrypted_json.contains("\"error\""), + "Got error: {}", + encrypted_json + ); + + let encrypted_cipher: Cipher = + serde_json::from_str(&encrypted_json).expect("Failed to parse encrypted cipher JSON"); + + let encrypted_name = encrypted_cipher.name.to_string(); + assert!( + encrypted_name.starts_with("2."), + "Name should be encrypted: {}", + encrypted_name + ); + + let login = encrypted_cipher.login.expect("Login should be present"); + if let Some(username) = &login.username { + assert!( + username.to_string().starts_with("2."), + "Username should be encrypted" + ); + } + if let Some(password) = &login.password { + assert!( + password.to_string().starts_with("2."), + "Password should be encrypted" + ); + } + } + + #[test] + fn encrypt_cipher_works_with_generated_org_key() { + let org_keys_ptr = unsafe { generate_organization_keys() }; + let org_keys_cstr = unsafe { CStr::from_ptr(org_keys_ptr) }; + let org_keys_json = org_keys_cstr.to_str().unwrap().to_owned(); + unsafe { free_c_string(org_keys_ptr as *mut c_char) }; + + let org_keys: serde_json::Value = serde_json::from_str(&org_keys_json).unwrap(); + let org_key_b64 = org_keys["key"].as_str().unwrap(); + + let cipher_view = create_test_cipher_view(); + let cipher_json = serde_json::to_string(&cipher_view).unwrap(); + + let encrypted_json = call_encrypt_cipher(&cipher_json, org_key_b64); + + assert!( + !encrypted_json.contains("\"error\""), + "Got error: {}", + encrypted_json + ); + + let encrypted_cipher: Cipher = serde_json::from_str(&encrypted_json).unwrap(); + assert!(encrypted_cipher.name.to_string().starts_with("2.")); + } + + #[test] + fn encrypt_cipher_rejects_invalid_json() { + let key_b64 = make_test_key_b64(); + + let error_json = call_encrypt_cipher("{ this is not valid json }", &key_b64); + + assert!( + error_json.contains("\"error\""), + "Should return error for invalid JSON" + ); + assert!(error_json.contains("Failed to parse CipherView JSON")); + } + + #[test] + fn encrypt_cipher_rejects_invalid_base64_key() { + let cipher_view = create_test_cipher_view(); + let cipher_json = serde_json::to_string(&cipher_view).unwrap(); + + let error_json = call_encrypt_cipher(&cipher_json, "not-valid-base64!!!"); + + assert!( + error_json.contains("\"error\""), + "Should return error for invalid base64" + ); + assert!(error_json.contains("Failed to decode base64 key")); + } + + #[test] + fn encrypt_cipher_rejects_wrong_key_length() { + let cipher_view = create_test_cipher_view(); + let cipher_json = serde_json::to_string(&cipher_view).unwrap(); + let short_key_b64 = STANDARD.encode(b"too short"); + + let error_json = call_encrypt_cipher(&cipher_json, &short_key_b64); + + assert!( + error_json.contains("\"error\""), + "Should return error for wrong key length" + ); + assert!(error_json.contains("invalid key format or length")); + } + + fn call_decrypt_cipher(cipher_json: &str, key_b64: &str) -> String { + let cipher_cstr = CString::new(cipher_json).unwrap(); + let key_cstr = CString::new(key_b64).unwrap(); + + let result_ptr = unsafe { decrypt_cipher(cipher_cstr.as_ptr(), key_cstr.as_ptr()) }; + let result_cstr = unsafe { CStr::from_ptr(result_ptr) }; + let result = result_cstr.to_str().unwrap().to_owned(); + unsafe { free_c_string(result_ptr as *mut c_char) }; + + result + } + + #[test] + fn encrypt_decrypt_roundtrip_preserves_plaintext() { + let key_b64 = make_test_key_b64(); + let original_view = create_test_cipher_view(); + let original_json = serde_json::to_string(&original_view).unwrap(); + + let encrypted_json = call_encrypt_cipher(&original_json, &key_b64); + assert!( + !encrypted_json.contains("\"error\""), + "Encryption failed: {}", + encrypted_json + ); + + let decrypted_json = call_decrypt_cipher(&encrypted_json, &key_b64); + assert!( + !decrypted_json.contains("\"error\""), + "Decryption failed: {}", + decrypted_json + ); + + let decrypted_view: CipherView = serde_json::from_str(&decrypted_json) + .expect("Failed to parse decrypted CipherView"); + + assert_eq!(decrypted_view.name, original_view.name); + assert_eq!(decrypted_view.notes, original_view.notes); + + let original_login = original_view.login.expect("Original should have login"); + let decrypted_login = decrypted_view.login.expect("Decrypted should have login"); + + assert_eq!(decrypted_login.username, original_login.username); + assert_eq!(decrypted_login.password, original_login.password); + } + + #[test] + fn decrypt_cipher_rejects_wrong_key() { + let encrypt_key = make_test_key_b64(); + let wrong_key = make_test_key_b64(); + + let original_view = create_test_cipher_view(); + let original_json = serde_json::to_string(&original_view).unwrap(); + + let encrypted_json = call_encrypt_cipher(&original_json, &encrypt_key); + assert!(!encrypted_json.contains("\"error\"")); + + let decrypted_json = call_decrypt_cipher(&encrypted_json, &wrong_key); + + // Decryption with wrong key should fail or produce garbage + // The SDK may return an error or the MAC validation will fail + let result: Result = serde_json::from_str(&decrypted_json); + if !decrypted_json.contains("\"error\"") { + // If no error, the decrypted data should not match original + if let Ok(view) = result { + assert_ne!( + view.name, original_view.name, + "Decryption with wrong key should not produce original plaintext" + ); + } + } + } +} diff --git a/util/RustSdk/rust/src/lib.rs b/util/RustSdk/rust/src/lib.rs index c18ae4308f..65b9d4f116 100644 --- a/util/RustSdk/rust/src/lib.rs +++ b/util/RustSdk/rust/src/lib.rs @@ -1,4 +1,7 @@ #![allow(clippy::missing_safety_doc)] + +mod cipher; + use std::{ ffi::{c_char, CStr, CString}, num::NonZeroU32, @@ -6,13 +9,11 @@ use std::{ use base64::{engine::general_purpose::STANDARD, Engine}; -use bitwarden_core::key_management::KeyIds; use bitwarden_crypto::{ - AsymmetricCryptoKey, AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, CompositeEncryptable, - Decryptable, HashPurpose, Kdf, KeyEncryptable, KeyStore, MasterKey, RsaKeyPair, - SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey, UserKey, + AsymmetricCryptoKey, AsymmetricPublicCryptoKey, BitwardenLegacyKeyBytes, HashPurpose, Kdf, + KeyEncryptable, MasterKey, RsaKeyPair, SpkiPublicKeyBytes, SymmetricCryptoKey, + UnsignedSharedKey, UserKey, }; -use bitwarden_vault::{Cipher, CipherView}; #[no_mangle] pub unsafe extern "C" fn generate_user_keys( @@ -137,183 +138,6 @@ pub unsafe extern "C" fn generate_user_organization_key( result.into_raw() } -/// Create an error JSON response and return it as a C string pointer. -fn error_response(message: &str) -> *const c_char { - let error_json = serde_json::json!({ "error": message }).to_string(); - CString::new(error_json).unwrap().into_raw() -} - -/// Encrypt a CipherView with a symmetric key, returning an encrypted Cipher as JSON. -/// -/// # Arguments -/// * `cipher_view_json` - JSON string representing a CipherView (camelCase format) -/// * `symmetric_key_b64` - Base64-encoded symmetric key (64 bytes for AES-256-CBC-HMAC-SHA256) -/// -/// # Returns -/// JSON string representing the encrypted Cipher -/// -/// # Safety -/// Both pointers must be valid null-terminated strings. -#[no_mangle] -pub unsafe extern "C" fn encrypt_cipher( - cipher_view_json: *const c_char, - symmetric_key_b64: *const c_char, -) -> *const c_char { - let cipher_view_json = match CStr::from_ptr(cipher_view_json).to_str() { - Ok(s) => s, - Err(_) => return error_response("Invalid UTF-8 in cipher_view_json"), - }; - - let key_b64 = match CStr::from_ptr(symmetric_key_b64).to_str() { - Ok(s) => s, - Err(_) => return error_response("Invalid UTF-8 in symmetric_key_b64"), - }; - - let cipher_view: CipherView = match serde_json::from_str(cipher_view_json) { - Ok(v) => v, - Err(_) => return error_response("Failed to parse CipherView JSON"), - }; - - let key_bytes = match STANDARD.decode(key_b64) { - Ok(b) => b, - Err(_) => return error_response("Failed to decode base64 key"), - }; - - let key = - match SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_bytes.as_slice())) { - Ok(k) => k, - Err(_) => { - return error_response( - "Failed to create symmetric key: invalid key format or length", - ) - } - }; - - let store: KeyStore = KeyStore::default(); - let mut ctx = store.context_mut(); - let key_id = ctx.add_local_symmetric_key(key); - - let cipher = match cipher_view.encrypt_composite(&mut ctx, key_id) { - Ok(c) => c, - Err(_) => return error_response("Failed to encrypt cipher: encryption operation failed"), - }; - - match serde_json::to_string(&cipher) { - Ok(json) => CString::new(json).unwrap().into_raw(), - Err(_) => error_response("Failed to serialize encrypted cipher"), - } -} - -/// Decrypt an encrypted Cipher with a symmetric key, returning a CipherView as JSON. -/// -/// # Arguments -/// * `cipher_json` - JSON string representing an encrypted Cipher -/// * `symmetric_key_b64` - Base64-encoded symmetric key (64 bytes for AES-256-CBC-HMAC-SHA256) -/// -/// # Returns -/// JSON string representing the decrypted CipherView -/// -/// # Safety -/// Both pointers must be valid null-terminated strings. -#[no_mangle] -pub unsafe extern "C" fn decrypt_cipher( - cipher_json: *const c_char, - symmetric_key_b64: *const c_char, -) -> *const c_char { - let cipher_json = match CStr::from_ptr(cipher_json).to_str() { - Ok(s) => s, - Err(_) => return error_response("Invalid UTF-8 in cipher_json"), - }; - - let key_b64 = match CStr::from_ptr(symmetric_key_b64).to_str() { - Ok(s) => s, - Err(_) => return error_response("Invalid UTF-8 in symmetric_key_b64"), - }; - - let cipher: Cipher = match serde_json::from_str(cipher_json) { - Ok(c) => c, - Err(_) => return error_response("Failed to parse Cipher JSON"), - }; - - let key_bytes = match STANDARD.decode(key_b64) { - Ok(b) => b, - Err(_) => return error_response("Failed to decode base64 key"), - }; - - let key = - match SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_bytes.as_slice())) { - Ok(k) => k, - Err(_) => { - return error_response( - "Failed to create symmetric key: invalid key format or length", - ) - } - }; - - let store: KeyStore = KeyStore::default(); - let mut ctx = store.context_mut(); - let key_id = ctx.add_local_symmetric_key(key); - - let cipher_view: CipherView = match cipher.decrypt(&mut ctx, key_id) { - Ok(v) => v, - Err(_) => return error_response("Failed to decrypt cipher: decryption operation failed"), - }; - - match serde_json::to_string(&cipher_view) { - Ok(json) => CString::new(json).unwrap().into_raw(), - Err(_) => error_response("Failed to serialize decrypted cipher"), - } -} - -/// Encrypt a plaintext string with a symmetric key, returning an EncString. -/// -/// # Arguments -/// * `plaintext` - The plaintext string to encrypt -/// * `symmetric_key_b64` - Base64-encoded symmetric key (64 bytes for AES-256-CBC-HMAC-SHA256) -/// -/// # Returns -/// EncString in format "2.{iv}|{data}|{mac}" -/// -/// # Safety -/// Both pointers must be valid null-terminated strings. -#[no_mangle] -pub unsafe extern "C" fn encrypt_string( - plaintext: *const c_char, - symmetric_key_b64: *const c_char, -) -> *const c_char { - let plaintext = match CStr::from_ptr(plaintext).to_str() { - Ok(s) => s, - Err(_) => return error_response("Invalid UTF-8 in plaintext"), - }; - - let key_b64 = match CStr::from_ptr(symmetric_key_b64).to_str() { - Ok(s) => s, - Err(_) => return error_response("Invalid UTF-8 in symmetric_key_b64"), - }; - - let key_bytes = match STANDARD.decode(key_b64) { - Ok(b) => b, - Err(_) => return error_response("Failed to decode base64 key"), - }; - - let key = - match SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_bytes.as_slice())) { - Ok(k) => k, - Err(_) => { - return error_response( - "Failed to create symmetric key: invalid key format or length", - ) - } - }; - - let encrypted = match plaintext.to_string().encrypt_with_key(&key) { - Ok(e) => e, - Err(_) => return error_response("Failed to encrypt string"), - }; - - CString::new(encrypted.to_string()).unwrap().into_raw() -} - /// # Safety /// /// The `str` pointer must be a valid pointer previously returned by `CString::into_raw` @@ -324,245 +148,3 @@ pub unsafe extern "C" fn free_c_string(str: *mut c_char) { drop(CString::from_raw(str)); } } - -#[cfg(test)] -mod tests { - use super::*; - use bitwarden_vault::{Cipher, CipherType, LoginView}; - - fn create_test_cipher_view() -> CipherView { - CipherView { - id: None, - organization_id: None, - folder_id: None, - collection_ids: vec![], - key: None, - name: "Test Login".to_string(), - notes: Some("Secret notes".to_string()), - r#type: CipherType::Login, - login: Some(LoginView { - username: Some("testuser@example.com".to_string()), - password: Some("SuperSecretP@ssw0rd!".to_string()), - password_revision_date: None, - uris: None, - totp: None, - autofill_on_page_load: None, - fido2_credentials: None, - }), - identity: None, - card: None, - secure_note: None, - ssh_key: None, - favorite: false, - reprompt: bitwarden_vault::CipherRepromptType::None, - organization_use_totp: false, - edit: true, - permissions: None, - view_password: true, - local_data: None, - attachments: None, - fields: None, - password_history: None, - creation_date: "2025-01-01T00:00:00Z".parse().unwrap(), - deleted_date: None, - revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), - archived_date: None, - } - } - - fn call_encrypt_cipher(cipher_json: &str, key_b64: &str) -> String { - let cipher_cstr = CString::new(cipher_json).unwrap(); - let key_cstr = CString::new(key_b64).unwrap(); - - let result_ptr = unsafe { encrypt_cipher(cipher_cstr.as_ptr(), key_cstr.as_ptr()) }; - let result_cstr = unsafe { CStr::from_ptr(result_ptr) }; - let result = result_cstr.to_str().unwrap().to_owned(); - unsafe { free_c_string(result_ptr as *mut c_char) }; - - result - } - - fn make_test_key_b64() -> String { - SymmetricCryptoKey::make_aes256_cbc_hmac_key() - .to_base64() - .into() - } - - #[test] - fn encrypt_cipher_produces_encrypted_fields() { - let key_b64 = make_test_key_b64(); - let cipher_view = create_test_cipher_view(); - let cipher_json = serde_json::to_string(&cipher_view).unwrap(); - - let encrypted_json = call_encrypt_cipher(&cipher_json, &key_b64); - - assert!( - !encrypted_json.contains("\"error\""), - "Got error: {}", - encrypted_json - ); - - let encrypted_cipher: Cipher = - serde_json::from_str(&encrypted_json).expect("Failed to parse encrypted cipher JSON"); - - let encrypted_name = encrypted_cipher.name.to_string(); - assert!( - encrypted_name.starts_with("2."), - "Name should be encrypted: {}", - encrypted_name - ); - - let login = encrypted_cipher.login.expect("Login should be present"); - if let Some(username) = &login.username { - assert!( - username.to_string().starts_with("2."), - "Username should be encrypted" - ); - } - if let Some(password) = &login.password { - assert!( - password.to_string().starts_with("2."), - "Password should be encrypted" - ); - } - } - - #[test] - fn encrypt_cipher_works_with_generated_org_key() { - let org_keys_ptr = unsafe { generate_organization_keys() }; - let org_keys_cstr = unsafe { CStr::from_ptr(org_keys_ptr) }; - let org_keys_json = org_keys_cstr.to_str().unwrap().to_owned(); - unsafe { free_c_string(org_keys_ptr as *mut c_char) }; - - let org_keys: serde_json::Value = serde_json::from_str(&org_keys_json).unwrap(); - let org_key_b64 = org_keys["key"].as_str().unwrap(); - - let cipher_view = create_test_cipher_view(); - let cipher_json = serde_json::to_string(&cipher_view).unwrap(); - - let encrypted_json = call_encrypt_cipher(&cipher_json, org_key_b64); - - assert!( - !encrypted_json.contains("\"error\""), - "Got error: {}", - encrypted_json - ); - - let encrypted_cipher: Cipher = serde_json::from_str(&encrypted_json).unwrap(); - assert!(encrypted_cipher.name.to_string().starts_with("2.")); - } - - #[test] - fn encrypt_cipher_rejects_invalid_json() { - let key_b64 = make_test_key_b64(); - - let error_json = call_encrypt_cipher("{ this is not valid json }", &key_b64); - - assert!( - error_json.contains("\"error\""), - "Should return error for invalid JSON" - ); - assert!(error_json.contains("Failed to parse CipherView JSON")); - } - - #[test] - fn encrypt_cipher_rejects_invalid_base64_key() { - let cipher_view = create_test_cipher_view(); - let cipher_json = serde_json::to_string(&cipher_view).unwrap(); - - let error_json = call_encrypt_cipher(&cipher_json, "not-valid-base64!!!"); - - assert!( - error_json.contains("\"error\""), - "Should return error for invalid base64" - ); - assert!(error_json.contains("Failed to decode base64 key")); - } - - #[test] - fn encrypt_cipher_rejects_wrong_key_length() { - let cipher_view = create_test_cipher_view(); - let cipher_json = serde_json::to_string(&cipher_view).unwrap(); - let short_key_b64 = STANDARD.encode(b"too short"); - - let error_json = call_encrypt_cipher(&cipher_json, &short_key_b64); - - assert!( - error_json.contains("\"error\""), - "Should return error for wrong key length" - ); - assert!(error_json.contains("invalid key format or length")); - } - - fn call_decrypt_cipher(cipher_json: &str, key_b64: &str) -> String { - let cipher_cstr = CString::new(cipher_json).unwrap(); - let key_cstr = CString::new(key_b64).unwrap(); - - let result_ptr = unsafe { decrypt_cipher(cipher_cstr.as_ptr(), key_cstr.as_ptr()) }; - let result_cstr = unsafe { CStr::from_ptr(result_ptr) }; - let result = result_cstr.to_str().unwrap().to_owned(); - unsafe { free_c_string(result_ptr as *mut c_char) }; - - result - } - - #[test] - fn encrypt_decrypt_roundtrip_preserves_plaintext() { - let key_b64 = make_test_key_b64(); - let original_view = create_test_cipher_view(); - let original_json = serde_json::to_string(&original_view).unwrap(); - - let encrypted_json = call_encrypt_cipher(&original_json, &key_b64); - assert!( - !encrypted_json.contains("\"error\""), - "Encryption failed: {}", - encrypted_json - ); - - let decrypted_json = call_decrypt_cipher(&encrypted_json, &key_b64); - assert!( - !decrypted_json.contains("\"error\""), - "Decryption failed: {}", - decrypted_json - ); - - let decrypted_view: CipherView = serde_json::from_str(&decrypted_json) - .expect("Failed to parse decrypted CipherView"); - - assert_eq!(decrypted_view.name, original_view.name); - assert_eq!(decrypted_view.notes, original_view.notes); - - let original_login = original_view.login.expect("Original should have login"); - let decrypted_login = decrypted_view.login.expect("Decrypted should have login"); - - assert_eq!(decrypted_login.username, original_login.username); - assert_eq!(decrypted_login.password, original_login.password); - } - - #[test] - fn decrypt_cipher_rejects_wrong_key() { - let encrypt_key = make_test_key_b64(); - let wrong_key = make_test_key_b64(); - - let original_view = create_test_cipher_view(); - let original_json = serde_json::to_string(&original_view).unwrap(); - - let encrypted_json = call_encrypt_cipher(&original_json, &encrypt_key); - assert!(!encrypted_json.contains("\"error\"")); - - let decrypted_json = call_decrypt_cipher(&encrypted_json, &wrong_key); - - // Decryption with wrong key should fail or produce garbage - // The SDK may return an error or the MAC validation will fail - let result: Result = serde_json::from_str(&decrypted_json); - if !decrypted_json.contains("\"error\"") { - // If no error, the decrypted data should not match original - if let Ok(view) = result { - assert_ne!( - view.name, original_view.name, - "Decryption with wrong key should not produce original plaintext" - ); - } - } - } -}