core_ipc/
noise.rs

1//! Noise IK encrypted IPC transport (ADR-SEC-006).
2//!
3//! Provides forward-secret, mutually-authenticated encryption for all IPC
4//! traffic using the Noise Protocol Framework (IK pattern) via `snow`.
5//!
6//! Pattern: `Noise_IK_25519_ChaChaPoly_BLAKE2s`
7//! - IK: initiator's static key transmitted, responder's static key pre-known
8//! - X25519 DH, ChaCha20-Poly1305 AEAD, BLAKE2s hash
9//! - 2-message handshake (1 round-trip), then forward-secret transport
10//!
11//! `UCred` (PID + UID) is bound into the Noise prologue so that both sides must
12//! agree on the peer identity — cryptographically binding the OS-level transport
13//! identity to the encrypted channel.
14//!
15//! Noise transport messages are limited to 65535 bytes. Application frames up to
16//! 16 MiB are chunked into multiple Noise messages with a chunk-count header.
17//!
18//! Key management (generation, persistence, tamper detection) lives in
19//! `noise_keys`.
20
21use crate::framing::{MAX_FRAME_SIZE, read_frame, write_frame};
22use crate::transport::PeerCredentials;
23use tokio::io::{AsyncRead, AsyncWrite};
24
25// Re-export key management API so downstream `use core_ipc::noise::*` paths remain stable.
26pub use crate::noise_keys::{
27    ZeroizingKeypair, create_keys_dir, generate_keypair, read_bus_public_key,
28    read_bus_public_key_from, read_daemon_keypair, read_daemon_public_key,
29    set_runtime_dir_override, write_bus_keypair, write_daemon_keypair,
30};
31
32/// Noise protocol parameter string.
33pub(crate) const NOISE_PARAMS: &str = "Noise_IK_25519_ChaChaPoly_BLAKE2s";
34
35/// Maximum plaintext per Noise transport message: 65535 - 16 (AEAD tag) = 65519 bytes.
36const MAX_NOISE_PLAINTEXT: usize = 65535 - 16;
37
38/// Handshake timeout to prevent `DoS` via slow handshake.
39const HANDSHAKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
40
41/// Build the prologue bytes from peer credentials.
42///
43/// Format: `PDS-IPC-v1:<local_pid>:<local_uid>:<remote_pid>:<remote_uid>`
44///
45/// Both sides must construct identical prologues for the handshake to succeed.
46/// The server knows its own creds and the client's creds (from `UCred`), and vice
47/// versa (the client knows its own PID/UID and the server's from `UCred` after connect).
48///
49/// For the prologue to match, we use a canonical ordering: lower PID first.
50fn build_prologue(local: &PeerCredentials, remote: &PeerCredentials) -> Vec<u8> {
51    // Canonical ordering: lower PID first to ensure both sides produce identical bytes.
52    let (first, second) = if local.pid <= remote.pid {
53        (local, remote)
54    } else {
55        (remote, local)
56    };
57    format!(
58        "PDS-IPC-v1:{}:{}:{}:{}",
59        first.pid, first.uid, second.pid, second.uid
60    )
61    .into_bytes()
62}
63
64/// Encrypted IPC transport wrapping a completed Noise session.
65///
66/// Provides chunked encrypted frame I/O over the Noise transport state.
67/// Application frames are split into chunks of at most `MAX_NOISE_PLAINTEXT`
68/// bytes, each encrypted as a separate Noise transport message.
69///
70/// The `TransportState` requires `&mut self` for both encrypt and decrypt,
71/// so callers must coordinate access (e.g. via `Arc<Mutex<NoiseTransport>>`
72/// when split into separate read/write tasks).
73pub struct NoiseTransport {
74    state: snow::TransportState,
75}
76
77impl NoiseTransport {
78    /// Write an encrypted application frame.
79    ///
80    /// The payload is chunked into Noise transport messages of at most
81    /// `MAX_NOISE_PLAINTEXT` bytes each. Wire format per frame:
82    ///
83    /// ```text
84    /// [4-byte BE chunk_count][chunk_1][chunk_2]...[chunk_n]
85    /// ```
86    ///
87    /// Each chunk is written as a length-prefixed frame via `write_frame`.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error on I/O failure or if the payload exceeds `MAX_FRAME_SIZE`.
92    pub async fn write_encrypted_frame<W: AsyncWrite + Unpin>(
93        &mut self,
94        writer: &mut W,
95        payload: &[u8],
96    ) -> core_types::Result<()> {
97        if payload.len() > MAX_FRAME_SIZE as usize {
98            return Err(core_types::Error::Ipc(format!(
99                "payload size {} exceeds maximum {}",
100                payload.len(),
101                MAX_FRAME_SIZE
102            )));
103        }
104
105        // Calculate number of chunks needed.
106        let chunk_count = if payload.is_empty() {
107            1 // Send one empty encrypted chunk for zero-length payloads.
108        } else {
109            payload.len().div_ceil(MAX_NOISE_PLAINTEXT)
110        };
111
112        // Write chunk count as a 4-byte BE header (plaintext — the count itself
113        // is not sensitive and is needed to know how many chunks to read).
114        let count_bytes = u32::try_from(chunk_count)
115            .map_err(|_| core_types::Error::Ipc("too many chunks".into()))?;
116        write_frame(writer, &count_bytes.to_be_bytes()).await?;
117
118        // Write each chunk as an encrypted Noise message.
119        // Output buffer: plaintext + 16-byte AEAD tag.
120        let mut enc_buf = vec![0u8; MAX_NOISE_PLAINTEXT + 16];
121
122        for chunk_idx in 0..chunk_count {
123            let start = chunk_idx * MAX_NOISE_PLAINTEXT;
124            let end = (start + MAX_NOISE_PLAINTEXT).min(payload.len());
125            let chunk = &payload[start..end];
126
127            let len = self
128                .state
129                .write_message(chunk, &mut enc_buf)
130                .map_err(|e| core_types::Error::Ipc(format!("Noise encrypt failed: {e}")))?;
131
132            write_frame(writer, &enc_buf[..len]).await?;
133        }
134
135        Ok(())
136    }
137
138    /// Read and decrypt an application frame.
139    ///
140    /// Reads the chunk count header, then reads and decrypts each chunk,
141    /// reassembling the original plaintext payload.
142    ///
143    /// # Errors
144    ///
145    /// Returns an error on I/O failure, decryption failure, or if the
146    /// reassembled payload exceeds `MAX_FRAME_SIZE`.
147    pub async fn read_encrypted_frame<R: AsyncRead + Unpin>(
148        &mut self,
149        reader: &mut R,
150    ) -> core_types::Result<Vec<u8>> {
151        // Read chunk count header.
152        let count_frame = read_frame(reader).await?;
153        if count_frame.len() != 4 {
154            return Err(core_types::Error::Ipc(format!(
155                "invalid chunk count header: expected 4 bytes, got {}",
156                count_frame.len()
157            )));
158        }
159        let chunk_count = u32::from_be_bytes([
160            count_frame[0],
161            count_frame[1],
162            count_frame[2],
163            count_frame[3],
164        ]) as usize;
165
166        // Sanity check: max chunks for 16 MiB payload.
167        let max_chunks = (MAX_FRAME_SIZE as usize).div_ceil(MAX_NOISE_PLAINTEXT);
168        if chunk_count > max_chunks {
169            return Err(core_types::Error::Ipc(format!(
170                "chunk count {chunk_count} exceeds maximum {max_chunks}"
171            )));
172        }
173
174        let mut payload = Vec::with_capacity(chunk_count * MAX_NOISE_PLAINTEXT);
175        let mut dec_buf = vec![0u8; MAX_NOISE_PLAINTEXT];
176
177        for _ in 0..chunk_count {
178            let ciphertext = read_frame(reader).await?;
179            let len = self
180                .state
181                .read_message(&ciphertext, &mut dec_buf)
182                .map_err(|e| core_types::Error::Ipc(format!("Noise decrypt failed: {e}")))?;
183            payload.extend_from_slice(&dec_buf[..len]);
184        }
185
186        // Zeroize intermediate decrypt buffer (may contain secret fragments).
187        zeroize::Zeroize::zeroize(&mut dec_buf);
188
189        if payload.len() > MAX_FRAME_SIZE as usize {
190            return Err(core_types::Error::Ipc(format!(
191                "decrypted payload {} exceeds maximum {}",
192                payload.len(),
193                MAX_FRAME_SIZE
194            )));
195        }
196
197        Ok(payload)
198    }
199
200    /// Check if this transport was created by the initiator side.
201    #[must_use]
202    pub fn is_initiator(&self) -> bool {
203        self.state.is_initiator()
204    }
205
206    /// Get the remote party's static public key.
207    #[must_use]
208    pub fn remote_static(&self) -> Option<&[u8]> {
209        self.state.get_remote_static()
210    }
211}
212
213/// Perform the server-side (responder) Noise IK handshake.
214///
215/// Called after `stream.into_split()` and `extract_ucred()`, before the
216/// read/write loops. The server's static keypair was generated at startup
217/// and the client's identity is verified via the Noise handshake + `UCred`
218/// prologue binding.
219///
220/// IK handshake (responder perspective):
221/// 1. Read message 1 from initiator (contains initiator's ephemeral + encrypted static)
222/// 2. Write message 2 to initiator (contains responder's ephemeral)
223/// 3. Handshake complete — derive transport keys
224///
225/// # Errors
226///
227/// Returns an error if the handshake fails or times out.
228pub async fn server_handshake<R, W>(
229    reader: &mut R,
230    writer: &mut W,
231    server_keypair: &snow::Keypair,
232    local_creds: &PeerCredentials,
233    remote_creds: &PeerCredentials,
234) -> core_types::Result<NoiseTransport>
235where
236    R: AsyncRead + Unpin,
237    W: AsyncWrite + Unpin,
238{
239    let prologue = build_prologue(local_creds, remote_creds);
240
241    let mut handshake = snow::Builder::new(
242        NOISE_PARAMS
243            .parse()
244            .map_err(|e| core_types::Error::Platform(format!("invalid Noise params: {e}")))?,
245    )
246    .local_private_key(&server_keypair.private)
247    .map_err(|e| core_types::Error::Ipc(format!("Noise builder error: {e}")))?
248    .prologue(&prologue)
249    .map_err(|e| core_types::Error::Ipc(format!("Noise prologue error: {e}")))?
250    .build_responder()
251    .map_err(|e| core_types::Error::Ipc(format!("Noise responder build failed: {e}")))?;
252
253    tokio::time::timeout(HANDSHAKE_TIMEOUT, async {
254        // IK responder: read msg1, write msg2.
255
256        // Read message 1 from initiator.
257        let msg1 = read_frame(reader).await?;
258        let mut payload_buf = vec![0u8; 65535];
259        handshake
260            .read_message(&msg1, &mut payload_buf)
261            .map_err(|e| {
262                core_types::Error::Ipc(format!("Noise handshake msg1 read failed: {e}"))
263            })?;
264
265        // Write message 2 to initiator.
266        let mut msg2_buf = vec![0u8; 65535];
267        let msg2_len = handshake.write_message(&[], &mut msg2_buf).map_err(|e| {
268            core_types::Error::Ipc(format!("Noise handshake msg2 write failed: {e}"))
269        })?;
270        write_frame(writer, &msg2_buf[..msg2_len]).await?;
271
272        // Handshake complete — transition to transport mode.
273        let transport = handshake
274            .into_transport_mode()
275            .map_err(|e| core_types::Error::Ipc(format!("Noise transport mode failed: {e}")))?;
276
277        tracing::info!("Noise IK handshake completed (server)");
278        Ok(NoiseTransport { state: transport })
279    })
280    .await
281    .map_err(|_| core_types::Error::Ipc("Noise handshake timed out".into()))?
282}
283
284/// Perform the client-side (initiator) Noise IK handshake.
285///
286/// Called after `UnixStream::connect()` and `into_split()`, before the
287/// read/write loops. The client generates an ephemeral static keypair and
288/// pre-loads the server's public key (the "K" in IK).
289///
290/// IK handshake (initiator perspective):
291/// 1. Write message 1 to responder (ephemeral + encrypted static)
292/// 2. Read message 2 from responder (responder's ephemeral)
293/// 3. Handshake complete — derive transport keys
294///
295/// # Errors
296///
297/// Returns an error if the handshake fails or times out.
298pub async fn client_handshake<R, W>(
299    reader: &mut R,
300    writer: &mut W,
301    server_public_key: &[u8; 32],
302    client_keypair: &snow::Keypair,
303    local_creds: &PeerCredentials,
304    remote_creds: &PeerCredentials,
305) -> core_types::Result<NoiseTransport>
306where
307    R: AsyncRead + Unpin,
308    W: AsyncWrite + Unpin,
309{
310    let prologue = build_prologue(local_creds, remote_creds);
311
312    let mut handshake = snow::Builder::new(
313        NOISE_PARAMS
314            .parse()
315            .map_err(|e| core_types::Error::Platform(format!("invalid Noise params: {e}")))?,
316    )
317    .local_private_key(&client_keypair.private)
318    .map_err(|e| core_types::Error::Ipc(format!("Noise builder error: {e}")))?
319    .remote_public_key(server_public_key)
320    .map_err(|e| core_types::Error::Ipc(format!("Noise remote key error: {e}")))?
321    .prologue(&prologue)
322    .map_err(|e| core_types::Error::Ipc(format!("Noise prologue error: {e}")))?
323    .build_initiator()
324    .map_err(|e| core_types::Error::Ipc(format!("Noise initiator build failed: {e}")))?;
325
326    tokio::time::timeout(HANDSHAKE_TIMEOUT, async {
327        // IK initiator: write msg1, read msg2.
328
329        // Write message 1 to responder.
330        let mut msg1_buf = vec![0u8; 65535];
331        let msg1_len = handshake.write_message(&[], &mut msg1_buf).map_err(|e| {
332            core_types::Error::Ipc(format!("Noise handshake msg1 write failed: {e}"))
333        })?;
334        write_frame(writer, &msg1_buf[..msg1_len]).await?;
335
336        // Read message 2 from responder.
337        let msg2 = read_frame(reader).await?;
338        let mut payload_buf = vec![0u8; 65535];
339        handshake
340            .read_message(&msg2, &mut payload_buf)
341            .map_err(|e| {
342                core_types::Error::Ipc(format!("Noise handshake msg2 read failed: {e}"))
343            })?;
344
345        // Handshake complete — transition to transport mode.
346        let transport = handshake
347            .into_transport_mode()
348            .map_err(|e| core_types::Error::Ipc(format!("Noise transport mode failed: {e}")))?;
349
350        tracing::info!("Noise IK handshake completed (client)");
351        Ok(NoiseTransport { state: transport })
352    })
353    .await
354    .map_err(|_| core_types::Error::Ipc("Noise handshake timed out".into()))?
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn prologue_canonical_ordering() {
363        let a = PeerCredentials {
364            pid: 100,
365            uid: 1000,
366        };
367        let b = PeerCredentials {
368            pid: 200,
369            uid: 1000,
370        };
371
372        // Both orderings produce the same prologue.
373        assert_eq!(build_prologue(&a, &b), build_prologue(&b, &a));
374
375        // Contains expected format.
376        let p = String::from_utf8(build_prologue(&a, &b)).unwrap();
377        assert_eq!(p, "PDS-IPC-v1:100:1000:200:1000");
378    }
379
380    #[tokio::test]
381    async fn handshake_and_transport_roundtrip() {
382        // Generate server and client keypairs.
383        let server_kp = generate_keypair().unwrap();
384        let client_kp = generate_keypair().unwrap();
385
386        let server_pub: [u8; 32] = server_kp.public().try_into().unwrap();
387
388        // Simulated peer credentials (both sides must agree).
389        let server_creds = PeerCredentials { pid: 1, uid: 1000 };
390        let client_creds = PeerCredentials { pid: 2, uid: 1000 };
391
392        // Create a duplex channel simulating a UDS pair.
393        let (client_stream, server_stream) = tokio::io::duplex(65536);
394
395        let (mut client_reader, mut client_writer) = tokio::io::split(client_stream);
396        let (mut server_reader, mut server_writer) = tokio::io::split(server_stream);
397
398        // Run handshake concurrently.
399        let (client_result, server_result) = tokio::join!(
400            client_handshake(
401                &mut client_reader,
402                &mut client_writer,
403                &server_pub,
404                client_kp.as_inner(),
405                &client_creds,
406                &server_creds,
407            ),
408            server_handshake(
409                &mut server_reader,
410                &mut server_writer,
411                server_kp.as_inner(),
412                &server_creds,
413                &client_creds,
414            ),
415        );
416
417        let mut client_transport = client_result.unwrap();
418        let mut server_transport = server_result.unwrap();
419
420        // Client sends encrypted message, server decrypts.
421        let plaintext = b"hello encrypted world";
422
423        client_transport
424            .write_encrypted_frame(&mut client_writer, plaintext)
425            .await
426            .unwrap();
427
428        let decrypted = server_transport
429            .read_encrypted_frame(&mut server_reader)
430            .await
431            .unwrap();
432
433        assert_eq!(decrypted, plaintext);
434
435        // Server sends back, client decrypts.
436        let response = b"acknowledged";
437        server_transport
438            .write_encrypted_frame(&mut server_writer, response)
439            .await
440            .unwrap();
441
442        let decrypted_response = client_transport
443            .read_encrypted_frame(&mut client_reader)
444            .await
445            .unwrap();
446
447        assert_eq!(decrypted_response, response);
448    }
449
450    #[tokio::test]
451    async fn large_frame_chunking() {
452        // Test that frames larger than 65519 bytes are chunked correctly.
453        let server_kp = generate_keypair().unwrap();
454        let client_kp = generate_keypair().unwrap();
455        let server_pub: [u8; 32] = server_kp.public().try_into().unwrap();
456
457        let server_creds = PeerCredentials { pid: 10, uid: 1000 };
458        let client_creds = PeerCredentials { pid: 20, uid: 1000 };
459
460        let (client_stream, server_stream) = tokio::io::duplex(1024 * 1024);
461        let (mut cr, mut cw) = tokio::io::split(client_stream);
462        let (mut sr, mut sw) = tokio::io::split(server_stream);
463
464        let (mut ct, mut st) = tokio::join!(
465            async {
466                client_handshake(
467                    &mut cr,
468                    &mut cw,
469                    &server_pub,
470                    client_kp.as_inner(),
471                    &client_creds,
472                    &server_creds,
473                )
474                .await
475                .unwrap()
476            },
477            async {
478                server_handshake(
479                    &mut sr,
480                    &mut sw,
481                    server_kp.as_inner(),
482                    &server_creds,
483                    &client_creds,
484                )
485                .await
486                .unwrap()
487            },
488        );
489
490        // 200 KiB payload — requires multiple chunks (200*1024 / 65519 = ~4 chunks).
491        let large_payload = vec![0xABu8; 200 * 1024];
492
493        ct.write_encrypted_frame(&mut cw, &large_payload)
494            .await
495            .unwrap();
496        let decrypted = st.read_encrypted_frame(&mut sr).await.unwrap();
497        assert_eq!(decrypted, large_payload);
498    }
499
500    #[tokio::test]
501    async fn prologue_mismatch_fails_handshake() {
502        let server_kp = generate_keypair().unwrap();
503        let client_kp = generate_keypair().unwrap();
504        let server_pub: [u8; 32] = server_kp.public().try_into().unwrap();
505
506        // Deliberately mismatched credentials — server thinks client is PID 99.
507        let server_creds = PeerCredentials { pid: 1, uid: 1000 };
508        let client_creds_real = PeerCredentials { pid: 2, uid: 1000 };
509        let client_creds_fake = PeerCredentials { pid: 99, uid: 1000 };
510
511        let (client_stream, server_stream) = tokio::io::duplex(65536);
512        let (mut cr, mut cw) = tokio::io::split(client_stream);
513        let (mut sr, mut sw) = tokio::io::split(server_stream);
514
515        let (client_result, server_result) = tokio::join!(
516            // Client uses real creds for prologue.
517            client_handshake(
518                &mut cr,
519                &mut cw,
520                &server_pub,
521                client_kp.as_inner(),
522                &client_creds_real,
523                &server_creds,
524            ),
525            // Server uses WRONG creds for prologue (thinks client is PID 99).
526            server_handshake(
527                &mut sr,
528                &mut sw,
529                server_kp.as_inner(),
530                &server_creds,
531                &client_creds_fake,
532            ),
533        );
534
535        // At least one side should fail due to prologue mismatch.
536        assert!(
537            client_result.is_err() || server_result.is_err(),
538            "prologue mismatch should cause handshake failure"
539        );
540    }
541
542    #[tokio::test]
543    async fn empty_payload_roundtrip() {
544        let server_kp = generate_keypair().unwrap();
545        let client_kp = generate_keypair().unwrap();
546        let server_pub: [u8; 32] = server_kp.public().try_into().unwrap();
547
548        let sc = PeerCredentials { pid: 1, uid: 1000 };
549        let cc = PeerCredentials { pid: 2, uid: 1000 };
550
551        let (cs, ss) = tokio::io::duplex(65536);
552        let (mut cr, mut cw) = tokio::io::split(cs);
553        let (mut sr, mut sw) = tokio::io::split(ss);
554
555        let (mut ct, mut st) = tokio::join!(
556            async {
557                client_handshake(
558                    &mut cr,
559                    &mut cw,
560                    &server_pub,
561                    client_kp.as_inner(),
562                    &cc,
563                    &sc,
564                )
565                .await
566                .unwrap()
567            },
568            async {
569                server_handshake(&mut sr, &mut sw, server_kp.as_inner(), &sc, &cc)
570                    .await
571                    .unwrap()
572            },
573        );
574
575        ct.write_encrypted_frame(&mut cw, b"").await.unwrap();
576        let decrypted = st.read_encrypted_frame(&mut sr).await.unwrap();
577        assert!(decrypted.is_empty());
578    }
579}