webassembly/implementation_plan.md
2025-04-19 18:59:47 +02:00

23 KiB

Detailed Implementation Plan

1. Create Directory Structure

src/
├── lib.rs                 # Main entry point, exports WASM functions
├── api/                   # Public API modules
│   ├── mod.rs             # Re-exports public API functions
│   ├── keypair.rs         # Public keypair API
│   └── symmetric.rs       # Public symmetric encryption API
├── core/                  # Internal implementation modules
│   ├── mod.rs             # Re-exports core functionality
│   ├── error.rs           # Error types and conversions
│   ├── keypair.rs         # Core keypair implementation
│   └── symmetric.rs       # Core symmetric encryption implementation
└── tests/                 # Test modules
    ├── keypair_tests.rs   # Tests for keypair functionality
    └── symmetric_tests.rs # Tests for symmetric encryption

2. Implementation Steps

Step 1: Create Core Error Module (src/core/error.rs)

//! Error types for cryptographic operations.

/// Errors that can occur during cryptographic operations.
#[derive(Debug)]
pub enum CryptoError {
    /// The keypair has not been initialized.
    KeypairNotInitialized,
    /// The keypair has already been initialized.
    KeypairAlreadyInitialized,
    /// Signature verification failed.
    SignatureVerificationFailed,
    /// The signature format is invalid.
    SignatureFormatError,
    /// Encryption operation failed.
    EncryptionFailed,
    /// Decryption operation failed.
    DecryptionFailed,
    /// The key length is invalid.
    InvalidKeyLength,
    /// Other error with description.
    Other(String),
}

impl std::fmt::Display for CryptoError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            CryptoError::KeypairNotInitialized => write!(f, "Keypair not initialized"),
            CryptoError::KeypairAlreadyInitialized => write!(f, "Keypair already initialized"),
            CryptoError::SignatureVerificationFailed => write!(f, "Signature verification failed"),
            CryptoError::SignatureFormatError => write!(f, "Invalid signature format"),
            CryptoError::EncryptionFailed => write!(f, "Encryption failed"),
            CryptoError::DecryptionFailed => write!(f, "Decryption failed"),
            CryptoError::InvalidKeyLength => write!(f, "Invalid key length"),
            CryptoError::Other(s) => write!(f, "Crypto error: {}", s),
        }
    }
}

impl std::error::Error for CryptoError {}

/// Converts a CryptoError to an i32 status code for WebAssembly.
pub fn error_to_status_code(err: CryptoError) -> i32 {
    match err {
        CryptoError::KeypairNotInitialized => -1,
        CryptoError::KeypairAlreadyInitialized => -2,
        CryptoError::SignatureVerificationFailed => -3,
        CryptoError::SignatureFormatError => -4,
        CryptoError::EncryptionFailed => -5,
        CryptoError::DecryptionFailed => -6,
        CryptoError::InvalidKeyLength => -7,
        CryptoError::Other(_) => -99,
    }
}

Step 2: Create Core Keypair Module (src/core/keypair.rs)

//! Core implementation of keypair functionality.

use k256::ecdsa::{SigningKey, VerifyingKey, signature::{Signer, Verifier}, Signature};
use once_cell::sync::OnceCell;
use rand::rngs::OsRng;

use super::error::CryptoError;

/// A keypair for signing and verifying messages.
#[derive(Debug)]
pub struct KeyPair {
    pub verifying_key: VerifyingKey,
    pub signing_key: SigningKey,
}

/// Global keypair instance.
static KEYPAIR: OnceCell<KeyPair> = OnceCell::new();

/// Initializes the global keypair.
///
/// # Returns
///
/// * `Ok(())` if the keypair was initialized successfully.
/// * `Err(CryptoError::KeypairAlreadyInitialized)` if the keypair was already initialized.
pub fn keypair_new() -> Result<(), CryptoError> {
    let signing_key = SigningKey::random(&mut OsRng);
    let verifying_key = VerifyingKey::from(&signing_key);
    let keypair = KeyPair { verifying_key, signing_key };
    
    KEYPAIR.set(keypair).map_err(|_| CryptoError::KeypairAlreadyInitialized)
}

/// Gets the public key bytes.
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the public key bytes.
/// * `Err(CryptoError::KeypairNotInitialized)` if the keypair has not been initialized.
pub fn keypair_pub_key() -> Result<Vec<u8>, CryptoError> {
    KEYPAIR.get()
        .ok_or(CryptoError::KeypairNotInitialized)
        .map(|kp| kp.verifying_key.to_sec1_bytes().to_vec())
}

