1use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
2
3pub const VERSION: u32 = {
4 const fn unwrap(res: &Result<u32, std::num::ParseIntError>) -> u32 {
5 match res {
6 Ok(t) => *t,
7 Err(_) => panic!("failed to parse cargo version"),
8 }
9 }
10
11 let major = env!("CARGO_PKG_VERSION_MAJOR");
12 let minor = env!("CARGO_PKG_VERSION_MINOR");
13 let patch = env!("CARGO_PKG_VERSION_PATCH");
14
15 unwrap(&u32::from_str_radix(major, 10)) * 1_000_000
16 + unwrap(&u32::from_str_radix(minor, 10)) * 1_000_000
17 + unwrap(&u32::from_str_radix(patch, 10)) * 1_000_000
18};
19
20#[derive(serde::Serialize, serde::Deserialize, Debug)]
21pub struct Request {
22 tty: Option<String>,
23 environment: Option<Environment>,
24 action: Action,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 session_id: Option<String>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
31 purpose: Option<String>,
32}
33
34impl Request {
35 pub fn new(environment: Environment, action: Action) -> Self {
36 Self {
37 tty: None,
38 environment: Some(environment),
39 action,
40 session_id: None,
41 purpose: None,
42 }
43 }
44
45 pub fn new_with_session(
52 environment: Environment,
53 action: Action,
54 session_id: String,
55 purpose: Option<String>,
56 ) -> Self {
57 Self {
58 tty: None,
59 environment: Some(environment),
60 action,
61 session_id: Some(session_id),
62 purpose,
63 }
64 }
65
66 pub fn into_parts(
67 self,
68 ) -> (Action, Environment, Option<String>, Option<String>) {
69 (
70 self.action,
71 self.environment.unwrap_or_else(|| Environment {
72 tty: self.tty.map(|tty| SerializableOsString(tty.into())),
73 env_vars: vec![],
74 }),
75 self.session_id,
76 self.purpose,
77 )
78 }
79}
80
81pub const ENVIRONMENT_VARIABLES: &[&str] = &[
83 "TERM",
85 "DISPLAY",
87 "XAUTHORITY",
89 "XMODIFIERS",
91 "WAYLAND_DISPLAY",
93 "XDG_SESSION_TYPE",
95 "QT_QPA_PLATFORM",
98 "GTK_IM_MODULE",
100 "DBUS_SESSION_BUS_ADDRESS",
102 "QT_IM_MODULE",
104 "PINENTRY_USER_DATA",
106 "PINENTRY_GEOM_HINT",
108];
109
110pub static ENVIRONMENT_VARIABLES_OS: std::sync::LazyLock<
111 Vec<std::ffi::OsString>,
112> = std::sync::LazyLock::new(|| {
113 ENVIRONMENT_VARIABLES
114 .iter()
115 .map(std::ffi::OsString::from)
116 .collect()
117});
118
119#[derive(Hash, PartialEq, Eq, Debug, Clone)]
120struct SerializableOsString(std::ffi::OsString);
121
122impl serde::Serialize for SerializableOsString {
123 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
124 where
125 S: serde::Serializer,
126 {
127 serializer.serialize_str(&crate::base64::encode(self.0.as_bytes()))
128 }
129}
130
131impl<'de> serde::Deserialize<'de> for SerializableOsString {
132 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
133 where
134 D: serde::Deserializer<'de>,
135 {
136 struct Visitor;
137
138 impl serde::de::Visitor<'_> for Visitor {
139 type Value = SerializableOsString;
140
141 fn expecting(
142 &self,
143 formatter: &mut std::fmt::Formatter,
144 ) -> std::fmt::Result {
145 formatter.write_str("base64 encoded os string")
146 }
147
148 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
149 where
150 E: serde::de::Error,
151 {
152 Ok(SerializableOsString(std::ffi::OsString::from_vec(
153 crate::base64::decode(s).map_err(|_| {
154 E::invalid_value(serde::de::Unexpected::Str(s), &self)
155 })?,
156 )))
157 }
158 }
159
160 deserializer.deserialize_str(Visitor)
161 }
162}
163
164#[derive(serde::Serialize, serde::Deserialize, Debug, Default, Clone)]
165pub struct Environment {
166 tty: Option<SerializableOsString>,
167 env_vars: Vec<(SerializableOsString, SerializableOsString)>,
168}
169
170impl Environment {
171 pub fn new(
172 tty: Option<std::ffi::OsString>,
173 env_vars: Vec<(std::ffi::OsString, std::ffi::OsString)>,
174 ) -> Self {
175 Self {
176 tty: tty.map(SerializableOsString),
177 env_vars: env_vars
178 .into_iter()
179 .map(|(k, v)| {
180 (SerializableOsString(k), SerializableOsString(v))
181 })
182 .collect(),
183 }
184 }
185
186 pub fn tty(&self) -> Option<&std::ffi::OsStr> {
187 self.tty.as_ref().map(|tty| tty.0.as_os_str())
188 }
189
190 pub fn env_vars(
191 &self,
192 ) -> std::collections::HashMap<std::ffi::OsString, std::ffi::OsString>
193 {
194 self.env_vars
195 .iter()
196 .map(|(var, val)| (var.0.clone(), val.0.clone()))
197 .filter(|(var, _)| (*ENVIRONMENT_VARIABLES_OS).contains(var))
198 .collect()
199 }
200}
201
202#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
203pub struct DecryptItem {
204 pub cipherstring: String,
205 pub entry_key: Option<String>,
206 pub org_id: Option<String>,
207}
208
209#[derive(serde::Serialize, serde::Deserialize, Debug)]
210#[serde(tag = "outcome")]
211pub enum DecryptItemResult {
212 Ok { plaintext: String },
213 Err { error: String },
214}
215
216#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
217pub struct EncryptItem {
218 pub plaintext: String,
219 pub org_id: Option<String>,
220}
221
222#[derive(serde::Serialize, serde::Deserialize, Debug)]
223#[serde(tag = "outcome")]
224pub enum EncryptItemResult {
225 Ok { cipherstring: String },
226 Err { error: String },
227}
228
229#[derive(serde::Serialize, serde::Deserialize, Debug)]
230#[serde(tag = "type")]
231pub enum Action {
232 Login,
233 Register,
234 Unlock,
235 CheckLock,
236 Lock,
237 Sync,
238 Decrypt {
239 cipherstring: String,
240 entry_key: Option<String>,
241 org_id: Option<String>,
242 },
243 DecryptBatch {
247 items: Vec<DecryptItem>,
248 },
249 Encrypt {
250 plaintext: String,
251 org_id: Option<String>,
252 },
253 EncryptBatch {
257 items: Vec<EncryptItem>,
258 },
259 ClipboardStore {
260 text: String,
261 },
262 Quit,
263 Version,
264 TouchIdEnroll,
267 TouchIdDisable,
269 TouchIdStatus,
272}
273
274#[derive(serde::Serialize, serde::Deserialize, Debug)]
275#[serde(tag = "type")]
276pub enum Response {
277 Ack,
278 Error {
279 error: String,
280 },
281 Decrypt {
282 plaintext: String,
283 },
284 DecryptBatch {
285 results: Vec<DecryptItemResult>,
286 },
287 Encrypt {
288 cipherstring: String,
289 },
290 EncryptBatch {
291 results: Vec<EncryptItemResult>,
292 },
293 Version {
294 version: u32,
295 },
296 TouchIdStatus {
297 enrolled: bool,
298 gate: String,
299 keychain_label: Option<String>,
300 },
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 fn rmp_roundtrip<T>(value: &T) -> T
308 where
309 T: serde::Serialize + serde::de::DeserializeOwned,
310 {
311 let bytes = rmp_serde::to_vec(value).unwrap();
312 rmp_serde::from_slice(&bytes).unwrap()
313 }
314
315 #[test]
316 fn action_decrypt_msgpack_roundtrip() {
317 let a = Action::Decrypt {
318 cipherstring: "2.aaa|bbb|ccc".to_string(),
319 entry_key: Some("ek".to_string()),
320 org_id: None,
321 };
322 let bytes = rmp_serde::to_vec(&a).unwrap();
323 assert!(bytes.len() < 90, "msgpack payload {} bytes", bytes.len());
326 match rmp_roundtrip(&a) {
327 Action::Decrypt {
328 cipherstring,
329 entry_key,
330 org_id,
331 } => {
332 assert_eq!(cipherstring, "2.aaa|bbb|ccc");
333 assert_eq!(entry_key.as_deref(), Some("ek"));
334 assert_eq!(org_id, None);
335 }
336 other => panic!("unexpected variant: {other:?}"),
337 }
338 }
339
340 #[test]
341 fn decrypt_batch_roundtrip_preserves_order_and_results() {
342 let req = Action::DecryptBatch {
343 items: vec![
344 DecryptItem {
345 cipherstring: "a".into(),
346 entry_key: None,
347 org_id: None,
348 },
349 DecryptItem {
350 cipherstring: "b".into(),
351 entry_key: Some("k".into()),
352 org_id: Some("org".into()),
353 },
354 ],
355 };
356 match rmp_roundtrip(&req) {
357 Action::DecryptBatch { items } => {
358 assert_eq!(items.len(), 2);
359 assert_eq!(items[0].cipherstring, "a");
360 assert_eq!(items[1].entry_key.as_deref(), Some("k"));
361 }
362 other => panic!("unexpected variant: {other:?}"),
363 }
364
365 let resp = Response::DecryptBatch {
366 results: vec![
367 DecryptItemResult::Ok {
368 plaintext: "hello".into(),
369 },
370 DecryptItemResult::Err {
371 error: "decrypt failed".into(),
372 },
373 ],
374 };
375 match rmp_roundtrip(&resp) {
376 Response::DecryptBatch { results } => {
377 assert_eq!(results.len(), 2);
378 assert!(matches!(
379 results[0],
380 DecryptItemResult::Ok { ref plaintext } if plaintext == "hello"
381 ));
382 assert!(matches!(
383 results[1],
384 DecryptItemResult::Err { ref error } if error == "decrypt failed"
385 ));
386 }
387 other => panic!("unexpected variant: {other:?}"),
388 }
389 }
390}