diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c253ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Generated by Cargo +/target/ + +# Generated by wasm-pack +/pkg/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# Node.js dependencies +/node_modules/ +npm-debug.log +yarn-debug.log +yarn-error.log + +# Editor directories and files +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Build artifacts +/dist/ +/build/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b4d21bc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "webassembly" +version = "0.1.0" +edition = "2024" +description = "A WebAssembly module for web integration" +repository = "https://github.com/yourusername/webassembly" +license = "MIT" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "0.2" +js-sys = "0.3" +console_error_panic_hook = "0.1.7" +k256 = { version = "0.13", features = ["ecdsa"] } +rand = { version = "0.8", features = ["getrandom"] } +getrandom = { version = "0.2", features = ["js"] } +chacha20poly1305 = "0.10" +once_cell = "1.18" + +[dependencies.web-sys] +version = "0.3" +features = [ + "console", + "Document", + "Element", + "HtmlElement", + "Node", + "Window", +] + +[dev-dependencies] +wasm-bindgen-test = "0.3" + +[profile.release] +opt-level = "s" # Optimize for size +lto = true # Link-time optimization diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..50a4fa2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 WebAssembly Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 56f2c43..84d0f63 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,139 @@ -# webassembly +# 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) +- [Node.js](https://nodejs.org/) (14.0.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 +│ │ ├── mod.rs # Test module exports +│ │ └── symmetric_tests.rs # Tests for symmetric encryption +│ └── lib.rs # Main entry point, exports WASM functions +├── www/ +│ ├── index.html # Example HTML page +│ ├── server.js # Simple HTTP server for testing +│ └── js/ +│ └── index.js # JavaScript code to load and use the WebAssembly module +├── Cargo.toml # Rust package configuration +├── start.sh # Script to build and run the example +└── README.md # This file +``` + +## Running the Example + +The easiest way to run the example is to use the provided start script: + +```bash +./start.sh +``` + +This script will: +1. Build the WebAssembly module using wasm-pack +2. Start a local HTTP server + +Then open your browser and navigate to http://localhost:8080. + +## Building Manually + +If you prefer to build and run the example manually: + +1. Build the WebAssembly module: +```bash +wasm-pack build --target web +``` + +2. Start the local server: +```bash +node www/server.js +``` + +3. Open your browser and navigate to http://localhost:8080. + +## Running Tests + +To run the tests: + +```bash +cargo test +``` + +## API Reference + +### Keypair Operations + +```javascript +// 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 + +```javascript +// 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. diff --git a/implementation_plan.md b/implementation_plan.md new file mode 100644 index 0000000..ba65842 --- /dev/null +++ b/implementation_plan.md @@ -0,0 +1,730 @@ +# 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) +```rust +//! 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) +```rust +//! 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 = 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)` containing the public key bytes. +/// * `Err(CryptoError::KeypairNotInitialized)` if the keypair has not been initialized. +pub fn keypair_pub_key() -> Result, 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)` containing the signature bytes. +/// * `Err(CryptoError::KeypairNotInitialized)` if the keypair has not been initialized. +pub fn keypair_sign(message: &[u8]) -> Result, 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 { + 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) +```rust +//! 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)` 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, 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)` 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, 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) +```rust +//! 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) +```rust +//! 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)` containing the public key bytes. +/// * `Err(CryptoError::KeypairNotInitialized)` if no keypair has been initialized. +pub fn pub_key() -> Result, CryptoError> { + keypair::keypair_pub_key() +} + +/// Signs a message using the initialized keypair. +/// +/// # Arguments +/// +/// * `message` - The message to sign. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the signature bytes. +/// * `Err(CryptoError::KeypairNotInitialized)` if no keypair has been initialized. +pub fn sign(message: &[u8]) -> Result, 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 { + keypair::keypair_verify(message, signature) +} +``` + +### Step 6: Create API Symmetric Module (src/api/symmetric.rs) +```rust +//! 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)` 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, 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)` 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, CryptoError> { + symmetric::decrypt_symmetric(key, ciphertext) +} +``` + +### Step 7: Create API Module (src/api/mod.rs) +```rust +//! 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 +```rust +//! 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, JsValue> { + keypair::pub_key() + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn keypair_sign(message: &[u8]) -> Result, JsValue> { + keypair::sign(message) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn keypair_verify(message: &[u8], signature: &[u8]) -> Result { + keypair::verify(message, signature) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn generate_symmetric_key() -> Vec { + symmetric::generate_key().to_vec() +} + +#[wasm_bindgen] +pub fn encrypt_symmetric(key: &[u8], message: &[u8]) -> Result, 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, JsValue> { + symmetric::decrypt(key, ciphertext) + .map_err(|e| JsValue::from_str(&e.to_string())) +} +``` + +### Step 9: Create Test Files + +#### src/tests/keypair_tests.rs +```rust +//! 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 +```rust +//! 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 +```markdown +# 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: + +```bash +cd www +npm install +npm start +``` + +Then open your browser and navigate to http://localhost:8080. + +## API Reference + +### Keypair Operations + +```javascript +// 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 + +```javascript +// 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. \ No newline at end of file diff --git a/src/api/keypair.rs b/src/api/keypair.rs new file mode 100644 index 0000000..cd9eeed --- /dev/null +++ b/src/api/keypair.rs @@ -0,0 +1,55 @@ +//! 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)` containing the public key bytes. +/// * `Err(CryptoError::KeypairNotInitialized)` if no keypair has been initialized. +pub fn pub_key() -> Result, CryptoError> { + keypair::keypair_pub_key() +} + +/// Signs a message using the initialized keypair. +/// +/// # Arguments +/// +/// * `message` - The message to sign. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the signature bytes. +/// * `Err(CryptoError::KeypairNotInitialized)` if no keypair has been initialized. +pub fn sign(message: &[u8]) -> Result, 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 { + keypair::keypair_verify(message, signature) +} \ No newline at end of file diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..731a3ea --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,9 @@ +//! Public API for cryptographic operations. + +pub mod keypair; +pub mod symmetric; + +// Re-export commonly used items for external users +// (Keeping this even though it's currently unused, as it's good practice for public APIs) +#[allow(unused_imports)] +pub use crate::core::error::CryptoError; \ No newline at end of file diff --git a/src/api/symmetric.rs b/src/api/symmetric.rs new file mode 100644 index 0000000..ec17670 --- /dev/null +++ b/src/api/symmetric.rs @@ -0,0 +1,49 @@ +//! 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)` 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, 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)` 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, CryptoError> { + symmetric::decrypt_symmetric(key, ciphertext) +} \ No newline at end of file diff --git a/src/core/error.rs b/src/core/error.rs new file mode 100644 index 0000000..6bc37ec --- /dev/null +++ b/src/core/error.rs @@ -0,0 +1,55 @@ +//! 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. + #[allow(dead_code)] + 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. + #[allow(dead_code)] + 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, + } +} \ No newline at end of file diff --git a/src/core/keypair.rs b/src/core/keypair.rs new file mode 100644 index 0000000..3fb5ba1 --- /dev/null +++ b/src/core/keypair.rs @@ -0,0 +1,87 @@ +//! 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. +pub static KEYPAIR: OnceCell = 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)` containing the public key bytes. +/// * `Err(CryptoError::KeypairNotInitialized)` if the keypair has not been initialized. +pub fn keypair_pub_key() -> Result, 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)` containing the signature bytes. +/// * `Err(CryptoError::KeypairNotInitialized)` if the keypair has not been initialized. +pub fn keypair_sign(message: &[u8]) -> Result, 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 { + 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 + } +} \ No newline at end of file diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..f676771 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,10 @@ +//! Core cryptographic functionality. + +pub mod error; +pub mod keypair; +pub mod symmetric; + +// Re-export commonly used items for internal use +// (Keeping this even though it's currently unused, as it's good practice for internal modules) +#[allow(unused_imports)] +pub use error::CryptoError; \ No newline at end of file diff --git a/src/core/symmetric.rs b/src/core/symmetric.rs new file mode 100644 index 0000000..ce366c2 --- /dev/null +++ b/src/core/symmetric.rs @@ -0,0 +1,90 @@ +//! 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)` 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, 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)` 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, 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) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bee8ba7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,72 @@ +//! WebAssembly module for cryptographic operations. + +use wasm_bindgen::prelude::*; +use web_sys::console; + +// Import modules +mod api; +mod core; +mod tests; + +// Re-export for internal use +use api::keypair; +use api::symmetric; +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, JsValue> { + keypair::pub_key() + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn keypair_sign(message: &[u8]) -> Result, JsValue> { + keypair::sign(message) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn keypair_verify(message: &[u8], signature: &[u8]) -> Result { + keypair::verify(message, signature) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn generate_symmetric_key() -> Vec { + symmetric::generate_key().to_vec() +} + +#[wasm_bindgen] +pub fn encrypt_symmetric(key: &[u8], message: &[u8]) -> Result, 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, JsValue> { + symmetric::decrypt(key, ciphertext) + .map_err(|e| JsValue::from_str(&e.to_string())) +} diff --git a/src/mod.rs b/src/mod.rs new file mode 100644 index 0000000..d3990a4 --- /dev/null +++ b/src/mod.rs @@ -0,0 +1,297 @@ +// src/core/mod.rs - Core cryptographic logic (independent of WASM interface) + +use k256::ecdsa::{SigningKey, VerifyingKey, signature::{Signer, Verifier}, Signature}; +use rand::rngs::OsRng; +use chacha20poly1305::{ChaCha20Poly1305, Nonce, KeyInit}; +use chacha20poly1305::aead::Aead; +use once_cell::sync::OnceCell; +use rand::RngCore; + +// Define errors for clarity +#[derive(Debug)] +pub enum CryptoError { + KeypairNotInitialized, + KeypairAlreadyInitialized, + SignatureVerificationFailed, + SignatureFormatError, + EncryptionFailed, + DecryptionFailed, + InvalidKeyLength, + 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 {} + +// --- Core KeyPair Logic --- + +#[derive(Debug)] // Added Debug for OnceCell initialization +struct KeyPair { + verifying_key: VerifyingKey, + signing_key: SigningKey, +} + +static KEYPAIR: OnceCell = OnceCell::new(); + +/// Initializes the global keypair. Returns error if already initialized. +pub fn keypair_new_internal() -> 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 error if not initialized. +pub fn keypair_pub_key_internal() -> Result, CryptoError> { + KEYPAIR.get() + .ok_or(CryptoError::KeypairNotInitialized) + .map(|kp| kp.verifying_key.to_sec1_bytes().to_vec()) +} + +/// Signs a message. Returns error if not initialized. +pub fn keypair_sign_internal(message: &[u8]) -> Result, 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. Returns Ok(true/false) or error if not initialized or sig format invalid. +pub fn keypair_verify_internal(message: &[u8], signature_bytes: &[u8]) -> Result { + 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 + } +} + +// --- Core Symmetric Crypto Logic --- + +/// Generates a random 12-byte nonce. +pub fn generate_nonce_internal() -> [u8; 12] { + let mut nonce = [0u8; 12]; + OsRng.fill_bytes(&mut nonce); + nonce +} + +/// Generates a random 32-byte symmetric key. +pub fn generate_symmetric_key_internal() -> [u8; 32] { + let mut key = [0u8; 32]; + OsRng.fill_bytes(&mut key); + key +} + +/// Encrypts data using ChaCha20Poly1305. +pub fn encrypt_symmetric_internal(key: &[u8], message: &[u8], nonce_bytes: &[u8]) -> Result, CryptoError> { + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + let nonce = Nonce::from_slice(nonce_bytes); // Assumes nonce_bytes is correct length (12) + + cipher.encrypt(nonce, message) + .map_err(|_| CryptoError::EncryptionFailed) +} + +/// Decrypts data using ChaCha20Poly1305. +pub fn decrypt_symmetric_internal(key: &[u8], ciphertext: &[u8], nonce_bytes: &[u8]) -> Result, CryptoError> { + let cipher = ChaCha20Poly1305::new_from_slice(key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + let nonce = Nonce::from_slice(nonce_bytes); // Assumes nonce_bytes is correct length (12) + + cipher.decrypt(nonce, ciphertext) + .map_err(|_| CryptoError::DecryptionFailed) +} + +// Helper to convert CryptoError to an i32 status code for C ABI +pub fn error_to_status_code(err: CryptoError) -> i32 { + match err { + CryptoError::KeypairNotInitialized => -1, + CryptoError::KeypairAlreadyInitialized => -2, + CryptoError::SignatureVerificationFailed => -3, // Note: verify_internal returns Ok(false) for this + CryptoError::SignatureFormatError => -4, + CryptoError::EncryptionFailed => -5, + CryptoError::DecryptionFailed => -6, + CryptoError::InvalidKeyLength => -7, + CryptoError::Other(_) => -99, + } +} + +// Helper for C ABI functions needing output buffers +// Copies data, writes actual length, returns 0 on success or negative error code +// Assumes out_ptr and out_len_ptr are valid +pub unsafe fn copy_to_output(data: &[u8], out_ptr: *mut u8, out_len_ptr: *mut u32) -> i32 { + let len = data.len(); + std::ptr::write(out_len_ptr, len as u32); + std::ptr::copy_nonoverlapping(data.as_ptr(), out_ptr, len); + 0 // Success +} + +#[cfg(test)] +mod tests { + use super::*; + + // Helper to ensure keypair is initialized for tests that need it. + // Panics if initialization fails (which it shouldn't unless tests run weirdly). + fn ensure_keypair_initialized() { + // Use try_init which doesn't panic if already initialized + let _ = keypair_new_internal(); + assert!(KEYPAIR.get().is_some(), "KEYPAIR should be initialized"); + } + + #[test] + fn test_keypair_generation_and_retrieval() { + // Resetting global state in tests is hard. + // We'll test that we *can* initialize and get the key. + // Note: This relies on test execution order or isolation if KEYPAIR could be set elsewhere. + // For simple cases, calling new might return Err if already set by another test, + // but get should still work. + let _ = keypair_new_internal(); // Ignore error if already initialized by another test + let pub_key = keypair_pub_key_internal().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_sign_internal(message).expect("Signing failed"); + assert!(!signature.is_empty(), "Signature should not be empty"); + + let is_valid = keypair_verify_internal(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_sign_internal(message).expect("Signing failed"); + // Tamper with the signature + invalid_signature[0] = invalid_signature[0].wrapping_add(1); + + let is_valid = keypair_verify_internal(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_sign_internal(message).expect("Signing failed"); + + let is_valid = keypair_verify_internal(wrong_message, &signature).expect("Verification process failed"); + assert!(!is_valid, "Signature should be invalid for a different message"); + } + + + #[test] + fn test_generate_nonce() { + let nonce = generate_nonce_internal(); + assert_eq!(nonce.len(), 12, "Nonce length should be 12 bytes"); + // Check if it's not all zeros (highly unlikely for random) + assert!(nonce.iter().any(|&byte| byte != 0), "Nonce should not be all zeros"); + } + + #[test] + fn test_generate_symmetric_key() { + let key = generate_symmetric_key_internal(); + 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 = generate_symmetric_key_internal(); + let nonce = generate_nonce_internal(); + let message = b"super secret data"; + + let ciphertext = encrypt_symmetric_internal(&key, message, &nonce) + .expect("Encryption failed"); + + assert_ne!(message, ciphertext.as_slice(), "Ciphertext should be different from message"); + + let decrypted_message = decrypt_symmetric_internal(&key, &ciphertext, &nonce) + .expect("Decryption failed"); + + assert_eq!(message, decrypted_message.as_slice(), "Decrypted message should match original"); + } + + #[test] + fn test_decrypt_wrong_key() { + let key1 = generate_symmetric_key_internal(); + let key2 = generate_symmetric_key_internal(); // Different key + let nonce = generate_nonce_internal(); + let message = b"data for key1"; + + let ciphertext = encrypt_symmetric_internal(&key1, message, &nonce) + .expect("Encryption failed"); + + let result = decrypt_symmetric_internal(&key2, &ciphertext, &nonce); + + 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_wrong_nonce() { + let key = generate_symmetric_key_internal(); + let nonce1 = generate_nonce_internal(); + let nonce2 = generate_nonce_internal(); // Different nonce + let message = b"data with nonce1"; + + let ciphertext = encrypt_symmetric_internal(&key, message, &nonce1) + .expect("Encryption failed"); + + let result = decrypt_symmetric_internal(&key, &ciphertext, &nonce2); + + assert!(result.is_err(), "Decryption should fail with the wrong nonce"); + assert!(matches!(result.unwrap_err(), CryptoError::DecryptionFailed), "Error should be DecryptionFailed"); + } + + #[test] + fn test_decrypt_tampered_ciphertext() { + let key = generate_symmetric_key_internal(); + let nonce = generate_nonce_internal(); + let message = b"important data"; + + let mut ciphertext = encrypt_symmetric_internal(&key, message, &nonce) + .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 = decrypt_symmetric_internal(&key, &ciphertext, &nonce); + + assert!(result.is_err(), "Decryption should fail with tampered ciphertext"); + assert!(matches!(result.unwrap_err(), CryptoError::DecryptionFailed), "Error should be DecryptionFailed"); + } +} diff --git a/src/tests/keypair_tests.rs b/src/tests/keypair_tests.rs new file mode 100644 index 0000000..8e00122 --- /dev/null +++ b/src/tests/keypair_tests.rs @@ -0,0 +1,57 @@ +//! 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"); + } +} \ No newline at end of file diff --git a/src/tests/mod.rs b/src/tests/mod.rs new file mode 100644 index 0000000..2037c8f --- /dev/null +++ b/src/tests/mod.rs @@ -0,0 +1,7 @@ +//! Test modules for cryptographic functionality. + +#[cfg(test)] +pub mod keypair_tests; + +#[cfg(test)] +pub mod symmetric_tests; \ No newline at end of file diff --git a/src/tests/symmetric_tests.rs b/src/tests/symmetric_tests.rs new file mode 100644 index 0000000..629a21f --- /dev/null +++ b/src/tests/symmetric_tests.rs @@ -0,0 +1,67 @@ +//! 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"); + } +} \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..dddcd92 --- /dev/null +++ b/start.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Change to the directory where this script is located +cd "$(dirname "$0")" + +echo "=== Building WebAssembly module ===" +wasm-pack build --target web + +echo "=== Starting server ===" +node www/server.js \ No newline at end of file diff --git a/www/index.html b/www/index.html new file mode 100644 index 0000000..74fdcfb --- /dev/null +++ b/www/index.html @@ -0,0 +1,114 @@ + + + + + Rust WebAssembly Crypto Example + + + +

