1
0
mirror of https://github.com/bitwarden/server synced 2026-02-13 06:53:56 +00:00

Split up lib and cipher from one another

This commit is contained in:
Mick Letofsky
2026-01-27 13:25:18 +01:00
parent d645a143e3
commit 4c4b463a3d
3 changed files with 442 additions and 424 deletions

View File

@@ -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")

View File

@@ -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<KeyIds> = 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<KeyIds> = 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<CipherView, _> = 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"
);
}
}
}
}

View File

@@ -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<KeyIds> = 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<KeyIds> = 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<CipherView, _> = 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"
);
}
}
}
}