|
20 | 20 | //! for a total of 2^96 unique nonces - far exceeding any practical usage. |
21 | 21 |
|
22 | 22 | use crate::metrics::OperationMetrics; |
| 23 | + |
| 24 | +// Native: ring for AES-256-GCM (hardware-accelerated, requires clang) |
| 25 | +#[cfg(not(target_arch = "wasm32"))] |
23 | 26 | use ring::{ |
24 | 27 | aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM}, |
25 | 28 | rand::{SecureRandom, SystemRandom}, |
26 | 29 | }; |
| 30 | + |
| 31 | +// wasm32: RustCrypto aes-gcm (pure Rust, compiles on wasm32-unknown-unknown) |
| 32 | +#[cfg(target_arch = "wasm32")] |
| 33 | +use aes_gcm::{ |
| 34 | + aead::{Aead, KeyInit, Payload}, |
| 35 | + Aes256Gcm, Nonce as AesGcmNonce, |
| 36 | +}; |
27 | 37 | #[cfg(not(target_arch = "wasm32"))] |
28 | 38 | use std::sync::atomic::{AtomicU64, Ordering}; |
29 | 39 | #[cfg(not(target_arch = "wasm32"))] |
@@ -330,6 +340,7 @@ impl ZeroKnowledgeEncryptor { |
330 | 340 | /// |
331 | 341 | /// # Returns |
332 | 342 | /// Encrypted data in format: `[nonce(12)][ciphertext+auth_tag]` |
| 343 | + #[cfg(not(target_arch = "wasm32"))] |
333 | 344 | pub fn encrypt_aes_gcm( |
334 | 345 | &self, |
335 | 346 | plaintext: &[u8], |
@@ -391,6 +402,7 @@ impl ZeroKnowledgeEncryptor { |
391 | 402 | /// |
392 | 403 | /// # Returns |
393 | 404 | /// Decrypted plaintext data |
| 405 | + #[cfg(not(target_arch = "wasm32"))] |
394 | 406 | pub fn decrypt_aes_gcm( |
395 | 407 | &self, |
396 | 408 | ciphertext: &[u8], |
@@ -489,6 +501,91 @@ impl ZeroKnowledgeEncryptor { |
489 | 501 | .into(), |
490 | 502 | )) |
491 | 503 | } |
| 504 | + |
| 505 | + /// Encrypt data using AES-256-GCM with authenticated additional data (wasm32) |
| 506 | + /// |
| 507 | + /// Uses RustCrypto's `aes-gcm` crate (pure Rust, compiles on wasm32-unknown-unknown). |
| 508 | + /// Produces identical wire format to the native `ring` path: |
| 509 | + /// `[nonce(12)][ciphertext][auth_tag(16)]` |
| 510 | + #[cfg(target_arch = "wasm32")] |
| 511 | + pub fn encrypt_aes_gcm( |
| 512 | + &self, |
| 513 | + plaintext: &[u8], |
| 514 | + key: &[u8], |
| 515 | + aad: &[u8], |
| 516 | + ) -> Result<Vec<u8>, EncryptionError> { |
| 517 | + if key.len() != 32 { |
| 518 | + return Err(EncryptionError::InvalidKeyLength(key.len())); |
| 519 | + } |
| 520 | + |
| 521 | + let nonce_bytes = self.generate_nonce()?; |
| 522 | + |
| 523 | + let cipher = Aes256Gcm::new_from_slice(key) |
| 524 | + .map_err(|e| EncryptionError::EncryptionFailed(format!("key error: {e}")))?; |
| 525 | + |
| 526 | + let nonce = AesGcmNonce::from_slice(&nonce_bytes); |
| 527 | + |
| 528 | + // encrypt() returns ciphertext || tag (no nonce) — same layout as ring's seal_in_place_append_tag |
| 529 | + let ciphertext_with_tag = cipher |
| 530 | + .encrypt(nonce, Payload { msg: plaintext, aad }) |
| 531 | + .map_err(|e| EncryptionError::EncryptionFailed(format!("AES-GCM encrypt failed: {e}")))?; |
| 532 | + |
| 533 | + // Wire format: nonce(12) || ciphertext || tag(16) — identical to native ring output |
| 534 | + let mut result = Vec::with_capacity(12 + ciphertext_with_tag.len()); |
| 535 | + result.extend_from_slice(&nonce_bytes); |
| 536 | + result.extend_from_slice(&ciphertext_with_tag); |
| 537 | + |
| 538 | + // Metrics: Instant unavailable on wasm32 |
| 539 | + if let Ok(mut metrics) = self.last_metrics.lock() { |
| 540 | + *metrics = OperationMetrics::new().with_encryption(0u64, false); |
| 541 | + } |
| 542 | + |
| 543 | + Ok(result) |
| 544 | + } |
| 545 | + |
| 546 | + /// Decrypt data using AES-256-GCM with authenticated additional data (wasm32) |
| 547 | + /// |
| 548 | + /// Uses RustCrypto's `aes-gcm` crate (pure Rust, compiles on wasm32-unknown-unknown). |
| 549 | + /// Expects identical wire format to the native `ring` path: |
| 550 | + /// `[nonce(12)][ciphertext][auth_tag(16)]` |
| 551 | + #[cfg(target_arch = "wasm32")] |
| 552 | + pub fn decrypt_aes_gcm( |
| 553 | + &self, |
| 554 | + ciphertext: &[u8], |
| 555 | + key: &[u8], |
| 556 | + aad: &[u8], |
| 557 | + ) -> Result<Vec<u8>, EncryptionError> { |
| 558 | + if key.len() != 32 { |
| 559 | + return Err(EncryptionError::InvalidKeyLength(key.len())); |
| 560 | + } |
| 561 | + |
| 562 | + // Minimum: nonce(12) + tag(16) = 28 bytes |
| 563 | + if ciphertext.len() < 28 { |
| 564 | + return Err(EncryptionError::InvalidCiphertext( |
| 565 | + "ciphertext too short (minimum 28 bytes: 12 nonce + 16 tag)".into(), |
| 566 | + )); |
| 567 | + } |
| 568 | + |
| 569 | + let nonce_bytes = &ciphertext[..12]; |
| 570 | + let encrypted_data = &ciphertext[12..]; // ciphertext + tag |
| 571 | + |
| 572 | + let cipher = Aes256Gcm::new_from_slice(key) |
| 573 | + .map_err(|e| EncryptionError::DecryptionFailed(format!("key error: {e}")))?; |
| 574 | + |
| 575 | + let nonce = AesGcmNonce::from_slice(nonce_bytes); |
| 576 | + |
| 577 | + // decrypt() verifies auth tag and returns plaintext |
| 578 | + let plaintext = cipher |
| 579 | + .decrypt(nonce, Payload { msg: encrypted_data, aad }) |
| 580 | + .map_err(|_| EncryptionError::AuthenticationFailed)?; |
| 581 | + |
| 582 | + // Metrics: Instant unavailable on wasm32 |
| 583 | + if let Ok(mut metrics) = self.last_metrics.lock() { |
| 584 | + *metrics = OperationMetrics::new().with_encryption(0u64, false); |
| 585 | + } |
| 586 | + |
| 587 | + Ok(plaintext) |
| 588 | + } |
492 | 589 | } |
493 | 590 |
|
494 | 591 | // Note: Default is intentionally NOT implemented. |
|
0 commit comments