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
264 lines
7.7 KiB
Rust
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);
|
|
}
|
|
}
|