alien-bindings 3.3.17

Alien direct in-process resource bindings
Documentation
use super::{encode_context, frame, unframe};
use crate::error::{ErrorData, Result};
use crate::traits::{Binding, Key};
use alien_error::{Context, IntoAlienError};
use alien_gcp_clients::cloud_kms::{CloudKmsApi, DecryptRequest, EncryptRequest};
use async_trait::async_trait;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use std::collections::BTreeMap;
use std::sync::Arc;

#[derive(Debug)]
pub struct GcpCloudKmsKey {
    client: Arc<dyn CloudKmsApi>,
    crypto_key_name: String,
}

impl GcpCloudKmsKey {
    pub fn new(client: Arc<dyn CloudKmsApi>, crypto_key_name: String) -> Self {
        Self {
            client,
            crypto_key_name,
        }
    }
}

impl Binding for GcpCloudKmsKey {}

#[async_trait]
impl Key for GcpCloudKmsKey {
    async fn encrypt(
        &self,
        plaintext: &[u8],
        context: Option<&BTreeMap<String, String>>,
    ) -> Result<Vec<u8>> {
        let canonical = encode_context(context)?;
        let response = self
            .client
            .encrypt(
                &self.crypto_key_name,
                EncryptRequest {
                    plaintext: STANDARD.encode(frame(plaintext, &canonical)?),
                    additional_authenticated_data: Some(STANDARD.encode(&canonical)),
                },
            )
            .await
            .context(ErrorData::CloudPlatformError {
                message: "GCP Cloud KMS encrypt failed".to_string(),
                resource_id: None,
            })?;
        STANDARD
            .decode(response.ciphertext)
            .into_alien_error()
            .context(ErrorData::CloudPlatformError {
                message: "GCP Cloud KMS returned invalid ciphertext encoding".to_string(),
                resource_id: None,
            })
    }

    async fn decrypt(
        &self,
        ciphertext: &[u8],
        context: Option<&BTreeMap<String, String>>,
    ) -> Result<Vec<u8>> {
        let canonical = encode_context(context)?;
        let response = self
            .client
            .decrypt(
                &self.crypto_key_name,
                DecryptRequest {
                    ciphertext: STANDARD.encode(ciphertext),
                    additional_authenticated_data: Some(STANDARD.encode(&canonical)),
                },
            )
            .await
            .context(ErrorData::CloudPlatformError {
                message: "GCP Cloud KMS decrypt failed".to_string(),
                resource_id: None,
            })?;
        let framed = STANDARD
            .decode(response.plaintext)
            .into_alien_error()
            .context(ErrorData::KeyCiphertextInvalid {
                reason: "provider plaintext encoding is invalid".to_string(),
            })?;
        unframe(&framed, &canonical)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alien_gcp_clients::cloud_kms::{DecryptResponse, EncryptResponse, MockCloudKmsApi};

    #[tokio::test]
    async fn passes_canonical_context_as_aad() {
        let context = BTreeMap::from([("tenant".to_string(), "acme".to_string())]);
        let canonical = encode_context(Some(&context)).unwrap();
        let expected_aad = STANDARD.encode(&canonical);
        let framed = frame(b"root", &canonical).unwrap();
        let encrypt_aad = expected_aad.clone();
        let mut client = MockCloudKmsApi::new();
        client
            .expect_encrypt()
            .withf(move |name, request| {
                name == "key-name"
                    && request.additional_authenticated_data.as_ref() == Some(&encrypt_aad)
            })
            .returning(|_, _| {
                Ok(EncryptResponse {
                    name: "version-name".to_string(),
                    ciphertext: STANDARD.encode(b"ciphertext"),
                })
            });
        client
            .expect_decrypt()
            .withf(move |name, request| {
                name == "key-name"
                    && request.ciphertext == STANDARD.encode(b"ciphertext")
                    && request.additional_authenticated_data.as_ref() == Some(&expected_aad)
            })
            .returning(move |_, _| {
                Ok(DecryptResponse {
                    plaintext: STANDARD.encode(&framed),
                })
            });

        let key = GcpCloudKmsKey::new(Arc::new(client), "key-name".to_string());
        let ciphertext = key.encrypt(b"root", Some(&context)).await.unwrap();
        assert_eq!(
            key.decrypt(&ciphertext, Some(&context)).await.unwrap(),
            b"root"
        );
    }
}