// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Static single-key KMS backend implementation //! //! This backend holds one pre-configured AES-256 key and uses it directly //! to encrypt/decrypt data encryption keys (DEKs) via AES-256-GCM. //! //! ## Ciphertext format //! //! encrypted_data(plaintext_len+16) || nonce (12 bytes) use crate::backends::{BackendCapabilities, BackendInfo, KmsBackend, KmsClient}; use crate::config::{BackendConfig, KmsConfig}; use crate::encryption::DataKeyEnvelope; use crate::error::{KmsError, Result}; use crate::types::*; use aes_gcm::{ Aes256Gcm, Key, Nonce, aead::{Aead, KeyInit, Payload}, }; use async_trait::async_trait; use jiff::Zoned; use rand::RngExt; use std::collections::{BTreeMap, HashMap}; use tracing::debug; use zeroize::Zeroizing; /// AES-GCM nonce size in bytes, appended to each ciphertext. const NONCE_SIZE: usize = 12; /// AES-256 key size in bytes. const KEY_SIZE: usize = 32; fn context_aad(context: &HashMap) -> Result> { let canonical: BTreeMap<&str, &str> = context.iter().map(|(key, value)| (key.as_str(), value.as_str())).collect(); serde_json::to_vec(&canonical).map_err(Into::into) } /// Static single-key KMS backend. /// /// Uses a pre-configured AES-256 key to derive data encryption keys. This is a /// read-only backend: it cannot create, delete, or rotate keys. pub struct StaticKmsBackend { /// The configured key identifier (name). key_id: String, /// The raw 32-byte AES-256 key material (zeroed on drop). key: Zeroizing<[u8; KEY_SIZE]>, } impl StaticKmsBackend { /// Create a new static KMS backend from configuration. pub async fn new(mut config: KmsConfig) -> Result { let BackendConfig::Static(ref mut static_config) = config.backend_config else { return Err(KmsError::configuration_error("Static KMS backend requires StaticConfig")); }; let key = static_config.decode_key()?; // Zeroize the base64-encoded secret key in the config after extracting the raw key. use zeroize::Zeroize; static_config.secret_key.zeroize(); debug!( key_id = %static_config.key_id, "Static KMS backend initialized" ); Ok(Self { key_id: static_config.key_id.clone(), key: key.into(), }) } /// Build the key metadata for the single configured key. fn key_metadata(&self) -> KeyMetadata { KeyMetadata { key_id: self.key_id.clone(), key_state: KeyState::Enabled, key_usage: KeyUsage::EncryptDecrypt, description: Some("Static single-key KMS backend".to_string()), creation_date: Zoned::now(), deletion_date: None, origin: "EXTERNAL".to_string(), key_manager: "STATIC".to_string(), tags: HashMap::new(), } } } #[async_trait] impl KmsClient for StaticKmsBackend { async fn generate_data_key(&self, request: &GenerateKeyRequest, _context: Option<&OperationContext>) -> Result { if request.master_key_id != self.key_id { return Err(KmsError::key_not_found(&request.master_key_id)); } // Generate 12-byte random nonce let mut nonce_bytes = [0u8; NONCE_SIZE]; rand::rng().fill(&mut nonce_bytes[..]); // Generate 32 random bytes as plaintext DEK let mut plaintext = [0u8; KEY_SIZE]; rand::rng().fill(&mut plaintext[..]); // Encrypt DEK with AES-256-GCM using the static key directly let key = Key::::from(*self.key); let cipher = Aes256Gcm::new(&key); let nonce = Nonce::from(nonce_bytes); let aad = context_aad(&request.encryption_context)?; let encrypted = cipher .encrypt( &nonce, Payload { msg: plaintext.as_ref(), aad: &aad, }, ) .map_err(|e| KmsError::cryptographic_error("AES-256-GCM encrypt", e.to_string()))?; let envelope = DataKeyEnvelope { key_id: uuid::Uuid::new_v4().to_string(), master_key_id: request.master_key_id.clone(), key_spec: request.key_spec.clone(), encrypted_key: encrypted, nonce: nonce_bytes.to_vec(), encryption_context: request.encryption_context.clone(), created_at: Zoned::now(), // The static backend has a single fixed key with no rotation. master_key_version: None, }; let ciphertext = serde_json::to_vec(&envelope)?; Ok(DataKeyInfo::new( self.key_id.clone(), 0, Some(plaintext.to_vec()), ciphertext, request.key_spec.clone(), )) } async fn encrypt(&self, request: &EncryptRequest, _context: Option<&OperationContext>) -> Result { if request.key_id != self.key_id { return Err(KmsError::key_not_found(&request.key_id)); } // Generate 12-byte random nonce let mut nonce_bytes = [0u8; NONCE_SIZE]; rand::rng().fill(&mut nonce_bytes[..]); let key = Key::::from(*self.key); let cipher = Aes256Gcm::new(&key); let nonce = Nonce::from(nonce_bytes); let aad = context_aad(&request.encryption_context)?; let encrypted = cipher .encrypt( &nonce, Payload { msg: request.plaintext.as_ref(), aad: &aad, }, ) .map_err(|e| KmsError::cryptographic_error("AES-256-GCM encrypt", e.to_string()))?; let envelope = DataKeyEnvelope { key_id: uuid::Uuid::new_v4().to_string(), master_key_id: request.key_id.clone(), key_spec: "AES_256".to_string(), encrypted_key: encrypted, nonce: nonce_bytes.to_vec(), encryption_context: request.encryption_context.clone(), created_at: Zoned::now(), // The static backend has a single fixed key with no rotation. master_key_version: None, }; let ciphertext = serde_json::to_vec(&envelope)?; Ok(EncryptResponse { ciphertext, key_id: self.key_id.clone(), key_version: 0, algorithm: "AES-256-GCM".to_string(), }) } async fn decrypt(&self, request: &DecryptRequest, _context: Option<&OperationContext>) -> Result> { let envelope: DataKeyEnvelope = serde_json::from_slice(&request.ciphertext) .map_err(|error| KmsError::cryptographic_error("parse", format!("Failed to parse data key envelope: {error}")))?; if envelope.master_key_id != self.key_id { return Err(KmsError::key_not_found(&envelope.master_key_id)); } for (key, expected_value) in &envelope.encryption_context { match request.encryption_context.get(key) { Some(actual_value) if actual_value == expected_value => {} Some(actual_value) => { return Err(KmsError::context_mismatch(format!( "Context mismatch for key '{key}': expected '{expected_value}', got '{actual_value}'" ))); } None if request.encryption_context.is_empty() => {} None => return Err(KmsError::context_mismatch(format!("Missing context key '{key}'"))), } } let key = Key::::from(*self.key); let cipher = Aes256Gcm::new(&key); let nonce = Nonce::try_from(envelope.nonce.as_slice()) .map_err(|_| KmsError::cryptographic_error("nonce", "invalid nonce length"))?; let aad = context_aad(&envelope.encryption_context)?; let plaintext = cipher .decrypt( &nonce, Payload { msg: envelope.encrypted_key.as_ref(), aad: &aad, }, ) .map_err(|e| KmsError::cryptographic_error("AES-256-GCM decrypt", e.to_string()))?; Ok(plaintext) } async fn create_key(&self, key_id: &str, _algorithm: &str, _context: Option<&OperationContext>) -> Result { if key_id == self.key_id { return Err(KmsError::key_already_exists(key_id)); } Err(KmsError::invalid_operation("Static KMS is read-only: cannot create new keys")) } async fn describe_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { if key_id != self.key_id { return Err(KmsError::key_not_found(key_id)); } let metadata = self.key_metadata(); Ok(KeyInfo { key_id: metadata.key_id, description: metadata.description, algorithm: "AES_256".to_string(), usage: metadata.key_usage, status: KeyStatus::Active, version: 1, metadata: HashMap::new(), tags: HashMap::new(), created_at: metadata.creation_date, rotated_at: None, created_by: None, }) } async fn list_keys(&self, request: &ListKeysRequest, _context: Option<&OperationContext>) -> Result { let key_info = KeyInfo { key_id: self.key_id.clone(), description: Some("Static single-key KMS backend".to_string()), algorithm: "AES_256".to_string(), usage: KeyUsage::EncryptDecrypt, status: KeyStatus::Active, version: 1, metadata: HashMap::new(), tags: HashMap::new(), created_at: Zoned::now(), rotated_at: None, created_by: None, }; // Apply prefix filter if provided if let Some(ref marker) = request.marker && self.key_id <= *marker { return Ok(ListKeysResponse { keys: vec![], next_marker: None, truncated: false, }); } Ok(ListKeysResponse { keys: vec![key_info], next_marker: None, truncated: false, }) } async fn enable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { if key_id != self.key_id { return Err(KmsError::key_not_found(key_id)); } // Static KMS key is always enabled Ok(()) } async fn disable_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { if key_id != self.key_id { return Err(KmsError::key_not_found(key_id)); } Err(KmsError::invalid_operation("Static KMS is read-only: cannot disable keys")) } async fn schedule_key_deletion( &self, key_id: &str, _pending_window_days: u32, _context: Option<&OperationContext>, ) -> Result<()> { if key_id != self.key_id { return Err(KmsError::key_not_found(key_id)); } Err(KmsError::invalid_operation("Static KMS is read-only: cannot schedule key deletion")) } async fn cancel_key_deletion(&self, key_id: &str, _context: Option<&OperationContext>) -> Result<()> { if key_id != self.key_id { return Err(KmsError::key_not_found(key_id)); } Err(KmsError::invalid_operation("Static KMS is read-only: cannot cancel key deletion")) } async fn rotate_key(&self, key_id: &str, _context: Option<&OperationContext>) -> Result { if key_id != self.key_id { return Err(KmsError::key_not_found(key_id)); } Err(KmsError::invalid_operation("Static KMS is read-only: cannot rotate keys")) } async fn health_check(&self) -> Result<()> { // Static KMS is always healthy if it was successfully initialized Ok(()) } fn backend_info(&self) -> BackendInfo { BackendInfo::new("static".to_string(), env!("CARGO_PKG_VERSION").to_string(), "local".to_string(), true) .with_metadata("key_id".to_string(), self.key_id.clone()) } } #[async_trait] impl KmsBackend for StaticKmsBackend { async fn create_key(&self, request: CreateKeyRequest) -> Result { let key_name = request.key_name.as_deref().unwrap_or(""); if key_name == self.key_id { return Err(KmsError::key_already_exists(&self.key_id)); } Err(KmsError::invalid_operation("Static KMS is read-only: cannot create new keys")) } async fn encrypt(&self, request: EncryptRequest) -> Result { ::encrypt(self, &request, None).await } async fn decrypt(&self, request: DecryptRequest) -> Result { let key_id = self.key_id.clone(); let plaintext = ::decrypt(self, &request, None).await?; Ok(DecryptResponse { plaintext, key_id, encryption_algorithm: Some("AES-256-GCM".to_string()), }) } async fn generate_data_key(&self, request: GenerateDataKeyRequest) -> Result { let gen_req = GenerateKeyRequest { master_key_id: request.key_id.clone(), key_spec: request.key_spec.as_str().to_string(), key_length: None, encryption_context: request.encryption_context, grant_tokens: Vec::new(), }; let data_key = ::generate_data_key(self, &gen_req, None).await?; let plaintext_key = data_key .plaintext .clone() .ok_or_else(|| KmsError::internal_error("Generated data key is missing plaintext"))?; Ok(GenerateDataKeyResponse { key_id: data_key.key_id.clone(), plaintext_key, ciphertext_blob: data_key.ciphertext.clone(), }) } async fn describe_key(&self, request: DescribeKeyRequest) -> Result { let key_info = ::describe_key(self, &request.key_id, None).await?; let key_metadata = KeyMetadata { key_id: key_info.key_id.clone(), key_state: if key_info.status == KeyStatus::Active { KeyState::Enabled } else { KeyState::Disabled }, key_usage: key_info.usage, description: key_info.description, creation_date: key_info.created_at, deletion_date: None, origin: "EXTERNAL".to_string(), key_manager: "STATIC".to_string(), tags: key_info.tags, }; Ok(DescribeKeyResponse { key_metadata }) } async fn list_keys(&self, request: ListKeysRequest) -> Result { ::list_keys(self, &request, None).await } async fn delete_key(&self, request: DeleteKeyRequest) -> Result { if request.key_id != self.key_id { return Err(KmsError::key_not_found(&request.key_id)); } Err(KmsError::invalid_operation("Static KMS is read-only: cannot delete keys")) } async fn cancel_key_deletion(&self, request: CancelKeyDeletionRequest) -> Result { if request.key_id != self.key_id { return Err(KmsError::key_not_found(&request.key_id)); } Err(KmsError::invalid_operation("Static KMS is read-only: cannot cancel key deletion")) } async fn health_check(&self) -> Result { Ok(true) } fn capabilities(&self) -> BackendCapabilities { // Static KMS is a read-only single-key backend: it only performs // cryptographic operations and rejects every lifecycle mutation. BackendCapabilities::minimal() } } #[cfg(test)] mod tests { use super::*; use crate::backends::{KmsBackend as KmsBackendTrait, KmsClient}; use crate::config::{BackendConfig, KmsBackend, StaticConfig}; use crate::encryption::is_data_key_envelope; use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64; /// Generate a random 32-byte key and return (key_id, raw_key). fn random_static_key(key_id: &str) -> (String, [u8; 32]) { let mut key = [0u8; 32]; rand::rng().fill(&mut key[..]); (key_id.to_string(), key) } fn static_config(key_id: &str, raw_key: &[u8; 32]) -> StaticConfig { StaticConfig { key_id: key_id.to_string(), secret_key: BASE64.encode(raw_key), } } fn kms_config(config: StaticConfig) -> KmsConfig { KmsConfig { backend: KmsBackend::Static, default_key_id: Some(config.key_id.clone()), backend_config: BackendConfig::Static(config), ..Default::default() } } async fn create_test_backend() -> (StaticKmsBackend, String, [u8; 32]) { let (key_id, key) = random_static_key("test-static-key"); let config = static_config(&key_id, &key); let backend = StaticKmsBackend::new(kms_config(config)) .await .expect("Failed to create backend"); (backend, key_id, key) } #[tokio::test] async fn test_generate_and_decrypt_data_key() { let (backend, key_id, _key) = create_test_backend().await; // Generate data key let request = GenerateKeyRequest::new(key_id.clone(), "AES_256".to_string()) .with_context("bucket".to_string(), "test-bucket".to_string()); let data_key = KmsClient::generate_data_key(&backend, &request, None) .await .expect("Failed to generate data key"); assert_eq!(data_key.key_id, key_id); assert_eq!(data_key.version, 0); assert!(data_key.plaintext.is_some()); assert_eq!(data_key.plaintext.as_ref().expect("plaintext should be set").len(), 32); let envelope: DataKeyEnvelope = serde_json::from_slice(&data_key.ciphertext).expect("static data key should use a KMS envelope"); assert_eq!(envelope.master_key_id, key_id); assert_eq!(envelope.encrypted_key.len(), 32 + 16); assert_eq!(envelope.nonce.len(), NONCE_SIZE); assert_eq!(envelope.encryption_context.get("bucket").map(String::as_str), Some("test-bucket")); // Decrypt the data key let decrypt_request = DecryptRequest::new(data_key.ciphertext.clone()).with_context("bucket".to_string(), "test-bucket".to_string()); let decrypted = KmsClient::decrypt(&backend, &decrypt_request, None) .await .expect("Failed to decrypt"); assert_eq!(decrypted.as_slice(), data_key.plaintext.as_deref().expect("plaintext should exist")); } #[tokio::test] async fn generated_data_key_uses_kms_envelope_for_sse_read_routing() { let (backend, key_id, _key) = create_test_backend().await; let request = GenerateKeyRequest::new(key_id, "AES_256".to_string()) .with_context("bucket".to_string(), "source-bucket".to_string()) .with_context("object".to_string(), "source-object".to_string()); let data_key = KmsClient::generate_data_key(&backend, &request, None) .await .expect("generate static KMS data key"); assert!( is_data_key_envelope(&data_key.ciphertext), "static KMS ciphertext must use the KMS envelope recognized by the SSE read path" ); } #[tokio::test] async fn generated_data_key_rejects_a_different_encryption_context() { let (backend, key_id, _key) = create_test_backend().await; let generate_request = GenerateDataKeyRequest { key_id, key_spec: KeySpec::Aes256, encryption_context: HashMap::from([ ("bucket".to_string(), "source-bucket".to_string()), ("object".to_string(), "source-object".to_string()), ]), }; let generated = KmsBackendTrait::generate_data_key(&backend, generate_request) .await .expect("generate context-bound static KMS data key"); let decrypt_request = DecryptRequest { ciphertext: generated.ciphertext_blob, encryption_context: HashMap::from([ ("bucket".to_string(), "different-bucket".to_string()), ("object".to_string(), "different-object".to_string()), ]), grant_tokens: Vec::new(), }; let error = KmsBackendTrait::decrypt(&backend, decrypt_request) .await .expect_err("a static KMS data key must not decrypt under a different object context"); assert!(matches!(error, KmsError::ContextMismatch { .. })); } #[tokio::test] async fn generated_data_key_rejects_tampered_envelope_context() { let (backend, key_id, _key) = create_test_backend().await; let request = GenerateKeyRequest::new(key_id, "AES_256".to_string()) .with_context("bucket".to_string(), "source-bucket".to_string()); let generated = KmsClient::generate_data_key(&backend, &request, None) .await .expect("generate context-bound data key"); let mut envelope: DataKeyEnvelope = serde_json::from_slice(&generated.ciphertext).expect("parse static KMS envelope"); envelope .encryption_context .insert("bucket".to_string(), "different-bucket".to_string()); let decrypt_request = DecryptRequest::new(serde_json::to_vec(&envelope).expect("serialize tampered envelope")) .with_context("bucket".to_string(), "different-bucket".to_string()); let error = KmsClient::decrypt(&backend, &decrypt_request, None) .await .expect_err("tampering with authenticated envelope context must fail"); assert!(matches!(error, KmsError::CryptographicError { .. })); } #[tokio::test] async fn test_generate_data_key_wrong_key_id() { let (backend, _key_id, _key) = create_test_backend().await; let request = GenerateKeyRequest::new("wrong-key-id".to_string(), "AES_256".to_string()); let result = KmsClient::generate_data_key(&backend, &request, None).await; assert!(result.is_err()); assert!(result.expect_err("should be Err").to_string().contains("wrong-key-id")); } #[tokio::test] async fn test_decrypt_invalid_ciphertext() { let (backend, _key_id, _key) = create_test_backend().await; // Ciphertext too short let short = vec![0u8; 10]; let request = DecryptRequest::new(short); let result = KmsClient::decrypt(&backend, &request, None).await; assert!(result.is_err()); } #[tokio::test] async fn test_decrypt_tampered_ciphertext() { let (backend, key_id, _key) = create_test_backend().await; // Generate a valid ciphertext first let gen_request = GenerateKeyRequest::new(key_id, "AES_256".to_string()); let data_key = KmsClient::generate_data_key(&backend, &gen_request, None) .await .expect("generate"); // Tamper with the ciphertext (flip a bit in the encrypted portion) let mut tampered = data_key.ciphertext.clone(); if !tampered.is_empty() { tampered[0] ^= 0x01; } let request = DecryptRequest::new(tampered); let result = KmsClient::decrypt(&backend, &request, None).await; assert!(result.is_err()); } #[tokio::test] async fn test_create_key_returns_exists_for_configured_key() { let (backend, key_id, _key) = create_test_backend().await; // Creating the pre-configured key should return KeyAlreadyExists let result = KmsClient::create_key(&backend, &key_id, "AES_256", None).await; assert!(result.is_err()); assert!(result.expect_err("should be Err").to_string().contains("already exists")); } #[tokio::test] async fn test_create_key_returns_error_for_other_keys() { let (backend, _key_id, _key) = create_test_backend().await; // Creating any other key should return invalid operation (read-only) let result = KmsClient::create_key(&backend, "other-key", "AES_256", None).await; assert!(result.is_err()); let err_msg = result.expect_err("should be Err").to_string(); assert!(err_msg.contains("read-only") || err_msg.contains("cannot create")); } #[tokio::test] async fn test_describe_key() { let (backend, key_id, _key) = create_test_backend().await; let key_info = KmsClient::describe_key(&backend, &key_id, None) .await .expect("describe_key should succeed"); assert_eq!(key_info.key_id, key_id); assert_eq!(key_info.status, KeyStatus::Active); assert_eq!(key_info.algorithm, "AES_256"); // Wrong key ID let result = KmsClient::describe_key(&backend, "nonexistent", None).await; assert!(result.is_err()); } #[tokio::test] async fn test_list_keys() { let (backend, key_id, _key) = create_test_backend().await; let response = KmsClient::list_keys(&backend, &ListKeysRequest::default(), None) .await .expect("list_keys should succeed"); assert_eq!(response.keys.len(), 1); assert_eq!(response.keys[0].key_id, key_id); assert!(!response.truncated); } #[tokio::test] async fn test_disable_key_returns_error() { let (backend, key_id, _key) = create_test_backend().await; let result = KmsClient::disable_key(&backend, &key_id, None).await; assert!(result.is_err()); assert!(result.expect_err("should be Err").to_string().contains("read-only")); } #[tokio::test] async fn test_enable_key_is_noop() { let (backend, key_id, _key) = create_test_backend().await; // Enable should succeed (no-op for static KMS) KmsClient::enable_key(&backend, &key_id, None) .await .expect("enable_key should be no-op"); // Wrong key should still fail let result = KmsClient::enable_key(&backend, "wrong", None).await; assert!(result.is_err()); } #[tokio::test] async fn test_delete_key_returns_error() { let (backend, key_id, _key) = create_test_backend().await; let result = KmsClient::schedule_key_deletion(&backend, &key_id, 7, None).await; assert!(result.is_err()); assert!(result.expect_err("should be Err").to_string().contains("read-only")); } #[tokio::test] async fn test_rotate_key_returns_error() { let (backend, key_id, _key) = create_test_backend().await; let result = KmsClient::rotate_key(&backend, &key_id, None).await; assert!(result.is_err()); } #[tokio::test] async fn test_health_check() { let (backend, _key_id, _key) = create_test_backend().await; KmsClient::health_check(&backend).await.expect("health_check should succeed"); } #[tokio::test] async fn test_backend_info() { let (backend, key_id, _key) = create_test_backend().await; let info = KmsClient::backend_info(&backend); assert_eq!(info.backend_type, "static"); assert_eq!(info.endpoint, "local"); assert!(info.healthy); assert_eq!(info.metadata.get("key_id"), Some(&key_id)); } #[tokio::test] async fn test_encrypt_decrypt_direct() { let (backend, key_id, _key) = create_test_backend().await; let plaintext = b"Hello, static KMS world!"; let enc_request = EncryptRequest::new(key_id.clone(), plaintext.to_vec()); let enc_response = KmsClient::encrypt(&backend, &enc_request, None) .await .expect("encrypt should succeed"); assert_eq!(enc_response.key_id, key_id); assert!(!enc_response.ciphertext.is_empty()); let dec_request = DecryptRequest::new(enc_response.ciphertext); let decrypted = KmsClient::decrypt(&backend, &dec_request, None) .await .expect("decrypt should succeed"); assert_eq!(decrypted, plaintext); } #[tokio::test] async fn test_kms_backend_trait_methods() { use crate::backends::KmsBackend; let (backend, key_id, _key) = create_test_backend().await; // Test KmsBackend::generate_data_key let gen_req = GenerateDataKeyRequest::new(key_id.clone(), KeySpec::Aes256); let gen_resp = KmsBackend::generate_data_key(&backend, gen_req) .await .expect("KmsBackend::generate_data_key should succeed"); assert_eq!(gen_resp.key_id, key_id); assert_eq!(gen_resp.plaintext_key.len(), 32); assert!(!gen_resp.ciphertext_blob.is_empty()); // Test KmsBackend::decrypt via the generated ciphertext let dec_req = DecryptRequest::new(gen_resp.ciphertext_blob); let dec_resp = KmsBackend::decrypt(&backend, dec_req) .await .expect("KmsBackend::decrypt should succeed"); assert_eq!(dec_resp.plaintext, gen_resp.plaintext_key); // Test KmsBackend::describe_key let desc_req = DescribeKeyRequest { key_id: key_id.clone() }; let desc_resp = KmsBackend::describe_key(&backend, desc_req) .await .expect("describe_key should succeed"); assert_eq!(desc_resp.key_metadata.key_id, key_id); // Test KmsBackend::create_key for the configured key (should return KeyAlreadyExists, same as KmsClient) let create_req = CreateKeyRequest { key_name: Some(key_id.clone()), ..Default::default() }; let create_err = KmsBackend::create_key(&backend, create_req) .await .expect_err("create_key for configured key should return KeyAlreadyExists"); assert!(create_err.to_string().contains("already exists")); } }