Skip to main content

bwx/
protocol.rs

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    /// Human-readable description of what the user ran (e.g. `get
28    /// google.com`). Not authentication-relevant; used only to enrich
29    /// UI prompts on the agent side (Touch ID dialog, pinentry CONFIRM).
30    #[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    /// Like `new`, but tags the request with a per-CLI-process session
46    /// token and a human-readable purpose string. The agent uses the
47    /// session to coalesce Touch ID prompts so that a single `bwx
48    /// <command>` invocation only pops one biometric dialog regardless
49    /// of how many `Decrypt`/`Encrypt` IPCs it fires; the purpose is
50    /// shown on the prompt itself.
51    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
81// Taken from https://github.com/gpg/gnupg/blob/36dbca3e6944d13e75e96eace634e58a7d7e201d/common/session-env.c#L62-L91
82pub const ENVIRONMENT_VARIABLES: &[&str] = &[
83    // Used to set ttytype
84    "TERM",
85    // The X display
86    "DISPLAY",
87    // Xlib Authentication
88    "XAUTHORITY",
89    // Used by Xlib to select X input modules (e.g. "@im=SCIM")
90    "XMODIFIERS",
91    // For the Wayland display engine.
92    "WAYLAND_DISPLAY",
93    // Used by Qt and other non-GTK toolkits to check for X11 or Wayland
94    "XDG_SESSION_TYPE",
95    // Used by Qt to explicitly request X11 or Wayland; in particular, needed to
96    // make Qt use Wayland on GNOME
97    "QT_QPA_PLATFORM",
98    // Used by GTK to select GTK input modules (e.g. "scim-bridge")
99    "GTK_IM_MODULE",
100    // Used by GNOME 3 to talk to gcr over dbus
101    "DBUS_SESSION_BUS_ADDRESS",
102    // Used by Qt to select Qt input modules (e.g. "xim")
103    "QT_IM_MODULE",
104    // Used for communication with non-standard Pinentries
105    "PINENTRY_USER_DATA",
106    // Used to pass window information
107    "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)]
203#[serde(tag = "type")]
204pub enum Action {
205    Login,
206    Register,
207    Unlock,
208    CheckLock,
209    Lock,
210    Sync,
211    Decrypt {
212        cipherstring: String,
213        entry_key: Option<String>,
214        org_id: Option<String>,
215    },
216    Encrypt {
217        plaintext: String,
218        org_id: Option<String>,
219    },
220    ClipboardStore {
221        text: String,
222    },
223    Quit,
224    Version,
225    /// Enroll the currently-unlocked vault keys under a Touch ID-gated
226    /// Keychain wrapper key. Requires the agent to already be unlocked.
227    TouchIdEnroll,
228    /// Remove the Keychain wrapper key and the on-disk enrollment blob.
229    TouchIdDisable,
230    /// Report whether Touch ID enrollment is active and summarise the
231    /// current `touchid_gate` setting.
232    TouchIdStatus,
233}
234
235#[derive(serde::Serialize, serde::Deserialize, Debug)]
236#[serde(tag = "type")]
237pub enum Response {
238    Ack,
239    Error {
240        error: String,
241    },
242    Decrypt {
243        plaintext: String,
244    },
245    Encrypt {
246        cipherstring: String,
247    },
248    Version {
249        version: u32,
250    },
251    TouchIdStatus {
252        enrolled: bool,
253        gate: String,
254        keychain_label: Option<String>,
255    },
256}