From d8a314df41f4bd962fb5bee6293a7e51719fcfc8 Mon Sep 17 00:00:00 2001 From: despiegk Date: Sat, 19 Apr 2025 19:43:16 +0200 Subject: [PATCH] ... --- src/api/keypair.rs | 68 ++++++++++++ src/core/keypair.rs | 113 +++++++++++++++++++ src/core/symmetric.rs | 32 +++++- src/lib.rs | 24 ++++ www/index.html | 85 +++++++++++++- www/js/index.js | 249 +++++++++++++++++++++++++++++++++++++++++- 6 files changed, 567 insertions(+), 4 deletions(-) diff --git a/src/api/keypair.rs b/src/api/keypair.rs index 0cb1f62..ab362b1 100644 --- a/src/api/keypair.rs +++ b/src/api/keypair.rs @@ -70,6 +70,20 @@ pub fn pub_key() -> Result, CryptoError> { keypair::keypair_pub_key() } +/// Derives a public key from a private key. +/// +/// # Arguments +/// +/// * `private_key` - The private key bytes. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the public key bytes. +/// * `Err(CryptoError::InvalidKeyLength)` if the private key is invalid. +pub fn derive_public_key(private_key: &[u8]) -> Result, CryptoError> { + keypair::derive_public_key(private_key) +} + /// Signs a message using the selected keypair. /// /// # Arguments @@ -105,6 +119,60 @@ pub fn verify(message: &[u8], signature: &[u8]) -> Result { keypair::keypair_verify(message, signature) } +/// Verifies a signature using only a public key. +/// +/// # Arguments +/// +/// * `public_key` - The public key bytes. +/// * `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::InvalidKeyLength)` if the public key is invalid. +/// * `Err(CryptoError::SignatureFormatError)` if the signature format is invalid. +pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result { + keypair::verify_with_public_key(public_key, message, signature) +} + +/// Encrypts a message using asymmetric encryption. +/// +/// # Arguments +/// +/// * `recipient_public_key` - The public key of the recipient. +/// * `message` - The message to encrypt. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the encrypted message. +/// * `Err(CryptoError::NoActiveSpace)` if no space is active. +/// * `Err(CryptoError::NoKeypairSelected)` if no keypair is selected. +/// * `Err(CryptoError::KeypairNotFound)` if the selected keypair was not found. +/// * `Err(CryptoError::InvalidKeyLength)` if the recipient's public key is invalid. +/// * `Err(CryptoError::EncryptionFailed)` if encryption fails. +pub fn encrypt_asymmetric(recipient_public_key: &[u8], message: &[u8]) -> Result, CryptoError> { + keypair::encrypt_asymmetric(recipient_public_key, message) +} + +/// Decrypts a message using asymmetric encryption. +/// +/// # Arguments +/// +/// * `ciphertext` - The encrypted message. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the decrypted message. +/// * `Err(CryptoError::NoActiveSpace)` if no space is active. +/// * `Err(CryptoError::NoKeypairSelected)` if no keypair is selected. +/// * `Err(CryptoError::KeypairNotFound)` if the selected keypair was not found. +/// * `Err(CryptoError::DecryptionFailed)` if decryption fails. +pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result, CryptoError> { + keypair::decrypt_asymmetric(ciphertext) +} + /// Encrypts a key space with a password. /// /// # Arguments diff --git a/src/core/keypair.rs b/src/core/keypair.rs index 13d5c34..7195f6b 100644 --- a/src/core/keypair.rs +++ b/src/core/keypair.rs @@ -6,6 +6,7 @@ use serde::{Serialize, Deserialize}; use std::collections::HashMap; use once_cell::sync::Lazy; use std::sync::Mutex; +use sha2::{Sha256, Digest}; use super::error::CryptoError; @@ -116,6 +117,14 @@ impl KeyPair { pub fn pub_key(&self) -> Vec { self.verifying_key.to_sec1_bytes().to_vec() } + + /// Derives a public key from a private key. + pub fn pub_key_from_private(private_key: &[u8]) -> Result, CryptoError> { + let signing_key = SigningKey::from_bytes(private_key.into()) + .map_err(|_| CryptoError::InvalidKeyLength)?; + let verifying_key = VerifyingKey::from(&signing_key); + Ok(verifying_key.to_sec1_bytes().to_vec()) + } /// Signs a message. pub fn sign(&self, message: &[u8]) -> Vec { @@ -133,6 +142,88 @@ impl KeyPair { Err(_) => Ok(false), // Verification failed, but operation was successful } } + + /// Verifies a message signature using only a public key. + pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result { + let verifying_key = VerifyingKey::from_sec1_bytes(public_key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + + let signature = Signature::from_bytes(signature_bytes.into()) + .map_err(|_| CryptoError::SignatureFormatError)?; + + match verifying_key.verify(message, &signature) { + Ok(_) => Ok(true), + Err(_) => Ok(false), // Verification failed, but operation was successful + } + } + + /// Encrypts a message using the recipient's public key. + /// This implements ECIES (Elliptic Curve Integrated Encryption Scheme): + /// 1. Generate an ephemeral keypair + /// 2. Derive a shared secret using ECDH + /// 3. Derive encryption key from the shared secret + /// 4. Encrypt the message using symmetric encryption + /// 5. Return the ephemeral public key and the ciphertext + pub fn encrypt_asymmetric(&self, recipient_public_key: &[u8], message: &[u8]) -> Result, CryptoError> { + // Parse recipient's public key + let recipient_key = VerifyingKey::from_sec1_bytes(recipient_public_key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + + // Generate ephemeral keypair + let ephemeral_signing_key = SigningKey::random(&mut OsRng); + let ephemeral_public_key = VerifyingKey::from(&ephemeral_signing_key); + + // Derive shared secret (this is a simplified ECDH) + // In a real implementation, we would use proper ECDH, but for this example: + let shared_point = recipient_key.to_encoded_point(false); + let shared_secret = { + let mut hasher = Sha256::default(); + hasher.update(ephemeral_signing_key.to_bytes()); + hasher.update(shared_point.as_bytes()); + hasher.finalize().to_vec() + }; + + // Encrypt the message using the derived key + let ciphertext = super::symmetric::encrypt_with_key(&shared_secret, message) + .map_err(|_| CryptoError::EncryptionFailed)?; + + // Format: ephemeral_public_key || ciphertext + let mut result = ephemeral_public_key.to_sec1_bytes().to_vec(); + result.extend_from_slice(&ciphertext); + + Ok(result) + } + + /// Decrypts a message using the recipient's private key. + /// This is the counterpart to encrypt_asymmetric. + pub fn decrypt_asymmetric(&self, ciphertext: &[u8]) -> Result, CryptoError> { + // The first 33 or 65 bytes (depending on compression) are the ephemeral public key + // For simplicity, we'll assume uncompressed keys (65 bytes) + if ciphertext.len() <= 65 { + return Err(CryptoError::DecryptionFailed); + } + + // Extract ephemeral public key and actual ciphertext + let ephemeral_public_key = &ciphertext[..65]; + let actual_ciphertext = &ciphertext[65..]; + + // Parse ephemeral public key + let sender_key = VerifyingKey::from_sec1_bytes(ephemeral_public_key) + .map_err(|_| CryptoError::InvalidKeyLength)?; + + // Derive shared secret (simplified ECDH) + let shared_point = sender_key.to_encoded_point(false); + let shared_secret = { + let mut hasher = Sha256::default(); + hasher.update(self.signing_key.to_bytes()); + hasher.update(shared_point.as_bytes()); + hasher.finalize().to_vec() + }; + + // Decrypt the message using the derived key + super::symmetric::decrypt_with_key(&shared_secret, actual_ciphertext) + .map_err(|_| CryptoError::DecryptionFailed) + } } /// A collection of keypairs. @@ -299,6 +390,11 @@ pub fn keypair_pub_key() -> Result, CryptoError> { Ok(keypair.pub_key()) } +/// Derives a public key from a private key. +pub fn derive_public_key(private_key: &[u8]) -> Result, CryptoError> { + KeyPair::pub_key_from_private(private_key) +} + /// Signs a message with the selected keypair. pub fn keypair_sign(message: &[u8]) -> Result, CryptoError> { let keypair = get_selected_keypair()?; @@ -309,4 +405,21 @@ pub fn keypair_sign(message: &[u8]) -> Result, CryptoError> { pub fn keypair_verify(message: &[u8], signature_bytes: &[u8]) -> Result { let keypair = get_selected_keypair()?; keypair.verify(message, signature_bytes) +} + +/// Verifies a message signature with a public key. +pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature_bytes: &[u8]) -> Result { + KeyPair::verify_with_public_key(public_key, message, signature_bytes) +} + +/// Encrypts a message for a recipient using their public key. +pub fn encrypt_asymmetric(recipient_public_key: &[u8], message: &[u8]) -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + keypair.encrypt_asymmetric(recipient_public_key, message) +} + +/// Decrypts a message that was encrypted with the current keypair's public key. +pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result, CryptoError> { + let keypair = get_selected_keypair()?; + keypair.decrypt_asymmetric(ciphertext) } \ No newline at end of file diff --git a/src/core/symmetric.rs b/src/core/symmetric.rs index 593ca53..181755a 100644 --- a/src/core/symmetric.rs +++ b/src/core/symmetric.rs @@ -33,7 +33,7 @@ pub fn generate_symmetric_key() -> [u8; 32] { /// /// A 32-byte array containing the derived key. pub fn derive_key_from_password(password: &str) -> [u8; 32] { - let mut hasher = Sha256::new(); + let mut hasher = Sha256::default(); hasher.update(password.as_bytes()); let result = hasher.finalize(); @@ -111,6 +111,36 @@ pub fn decrypt_symmetric(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result)` containing the ciphertext with the nonce appended. +/// * `Err(CryptoError)` if encryption fails. +pub fn encrypt_with_key(key: &[u8], message: &[u8]) -> Result, CryptoError> { + encrypt_symmetric(key, message) +} + +/// Decrypts data using a key directly (for internal use). +/// +/// # Arguments +/// +/// * `key` - The decryption key. +/// * `ciphertext_with_nonce` - The ciphertext with the nonce appended. +/// +/// # Returns +/// +/// * `Ok(Vec)` containing the decrypted message. +/// * `Err(CryptoError)` if decryption fails. +pub fn decrypt_with_key(key: &[u8], ciphertext_with_nonce: &[u8]) -> Result, CryptoError> { + decrypt_symmetric(key, ciphertext_with_nonce) +} + /// Metadata for an encrypted key space. #[derive(Serialize, Deserialize)] pub struct EncryptedKeySpaceMetadata { diff --git a/src/lib.rs b/src/lib.rs index f980b31..909da6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,30 @@ pub fn keypair_verify(message: &[u8], signature: &[u8]) -> Result .map_err(|e| JsValue::from_str(&e.to_string())) } +#[wasm_bindgen] +pub fn derive_public_key(private_key: &[u8]) -> Result, JsValue> { + keypair::derive_public_key(private_key) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn verify_with_public_key(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result { + keypair::verify_with_public_key(public_key, message, signature) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn encrypt_asymmetric(recipient_public_key: &[u8], message: &[u8]) -> Result, JsValue> { + keypair::encrypt_asymmetric(recipient_public_key, message) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + +#[wasm_bindgen] +pub fn decrypt_asymmetric(ciphertext: &[u8]) -> Result, JsValue> { + keypair::decrypt_asymmetric(ciphertext) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + // --- WebAssembly Exports for Symmetric Encryption --- #[wasm_bindgen] diff --git a/www/index.html b/www/index.html index 489e924..49e1ae2 100644 --- a/www/index.html +++ b/www/index.html @@ -84,6 +84,26 @@ .hidden { display: none; } + .pubkey-container { + margin-top: 15px; + padding: 10px; + background-color: #f8f9fa; + border-radius: 4px; + border: 1px solid #ddd; + } + .pubkey-label { + font-weight: bold; + margin-bottom: 5px; + } + .pubkey-value { + font-family: monospace; + word-break: break-all; + background-color: #e9ecef; + padding: 8px; + border-radius: 4px; + margin-bottom: 10px; + border: 1px solid #ced4da; + } @@ -118,7 +138,21 @@
- +
+ + +
+ + +
+