/// Signs a message.
///
/// # Arguments
///
/// * `message` - The message to sign.
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the signature bytes.
/// * `Err(CryptoError::KeypairNotInitialized)` if the keypair has not been initialized.
pub fn keypair_sign(message: &[u8]) -> Result<Vec<u8>, CryptoError> {
    KEYPAIR.get()
        .ok_or(CryptoError::KeypairNotInitialized)
        .map(|kp| {
            let signature: Signature = kp.signing_key.sign(message);
            signature.to_bytes().to_vec()
        })
}

/// Verifies a message signature.
///
/// # Arguments
///
/// * `message` - The message that was signed.
/// * `signature_bytes` - The signature to verify.
///
/// # Returns
///
/// * `Ok(true)` if the signature is valid.
/// * `Ok(false)` if the signature is invalid.
/// * `Err(CryptoError::KeypairNotInitialized)` if the keypair has not been initialized.
/// * `Err(CryptoError::SignatureFormatError)` if the signature format is invalid.
pub fn keypair_verify(message: &[u8], signature_bytes: &[u8]) -> Result<bool, CryptoError> {
    let keypair = KEYPAIR.get().ok_or(CryptoError::KeypairNotInitialized)?;
    
    let signature = Signature::from_bytes(signature_bytes.into())
        .map_err(|_| CryptoError::SignatureFormatError)?;
        
    match keypair.verifying_key.verify(message, &signature) {
        Ok(_) => Ok(true),
        Err(_) => Ok(false), // Verification failed, but operation was successful
    }
}

Step 3: Create Core Symmetric Module (src/core/symmetric.rs)

//! Core implementation of symmetric encryption functionality.

use chacha20poly1305::{ChaCha20Poly1305, KeyInit, Nonce};
use chacha20poly1305::aead::Aead;
use rand::{rngs::OsRng, RngCore};

use super::error::CryptoError;

/// The size of the nonce in bytes.
const NONCE_SIZE: usize = 12;

/// Generates a random 32-byte symmetric key.
///
/// # Returns
///
/// A 32-byte array containing the random key.
pub fn generate_symmetric_key() -> [u8; 32] {
    let mut key = [0u8; 32];
    OsRng.fill_bytes(&mut key);
    key
}

/// Encrypts data using ChaCha20Poly1305 with an internally generated nonce.
///
/// The nonce is appended to the ciphertext so it can be extracted during decryption.
///
/// # Arguments
///
/// * `key` - The encryption key (should be 32 bytes).
/// * `message` - The message to encrypt.
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the ciphertext with the nonce appended.
/// * `Err(CryptoError::InvalidKeyLength)` if the key length is invalid.
/// * `Err(CryptoError::EncryptionFailed)` if encryption fails.
pub fn encrypt_symmetric(key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
    // Create cipher
    let cipher = ChaCha20Poly1305::new_from_slice(key)
        .map_err(|_| CryptoError::InvalidKeyLength)?;
    
    // Generate random nonce
    let mut nonce_bytes = [0u8; NONCE_SIZE];
    OsRng.fill_bytes(&mut nonce_bytes);
    let nonce = Nonce::from_slice(&nonce_bytes);
    
    // Encrypt message
    let ciphertext = cipher.encrypt(nonce, message)
        .map_err(|_| CryptoError::EncryptionFailed)?;
    
    // Append nonce to ciphertext
    let mut result = ciphertext;
    result.extend_from_slice(&nonce_bytes);
    
    Ok(result)
}

