...
This commit is contained in:
parent
3c65e57676
commit
8d707e61a2
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal 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
38
Cargo.toml
Normal 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
21
LICENSE
Normal 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
139
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.
|
||||||
|
730
implementation_plan.md
Normal file
730
implementation_plan.md
Normal 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
55
src/api/keypair.rs
Normal 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
9
src/api/mod.rs
Normal 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
49
src/api/symmetric.rs
Normal 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
55
src/core/error.rs
Normal 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
87
src/core/keypair.rs
Normal 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
10
src/core/mod.rs
Normal 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
90
src/core/symmetric.rs
Normal 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
72
src/lib.rs
Normal 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
297
src/mod.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
57
src/tests/keypair_tests.rs
Normal file
57
src/tests/keypair_tests.rs
Normal 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
7
src/tests/mod.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//! Test modules for cryptographic functionality.
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod keypair_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod symmetric_tests;
|
67
src/tests/symmetric_tests.rs
Normal file
67
src/tests/symmetric_tests.rs
Normal 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
10
start.sh
Executable 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
114
www/index.html
Normal 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
146
www/js/index.js
Normal 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
82
www/server.js
Normal 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`);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user