Skip to content

Commit ae25767

Browse files
committed
added tests
1 parent c14a768 commit ae25767

File tree

3 files changed

+693
-0
lines changed

3 files changed

+693
-0
lines changed

crates/fula-js/src/lib.rs

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,218 @@ pub fn get_version() -> String {
608608
env!("CARGO_PKG_VERSION").to_string()
609609
}
610610

611+
// ============================================================================
612+
// HPKE Test Functions (for cross-platform verification)
613+
// ============================================================================
614+
615+
/// Encrypt a DEK using HPKE with the given public key
616+
///
617+
/// This simulates what FxFiles does when encrypting a file.
618+
/// Returns JSON string containing the EncryptedData (wrapped DEK).
619+
///
620+
/// @param publicKeyBytes - 32-byte X25519 public key
621+
/// @param dekBytes - 32-byte DEK to encrypt
622+
/// @returns JSON string with {version, encapsulated_key, ciphertext}
623+
#[wasm_bindgen(js_name = testHpkeEncryptDek)]
624+
pub fn test_hpke_encrypt_dek(
625+
public_key_bytes: &[u8],
626+
dek_bytes: &[u8],
627+
) -> Result<String, JsError> {
628+
if public_key_bytes.len() != 32 {
629+
return Err(JsError::new(&format!(
630+
"Public key must be 32 bytes, got {}", public_key_bytes.len()
631+
)));
632+
}
633+
if dek_bytes.len() != 32 {
634+
return Err(JsError::new(&format!(
635+
"DEK must be 32 bytes, got {}", dek_bytes.len()
636+
)));
637+
}
638+
639+
let mut pk_arr = [0u8; 32];
640+
pk_arr.copy_from_slice(public_key_bytes);
641+
let public_key = fula_crypto::PublicKey::from_bytes(&pk_arr)
642+
.map_err(|e| JsError::new(&format!("Invalid public key: {}", e)))?;
643+
644+
let mut dek_arr = [0u8; 32];
645+
dek_arr.copy_from_slice(dek_bytes);
646+
let dek = fula_crypto::keys::DekKey::from_bytes(&dek_arr)
647+
.map_err(|e| JsError::new(&format!("Invalid DEK: {}", e)))?;
648+
649+
let encryptor = fula_crypto::hpke::Encryptor::new(&public_key);
650+
let wrapped = encryptor.encrypt_dek(&dek)
651+
.map_err(|e| JsError::new(&format!("HPKE encryption failed: {}", e)))?;
652+
653+
serde_json::to_string(&wrapped)
654+
.map_err(|e| JsError::new(&format!("JSON serialization failed: {}", e)))
655+
}
656+
657+
/// Decrypt a wrapped DEK using HPKE with the given secret key
658+
///
659+
/// This simulates what WebUI WASM should do when decrypting a file.
660+
///
661+
/// @param secretKeyBytes - 32-byte X25519 secret key
662+
/// @param wrappedDekJson - JSON string from testHpkeEncryptDek
663+
/// @returns 32-byte decrypted DEK
664+
#[wasm_bindgen(js_name = testHpkeDecryptDek)]
665+
pub fn test_hpke_decrypt_dek(
666+
secret_key_bytes: &[u8],
667+
wrapped_dek_json: &str,
668+
) -> Result<Vec<u8>, JsError> {
669+
if secret_key_bytes.len() != 32 {
670+
return Err(JsError::new(&format!(
671+
"Secret key must be 32 bytes, got {}", secret_key_bytes.len()
672+
)));
673+
}
674+
675+
let mut sk_arr = [0u8; 32];
676+
sk_arr.copy_from_slice(secret_key_bytes);
677+
let secret = fula_crypto::SecretKey::from_bytes(&sk_arr)
678+
.map_err(|e| JsError::new(&format!("Invalid secret key: {}", e)))?;
679+
680+
let key_manager = fula_crypto::keys::KeyManager::from_secret_key(secret);
681+
let wrapped: fula_crypto::hpke::EncryptedData = serde_json::from_str(wrapped_dek_json)
682+
.map_err(|e| JsError::new(&format!("Invalid wrapped DEK JSON: {}", e)))?;
683+
684+
let decryptor = fula_crypto::hpke::Decryptor::new(key_manager.keypair());
685+
let dek = decryptor.decrypt_dek(&wrapped)
686+
.map_err(|e| JsError::new(&format!("HPKE decryption failed: {}", e)))?;
687+
688+
Ok(dek.as_bytes().to_vec())
689+
}
690+
691+
/// AES-256-GCM encrypt data with a DEK
692+
///
693+
/// @param dekBytes - 32-byte DEK
694+
/// @param plaintext - Data to encrypt
695+
/// @returns JSON string with {nonce, ciphertext} (both base64 encoded)
696+
#[wasm_bindgen(js_name = testAesGcmEncrypt)]
697+
pub fn test_aes_gcm_encrypt(
698+
dek_bytes: &[u8],
699+
plaintext: &[u8],
700+
) -> Result<String, JsError> {
701+
if dek_bytes.len() != 32 {
702+
return Err(JsError::new(&format!(
703+
"DEK must be 32 bytes, got {}", dek_bytes.len()
704+
)));
705+
}
706+
707+
let mut dek_arr = [0u8; 32];
708+
dek_arr.copy_from_slice(dek_bytes);
709+
let dek = fula_crypto::keys::DekKey::from_bytes(&dek_arr)
710+
.map_err(|e| JsError::new(&format!("Invalid DEK: {}", e)))?;
711+
712+
let nonce = fula_crypto::symmetric::Nonce::generate();
713+
let aead = fula_crypto::symmetric::Aead::new_default(&dek);
714+
let ciphertext = aead.encrypt(&nonce, plaintext)
715+
.map_err(|e| JsError::new(&format!("AES-GCM encryption failed: {}", e)))?;
716+
717+
use base64::Engine;
718+
let result = serde_json::json!({
719+
"nonce": base64::engine::general_purpose::STANDARD.encode(nonce.as_bytes()),
720+
"ciphertext": base64::engine::general_purpose::STANDARD.encode(&ciphertext),
721+
});
722+
723+
serde_json::to_string(&result)
724+
.map_err(|e| JsError::new(&format!("JSON serialization failed: {}", e)))
725+
}
726+
727+
/// AES-256-GCM decrypt data with a DEK
728+
///
729+
/// @param dekBytes - 32-byte DEK
730+
/// @param nonceBase64 - Base64 encoded 12-byte nonce
731+
/// @param ciphertextBase64 - Base64 encoded ciphertext
732+
/// @returns Decrypted plaintext
733+
#[wasm_bindgen(js_name = testAesGcmDecrypt)]
734+
pub fn test_aes_gcm_decrypt(
735+
dek_bytes: &[u8],
736+
nonce_base64: &str,
737+
ciphertext_base64: &str,
738+
) -> Result<Vec<u8>, JsError> {
739+
if dek_bytes.len() != 32 {
740+
return Err(JsError::new(&format!(
741+
"DEK must be 32 bytes, got {}", dek_bytes.len()
742+
)));
743+
}
744+
745+
use base64::Engine;
746+
let nonce_bytes = base64::engine::general_purpose::STANDARD.decode(nonce_base64)
747+
.map_err(|e| JsError::new(&format!("Invalid nonce base64: {}", e)))?;
748+
let ciphertext = base64::engine::general_purpose::STANDARD.decode(ciphertext_base64)
749+
.map_err(|e| JsError::new(&format!("Invalid ciphertext base64: {}", e)))?;
750+
751+
let mut dek_arr = [0u8; 32];
752+
dek_arr.copy_from_slice(dek_bytes);
753+
let dek = fula_crypto::keys::DekKey::from_bytes(&dek_arr)
754+
.map_err(|e| JsError::new(&format!("Invalid DEK: {}", e)))?;
755+
756+
let nonce = fula_crypto::symmetric::Nonce::from_bytes(&nonce_bytes)
757+
.map_err(|e| JsError::new(&format!("Invalid nonce: {}", e)))?;
758+
let aead = fula_crypto::symmetric::Aead::new_default(&dek);
759+
let plaintext = aead.decrypt(&nonce, &ciphertext)
760+
.map_err(|e| JsError::new(&format!("AES-GCM decryption failed: {}", e)))?;
761+
762+
Ok(plaintext)
763+
}
764+
765+
/// Full encryption round-trip test
766+
///
767+
/// This tests the EXACT flow: derive key -> encrypt DEK with HPKE -> encrypt data with AES-GCM
768+
/// then decrypt in reverse order. If this works, the WASM crypto is correct.
769+
///
770+
/// @param context - Key derivation context (e.g., "fula-files-v1")
771+
/// @param input - Key derivation input (e.g., credentials)
772+
/// @param plaintext - Data to encrypt and decrypt
773+
/// @returns Original plaintext if successful (proves round-trip works)
774+
#[wasm_bindgen(js_name = testFullEncryptionRoundtrip)]
775+
pub fn test_full_encryption_roundtrip(
776+
context: &str,
777+
input: &[u8],
778+
plaintext: &[u8],
779+
) -> Result<Vec<u8>, JsError> {
780+
// Step 1: Derive key using Argon2id
781+
let secret_bytes = fula_crypto::hashing::derive_key_argon2id(context, input);
782+
783+
// Step 2: Create keypair
784+
let secret = fula_crypto::SecretKey::from_bytes(&secret_bytes)
785+
.map_err(|e| JsError::new(&format!("Invalid derived key: {}", e)))?;
786+
let public = secret.public_key();
787+
788+
// Step 3: Generate random DEK
789+
let dek = fula_crypto::keys::DekKey::generate();
790+
791+
// Step 4: Encrypt DEK with HPKE (simulating FxFiles upload)
792+
let encryptor = fula_crypto::hpke::Encryptor::new(&public);
793+
let wrapped_dek = encryptor.encrypt_dek(&dek)
794+
.map_err(|e| JsError::new(&format!("HPKE DEK encryption failed: {}", e)))?;
795+
796+
// Step 5: Encrypt plaintext with AES-GCM
797+
let nonce = fula_crypto::symmetric::Nonce::generate();
798+
let aead = fula_crypto::symmetric::Aead::new_default(&dek);
799+
let ciphertext = aead.encrypt(&nonce, plaintext)
800+
.map_err(|e| JsError::new(&format!("AES-GCM encryption failed: {}", e)))?;
801+
802+
// --- Simulating WebUI decryption ---
803+
804+
// Step 6: Derive key again (same as WebUI would do)
805+
let secret_bytes_2 = fula_crypto::hashing::derive_key_argon2id(context, input);
806+
let secret_2 = fula_crypto::SecretKey::from_bytes(&secret_bytes_2)
807+
.map_err(|e| JsError::new(&format!("Invalid derived key on decrypt: {}", e)))?;
808+
809+
// Step 7: Decrypt DEK with HPKE (simulating WebUI WASM)
810+
let key_manager = fula_crypto::keys::KeyManager::from_secret_key(secret_2);
811+
let decryptor = fula_crypto::hpke::Decryptor::new(key_manager.keypair());
812+
let decrypted_dek = decryptor.decrypt_dek(&wrapped_dek)
813+
.map_err(|e| JsError::new(&format!("HPKE DEK decryption failed: {}", e)))?;
814+
815+
// Step 8: Decrypt plaintext with AES-GCM
816+
let aead_decrypt = fula_crypto::symmetric::Aead::new_default(&decrypted_dek);
817+
let decrypted = aead_decrypt.decrypt(&nonce, &ciphertext)
818+
.map_err(|e| JsError::new(&format!("AES-GCM decryption failed: {}", e)))?;
819+
820+
Ok(decrypted)
821+
}
822+
611823
// ============================================================================
612824
// Tests
613825
// ============================================================================

0 commit comments

Comments
 (0)