Skip to main content

galata_vault/
value.rs

1//! What the token client hands back. Plaintext sits in buffers zeroized on
2//! drop, and none of these types can print it: no `Display`, no `Serialize`,
3//! and a `Debug` that shows names, versions, formats and sizes only.
4//! Plaintext is reached through `expose()`, which returns exactly the bytes
5//! that were written.
6
7use crate::seal::ConfigFormat;
8use serde::de::DeserializeOwned;
9use zeroize::Zeroizing;
10
11use crate::error::{Error, code};
12
13/// A decrypted secret.
14pub struct SecretValue {
15    pub(crate) name: String,
16    pub(crate) version: u64,
17    pub(crate) value: Zeroizing<Vec<u8>>,
18}
19
20impl SecretValue {
21    /// The secret's name.
22    pub fn name(&self) -> &str {
23        &self.name
24    }
25
26    /// The version read.
27    pub fn version(&self) -> u64 {
28        self.version
29    }
30
31    /// The value's length in bytes.
32    pub fn len(&self) -> usize {
33        self.value.len()
34    }
35
36    /// Whether the value is empty.
37    pub fn is_empty(&self) -> bool {
38        self.value.is_empty()
39    }
40
41    /// The plaintext, exactly as written.
42    pub fn expose(&self) -> &[u8] {
43        &self.value
44    }
45}
46
47impl std::fmt::Debug for SecretValue {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("SecretValue")
50            .field("name", &self.name)
51            .field("version", &self.version)
52            .field("size", &self.value.len())
53            .finish()
54    }
55}
56
57/// Several secrets, fetched all-or-nothing.
58#[derive(Debug)]
59pub struct SecretSet {
60    pub(crate) values: Vec<SecretValue>,
61}
62
63impl SecretSet {
64    /// The secret named `name`, if the set holds it.
65    pub fn get(&self, name: &str) -> Option<&SecretValue> {
66        self.values.iter().find(|v| v.name == name)
67    }
68
69    /// Every secret in the set, sorted by name.
70    pub fn iter(&self) -> impl Iterator<Item = &SecretValue> {
71        self.values.iter()
72    }
73
74    /// How many secrets the set holds.
75    pub fn len(&self) -> usize {
76        self.values.len()
77    }
78
79    /// Whether the set is empty.
80    pub fn is_empty(&self) -> bool {
81        self.values.is_empty()
82    }
83}
84
85impl IntoIterator for SecretSet {
86    type Item = SecretValue;
87    type IntoIter = std::vec::IntoIter<SecretValue>;
88
89    fn into_iter(self) -> Self::IntoIter {
90        self.values.into_iter()
91    }
92}
93
94/// A decrypted config document.
95pub struct ConfigDocument {
96    pub(crate) name: String,
97    pub(crate) version: u64,
98    pub(crate) format: ConfigFormat,
99    pub(crate) body: Zeroizing<Vec<u8>>,
100}
101
102impl ConfigDocument {
103    /// The config's name.
104    pub fn name(&self) -> &str {
105        &self.name
106    }
107
108    /// The version read.
109    pub fn version(&self) -> u64 {
110        self.version
111    }
112
113    /// The declared format.
114    pub fn format(&self) -> ConfigFormat {
115        self.format
116    }
117
118    /// The body's length in bytes.
119    pub fn len(&self) -> usize {
120        self.body.len()
121    }
122
123    /// Whether the body is empty.
124    pub fn is_empty(&self) -> bool {
125        self.body.is_empty()
126    }
127
128    /// The body, exactly as written: no newline, whitespace or encoding
129    /// normalisation.
130    pub fn expose(&self) -> &[u8] {
131        &self.body
132    }
133
134    /// The body as text. Configs are checked for UTF-8 before they are
135    /// written, so this fails only for a document written some other way.
136    pub fn text(&self) -> Result<&str, Error> {
137        std::str::from_utf8(&self.body)
138            .map_err(|_| Error::local(code::NOT_TEXT, format!("config {} is not UTF-8", self.name)))
139    }
140
141    /// Parse a `toml` or `json` body into a caller type. `yaml` and `text`
142    /// are refused: parse those yourself from [`ConfigDocument::expose`].
143    pub fn deserialize<T: DeserializeOwned>(&self) -> Result<T, Error> {
144        match self.format {
145            ConfigFormat::Toml => toml::from_str::<T>(self.text()?).map_err(|e| {
146                let line = e.span().map_or(1, |s| {
147                    self.body[..s.start.min(self.body.len())]
148                        .iter()
149                        .filter(|b| **b == b'\n')
150                        .count()
151                        + 1
152                });
153                Error::local(
154                    code::INVALID_CONFIG,
155                    format!(
156                        "config {}, line {line}: the toml does not fit the type",
157                        self.name
158                    ),
159                )
160            }),
161            ConfigFormat::Json => serde_json::from_slice::<T>(&self.body).map_err(|e| {
162                Error::local(
163                    code::INVALID_CONFIG,
164                    format!(
165                        "config {}, line {}: the json does not fit the type",
166                        self.name,
167                        e.line()
168                    ),
169                )
170            }),
171            other => Err(Error::local(
172                code::UNSUPPORTED_FORMAT,
173                format!(
174                    "config {} is {other}; only toml and json deserialize, so parse it from expose()",
175                    self.name
176                ),
177            )),
178        }
179    }
180}
181
182impl std::fmt::Debug for ConfigDocument {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        f.debug_struct("ConfigDocument")
185            .field("name", &self.name)
186            .field("version", &self.version)
187            .field("format", &self.format)
188            .field("size", &self.body.len())
189            .finish()
190    }
191}
192
193/// What a write expects the record to be, checked by the server: a stale
194/// write fails with a conflict naming both versions, and is never retried.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196#[non_exhaustive]
197pub enum Expect {
198    /// Its latest version is this live one (`If-Match`).
199    Version(u64),
200    /// It does not exist, or was deleted (`If-None-Match: *`).
201    Absent,
202}
203
204/// A config document to write.
205pub struct NewConfig {
206    pub(crate) format: ConfigFormat,
207    pub(crate) body: Zeroizing<Vec<u8>>,
208    pub(crate) allow_literals: bool,
209    pub(crate) expect: Option<Expect>,
210}
211
212impl NewConfig {
213    /// A document of `format`.
214    pub fn new(format: ConfigFormat, body: impl Into<Vec<u8>>) -> NewConfig {
215        NewConfig {
216            format,
217            body: Zeroizing::new(body.into()),
218            allow_literals: false,
219            expect: None,
220        }
221    }
222
223    /// A `toml` document.
224    pub fn toml(body: impl Into<Vec<u8>>) -> NewConfig {
225        NewConfig::new(ConfigFormat::Toml, body)
226    }
227
228    /// A `json` document.
229    pub fn json(body: impl Into<Vec<u8>>) -> NewConfig {
230        NewConfig::new(ConfigFormat::Json, body)
231    }
232
233    /// A `yaml` document (checked for UTF-8 only).
234    pub fn yaml(body: impl Into<Vec<u8>>) -> NewConfig {
235        NewConfig::new(ConfigFormat::Yaml, body)
236    }
237
238    /// A `text` document (checked for UTF-8 only).
239    pub fn text(body: impl Into<Vec<u8>>) -> NewConfig {
240        NewConfig::new(ConfigFormat::Text, body)
241    }
242
243    /// Write this one document even if it holds a credential literal.
244    pub fn allow_literals(mut self) -> NewConfig {
245        self.allow_literals = true;
246        self
247    }
248
249    /// Succeed only if the config is still at `version`, the one the caller
250    /// read. A stale edit fails with a conflict naming both versions, and is
251    /// never retried.
252    pub fn expect_version(mut self, version: u64) -> NewConfig {
253        self.expect = Some(Expect::Version(version));
254        self
255    }
256
257    /// Succeed only if the config does not exist yet (or was deleted).
258    pub fn expect_absent(mut self) -> NewConfig {
259        self.expect = Some(Expect::Absent);
260        self
261    }
262}
263
264impl std::fmt::Debug for NewConfig {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        f.debug_struct("NewConfig")
267            .field("format", &self.format)
268            .field("size", &self.body.len())
269            .field("allow_literals", &self.allow_literals)
270            .field("expect", &self.expect)
271            .finish()
272    }
273}
274
275/// A record's latest version, as listed. Never its value.
276#[derive(Debug, Clone, PartialEq, Eq)]
277#[non_exhaustive]
278pub struct Entry {
279    /// The record's name.
280    pub name: String,
281    /// Its latest version.
282    pub version: u64,
283    /// When the latest version was written (Unix seconds, as its writer
284    /// signed it).
285    pub updated_at: i64,
286    /// The latest version's ciphertext size in bytes.
287    pub size: u32,
288    /// Whether the latest version is a tombstone. Only the `_all` listings
289    /// return deleted records.
290    pub deleted: bool,
291}
292
293/// When things expire, as data: printing a warning is the caller's choice.
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295#[non_exhaustive]
296pub struct Expiry {
297    /// When the token expires; `None` for an owner-opened handle.
298    pub token_expires_at: Option<i64>,
299    /// The latest the server announced; `None` before any response.
300    pub vault_expires_at: Option<i64>,
301}