1use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use std::time::{Duration, Instant};
6use zeroize::{Zeroize, Zeroizing};
7
8use crate::{Error, RemotePath, Result};
9
10pub const KMS_DIAGNOSTIC_CONTENT_BYTES: usize = 4096;
12
13#[async_trait]
15pub trait KmsDiagnosticStore: Send + Sync {
16 async fn put_kms_diagnostic_object(
18 &self,
19 path: &RemotePath,
20 content: Zeroizing<Vec<u8>>,
21 key_id: &str,
22 ) -> Result<()>;
23
24 async fn get_kms_diagnostic_object(
26 &self,
27 path: &RemotePath,
28 max_bytes: usize,
29 ) -> Result<Zeroizing<Vec<u8>>>;
30
31 async fn delete_kms_diagnostic_object(&self, path: &RemotePath) -> Result<()>;
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum KmsRoundTripPhase {
38 Write,
39 Read,
40 Verify,
41 Cleanup,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum KmsRoundTripErrorClass {
47 Auth,
48 NotFound,
49 Network,
50 Conflict,
51 General,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct KmsRoundTripTimings {
57 pub write_ms: u64,
58 pub read_ms: u64,
59 pub cleanup_ms: u64,
60 pub total_ms: u64,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct KmsRoundTripReport {
66 pub bucket: String,
67 pub key_id: String,
68 pub passed: bool,
69 pub cleanup_passed: bool,
70 pub timings: KmsRoundTripTimings,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct KmsRoundTripError {
76 pub phase: KmsRoundTripPhase,
77 pub class: KmsRoundTripErrorClass,
78 pub cleanup_failed: bool,
79}
80
81impl KmsRoundTripError {
82 pub fn into_core_error(self) -> Error {
84 let message = self.to_string();
85 match self.class {
86 KmsRoundTripErrorClass::Auth => Error::Auth(message),
87 KmsRoundTripErrorClass::NotFound => Error::NotFound(message),
88 KmsRoundTripErrorClass::Network => Error::Network(message),
89 KmsRoundTripErrorClass::Conflict => Error::Conflict(message),
90 KmsRoundTripErrorClass::General => Error::General(message),
91 }
92 }
93
94 fn safe_message(self) -> &'static str {
95 match (self.phase, self.cleanup_failed) {
96 (KmsRoundTripPhase::Write, true) => {
97 "KMS round-trip SSE-KMS write failed; temporary object cleanup also failed"
98 }
99 (KmsRoundTripPhase::Read, true) => {
100 "KMS round-trip read/decrypt failed; temporary object cleanup also failed"
101 }
102 (KmsRoundTripPhase::Verify, true) => {
103 "KMS round-trip verification mismatched; temporary object cleanup also failed"
104 }
105 (KmsRoundTripPhase::Write, false) => "KMS round-trip SSE-KMS write failed",
106 (KmsRoundTripPhase::Read, false) => "KMS round-trip read/decrypt failed",
107 (KmsRoundTripPhase::Verify, false) => "KMS round-trip verification mismatched",
108 (KmsRoundTripPhase::Cleanup, _) => {
109 "KMS round-trip verification passed, but temporary object cleanup failed"
110 }
111 }
112 }
113}
114
115impl std::fmt::Display for KmsRoundTripError {
116 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 formatter.write_str(self.safe_message())
118 }
119}
120
121impl std::error::Error for KmsRoundTripError {}
122
123pub async fn run_kms_round_trip(
125 store: &dyn KmsDiagnosticStore,
126 bucket: &str,
127 key_id: &str,
128) -> std::result::Result<KmsRoundTripReport, KmsRoundTripError> {
129 let mut content = Zeroizing::new(vec![0_u8; KMS_DIAGNOSTIC_CONTENT_BYTES]);
130 getrandom::fill(content.as_mut_slice()).map_err(|_| KmsRoundTripError {
131 phase: KmsRoundTripPhase::Write,
132 class: KmsRoundTripErrorClass::General,
133 cleanup_failed: false,
134 })?;
135
136 let mut object_id = [0_u8; 16];
137 getrandom::fill(&mut object_id).map_err(|_| KmsRoundTripError {
138 phase: KmsRoundTripPhase::Write,
139 class: KmsRoundTripErrorClass::General,
140 cleanup_failed: false,
141 })?;
142 let path = RemotePath::new(
143 "",
144 bucket,
145 format!(
146 ".rustfs-kms-diagnostic/{:032x}",
147 u128::from_be_bytes(object_id)
148 ),
149 );
150
151 let total_started = Instant::now();
152 let write_started = Instant::now();
153 let write_result = store
154 .put_kms_diagnostic_object(&path, Zeroizing::new(content.to_vec()), key_id)
155 .await;
156 let write_ms = duration_ms(write_started.elapsed());
157
158 let read_started = Instant::now();
159 let primary_failure = match write_result {
160 Err(error) => Some((KmsRoundTripPhase::Write, classify_and_discard_error(error))),
161 Ok(()) => match store
162 .get_kms_diagnostic_object(&path, KMS_DIAGNOSTIC_CONTENT_BYTES)
163 .await
164 {
165 Err(error) => Some((KmsRoundTripPhase::Read, classify_and_discard_error(error))),
166 Ok(actual) if actual.as_slice() != content.as_slice() => {
167 Some((KmsRoundTripPhase::Verify, KmsRoundTripErrorClass::General))
168 }
169 Ok(_) => None,
170 },
171 };
172 let read_ms = duration_ms(read_started.elapsed());
173
174 let cleanup_started = Instant::now();
175 let cleanup_failure = store
176 .delete_kms_diagnostic_object(&path)
177 .await
178 .err()
179 .map(classify_and_discard_error);
180 let cleanup_ms = duration_ms(cleanup_started.elapsed());
181
182 match (primary_failure, cleanup_failure) {
183 (Some((phase, class)), cleanup) => Err(KmsRoundTripError {
184 phase,
185 class,
186 cleanup_failed: cleanup.is_some(),
187 }),
188 (None, Some(class)) => Err(KmsRoundTripError {
189 phase: KmsRoundTripPhase::Cleanup,
190 class,
191 cleanup_failed: true,
192 }),
193 (None, None) => Ok(KmsRoundTripReport {
194 bucket: bucket.to_string(),
195 key_id: key_id.to_string(),
196 passed: true,
197 cleanup_passed: true,
198 timings: KmsRoundTripTimings {
199 write_ms,
200 read_ms,
201 cleanup_ms,
202 total_ms: duration_ms(total_started.elapsed()),
203 },
204 }),
205 }
206}
207
208fn duration_ms(duration: Duration) -> u64 {
209 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
210}
211
212fn classify_and_discard_error(mut error: Error) -> KmsRoundTripErrorClass {
213 match &mut error {
214 Error::Auth(message) => {
215 message.zeroize();
216 KmsRoundTripErrorClass::Auth
217 }
218 Error::NotFound(message) | Error::AliasNotFound(message) => {
219 message.zeroize();
220 KmsRoundTripErrorClass::NotFound
221 }
222 Error::VersionNotFound { path, version_id } | Error::DeleteMarker { path, version_id } => {
223 path.zeroize();
224 version_id.zeroize();
225 KmsRoundTripErrorClass::NotFound
226 }
227 Error::Network(message) => {
228 message.zeroize();
229 KmsRoundTripErrorClass::Network
230 }
231 Error::Conflict(message) | Error::AliasExists(message) => {
232 message.zeroize();
233 KmsRoundTripErrorClass::Conflict
234 }
235 Error::GovernanceDenied { path, version_id } => {
236 path.zeroize();
237 version_id.zeroize();
238 KmsRoundTripErrorClass::Conflict
239 }
240 Error::Config(message)
241 | Error::InvalidPath(message)
242 | Error::UnsupportedFeature(message)
243 | Error::General(message)
244 | Error::RequestRejected(message)
245 | Error::Interrupted(message) => {
246 message.zeroize();
247 KmsRoundTripErrorClass::General
248 }
249 Error::Io(_)
250 | Error::TomlParse(_)
251 | Error::TomlSerialize(_)
252 | Error::Json(_)
253 | Error::InvalidUrl(_) => KmsRoundTripErrorClass::General,
254 }
255}