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