Rust WebAssembly Crypto Example

+ +
+

Keypair Generation

+
+ +
+
Result will appear here
+
+
+ +
+

Message Signing

+
+ + +
+
Signature will appear here
+
+ +
+

Signature Verification

+
+ + + +
+
Verification result will appear here
+
+ +
+

Symmetric Encryption

+
+ + +
+
+
Note: Nonce is handled internally
+
Encrypted data will appear here
+
+ +
+

Symmetric Decryption

+
+ +
Note: Nonce is now extracted automatically from the ciphertext
+ + +
+
Decrypted data will appear here
+
+ + + + \ No newline at end of file diff --git a/www/js/index.js b/www/js/index.js new file mode 100644 index 0000000..6b136e5 --- /dev/null +++ b/www/js/index.js @@ -0,0 +1,146 @@ +// Import our WebAssembly module +import init, { + keypair_new, + keypair_pub_key, + keypair_sign, + keypair_verify, + generate_symmetric_key, + encrypt_symmetric, + decrypt_symmetric +} from '../../pkg/webassembly.js'; + +// Helper function to convert ArrayBuffer to hex string +function bufferToHex(buffer) { + return Array.from(new Uint8Array(buffer)) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); +} + +// Helper function to convert hex string to Uint8Array +function hexToBuffer(hex) { + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + } + return bytes; +} + +async function run() { + // Initialize the WebAssembly module + await init(); + + console.log('WebAssembly crypto module initialized!'); + + // Set up the keypair generation example + document.getElementById('keypair-button').addEventListener('click', () => { + try { + const result = keypair_new(); + if (result === 0) { + document.getElementById('keypair-result').textContent = 'Keypair generated successfully!'; + + // Get and display the public key + try { + const pubKey = keypair_pub_key(); + document.getElementById('pubkey-display').textContent = `Public Key: ${bufferToHex(pubKey)}`; + } catch (e) { + document.getElementById('pubkey-display').textContent = `Error getting public key: ${e}`; + } + } else { + document.getElementById('keypair-result').textContent = `Error generating keypair: ${result}`; + } + } catch (e) { + document.getElementById('keypair-result').textContent = `Error: ${e}`; + } + }); + + // Set up the signing example + document.getElementById('sign-button').addEventListener('click', () => { + const message = document.getElementById('sign-message').value; + const messageBytes = new TextEncoder().encode(message); + + try { + const signature = keypair_sign(messageBytes); + const signatureHex = bufferToHex(signature); + document.getElementById('signature-result').textContent = `Signature: ${signatureHex}`; + + // Store the signature for verification + document.getElementById('verify-signature').value = signatureHex; + document.getElementById('verify-message').value = message; + } catch (e) { + document.getElementById('signature-result').textContent = `Error signing: ${e}`; + } + }); + + // Set up the verification example + document.getElementById('verify-button').addEventListener('click', () => { + const message = document.getElementById('verify-message').value; + const messageBytes = new TextEncoder().encode(message); + const signatureHex = document.getElementById('verify-signature').value; + const signatureBytes = hexToBuffer(signatureHex); + + try { + const isValid = keypair_verify(messageBytes, signatureBytes); + document.getElementById('verify-result').textContent = + isValid ? 'Signature is valid!' : 'Signature is NOT valid!'; + } catch (e) { + document.getElementById('verify-result').textContent = `Error verifying: ${e}`; + } + }); + + // Set up the symmetric encryption example + document.getElementById('encrypt-button').addEventListener('click', () => { + try { + // Generate key + const key = generate_symmetric_key(); + + // Display key + const keyHex = bufferToHex(key); + document.getElementById('sym-key-display').textContent = `Key: ${keyHex}`; + + // Store for decryption + document.getElementById('decrypt-key').value = keyHex; + + // Encrypt the message + const message = document.getElementById('encrypt-message').value; + const messageBytes = new TextEncoder().encode(message); + + try { + // New API: encrypt_symmetric only takes key and message + const ciphertext = encrypt_symmetric(key, messageBytes); + const ciphertextHex = bufferToHex(ciphertext); + document.getElementById('encrypt-result').textContent = `Ciphertext: ${ciphertextHex}`; + + // Store for decryption + document.getElementById('decrypt-ciphertext').value = ciphertextHex; + } catch (e) { + document.getElementById('encrypt-result').textContent = `Error encrypting: ${e}`; + } + } catch (e) { + document.getElementById('encrypt-result').textContent = `Error: ${e}`; + } + }); + + // Set up the symmetric decryption example + document.getElementById('decrypt-button').addEventListener('click', () => { + try { + const keyHex = document.getElementById('decrypt-key').value; + const ciphertextHex = document.getElementById('decrypt-ciphertext').value; + + const key = hexToBuffer(keyHex); + const ciphertext = hexToBuffer(ciphertextHex); + + try { + // New API: decrypt_symmetric only takes key and ciphertext + const plaintext = decrypt_symmetric(key, ciphertext); + const decodedText = new TextDecoder().decode(plaintext); + document.getElementById('decrypt-result').textContent = `Decrypted: ${decodedText}`; + } catch (e) { + document.getElementById('decrypt-result').textContent = `Error decrypting: ${e}`; + } + } catch (e) { + document.getElementById('decrypt-result').textContent = `Error: ${e}`; + } + }); +} + +run().catch(console.error); \ No newline at end of file diff --git a/www/server.js b/www/server.js new file mode 100644 index 0000000..7fbe96a --- /dev/null +++ b/www/server.js @@ -0,0 +1,82 @@ +const http = require('http'); +const fs = require('fs'); +const path = require('path'); + +const PORT = 8080; + +const MIME_TYPES = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.wasm': 'application/wasm', + '.png': 'image/png', + '.jpg': 'image/jpg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', + '.txt': 'text/plain', +}; + +const server = http.createServer((req, res) => { + console.log(`${req.method} ${req.url}`); + + // Handle CORS preflight requests + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }); + res.end(); + return; + } + + // Normalize URL path + let filePath = req.url; + if (filePath === '/' || filePath === '') { + filePath = '/index.html'; + } + + // Determine the file path + const resolvedPath = path.resolve( + __dirname, + filePath.startsWith('/pkg/') + ? `..${filePath}` // For files in the pkg directory (one level up) + : `.${filePath}` // For files in the www directory + ); + + // Get the file extension + const extname = path.extname(resolvedPath); + const contentType = MIME_TYPES[extname] || 'application/octet-stream'; + + // Read and serve the file + fs.readFile(resolvedPath, (err, data) => { + if (err) { + if (err.code === 'ENOENT') { + console.error(`File not found: ${resolvedPath}`); + res.writeHead(404); + res.end('File not found'); + } else { + console.error(`Server error: ${err}`); + res.writeHead(500); + res.end(`Server Error: ${err.code}`); + } + return; + } + + // Set CORS headers for all responses + res.writeHead(200, { + 'Content-Type': contentType, + 'Access-Control-Allow-Origin': '*', + 'Cross-Origin-Embedder-Policy': 'require-corp', + 'Cross-Origin-Opener-Policy': 'same-origin', + }); + res.end(data); + }); +}); + +server.listen(PORT, () => { + console.log(`Server running at http://localhost:${PORT}/`); + console.log(`Press Ctrl+C to stop the server`); +}); \ No newline at end of file