mod asymmetric; use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use std::collections::HashMap; use std::io; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; use aes::cipher::{BlockCipherEncrypt, KeyInit}; use aes::{Aes128, Aes192, Aes256}; use aes_kw::{KwAes128, KwAes192, KwAes256}; use avril_core::serde_v8::{JsBuffer, ToJsBuffer}; use avril_core::{Extension, Op, message}; use cbc::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, block_padding::Pkcs7}; use ctr::cipher::StreamCipher; use ghash::{GHash, universal_hash::UniversalHash}; use hmac::{Hmac, Mac}; use sha1::Digest as _; use sha2::{Sha256, Sha384, Sha512}; use subtle::ConstantTimeEq; use zeroize::{Zeroize, Zeroizing}; type DeriveKeyArgs = ( u64, String, JsBuffer, u32, JsBuffer, usize, String, String, Vec, bool, u64, ); struct Key { bytes: Vec, algorithm: String, hash: String, usages: Vec, extractable: bool, key_type: String, length: usize, curve: String, public_exponent: Vec, } impl Drop for Key { fn drop(&mut self) { self.bytes.zeroize(); } } #[derive(serde::Serialize)] struct KeyMetadata { id: u64, length: usize, exponent: Option, #[serde(rename = "type")] key_type: String, } struct KeyStore { keys: Mutex>>, next_id: AtomicU64, } impl KeyStore { fn new() -> Self { Self { keys: Mutex::new(HashMap::new()), next_id: AtomicU64::new(0), } } fn next_id(&self) -> io::Result { self.next_id .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| { (id < 9_007_199_254_740_991).then_some(id + 1) }) .map(|id| id + 1) .map_err(|_| io::Error::other("Crypto key handles exhausted")) } fn metadata(&self, id: u64) -> io::Result { let key = self.get(id)?; Ok(KeyMetadata { id, length: key.length, exponent: (!key.public_exponent.is_empty()) .then(|| ToJsBuffer::from(key.public_exponent.clone())), key_type: key.key_type.clone(), }) } fn insert(&self, key: Key) -> io::Result { self.insert_shared(Arc::new(key)) } fn insert_shared(&self, key: Arc) -> io::Result { let id = self.next_id()?; self.keys .lock() .map_err(|_| io::Error::other("Crypto key registry poisoned"))? .insert(id, key); Ok(id) } fn get(&self, id: u64) -> io::Result> { self.keys .lock() .map_err(|_| io::Error::other("Crypto key registry poisoned"))? .get(&id) .cloned() .ok_or_else(|| io::Error::other("CryptoKey is no longer available")) } fn close(&self, id: u64) { if let Ok(mut keys) = self.keys.lock() { keys.remove(&id); } } fn clone_key(&self, id: u64) -> io::Result { self.insert_shared(self.get(id)?) } } fn allowed_usages(algorithm: &str, key_type: &str) -> io::Result<&'static [&'static str]> { Ok(match (algorithm, key_type) { ("AES-CBC" | "AES-CTR" | "AES-GCM", "secret") => { &["encrypt", "decrypt", "wrapKey", "unwrapKey"] } ("AES-KW", "secret") => &["wrapKey", "unwrapKey"], ("HMAC", "secret") => &["sign", "verify"], ("PBKDF2" | "HKDF", "secret") => &["deriveBits", "deriveKey"], ("RSA-OAEP", "public") => &["encrypt", "wrapKey"], ("RSA-OAEP", "private") => &["decrypt", "unwrapKey"], ("RSA-PSS" | "RSASSA-PKCS1-V1_5", "public") => &["verify"], ("RSA-PSS" | "RSASSA-PKCS1-V1_5", "private") => &["sign"], ("ECDSA" | "ED25519", "public") => &["verify"], ("ECDSA" | "ED25519", "private") => &["sign"], ("ECDH" | "X25519", "public") => &[], ("ECDH" | "X25519", "private") => &["deriveBits", "deriveKey"], _ => return Err(io::Error::other("Unsupported CryptoKey algorithm or type")), }) } fn apply_bit_length(bytes: &mut [u8], bits: usize) { if !bits.is_multiple_of(8) && !bytes.is_empty() { let last = bytes.len() - 1; bytes[last] &= 0xff << (8 - bits % 8); } } fn truncate_bits(bytes: &mut Vec, requested: usize) -> io::Result { let bits = if requested == 0 { bytes.len() * 8 } else { requested }; if bits == 0 || bits > bytes.len() * 8 || (requested != 0 && bits <= bytes.len() * 8 - 8) { return Err(io::Error::other("Invalid HMAC key length")); } bytes.truncate(bits.div_ceil(8)); apply_bit_length(bytes, bits); Ok(bits) } fn insert_key(store: &KeyStore, key: Key) -> io::Result { let asymmetric = matches!( key.algorithm.as_str(), "RSA-OAEP" | "RSA-PSS" | "RSASSA-PKCS1-V1_5" | "ECDSA" | "ECDH" | "ED25519" | "X25519" ); let allowed = allowed_usages(&key.algorithm, &key.key_type)?; if key .usages .iter() .any(|usage| !allowed.contains(&usage.as_str())) { return Err(io::Error::other("Invalid CryptoKey usage")); } if key.key_type == "secret" && key.usages.is_empty() { return Err(io::Error::other( "Secret CryptoKeys must have at least one usage", )); } if matches!(key.algorithm.as_str(), "PBKDF2" | "HKDF") && key.extractable { return Err(io::Error::other( "Key derivation keys must be non-extractable", )); } if asymmetric { if key.bytes.is_empty() || (key.key_type == "private" && key.usages.is_empty()) { return Err(io::Error::other("Invalid asymmetric key")); } if matches!( key.algorithm.as_str(), "RSA-OAEP" | "RSA-PSS" | "RSASSA-PKCS1-V1_5" ) && hash_len(&key.hash).is_err() { return Err(io::Error::other("Invalid CryptoKey hash")); } } else { let valid_length = match key.algorithm.as_str() { "AES-CBC" | "AES-CTR" | "AES-GCM" | "AES-KW" => matches!(key.bytes.len(), 16 | 24 | 32), "HMAC" => { key.length > 0 && key.length <= key.bytes.len() * 8 && key.bytes.len() == key.length.div_ceil(8) } "PBKDF2" | "HKDF" => true, _ => false, }; if !valid_length { return Err(io::Error::other("Invalid CryptoKey length")); } if key.algorithm == "HMAC" && hash_len(&key.hash).is_err() { return Err(io::Error::other("Invalid CryptoKey hash")); } } store.insert(key) } fn insert_key_pair( store: &KeyStore, algorithm: String, hash: String, curve: String, usages: Vec, extractable: bool, mut pair: asymmetric::KeyPairDer, ) -> io::Result<(u64, u64, usize, Option)> { let (public_usages, private_usages): (Vec, Vec) = match algorithm.as_str() { "RSA-OAEP" => ( usages .iter() .filter(|u| matches!(u.as_str(), "encrypt" | "wrapKey")) .cloned() .collect(), usages .iter() .filter(|u| matches!(u.as_str(), "decrypt" | "unwrapKey")) .cloned() .collect(), ), "RSA-PSS" | "RSASSA-PKCS1-V1_5" | "ECDSA" | "ED25519" => ( usages .iter() .filter(|u| u.as_str() == "verify") .cloned() .collect(), usages .iter() .filter(|u| u.as_str() == "sign") .cloned() .collect(), ), "ECDH" | "X25519" => ( Vec::new(), usages .iter() .filter(|u| matches!(u.as_str(), "deriveBits" | "deriveKey")) .cloned() .collect(), ), _ => return Err(io::Error::other("Unsupported asymmetric key algorithm")), }; let public = insert_key( store, Key { bytes: pair.public, algorithm: algorithm.clone(), hash: hash.clone(), usages: public_usages, extractable: true, key_type: "public".into(), length: pair.length, curve: curve.clone(), public_exponent: pair.exponent.clone(), }, )?; let private = match insert_key( store, Key { bytes: std::mem::take(&mut *pair.private), algorithm, hash, usages: private_usages, extractable, key_type: "private".into(), length: pair.length, curve, public_exponent: pair.exponent.clone(), }, ) { Ok(id) => id, Err(error) => { store.close(public); return Err(error); } }; let exponent = (!pair.exponent.is_empty()).then(|| ToJsBuffer::from(pair.exponent)); Ok((public, private, pair.length, exponent)) } fn require(key: &Key, usage: &str, algorithm: &str) -> io::Result<()> { if !key.usages.iter().any(|item| item == usage) { return Err(io::Error::other("CryptoKey does not permit this usage")); } if key.algorithm != algorithm { return Err(io::Error::other("CryptoKey algorithm mismatch")); } Ok(()) } fn hash_len(hash: &str) -> io::Result { match hash.to_ascii_uppercase().as_str() { "SHA-1" => Ok(20), "SHA-256" => Ok(32), "SHA-384" => Ok(48), "SHA-512" => Ok(64), _ => Err(io::Error::other("Unsupported hash algorithm")), } } fn digest(hash: &str, data: &[u8]) -> io::Result> { match hash.to_ascii_uppercase().as_str() { "SHA-1" => Ok(sha1::Sha1::digest(data).to_vec()), "SHA-256" => Ok(Sha256::digest(data).to_vec()), "SHA-384" => Ok(Sha384::digest(data).to_vec()), "SHA-512" => Ok(Sha512::digest(data).to_vec()), _ => Err(io::Error::other("Unsupported hash algorithm")), } } fn hmac_sign(hash: &str, key: &[u8], input: &[u8]) -> io::Result> { macro_rules! sign { ($digest:ty) => {{ let mut mac = as Mac>::new_from_slice(key).map_err(io::Error::other)?; mac.update(input); Ok(mac.finalize().into_bytes().to_vec()) }}; } match hash.to_ascii_uppercase().as_str() { "SHA-1" => sign!(sha1::Sha1), "SHA-256" => sign!(Sha256), "SHA-384" => sign!(Sha384), "SHA-512" => sign!(Sha512), _ => Err(io::Error::other("Unsupported HMAC hash")), } } const AES_GCM_TAG_LENGTHS: [usize; 7] = [32, 64, 96, 104, 112, 120, 128]; fn validate_gcm_tag_length(bits: usize) -> io::Result<()> { if AES_GCM_TAG_LENGTHS.contains(&bits) { Ok(()) } else { Err(io::Error::other("Invalid AES-GCM tag length")) } } macro_rules! with_aes_block_cipher { ($key:expr, $encrypt_block:ident => $body:block) => {{ match $key.len() { 16 => { let cipher = Aes128::new_from_slice($key).map_err(io::Error::other)?; let mut $encrypt_block = |block: &mut ghash::Block| cipher.encrypt_block(block); $body } 24 => { let cipher = Aes192::new_from_slice($key).map_err(io::Error::other)?; let mut $encrypt_block = |block: &mut ghash::Block| cipher.encrypt_block(block); $body } 32 => { let cipher = Aes256::new_from_slice($key).map_err(io::Error::other)?; let mut $encrypt_block = |block: &mut ghash::Block| cipher.encrypt_block(block); $body } _ => Err(io::Error::other("Invalid AES-GCM key length")), } }}; } fn gcm_counter_block(counter: &mut ghash::Block) { let bytes = counter.as_mut_slice(); let value = u32::from_be_bytes( bytes[12..16] .try_into() .expect("GCM counter block is 16 bytes"), ) .wrapping_add(1); bytes[12..16].copy_from_slice(&value.to_be_bytes()); } fn gcm_j0(authenticator: &GHash, iv: &[u8]) -> ghash::Block { if iv.len() == 12 { let mut block = ghash::Block::default(); let bytes = block.as_mut_slice(); bytes[..12].copy_from_slice(iv); bytes[15] = 1; block } else { let mut authenticator = authenticator.clone(); authenticator.update_padded(iv); let mut lengths = ghash::Block::default(); lengths.as_mut_slice()[8..] .copy_from_slice(&((iv.len() as u64).wrapping_mul(8)).to_be_bytes()); authenticator.update(&[lengths]); authenticator.finalize() } } fn gcm_tag( authenticator: &GHash, mask: &ghash::Block, aad: &[u8], ciphertext: &[u8], ) -> ghash::Block { let mut authenticator = authenticator.clone(); authenticator.update_padded(aad); authenticator.update_padded(ciphertext); let mut lengths = ghash::Block::default(); let bytes = lengths.as_mut_slice(); bytes[..8].copy_from_slice(&((aad.len() as u64).wrapping_mul(8)).to_be_bytes()); bytes[8..].copy_from_slice(&((ciphertext.len() as u64).wrapping_mul(8)).to_be_bytes()); authenticator.update(&[lengths]); let mut tag = authenticator.finalize(); for (tag_byte, mask_byte) in tag.as_mut_slice().iter_mut().zip(mask.as_slice()) { *tag_byte ^= *mask_byte; } tag } fn gcm_keystream( encrypt_block: &mut dyn FnMut(&mut ghash::Block), j0: &ghash::Block, output: &mut [u8], ) { let mut counter = *j0; gcm_counter_block(&mut counter); for chunk in output.chunks_mut(16) { let mut keystream = counter; encrypt_block(&mut keystream); for (byte, key) in chunk.iter_mut().zip(keystream.iter()) { *byte ^= *key; } gcm_counter_block(&mut counter); } } fn aes_gcm_encrypt( key: &[u8], iv: &[u8], input: &[u8], aad: &[u8], bits: usize, ) -> io::Result> { validate_gcm_tag_length(bits)?; if iv.is_empty() { return Err(io::Error::other("Invalid AES-GCM IV")); } let tag_length = bits / 8; with_aes_block_cipher!(key, encrypt_block => { let mut hash_key = ghash::Block::default(); encrypt_block(&mut hash_key); let authenticator = GHash::new(&hash_key); hash_key.as_mut_slice().zeroize(); let j0 = gcm_j0(&authenticator, iv); let mut mask = j0; encrypt_block(&mut mask); let mut output = input.to_vec(); gcm_keystream(&mut encrypt_block, &j0, &mut output); let tag = gcm_tag(&authenticator, &mask, aad, &output); mask.as_mut_slice().zeroize(); output.extend_from_slice(&tag.as_slice()[..tag_length]); Ok(output) }) } fn aes_gcm_decrypt( key: &[u8], iv: &[u8], input: &[u8], aad: &[u8], bits: usize, ) -> io::Result> { validate_gcm_tag_length(bits)?; if iv.is_empty() { return Err(io::Error::other("Invalid AES-GCM IV")); } let tag_length = bits / 8; if input.len() < tag_length { return Err(io::Error::other("Invalid AES-GCM ciphertext")); } let (ciphertext, tag) = input.split_at(input.len() - tag_length); with_aes_block_cipher!(key, encrypt_block => { let mut hash_key = ghash::Block::default(); encrypt_block(&mut hash_key); let authenticator = GHash::new(&hash_key); hash_key.as_mut_slice().zeroize(); let j0 = gcm_j0(&authenticator, iv); let mut mask = j0; encrypt_block(&mut mask); let expected = gcm_tag(&authenticator, &mask, aad, ciphertext); mask.as_mut_slice().zeroize(); if bool::from(expected.as_slice()[..tag_length].ct_eq(tag)) { let mut output = Zeroizing::new(ciphertext.to_vec()); gcm_keystream(&mut encrypt_block, &j0, &mut output); Ok(std::mem::take(&mut *output)) } else { Err(io::Error::other("Invalid AES-GCM ciphertext")) } }) } fn aes_ctr(key: &[u8], iv: &[u8], input: &[u8], bits: usize) -> io::Result> { if !(1..=128).contains(&bits) || iv.len() != 16 { return Err(io::Error::other("Invalid AES-CTR counter")); } let blocks = input.len().div_ceil(16) as u128; if bits < 128 && blocks > (1u128 << bits) { return Err(io::Error::other("AES-CTR input exceeds the counter space")); } let counter = u128::from_be_bytes(iv.try_into().map_err(io::Error::other)?); let mask = if bits == 128 { u128::MAX } else { (1u128 << bits) - 1 }; let prefix = if bits < 128 && mask - (counter & mask) + 1 < blocks { (mask - (counter & mask) + 1) as usize * 16 } else { input.len() }; let mut output = input.to_vec(); macro_rules! crypt { ($aes:ty) => {{ let mut cipher = ctr::Ctr128BE::<$aes>::new_from_slices(key, iv).map_err(io::Error::other)?; cipher.apply_keystream(&mut output[..prefix]); if prefix < output.len() { // Wrap only the counter field; the nonce bits must not carry. let nonce = (counter & !mask).to_be_bytes(); let mut cipher = ctr::Ctr128BE::<$aes>::new_from_slices(key, &nonce) .map_err(io::Error::other)?; cipher.apply_keystream(&mut output[prefix..]); } }}; } match key.len() { 16 => crypt!(Aes128), 24 => crypt!(Aes192), 32 => crypt!(Aes256), _ => return Err(io::Error::other("Invalid AES key length")), } Ok(output) } fn aes_encrypt( key: &[u8], mode: &str, iv: &[u8], input: &[u8], aad: &[u8], tag_length: usize, ) -> io::Result> { macro_rules! cbc_encrypt { ($aes:ty) => {{ let cipher = cbc::Encryptor::<$aes>::new_from_slices(key, iv).map_err(io::Error::other)?; Ok(cipher.encrypt_padded_vec::(input)) }}; } match mode { "AES-CBC" => match key.len() { 16 => cbc_encrypt!(Aes128), 24 => cbc_encrypt!(Aes192), 32 => cbc_encrypt!(Aes256), _ => Err(io::Error::other("Invalid AES key length")), }, "AES-CTR" => aes_ctr(key, iv, input, tag_length), "AES-GCM" => aes_gcm_encrypt(key, iv, input, aad, tag_length), _ => Err(io::Error::other("Unsupported AES mode")), } } fn aes_decrypt( key: &[u8], mode: &str, iv: &[u8], input: &[u8], aad: &[u8], tag_length: usize, ) -> io::Result> { macro_rules! cbc_decrypt { ($aes:ty) => {{ let cipher = cbc::Decryptor::<$aes>::new_from_slices(key, iv).map_err(io::Error::other)?; cipher .decrypt_padded_vec::(input) .map_err(io::Error::other) }}; } match mode { "AES-CBC" => match key.len() { 16 => cbc_decrypt!(Aes128), 24 => cbc_decrypt!(Aes192), 32 => cbc_decrypt!(Aes256), _ => Err(io::Error::other("Invalid AES key length")), }, "AES-CTR" => aes_ctr(key, iv, input, tag_length), "AES-GCM" => aes_gcm_decrypt(key, iv, input, aad, tag_length), _ => Err(io::Error::other("Unsupported AES mode")), } } fn wrap_kw(key: &[u8], input: &[u8]) -> io::Result> { macro_rules! wrap { ($kind:ty) => {{ let cipher = <$kind>::new_from_slice(key).map_err(io::Error::other)?; let mut output = vec![0; input.len() + 8]; let output_len = cipher .wrap_key(input, &mut output) .map_err(io::Error::other)? .len(); output.truncate(output_len); Ok(output) }}; } match key.len() { 16 => wrap!(KwAes128), 24 => wrap!(KwAes192), 32 => wrap!(KwAes256), _ => Err(io::Error::other("Invalid AES-KW key length")), } } fn unwrap_kw(key: &[u8], input: &[u8]) -> io::Result> { macro_rules! unwrap { ($kind:ty) => {{ let cipher = <$kind>::new_from_slice(key).map_err(io::Error::other)?; let mut output = vec![0; input.len()]; let output_len = cipher .unwrap_key(input, &mut output) .map_err(io::Error::other)? .len(); output.truncate(output_len); Ok(output) }}; } match key.len() { 16 => unwrap!(KwAes128), 24 => unwrap!(KwAes192), 32 => unwrap!(KwAes256), _ => Err(io::Error::other("Invalid AES-KW key length")), } } fn jwk_algorithm(key: &Key) -> io::Result { if key.algorithm == "HMAC" { Ok(format!("HS{}", &key.hash[4..])) } else { let mode = match key.algorithm.as_str() { "AES-CBC" => "CBC", "AES-CTR" => "CTR", "AES-GCM" => "GCM", "AES-KW" => "KW", _ => return Err(io::Error::other("Unsupported JWK algorithm")), }; Ok(format!("A{}{}", key.bytes.len() * 8, mode)) } } fn key_jwk(key: &Key) -> io::Result { if key.key_type != "secret" { return asymmetric::export_jwk( &key.bytes, &key.algorithm, &key.hash, &key.key_type, &key.curve, &key.usages, key.extractable, ); } Ok(serde_json::json!({ "kty": "oct", "k": URL_SAFE_NO_PAD.encode(&key.bytes), "alg": jwk_algorithm(key)?, "key_ops": key.usages, "ext": key.extractable, })) } fn key_from_jwk( mut jwk: SensitiveJwk, algorithm: String, hash: String, usages: Vec, extractable: bool, requested_length: usize, ) -> io::Result { let value = &mut jwk.0; if value.get("kty").and_then(|item| item.as_str()) != Some("oct") { return Err(io::Error::other("JWK kty does not match a symmetric key")); } if let Some(ext) = value.get("ext") && (!ext.is_boolean() || (ext.as_bool() == Some(false) && extractable)) { return Err(io::Error::other("JWK extractability does not match")); } if let Some(ops) = value.get("key_ops") { let ops = ops .as_array() .ok_or_else(|| io::Error::other("Invalid JWK key_ops"))?; let mut seen = std::collections::HashSet::new(); let valid_ops = [ "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey", ]; let allowed = allowed_usages(&algorithm, "secret")?; if ops.iter().any(|item| { item.as_str().is_none_or(|usage| { !valid_ops.contains(&usage) || !allowed.contains(&usage) || !seen.insert(usage) }) }) || usages .iter() .any(|usage| !ops.iter().any(|item| item.as_str() == Some(usage))) { return Err(io::Error::other( "JWK key_ops do not permit the requested usages", )); } } let encoded = value .get("k") .and_then(|item| item.as_str()) .ok_or_else(|| io::Error::other("JWK is missing key data"))?; let mut bytes = URL_SAFE_NO_PAD.decode(encoded).map_err(io::Error::other)?; if let Some(serde_json::Value::String(encoded)) = value.get_mut("k") { encoded.zeroize(); } let length = if algorithm == "HMAC" { truncate_bits(&mut bytes, requested_length)? } else { bytes.len() * 8 }; let key = Key { bytes, algorithm, hash, usages, extractable, key_type: "secret".into(), length, curve: String::new(), public_exponent: Vec::new(), }; if let Some(alg) = value.get("alg") { let alg = alg .as_str() .ok_or_else(|| io::Error::other("Invalid JWK alg"))?; if jwk_algorithm(&key)? != alg { return Err(io::Error::other("JWK alg does not match the key algorithm")); } } Ok(key) } #[derive(serde::Serialize, serde::Deserialize)] #[serde(transparent)] struct SensitiveJwk(serde_json::Value); impl Drop for SensitiveJwk { fn drop(&mut self) { for field in ["d", "p", "q", "dp", "dq", "qi", "k"] { if let Some(serde_json::Value::String(encoded)) = self.0.get_mut(field) { encoded.zeroize(); } } } } fn import_asymmetric_jwk( mut sensitive: SensitiveJwk, algorithm: String, hash: String, curve: String, usages: Vec, extractable: bool, ) -> io::Result { let value = &mut sensitive.0; if let Some(ext) = value.get("ext") && (!ext.is_boolean() || (ext.as_bool() == Some(false) && extractable)) { return Err(io::Error::other("JWK extractability does not match")); } if let Some(ops) = value.get("key_ops") { let ops = ops .as_array() .ok_or_else(|| io::Error::other("Invalid JWK key_ops"))?; let valid = [ "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey", ]; let mut seen = std::collections::HashSet::new(); if ops.iter().any(|item| { item.as_str() .is_none_or(|op| !valid.contains(&op) || !seen.insert(op)) }) || usages .iter() .any(|usage| !ops.iter().any(|item| item.as_str() == Some(usage))) { return Err(io::Error::other( "JWK key_ops do not permit the requested usages", )); } } let (imported, key_type) = asymmetric::import_jwk(value, &algorithm, &hash, &curve)?; if let Some(ops) = value.get("key_ops").and_then(serde_json::Value::as_array) { let allowed = allowed_usages(&algorithm, &key_type)?; if ops .iter() .any(|item| item.as_str().is_none_or(|usage| !allowed.contains(&usage))) { return Err(io::Error::other( "JWK key_ops are incompatible with the key type", )); } } let key = Key { bytes: imported.bytes, algorithm, hash, usages, extractable, key_type, length: imported.length, curve: imported.curve, public_exponent: imported.exponent, }; if let Some(alg) = value.get("alg") { let alg = alg .as_str() .ok_or_else(|| io::Error::other("Invalid JWK alg"))?; let expected = SensitiveJwk(key_jwk(&key)?); if expected.0.get("alg").and_then(|v| v.as_str()) != Some(alg) { return Err(io::Error::other("JWK alg does not match the key algorithm")); } } Ok(key) } fn key_material(key: &Key, format: &str) -> io::Result> { match format { "raw" if key.key_type == "secret" => Ok(key.bytes.clone()), "raw" | "spki" | "pkcs8" if key.key_type != "secret" => asymmetric::export_key( format, &key.bytes, &key.algorithm, &key.key_type, &key.curve, ), "jwk" => { let jwk = SensitiveJwk(key_jwk(key)?); serde_json::to_vec(&jwk).map_err(io::Error::other) } _ => Err(io::Error::other("Unsupported key format")), } } struct WrapKeyRequest { wrapping: Arc, target: Arc, format: String, algorithm: String, hash: String, iv: Vec, counter: Vec, additional: Vec, tag_length: usize, } struct WrappingParams<'a> { algorithm: &'a str, hash: &'a str, iv: &'a [u8], counter: &'a [u8], additional: &'a [u8], tag_length: usize, encrypt: bool, } fn wrapping_transform(key: &Key, params: WrappingParams<'_>, input: &[u8]) -> io::Result> { let WrappingParams { algorithm, hash, iv, counter, additional, tag_length, encrypt, } = params; match algorithm { "AES-KW" => { if encrypt { wrap_kw(&key.bytes, input) } else { unwrap_kw(&key.bytes, input) } } "AES-CBC" | "AES-CTR" | "AES-GCM" => { let iv = if algorithm == "AES-CTR" { counter } else { iv }; if encrypt { aes_encrypt(&key.bytes, algorithm, iv, input, additional, tag_length) } else { aes_decrypt(&key.bytes, algorithm, iv, input, additional, tag_length) } } "RSA-OAEP" => { if encrypt { asymmetric::rsa_encrypt(algorithm, &key.bytes, hash, iv, input) } else { asymmetric::rsa_decrypt(algorithm, &key.bytes, hash, iv, input) } } _ => Err(io::Error::other("Unsupported wrapping algorithm")), } } fn wrapping_material(key: &Key, format: &str, algorithm: &str) -> io::Result>> { let mut material = Zeroizing::new(key_material(key, format)?); if format == "jwk" && algorithm == "AES-KW" { let padded_length = material.len().div_ceil(8) * 8; material.resize(padded_length, b' '); } Ok(material) } fn wrap_key(request: WrapKeyRequest) -> io::Result> { let WrapKeyRequest { wrapping, target, format, algorithm, hash, iv, counter, additional, tag_length, } = request; require(&wrapping, "wrapKey", &wrapping.algorithm)?; if wrapping.algorithm != algorithm { return Err(io::Error::other( "Wrapping algorithm does not match the key", )); } if !target.extractable { return Err(io::Error::other("CryptoKey is not extractable")); } let material = wrapping_material(&target, &format, &algorithm)?; wrapping_transform( &wrapping, WrappingParams { algorithm: &algorithm, hash: &hash, iv: &iv, counter: &counter, additional: &additional, tag_length, encrypt: true, }, &material, ) } type UnwrapKeyOpArgs = ( u64, String, JsBuffer, String, String, String, Vec, bool, JsBuffer, JsBuffer, JsBuffer, usize, usize, ); struct UnwrapKeyRequest { wrapping: Arc, format: String, wrapped: Vec, algorithm: String, hash: String, curve: String, usages: Vec, extractable: bool, iv: Vec, counter: Vec, additional: Vec, tag_length: usize, length: usize, } fn import_unwrapped_raw( store: &KeyStore, bytes: &[u8], algorithm: String, hash: String, usages: Vec, extractable: bool, requested_bits: usize, ) -> io::Result { let mut raw = Zeroizing::new(bytes.to_vec()); let length = if algorithm == "HMAC" { truncate_bits(&mut raw, requested_bits)? } else { raw.len() * 8 }; insert_key( store, Key { bytes: std::mem::take(&mut *raw), algorithm, hash, usages, extractable, key_type: "secret".into(), length, curve: String::new(), public_exponent: Vec::new(), }, ) } fn unwrap_key(store: &KeyStore, request: UnwrapKeyRequest) -> io::Result { let UnwrapKeyRequest { wrapping, format, wrapped, algorithm, hash, curve, usages, extractable, iv, counter, additional, tag_length, length, } = request; require(&wrapping, "unwrapKey", &wrapping.algorithm)?; let bytes = Zeroizing::new(wrapping_transform( &wrapping, WrappingParams { algorithm: &wrapping.algorithm, hash: &wrapping.hash, iv: &iv, counter: &counter, additional: &additional, tag_length, encrypt: false, }, &wrapped, )?); if format == "jwk" { let jwk: SensitiveJwk = SensitiveJwk(serde_json::from_slice(&bytes).map_err(io::Error::other)?); let key = if matches!( algorithm.as_str(), "RSA-OAEP" | "RSA-PSS" | "RSASSA-PKCS1-V1_5" | "ECDSA" | "ECDH" | "ED25519" | "X25519" ) { import_asymmetric_jwk(jwk, algorithm, hash, curve, usages, extractable)? } else { if jwk.0.get("kty").and_then(|value| value.as_str()) != Some("oct") { return Err(io::Error::other("Invalid wrapped JWK")); } key_from_jwk(jwk, algorithm, hash, usages, extractable, length)? }; return insert_key(store, key); } if matches!(format.as_str(), "spki" | "pkcs8" | "raw") && matches!( algorithm.as_str(), "RSA-OAEP" | "RSA-PSS" | "RSASSA-PKCS1-V1_5" | "ECDSA" | "ECDH" | "ED25519" | "X25519" ) { let imported = asymmetric::import_key(&format, &bytes, &algorithm, &curve)?; let key_type = if format == "pkcs8" { "private" } else { "public" } .to_owned(); return insert_key( store, Key { bytes: imported.bytes, algorithm, hash, usages, extractable, key_type, length: imported.length, curve: imported.curve, public_exponent: imported.exponent, }, ); } if format != "raw" { return Err(io::Error::other("Unsupported key format")); } import_unwrapped_raw(store, &bytes, algorithm, hash, usages, extractable, length) } struct CryptoOperation { operation: String, key: Arc, peer: Option>, hash: String, iv: Vec, counter: Vec, additional: Vec, tag_length: usize, iterations: u32, salt: Vec, info: Vec, input: Vec, } fn crypt( CryptoOperation { operation, key: base_key, peer: peer_key, hash, iv, counter: _counter, additional, tag_length, iterations, salt, info, input, }: CryptoOperation, ) -> io::Result> { match operation.as_str() { "encrypt" | "decrypt" => { let usage = &operation; require(&base_key, usage, &base_key.algorithm)?; if base_key.algorithm == "RSA-OAEP" { return if operation == "encrypt" { asymmetric::rsa_encrypt( &base_key.algorithm, &base_key.bytes, &base_key.hash, &iv, &input, ) } else { asymmetric::rsa_decrypt( &base_key.algorithm, &base_key.bytes, &base_key.hash, &iv, &input, ) }; } if !base_key.algorithm.starts_with("AES-") { return Err(io::Error::other("Unsupported encryption key algorithm")); } let nonce = if base_key.algorithm == "AES-CTR" { &_counter } else { &iv }; if operation == "encrypt" { aes_encrypt( &base_key.bytes, &base_key.algorithm, nonce, &input, &additional, tag_length, ) } else { aes_decrypt( &base_key.bytes, &base_key.algorithm, nonce, &input, &additional, tag_length, ) } } "wrapKey" | "unwrapKey" => { require(&base_key, &operation, &base_key.algorithm)?; if base_key.algorithm == "RSA-OAEP" { return if operation == "wrapKey" { asymmetric::rsa_encrypt( &base_key.algorithm, &base_key.bytes, &base_key.hash, &iv, &input, ) } else { asymmetric::rsa_decrypt( &base_key.algorithm, &base_key.bytes, &base_key.hash, &iv, &input, ) }; } if base_key.algorithm != "AES-KW" { return Err(io::Error::other("Unsupported wrapping algorithm")); } if operation == "wrapKey" { wrap_kw(&base_key.bytes, &input) } else { unwrap_kw(&base_key.bytes, &input) } } "sign" | "verify" => { require(&base_key, &operation, &base_key.algorithm)?; if base_key.algorithm == "HMAC" { let signature = hmac_sign(&base_key.hash, &base_key.bytes, &input)?; return if operation == "sign" { Ok(signature) } else if signature.len() == additional.len() && bool::from(signature.ct_eq(&additional)) { Ok(vec![1]) } else { Ok(vec![0]) }; } let hash = if hash.is_empty() { base_key.hash.as_str() } else { hash.as_str() }; if operation == "sign" { if matches!(base_key.algorithm.as_str(), "RSA-PSS" | "RSASSA-PKCS1-V1_5") { asymmetric::rsa_sign( &base_key.algorithm, &base_key.bytes, hash, tag_length, &input, ) } else { asymmetric::sign( &base_key.algorithm, &base_key.bytes, hash, &base_key.curve, &input, ) } } else { let valid = if matches!(base_key.algorithm.as_str(), "RSA-PSS" | "RSASSA-PKCS1-V1_5") { asymmetric::rsa_verify( &base_key.algorithm, &base_key.bytes, hash, tag_length, &input, &additional, )? } else { asymmetric::verify( &base_key.algorithm, &base_key.bytes, hash, &base_key.curve, &input, &additional, )? }; Ok(vec![u8::from(valid)]) } } "deriveBits" | "deriveKey" => { require(&base_key, &operation, &base_key.algorithm)?; if matches!(base_key.algorithm.as_str(), "HKDF" | "PBKDF2") && !tag_length.is_multiple_of(8) { return Err(io::Error::other("Invalid derived bit length")); } if base_key.algorithm == "PBKDF2" && iterations == 0 { return Err(io::Error::other("Invalid PBKDF2 iteration count")); } let length = tag_length.div_ceil(8); let derive_hash = if hash.is_empty() { base_key.hash.as_str() } else { hash.as_str() }; match base_key.algorithm.as_str() { "PBKDF2" => { let mut output = vec![0; length]; match derive_hash.to_ascii_uppercase().as_str() { "SHA-1" => pbkdf2::pbkdf2_hmac::( &base_key.bytes, &salt, iterations, &mut output, ), "SHA-256" => pbkdf2::pbkdf2_hmac::( &base_key.bytes, &salt, iterations, &mut output, ), "SHA-384" => pbkdf2::pbkdf2_hmac::( &base_key.bytes, &salt, iterations, &mut output, ), "SHA-512" => pbkdf2::pbkdf2_hmac::( &base_key.bytes, &salt, iterations, &mut output, ), _ => return Err(io::Error::other("Unsupported PBKDF2 hash")), } if operation == "deriveKey" { apply_bit_length(&mut output, tag_length); } Ok(output) } "HKDF" => { macro_rules! expand { ($digest:ty) => {{ let hk = hkdf::Hkdf::<$digest>::new(Some(&salt), &base_key.bytes); let mut output = vec![0; length]; hk.expand(&info, &mut output) .map_err(|_| io::Error::other("HKDF output length is invalid"))?; if operation == "deriveKey" { apply_bit_length(&mut output, tag_length); } Ok(output) }}; } match derive_hash.to_ascii_uppercase().as_str() { "SHA-1" => expand!(sha1::Sha1), "SHA-256" => expand!(Sha256), "SHA-384" => expand!(Sha384), "SHA-512" => expand!(Sha512), _ => Err(io::Error::other("Unsupported HKDF hash")), } } "ECDH" | "X25519" => { let peer = peer_key.ok_or_else(|| io::Error::other("ECDH peer key missing"))?; if peer.algorithm != base_key.algorithm || peer.key_type != "public" || peer.curve != base_key.curve { return Err(io::Error::other("ECDH peer key does not match")); } let mut secret = if base_key.algorithm == "ECDH" { asymmetric::ecdh( &base_key.algorithm, &base_key.curve, &base_key.bytes, &peer.bytes, )? } else { asymmetric::x25519(&base_key.bytes, &peer.bytes)? }; let output_len = tag_length.div_ceil(8); if output_len > secret.len() { secret.zeroize(); return Err(io::Error::other( "Requested secret length exceeds the shared secret", )); } secret.truncate(output_len); apply_bit_length(&mut secret, tag_length); Ok(secret) } _ => Err(io::Error::other("CryptoKey cannot derive bits")), } } _ => Err(io::Error::other("Crypto operation not yet implemented")), } } macro_rules! with_store { ($store:ident, $operation:expr) => {{ let $store = $store.clone(); $operation }}; } fn send_key_result( sender: futures::channel::oneshot::Sender>, result: io::Result, store: &KeyStore, ids: impl FnOnce(&T) -> Vec, ) { if let Err(Ok(value)) = sender.send(result) { for id in ids(&value) { store.close(id); } } } fn spawn_crypto(job: impl FnOnce() + Send + 'static) { crypto_pool().spawn(move || { let _ = catch_unwind(AssertUnwindSafe(job)); }); } fn crypto_pool() -> &'static rayon::ThreadPool { static POOL: OnceLock = OnceLock::new(); POOL.get_or_init(|| { rayon::ThreadPoolBuilder::new() .num_threads(4) .thread_name(|index| format!("avril:crypto:{index}")) .build() .expect("failed to initialize crypto worker pool") }) } pub fn init() -> Extension { let store = Arc::new(KeyStore::new()); Extension { name: "ext:crypto", dependencies: &["ext:web"], ops: vec![ Op::new("op_crypto_random", |mut bytes: JsBuffer| { if bytes.len() > 65536 { return Err(io::Error::other("Random destination exceeds 65536 bytes")); } getrandom::fill(&mut bytes).map_err(io::Error::other) }), Op::new("op_crypto_uuid", |(): ()| { let mut bytes = [0; 16]; getrandom::fill(&mut bytes).map_err(io::Error::other)?; bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; Ok::<_, io::Error>(format!( "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15] )) }), with_store!( store, Op::new("op_crypto_key_dispose", move |id: u64| { store.close(id); Ok::<_, io::Error>(()) }) ), with_store!( store, Op::new("op_crypto_key_clone", move |id| store.clone_key(id)) ), with_store!( store, Op::with_scope("op_crypto_key_serialize", move |scope, id| { message::serialize_host(scope, store.get(id)?) }) ), with_store!( store, Op::with_scope("op_crypto_key_deserialize", move |scope, id: u32| { store.insert_shared(message::deserialize_host::(scope, id)?) }) ), with_store!( store, Op::asynchronous( "op_crypto_key_pair_generate", move |(algorithm, hash, length, exponent, curve, usages, extractable): ( String, String, usize, JsBuffer, String, Vec, bool, )| { let store = store.clone(); let exponent = exponent.as_ref().to_vec(); async move { let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let result = asymmetric::generate(&algorithm, length, &exponent, &curve) .and_then(|pair| { insert_key_pair( &store, algorithm, hash, curve, usages, extractable, pair, ) }); send_key_result(sender, result, &store, |pair| { vec![pair.0, pair.1] }); }); receiver.await.map_err(io::Error::other)? } }, ) ), with_store!( store, Op::asynchronous( "op_crypto_key_generate", move |(algorithm, length, hash, usages, extractable): ( String, usize, String, Vec, bool, )| { let store = store.clone(); async move { let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let mut bytes = vec![0; length.div_ceil(8)]; let result = getrandom::fill(&mut bytes) .map_err(io::Error::other) .and_then(|_| { if algorithm == "HMAC" { apply_bit_length(&mut bytes, length); } insert_key( &store, Key { length, bytes, algorithm, hash, usages, extractable, key_type: "secret".into(), curve: String::new(), public_exponent: Vec::new(), }, ) }); send_key_result(sender, result, &store, |id| vec![*id]); }); receiver.await.map_err(io::Error::other)? } }, ) ), with_store!( store, Op::asynchronous( "op_crypto_key_import", move |(algorithm, bytes, length, hash, usages, extractable): ( String, JsBuffer, usize, String, Vec, bool, )| { let store = store.clone(); let mut bytes = bytes.as_ref().to_vec(); async move { let length = if algorithm == "HMAC" { truncate_bits(&mut bytes, length)? } else { bytes.len() * 8 }; insert_key( &store, Key { bytes, algorithm, hash, usages, extractable, key_type: "secret".into(), length, curve: String::new(), public_exponent: Vec::new(), }, ) } }, ) ), with_store!( store, Op::asynchronous( "op_crypto_key_import_asymmetric", move |(format, data, algorithm, hash, curve, usages, extractable): ( String, JsBuffer, String, String, String, Vec, bool, )| { let store = store.clone(); let data = Zeroizing::new(data.as_ref().to_vec()); let key_type = if format == "pkcs8" { "private" } else { "public" } .to_owned(); let public_extractable = extractable; async move { let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let result = asymmetric::import_key(&format, &data, &algorithm, &curve) .and_then(|imported| { insert_key( &store, Key { bytes: imported.bytes, algorithm, hash, usages, extractable: public_extractable, key_type, length: imported.length, curve: imported.curve, public_exponent: imported.exponent, }, ) }) .and_then(|id| store.metadata(id)); send_key_result(sender, result, &store, |value| vec![value.id]); }); receiver.await.map_err(io::Error::other)? } }, ) ), with_store!( store, Op::asynchronous( "op_crypto_key_import_jwk", move |(jwk, algorithm, hash, curve, usages, extractable, length): ( SensitiveJwk, String, String, String, Vec, bool, usize, )| { let store = store.clone(); async move { let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let result = if matches!( algorithm.as_str(), "RSA-OAEP" | "RSA-PSS" | "RSASSA-PKCS1-V1_5" | "ECDSA" | "ECDH" | "ED25519" | "X25519" ) { import_asymmetric_jwk( jwk, algorithm, hash, curve, usages, extractable, ) } else { key_from_jwk(jwk, algorithm, hash, usages, extractable, length) } .and_then(|key| insert_key(&store, key)); let result = result.and_then(|id| store.metadata(id)); send_key_result(sender, result, &store, |value| vec![value.id]); }); receiver.await.map_err(io::Error::other)? } }, ) ), with_store!( store, Op::asynchronous( "op_crypto_key_export", move |(id, format): (u64, String)| { let key = store.get(id); async move { let key = key?; if !key.extractable { return Err(io::Error::other("CryptoKey is not extractable")); } let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let bytes = if key.key_type == "secret" { if format != "raw" { Err(io::Error::other("Unsupported symmetric key format")) } else { Ok(key.bytes.clone()) } } else { asymmetric::export_key( &format, &key.bytes, &key.algorithm, &key.key_type, &key.curve, ) }; let _ = sender.send(bytes); }); let bytes = receiver.await.map_err(io::Error::other)??; Ok::<_, io::Error>(ToJsBuffer::from(bytes)) } }, ) ), with_store!( store, Op::asynchronous("op_crypto_key_export_jwk", move |id: u64| { let key = store.get(id); async move { let key = key?; if !key.extractable { return Err(io::Error::other("CryptoKey is not extractable")); } let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let _ = sender.send(key_jwk(&key).map(SensitiveJwk)); }); receiver.await.map_err(io::Error::other)? } }) ), with_store!( store, Op::asynchronous( "op_crypto_derive_key", move |( id, hash, salt, iterations, info, length, algorithm, key_hash, usages, extractable, peer_id, ): DeriveKeyArgs| { let store = store.clone(); let salt = salt.as_ref().to_vec(); let info = info.as_ref().to_vec(); let base_key = store.get(id); let peer_key = if peer_id == 0 { Ok(None) } else { store.get(peer_id).map(Some) }; async move { let base_key = base_key?; let peer_key = peer_key?; let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let result = crypt(CryptoOperation { operation: "deriveKey".into(), key: base_key, peer: peer_key, hash, iv: Vec::new(), counter: Vec::new(), additional: Vec::new(), tag_length: length, iterations, salt, info, input: Vec::new(), }) .and_then(|bytes| { insert_key( &store, Key { bytes, algorithm, hash: key_hash, usages, extractable, key_type: "secret".into(), length, curve: String::new(), public_exponent: Vec::new(), }, ) }); send_key_result(sender, result, &store, |id| vec![*id]); }); receiver.await.map_err(io::Error::other)? } }, ) ), with_store!( store, Op::asynchronous( "op_crypto_wrap_key", move |( wrapping_id, target_id, format, algorithm, hash, iv, counter, additional, tag_length, ): ( u64, u64, String, String, String, JsBuffer, JsBuffer, JsBuffer, usize, )| { let iv = iv.as_ref().to_vec(); let counter = counter.as_ref().to_vec(); let additional = additional.as_ref().to_vec(); let wrapping = store.get(wrapping_id); let target = store.get(target_id); async move { let wrapping = wrapping?; let target = target?; let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let _ = sender.send(wrap_key(WrapKeyRequest { wrapping, target, format, algorithm, hash, iv, counter, additional, tag_length, })); }); let bytes = receiver.await.map_err(io::Error::other)??; Ok::<_, io::Error>(ToJsBuffer::from(bytes)) } }, ) ), with_store!( store, Op::asynchronous( "op_crypto_unwrap_key", move |( wrapping_id, format, wrapped, algorithm, hash, curve, usages, extractable, iv, counter, additional, tag_length, length, ): UnwrapKeyOpArgs| { let store = store.clone(); let wrapped = wrapped.as_ref().to_vec(); let iv = iv.as_ref().to_vec(); let counter = counter.as_ref().to_vec(); let additional = additional.as_ref().to_vec(); let wrapping = store.get(wrapping_id); async move { let wrapping = wrapping?; let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let result = unwrap_key( &store, UnwrapKeyRequest { wrapping, format, wrapped, algorithm, hash, curve, usages, extractable, iv, counter, additional, tag_length, length, }, ) .and_then(|id| store.metadata(id)); send_key_result(sender, result, &store, |value| vec![value.id]); }); receiver.await.map_err(io::Error::other)? } }, ) ), with_store!( store, Op::asynchronous( "op_crypto_run", move |( operation, id, hash, iv, counter, additional, tag_length, iterations, salt, info, input, peer_id, ): ( String, u64, String, JsBuffer, JsBuffer, JsBuffer, usize, u32, JsBuffer, JsBuffer, JsBuffer, u64, )| { let input = input.as_ref().to_vec(); let iv = iv.as_ref().to_vec(); let counter = counter.as_ref().to_vec(); let additional = additional.as_ref().to_vec(); let salt = salt.as_ref().to_vec(); let info = info.as_ref().to_vec(); let key = store.get(id); let peer = match &key { Ok(key) if operation == "deriveBits" && matches!(key.algorithm.as_str(), "ECDH" | "X25519") => { store.get(peer_id).map(Some) } _ => Ok(None), }; async move { let key = key?; let peer = peer?; let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let result = crypt(CryptoOperation { operation, key, peer, hash, iv, counter, additional, tag_length, iterations, salt, info, input, }); let _ = sender.send(result); }); let bytes = receiver.await.map_err(io::Error::other)??; Ok::<_, io::Error>(ToJsBuffer::from(bytes)) } }, ) ), Op::asynchronous("op_crypto_digest", |(name, input): (String, JsBuffer)| { let name = name.to_ascii_uppercase(); let input = input.as_ref().to_vec(); async move { let (sender, receiver) = futures::channel::oneshot::channel(); spawn_crypto(move || { let result = digest(&name, &input); let _ = sender.send(result); }); let output = receiver.await.map_err(io::Error::other)??; Ok::<_, io::Error>(ToJsBuffer::from(output)) } }), ], modules: &[( "ext:crypto/main.js", include_str!(concat!(env!("OUT_DIR"), "/main.js")), )], entry_point: Some("ext:crypto/main.js"), } } #[cfg(test)] mod tests { use super::*; fn test_key(bytes: Vec) -> Key { Key { bytes, algorithm: "AES-GCM".into(), hash: String::new(), usages: vec!["encrypt".into()], extractable: false, key_type: "secret".into(), length: 128, curve: String::new(), public_exponent: Vec::new(), } } #[test] fn dropping_a_runtime_store_releases_its_native_keys() { let store = Arc::new(KeyStore::new()); let id = store.insert(test_key(vec![7; 16])).unwrap(); let key = store.get(id).unwrap(); let weak = Arc::downgrade(&key); drop(key); drop(store); assert!(weak.upgrade().is_none()); } #[test] fn stores_are_isolated_and_handles_are_safe_integers() { let first = KeyStore::new(); let second = KeyStore::new(); let first_id = first.insert(test_key(vec![1; 16])).unwrap(); let second_id = second.insert(test_key(vec![2; 16])).unwrap(); assert_eq!(first_id, second_id); assert_eq!(first.get(first_id).unwrap().bytes, vec![1; 16]); assert_eq!(second.get(second_id).unwrap().bytes, vec![2; 16]); first .next_id .store(9_007_199_254_740_991, Ordering::Relaxed); assert!(first.next_id().is_err()); } #[test] fn imported_public_keys_preserve_nonextractability() { let store = KeyStore::new(); let mut key = test_key(vec![4; 32]); key.algorithm = "ED25519".into(); key.usages = vec!["verify".into()]; key.key_type = "public".into(); key.length = 256; key.extractable = false; let id = insert_key(&store, key).unwrap(); assert!(!store.get(id).unwrap().extractable); } #[test] fn invalid_secret_metadata_is_rejected_natively() { let store = KeyStore::new(); let mut key = test_key(vec![4; 16]); key.usages.clear(); assert!(insert_key(&store, key).is_err()); let mut key = test_key(vec![4; 16]); key.algorithm = "HKDF".into(); key.usages = vec!["deriveBits".into()]; key.extractable = true; assert!(insert_key(&store, key).is_err()); } #[test] fn native_gcm_accepts_only_webcrypto_tag_lengths() { for bits in [32, 64, 96, 104, 112, 120, 128] { let encrypted = aes_gcm_encrypt(&[0; 16], &[0; 12], &[], &[], bits).unwrap(); assert!( aes_gcm_decrypt(&[0; 16], &[0; 12], &encrypted, &[], bits) .unwrap() .is_empty() ); } for bits in [0, 31, 33, 48, 129] { assert!(aes_gcm_encrypt(&[0; 16], &[0; 12], &[], &[], bits).is_err()); assert!(aes_gcm_decrypt(&[0; 16], &[0; 12], &[], &[], bits).is_err()); } } #[test] fn raw_hmac_unwrap_enforces_and_applies_requested_bits() { let store = KeyStore::new(); let id = import_unwrapped_raw( &store, &[0xff, 0xff], "HMAC".into(), "SHA-256".into(), vec!["sign".into()], false, 9, ) .unwrap(); let key = store.get(id).unwrap(); assert_eq!(key.length, 9); assert_eq!(key.bytes, [0xff, 0x80]); assert!( import_unwrapped_raw( &store, &[0xff, 0xff], "HMAC".into(), "SHA-256".into(), vec!["sign".into()], false, 8 ) .is_err() ); assert!( import_unwrapped_raw( &store, &[0xff, 0xff], "HMAC".into(), "SHA-256".into(), vec!["sign".into()], false, 17 ) .is_err() ); } #[test] fn jwk_padding_is_only_applied_for_aes_kw() { let mut key = test_key(vec![0x31; 16]); key.algorithm = "HMAC".into(); key.hash = "SHA-256".into(); key.extractable = true; key.length = 128; let raw = key_material(&key, "jwk").unwrap(); assert_eq!(raw.last(), Some(&b'}')); let kw = wrapping_material(&key, "jwk", "AES-KW").unwrap(); assert_eq!(kw.len() % 8, 0); assert!(serde_json::from_slice::(&kw).is_ok()); let gcm = wrapping_material(&key, "jwk", "AES-GCM").unwrap(); assert_eq!(&*gcm, &raw); } #[test] fn worker_panics_do_not_poison_the_crypto_pool() { let (sender, receiver) = std::sync::mpsc::channel(); spawn_crypto(|| panic!("worker panic regression")); spawn_crypto(move || { let _ = sender.send(digest("SHA-256", b"abc")); }); assert_eq!( receiver .recv_timeout(std::time::Duration::from_secs(2)) .unwrap() .unwrap(), Sha256::digest(b"abc").to_vec() ); } #[test] fn gcm_ext_iv_matches_nist_vector() { // From the NIST CAVS `gcmEncryptExtIV128.rsp` vectors (1-byte IV). let key = [ 0x15, 0xb2, 0xd4, 0x14, 0x82, 0x64, 0x53, 0xf9, 0xe1, 0xc7, 0xdd, 0x0b, 0x69, 0xd8, 0xd1, 0xeb, ]; let iv = [0xb6]; let plaintext = [ 0x8c, 0xfa, 0x25, 0x55, 0x30, 0xc6, 0xfb, 0xc1, 0x9d, 0x51, 0xbd, 0x4a, 0xeb, 0x39, 0xc9, 0x1b, ]; let ciphertext = aes_gcm_encrypt(&key, &iv, &plaintext, &[], 128).unwrap(); assert_eq!( ciphertext, [ 0x48, 0x22, 0xcb, 0x98, 0xbd, 0x5f, 0x5d, 0x92, 0x1e, 0xe1, 0x92, 0x85, 0xc9, 0x03, 0x23, 0x75, 0x8a, 0x40, 0x67, 0x0e, 0xba, 0xc9, 0x8c, 0xf4, 0xe9, 0xcc, 0x1b, 0xf8, 0xf8, 0x03, 0x16, 0x7d, ] ); assert_eq!( aes_gcm_decrypt(&key, &iv, &ciphertext, &[], 128).unwrap(), plaintext ); } #[test] fn long_gcm_ivs_round_trip() { let iv = [7; 65]; let ciphertext = aes_gcm_encrypt(&[0; 16], &iv, b"abc", &[], 128).unwrap(); assert_eq!( aes_gcm_decrypt(&[0; 16], &iv, &ciphertext, &[], 128).unwrap(), b"abc" ); } #[test] fn pinned_operations_survive_finalizer_disposal_until_finished() { let store = KeyStore::new(); let id = store.insert(test_key(vec![3; 16])).unwrap(); let pinned = store.get(id).unwrap(); let weak = Arc::downgrade(&pinned); store.close(id); assert!(weak.upgrade().is_some()); drop(pinned); assert!(weak.upgrade().is_none()); } }