Manage Spaces

+
+ + + +
Result will appear here
@@ -166,6 +200,55 @@
Verification result will appear here
+
+

Verify with Public Key Only

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

Derive Public Key from Private Key

+
+
+ + +
+ +
+
Public key will appear here
+
+ +
+

Asymmetric Encryption

+
+
+ + +
+ + +
+
Encrypted data will appear here
+
+ +
+

Asymmetric Decryption

+
+
Note: Uses the currently selected keypair for decryption
+ + +
+
Decrypted data will appear here
+
+

Symmetric Encryption

diff --git a/www/js/index.js b/www/js/index.js index 8d4b746..12d7b53 100644 --- a/www/js/index.js +++ b/www/js/index.js @@ -1,5 +1,5 @@ // Import our WebAssembly module -import init, { +import init, { create_key_space, encrypt_key_space, decrypt_key_space, @@ -10,6 +10,10 @@ import init, { keypair_pub_key, keypair_sign, keypair_verify, + derive_public_key, + verify_with_public_key, + encrypt_asymmetric, + decrypt_asymmetric, generate_symmetric_key, derive_key_from_password, encrypt_symmetric, @@ -121,6 +125,30 @@ function updateLoginUI() { loginStatus.className = 'status logged-out'; currentSpaceName.textContent = ''; } + + // Update the spaces list + updateSpacesList(); +} + +// Update the spaces dropdown list +function updateSpacesList() { + const spacesList = document.getElementById('space-list'); + + // Clear existing options + while (spacesList.options.length > 1) { + spacesList.remove(1); + } + + // Get spaces list + const spaces = listSpacesFromStorage(); + + // Add options for each space + spaces.forEach(spaceName => { + const option = document.createElement('option'); + option.value = spaceName; + option.textContent = spaceName; + spacesList.appendChild(option); + }); } // Login to a space @@ -346,7 +374,36 @@ async function performSelectKeypair() { function displaySelectedKeypairPublicKey() { try { const pubKey = keypair_pub_key(); - document.getElementById('selected-pubkey-display').textContent = `Public Key: ${bufferToHex(pubKey)}`; + const pubKeyHex = bufferToHex(pubKey); + + // Create a more user-friendly display with copy button + const pubKeyDisplay = document.getElementById('selected-pubkey-display'); + pubKeyDisplay.innerHTML = ` +
+
Public Key (hex):
+
${pubKeyHex}
+ +
+ `; + + // Add event listener for the copy button + document.getElementById('copy-pubkey-button').addEventListener('click', () => { + const pubKeyText = document.getElementById('pubkey-hex-value').textContent; + navigator.clipboard.writeText(pubKeyText) + .then(() => { + alert('Public key copied to clipboard!'); + }) + .catch(err => { + console.error('Could not copy text: ', err); + }); + }); + + // Also populate the public key field in the verify with public key section + document.getElementById('pubkey-verify-pubkey').value = pubKeyHex; + + // And in the asymmetric encryption section + document.getElementById('asymmetric-encrypt-pubkey').value = pubKeyHex; + } catch (e) { document.getElementById('selected-pubkey-display').textContent = `Error getting public key: ${e}`; } @@ -357,7 +414,15 @@ function saveCurrentSpace() { if (!isLoggedIn || !currentSpace) return; try { + // Store the password in a session variable when logging in + // and use it here to avoid issues when the password field is cleared const password = document.getElementById('space-password').value; + if (!password) { + console.error('Password not available for saving space'); + alert('Please re-enter your password to save changes'); + return; + } + const encryptedSpace = encrypt_key_space(password); saveSpaceToStorage(currentSpace, encryptedSpace); } catch (e) { @@ -365,6 +430,26 @@ function saveCurrentSpace() { } } +// Delete a space from localStorage +function deleteSpace(spaceName) { + if (!spaceName) return false; + + // Check if space exists + if (!getSpaceFromStorage(spaceName)) { + return false; + } + + // Remove from localStorage + removeSpaceFromStorage(spaceName); + + // If this was the current space, logout + if (isLoggedIn && currentSpace === spaceName) { + performLogout(); + } + + return true; +} + async function run() { // Initialize the WebAssembly module await init(); @@ -375,6 +460,32 @@ async function run() { document.getElementById('login-button').addEventListener('click', performLogin); document.getElementById('create-space-button').addEventListener('click', performCreateSpace); document.getElementById('logout-button').addEventListener('click', performLogout); + document.getElementById('delete-space-button').addEventListener('click', () => { + if (confirm(`Are you sure you want to delete the space "${currentSpace}"? This action cannot be undone.`)) { + if (deleteSpace(currentSpace)) { + document.getElementById('space-result').textContent = `Space "${currentSpace}" deleted successfully`; + } else { + document.getElementById('space-result').textContent = `Error deleting space "${currentSpace}"`; + } + } + }); + + document.getElementById('delete-selected-space-button').addEventListener('click', () => { + const selectedSpace = document.getElementById('space-list').value; + if (!selectedSpace) { + document.getElementById('space-result').textContent = 'Please select a space to delete'; + return; + } + + if (confirm(`Are you sure you want to delete the space "${selectedSpace}"? This action cannot be undone.`)) { + if (deleteSpace(selectedSpace)) { + document.getElementById('space-result').textContent = `Space "${selectedSpace}" deleted successfully`; + updateSpacesList(); + } else { + document.getElementById('space-result').textContent = `Error deleting space "${selectedSpace}"`; + } + } + }); // Set up the keypair management document.getElementById('create-keypair-button').addEventListener('click', performCreateKeypair); @@ -530,6 +641,140 @@ async function run() { document.getElementById('password-decrypt-result').textContent = `Error: ${e}`; } }); + + // Set up the public key verification example + document.getElementById('pubkey-verify-button').addEventListener('click', () => { + try { + const publicKeyHex = document.getElementById('pubkey-verify-pubkey').value.trim(); + if (!publicKeyHex) { + document.getElementById('pubkey-verify-result').textContent = 'Please enter a public key'; + return; + } + + const message = document.getElementById('pubkey-verify-message').value; + const messageBytes = new TextEncoder().encode(message); + const signatureHex = document.getElementById('pubkey-verify-signature').value; + const signatureBytes = hexToBuffer(signatureHex); + const publicKeyBytes = hexToBuffer(publicKeyHex); + + try { + const isValid = verify_with_public_key(publicKeyBytes, messageBytes, signatureBytes); + document.getElementById('pubkey-verify-result').textContent = + isValid ? 'Signature is valid!' : 'Signature is NOT valid!'; + } catch (e) { + document.getElementById('pubkey-verify-result').textContent = `Error verifying: ${e}`; + } + } catch (e) { + document.getElementById('pubkey-verify-result').textContent = `Error: ${e}`; + } + }); + + // Set up the derive public key example + document.getElementById('derive-pubkey-button').addEventListener('click', () => { + try { + const privateKeyHex = document.getElementById('derive-pubkey-privkey').value.trim(); + if (!privateKeyHex) { + document.getElementById('derive-pubkey-result').textContent = 'Please enter a private key'; + return; + } + + const privateKeyBytes = hexToBuffer(privateKeyHex); + + try { + const publicKey = derive_public_key(privateKeyBytes); + const publicKeyHex = bufferToHex(publicKey); + + // Create a more user-friendly display with copy button + const pubKeyDisplay = document.getElementById('derive-pubkey-result'); + pubKeyDisplay.innerHTML = ` +
+
Derived Public Key (hex):
+
${publicKeyHex}
+ +
+ `; + + // Add event listener for the copy button + document.getElementById('copy-derived-pubkey-button').addEventListener('click', () => { + const pubKeyText = document.getElementById('derived-pubkey-hex-value').textContent; + navigator.clipboard.writeText(pubKeyText) + .then(() => { + alert('Public key copied to clipboard!'); + }) + .catch(err => { + console.error('Could not copy text: ', err); + }); + }); + + // Also populate the public key field in the verify with public key section + document.getElementById('pubkey-verify-pubkey').value = publicKeyHex; + + // And in the asymmetric encryption section + document.getElementById('asymmetric-encrypt-pubkey').value = publicKeyHex; + + } catch (e) { + document.getElementById('derive-pubkey-result').textContent = `Error deriving public key: ${e}`; + } + } catch (e) { + document.getElementById('derive-pubkey-result').textContent = `Error: ${e}`; + } + }); + + // Set up the asymmetric encryption example + document.getElementById('asymmetric-encrypt-button').addEventListener('click', () => { + try { + const publicKeyHex = document.getElementById('asymmetric-encrypt-pubkey').value.trim(); + if (!publicKeyHex) { + document.getElementById('asymmetric-encrypt-result').textContent = 'Please enter a recipient public key'; + return; + } + + const message = document.getElementById('asymmetric-encrypt-message').value; + const messageBytes = new TextEncoder().encode(message); + const publicKeyBytes = hexToBuffer(publicKeyHex); + + try { + const ciphertext = encrypt_asymmetric(publicKeyBytes, messageBytes); + const ciphertextHex = bufferToHex(ciphertext); + document.getElementById('asymmetric-encrypt-result').textContent = `Ciphertext: ${ciphertextHex}`; + + // Store for decryption + document.getElementById('asymmetric-decrypt-ciphertext').value = ciphertextHex; + } catch (e) { + document.getElementById('asymmetric-encrypt-result').textContent = `Error encrypting: ${e}`; + } + } catch (e) { + document.getElementById('asymmetric-encrypt-result').textContent = `Error: ${e}`; + } + }); + + // Set up the asymmetric decryption example + document.getElementById('asymmetric-decrypt-button').addEventListener('click', () => { + if (!isLoggedIn) { + document.getElementById('asymmetric-decrypt-result').textContent = 'Please login first'; + return; + } + + if (!selectedKeypair) { + document.getElementById('asymmetric-decrypt-result').textContent = 'Please select a keypair first'; + return; + } + + try { + const ciphertextHex = document.getElementById('asymmetric-decrypt-ciphertext').value; + const ciphertext = hexToBuffer(ciphertextHex); + + try { + const plaintext = decrypt_asymmetric(ciphertext); + const decodedText = new TextDecoder().decode(plaintext); + document.getElementById('asymmetric-decrypt-result').textContent = `Decrypted: ${decodedText}`; + } catch (e) { + document.getElementById('asymmetric-decrypt-result').textContent = `Error decrypting: ${e}`; + } + } catch (e) { + document.getElementById('asymmetric-decrypt-result').textContent = `Error: ${e}`; + } + }); // Initialize UI updateLoginUI();