1use serde::de::{Deserializer, IgnoredAny, MapAccess, Visitor};
10use serde::{Deserialize, Serialize};
11use zeroize::{Zeroize, Zeroizing};
12
13#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct Prelogin {
17 pub kdf: i32,
19 pub kdf_iterations: u32,
20 #[serde(default)]
21 pub kdf_memory: Option<u32>,
22 #[serde(default)]
23 pub kdf_parallelism: Option<u32>,
24}
25
26#[derive(Clone, Serialize)]
28pub struct TokenResponse {
29 pub access_token: String,
30 pub refresh_token: Option<String>,
31 pub key: Option<String>,
34 pub private_key: Option<String>,
36}
37
38impl<'de> Deserialize<'de> for TokenResponse {
41 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
42 where
43 D: Deserializer<'de>,
44 {
45 struct TokenVisitor;
46
47 impl<'de> Visitor<'de> for TokenVisitor {
48 type Value = TokenResponse;
49
50 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 formatter.write_str("a token response")
52 }
53
54 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
55 where
56 M: MapAccess<'de>,
57 {
58 let mut access: Option<Zeroizing<String>> = None;
59 let mut refresh: Option<Zeroizing<String>> = None;
60 let mut key: Option<Zeroizing<String>> = None;
61 let mut private_key: Option<Zeroizing<String>> = None;
62 while let Some(field) = map.next_key::<String>()? {
63 match field.as_str() {
64 "access_token" => access = Some(Zeroizing::new(map.next_value()?)),
65 "refresh_token" => {
66 refresh = map.next_value::<Option<String>>()?.map(Zeroizing::new);
67 }
68 "Key" | "key" => {
69 key = map.next_value::<Option<String>>()?.map(Zeroizing::new);
70 }
71 "PrivateKey" | "privateKey" => {
72 private_key = map.next_value::<Option<String>>()?.map(Zeroizing::new);
73 }
74 _ => {
75 let _: IgnoredAny = map.next_value()?;
76 }
77 }
78 }
79 let mut access = access
80 .ok_or_else(|| <M::Error as serde::de::Error>::missing_field("access_token"))?;
81 Ok(TokenResponse {
82 access_token: std::mem::take(&mut *access),
83 refresh_token: refresh.map(|value| (*value).clone()),
84 key: key.map(|value| (*value).clone()),
85 private_key: private_key.map(|value| (*value).clone()),
86 })
87 }
88 }
89
90 deserializer.deserialize_map(TokenVisitor)
91 }
92}
93
94impl Drop for TokenResponse {
97 fn drop(&mut self) {
98 self.access_token.zeroize();
99 if let Some(refresh) = &mut self.refresh_token {
100 refresh.zeroize();
101 }
102 if let Some(key) = &mut self.key {
103 key.zeroize();
104 }
105 if let Some(private_key) = &mut self.private_key {
106 private_key.zeroize();
107 }
108 }
109}
110
111impl std::fmt::Debug for TokenResponse {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.debug_struct("TokenResponse")
116 .field("access_token", &"[redacted]")
117 .field("refresh_token", &self.refresh_token.as_ref().map(|_| "[redacted]"))
118 .field("key", &self.key.as_ref().map(|_| "[redacted]"))
119 .field("private_key", &self.private_key.as_ref().map(|_| "[redacted]"))
120 .finish()
121 }
122}
123
124#[derive(Debug, Clone, Default, Deserialize, Serialize)]
126#[serde(rename_all = "camelCase")]
127pub struct Profile {
128 #[serde(default)]
131 pub id: Option<String>,
132 #[serde(default, rename = "key", alias = "Key")]
133 pub key: Option<String>,
134 #[serde(default, rename = "privateKey", alias = "PrivateKey")]
135 pub private_key: Option<String>,
136 #[serde(default)]
137 pub organizations: Vec<Organization>,
138 #[serde(default)]
139 pub email: Option<String>,
140}
141
142#[derive(Debug, Clone, Deserialize, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub struct Organization {
146 pub id: String,
147 #[serde(default)]
148 pub key: Option<String>,
149 #[serde(default)]
150 pub name: Option<String>,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
155#[serde(rename_all = "camelCase")]
156pub struct Folder {
157 pub id: String,
158 pub name: String,
159}
160
161#[derive(Debug, Clone, Default, Deserialize, Serialize)]
165#[serde(rename_all = "camelCase")]
166pub struct FolderList {
167 #[serde(default)]
168 pub data: Vec<Folder>,
169}
170
171#[derive(Clone, Deserialize, Serialize)]
173#[serde(rename_all = "camelCase")]
174pub struct AttachmentMeta {
175 pub id: String,
176 pub file_name: String,
177 #[serde(default, deserialize_with = "size_from_number_or_string")]
180 pub size: Option<u64>,
181 #[serde(default)]
182 pub size_name: Option<String>,
183 #[serde(default)]
184 pub url: Option<String>,
185 #[serde(default)]
187 pub key: Option<String>,
188}
189
190impl std::fmt::Debug for AttachmentMeta {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 f.debug_struct("AttachmentMeta")
195 .field("id", &self.id)
196 .field("file_name", &self.file_name)
197 .field("size", &self.size)
198 .field("size_name", &self.size_name)
199 .field("url", &self.url.as_ref().map(|_| "[redacted]"))
200 .field("key", &self.key.as_ref().map(|_| "[redacted]"))
201 .finish()
202 }
203}
204
205#[derive(Debug, Clone, Default, Deserialize, Serialize)]
207#[serde(rename_all = "camelCase")]
208pub struct SyncData {
209 #[serde(default)]
210 pub profile: Profile,
211 #[serde(default)]
212 pub folders: Vec<Folder>,
213 #[serde(default)]
216 pub ciphers: Vec<serde_json::Value>,
217}
218
219fn size_from_number_or_string<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
221where
222 D: serde::Deserializer<'de>,
223{
224 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
225 Ok(match value {
226 Some(serde_json::Value::Number(number)) => number.as_u64(),
227 Some(serde_json::Value::String(size)) => size.trim().parse().ok(),
228 _ => None,
229 })
230}
231
232#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
234pub struct TwoFactorCode {
235 pub token: String,
236 pub provider: i32,
238}
239
240impl std::fmt::Debug for TwoFactorCode {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 f.debug_struct("TwoFactorCode")
244 .field("token", &"[redacted]")
245 .field("provider", &self.provider)
246 .finish()
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
257 fn token_response_parses_both_key_spellings_and_skips_unknown_fields() {
258 let upper: TokenResponse = serde_json::from_str(
259 r#"{"access_token":"a","Key":"wrapped","PrivateKey":"private","unknown":1}"#,
260 )
261 .expect("the upper-case spells parse");
262 assert_eq!(upper.key.as_deref(), Some("wrapped"));
263 assert_eq!(upper.private_key.as_deref(), Some("private"));
264
265 let lower: TokenResponse =
266 serde_json::from_str(r#"{"access_token":"a","key":"wrapped","privateKey":"private"}"#)
267 .expect("the lower-case spells parse");
268 assert_eq!(lower.key.as_deref(), Some("wrapped"));
269 assert_eq!(lower.private_key.as_deref(), Some("private"));
270 }
271
272 #[test]
274 fn a_token_response_without_an_access_token_is_refused() {
275 let error = serde_json::from_str::<TokenResponse>(r#"{"key":"wrapped"}"#)
276 .expect_err("the access token is required");
277
278 assert!(error.to_string().contains("access_token"), "{error}");
279 }
280
281 #[test]
284 fn the_profile_carries_the_account_user_id() {
285 let profile: Profile =
286 serde_json::from_str(r#"{"id":"user-1","key":"wrapped"}"#).expect("the profile");
287
288 assert_eq!(profile.id.as_deref(), Some("user-1"));
289 }
290
291 #[test]
294 fn attachment_meta_debug_redacts_the_url_and_key() {
295 let meta = AttachmentMeta {
296 id: "attachment-id".to_owned(),
297 file_name: "file".to_owned(),
298 size: Some(97),
299 size_name: None,
300 url: Some("https://vault.example.com/signed?token=secret".to_owned()),
301 key: Some("2.c2VjcmV0".to_owned()),
302 };
303
304 let text = format!("{meta:?}");
305
306 assert!(!text.contains("secret"), "{text}");
307 assert!(!text.contains("2.c2VjcmV0"), "{text}");
308 }
309}