synor/crates/synor-crypto-wasm/src/lib.rs
Gulshan Yadav 6094319ddf feat(crypto-wasm): add Dilithium3 post-quantum signatures
Implements WASM-compatible Dilithium3 (ML-DSA-65) signatures using the
pure Rust pqc_dilithium crate. This provides NIST Security Category 3
post-quantum signature support for the web wallet.

Changes:
- Add pqc_dilithium dependency with WASM feature
- Create DilithiumSigningKey wrapper for WASM bindings
- Add dilithiumVerify and dilithiumSizes helper functions
- Update tests to work on both native and WASM targets
- Update README to reflect completed Dilithium3 support

Key sizes (Dilithium3 / ML-DSA-65):
- Public Key: 1,952 bytes
- Signature: 3,293 bytes
2026-01-08 07:31:36 +05:30

264 lines
7.7 KiB
Rust

//! WASM-compatible cryptography for Synor web wallet.
//!
//! This crate provides cryptographic primitives that can be compiled to WebAssembly
//! for use in the Synor web wallet. It includes:
//!
//! - Ed25519 signature generation and verification
//! - Key derivation from BIP-39 mnemonics
//! - Bech32m address encoding
//! - Dilithium3 (ML-DSA-65) post-quantum signatures
//!
//! # Usage in JavaScript
//!
//! ```javascript
//! import init, { Keypair, Mnemonic, DilithiumSigningKey } from 'synor-crypto-wasm';
//!
//! await init();
//!
//! // Generate mnemonic
//! const mnemonic = Mnemonic.generate(24);
//! console.log(mnemonic.phrase());
//!
//! // Create Ed25519 keypair from mnemonic
//! const keypair = Keypair.fromMnemonic(mnemonic.phrase(), "");
//! console.log(keypair.address("mainnet"));
//!
//! // Sign and verify with Ed25519
//! const message = new TextEncoder().encode("Hello Synor!");
//! const signature = keypair.sign(message);
//! const isValid = keypair.verify(message, signature);
//!
//! // Post-quantum signatures with Dilithium3
//! const pqKey = new DilithiumSigningKey();
//! const pqSignature = pqKey.sign(message);
//! const pqValid = pqKey.verify(message, pqSignature);
//! ```
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use rand::rngs::OsRng;
use sha3::{Digest, Sha3_256};
use wasm_bindgen::prelude::*;
use zeroize::Zeroize;
mod address;
mod dilithium_wasm;
mod mnemonic_wasm;
pub use address::*;
pub use dilithium_wasm::*;
pub use mnemonic_wasm::*;
/// Initialize the WASM module.
#[wasm_bindgen(start)]
pub fn init() {
// WASM initialization - can be extended for panic hooks, logging, etc.
}
/// Ed25519 keypair for signing transactions.
#[wasm_bindgen]
pub struct Keypair {
signing_key: SigningKey,
#[wasm_bindgen(skip)]
pub seed: [u8; 32],
}
#[wasm_bindgen]
impl Keypair {
/// Generate a new random keypair.
#[wasm_bindgen(constructor)]
pub fn new() -> Result<Keypair, JsValue> {
let signing_key = SigningKey::generate(&mut OsRng);
let seed = signing_key.to_bytes();
Ok(Keypair { signing_key, seed })
}
/// Create a keypair from a 32-byte seed.
#[wasm_bindgen(js_name = fromSeed)]
pub fn from_seed(seed: &[u8]) -> Result<Keypair, JsValue> {
if seed.len() != 32 {
return Err(JsValue::from_str("Seed must be exactly 32 bytes"));
}
let mut seed_arr = [0u8; 32];
seed_arr.copy_from_slice(seed);
let signing_key = SigningKey::from_bytes(&seed_arr);
Ok(Keypair {
signing_key,
seed: seed_arr,
})
}
/// Create a keypair from a BIP-39 mnemonic phrase.
#[wasm_bindgen(js_name = fromMnemonic)]
pub fn from_mnemonic(phrase: &str, passphrase: &str) -> Result<Keypair, JsValue> {
let mnemonic = bip39::Mnemonic::from_phrase(phrase, bip39::Language::English)
.map_err(|e| JsValue::from_str(&format!("Invalid mnemonic: {:?}", e)))?;
// Derive seed from mnemonic
let seed = bip39::Seed::new(&mnemonic, passphrase);
let seed_bytes = seed.as_bytes();
// Use first 32 bytes for Ed25519
let mut ed_seed = [0u8; 32];
ed_seed.copy_from_slice(&seed_bytes[..32]);
let signing_key = SigningKey::from_bytes(&ed_seed);
Ok(Keypair {
signing_key,
seed: ed_seed,
})
}
/// Get the public key as hex string.
#[wasm_bindgen(js_name = publicKeyHex)]
pub fn public_key_hex(&self) -> String {
hex::encode(self.signing_key.verifying_key().to_bytes())
}
/// Get the public key as bytes.
#[wasm_bindgen(js_name = publicKeyBytes)]
pub fn public_key_bytes(&self) -> Vec<u8> {
self.signing_key.verifying_key().to_bytes().to_vec()
}
/// Get the Synor address for this keypair.
#[wasm_bindgen]
pub fn address(&self, network: &str) -> Result<String, JsValue> {
let pubkey = self.signing_key.verifying_key().to_bytes();
address::encode_address(network, &pubkey)
}
/// Sign a message.
#[wasm_bindgen]
pub fn sign(&self, message: &[u8]) -> Vec<u8> {
let signature = self.signing_key.sign(message);
signature.to_bytes().to_vec()
}
/// Verify a signature.
#[wasm_bindgen]
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, JsValue> {
if signature.len() != 64 {
return Err(JsValue::from_str("Signature must be 64 bytes"));
}
let sig_bytes: [u8; 64] = signature
.try_into()
.map_err(|_| JsValue::from_str("Invalid signature length"))?;
let sig = Signature::from_bytes(&sig_bytes);
use ed25519_dalek::Verifier;
Ok(self
.signing_key
.verifying_key()
.verify(message, &sig)
.is_ok())
}
}
impl Default for Keypair {
fn default() -> Self {
Self::new().expect("Failed to generate keypair")
}
}
impl Drop for Keypair {
fn drop(&mut self) {
self.seed.zeroize();
}
}
/// Verify a signature with a public key.
#[wasm_bindgen(js_name = verifyWithPublicKey)]
pub fn verify_with_public_key(
public_key: &[u8],
message: &[u8],
signature: &[u8],
) -> Result<bool, JsValue> {
if public_key.len() != 32 {
return Err(JsValue::from_str("Public key must be 32 bytes"));
}
if signature.len() != 64 {
return Err(JsValue::from_str("Signature must be 64 bytes"));
}
let pk_bytes: [u8; 32] = public_key
.try_into()
.map_err(|_| JsValue::from_str("Invalid public key"))?;
let sig_bytes: [u8; 64] = signature
.try_into()
.map_err(|_| JsValue::from_str("Invalid signature"))?;
let verifying_key = VerifyingKey::from_bytes(&pk_bytes)
.map_err(|_| JsValue::from_str("Invalid public key format"))?;
let signature = Signature::from_bytes(&sig_bytes);
use ed25519_dalek::Verifier;
Ok(verifying_key.verify(message, &signature).is_ok())
}
/// Compute SHA3-256 hash.
#[wasm_bindgen(js_name = sha3_256)]
pub fn sha3_256_hash(data: &[u8]) -> Vec<u8> {
let mut hasher = Sha3_256::new();
hasher.update(data);
hasher.finalize().to_vec()
}
/// Compute BLAKE3 hash.
#[wasm_bindgen(js_name = blake3)]
pub fn blake3_hash(data: &[u8]) -> Vec<u8> {
blake3::hash(data).as_bytes().to_vec()
}
/// Derive key using HKDF-SHA256.
#[wasm_bindgen(js_name = deriveKey)]
pub fn derive_key(
input_key: &[u8],
salt: &[u8],
info: &[u8],
output_len: usize,
) -> Result<Vec<u8>, JsValue> {
use hkdf::Hkdf;
use sha3::Sha3_256;
let hk = Hkdf::<Sha3_256>::new(Some(salt), input_key);
let mut output = vec![0u8; output_len];
hk.expand(info, &mut output)
.map_err(|_| JsValue::from_str("HKDF expansion failed"))?;
Ok(output)
}
// bip39 crate is used in mnemonic_wasm.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keypair_generation() {
let keypair = Keypair::new().unwrap();
assert_eq!(keypair.public_key_bytes().len(), 32);
}
#[test]
fn test_sign_verify() {
let keypair = Keypair::new().unwrap();
let message = b"Hello, Synor!";
let signature = keypair.sign(message);
assert!(keypair.verify(message, &signature).unwrap());
}
#[test]
fn test_address_generation() {
let keypair = Keypair::new().unwrap();
let address = keypair.address("mainnet").unwrap();
assert!(address.starts_with("synor1"));
}
#[test]
fn test_mnemonic_keypair() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let keypair = Keypair::from_mnemonic(phrase, "").unwrap();
assert_eq!(keypair.public_key_bytes().len(), 32);
}
}