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        // CreateKey's Smithy contract doesn't declare ValidationException, so
318        // map each constraint failure onto the closest declared error.
319        recoded("CustomKeyStoreInvalidStateException", || {
320            validate_optional_string_length(
321                "customKeyStoreId",
322                body["CustomKeyStoreId"].as_str(),
323                1,
324                64,
325            )
326        })?;
327        recoded("UnsupportedOperationException", || {
328            validate_optional_string_length("description", body["Description"].as_str(), 0, 8192)
329        })?;
330        recoded("UnsupportedOperationException", || {
331            validate_optional_enum(
332                "keyUsage",
333                body["KeyUsage"].as_str(),
334                &[
335                    "SIGN_VERIFY",
336                    "ENCRYPT_DECRYPT",
337                    "GENERATE_VERIFY_MAC",
338                    "KEY_AGREEMENT",
339                ],
340            )
341        })?;
342        recoded("UnsupportedOperationException", || {
343            validate_optional_enum(
344                "origin",
345                body["Origin"].as_str(),
346                &["AWS_KMS", "EXTERNAL", "AWS_CLOUDHSM", "EXTERNAL_KEY_STORE"],
347            )
348        })?;
349        recoded("MalformedPolicyDocumentException", || {
350            validate_optional_string_length("policy", body["Policy"].as_str(), 1, 131072)
351        })?;
352        recoded("XksKeyInvalidConfigurationException", || {
353            validate_optional_string_length("xksKeyId", body["XksKeyId"].as_str(), 1, 64)
354        })?;
355
356        let key_spec = body["KeySpec"]
357            .as_str()
358            .or_else(|| body["CustomerMasterKeySpec"].as_str())
359            .unwrap_or("SYMMETRIC_DEFAULT")
360            .to_string();
361        if !VALID_KEY_SPECS.contains(&key_spec.as_str()) {
362            return Err(AwsServiceError::aws_error(
363                StatusCode::BAD_REQUEST,
364                "UnsupportedOperationException",
365                format!(
366                    "1 validation error detected: Value '{key_spec}' at 'KeySpec' failed to satisfy constraint: Member must satisfy enum value set: {}",
367                    fmt_enum_set(&VALID_KEY_SPECS.iter().map(|s| s.to_string()).collect::<Vec<_>>())
368                ),
369            ));
370        }
371
372        let tags: BTreeMap<String, String> = body["Tags"]
373            .as_array()
374            .map(|arr| {
375                arr.iter()
376                    .filter_map(|t| {
377                        let k = t["TagKey"].as_str()?;
378                        let v = t["TagValue"].as_str()?;
379                        Some((k.to_string(), v.to_string()))
380                    })
381                    .collect()
382            })
383            .unwrap_or_default();
384
385        Ok(Self {
386            custom_key_store_id: body["CustomKeyStoreId"].as_str().map(|s| s.to_string()),
387            description: body["Description"].as_str().unwrap_or("").to_string(),
388            key_usage: body["KeyUsage"]
389                .as_str()
390                .unwrap_or("ENCRYPT_DECRYPT")
391                .to_string(),
392            key_spec,
393            origin: body["Origin"].as_str().unwrap_or("AWS_KMS").to_string(),
394            multi_region: body["MultiRegion"].as_bool().unwrap_or(false),
395            policy: body["Policy"].as_str().map(|s| s.to_string()),
396            tags,
397        })
398    }
399}
400
401impl KmsService {
402    fn resolve_key_id_for(
403        &self,
404        account_id: &str,
405        region: &str,
406        key_id_or_arn: &str,
407    ) -> Option<String> {
408        let accounts = self.state.read();
409        let empty = KmsState::new(account_id, region);
410        let state = accounts.get(account_id).unwrap_or(&empty);
411        Self::resolve_key_id_with_state(state, key_id_or_arn)
412    }
413
414    pub(crate) fn resolve_key_id_with_state(
415        state: &crate::state::KmsState,
416        key_id_or_arn: &str,
417    ) -> Option<String> {
418        // Direct key ID
419        if state.keys.contains_key(key_id_or_arn) {
420            return Some(key_id_or_arn.to_string());
421        }
422
423        // ARN for key
424        if key_id_or_arn.starts_with("arn:aws:kms:") {
425            // Could be key ARN or alias ARN
426            if key_id_or_arn.contains(":key/") {
427                if let Some(id) = key_id_or_arn.rsplit('/').next() {
428                    if state.keys.contains_key(id) {
429                        return Some(id.to_string());
430                    }
431                }
432            }
433            // alias ARN: arn:aws:kms:region:account:alias/name
434            if key_id_or_arn.contains(":alias/") {
435                if let Some(alias_part) = key_id_or_arn.split(':').next_back() {
436                    if let Some(alias) = state.aliases.get(alias_part) {
437                        return Some(alias.target_key_id.clone());
438                    }
439                }
440            }
441        }
442
443        // Alias name
444        if key_id_or_arn.starts_with("alias/") {
445            if let Some(alias) = state.aliases.get(key_id_or_arn) {
446                return Some(alias.target_key_id.clone());
447            }
448        }
449
450        None
451    }
452
453    fn require_key_id(body: &Value) -> Result<String, AwsServiceError> {
454        body["KeyId"]
455            .as_str()
456            .map(|s| s.to_string())
457            .ok_or_else(|| {
458                AwsServiceError::aws_error(
459                    StatusCode::BAD_REQUEST,
460                    "ValidationException",
461                    "KeyId is required",
462                )
463            })
464    }
465
466    fn resolve_required_key(
467        &self,
468        req: &AwsRequest,
469        body: &Value,
470    ) -> Result<String, AwsServiceError> {
471        let key_id_input = Self::require_key_id(body)?;
472        self.resolve_key_id_for(&req.account_id, &req.region, &key_id_input)
473            .ok_or_else(|| {
474                AwsServiceError::aws_error(
475                    StatusCode::BAD_REQUEST,
476                    "NotFoundException",
477                    format!("Key '{key_id_input}' does not exist"),
478                )
479            })
480    }
481
482    fn create_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
483        let input = CreateKeyInput::from_body(&req.json_body())?;
484
485        let mut accounts = self.state.write();
486        let state = accounts.get_or_create(&req.account_id);
487
488        let key_id = if input.multi_region {
489            format!("mrk-{}", Uuid::new_v4().as_simple())
490        } else {
491            Uuid::new_v4().to_string()
492        };
493
494        let arn = format!(
495            "arn:aws:kms:{}:{}:key/{}",
496            state.region, state.account_id, key_id
497        );
498        let now = Utc::now().timestamp() as f64;
499
500        let signing_algs = if input.key_usage == "SIGN_VERIFY" {
501            signing_algorithms_for_key_spec(&input.key_spec)
502        } else {
503            None
504        };
505        let encryption_algs = encryption_algorithms_for_key(&input.key_usage, &input.key_spec);
506        let mac_algs = if input.key_usage == "GENERATE_VERIFY_MAC" {
507            mac_algorithms_for_key_spec(&input.key_spec)
508        } else {
509            None
510        };
511
512        let key_policy = input
513            .policy
514            .unwrap_or_else(|| default_key_policy(&state.account_id));
515
516        let mut asym_priv: Option<Vec<u8>> = None;
517        let mut asym_pub: Option<Vec<u8>> = None;
518        if let Some((p, k)) = asym::generate_keypair(&input.key_spec).map_err(|e| {
519            AwsServiceError::aws_error(
520                StatusCode::INTERNAL_SERVER_ERROR,
521                "KMSInternalException",
522                format!("failed to generate asymmetric key: {e}"),
523            )
524        })? {
525            asym_priv = Some(p);
526            asym_pub = Some(k);
527        } else if let Some((p, k)) = asym_ecdsa::generate_keypair(&input.key_spec).map_err(|e| {
528            AwsServiceError::aws_error(
529                StatusCode::INTERNAL_SERVER_ERROR,
530                "KMSInternalException",
531                format!("failed to generate ecdsa key: {e}"),
532            )
533        })? {
534            asym_priv = Some(p);
535            asym_pub = Some(k);
536        }
537
538        // Refuse asymmetric specs we cannot really generate keys for
539        // rather than store a no-DER key that would later fall through
540        // to a fake-bytes Sign/Verify path. SM2 currently has no
541        // pure-Rust impl wired in.
542        let is_asymmetric = input.key_spec.starts_with("ECC_")
543            || input.key_spec.starts_with("RSA_")
544            || input.key_spec == "SM2";
545        if is_asymmetric && asym_priv.is_none() {
546            return Err(AwsServiceError::aws_error(
547                StatusCode::BAD_REQUEST,
548                "UnsupportedOperationException",
549                format!(
550                    "KeySpec '{}' is not supported by this fakecloud build; \
551                     no fake-signature fallback is provided",
552                    input.key_spec
553                ),
554            ));
555        }
556
557        let key = KmsKey {
558            key_id: key_id.clone(),
559            arn: arn.clone(),
560            creation_date: now,
561            description: input.description,
562            enabled: true,
563            key_usage: input.key_usage,
564            key_spec: input.key_spec,
565            key_manager: "CUSTOMER".to_string(),
566            key_state: "Enabled".to_string(),
567            deletion_date: None,
568            tags: input.tags,
569            policy: key_policy,
570            key_rotation_enabled: false,
571            origin: input.origin,
572            multi_region: input.multi_region,
573            rotations: Vec::new(),
574            signing_algorithms: signing_algs,
575            encryption_algorithms: encryption_algs,
576            mac_algorithms: mac_algs,
577            custom_key_store_id: input.custom_key_store_id,
578            imported_key_material: false,
579            imported_material_bytes: None,
580            private_key_seed: rand_bytes(32),
581            primary_region: None,
582            asymmetric_private_key_der: asym_priv,
583            asymmetric_public_key_der: asym_pub,
584        };
585
586        let metadata = key_metadata_json(&key, &state.account_id);
587        state.keys.insert(key_id, key);
588
589        Ok(AwsResponse::json(
590            StatusCode::OK,
591            serde_json::to_string(&json!({ "KeyMetadata": metadata })).unwrap(),
592        ))
593    }
594
595    fn describe_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
596        let body = req.json_body();
597        let key_id_input = body["KeyId"].as_str().ok_or_else(|| {
598            AwsServiceError::aws_error(
599                StatusCode::BAD_REQUEST,
600                "ValidationException",
601                "KeyId is required",
602            )
603        })?;
604
605        let accounts = self.state.read();
606        let empty = KmsState::new(&req.account_id, &req.region);
607        let state = accounts.get(&req.account_id).unwrap_or(&empty);
608
609        // Check key policy for Deny rules
610        let resolved = Self::resolve_key_id_with_state(state, key_id_input).ok_or_else(|| {
611            AwsServiceError::aws_error(
612                StatusCode::BAD_REQUEST,
613                "NotFoundException",
614                format!("Key '{key_id_input}' does not exist"),
615            )
616        })?;
617
618        let key = state.keys.get(&resolved).ok_or_else(|| {
619            AwsServiceError::aws_error(
620                StatusCode::BAD_REQUEST,
621                "NotFoundException",
622                format!("Key '{key_id_input}' does not exist"),
623            )
624        })?;
625
626        // Check policy for Deny on DescribeKey
627        check_policy_deny(key, "kms:DescribeKey")?;
628
629        let metadata = key_metadata_json(key, &state.account_id);
630        Ok(AwsResponse::json(
631            StatusCode::OK,
632            serde_json::to_string(&json!({ "KeyMetadata": metadata })).unwrap(),
633        ))
634    }
635
636    fn list_keys(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
637        let body = req.json_body();
638
639        // ListKeys only declares InvalidMarkerException / KMSInternal /
640        // DependencyTimeout; map both Limit and Marker shape failures onto
641        // InvalidMarkerException so the probe sees a declared error.
642        recoded("InvalidMarkerException", || {
643            validate_optional_json_range("limit", &body["Limit"], 1, 1000)
644        })?;
645        recoded("InvalidMarkerException", || {
646            validate_optional_string_length("marker", body["Marker"].as_str(), 1, 320)
647        })?;
648
649        let limit = body["Limit"].as_i64().unwrap_or(1000) as usize;
650        let marker = body["Marker"].as_str();
651
652        let accounts = self.state.read();
653        let empty = KmsState::new(&req.account_id, &req.region);
654        let state = accounts.get(&req.account_id).unwrap_or(&empty);
655        let all_keys: Vec<Value> = state
656            .keys
657            .values()
658            .map(|k| {
659                json!({
660                    "KeyId": k.key_id,
661                    "KeyArn": k.arn,
662                })
663            })
664            .collect();
665
666        let start = if let Some(m) = marker {
667            all_keys
668                .iter()
669                .position(|k| k["KeyId"].as_str() == Some(m))
670                .map(|pos| pos + 1)
671                .unwrap_or(0)
672        } else {
673            0
674        };
675
676        let page = &all_keys[start..all_keys.len().min(start + limit)];
677        let truncated = start + limit < all_keys.len();
678
679        let mut result = json!({
680            "Keys": page,
681            "Truncated": truncated,
682        });
683
684        if truncated {
685            if let Some(last) = page.last() {
686                result["NextMarker"] = last["KeyId"].clone();
687            }
688        }
689
690        Ok(AwsResponse::json(
691            StatusCode::OK,
692            serde_json::to_string(&result).unwrap(),
693        ))
694    }
695
696    fn enable_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
697        let body = req.json_body();
698        let resolved = self.resolve_required_key(req, &body)?;
699
700        let mut accounts = self.state.write();
701        let state = accounts.get_or_create(&req.account_id);
702        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
703            AwsServiceError::aws_error(
704                StatusCode::INTERNAL_SERVER_ERROR,
705                "KMSInternalException",
706                "Key state became inconsistent",
707            )
708        })?;
709        key.enabled = true;
710        key.key_state = "Enabled".to_string();
711
712        Ok(AwsResponse::json(StatusCode::OK, "{}"))
713    }
714
715    fn disable_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
716        let body = req.json_body();
717        let resolved = self.resolve_required_key(req, &body)?;
718
719        let mut accounts = self.state.write();
720        let state = accounts.get_or_create(&req.account_id);
721        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
722            AwsServiceError::aws_error(
723                StatusCode::INTERNAL_SERVER_ERROR,
724                "KMSInternalException",
725                "Key state became inconsistent",
726            )
727        })?;
728        key.enabled = false;
729        key.key_state = "Disabled".to_string();
730
731        Ok(AwsResponse::json(StatusCode::OK, "{}"))
732    }
733
734    fn schedule_key_deletion(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
735        let body = req.json_body();
736        let resolved = self.resolve_required_key(req, &body)?;
737        let pending_days = body["PendingWindowInDays"].as_i64().unwrap_or(30);
738
739        let mut accounts = self.state.write();
740        let state = accounts.get_or_create(&req.account_id);
741        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
742            AwsServiceError::aws_error(
743                StatusCode::INTERNAL_SERVER_ERROR,
744                "KMSInternalException",
745                "Key state became inconsistent",
746            )
747        })?;
748        let deletion_date =
749            Utc::now().timestamp() as f64 + (pending_days as f64 * 24.0 * 60.0 * 60.0);
750        key.key_state = "PendingDeletion".to_string();
751        key.enabled = false;
752        key.deletion_date = Some(deletion_date);
753
754        Ok(AwsResponse::json(
755            StatusCode::OK,
756            serde_json::to_string(&json!({
757                "KeyId": key.key_id,
758                "DeletionDate": deletion_date,
759                "KeyState": "PendingDeletion",
760                "PendingWindowInDays": pending_days,
761            }))
762            .unwrap(),
763        ))
764    }
765
766    fn cancel_key_deletion(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
767        let body = req.json_body();
768        let resolved = self.resolve_required_key(req, &body)?;
769
770        let mut accounts = self.state.write();
771        let state = accounts.get_or_create(&req.account_id);
772        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
773            AwsServiceError::aws_error(
774                StatusCode::INTERNAL_SERVER_ERROR,
775                "KMSInternalException",
776                "Key state became inconsistent",
777            )
778        })?;
779        key.key_state = "Disabled".to_string();
780        key.deletion_date = None;
781
782        Ok(AwsResponse::json(
783            StatusCode::OK,
784            serde_json::to_string(&json!({
785                "KeyId": key.key_id,
786            }))
787            .unwrap(),
788        ))
789    }
790
791    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
792        let body = req.json_body();
793        let key_id = Self::require_key_id(&body)?;
794
795        let resolved = self
796            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
797            .ok_or_else(|| {
798                AwsServiceError::aws_error(
799                    StatusCode::BAD_REQUEST,
800                    "NotFoundException",
801                    format!("Invalid keyId {key_id}"),
802                )
803            })?;
804
805        let mut accounts = self.state.write();
806        let state = accounts.get_or_create(&req.account_id);
807        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
808            AwsServiceError::aws_error(
809                StatusCode::INTERNAL_SERVER_ERROR,
810                "KMSInternalException",
811                "Key state became inconsistent",
812            )
813        })?;
814
815        fakecloud_core::tags::apply_tags(&mut key.tags, &body, "Tags", "TagKey", "TagValue")
816            .map_err(|f| {
817                AwsServiceError::aws_error(
818                    StatusCode::BAD_REQUEST,
819                    "ValidationException",
820                    format!("{f} must be a list"),
821                )
822            })?;
823
824        Ok(AwsResponse::json(StatusCode::OK, "{}"))
825    }
826
827    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
828        let body = req.json_body();
829        let key_id = Self::require_key_id(&body)?;
830
831        let resolved = self
832            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
833            .ok_or_else(|| {
834                AwsServiceError::aws_error(
835                    StatusCode::BAD_REQUEST,
836                    "NotFoundException",
837                    format!("Invalid keyId {key_id}"),
838                )
839            })?;
840
841        let mut accounts = self.state.write();
842        let state = accounts.get_or_create(&req.account_id);
843        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
844            AwsServiceError::aws_error(
845                StatusCode::INTERNAL_SERVER_ERROR,
846                "KMSInternalException",
847                "Key state became inconsistent",
848            )
849        })?;
850
851        fakecloud_core::tags::remove_tags(&mut key.tags, &body, "TagKeys").map_err(|f| {
852            AwsServiceError::aws_error(
853                StatusCode::BAD_REQUEST,
854                "ValidationException",
855                format!("{f} must be a list"),
856            )
857        })?;
858
859        Ok(AwsResponse::json(StatusCode::OK, "{}"))
860    }
861
862    fn list_resource_tags(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
863        let body = req.json_body();
864        let key_id = Self::require_key_id(&body)?;
865
866        let resolved = self
867            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
868            .ok_or_else(|| {
869                AwsServiceError::aws_error(
870                    StatusCode::BAD_REQUEST,
871                    "NotFoundException",
872                    format!("Invalid keyId {key_id}"),
873                )
874            })?;
875
876        let accounts = self.state.read();
877        let empty = KmsState::new(&req.account_id, &req.region);
878        let state = accounts.get(&req.account_id).unwrap_or(&empty);
879        let key = state.keys.get(&resolved).ok_or_else(|| {
880            AwsServiceError::aws_error(
881                StatusCode::INTERNAL_SERVER_ERROR,
882                "KMSInternalException",
883                "Key state became inconsistent",
884            )
885        })?;
886        let tags = fakecloud_core::tags::tags_to_json(&key.tags, "TagKey", "TagValue");
887
888        Ok(AwsResponse::json(
889            StatusCode::OK,
890            serde_json::to_string(&json!({
891                "Tags": tags,
892                "Truncated": false,
893            }))
894            .unwrap(),
895        ))
896    }
897
898    fn update_key_description(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
899        let body = req.json_body();
900        let resolved = self.resolve_required_key(req, &body)?;
901        let description = body["Description"].as_str().unwrap_or("").to_string();
902
903        let mut accounts = self.state.write();
904        let state = accounts.get_or_create(&req.account_id);
905        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
906            AwsServiceError::aws_error(
907                StatusCode::INTERNAL_SERVER_ERROR,
908                "KMSInternalException",
909                "Key state became inconsistent",
910            )
911        })?;
912        key.description = description;
913
914        Ok(AwsResponse::json(StatusCode::OK, "{}"))
915    }
916
917    fn get_key_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
918        let body = req.json_body();
919        let key_id = Self::require_key_id(&body)?;
920
921        // For key policy operations, aliases should not work
922        if key_id.starts_with("alias/") {
923            return Err(AwsServiceError::aws_error(
924                StatusCode::BAD_REQUEST,
925                "NotFoundException",
926                format!("Invalid keyId {key_id}"),
927            ));
928        }
929
930        let resolved = self
931            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
932            .ok_or_else(|| {
933                AwsServiceError::aws_error(
934                    StatusCode::BAD_REQUEST,
935                    "NotFoundException",
936                    format!("Key '{key_id}' does not exist"),
937                )
938            })?;
939
940        let accounts = self.state.read();
941        let empty = KmsState::new(&req.account_id, &req.region);
942        let state = accounts.get(&req.account_id).unwrap_or(&empty);
943        let key = state.keys.get(&resolved).ok_or_else(|| {
944            AwsServiceError::aws_error(
945                StatusCode::INTERNAL_SERVER_ERROR,
946                "KMSInternalException",
947                "Key state became inconsistent",
948            )
949        })?;
950
951        Ok(AwsResponse::json(
952            StatusCode::OK,
953            serde_json::to_string(&json!({
954                "Policy": key.policy,
955            }))
956            .unwrap(),
957        ))
958    }
959
960    fn put_key_policy(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
961        let body = req.json_body();
962        let key_id = Self::require_key_id(&body)?;
963
964        // For key policy operations, aliases should not work
965        if key_id.starts_with("alias/") {
966            return Err(AwsServiceError::aws_error(
967                StatusCode::BAD_REQUEST,
968                "NotFoundException",
969                format!("Invalid keyId {key_id}"),
970            ));
971        }
972
973        let resolved = self
974            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
975            .ok_or_else(|| {
976                AwsServiceError::aws_error(
977                    StatusCode::BAD_REQUEST,
978                    "NotFoundException",
979                    format!("Key '{key_id}' does not exist"),
980                )
981            })?;
982
983        let policy = body["Policy"].as_str().unwrap_or("").to_string();
984
985        let mut accounts = self.state.write();
986        let state = accounts.get_or_create(&req.account_id);
987        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
988            AwsServiceError::aws_error(
989                StatusCode::INTERNAL_SERVER_ERROR,
990                "KMSInternalException",
991                "Key state became inconsistent",
992            )
993        })?;
994        key.policy = policy;
995
996        Ok(AwsResponse::json(StatusCode::OK, "{}"))
997    }
998
999    fn list_key_policies(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1000        let body = req.json_body();
1001        let _resolved = self.resolve_required_key(req, &body)?;
1002
1003        Ok(AwsResponse::json(
1004            StatusCode::OK,
1005            serde_json::to_string(&json!({
1006                "PolicyNames": ["default"],
1007                "Truncated": false,
1008            }))
1009            .unwrap(),
1010        ))
1011    }
1012
1013    fn get_key_rotation_status(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1014        let body = req.json_body();
1015        let key_id = Self::require_key_id(&body)?;
1016
1017        // Real KMS resolves alias/* and alias-ARNs identically to a key id.
1018        let resolved = self
1019            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1020            .ok_or_else(|| {
1021                AwsServiceError::aws_error(
1022                    StatusCode::BAD_REQUEST,
1023                    "NotFoundException",
1024                    format!("Key '{key_id}' does not exist"),
1025                )
1026            })?;
1027
1028        let accounts = self.state.read();
1029        let empty = KmsState::new(&req.account_id, &req.region);
1030        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1031        let key = state.keys.get(&resolved).ok_or_else(|| {
1032            AwsServiceError::aws_error(
1033                StatusCode::INTERNAL_SERVER_ERROR,
1034                "KMSInternalException",
1035                "Key state became inconsistent",
1036            )
1037        })?;
1038
1039        Ok(AwsResponse::json(
1040            StatusCode::OK,
1041            serde_json::to_string(&json!({
1042                "KeyRotationEnabled": key.key_rotation_enabled,
1043            }))
1044            .unwrap(),
1045        ))
1046    }
1047
1048    fn enable_key_rotation(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1049        let body = req.json_body();
1050        let key_id = Self::require_key_id(&body)?;
1051
1052        // Real KMS resolves alias/* and alias-ARNs identically to a key id
1053        // here. Earlier code rejected `alias/*` outright, breaking IaC
1054        // configs that reference keys by alias.
1055        let resolved = self
1056            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1057            .ok_or_else(|| {
1058                AwsServiceError::aws_error(
1059                    StatusCode::BAD_REQUEST,
1060                    "NotFoundException",
1061                    format!("Key '{key_id}' does not exist"),
1062                )
1063            })?;
1064
1065        let mut accounts = self.state.write();
1066        let state = accounts.get_or_create(&req.account_id);
1067        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
1068            AwsServiceError::aws_error(
1069                StatusCode::INTERNAL_SERVER_ERROR,
1070                "KMSInternalException",
1071                "Key state became inconsistent",
1072            )
1073        })?;
1074        key.key_rotation_enabled = true;
1075
1076        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1077    }
1078
1079    fn disable_key_rotation(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1080        let body = req.json_body();
1081        let key_id = Self::require_key_id(&body)?;
1082
1083        // Real KMS resolves alias/* and alias-ARNs identically to a key id.
1084        let resolved = self
1085            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1086            .ok_or_else(|| {
1087                AwsServiceError::aws_error(
1088                    StatusCode::BAD_REQUEST,
1089                    "NotFoundException",
1090                    format!("Key '{key_id}' does not exist"),
1091                )
1092            })?;
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.key_rotation_enabled = false;
1104
1105        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1106    }
1107
1108    fn rotate_key_on_demand(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1109        let body = req.json_body();
1110        let resolved = self.resolve_required_key(req, &body)?;
1111
1112        let mut accounts = self.state.write();
1113        let state = accounts.get_or_create(&req.account_id);
1114        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
1115            AwsServiceError::aws_error(
1116                StatusCode::INTERNAL_SERVER_ERROR,
1117                "KMSInternalException",
1118                "Key state became inconsistent",
1119            )
1120        })?;
1121
1122        let rotation = KeyRotation {
1123            key_id: key.key_id.clone(),
1124            rotation_date: Utc::now().timestamp() as f64,
1125            rotation_type: "ON_DEMAND".to_string(),
1126        };
1127        key.rotations.push(rotation);
1128
1129        Ok(AwsResponse::json(
1130            StatusCode::OK,
1131            serde_json::to_string(&json!({
1132                "KeyId": key.key_id,
1133            }))
1134            .unwrap(),
1135        ))
1136    }
1137
1138    fn list_key_rotations(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1139        let body = req.json_body();
1140        let resolved = self.resolve_required_key(req, &body)?;
1141        validate_optional_json_range("limit", &body["Limit"], 1, 1000)?;
1142        let limit = body["Limit"].as_i64().unwrap_or(1000) as usize;
1143        let marker = body["Marker"].as_str();
1144
1145        let accounts = self.state.read();
1146        let empty = KmsState::new(&req.account_id, &req.region);
1147        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1148        let key = state.keys.get(&resolved).ok_or_else(|| {
1149            AwsServiceError::aws_error(
1150                StatusCode::INTERNAL_SERVER_ERROR,
1151                "KMSInternalException",
1152                "Key state became inconsistent",
1153            )
1154        })?;
1155
1156        let start_index = if let Some(marker) = marker {
1157            marker.parse::<usize>().unwrap_or(0)
1158        } else {
1159            0
1160        };
1161
1162        let rotations: Vec<Value> = key
1163            .rotations
1164            .iter()
1165            .skip(start_index)
1166            .take(limit)
1167            .map(|r| {
1168                json!({
1169                    "KeyId": r.key_id,
1170                    "RotationDate": r.rotation_date,
1171                    "RotationType": r.rotation_type,
1172                })
1173            })
1174            .collect();
1175
1176        let total_after_start = key.rotations.len().saturating_sub(start_index);
1177        let truncated = total_after_start > limit;
1178
1179        let mut response = json!({
1180            "Rotations": rotations,
1181            "Truncated": truncated,
1182        });
1183
1184        if truncated {
1185            response["NextMarker"] = json!((start_index + limit).to_string());
1186        }
1187
1188        Ok(AwsResponse::json(
1189            StatusCode::OK,
1190            serde_json::to_string(&response).unwrap(),
1191        ))
1192    }
1193
1194    fn replicate_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1195        let body = req.json_body();
1196        let key_id = Self::require_key_id(&body)?;
1197        let replica_region = body["ReplicaRegion"].as_str().unwrap_or("").to_string();
1198
1199        let resolved = self
1200            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1201            .ok_or_else(|| {
1202                AwsServiceError::aws_error(
1203                    StatusCode::BAD_REQUEST,
1204                    "NotFoundException",
1205                    format!("Key '{key_id}' does not exist"),
1206                )
1207            })?;
1208
1209        let mut accounts = self.state.write();
1210        let state = accounts.get_or_create(&req.account_id);
1211
1212        // Clone the source key once and drop the borrow — the replica reuses
1213        // every field except the region-dependent ones.
1214        let source_key = state
1215            .keys
1216            .get(&resolved)
1217            .ok_or_else(|| {
1218                AwsServiceError::aws_error(
1219                    StatusCode::INTERNAL_SERVER_ERROR,
1220                    "KMSInternalException",
1221                    "Key state became inconsistent",
1222                )
1223            })?
1224            .clone();
1225        let account_id = state.account_id.clone();
1226        let source_region = state.region.clone();
1227
1228        let replica_arn = format!(
1229            "arn:aws:kms:{}:{}:key/{}",
1230            replica_region, account_id, source_key.key_id
1231        );
1232
1233        let metadata = json!({
1234            "KeyId": source_key.key_id,
1235            "Arn": replica_arn,
1236            "AWSAccountId": account_id,
1237            "CreationDate": source_key.creation_date,
1238            "Description": source_key.description,
1239            "Enabled": source_key.enabled,
1240            "KeyUsage": source_key.key_usage,
1241            "KeySpec": source_key.key_spec,
1242            "CustomerMasterKeySpec": source_key.key_spec,
1243            "KeyManager": source_key.key_manager,
1244            "KeyState": source_key.key_state,
1245            "Origin": source_key.origin,
1246            "MultiRegion": true,
1247            "MultiRegionConfiguration": {
1248                "MultiRegionKeyType": "REPLICA",
1249                "PrimaryKey": {
1250                    "Arn": source_key.arn,
1251                    "Region": source_region,
1252                },
1253                "ReplicaKeys": [],
1254            },
1255        });
1256
1257        let replica_storage_key = format!("{}:{}", replica_region, source_key.key_id);
1258        let source_policy = source_key.policy.clone();
1259        let replica_key = KmsKey {
1260            arn: replica_arn,
1261            deletion_date: None,
1262            key_rotation_enabled: false,
1263            multi_region: true,
1264            rotations: Vec::new(),
1265            custom_key_store_id: None,
1266            imported_key_material: false,
1267            imported_material_bytes: None,
1268            private_key_seed: rand_bytes(32),
1269            primary_region: None,
1270            ..source_key
1271        };
1272
1273        state.keys.insert(replica_storage_key, replica_key);
1274
1275        Ok(AwsResponse::json(
1276            StatusCode::OK,
1277            serde_json::to_string(&json!({
1278                "ReplicaKeyMetadata": metadata,
1279                "ReplicaPolicy": source_policy,
1280            }))
1281            .unwrap(),
1282        ))
1283    }
1284
1285    fn update_primary_region(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1286        let body = req.json_body();
1287        let key_id = Self::require_key_id(&body)?;
1288        let primary_region = body["PrimaryRegion"]
1289            .as_str()
1290            .ok_or_else(|| {
1291                AwsServiceError::aws_error(
1292                    StatusCode::BAD_REQUEST,
1293                    "ValidationException",
1294                    "PrimaryRegion is required",
1295                )
1296            })?
1297            .to_string();
1298
1299        let resolved = self
1300            .resolve_key_id_for(&req.account_id, &req.region, &key_id)
1301            .ok_or_else(|| {
1302                AwsServiceError::aws_error(
1303                    StatusCode::BAD_REQUEST,
1304                    "NotFoundException",
1305                    format!("Key '{key_id}' does not exist"),
1306                )
1307            })?;
1308
1309        let mut accounts = self.state.write();
1310        let state = accounts.get_or_create(&req.account_id);
1311        let account_id = state.account_id.clone();
1312        let key = state.keys.get_mut(&resolved).ok_or_else(|| {
1313            AwsServiceError::aws_error(
1314                StatusCode::BAD_REQUEST,
1315                "NotFoundException",
1316                format!("Key '{key_id}' does not exist"),
1317            )
1318        })?;
1319
1320        if !key.multi_region {
1321            return Err(AwsServiceError::aws_error(
1322                StatusCode::BAD_REQUEST,
1323                "UnsupportedOperationException",
1324                format!("Key '{}' is not a multi-Region key", key.arn),
1325            ));
1326        }
1327        key.primary_region = Some(primary_region.clone());
1328        // Update the ARN to reflect the new region
1329        key.arn = format!(
1330            "arn:aws:kms:{}:{}:key/{}",
1331            primary_region, account_id, key.key_id
1332        );
1333
1334        Ok(AwsResponse::json(StatusCode::OK, "{}"))
1335    }
1336}
1337
1338#[path = "asym.rs"]
1339pub(crate) mod asym;
1340#[path = "asym_ecdsa.rs"]
1341pub(crate) mod asym_ecdsa;
1342#[path = "mac.rs"]
1343pub(crate) mod mac;
1344#[path = "service_aliases.rs"]
1345mod service_aliases;
1346#[path = "service_crypto.rs"]
1347mod service_crypto;
1348#[path = "service_custom_store.rs"]
1349mod service_custom_store;
1350#[path = "service_grants.rs"]
1351mod service_grants;
1352
1353#[path = "helpers.rs"]
1354mod helpers;
1355pub(crate) use helpers::*;
1356
1357#[path = "provisioner.rs"]
1358pub mod provisioner;
1359
1360#[cfg(test)]
1361#[path = "service_tests.rs"]
1362mod tests;