Skip to main content

sentinelpass_protocol/
connection.rs

1//! Connection-level session negotiation, deadlines, and framing
2//! (WBS-509/510/511). Wraps a platform transport connection and speaks
3//! either the v1 SECURED session protocol (default) or the legacy PLAIN
4//! envelope protocol (server-accepted migration window, removed in 1.0).
5
6use crate::session::{self, SessionCrypto, SESSION_READ_DEADLINE};
7use crate::transport::{TransportError, TransportResult};
8
9/// A connected transport (either platform).
10pub enum TransportConnection {
11    #[cfg(unix)]
12    Unix(crate::transport::unix::UnixSocketConnection),
13    #[cfg(windows)]
14    Windows(crate::transport::windows::WindowsNamedPipeConnection),
15}
16
17#[cfg(unix)]
18impl From<crate::transport::unix::UnixSocketConnection> for TransportConnection {
19    fn from(conn: crate::transport::unix::UnixSocketConnection) -> Self {
20        Self::Unix(conn)
21    }
22}
23
24#[cfg(windows)]
25impl From<crate::transport::windows::WindowsNamedPipeConnection> for TransportConnection {
26    fn from(conn: crate::transport::windows::WindowsNamedPipeConnection) -> Self {
27        Self::Windows(conn)
28    }
29}
30
31impl TransportConnection {
32    async fn read_frame(&mut self) -> TransportResult<Vec<u8>> {
33        match self {
34            #[cfg(unix)]
35            TransportConnection::Unix(conn) => conn.read_message().await,
36            #[cfg(windows)]
37            TransportConnection::Windows(conn) => conn.read_message().await,
38        }
39    }
40
41    async fn write_frame(&mut self, data: &[u8]) -> TransportResult<()> {
42        match self {
43            #[cfg(unix)]
44            TransportConnection::Unix(conn) => conn.write_message(data).await,
45            #[cfg(windows)]
46            TransportConnection::Windows(conn) => conn.write_message(data).await,
47        }
48    }
49}
50
51/// One negotiated IPC connection.
52pub enum IpcConnection {
53    /// v1 session: every envelope frame is sealed (WBS-509/510).
54    Secured {
55        conn: Box<TransportConnection>,
56        crypto: SessionCrypto,
57    },
58    /// Legacy plaintext envelopes — migration window only, server-side
59    /// accepted and announced (ADR-007; removed in 1.0).
60    Plain { conn: Box<TransportConnection> },
61}
62
63impl IpcConnection {
64    /// Client-side negotiation (WBS-509): send SessionHello, read
65    /// SessionAccept, derive the directional session keys.
66    pub async fn connect_client(
67        mut conn: TransportConnection,
68        token: &str,
69    ) -> TransportResult<Self> {
70        let (hello, client_random) = session::new_hello();
71        let hello_bytes = serde_json::to_vec(&hello)
72            .map_err(|e| TransportError::Other(format!("hello encode: {e}")))?;
73        tokio::time::timeout(SESSION_READ_DEADLINE, conn.write_frame(&hello_bytes))
74            .await
75            .map_err(|_| TransportError::Timeout)??;
76
77        let reply = tokio::time::timeout(SESSION_READ_DEADLINE, conn.read_frame())
78            .await
79            .map_err(|_| TransportError::Timeout)??;
80        let accept = session::parse_accept(&reply)?;
81        let server_random = session::server_random_of(&accept)?;
82        let (c2s, s2c) = session::derive_directional_keys(token, &client_random, &server_random)?;
83        Ok(IpcConnection::Secured {
84            conn: Box::new(conn),
85            crypto: SessionCrypto::client(c2s, s2c),
86        })
87    }
88
89    /// Server-side negotiation: peek the first frame. A SessionHello →
90    /// SECURED (reply SessionAccept, derive keys). Anything else → the
91    /// legacy PLAIN envelope protocol; the first frame is handed back to
92    /// the caller for normal processing.
93    pub async fn accept_server(
94        mut conn: TransportConnection,
95        token: &str,
96    ) -> TransportResult<(Self, Option<Vec<u8>>)> {
97        let first = tokio::time::timeout(SESSION_READ_DEADLINE, conn.read_frame())
98            .await
99            .map_err(|_| TransportError::Timeout)??;
100
101        if session::is_session_hello(&first) {
102            let hello = session::parse_hello(&first)?;
103            let client_random = session::client_random_of(&hello)?;
104            let (accept, server_random) = session::new_accept();
105            let accept_bytes = serde_json::to_vec(&accept)
106                .map_err(|e| TransportError::Other(format!("accept encode: {e}")))?;
107            tokio::time::timeout(SESSION_READ_DEADLINE, conn.write_frame(&accept_bytes))
108                .await
109                .map_err(|_| TransportError::Timeout)??;
110            let (c2s, s2c) =
111                session::derive_directional_keys(token, &client_random, &server_random)?;
112            Ok((
113                IpcConnection::Secured {
114                    conn: Box::new(conn),
115                    crypto: SessionCrypto::server(c2s, s2c),
116                },
117                None,
118            ))
119        } else {
120            // Legacy plaintext client (migration window; announced once per
121            // connection so operators can find stragglers).
122            tracing::warn!(
123                "accepted LEGACY plaintext IPC connection (no session handshake) — \
124                 upgrade the client; the plaintext window is removed in 1.0"
125            );
126            Ok((
127                IpcConnection::Plain {
128                    conn: Box::new(conn),
129                },
130                Some(first),
131            ))
132        }
133    }
134
135    /// Send one envelope frame (sealed in secured mode), write-bounded by
136    /// the session deadline (WBS-511).
137    pub async fn send_frame(&mut self, envelope_bytes: &[u8]) -> TransportResult<()> {
138        match self {
139            IpcConnection::Secured { conn, crypto } => {
140                let sealed = crypto.seal(envelope_bytes)?;
141                tokio::time::timeout(SESSION_READ_DEADLINE, conn.write_frame(&sealed))
142                    .await
143                    .map_err(|_| TransportError::Timeout)??;
144                Ok(())
145            }
146            IpcConnection::Plain { conn } => {
147                tokio::time::timeout(SESSION_READ_DEADLINE, conn.write_frame(envelope_bytes))
148                    .await
149                    .map_err(|_| TransportError::Timeout)??;
150                Ok(())
151            }
152        }
153    }
154
155    /// Receive one envelope frame (opened in secured mode), bounded by the
156    /// read deadline (WBS-511).
157    pub async fn recv_frame(&mut self) -> TransportResult<Vec<u8>> {
158        let raw = tokio::time::timeout(SESSION_READ_DEADLINE, self.read_raw())
159            .await
160            .map_err(|_| TransportError::Timeout)??;
161        match self {
162            IpcConnection::Secured { crypto, .. } => crypto.open(&raw),
163            IpcConnection::Plain { .. } => Ok(raw),
164        }
165    }
166
167    async fn read_raw(&mut self) -> TransportResult<Vec<u8>> {
168        match self {
169            IpcConnection::Secured { conn, .. } => conn.read_frame().await,
170            IpcConnection::Plain { conn } => conn.read_frame().await,
171        }
172    }
173}