This commit is contained in:
despiegk 2025-04-19 18:59:47 +02:00
parent 3c65e57676
commit 8d707e61a2
21 changed files with 2170 additions and 1 deletions

36
.gitignore vendored Normal file
View File

@ -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

38
Cargo.toml Normal file
View File

@ -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

21
LICENSE Normal file
View File

@ -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.

139
README.md
View File

@ -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.

730
implementation_plan.md Normal file
View File

@ -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<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)
```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<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)
```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<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)
```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<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)
```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<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
```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.

55
src/api/keypair.rs Normal file
View File

@ -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<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)
}

9
src/api/mod.rs Normal file
View File

@ -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;

49
src/api/symmetric.rs Normal file
View File

@ -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<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)
}

55
src/core/error.rs Normal file
View File

@ -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,
}
}

87
src/core/keypair.rs Normal file
View File

@ -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<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
}
}

10
src/core/mod.rs Normal file
View File

@ -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;

90
src/core/symmetric.rs Normal file
View File

@ -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<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)
}

72
src/lib.rs Normal file
View File

@ -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<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()))
}

297
src/mod.rs Normal file
View File

@ -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<KeyPair> = 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<Vec<u8>, 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<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. Returns Ok(true/false) or error if not initialized or sig format invalid.
pub fn keypair_verify_internal(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
}
}
// --- 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<Vec<u8>, 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<Vec<u8>, 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");
}
}

View File

@ -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");
}
}

7
src/tests/mod.rs Normal file
View File

@ -0,0 +1,7 @@
//! Test modules for cryptographic functionality.
#[cfg(test)]
pub mod keypair_tests;
#[cfg(test)]
pub mod symmetric_tests;

View File

@ -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");
}
}

10
start.sh Executable file
View File

@ -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

114
www/index.html Normal file
View File

@ -0,0 +1,114 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust WebAssembly Crypto Example</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.container {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
margin-bottom: 20px;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 4px;
}
input, textarea {
padding: 8px;
margin: 5px;
border: 1px solid #ddd;
border-radius: 4px;
width: 80%;
}
.result {
margin-top: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
word-break: break-all;
}
.key-display {
font-family: monospace;
font-size: 12px;
word-break: break-all;
}
.note {
font-style: italic;
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<h1>Rust WebAssembly Crypto Example</h1>
<div class="container">
<h2>Keypair Generation</h2>
<div>
<button id="keypair-button">Generate Keypair</button>
</div>
<div class="result" id="keypair-result">Result will appear here</div>
<div class="key-display" id="pubkey-display"></div>
</div>
<div class="container">
<h2>Message Signing</h2>
<div>
<textarea id="sign-message" placeholder="Enter message to sign" rows="3">Hello, this is a test message to sign</textarea>
<button id="sign-button">Sign Message</button>
</div>
<div class="result" id="signature-result">Signature will appear here</div>
</div>
<div class="container">
<h2>Signature Verification</h2>
<div>
<textarea id="verify-message" placeholder="Enter message to verify" rows="3"></textarea>
<textarea id="verify-signature" placeholder="Enter signature to verify" rows="3"></textarea>
<button id="verify-button">Verify Signature</button>
</div>
<div class="result" id="verify-result">Verification result will appear here</div>
</div>
<div class="container">
<h2>Symmetric Encryption</h2>
<div>
<textarea id="encrypt-message" placeholder="Enter message to encrypt" rows="3">This is a secret message that will be encrypted</textarea>
<button id="encrypt-button">Generate Key & Encrypt</button>
</div>
<div class="key-display" id="sym-key-display"></div>
<div class="note" id="nonce-display">Note: Nonce is handled internally</div>
<div class="result" id="encrypt-result">Encrypted data will appear here</div>
</div>
<div class="container">
<h2>Symmetric Decryption</h2>
<div>
<input type="text" id="decrypt-key" placeholder="Enter key (hex)" />
<div class="note">Note: Nonce is now extracted automatically from the ciphertext</div>
<textarea id="decrypt-ciphertext" placeholder="Enter ciphertext (hex)" rows="3"></textarea>
<button id="decrypt-button">Decrypt</button>
</div>
<div class="result" id="decrypt-result">Decrypted data will appear here</div>
</div>
<script type="module" src="./js/index.js"></script>
</body>
</html>

146
www/js/index.js Normal file
View File

@ -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);

82
www/server.js Normal file
View File

@ -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`);
});