Skip to main content

fakecloud_kms/
service.rs

1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use base64::Engine;
6use chrono::Utc;
7use http::StatusCode;
8use serde_json::{json, Value};
9use tokio::sync::Mutex as AsyncMutex;
10use uuid::Uuid;
11
12use fakecloud_aws::arn::Arn;
13use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
14use fakecloud_core::validation::*;
15use fakecloud_persistence::SnapshotStore;
16
17use crate::state::{
18    CustomKeyStore, KeyRotation, KmsAlias, KmsGrant, KmsKey, KmsSnapshot, KmsState, SharedKmsState,
19    KMS_SNAPSHOT_SCHEMA_VERSION,
20};
21
22const FAKE_ENVELOPE_PREFIX: &str = "fakecloud-kms:";
23const IMPORTED_ENVELOPE_PREFIX: &str = "fakecloud-imported:";
24
25/// Result of decoding a FakeCloud KMS ciphertext blob. We carry the
26/// plaintext as base64 so the two callers that care (`Decrypt` returns
27/// it to the client, `ReEncrypt` re-wraps it with a new key) can both
28/// hand it straight to the response builder without an extra encode.
29pub(crate) struct DecodedCiphertext {
30    source_arn: String,
31    plaintext_b64: String,
32}
33
34const VALID_KEY_SPECS: &[&str] = &[
35    "ECC_NIST_P256",
36    "ECC_NIST_P384",
37    "ECC_NIST_P521",
38    "ECC_SECG_P256K1",
39    "HMAC_224",
40    "HMAC_256",
41    "HMAC_384",
42    "HMAC_512",
43    "RSA_2048",
44    "RSA_3072",
45    "RSA_4096",
46    "SM2",
47    "SYMMETRIC_DEFAULT",
48];
49
50const VALID_SIGNING_ALGORITHMS: &[&str] = &[
51    "RSASSA_PKCS1_V1_5_SHA_256",
52    "RSASSA_PKCS1_V1_5_SHA_384",
53    "RSASSA_PKCS1_V1_5_SHA_512",
54    "RSASSA_PSS_SHA_256",
55    "RSASSA_PSS_SHA_384",
56    "RSASSA_PSS_SHA_512",
57    "ECDSA_SHA_256",
58    "ECDSA_SHA_384",
59    "ECDSA_SHA_512",
60];
61
62/// Single source of truth for supported KMS actions. Referenced by both
63/// `supported_actions()` (used by the dispatch layer) and
64/// `iam_action_for()` (used by the IAM enforcement layer).
65static KMS_ACTIONS: &[&str] = &[
66    "CreateKey",
67    "DescribeKey",
68    "GetKeyLastUsage",
69    "ListKeys",
70    "EnableKey",
71    "DisableKey",
72    "ScheduleKeyDeletion",
73    "CancelKeyDeletion",
74    "Encrypt",
75    "Decrypt",
76    "ReEncrypt",
77    "GenerateDataKey",
78    "GenerateDataKeyWithoutPlaintext",
79    "GenerateRandom",
80    "CreateAlias",
81    "DeleteAlias",
82    "UpdateAlias",
83    "ListAliases",
84    "TagResource",
85    "UntagResource",
86    "ListResourceTags",
87    "UpdateKeyDescription",
88    "GetKeyPolicy",
89    "PutKeyPolicy",
90    "ListKeyPolicies",
91    "GetKeyRotationStatus",
92    "EnableKeyRotation",
93    "DisableKeyRotation",
94    "RotateKeyOnDemand",
95    "ListKeyRotations",
96    "Sign",
97    "Verify",
98    "GetPublicKey",
99    "CreateGrant",
100    "ListGrants",
101    "ListRetirableGrants",
102    "RevokeGrant",
103    "RetireGrant",
104    "GenerateMac",
105    "VerifyMac",
106    "ReplicateKey",
107    "GenerateDataKeyPair",
108    "GenerateDataKeyPairWithoutPlaintext",
109    "DeriveSharedSecret",
110    "GetParametersForImport",
111    "ImportKeyMaterial",
112    "DeleteImportedKeyMaterial",
113    "UpdatePrimaryRegion",
114    "CreateCustomKeyStore",
115    "DeleteCustomKeyStore",
116    "DescribeCustomKeyStores",
117    "ConnectCustomKeyStore",
118    "DisconnectCustomKeyStore",
119    "UpdateCustomKeyStore",
120];
121
122pub struct KmsService {
123    state: SharedKmsState,
124    snapshot_store: Option<Arc<dyn SnapshotStore>>,
125    snapshot_lock: Arc<AsyncMutex<()>>,
126}
127
128impl KmsService {
129    pub fn new(state: SharedKmsState) -> Self {
130        // Warm the RSA keypair cache in the background. Generation is
131        // CPU-bound (~20s for RSA-4096) and reused across every CreateKey
132        // with that bit width; doing it here off a blocking thread keeps
133        // the first concurrent CreateKey from racing N tokio workers into
134        // a 30s read-timeout cliff (see `asym::generate_keypair`).
135        if tokio::runtime::Handle::try_current().is_ok() {
136            tokio::task::spawn_blocking(|| {
137                let _ = self::asym::generate_keypair("RSA_2048");
138                let _ = self::asym::generate_keypair("RSA_3072");
139                let _ = self::asym::generate_keypair("RSA_4096");
140            });
141        }
142        Self {
143            state,
144            snapshot_store: None,
145            snapshot_lock: Arc::new(AsyncMutex::new(())),
146        }
147    }
148
149    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
150        self.snapshot_store = Some(store);
151        self
152    }
153
154    /// Persist current state as a snapshot. Held across the
155    /// clone-serialize-write sequence to prevent stale-last writes,
156    /// with serde + file I/O offloaded to the blocking pool.
157    async fn save_snapshot(&self) {
158        let Some(store) = self.snapshot_store.clone() else {
159            return;
160        };
161        let _guard = self.snapshot_lock.lock().await;
162        let snapshot = KmsSnapshot {
163            schema_version: KMS_SNAPSHOT_SCHEMA_VERSION,
164            state: None,
165            accounts: Some(self.state.read().clone()),
166        };
167        let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
168            let bytes = serde_json::to_vec(&snapshot)
169                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
170            store.save(&bytes)
171        })
172        .await;
173        match join {
174            Ok(Ok(())) => {}
175            Ok(Err(err)) => tracing::error!(%err, "failed to write kms snapshot"),
176            Err(err) => tracing::error!(%err, "kms snapshot task panicked"),
177        }
178    }
179}
180
181#[async_trait]
182impl AwsService for KmsService {
183    fn service_name(&self) -> &str {
184        "kms"
185    }
186
187    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
188        let mutates = is_mutating_action(req.action.as_str());
189        let result = match req.action.as_str() {
190            "CreateKey" => self.create_key(&req),
191            "DescribeKey" => self.describe_key(&req),
192            "GetKeyLastUsage" => self.get_key_last_usage(&req),
193            "ListKeys" => self.list_keys(&req),
194            "EnableKey" => self.enable_key(&req),
195            "DisableKey" => self.disable_key(&req),
196            "ScheduleKeyDeletion" => self.schedule_key_deletion(&req),
197            "CancelKeyDeletion" => self.cancel_key_deletion(&req),
198            "Encrypt" => self.encrypt(&req),
199            "Decrypt" => self.decrypt(&req),
200            "ReEncrypt" => self.re_encrypt(&req),
201            "GenerateDataKey" => self.generate_data_key(&req),
202            "GenerateDataKeyWithoutPlaintext" => self.generate_data_key_without_plaintext(&req),
203            "GenerateRandom" => self.generate_random(&req),
204            "CreateAlias" => self.create_alias(&req),
205            "DeleteAlias" => self.delete_alias(&req),
206            "UpdateAlias" => self.update_alias(&req),
207            "ListAliases" => self.list_aliases(&req),
208            "TagResource" => self.tag_resource(&req),
209            "UntagResource" => self.untag_resource(&req),
210            "ListResourceTags" => self.list_resource_tags(&req),
211            "UpdateKeyDescription" => self.update_key_description(&req),
212            "GetKeyPolicy" => self.get_key_policy(&req),
213            "PutKeyPolicy" => self.put_key_policy(&req),
214            "ListKeyPolicies" => self.list_key_policies(&req),
215            "GetKeyRotationStatus" => self.get_key_rotation_status(&req),
216            "EnableKeyRotation" => self.enable_key_rotation(&req),
217            "DisableKeyRotation" => self.disable_key_rotation(&req),
218            "RotateKeyOnDemand" => self.rotate_key_on_demand(&req),
219            "ListKeyRotations" => self.list_key_rotations(&req),
220            "Sign" => self.sign(&req),
221            "Verify" => self.verify(&req),
222            "GetPublicKey" => self.get_public_key(&req),
223            "CreateGrant" => self.create_grant(&req),
224            "ListGrants" => self.list_grants(&req),
225            "ListRetirableGrants" => self.list_retirable_grants(&req),
226            "RevokeGrant" => self.revoke_grant(&req),
227            "RetireGrant" => self.retire_grant(&req),
228            "GenerateMac" => self.generate_mac(&req),
229            "VerifyMac" => self.verify_mac(&req),
230            "ReplicateKey" => self.replicate_key(&req),
231            "GenerateDataKeyPair" => self.generate_data_key_pair(&req),
232            "GenerateDataKeyPairWithoutPlaintext" => {
233                self.generate_data_key_pair_without_plaintext(&req)
234            }
235            "DeriveSharedSecret" => self.derive_shared_secret(&req),
236            "GetParametersForImport" => self.get_parameters_for_import(&req),
237            "ImportKeyMaterial" => self.import_key_material(&req),
238            "DeleteImportedKeyMaterial" => self.delete_imported_key_material(&req),
239            "UpdatePrimaryRegion" => self.update_primary_region(&req),
240            "CreateCustomKeyStore" => self.create_custom_key_store(&req),
241            "DeleteCustomKeyStore" => self.delete_custom_key_store(&req),
242            "DescribeCustomKeyStores" => self.describe_custom_key_stores(&req),
243            "ConnectCustomKeyStore" => self.connect_custom_key_store(&req),
244            "DisconnectCustomKeyStore" => self.disconnect_custom_key_store(&req),
245            "UpdateCustomKeyStore" => self.update_custom_key_store(&req),
246            _ => Err(AwsServiceError::action_not_implemented("kms", &req.action)),
247        };
248        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
249            self.save_snapshot().await;
250        }
251        result
252    }
253
254    fn supported_actions(&self) -> &[&str] {
255        KMS_ACTIONS
256    }
257
258    fn iam_enforceable(&self) -> bool {
259        true
260    }
261
262    fn iam_action_for(&self, request: &AwsRequest) -> Option<fakecloud_core::auth::IamAction> {
263        let action = KMS_ACTIONS.iter().copied().find(|a| *a == request.action)?;
264        let resource = kms_resource_for(action, &self.state, request);
265        Some(fakecloud_core::auth::IamAction {
266            service: "kms",
267            action,
268            resource,
269        })
270    }
271
272    fn resource_tags_for(
273        &self,
274        resource_arn: &str,
275    ) -> Option<std::collections::HashMap<String, String>> {
276        if resource_arn == "*" {
277            return Some(std::collections::HashMap::new());
278        }
279        let key_id = resource_arn.rsplit_once(":key/")?.1;
280        let account_id = resource_arn.split(':').nth(4).unwrap_or("").to_string();
281        let accounts = self.state.read();
282        let state = accounts.get(&account_id)?;
283        let key = state.keys.get(key_id)?;
284        Some(
285            key.tags
286                .iter()
287                .map(|(k, v)| (k.clone(), v.clone()))
288                .collect(),
289        )
290    }
291
292    fn request_tags_from(
293        &self,
294        request: &AwsRequest,
295        action: &str,
296    ) -> Option<std::collections::HashMap<String, String>> {
297        match action {
298            "CreateKey" | "TagResource" => {
299                let body = request.json_body();
300                let mut tags = std::collections::HashMap::new();
301                if let Some(arr) = body["Tags"].as_array() {
302                    for tag in arr {
303                        if let (Some(k), Some(v)) =
304                            (tag["TagKey"].as_str(), tag["TagValue"].as_str())
305                        {
306                            tags.insert(k.to_string(), v.to_string());
307                        }
308                    }
309                }
310                Some(tags)
311            }
312            _ => Some(std::collections::HashMap::new()),
313        }
314    }
315}
316
317/// Parsed + validated inputs for `CreateKey`.
318struct CreateKeyInput {
319    custom_key_store_id: Option<String>,
320    description: String,
321    key_usage: String,
322    key_spec: String,
323    origin: String,
324    multi_region: bool,
325    policy: Option<String>,
326    tags: BTreeMap<String, String>,
327}
328
329impl CreateKeyInput {
330    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
331        // CreateKey's Smithy contract doesn't declare ValidationException, so
332        // map each constraint failure onto the closest declared error.
333        recoded("CustomKeyStoreInvalidStateException", || {
334            validate_optional_string_length(
335                "customKeyStoreId",
336                body["CustomKeyStoreId"].as_str(),
337                1,
338                64,
339            )
340        })?;
341        recoded("UnsupportedOperationException", || {
342            validate_optional_string_length("description", body["Description"].as_str(), 0, 8192)
343        })?;
344        recoded("UnsupportedOperationException", || {
345            validate_optional_enum(
346                "keyUsage",
347                body["KeyUsage"].as_str(),
348                &[
349                    "SIGN_VERIFY",
350                    "ENCRYPT_DECRYPT",
351                    "GENERATE_VERIFY_MAC",
352                    "KEY_AGREEMENT",
353                ],
354            )
355        })?;
356        recoded("UnsupportedOperationException", || {
357            validate_optional_enum(
358                "origin",
359                body["Origin"].as_str(),
360                &["AWS_KMS", "EXTERNAL", "AWS_CLOUDHSM", "EXTERNAL_KEY_STORE"],
361            )
362        })?;
363        recoded("MalformedPolicyDocumentException", || {
364            validate_optional_string_length("policy", body["Policy"].as_str(), 1, 131072)
365        })?;
366        recoded("XksKeyInvalidConfigurationException", || {
367            validate_optional_string_length("xksKeyId", body["XksKeyId"].as_str(), 1, 64)
368        })?;
369
370        let key_spec = body["KeySpec"]
371            .as_str()
372            .or_else(|| body["CustomerMasterKeySpec"].as_str())
373            .unwrap_or("SYMMETRIC_DEFAULT")
374            .to_string();
375        if !VALID_KEY_SPECS.contains(&key_spec.as_str()) {
376            return Err(AwsServiceError::aws_error(
377                StatusCode::BAD_REQUEST,
378                "UnsupportedOperationException",
379                format!(
380                    "1 validation error detected: Value '{key_spec}' at 'KeySpec' failed to satisfy constraint: Member must satisfy enum value set: {}",
381                    fmt_enum_set(&VALID_KEY_SPECS.iter().map(|s| s.to_string()).collect::<Vec<_>>())
382                ),
383            ));
384        }
385
386        let tags: BTreeMap<String, String> = body["Tags"]
387            .as_array()
388            .map(|arr| {
389                arr.iter()
390                    .filter_map(|t| {
391                        let k = t["TagKey"].as_str()?;
392                        let v = t["TagValue"].as_str()?;
393                        Some((k.to_string(), v.to_string()))
394                    })
395                    .collect()
396            })
397            .unwrap_or_default();
398
399        Ok(Self {
400            custom_key_store_id: body["CustomKeyStoreId"].as_str().map(|s| s.to_string()),
401            description: body["Description"].as_str().unwrap_or("").to_string(),
402            key_usage: body["KeyUsage"]
403                .as_str()
404                .unwrap_or("ENCRYPT_DECRYPT")
405                .to_string(),
406            key_spec,
407            origin: body["Origin"].as_str().unwrap_or("AWS_KMS").to_string(),
408            multi_region: body["MultiRegion"].as_bool().unwrap_or(false),
409            policy: body["Policy"].as_str().map(|s| s.to_string()),
410            tags,
411        })
412    }
413}
414
415impl KmsService {
416    fn resolve_key_id_for(
417        &self,
418        account_id: &str,
419        region: &str,
420        key_id_or_arn: &str,
421    ) -> Option<String> {
422        let accounts = self.state.read();
423        let empty = KmsState::new(account_id, region);
424        let state = accounts.get(account_id).unwrap_or(&empty);
425        Self::resolve_key_id_with_state(state, key_id_or_arn)
426    }
427
428    pub(crate) fn resolve_key_id_with_state(
429        state: &crate::state::KmsState,
430        key_id_or_arn: &str,
431    ) -> Option<String> {
432        // Direct key ID
433        if state.keys.contains_key(key_id_or_arn) {
434            return Some(key_id_or_arn.to_string());
435        }
436
437        // ARN for key
438        if key_id_or_arn.starts_with("arn:aws:kms:") {
439            // Could be key ARN or alias ARN
440            if key_id_or_arn.contains(":key/") {
441                if let Some(id) = key_id_or_arn.rsplit('/').next() {
442                    if state.keys.contains_key(id) {
443                        return Some(id.to_string());
444                    }
445                }
446            }
447            // alias ARN: arn:aws:kms:region:account:alias/name
448            if key_id_or_arn.contains(":alias/") {
449                if let Some(alias_part) = key_id_or_arn.split(':').next_back() {
450                    if let Some(alias) = state.aliases.get(alias_part) {
451                        return Some(alias.target_key_id.clone());
452                    }
453                }
454            }
455        }
456
457        // Alias name
458        if key_id_or_arn.starts_with("alias/") {
459            if let Some(alias) = state.aliases.get(key_id_or_arn) {
460                return Some(alias.target_key_id.clone());
461            }
462        }
463
464        None
465    }
466
467    fn require_key_id(body: &Value) -> Result<String, AwsServiceError> {
468        body["KeyId"]
469            .as_str()
470            .map(|s| s.to_string())
471            .ok_or_else(|| {
472                AwsServiceError::aws_error(
473                    StatusCode::BAD_REQUEST,
474                    "ValidationException",
475                    "KeyId is required",
476                )
477            })
478    }
479
480    fn resolve_required_key(
481        &self,
482        req: &AwsRequest,
483        body: &Value,
484    ) -> Result<String, AwsServiceError> {
485        let key_id_input = Self::require_key_id(body)?;
486        self.resolve_key_id_for(&req.account_id, &req.region, &key_id_input)
487            .ok_or_else(|| {
488                AwsServiceError::aws_error(
489                    StatusCode::BAD_REQUEST,
490                    "NotFoundException",
491                    format!("Key '{key_id_input}' does not exist"),
492                )
493            })
494    }
495
496    fn create_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
497        let input = CreateKeyInput::from_body(&req.json_body())?;
498
499        let mut accounts = self.state.write();
500        let state = accounts.get_or_create(&req.account_id);
501
502        let key_id = if input.multi_region {
503            format!("mrk-{}", Uuid::new_v4().as_simple())
504        } else {
505            Uuid::new_v4().to_string()
506        };
507
508        let arn = format!(
509            "arn:aws:kms:{}:{}:key/{}",
510            state.region, state.account_id, key_id
511        );
512        let now = Utc::now().timestamp() as f64;
513
514        let signing_algs = if input.key_usage == "SIGN_VERIFY" {
515            signing_algorithms_for_key_spec(&input.key_spec)
516        } else {
517            None
518        };
519        let encryption_algs = encryption_algorithms_for_key(&input.key_usage, &input.key_spec);
520        let mac_algs = if input.key_usage == "GENERATE_VERIFY_MAC" {
521            mac_algorithms_for_key_spec(&input.key_spec)
522        } else {
523            None
524        };
525
526        let key_policy = input
527            .policy
528            .unwrap_or_else(|| default_key_policy(&state.account_id));
529
530        let mut asym_priv: Option<Vec<u8>> = None;
531        let mut asym_pub: Option<Vec<u8>> = None;
532        if let Some((p, k)) = asym::generate_keypair(&input.key_spec).map_err(|e| {
533            AwsServiceError::aws_error(
534                StatusCode::INTERNAL_SERVER_ERROR,
535                "KMSInternalException",
536                format!("failed to generate asymmetric key: {e}"),
537            )
538        })? {
539            asym_priv = Some(p);
540            asym_pub = Some(k);
541        } else if let Some((p, k)) = asym_ecdsa::generate_keypair(&input.key_spec).map_err(|e| {
542            AwsServiceError::aws_error(
543                StatusCode::INTERNAL_SERVER_ERROR,
544                "KMSInternalException",
545                format!("failed to generate ecdsa key: {e}"),
546            )
547        })? {
548            asym_priv = Some(p);
549            asym_pub = Some(k);
550        }
551
552        // Refuse asymmetric specs we cannot really generate keys for
553        // rather than store a no-DER key that would later fall through
554        // to a fake-bytes Sign/Verify path. SM2 currently has no
555        // pure-Rust impl wired in.
556        let is_asymmetric = input.key_spec.starts_with("ECC_")
557            || input.key_spec.starts_with("RSA_")
558            || input.key_spec == "SM2";
559        if is_asymmetric && asym_priv.is_none() {
560            return Err(AwsServiceError::aws_error(
561                StatusCode::BAD_REQUEST,
562                "UnsupportedOperationException",
563                format!(
564                    "KeySpec '{}' is not supported by this fakecloud build; \
565                     no fake-signature fallback is provided",
566                    input.key_spec
567                ),
568            ));
569        }
570
571        let key = KmsKey {
572            key_id: key_id.clone(),
573            arn: arn.clone(),
574            creation_date: now,
575            description: input.description,
576            enabled: true,
577            key_usage: input.key_usage,
578            key_spec: input.key_spec,
579            key_manager: "CUSTOMER".to_string(),
580            key_state: "Enabled".to_string(),
581            deletion_date: None,
582            tags: input.tags,
583            policy: key_policy,
584            key_rotation_enabled: false,
585            origin: input.origin,
586            multi_region: input.multi_region,
587            rotations: Vec::new(),
588            signing_algorithms: signing_algs,
589            encryption_algorithms: encryption_algs,
590            mac_algorithms: mac_algs,
591            custom_key_store_id: input.custom_key_store_id,
592            imported_key_material: false,
593            imported_material_bytes: None,
594            private_key_seed: rand_bytes(32),
595            primary_region: None,
596            asymmetric_private_key_der: asym_priv,
597            asymmetric_public_key_der: asym_pub,
598        };
599
600        let metadata = key_metadata_json(&key, &state.account_id);
601        state.keys.insert(key_id, key);
602
603        Ok(AwsResponse::json(
604            StatusCode::OK,
605            serde_json::to_string(&json!({ "KeyMetadata": metadata })).unwrap(),
606        ))
607    }
608
609    fn describe_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
610        let body = req.json_body();
611        let key_id_input = body["KeyId"].as_str().ok_or_else(|| {
612            AwsServiceError::aws_error(
613                StatusCode::BAD_REQUEST,
614                "ValidationException",
615                "KeyId is required",
616            )
617        })?;
618
619        let accounts = self.state.read();
620        let empty = KmsState::new(&req.account_id, &req.region);
621        let state = accounts.get(&req.account_id).unwrap_or(&empty);
622
623        // Check key policy for Deny rules
624        let resolved = Self::resolve_key_id_with_state(state, key_id_input).ok_or_else(|| {
625            AwsServiceError::aws_error(
626                StatusCode::BAD_REQUEST,
627                "NotFoundException",
628                format!("Key '{key_id_input}' does not exist"),
629            )
630        })?;
631
632        let key = state.keys.get(&resolved).ok_or_else(|| {
633            AwsServiceError::aws_error(
634                StatusCode::BAD_REQUEST,
635                "NotFoundException",
636                format!("Key '{key_id_input}' does not exist"),
637            )
638        })?;
639
640        // Check policy for Deny on DescribeKey
641        check_policy_deny(key, "kms:DescribeKey")?;
642
643        let metadata = key_metadata_json(key, &state.account_id);
644        Ok(AwsResponse::json(
645            StatusCode::OK,
646            serde_json::to_string(&json!({ "KeyMetadata": metadata })).unwrap(),
647        ))
648    }
649
650    fn get_key_last_usage(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
651        let body = req.json_body();
652        let key_id_input = body["KeyId"].as_str().ok_or_else(|| {
653            AwsServiceError::aws_error(
654                StatusCode::BAD_REQUEST,
655                "ValidationException",
656                "KeyId is required",
657            )
658        })?;
659
660        let accounts = self.state.read();
661        let empty = KmsState::new(&req.account_id, &req.region);
662        let state = accounts.get(&req.account_id).unwrap_or(&empty);
663
664        let resolved = Self::resolve_key_id_with_state(state, key_id_input).ok_or_else(|| {
665            AwsServiceError::aws_error(
666                StatusCode::BAD_REQUEST,
667                "NotFoundException",
668                format!("Key '{key_id_input}' does not exist"),
669            )
670        })?;
671        let key = state.keys.get(&resolved).ok_or_else(|| {
672            AwsServiceError::aws_error(
673                StatusCode::BAD_REQUEST,
674                "NotFoundException",
675                format!("Key '{key_id_input}' does not exist"),
676            )
677        })?;
678
679        // KMS started tracking on the key's creation date. We don't yet
680        // record per-op timestamps, so KeyLastUsage is omitted — the AWS
681        // spec explicitly allows this when no tracked op has run.
682        Ok(AwsResponse::json(
683            StatusCode::OK,
684            serde_json::to_string(&json!({
685                "KeyId": key.key_id,
686                "KeyCreationDate": key.creation_date,
687                "TrackingStartDate": key.creation_date,
688            }))
689            .unwrap(),
690        ))
691    }
692
693    fn list_keys(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
694        let body = req.json_body();
695
696        // ListKeys only declares InvalidMarkerException / KMSInternal /
697        // DependencyTimeout; map both Limit and Marker shape failures onto
698        // InvalidMarkerException so the probe sees a declared error.
699        recoded("InvalidMarkerException", || {
700            validate_optional_json_range("limit", &body["Limit"], 1, 1000)
701        })?;
702        recoded("InvalidMarkerException", || {
703            validate_optional_string_length("marker", body["Marker"].as_str(), 1, 320)
704        })?;
705
706        let limit = body["Limit"].as_i64().unwrap_or(1000) as usize;
707        let marker = body["Marker"].as_str();
708
709        let accounts = self.state.read();
710        let empty = KmsState::new(&req.account_id, &req.region);
711        let state = accounts.get(&req.account_id).unwrap_or(&empty);
712        let all_keys: Vec<Value> = state
713            .keys
714            .values()
715            .map(|k| {
716                json!({
717                    "KeyId": k.key_id,
718                    "KeyArn": k.arn,
719                })
720            })
721            .collect();
722
723        let start = if let Some(m) = marker {
724            all_keys
725                .iter()
726                .position(|k| k["KeyId"].as_str() == Some(m))
727                .map(|pos| pos + 1)
728                .unwrap_or(0)
729        } else {
730            0
731        };
732
733        let page = &all_keys[start..all_keys.len().min(start + limit)];
734        let truncated = start + limit < all_keys.len();
735
736        let mut result = json!({
737            "Keys": page,
738            "Truncated": truncated,
739        });
740
741        if truncated {
742            if let Some(last) = page.last() {
743                result["NextMarker"] = last["KeyId"].clone();
744            }
745        }
746
747        Ok(AwsResponse::json(
748            StatusCode::OK,
749            serde_json::to_string(&result).unwrap(),
750        ))
751    }
752
753    fn enable_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
754        let body = req.json_body();
755        let resolved = self.resolve_required_key(req, &body)?;
756
757        let mut accounts = self.state.write();
758        let state = accounts.get_or_create(&req.account_id);
759        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
760            AwsServiceError::aws_error(
761                StatusCode::INTERNAL_SERVER_ERROR,
762                "KMSInternalException",
763                "Key state became inconsistent",
764            )
765        })?;
766        key.enabled = true;
767        key.key_state = "Enabled".to_string();
768
769        Ok(AwsResponse::json(StatusCode::OK, "{}"))
770    }
771
772    fn disable_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
773        let body = req.json_body();
774        let resolved = self.resolve_required_key(req, &body)?;
775
776        let mut accounts = self.state.write();
777        let state = accounts.get_or_create(&req.account_id);
778        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
779            AwsServiceError::aws_error(
780                StatusCode::INTERNAL_SERVER_ERROR,
781                "KMSInternalException",
782                "Key state became inconsistent",
783            )
784        })?;
785        key.enabled = false;
786        key.key_state = "Disabled".to_string();
787
788        Ok(AwsResponse::json(StatusCode::OK, "{}"))
789    }
790
791    fn schedule_key_deletion(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
792        let body = req.json_body();
793        let resolved = self.resolve_required_key(req, &body)?;
794        let pending_days = body["PendingWindowInDays"].as_i64().unwrap_or(30);
795
796        let mut accounts = self.state.write();
797        let state = accounts.get_or_create(&req.account_id);
798        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
799            AwsServiceError::aws_error(
800                StatusCode::INTERNAL_SERVER_ERROR,
801                "KMSInternalException",
802                "Key state became inconsistent",
803            )
804        })?;
805        let deletion_date =
806            Utc::now().timestamp() as f64 + (pending_days as f64 * 24.0 * 60.0 * 60.0);
807        key.key_state = "PendingDeletion".to_string();
808        key.enabled = false;
809        key.deletion_date = Some(deletion_date);
810
811        Ok(AwsResponse::json(
812            StatusCode::OK,
813            serde_json::to_string(&json!({
814                "KeyId": key.key_id,
815                "DeletionDate": deletion_date,
816                "KeyState": "PendingDeletion",
817                "PendingWindowInDays": pending_days,
818            }))
819            .unwrap(),
820        ))
821    }
822
823    fn cancel_key_deletion(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
824        let body = req.json_body();
825        let resolved = self.resolve_required_key(req, &body)?;
826
827        let mut accounts = self.state.write();
828        let state = accounts.get_or_create(&req.account_id);
829        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
830            AwsServiceError::aws_error(
831                StatusCode::INTERNAL_SERVER_ERROR,
832                "KMSInternalException",
833                "Key state became inconsistent",
834            )
835        })?;
836        key.key_state = "Disabled".to_string();
837        key.deletion_date = None;
838
839        Ok(AwsResponse::json(
840            StatusCode::OK,
841            serde_json::to_string(&json!({
842                "KeyId": key.key_id,
843            }))
844            .unwrap(),
845        ))
846    }
847
848    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
849        let body = req.json_body();
850        let key_id = Self::require_key_id(&body)?;
851
852        let resolved = self
853            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
854            .ok_or_else(|| {
855                AwsServiceError::aws_error(
856                    StatusCode::BAD_REQUEST,
857                    "NotFoundException",
858                    format!("Invalid keyId {key_id}"),
859                )
860            })?;
861
862        let mut accounts = self.state.write();
863        let state = accounts.get_or_create(&req.account_id);
864        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
865            AwsServiceError::aws_error(
866                StatusCode::INTERNAL_SERVER_ERROR,
867                "KMSInternalException",
868                "Key state became inconsistent",
869            )
870        })?;
871
872        fakecloud_core::tags::apply_tags(&mut key.tags, &body, "Tags", "TagKey", "TagValue")
873            .map_err(|f| {
874                AwsServiceError::aws_error(
875                    StatusCode::BAD_REQUEST,
876                    "ValidationException",
877                    format!("{f} must be a list"),
878                )
879            })?;
880
881        Ok(AwsResponse::json(StatusCode::OK, "{}"))
882    }
883
884    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
885        let body = req.json_body();
886        let key_id = Self::require_key_id(&body)?;
887
888        let resolved = self
889            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
890            .ok_or_else(|| {
891                AwsServiceError::aws_error(
892                    StatusCode::BAD_REQUEST,
893                    "NotFoundException",
894                    format!("Invalid keyId {key_id}"),
895                )
896            })?;
897
898        let mut accounts = self.state.write();
899        let state = accounts.get_or_create(&req.account_id);
900        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
901            AwsServiceError::aws_error(
902                StatusCode::INTERNAL_SERVER_ERROR,
903                "KMSInternalException",
904                "Key state became inconsistent",
905            )
906        })?;
907
908        fakecloud_core::tags::remove_tags(&mut key.tags, &body, "TagKeys").map_err(|f| {
909            AwsServiceError::aws_error(
910                StatusCode::BAD_REQUEST,
911                "ValidationException",
912                format!("{f} must be a list"),
913            )
914        })?;
915
916        Ok(AwsResponse::json(StatusCode::OK, "{}"))
917    }
918
919    fn list_resource_tags(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
920        let body = req.json_body();
921        let key_id = Self::require_key_id(&body)?;
922
923        let resolved = self
924            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
925            .ok_or_else(|| {
926                AwsServiceError::aws_error(
927                    StatusCode::BAD_REQUEST,
928                    "NotFoundException",
929                    format!("Invalid keyId {key_id}"),
930                )
931            })?;
932
933        let accounts = self.state.read();
934        let empty = KmsState::new(&req.account_id, &req.region);
935        let state = accounts.get(&req.account_id).unwrap_or(&empty);
936        let key = state.keys.get(&resolved).ok_or_else(|| {
937            AwsServiceError::aws_error(
938                StatusCode::INTERNAL_SERVER_ERROR,
939                "KMSInternalException",
940                "Key state became inconsistent",
941            )
942        })?;
943        let tags = fakecloud_core::tags::tags_to_json(&key.tags, "TagKey", "TagValue");
944
945        Ok(AwsResponse::json(
946            StatusCode::OK,
947            serde_json::to_string(&json!({
948                "Tags": tags,
949                "Truncated": false,
950            }))
951            .unwrap(),
952        ))
953    }
954
955    fn update_key_description(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
956        let body = req.json_body();
957        let resolved = self.resolve_required_key(req, &body)?;
958        let description = body["Description"].as_str().unwrap_or("").to_string();
959
960        let mut accounts = self.state.write();
961        let state = accounts.get_or_create(&req.account_id);
962        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
963            AwsServiceError::aws_error(
964                StatusCode::INTERNAL_SERVER_ERROR,
965                "KMSInternalException",
966                "Key state became inconsistent",
967            )
968        })?;
969        key.description = description;
970
971        Ok(AwsResponse::json(StatusCode::OK, "{}"))
972    }
973
974    fn get_key_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
975        let body = req.json_body();
976        let key_id = Self::require_key_id(&body)?;
977
978        // For key policy operations, aliases should not work
979        if key_id.starts_with("alias/") {
980            return Err(AwsServiceError::aws_error(
981                StatusCode::BAD_REQUEST,
982                "NotFoundException",
983                format!("Invalid keyId {key_id}"),
984            ));
985        }
986
987        let resolved = self
988            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
989            .ok_or_else(|| {
990                AwsServiceError::aws_error(
991                    StatusCode::BAD_REQUEST,
992                    "NotFoundException",
993                    format!("Key '{key_id}' does not exist"),
994                )
995            })?;
996
997        let accounts = self.state.read();
998        let empty = KmsState::new(&req.account_id, &req.region);
999        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1000        let key = state.keys.get(&resolved).ok_or_else(|| {
1001            AwsServiceError::aws_error(
1002                StatusCode::INTERNAL_SERVER_ERROR,
1003                "KMSInternalException",
1004                "Key state became inconsistent",
1005            )
1006        })?;
1007
1008        Ok(AwsResponse::json(
1009            StatusCode::OK,
1010            serde_json::to_string(&json!({
1011                "Policy": key.policy,
1012            }))
1013            .unwrap(),
1014        ))
1015    }
1016
1017    fn put_key_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1018        let body = req.json_body();
1019        let key_id = Self::require_key_id(&body)?;
1020
1021        // For key policy operations, aliases should not work
1022        if key_id.starts_with("alias/") {
1023            return Err(AwsServiceError::aws_error(
1024                StatusCode::BAD_REQUEST,
1025                "NotFoundException",
1026                format!("Invalid keyId {key_id}"),
1027            ));
1028        }
1029
1030        let resolved = self
1031            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1032            .ok_or_else(|| {
1033                AwsServiceError::aws_error(
1034                    StatusCode::BAD_REQUEST,
1035                    "NotFoundException",
1036                    format!("Key '{key_id}' does not exist"),
1037                )
1038            })?;
1039
1040        let policy = body["Policy"].as_str().unwrap_or("").to_string();
1041
1042        let mut accounts = self.state.write();
1043        let state = accounts.get_or_create(&req.account_id);
1044        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
1045            AwsServiceError::aws_error(
1046                StatusCode::INTERNAL_SERVER_ERROR,
1047                "KMSInternalException",
1048                "Key state became inconsistent",
1049            )
1050        })?;
1051        key.policy = policy;
1052
1053        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1054    }
1055
1056    fn list_key_policies(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1057        let body = req.json_body();
1058        let _resolved = self.resolve_required_key(req, &body)?;
1059
1060        Ok(AwsResponse::json(
1061            StatusCode::OK,
1062            serde_json::to_string(&json!({
1063                "PolicyNames": ["default"],
1064                "Truncated": false,
1065            }))
1066            .unwrap(),
1067        ))
1068    }
1069
1070    fn get_key_rotation_status(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1071        let body = req.json_body();
1072        let key_id = Self::require_key_id(&body)?;
1073
1074        // Real KMS resolves alias/* and alias-ARNs identically to a key id.
1075        let resolved = self
1076            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1077            .ok_or_else(|| {
1078                AwsServiceError::aws_error(
1079                    StatusCode::BAD_REQUEST,
1080                    "NotFoundException",
1081                    format!("Key '{key_id}' does not exist"),
1082                )
1083            })?;
1084
1085        let accounts = self.state.read();
1086        let empty = KmsState::new(&req.account_id, &req.region);
1087        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1088        let key = state.keys.get(&resolved).ok_or_else(|| {
1089            AwsServiceError::aws_error(
1090                StatusCode::INTERNAL_SERVER_ERROR,
1091                "KMSInternalException",
1092                "Key state became inconsistent",
1093            )
1094        })?;
1095
1096        Ok(AwsResponse::json(
1097            StatusCode::OK,
1098            serde_json::to_string(&json!({
1099                "KeyRotationEnabled": key.key_rotation_enabled,
1100            }))
1101            .unwrap(),
1102        ))
1103    }
1104
1105    fn enable_key_rotation(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1106        let body = req.json_body();
1107        let key_id = Self::require_key_id(&body)?;
1108
1109        // Real KMS resolves alias/* and alias-ARNs identically to a key id
1110        // here. Earlier code rejected `alias/*` outright, breaking IaC
1111        // configs that reference keys by alias.
1112        let resolved = self
1113            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1114            .ok_or_else(|| {
1115                AwsServiceError::aws_error(
1116                    StatusCode::BAD_REQUEST,
1117                    "NotFoundException",
1118                    format!("Key '{key_id}' does not exist"),
1119                )
1120            })?;
1121
1122        let mut accounts = self.state.write();
1123        let state = accounts.get_or_create(&req.account_id);
1124        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
1125            AwsServiceError::aws_error(
1126                StatusCode::INTERNAL_SERVER_ERROR,
1127                "KMSInternalException",
1128                "Key state became inconsistent",
1129            )
1130        })?;
1131        key.key_rotation_enabled = true;
1132
1133        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1134    }
1135
1136    fn disable_key_rotation(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1137        let body = req.json_body();
1138        let key_id = Self::require_key_id(&body)?;
1139
1140        // Real KMS resolves alias/* and alias-ARNs identically to a key id.
1141        let resolved = self
1142            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1143            .ok_or_else(|| {
1144                AwsServiceError::aws_error(
1145                    StatusCode::BAD_REQUEST,
1146                    "NotFoundException",
1147                    format!("Key '{key_id}' does not exist"),
1148                )
1149            })?;
1150
1151        let mut accounts = self.state.write();
1152        let state = accounts.get_or_create(&req.account_id);
1153        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
1154            AwsServiceError::aws_error(
1155                StatusCode::INTERNAL_SERVER_ERROR,
1156                "KMSInternalException",
1157                "Key state became inconsistent",
1158            )
1159        })?;
1160        key.key_rotation_enabled = false;
1161
1162        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1163    }
1164
1165    fn rotate_key_on_demand(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1166        let body = req.json_body();
1167        let resolved = self.resolve_required_key(req, &body)?;
1168
1169        let mut accounts = self.state.write();
1170        let state = accounts.get_or_create(&req.account_id);
1171        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
1172            AwsServiceError::aws_error(
1173                StatusCode::INTERNAL_SERVER_ERROR,
1174                "KMSInternalException",
1175                "Key state became inconsistent",
1176            )
1177        })?;
1178
1179        let rotation = KeyRotation {
1180            key_id: key.key_id.clone(),
1181            rotation_date: Utc::now().timestamp() as f64,
1182            rotation_type: "ON_DEMAND".to_string(),
1183        };
1184        key.rotations.push(rotation);
1185
1186        Ok(AwsResponse::json(
1187            StatusCode::OK,
1188            serde_json::to_string(&json!({
1189                "KeyId": key.key_id,
1190            }))
1191            .unwrap(),
1192        ))
1193    }
1194
1195    fn list_key_rotations(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1196        let body = req.json_body();
1197        let resolved = self.resolve_required_key(req, &body)?;
1198        validate_optional_json_range("limit", &body["Limit"], 1, 1000)?;
1199        let limit = body["Limit"].as_i64().unwrap_or(1000) as usize;
1200        let marker = body["Marker"].as_str();
1201
1202        let accounts = self.state.read();
1203        let empty = KmsState::new(&req.account_id, &req.region);
1204        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1205        let key = state.keys.get(&resolved).ok_or_else(|| {
1206            AwsServiceError::aws_error(
1207                StatusCode::INTERNAL_SERVER_ERROR,
1208                "KMSInternalException",
1209                "Key state became inconsistent",
1210            )
1211        })?;
1212
1213        let start_index = if let Some(marker) = marker {
1214            marker.parse::<usize>().unwrap_or(0)
1215        } else {
1216            0
1217        };
1218
1219        let rotations: Vec<Value> = key
1220            .rotations
1221            .iter()
1222            .skip(start_index)
1223            .take(limit)
1224            .map(|r| {
1225                json!({
1226                    "KeyId": r.key_id,
1227                    "RotationDate": r.rotation_date,
1228                    "RotationType": r.rotation_type,
1229                })
1230            })
1231            .collect();
1232
1233        let total_after_start = key.rotations.len().saturating_sub(start_index);
1234        let truncated = total_after_start > limit;
1235
1236        let mut response = json!({
1237            "Rotations": rotations,
1238            "Truncated": truncated,
1239        });
1240
1241        if truncated {
1242            response["NextMarker"] = json!((start_index + limit).to_string());
1243        }
1244
1245        Ok(AwsResponse::json(
1246            StatusCode::OK,
1247            serde_json::to_string(&response).unwrap(),
1248        ))
1249    }
1250
1251    fn replicate_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1252        let body = req.json_body();
1253        let key_id = Self::require_key_id(&body)?;
1254        let replica_region = body["ReplicaRegion"].as_str().unwrap_or("").to_string();
1255
1256        let resolved = self
1257            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1258            .ok_or_else(|| {
1259                AwsServiceError::aws_error(
1260                    StatusCode::BAD_REQUEST,
1261                    "NotFoundException",
1262                    format!("Key '{key_id}' does not exist"),
1263                )
1264            })?;
1265
1266        let mut accounts = self.state.write();
1267        let state = accounts.get_or_create(&req.account_id);
1268
1269        // Clone the source key once and drop the borrow — the replica reuses
1270        // every field except the region-dependent ones.
1271        let source_key = state
1272            .keys
1273            .get(&resolved)
1274            .ok_or_else(|| {
1275                AwsServiceError::aws_error(
1276                    StatusCode::INTERNAL_SERVER_ERROR,
1277                    "KMSInternalException",
1278                    "Key state became inconsistent",
1279                )
1280            })?
1281            .clone();
1282        let account_id = state.account_id.clone();
1283        let source_region = state.region.clone();
1284
1285        let replica_arn = format!(
1286            "arn:aws:kms:{}:{}:key/{}",
1287            replica_region, account_id, source_key.key_id
1288        );
1289
1290        let metadata = json!({
1291            "KeyId": source_key.key_id,
1292            "Arn": replica_arn,
1293            "AWSAccountId": account_id,
1294            "CreationDate": source_key.creation_date,
1295            "Description": source_key.description,
1296            "Enabled": source_key.enabled,
1297            "KeyUsage": source_key.key_usage,
1298            "KeySpec": source_key.key_spec,
1299            "CustomerMasterKeySpec": source_key.key_spec,
1300            "KeyManager": source_key.key_manager,
1301            "KeyState": source_key.key_state,
1302            "Origin": source_key.origin,
1303            "MultiRegion": true,
1304            "MultiRegionConfiguration": {
1305                "MultiRegionKeyType": "REPLICA",
1306                "PrimaryKey": {
1307                    "Arn": source_key.arn,
1308                    "Region": source_region,
1309                },
1310                "ReplicaKeys": [],
1311            },
1312        });
1313
1314        let replica_storage_key = format!("{}:{}", replica_region, source_key.key_id);
1315        let source_policy = source_key.policy.clone();
1316        let replica_key = KmsKey {
1317            arn: replica_arn,
1318            deletion_date: None,
1319            key_rotation_enabled: false,
1320            multi_region: true,
1321            rotations: Vec::new(),
1322            custom_key_store_id: None,
1323            imported_key_material: false,
1324            imported_material_bytes: None,
1325            private_key_seed: rand_bytes(32),
1326            primary_region: None,
1327            ..source_key
1328        };
1329
1330        state.keys.insert(replica_storage_key, replica_key);
1331
1332        Ok(AwsResponse::json(
1333            StatusCode::OK,
1334            serde_json::to_string(&json!({
1335                "ReplicaKeyMetadata": metadata,
1336                "ReplicaPolicy": source_policy,
1337            }))
1338            .unwrap(),
1339        ))
1340    }
1341
1342    fn update_primary_region(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1343        let body = req.json_body();
1344        let key_id = Self::require_key_id(&body)?;
1345        let primary_region = body["PrimaryRegion"]
1346            .as_str()
1347            .ok_or_else(|| {
1348                AwsServiceError::aws_error(
1349                    StatusCode::BAD_REQUEST,
1350                    "ValidationException",
1351                    "PrimaryRegion is required",
1352                )
1353            })?
1354            .to_string();
1355
1356        let resolved = self
1357            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1358            .ok_or_else(|| {
1359                AwsServiceError::aws_error(
1360                    StatusCode::BAD_REQUEST,
1361                    "NotFoundException",
1362                    format!("Key '{key_id}' does not exist"),
1363                )
1364            })?;
1365
1366        let mut accounts = self.state.write();
1367        let state = accounts.get_or_create(&req.account_id);
1368        let account_id = state.account_id.clone();
1369        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
1370            AwsServiceError::aws_error(
1371                StatusCode::BAD_REQUEST,
1372                "NotFoundException",
1373                format!("Key '{key_id}' does not exist"),
1374            )
1375        })?;
1376
1377        if !key.multi_region {
1378            return Err(AwsServiceError::aws_error(
1379                StatusCode::BAD_REQUEST,
1380                "UnsupportedOperationException",
1381                format!("Key '{}' is not a multi-Region key", key.arn),
1382            ));
1383        }
1384        key.primary_region = Some(primary_region.clone());
1385        // Update the ARN to reflect the new region
1386        key.arn = format!(
1387            "arn:aws:kms:{}:{}:key/{}",
1388            primary_region, account_id, key.key_id
1389        );
1390
1391        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1392    }
1393}
1394
1395#[path = "asym.rs"]
1396pub(crate) mod asym;
1397#[path = "asym_ecdsa.rs"]
1398pub(crate) mod asym_ecdsa;
1399#[path = "mac.rs"]
1400pub(crate) mod mac;
1401#[path = "service_aliases.rs"]
1402mod service_aliases;
1403#[path = "service_crypto.rs"]
1404mod service_crypto;
1405#[path = "service_custom_store.rs"]
1406mod service_custom_store;
1407#[path = "service_grants.rs"]
1408mod service_grants;
1409
1410#[path = "helpers.rs"]
1411mod helpers;
1412pub(crate) use helpers::*;
1413
1414#[path = "provisioner.rs"]
1415pub mod provisioner;
1416
1417#[cfg(test)]
1418#[path = "service_tests.rs"]
1419mod tests;