/// Decrypts data using ChaCha20Poly1305, extracting the nonce from the ciphertext.
///
/// # Arguments
///
/// * `key` - The decryption key (should be 32 bytes).
/// * `ciphertext_with_nonce` - The ciphertext with the nonce appended.
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the decrypted message.
/// * `Err(CryptoError::InvalidKeyLength)` if the key length is invalid.
/// * `Err(CryptoError::DecryptionFailed)` if decryption fails or the ciphertext is too short.
pub fn decrypt_symmetric(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result<Vec<u8>, CryptoError> {
    // Check if ciphertext is long enough to contain a nonce
    if ciphertext_with_nonce.len() <= NONCE_SIZE {
        return Err(CryptoError::DecryptionFailed);
    }
    
    // Extract nonce from the end of ciphertext
    let ciphertext_len = ciphertext_with_nonce.len() - NONCE_SIZE;
    let ciphertext = &ciphertext_with_nonce[0..ciphertext_len];
    let nonce_bytes = &ciphertext_with_nonce[ciphertext_len..];
    
    // Create cipher
    let cipher = ChaCha20Poly1305::new_from_slice(key)
        .map_err(|_| CryptoError::InvalidKeyLength)?;
    
    let nonce = Nonce::from_slice(nonce_bytes);
    
    // Decrypt message
    cipher.decrypt(nonce, ciphertext)
        .map_err(|_| CryptoError::DecryptionFailed)
}

Step 4: Create Core Module (src/core/mod.rs)

//! Core cryptographic functionality.

pub mod error;
pub mod keypair;
pub mod symmetric;

// Re-export commonly used items
pub use error::CryptoError;

Step 5: Create API Keypair Module (src/api/keypair.rs)

//! Public API for keypair operations.

use crate::core::keypair;
use crate::core::error::CryptoError;

/// Initializes a new keypair for signing and verification.
///
/// # Returns
///
/// * `Ok(())` if the keypair was initialized successfully.
/// * `Err(CryptoError::KeypairAlreadyInitialized)` if a keypair was already initialized.
pub fn new() -> Result<(), CryptoError> {
    keypair::keypair_new()
}

/// Gets the public key of the initialized keypair.
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the public key bytes.
/// * `Err(CryptoError::KeypairNotInitialized)` if no keypair has been initialized.
pub fn pub_key() -> Result<Vec<u8>, CryptoError> {
    keypair::keypair_pub_key()
}

/// Signs a message using the initialized keypair.
///
/// # Arguments
///
/// * `message` - The message to sign.
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the signature bytes.
/// * `Err(CryptoError::KeypairNotInitialized)` if no keypair has been initialized.
pub fn sign(message: &[u8]) -> Result<Vec<u8>, CryptoError> {
    keypair::keypair_sign(message)
}

/// Verifies a signature against a message.
///
/// # Arguments
///
/// * `message` - The message that was signed.
/// * `signature` - The signature to verify.
///
/// # Returns
///
/// * `Ok(true)` if the signature is valid.
/// * `Ok(false)` if the signature is invalid.
/// * `Err(CryptoError::KeypairNotInitialized)` if no keypair has been initialized.
/// * `Err(CryptoError::SignatureFormatError)` if the signature format is invalid.
pub fn verify(message: &[u8], signature: &[u8]) -> Result<bool, CryptoError> {
    keypair::keypair_verify(message, signature)
}

Step 6: Create API Symmetric Module (src/api/symmetric.rs)

//! Public API for symmetric encryption operations.

use crate::core::symmetric;
use crate::core::error::CryptoError;

/// Generates a random 32-byte symmetric key.
///
/// # Returns
///
/// A 32-byte array containing the random key.
pub fn generate_key() -> [u8; 32] {
    symmetric::generate_symmetric_key()
}

/// Encrypts data using ChaCha20Poly1305.
///
/// A random nonce is generated internally and appended to the ciphertext.
///
/// # Arguments
///
/// * `key` - The encryption key (should be 32 bytes).
/// * `message` - The message to encrypt.
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the ciphertext with the nonce appended.
/// * `Err(CryptoError::InvalidKeyLength)` if the key length is invalid.
/// * `Err(CryptoError::EncryptionFailed)` if encryption fails.
pub fn encrypt(key: &[u8], message: &[u8]) -> Result<Vec<u8>, CryptoError> {
    symmetric::encrypt_symmetric(key, message)
}

/// Decrypts data using ChaCha20Poly1305.
///
/// The nonce is extracted from the end of the ciphertext.
///
/// # Arguments
///
/// * `key` - The decryption key (should be 32 bytes).
/// * `ciphertext` - The ciphertext with the nonce appended.
///
/// # Returns
///
/// * `Ok(Vec<u8>)` containing the decrypted message.
/// * `Err(CryptoError::InvalidKeyLength)` if the key length is invalid.
/// * `Err(CryptoError::DecryptionFailed)` if decryption fails or the ciphertext is too short.
pub fn decrypt(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
    symmetric::decrypt_symmetric(key, ciphertext)
}

Step 7: Create API Module (src/api/mod.rs)

//! Public API for cryptographic operations.

pub mod keypair;
pub mod symmetric;

// Re-export commonly used items
pub use crate::core::error::CryptoError;

Step 8: Update lib.rs

//! WebAssembly module for cryptographic operations.

use wasm_bindgen::prelude::*;
use web_sys::console;

// Import modules
mod api;
mod core;

// Re-export for internal use
use api::keypair;
use api::symmetric;
use core::error::CryptoError;
use core::error::error_to_status_code;

// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    // This provides better error messages in debug mode.
    // It's disabled in release mode so it doesn't bloat up the file size.
    #[cfg(debug_assertions)]
    console_error_panic_hook::set_once();

    console::log_1(&JsValue::from_str("Crypto module initialized"));

    Ok(())
}

