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