1use anyhow::{ensure, Context, Result};
3use serde::{Deserialize, Serialize};
4use serde_json::{json, Value};
5use std::collections::BTreeMap;
6pub const MAX_BODY: usize = 1024 * 1024;
7pub const MAX_ITEMS: usize = 10000;
8pub const MAX_TEXT: usize = 16 * 1024;
9pub const MAX_TOKEN: usize = 8192;
10pub const MAX_DEPTH: usize = 32;
11pub const MAX_NODES: usize = 65536;
12pub const MAX_RETAINED_BYTES: usize = 8 * 1024 * 1024;
13pub struct Request {
14 pub operation: String,
15 pub method: &'static str,
16 pub path: String,
17 pub body: Vec<u8>,
18 pub redacted: Value,
19}
20pub fn token(value: &str) -> Result<hyper::header::HeaderValue> {
21 ensure!(
22 !value.is_empty()
23 && value.len() <= MAX_TOKEN
24 && value.bytes().all(|c| c.is_ascii_graphic()),
25 "Vault token must be bounded printable ASCII without whitespace"
26 );
27 let mut header: hyper::header::HeaderValue = value.parse()?;
28 header.set_sensitive(true);
29 Ok(header)
30}
31pub fn path(value: &str, empty: bool) -> Result<()> {
32 ensure!(
33 (empty || !value.is_empty()) && value.len() <= 4096,
34 "Vault path length/empty refusal"
35 );
36 if value.is_empty() {
37 return Ok(());
38 }
39 ensure!(
40 value.split('/').count() <= 32
41 && value.split('/').all(|s| !s.is_empty()
42 && s.len() <= 256
43 && s != "."
44 && s != ".."
45 && s.bytes()
46 .all(|c| c.is_ascii_alphanumeric() || b"_-.".contains(&c))),
47 "Vault path requires bounded ASCII name segments without dot/empty segments"
48 );
49 Ok(())
50}
51pub fn mount(value: &str) -> Result<()> {
52 path(value, false)?;
53 ensure!(
54 value.len() <= 256 && value.split('/').next() != Some("sys"),
55 "invalid Vault mount"
56 );
57 Ok(())
58}
59pub fn action_within_budget(action: &Value) -> bool {
60 crate::utils::json_budget::within_budget(action, MAX_RETAINED_BYTES, MAX_NODES, MAX_DEPTH)
61}
62pub fn request(action: &Value, default_mount: &str, auth_mount: &str) -> Result<Request> {
63 ensure!(
64 action_within_budget(action),
65 "Vault action depth/node/retained-content limit"
66 );
67 let kind = action["type"]
68 .as_str()
69 .context("Vault action type required")?;
70 let operation = match kind {
71 "vault_request" => action["operation"]
72 .as_str()
73 .context("Vault operation required")?,
74 "vault_userpass_login" => "login",
75 "vault_clear_token" => "clear_token",
76 _ => anyhow::bail!("unknown Vault action"),
77 };
78 let fields: Vec<&str> = match operation {
79 "seal_status" | "health" | "leader" => vec!["type", "operation"],
80 "read" => vec!["type", "operation", "mount", "path", "version"],
81 "write" => vec!["type", "operation", "mount", "path", "data", "cas"],
82 "list" | "metadata" => vec!["type", "operation", "mount", "path"],
83 "login" => vec!["type", "username", "password", "auth_mount"],
84 "clear_token" => vec!["type"],
85 _ => anyhow::bail!("unsupported selected Vault operation"),
86 };
87 ensure!(
88 action
89 .as_object()
90 .context("Vault action must be object")?
91 .keys()
92 .all(|k| fields.contains(&k.as_str())),
93 "unsupported field for selected Vault operation"
94 );
95 let mut redacted = action.clone();
96 let mut body = Vec::new();
97 let (method, route) = match operation {
98 "seal_status" => ("GET", "/v1/sys/seal-status".into()),
99 "health" => ("GET", "/v1/sys/health".into()),
100 "leader" => ("GET", "/v1/sys/leader".into()),
101 "clear_token" => ("", "".into()),
102 "login" => {
103 let auth_mount = match action.get("auth_mount") {
104 None => auth_mount,
105 Some(v) => v.as_str().context("auth_mount must be string")?,
106 };
107 mount(auth_mount)?;
108 let username = action["username"].as_str().context("username required")?;
109 ensure!(
110 !username.is_empty()
111 && username.len() <= 256
112 && !username.starts_with(['-', '.'])
113 && !username.ends_with('.')
114 && username
115 .bytes()
116 .all(|c| c.is_ascii_alphanumeric() || b"_-.".contains(&c)),
117 "invalid userpass username"
118 );
119 let password = action["password"].as_str().context("password required")?;
120 ensure!(
121 !password.is_empty() && password.len() <= MAX_TEXT,
122 "password length refusal"
123 );
124 redacted.as_object_mut().unwrap().remove("password");
125 redacted["password_present"] = json!(true);
126 body = serde_json::to_vec(&json!({"password":password}))?;
127 ("POST", format!("/v1/auth/{auth_mount}/login/{username}"))
128 }
129 _ => {
130 let mount_name = match action.get("mount") {
131 None => default_mount,
132 Some(v) => v.as_str().context("mount must be string")?,
133 };
134 mount(mount_name)?;
135 let secret_path = action["path"]
136 .as_str()
137 .context("path required (empty allowed for list)")?;
138 let validation_path = if operation == "list" {
139 ensure!(secret_path != "/", "mount-root list requires empty path");
140 secret_path.strip_suffix('/').unwrap_or(secret_path)
141 } else {
142 secret_path
143 };
144 path(validation_path, operation == "list")?;
145 let (method, kind) = if operation == "write" {
146 ("PUT", "data")
147 } else if ["list", "metadata"].contains(&operation) {
148 ("GET", "metadata")
149 } else {
150 ("GET", "data")
151 };
152 let mut route = format!("/v1/{mount_name}/{kind}/{secret_path}");
153 if operation == "read" {
154 if let Some(version) = action.get("version") {
155 let version = version
156 .as_u64()
157 .context("version must be nonnegative integer;0 means latest")?;
158 route.push_str(&format!("?version={version}"));
159 }
160 }
161 if operation == "list" {
162 route.push_str("?list=true");
163 }
164 if operation == "write" {
165 let data = action["data"]
166 .as_object()
167 .context("KV write data must be object")?;
168 let mut envelope = json!({"data":data});
169 if let Some(cas) = action.get("cas") {
170 let cas = cas
171 .as_u64()
172 .context("cas must be nonnegative integer;0 means create-only")?;
173 envelope["options"] = json!({"cas":cas});
174 }
175 body = serde_json::to_vec(&envelope)?;
176 ensure!(body.len() <= MAX_BODY, "Vault write body limit");
177 json(&body)?;
178 }
179 (method, route)
180 }
181 };
182 Ok(Request {
183 operation: operation.into(),
184 method,
185 path: route,
186 body,
187 redacted,
188 })
189}
190#[derive(Deserialize, Serialize)]
191pub struct SealStatus {
192 #[serde(rename(deserialize = "type"))]
193 pub seal_type: String,
194 pub initialized: bool,
195 pub sealed: bool,
196 pub t: u64,
197 pub n: u64,
198 pub progress: u64,
199 pub nonce: String,
200 pub version: String,
201 pub cluster_name: Option<String>,
202 pub cluster_id: Option<String>,
203 pub migration: Option<bool>,
204 pub recovery_seal: Option<bool>,
205 pub storage_type: Option<String>,
206}
207#[derive(Deserialize, Serialize)]
208pub struct Health {
209 pub initialized: bool,
210 pub sealed: bool,
211 pub standby: bool,
212 pub performance_standby: Option<bool>,
213 pub replication_performance_mode: Option<String>,
214 pub replication_dr_mode: Option<String>,
215 pub server_time_utc: i64,
216 pub version: String,
217 pub enterprise: Option<bool>,
218 pub cluster_name: Option<String>,
219 pub cluster_id: Option<String>,
220}
221#[derive(Deserialize, Serialize)]
222pub struct Leader {
223 pub ha_enabled: bool,
224 pub is_self: bool,
225 pub leader_address: Option<String>,
226 pub leader_cluster_address: Option<String>,
227 pub performance_standby: Option<bool>,
228 pub active_time: Option<String>,
229}
230#[derive(Deserialize)]
231struct Envelope {
232 request_id: String,
233 lease_id: String,
234 lease_duration: u64,
235 renewable: bool,
236 data: Value,
237 auth: Option<Value>,
238 wrap_info: Option<Value>,
239 warnings: Option<Vec<String>>,
240}
241#[derive(Deserialize, Serialize)]
242struct VersionMetadata {
243 created_time: String,
244 deletion_time: String,
245 destroyed: bool,
246 version: u64,
247 custom_metadata: Option<BTreeMap<String, String>>,
248}
249#[derive(Deserialize, Serialize)]
250struct Read {
251 data: BTreeMap<String, Value>,
252 metadata: VersionMetadata,
253}
254#[derive(Deserialize, Serialize)]
255struct CreatedBy {
256 actor: Option<String>,
257 operation: Option<String>,
258 entity_id: Option<String>,
259 client_id: Option<String>,
260}
261#[derive(Deserialize, Serialize)]
262struct VersionEntry {
263 created_time: String,
264 deletion_time: String,
265 destroyed: bool,
266 created_by: Option<CreatedBy>,
267}
268#[derive(Deserialize, Serialize)]
269struct Metadata {
270 created_time: String,
271 updated_time: String,
272 current_version: u64,
273 oldest_version: u64,
274 max_versions: u64,
275 cas_required: bool,
276 delete_version_after: String,
277 custom_metadata: Option<BTreeMap<String, String>>,
278 versions: BTreeMap<String, VersionEntry>,
279}
280#[derive(Deserialize, Serialize)]
281struct Keys {
282 keys: Vec<String>,
283}
284#[derive(Deserialize, Serialize)]
285struct Authentication {
286 #[serde(skip_serializing)]
287 client_token: String,
288 accessor: Option<String>,
289 policies: Vec<String>,
290 token_policies: Option<Vec<String>>,
291 metadata: Option<BTreeMap<String, String>>,
292 lease_duration: u64,
293 renewable: bool,
294 entity_id: Option<String>,
295 token_type: String,
296 orphan: Option<bool>,
297 num_uses: Option<u64>,
298 #[serde(skip_serializing)]
299 mfa_requirement: Option<Value>,
300}
301fn typed<T: serde::de::DeserializeOwned + Serialize>(v: Value) -> Result<Value> {
302 Ok(serde_json::to_value(
303 serde_json::from_value::<T>(v).context("invalid Vault response schema")?,
304 )?)
305}
306fn timestamp(value: &str, empty: bool) -> Result<()> {
307 if empty && value.is_empty() {
308 return Ok(());
309 }
310 chrono::DateTime::parse_from_rfc3339(value).context("invalid Vault RFC3339 timestamp")?;
311 Ok(())
312}
313fn version(v: &Value) -> Result<()> {
314 ensure!(
315 v["version"].as_u64().context("version missing")? > 0,
316 "secret version must be positive"
317 );
318 timestamp(v["created_time"].as_str().unwrap(), false)?;
319 timestamp(v["deletion_time"].as_str().unwrap(), true)?;
320 Ok(())
321}
322pub fn parse(operation: &str, value: Value) -> Result<(Value, Option<hyper::header::HeaderValue>)> {
323 match operation {
324 "seal_status" => {
325 let v = typed::<SealStatus>(value)?;
326 ensure!(
327 v["t"].as_u64().unwrap() <= v["n"].as_u64().unwrap()
328 && v["progress"].as_u64().unwrap() <= v["n"].as_u64().unwrap(),
329 "invalid seal threshold/progress"
330 );
331 Ok((v, None))
332 }
333 "health" => Ok((typed::<Health>(value)?, None)),
334 "leader" => Ok((typed::<Leader>(value)?, None)),
335 _ => {
336 let e: Envelope = serde_json::from_value(value).context("invalid Vault envelope")?;
337 ensure!(
338 e.wrap_info.is_none(),
339 "response wrapping is not implemented"
340 );
341 ensure!(
342 e.lease_id.is_empty() && e.lease_duration == 0 && !e.renewable,
343 "selected Vault envelope must not contain a dynamic lease"
344 );
345 let (data, credential) = if operation == "login" {
346 ensure!(
347 e.data.is_null(),
348 "userpass login unexpectedly returned secret data"
349 );
350 let auth: Authentication =
351 serde_json::from_value(e.auth.context("missing login auth")?)
352 .context("invalid userpass auth response")?;
353 ensure!(
354 auth.mfa_requirement.is_none(),
355 "userpass MFA continuation is not implemented"
356 );
357 ensure!(
358 ["service", "batch"].contains(&auth.token_type.as_str()),
359 "unsupported Vault token type"
360 );
361 let credential = token(&auth.client_token)?;
362 (serde_json::to_value(auth)?, Some(credential))
363 } else {
364 ensure!(
365 e.auth.is_none(),
366 "KV v2 response must not contain authentication"
367 );
368 let data = match operation {
369 "read" => {
370 let v = typed::<Read>(e.data)?;
371 version(&v["metadata"])?;
372 ensure!(
373 v["metadata"]["destroyed"] == false
374 && v["metadata"]["deletion_time"] == "",
375 "unavailable secret version returned HTTP success"
376 );
377 v
378 }
379 "write" => {
380 let v = typed::<VersionMetadata>(e.data)?;
381 version(&v)?;
382 ensure!(
383 v["destroyed"] == false && v["deletion_time"] == "",
384 "write acknowledgement names unavailable version"
385 );
386 v
387 }
388 "list" => {
389 let v = typed::<Keys>(e.data)?;
390 for key in v["keys"].as_array().unwrap() {
391 let key = key.as_str().unwrap();
392 let name = key.strip_suffix('/').unwrap_or(key);
393 ensure!(
394 !name.is_empty()
395 && name.len() <= 256
396 && !name.contains('/')
397 && !crate::utils::sanitize::has_controls(&name),
398 "invalid Vault list key"
399 );
400 }
401 v
402 }
403 "metadata" => {
404 let v = typed::<Metadata>(e.data)?;
405 timestamp(v["created_time"].as_str().unwrap(), false)?;
406 timestamp(v["updated_time"].as_str().unwrap(), false)?;
407 let current = v["current_version"].as_u64().unwrap();
408 ensure!(
409 v["oldest_version"].as_u64().unwrap() <= current,
410 "invalid oldest/current version"
411 );
412 for (key, entry) in v["versions"].as_object().unwrap() {
413 let n = key.parse::<u64>().context("invalid metadata version key")?;
414 ensure!(
415 n > 0 && n <= current && n.to_string() == *key,
416 "invalid metadata version range/key"
417 );
418 timestamp(entry["created_time"].as_str().unwrap(), false)?;
419 timestamp(entry["deletion_time"].as_str().unwrap(), true)?;
420 }
421 v
422 }
423 _ => anyhow::bail!("unsupported Vault response operation"),
424 };
425 (data, None)
426 };
427 Ok((
428 json!({"request_id":e.request_id,"lease_id":e.lease_id,"lease_duration":e.lease_duration,"renewable":e.renewable,"warnings":e.warnings,"data":data}),
429 credential,
430 ))
431 }
432 }
433}
434pub fn errors(v: Value) -> Result<Vec<String>> {
435 #[derive(Deserialize)]
436 struct Errors {
437 errors: Vec<String>,
438 }
439 Ok(serde_json::from_value::<Errors>(v)
440 .context("invalid Vault error schema")?
441 .errors)
442}
443struct SecretRedactor {
444 forms: Vec<String>,
445}
446impl SecretRedactor {
447 fn new(secrets: &[String]) -> Self {
448 let mut forms = Vec::new();
449 for secret in secrets.iter().filter(|s| !s.is_empty()) {
450 let json = serde_json::to_string(secret).unwrap();
453 let debug = format!("{secret:?}");
454 forms.extend([
455 secret.clone(),
456 json[1..json.len() - 1].to_owned(),
457 debug[1..debug.len() - 1].to_owned(),
458 ]);
459 }
460 forms.sort_unstable();
461 forms.dedup();
462 Self { forms }
463 }
464 fn text(&self, text: &str) -> String {
465 if self.forms.iter().any(|form| text.contains(form)) {
469 crate::utils::redact::REDACTED.to_owned()
470 } else {
471 text.to_owned()
472 }
473 }
474 fn value(&self, value: &mut Value, redact_keys: bool, depth: usize) {
475 if depth > MAX_DEPTH {
476 let omitted = std::mem::replace(
479 value,
480 Value::String(crate::utils::redact::REDACTED.to_owned()),
481 );
482 crate::utils::json_budget::drop_iteratively(omitted);
483 return;
484 }
485 match value {
486 Value::String(s) => *s = self.text(s),
487 Value::Array(a) => {
488 for v in a {
489 self.value(v, redact_keys, depth + 1)
490 }
491 }
492 Value::Object(o) => {
493 let original = std::mem::take(o);
495 for (key, mut value) in original {
496 self.value(&mut value, redact_keys, depth + 1);
497 o.insert(if redact_keys { self.text(&key) } else { key }, value);
498 }
499 }
500 _ => {}
501 }
502 }
503}
504pub fn redact_text(text: &str, secrets: &[String]) -> String {
505 SecretRedactor::new(secrets).text(text)
506}
507pub fn redact_errors(errors: &[String], secrets: &[String]) -> Vec<String> {
508 let redactor = SecretRedactor::new(secrets);
509 errors.iter().map(|error| redactor.text(error)).collect()
510}
511fn redact_schema(value: &mut Value, secrets: &[String], free_form_paths: &[&str]) {
512 let redactor = SecretRedactor::new(secrets);
513 redactor.value(value, false, 0);
516 for path in free_form_paths {
517 if let Some(value) = value.pointer_mut(path) {
518 redactor.value(value, true, path.matches('/').count());
519 }
520 }
521}
522pub fn redact_request(value: &mut Value, secrets: &[String]) {
523 redact_schema(value, secrets, &["/data"])
524}
525pub fn redact_response(value: &mut Value, operation: &str, secrets: &[String]) {
526 let paths: &[&str] = match operation {
527 "login" => &["/data/metadata"],
528 "read" => &["/data/data", "/data/metadata/custom_metadata"],
529 "metadata" => &["/data/custom_metadata"],
530 _ => &[],
531 };
532 redact_schema(value, secrets, paths)
533}
534
535struct Seed<'a> {
536 depth: usize,
537 nodes: &'a mut usize,
538}
539impl<'de> serde::de::DeserializeSeed<'de> for Seed<'_> {
540 type Value = Value;
541 fn deserialize<D: serde::Deserializer<'de>>(
542 self,
543 d: D,
544 ) -> std::result::Result<Value, D::Error> {
545 use serde::de::Error;
546 *self.nodes += 1;
547 if self.depth > MAX_DEPTH || *self.nodes > MAX_NODES {
548 return Err(D::Error::custom("Vault JSON nesting/node limit"));
549 }
550 d.deserialize_any(self)
551 }
552}
553impl<'de> serde::de::Visitor<'de> for Seed<'_> {
554 type Value = Value;
555 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
556 f.write_str("bounded Vault JSON")
557 }
558 fn visit_bool<E: serde::de::Error>(self, v: bool) -> std::result::Result<Value, E> {
559 Ok(Value::Bool(v))
560 }
561 fn visit_i64<E: serde::de::Error>(self, v: i64) -> std::result::Result<Value, E> {
562 Ok(v.into())
563 }
564 fn visit_u64<E: serde::de::Error>(self, v: u64) -> std::result::Result<Value, E> {
565 Ok(v.into())
566 }
567 fn visit_f64<E: serde::de::Error>(self, v: f64) -> std::result::Result<Value, E> {
568 serde_json::Number::from_f64(v)
569 .map(Value::Number)
570 .ok_or_else(|| E::custom("invalid JSON number"))
571 }
572 fn visit_unit<E: serde::de::Error>(self) -> std::result::Result<Value, E> {
573 Ok(Value::Null)
574 }
575 fn visit_str<E: serde::de::Error>(self, v: &str) -> std::result::Result<Value, E> {
576 if v.len() > MAX_TEXT {
577 return Err(E::custom("Vault text limit"));
578 }
579 Ok(Value::String(v.into()))
580 }
581 fn visit_string<E: serde::de::Error>(self, v: String) -> std::result::Result<Value, E> {
582 if v.len() > MAX_TEXT {
583 return Err(E::custom("Vault text limit"));
584 }
585 Ok(Value::String(v))
586 }
587 fn visit_seq<A: serde::de::SeqAccess<'de>>(
588 self,
589 mut a: A,
590 ) -> std::result::Result<Value, A::Error> {
591 use serde::de::Error;
592 let mut values = Vec::new();
593 while let Some(v) = a.next_element_seed(Seed {
594 depth: self.depth + 1,
595 nodes: self.nodes,
596 })? {
597 if values.len() >= MAX_ITEMS {
598 return Err(A::Error::custom("Vault array limit"));
599 }
600 values.push(v);
601 }
602 Ok(Value::Array(values))
603 }
604 fn visit_map<A: serde::de::MapAccess<'de>>(
605 self,
606 mut a: A,
607 ) -> std::result::Result<Value, A::Error> {
608 use serde::de::Error;
609 let mut values = serde_json::Map::new();
610 while let Some(key) = a.next_key::<String>()? {
611 if key.len() > 256 || values.len() >= 256 {
612 return Err(A::Error::custom("Vault object field/name limit"));
613 }
614 if values.contains_key(&key) {
615 return Err(A::Error::custom("duplicate Vault JSON field"));
616 }
617 let value = a.next_value_seed(Seed {
618 depth: self.depth + 1,
619 nodes: self.nodes,
620 })?;
621 values.insert(key, value);
622 }
623 Ok(Value::Object(values))
624 }
625}
626pub fn json(body: &[u8]) -> Result<Value> {
627 ensure!(body.len() <= MAX_BODY, "Vault body limit");
628 use serde::de::DeserializeSeed;
629 let mut decoder = serde_json::Deserializer::from_slice(body);
630 let value = Seed {
631 depth: 0,
632 nodes: &mut 0,
633 }
634 .deserialize(&mut decoder)
635 .context("invalid Vault JSON")?;
636 decoder.end().context("trailing Vault JSON content")?;
637 Ok(value)
638}