Skip to main content

ghidra_worker_ctl/
token.rs

1//! Connection token for the loopback-TCP transport (spec §5/§6). The host generates a 128-bit
2//! token, hands it to the worker out-of-band via a short-lived 0600 file, and verifies the worker
3//! echoes it in the init handshake — so another local process that races the ephemeral port is
4//! rejected. Not a long-term secret: it exists only for the boot window and the file is deleted
5//! right after the handshake (see boot.rs).
6
7use std::io;
8use std::path::Path;
9
10/// A 128-bit connection token, rendered as 32 lowercase hex chars.
11#[derive(Debug, Clone)]
12pub struct ConnectionToken(String);
13
14impl ConnectionToken {
15    /// Generate a fresh random 128-bit token (UUID v4 supplies 122 bits of randomness — ample
16    /// for a transient boot gate; rendered as its 32 hex digits without dashes).
17    pub fn generate() -> Self {
18        let bytes = uuid::Uuid::new_v4().into_bytes();
19        let mut s = String::with_capacity(32);
20        for b in bytes {
21            s.push_str(&format!("{b:02x}"));
22        }
23        ConnectionToken(s)
24    }
25
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29
30    /// Constant-time comparison against a candidate. Length mismatch fails immediately (lengths
31    /// are not secret); equal-length inputs are compared with no early exit so timing does not
32    /// leak how many leading chars matched.
33    pub fn verify(&self, candidate: &str) -> bool {
34        let a = self.0.as_bytes();
35        let b = candidate.as_bytes();
36        if a.len() != b.len() {
37            return false;
38        }
39        let mut diff: u8 = 0;
40        for (x, y) in a.iter().zip(b.iter()) {
41            diff |= x ^ y;
42        }
43        diff == 0
44    }
45
46    /// Write the token to `path` as its raw hex string (no trailing newline), restricting the file
47    /// to the current user. On Unix this is a real `0600`; on Windows `set_permissions` cannot
48    /// express `0600`, so the caller (boot.rs) owner-locks the per-boot dir via
49    /// `ghidra_ipc::owner_only::restrict_to_owner` before writing, and deletes the dir immediately
50    /// after the handshake.
51    pub fn write_to_file(&self, path: &Path) -> io::Result<()> {
52        std::fs::write(path, self.0.as_bytes())?;
53        #[cfg(unix)]
54        {
55            use std::os::unix::fs::PermissionsExt;
56            std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
57        }
58        Ok(())
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn generated_token_is_128_bit_hex() {
68        let t = ConnectionToken::generate();
69        assert_eq!(t.as_str().len(), 32); // 16 bytes = 32 hex chars
70        assert!(t.as_str().chars().all(|c| c.is_ascii_hexdigit()));
71    }
72
73    #[test]
74    fn two_tokens_differ() {
75        assert_ne!(
76            ConnectionToken::generate().as_str(),
77            ConnectionToken::generate().as_str()
78        );
79    }
80
81    #[test]
82    fn verify_accepts_own_value_rejects_others_and_length_mismatch() {
83        let t = ConnectionToken::generate();
84        assert!(t.verify(t.as_str()));
85        assert!(!t.verify("deadbeef"));
86        assert!(!t.verify(&(t.as_str().to_string() + "x")));
87        assert!(!t.verify(""));
88    }
89
90    #[test]
91    fn write_to_file_roundtrips_the_exact_value() {
92        let dir = std::env::temp_dir().join(format!("ghidra-mcp-tok-{}", uuid::Uuid::new_v4()));
93        std::fs::create_dir_all(&dir).unwrap();
94        let path = dir.join("t.token");
95        let t = ConnectionToken::generate();
96        t.write_to_file(&path).unwrap();
97        let read = std::fs::read_to_string(&path).unwrap();
98        assert_eq!(read, t.as_str());
99        std::fs::remove_dir_all(&dir).ok();
100    }
101}