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