Skip to main content
Glama
aes.rs17 kB
use anyhow::Context as _; use aws_lc_rs::{ aead::{ self, Aad, LessSafeKey, Nonce, UnboundKey, }, cipher::{ self, DecryptionContext, EncryptingKey, EncryptionContext, PaddedBlockDecryptingKey, PaddedBlockEncryptingKey, UnboundCipherKey, }, error::Unspecified, }; use indexmap::IndexSet; use serde::{ Deserialize, Serialize, }; use serde_bytes::ByteBuf; use super::{ check_usages_subset, CryptoKey, CryptoKeyKind, ImportKeyInput, JsonWebKey, KeyData, KeyFormat, KeyType, KeyUsage, URL_SAFE_FORGIVING, }; use crate::{ convert_v8::{ DOMException, DOMExceptionName, }, environment::crypto_rng::CryptoRng, }; const AES_BLOCK_SIZE: usize = 16; #[derive(Deserialize, Serialize, Debug, PartialEq)] #[allow(clippy::enum_variant_names)] pub enum AesAlgorithm { /// The "AES-CTR" algorithm identifier is used to perform encryption and /// decryption using AES in Counter mode, as described in NIST-SP800-38A. #[serde(rename = "AES-CTR")] AesCtr, /// The "AES-CBC" algorithm identifier is used to perform encryption and /// decryption using AES in Cipher Block Chaining mode, as described in /// NIST-SP800-38A. #[serde(rename = "AES-CBC")] AesCbc, /// The "AES-GCM" algorithm identifier is used to perform authenticated /// encryption and decryption using AES in Galois/Counter Mode mode, as /// described in NIST-SP800-38D. #[serde(rename = "AES-GCM")] AesGcm, } #[derive(Deserialize)] pub(crate) struct AesCtrParams { /// The counter member contains the initial value of the counter block. /// counter MUST be 16 bytes (the AES block size). The counter bits are the /// rightmost length bits of the counter block. The rest of the counter /// block is for the nonce. The counter bits are incremented using the /// standard incrementing function specified in NIST SP 800-38A Appendix /// B.1: the counter bits are interpreted as a big-endian integer and /// incremented by one. counter: ByteBuf, /// The length member contains the length, in bits, of the rightmost part of /// the counter block that is incremented. length: u8, } #[derive(Deserialize)] pub(crate) struct AesCbcParams { /// The iv member represents the initialization vector. It MUST be 16 bytes. iv: ByteBuf, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct AesGcmParams { /// The iv member represents the initialization vector to use. May be up to /// 2^64-1 bytes long. iv: ByteBuf, /// The additionalData member represents the additional authentication data /// to include. additional_data: Option<ByteBuf>, /// The tagLength member represents the desired length of the authentication /// tag. May be 0 - 128. tag_length: Option<u8>, } #[derive(Deserialize, Serialize)] pub(crate) struct AesKeyAlgorithm { pub(crate) name: AesAlgorithm, /// The length member represents the length, in bits, of the key. pub(crate) length: u16, } impl AesKeyAlgorithm { fn jwk_alg(&self) -> String { match self.name { AesAlgorithm::AesCtr => format!("A{}CTR", self.length), AesAlgorithm::AesCbc => format!("A{}CBC", self.length), AesAlgorithm::AesGcm => format!("A{}GCM", self.length), } } } pub(crate) type AesKeyGenParams = AesKeyAlgorithm; pub(crate) type AesDerivedKeyParams = AesKeyAlgorithm; impl AesDerivedKeyParams { pub(crate) fn get_key_length(&self) -> anyhow::Result<usize> { anyhow::ensure!( [128, 192, 256].contains(&self.length), DOMException::new( "AES key length must be 128, 192, or 256 bits", DOMExceptionName::OperationError, ) ); Ok(self.length as usize) } } pub(crate) struct AesKey { key: Vec<u8>, } pub(crate) fn import_key( format: ImportKeyInput, algorithm: AesAlgorithm, extractable: bool, usages: IndexSet<KeyUsage>, ) -> anyhow::Result<CryptoKey> { let (data, algorithm) = match format { ImportKeyInput::Raw(data) => { let length = data.len() * 8; anyhow::ensure!( [128, 192, 256].contains(&length), DOMException::new("invalid key length", DOMExceptionName::DataError) ); ( data, AesKeyAlgorithm { name: algorithm, length: length as u16, }, ) }, ImportKeyInput::Jwk(jwk) => { jwk.check_kty("oct")?; let data = jwk .k .as_ref() .and_then(|k| base64::decode_config(k, URL_SAFE_FORGIVING).ok()) .ok_or_else(|| { anyhow::anyhow!(DOMException::new( "invalid key data", DOMExceptionName::DataError )) })?; let length = data.len() * 8; anyhow::ensure!( [128, 192, 256].contains(&length), DOMException::new("invalid key length", DOMExceptionName::DataError) ); let algorithm = AesKeyAlgorithm { name: algorithm, length: length as u16, }; jwk.check_alg(&algorithm.jwk_alg())?; jwk.check_key_ops_and_use(&usages, "enc")?; jwk.check_ext(extractable)?; (data, algorithm) }, ImportKeyInput::Pkcs8(_) | ImportKeyInput::Spki(_) => { anyhow::bail!(DOMException::new( "unsupported import format", DOMExceptionName::NotSupportedError )) }, }; Ok(CryptoKey { kind: CryptoKeyKind::Aes { algorithm, key: AesKey { key: data }, }, r#type: KeyType::Secret, extractable, usages, }) } impl AesKey { fn aes_key(&self) -> anyhow::Result<UnboundCipherKey> { let alg = match self.key.len() { 16 => &cipher::AES_128, 24 => &cipher::AES_192, 32 => &cipher::AES_256, l => anyhow::bail!("unexpected key length {l}"), }; Ok(UnboundCipherKey::new(alg, &self.key)?) } fn aes_gcm_key(&self) -> anyhow::Result<LessSafeKey> { let alg = match self.key.len() { 16 => &aead::AES_128_GCM, 24 => &aead::AES_192_GCM, 32 => &aead::AES_256_GCM, l => anyhow::bail!("unexpected key length {l}"), }; let key = LessSafeKey::new( UnboundKey::new(alg, &self.key) .ok() .context("invalid AES-GCM key")?, ); Ok(key) } pub(crate) fn export_key( &self, algorithm: &AesKeyAlgorithm, format: KeyFormat, ) -> anyhow::Result<KeyData> { match format { KeyFormat::Raw => Ok(KeyData::Raw(self.key.clone())), KeyFormat::Jwk => { let jwk = JsonWebKey { kty: Some("oct".to_owned()), k: Some(base64::encode_config(&self.key, base64::URL_SAFE_NO_PAD)), alg: Some(algorithm.jwk_alg()), ..Default::default() }; Ok(KeyData::Jwk(jwk)) }, KeyFormat::Pkcs8 | KeyFormat::Spki => { anyhow::bail!(DOMException::new( "unsupported export format", DOMExceptionName::NotSupportedError )) }, } } pub(crate) fn crypt_ctr( &self, algorithm: AesCtrParams, key_algorithm: &AesKeyAlgorithm, mut data: Vec<u8>, ) -> anyhow::Result<Vec<u8>> { anyhow::ensure!( key_algorithm.name == AesAlgorithm::AesCtr, DOMException::new( "invalid algorithm for key", DOMExceptionName::InvalidAccessError ) ); let AesCtrParams { counter, length } = algorithm; let Ok(counter) = <[u8; 16]>::try_from(&counter[..]) else { anyhow::bail!(DOMException::new( "counter must be 16 bytes", DOMExceptionName::OperationError )); }; anyhow::ensure!( length > 0 && length <= 128, DOMException::new("invalid counter length", DOMExceptionName::OperationError) ); if let Some(limit) = AES_BLOCK_SIZE.checked_shl(length as u32) && data.len() > limit { anyhow::bail!(DOMException::new( "too much data for counter length", DOMExceptionName::OperationError )); } let key = self.aes_key()?; let key = EncryptingKey::ctr(key)?; // WebCrypto's AES-CTR is defined to only increment the lowest `length` // bits of the counter, wrapping on overflow. // Most normal implementations of AES-CTR, including aws-lc-rs, use the // entire 128 bits of the counter as a big-endian integer (i.e. // length==128). // So we may need to do two AES-CTR operations to simulate the specified // overflow behaviour. // // counter_overflow_block_index is the minimum number such that `counter // + counter_overflow_block_index` changes the `length`th bit (counting // from LSB) if let Some(bit) = 1u128.checked_shl(length.into()) && let counter_overflow_block_index = bit - (u128::from_be_bytes(counter) & (bit - 1)) && let Ok(counter_overflow_block_index) = usize::try_from(counter_overflow_block_index) && let Some(counter_overflow_index) = counter_overflow_block_index.checked_mul(AES_BLOCK_SIZE) && counter_overflow_index < data.len() { key.less_safe_encrypt( &mut data[..counter_overflow_index], EncryptionContext::Iv128(counter.into()), )?; // simulate overflow let overflowed_counter = u128::from_be_bytes(counter) & !(bit - 1); key.less_safe_encrypt( &mut data[counter_overflow_index..], EncryptionContext::Iv128(overflowed_counter.to_be_bytes().into()), )?; } else { // overflow would not occur key.less_safe_encrypt(&mut data, EncryptionContext::Iv128(counter.into()))?; } Ok(data) } pub(crate) fn encrypt_cbc( &self, algorithm: AesCbcParams, key_algorithm: &AesKeyAlgorithm, mut data: Vec<u8>, ) -> anyhow::Result<Vec<u8>> { anyhow::ensure!( key_algorithm.name == AesAlgorithm::AesCbc, DOMException::new( "invalid algorithm for key", DOMExceptionName::InvalidAccessError ) ); let AesCbcParams { iv } = algorithm; let Ok(iv) = <[u8; 16]>::try_from(&iv[..]) else { anyhow::bail!(DOMException::new( "iv must be 16 bytes", DOMExceptionName::OperationError )); }; let key = self.aes_key()?; let key = PaddedBlockEncryptingKey::cbc_pkcs7(key)?; key.less_safe_encrypt(&mut data, EncryptionContext::Iv128(iv.into()))?; Ok(data) } pub(crate) fn decrypt_cbc( &self, algorithm: AesCbcParams, key_algorithm: &AesKeyAlgorithm, mut data: Vec<u8>, ) -> anyhow::Result<Vec<u8>> { anyhow::ensure!( key_algorithm.name == AesAlgorithm::AesCbc, DOMException::new( "invalid algorithm for key", DOMExceptionName::InvalidAccessError ) ); let AesCbcParams { iv } = algorithm; let Ok(iv) = <[u8; 16]>::try_from(&iv[..]) else { anyhow::bail!(DOMException::new( "iv must be 16 bytes", DOMExceptionName::OperationError )); }; let key = self.aes_key()?; let key = PaddedBlockDecryptingKey::cbc_pkcs7(key)?; let len = key .decrypt(&mut data, DecryptionContext::Iv128(iv.into())) .map_err(|_| DOMException::new("invalid ciphertext", DOMExceptionName::OperationError))? .len(); data.truncate(len); Ok(data) } pub(crate) fn encrypt_gcm( &self, algorithm: AesGcmParams, key_algorithm: &AesKeyAlgorithm, mut data: Vec<u8>, ) -> anyhow::Result<Vec<u8>> { anyhow::ensure!( key_algorithm.name == AesAlgorithm::AesGcm, DOMException::new( "invalid algorithm for key", DOMExceptionName::InvalidAccessError ) ); let AesGcmParams { iv, additional_data, tag_length, } = algorithm; let key = self.aes_gcm_key()?; let alg = key.algorithm(); // TODO: consider supporting shorter tag lengths (`ring` does not allow this) anyhow::ensure!( tag_length.is_none_or(|l| usize::from(l) == 8 * alg.tag_len()), DOMException::new( format!("tag length must be {} bits", 8 * alg.tag_len()), DOMExceptionName::NotSupportedError ) ); key.seal_in_place_append_tag( // TODO: consider supporting GHASH construction for nonces (`ring` does not // support this) Nonce::try_assume_unique_for_key(&iv).map_err(|_| { DOMException::new( format!("AES-GCM IV must be {} bits", 8 * alg.nonce_len()), DOMExceptionName::NotSupportedError, ) })?, Aad::from(additional_data.as_ref().map_or(&[][..], |b| b.as_ref())), &mut data, ) .ok() .context("AES-GCM encryption failed")?; Ok(data) } pub(crate) fn decrypt_gcm( &self, algorithm: AesGcmParams, key_algorithm: &AesKeyAlgorithm, mut data: Vec<u8>, ) -> anyhow::Result<Vec<u8>> { anyhow::ensure!( key_algorithm.name == AesAlgorithm::AesGcm, DOMException::new( "invalid algorithm for key", DOMExceptionName::InvalidAccessError ) ); let AesGcmParams { iv, additional_data, tag_length, } = algorithm; let key = self.aes_gcm_key()?; let alg = key.algorithm(); // TODO: consider supporting shorter tag lengths (`ring` does not allow this) anyhow::ensure!( tag_length.is_none_or(|l| usize::from(l) == 8 * alg.tag_len()), DOMException::new( format!("tag length must be {} bits", 8 * alg.tag_len()), DOMExceptionName::NotSupportedError ) ); anyhow::ensure!( data.len() >= alg.tag_len(), DOMException::new( "The provided data is too small.", DOMExceptionName::OperationError ) ); let plaintext_len = key .open_in_place( Nonce::try_assume_unique_for_key(&iv).map_err(|_| { DOMException::new( format!("AES-GCM IV must be {} bits", 8 * alg.nonce_len()), DOMExceptionName::NotSupportedError, ) })?, Aad::from(additional_data.as_ref().map_or(&[][..], |b| b.as_ref())), &mut data, ) .map_err(|Unspecified| { DOMException::new("Decryption failed", DOMExceptionName::OperationError) })? .len(); data.truncate(plaintext_len); Ok(data) } } pub(crate) fn generate_key( algorithm: AesKeyGenParams, _rng: &CryptoRng, extractable: bool, usages: IndexSet<KeyUsage>, ) -> anyhow::Result<CryptoKey> { check_usages_subset( &usages, &[ KeyUsage::Encrypt, KeyUsage::Decrypt, KeyUsage::WrapKey, KeyUsage::UnwrapKey, ], )?; let length = algorithm.get_key_length()?; let mut key_bytes = vec![0u8; length / 8]; aws_lc_rs::rand::fill(&mut key_bytes)?; Ok(CryptoKey { kind: CryptoKeyKind::Aes { algorithm: AesKeyAlgorithm { name: algorithm.name, length: length as u16, }, key: AesKey { key: key_bytes }, }, r#type: KeyType::Secret, extractable, usages, }) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server