// Native crypto primitives exposed as Tauri commands. The JS side calls // these via `invoke('crypto_…', …)` through `lib/nativeCryptoBackend.ts`. // // All byte arrays cross the IPC boundary as base64 strings to sidestep // serde_json's lack of native bytes support; JS encodes/decodes at the // thin wrapper layer. The extra encode step costs a few µs per call — // negligible against Argon2id's ~200ms and acceptable for bulk AEAD ops // which still outperform the WASM backend after the round-trip. // // Semantics: bit-compatible with libsodium-wrappers-sumo for all inputs. // AEAD authentication failures surface as `Err(String)` so the JS layer // can re-throw a deterministic error that existing callers already handle. use base64::{engine::general_purpose::STANDARD as B64, Engine}; use dryoc::classic::crypto_box; use dryoc::classic::crypto_pwhash::{self, PasswordHashAlgorithm}; use dryoc::classic::crypto_secretbox; use dryoc::constants::{ CRYPTO_BOX_PUBLICKEYBYTES, CRYPTO_BOX_SECRETKEYBYTES, CRYPTO_PWHASH_MEMLIMIT_MODERATE, CRYPTO_PWHASH_OPSLIMIT_MODERATE, CRYPTO_PWHASH_SALTBYTES, }; use dryoc::rng::randombytes_buf; use serde::{Deserialize, Serialize}; fn encode(bytes: &[u8]) -> String { B64.encode(bytes) } fn decode(s: &str) -> Result, String> { B64.decode(s).map_err(|e| format!("invalid base64: {}", e)) } // --------------------------------------------------------------------------- // Random // --------------------------------------------------------------------------- #[tauri::command] pub fn crypto_random_bytes(len: usize) -> Result { if len == 0 || len > 1024 * 1024 { return Err("invalid length".into()); } let buf = randombytes_buf(len); Ok(encode(&buf)) } // --------------------------------------------------------------------------- // crypto_secretbox — XSalsa20-Poly1305 // --------------------------------------------------------------------------- #[tauri::command] pub fn crypto_secretbox_encrypt( plaintext_b64: String, nonce_b64: String, key_b64: String, ) -> Result { let plaintext = decode(&plaintext_b64)?; let nonce = decode(&nonce_b64)?; let key = decode(&key_b64)?; if nonce.len() != 24 { return Err("nonce must be 24 bytes".into()); } if key.len() != 32 { return Err("key must be 32 bytes".into()); } let mut ciphertext = vec![0u8; plaintext.len() + 16]; let nonce_arr: [u8; 24] = nonce.as_slice().try_into().unwrap(); let key_arr: [u8; 32] = key.as_slice().try_into().unwrap(); crypto_secretbox::crypto_secretbox_easy( &mut ciphertext, &plaintext, &nonce_arr, &key_arr, ) .map_err(|e| format!("secretbox encrypt failed: {}", e))?; Ok(encode(&ciphertext)) } #[tauri::command] pub fn crypto_secretbox_decrypt( ciphertext_b64: String, nonce_b64: String, key_b64: String, ) -> Result { let ciphertext = decode(&ciphertext_b64)?; let nonce = decode(&nonce_b64)?; let key = decode(&key_b64)?; if nonce.len() != 24 { return Err("nonce must be 24 bytes".into()); } if key.len() != 32 { return Err("key must be 32 bytes".into()); } if ciphertext.len() < 16 { return Err("ciphertext too short".into()); } let mut plaintext = vec![0u8; ciphertext.len() - 16]; let nonce_arr: [u8; 24] = nonce.as_slice().try_into().unwrap(); let key_arr: [u8; 32] = key.as_slice().try_into().unwrap(); crypto_secretbox::crypto_secretbox_open_easy( &mut plaintext, &ciphertext, &nonce_arr, &key_arr, ) .map_err(|_| "secretbox auth failed".to_string())?; Ok(encode(&plaintext)) } // --------------------------------------------------------------------------- // crypto_box — X25519 + XSalsa20-Poly1305 // --------------------------------------------------------------------------- #[derive(Serialize, Deserialize)] pub struct KeyPairB64 { pub public_key: String, pub private_key: String, } #[tauri::command] pub fn crypto_box_keypair() -> Result { let (pk, sk) = crypto_box::crypto_box_keypair(); Ok(KeyPairB64 { public_key: encode(&pk), private_key: encode(&sk), }) } #[tauri::command] pub fn crypto_box_encrypt( plaintext_b64: String, nonce_b64: String, recipient_pk_b64: String, sender_sk_b64: String, ) -> Result { let plaintext = decode(&plaintext_b64)?; let nonce = decode(&nonce_b64)?; let pk = decode(&recipient_pk_b64)?; let sk = decode(&sender_sk_b64)?; if pk.len() != CRYPTO_BOX_PUBLICKEYBYTES { return Err("pk must be 32 bytes".into()); } if sk.len() != CRYPTO_BOX_SECRETKEYBYTES { return Err("sk must be 32 bytes".into()); } let mut ciphertext = vec![0u8; plaintext.len() + 16]; let nonce_arr: [u8; 24] = nonce.as_slice().try_into().map_err(|_| "bad nonce")?; let pk_arr: [u8; 32] = pk.as_slice().try_into().unwrap(); let sk_arr: [u8; 32] = sk.as_slice().try_into().unwrap(); crypto_box::crypto_box_easy(&mut ciphertext, &plaintext, &nonce_arr, &pk_arr, &sk_arr) .map_err(|e| format!("box encrypt failed: {}", e))?; Ok(encode(&ciphertext)) } #[tauri::command] pub fn crypto_box_decrypt( ciphertext_b64: String, nonce_b64: String, sender_pk_b64: String, recipient_sk_b64: String, ) -> Result { let ciphertext = decode(&ciphertext_b64)?; let nonce = decode(&nonce_b64)?; let pk = decode(&sender_pk_b64)?; let sk = decode(&recipient_sk_b64)?; if pk.len() != CRYPTO_BOX_PUBLICKEYBYTES { return Err("pk must be 32 bytes".into()); } if sk.len() != CRYPTO_BOX_SECRETKEYBYTES { return Err("sk must be 32 bytes".into()); } if ciphertext.len() < 16 { return Err("ciphertext too short".into()); } let mut plaintext = vec![0u8; ciphertext.len() - 16]; let nonce_arr: [u8; 24] = nonce.as_slice().try_into().map_err(|_| "bad nonce")?; let pk_arr: [u8; 32] = pk.as_slice().try_into().unwrap(); let sk_arr: [u8; 32] = sk.as_slice().try_into().unwrap(); crypto_box::crypto_box_open_easy( &mut plaintext, &ciphertext, &nonce_arr, &pk_arr, &sk_arr, ) .map_err(|_| "box auth failed".to_string())?; Ok(encode(&plaintext)) } // Sealed-box (anonymous) variant — sender identity not authenticated but // recipient still verified. Used by the conv-key wrapping flow. #[tauri::command] pub fn crypto_box_seal( plaintext_b64: String, recipient_pk_b64: String, ) -> Result { let plaintext = decode(&plaintext_b64)?; let pk = decode(&recipient_pk_b64)?; if pk.len() != CRYPTO_BOX_PUBLICKEYBYTES { return Err("pk must be 32 bytes".into()); } let pk_arr: [u8; 32] = pk.as_slice().try_into().unwrap(); let mut ciphertext = vec![0u8; plaintext.len() + 48]; crypto_box::crypto_box_seal(&mut ciphertext, &plaintext, &pk_arr) .map_err(|e| format!("seal failed: {}", e))?; Ok(encode(&ciphertext)) } #[tauri::command] pub fn crypto_box_seal_open( ciphertext_b64: String, recipient_pk_b64: String, recipient_sk_b64: String, ) -> Result { let ciphertext = decode(&ciphertext_b64)?; let pk = decode(&recipient_pk_b64)?; let sk = decode(&recipient_sk_b64)?; if pk.len() != CRYPTO_BOX_PUBLICKEYBYTES { return Err("pk must be 32 bytes".into()); } if sk.len() != CRYPTO_BOX_SECRETKEYBYTES { return Err("sk must be 32 bytes".into()); } if ciphertext.len() < 48 { return Err("ciphertext too short".into()); } let pk_arr: [u8; 32] = pk.as_slice().try_into().unwrap(); let sk_arr: [u8; 32] = sk.as_slice().try_into().unwrap(); let mut plaintext = vec![0u8; ciphertext.len() - 48]; crypto_box::crypto_box_seal_open(&mut plaintext, &ciphertext, &pk_arr, &sk_arr) .map_err(|_| "seal open failed".to_string())?; Ok(encode(&plaintext)) } // --------------------------------------------------------------------------- // crypto_pwhash — Argon2id // --------------------------------------------------------------------------- #[derive(Deserialize)] pub struct PwhashArgs { pub password: String, pub salt_b64: String, pub out_len: usize, // Opslimit / memlimit presets map to libsodium constants; callers pass // one of "interactive" | "moderate" | "sensitive". We default to // moderate which matches every current call-site. #[serde(default)] pub preset: Option, } fn pwhash_limits(preset: Option<&str>) -> (u64, usize) { match preset { Some("interactive") => (2, 64 * 1024 * 1024), Some("sensitive") => (4, 1024 * 1024 * 1024), _ => ( CRYPTO_PWHASH_OPSLIMIT_MODERATE as u64, CRYPTO_PWHASH_MEMLIMIT_MODERATE, ), } } #[tauri::command] pub fn crypto_pwhash(args: PwhashArgs) -> Result { let salt = decode(&args.salt_b64)?; if salt.len() != CRYPTO_PWHASH_SALTBYTES { return Err(format!( "salt must be {} bytes", CRYPTO_PWHASH_SALTBYTES )); } if args.out_len < 16 || args.out_len > 64 { return Err("out_len out of range (16..=64)".into()); } let salt_arr: [u8; CRYPTO_PWHASH_SALTBYTES] = salt.as_slice().try_into().unwrap(); let (opslimit, memlimit) = pwhash_limits(args.preset.as_deref()); let mut out = vec![0u8; args.out_len]; crypto_pwhash::crypto_pwhash( &mut out, args.password.as_bytes(), &salt_arr, opslimit, memlimit, PasswordHashAlgorithm::Argon2id13, ) .map_err(|e| format!("pwhash failed: {}", e))?; Ok(encode(&out)) }