// --- WebAssembly Exports ---

#[wasm_bindgen]
pub fn keypair_new() -> i32 {
    match keypair::new() {
        Ok(_) => 0, // Success
        Err(e) => error_to_status_code(e),
    }
}

#[wasm_bindgen]
pub fn keypair_pub_key() -> Result<Vec<u8>, JsValue> {
    keypair::pub_key()
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

#[wasm_bindgen]
pub fn keypair_sign(message: &[u8]) -> Result<Vec<u8>, JsValue> {
    keypair::sign(message)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

#[wasm_bindgen]
pub fn keypair_verify(message: &[u8], signature: &[u8]) -> Result<bool, JsValue> {
    keypair::verify(message, signature)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

#[wasm_bindgen]
pub fn generate_symmetric_key() -> Vec<u8> {
    symmetric::generate_key().to_vec()
}

#[wasm_bindgen]
pub fn encrypt_symmetric(key: &[u8], message: &[u8]) -> Result<Vec<u8>, JsValue> {
    symmetric::encrypt(key, message)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

#[wasm_bindgen]
pub fn decrypt_symmetric(key: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, JsValue> {
    symmetric::decrypt(key, ciphertext)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

Step 9: Create Test Files

src/tests/keypair_tests.rs

//! Tests for keypair functionality.

#[cfg(test)]
mod tests {
    use crate::core::keypair;

    // Helper to ensure keypair is initialized for tests that need it.
    fn ensure_keypair_initialized() {
        // Use try_init which doesn't panic if already initialized
        let _ = keypair::keypair_new(); 
        assert!(keypair::KEYPAIR.get().is_some(), "KEYPAIR should be initialized");
    }

    #[test]
    fn test_keypair_generation_and_retrieval() {
        let _ = keypair::keypair_new(); // Ignore error if already initialized by another test
        let pub_key = keypair::keypair_pub_key().expect("Should be able to get pub key after init");
        assert!(!pub_key.is_empty(), "Public key should not be empty");
        // Basic check for SEC1 format (0x02, 0x03, or 0x04 prefix)
        assert!(pub_key.len() == 33 || pub_key.len() == 65, "Public key length is incorrect");
        assert!(pub_key[0] == 0x02 || pub_key[0] == 0x03 || pub_key[0] == 0x04, "Invalid SEC1 format start byte");
    }

    #[test]
    fn test_sign_verify_valid() {
        ensure_keypair_initialized();
        let message = b"this is a test message";
        let signature = keypair::keypair_sign(message).expect("Signing failed");
        assert!(!signature.is_empty(), "Signature should not be empty");

        let is_valid = keypair::keypair_verify(message, &signature).expect("Verification failed");
        assert!(is_valid, "Signature should be valid");
    }

    #[test]
    fn test_verify_invalid_signature() {
        ensure_keypair_initialized();
        let message = b"another test message";
        let mut invalid_signature = keypair::keypair_sign(message).expect("Signing failed");
        // Tamper with the signature
        invalid_signature[0] = invalid_signature[0].wrapping_add(1); 

        let is_valid = keypair::keypair_verify(message, &invalid_signature).expect("Verification process failed");
        assert!(!is_valid, "Tampered signature should be invalid");
    }
    
    #[test]
    fn test_verify_wrong_message() {
        ensure_keypair_initialized();
        let message = b"original message";
        let wrong_message = b"different message";
        let signature = keypair::keypair_sign(message).expect("Signing failed");

        let is_valid = keypair::keypair_verify(wrong_message, &signature).expect("Verification process failed");
        assert!(!is_valid, "Signature should be invalid for a different message");
    }
}

src/tests/symmetric_tests.rs

//! Tests for symmetric encryption functionality.

#[cfg(test)]
mod tests {
    use crate::core::symmetric;
    use crate::core::error::CryptoError;

    #[test]
    fn test_generate_symmetric_key() {
        let key = symmetric::generate_symmetric_key();
        assert_eq!(key.len(), 32, "Symmetric key length should be 32 bytes");
        // Check if it's not all zeros (highly unlikely for random)
        assert!(key.iter().any(|&byte| byte != 0), "Key should not be all zeros");
    }

    #[test]
    fn test_encrypt_decrypt_symmetric() {
        let key = symmetric::generate_symmetric_key();
        let message = b"super secret data";

        let ciphertext = symmetric::encrypt_symmetric(&key, message)
            .expect("Encryption failed");
        
        assert_ne!(message, ciphertext.as_slice(), "Ciphertext should be different from message");

        let decrypted_message = symmetric::decrypt_symmetric(&key, &ciphertext)
            .expect("Decryption failed");

        assert_eq!(message, decrypted_message.as_slice(), "Decrypted message should match original");
    }
    
    #[test]
    fn test_decrypt_wrong_key() {
        let key1 = symmetric::generate_symmetric_key();
        let key2 = symmetric::generate_symmetric_key(); // Different key
        let message = b"data for key1";

        let ciphertext = symmetric::encrypt_symmetric(&key1, message)
            .expect("Encryption failed");

        let result = symmetric::decrypt_symmetric(&key2, &ciphertext);
        
        assert!(result.is_err(), "Decryption should fail with the wrong key");
        assert!(matches!(result.unwrap_err(), CryptoError::DecryptionFailed), "Error should be DecryptionFailed");
    }

    #[test]
    fn test_decrypt_tampered_ciphertext() {
        let key = symmetric::generate_symmetric_key();
        let message = b"important data";

        let mut ciphertext = symmetric::encrypt_symmetric(&key, message)
            .expect("Encryption failed");
            
        // Tamper with ciphertext (e.g., flip a bit)
        if !ciphertext.is_empty() {
            ciphertext[0] ^= 0x01; 
        } else {
            panic!("Ciphertext is empty, cannot tamper");
        }

        let result = symmetric::decrypt_symmetric(&key, &ciphertext);
        
        assert!(result.is_err(), "Decryption should fail with tampered ciphertext");
        assert!(matches!(result.unwrap_err(), CryptoError::DecryptionFailed), "Error should be DecryptionFailed");
    }
}

Step 10: Update README.md

# Rust WebAssembly Cryptography Module

This project provides a WebAssembly module written in Rust that offers cryptographic functionality for web applications.

## Features

- **Asymmetric Cryptography**
  - ECDSA keypair generation
  - Message signing
  - Signature verification

- **Symmetric Cryptography**
  - ChaCha20Poly1305 encryption/decryption
  - Secure key generation

## Prerequisites

Before you begin, ensure you have the following installed:

- [Rust](https://www.rust-lang.org/tools/install) (1.70.0 or later)
- [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) (0.10.0 or later)
- A modern web browser that supports WebAssembly

## Project Structure

webassembly/ ├── src/ │ ├── api/ # Public API modules │ │ ├── keypair.rs # Public keypair API │ │ ├── mod.rs # API module exports │ │ └── symmetric.rs # Public symmetric encryption API │ ├── core/ # Internal implementation modules │ │ ├── error.rs # Error types and conversions │ │ ├── keypair.rs # Core keypair implementation │ │ ├── mod.rs # Core module exports │ │ └── symmetric.rs # Core symmetric encryption implementation │ ├── tests/ # Test modules │ │ ├── keypair_tests.rs # Tests for keypair functionality │ │ └── symmetric_tests.rs # Tests for symmetric encryption │ └── lib.rs # Main entry point, exports WASM functions ├── www/ │ ├── index.html # Example HTML page │ └── js/ │ └── index.js # JavaScript code to load and use the WebAssembly module ├── Cargo.toml # Rust package configuration └── README.md # This file


## Building the WebAssembly Module

To build the WebAssembly module, run:

```bash
wasm-pack build --target web

This will create a pkg directory containing the compiled WebAssembly module and JavaScript bindings.

Running the Example

After building the WebAssembly module, you can run the example using a local web server:

cd www
npm install
npm start

Then open your browser and navigate to http://localhost:8080.

API Reference

Keypair Operations

// Initialize a new keypair
const result = await wasm.keypair_new();
if (result === 0) {
  console.log("Keypair initialized successfully");
}

// Get the public key
const pubKey = await wasm.keypair_pub_key();

// Sign a message
const message = new TextEncoder().encode("Hello, world!");
const signature = await wasm.keypair_sign(message);

// Verify a signature
const isValid = await wasm.keypair_verify(message, signature);
console.log("Signature valid:", isValid);

Symmetric Encryption

// Generate a symmetric key
const key = wasm.generate_symmetric_key();

// Encrypt a message
const message = new TextEncoder().encode("Secret message");
const ciphertext = await wasm.encrypt_symmetric(key, message);

// Decrypt a message
const decrypted = await wasm.decrypt_symmetric(key, ciphertext);
const decryptedText = new TextDecoder().decode(decrypted);
console.log("Decrypted:", decryptedText);

Security Considerations

  • The keypair is stored in memory and is not persisted between page reloads.
  • The symmetric encryption uses ChaCha20Poly1305, which provides authenticated encryption.
  • The nonce for symmetric encryption is generated randomly and appended to the ciphertext.

License

This project is licensed under the MIT License - see the LICENSE file for details.