Skip to main content

openrtc/
explicit_transfer_crypto.rs

1//! Encrypted explicit file-transfer helpers shared by native hosts.
2//!
3//! Iroh bi-stream wire format:
4//! - each logical write is one length-framed `ORTCE1` envelope (`RAW_STREAM_TYPE_ID = 3`)
5//! - the first decrypted frame is the `0x02` protocol byte
6//! - the second decrypted frame is `4-byte JSON length || header JSON`
7//! - subsequent decrypted frames are file chunks
8//!
9//! The `0x02` byte remains the explicit-file stream discriminator; current
10//! transport uses the admitted logical-Iroh stream rather than a direct WebRTC route.
11
12use iroh::endpoint::{RecvStream, SendStream};
13
14use crate::application_crypto::{
15    is_encrypted_payload, open_payload as open_crypto_payload,
16    protect_payload as protect_crypto_payload, CryptoError, APPLICATION_KEY_BYTES,
17    RAW_STREAM_TYPE_ID,
18};
19use crate::application_crypto_streams::{CryptoRecvStream, CryptoSendStream};
20
21pub const EXPLICIT_FILE_PROTOCOL_BYTE: u8 = 0x02;
22
23pub fn protect_payload(
24    key: &[u8; APPLICATION_KEY_BYTES],
25    payload: &[u8],
26) -> Result<Vec<u8>, CryptoError> {
27    protect_crypto_payload(key, RAW_STREAM_TYPE_ID, payload)
28}
29
30pub fn open_payload(
31    key: &[u8; APPLICATION_KEY_BYTES],
32    payload: &[u8],
33) -> Result<Vec<u8>, CryptoError> {
34    open_crypto_payload(key, RAW_STREAM_TYPE_ID, payload, true)
35}
36
37pub fn open_payload_optional(
38    key: Option<&[u8; APPLICATION_KEY_BYTES]>,
39    payload: &[u8],
40) -> Result<Vec<u8>, String> {
41    match key {
42        Some(key) => open_payload(key, payload).map_err(|error| format!("{error:?}")),
43        None if is_encrypted_payload(payload) => Err(
44            "encrypted explicit transfer payload rejected because no application crypto key is installed"
45                .to_string(),
46        ),
47        None => Ok(payload.to_vec()),
48    }
49}
50
51pub enum TransferSender {
52    Plain(SendStream),
53    Encrypted(CryptoSendStream),
54}
55
56impl TransferSender {
57    pub async fn write_protocol_byte(&mut self) -> Result<(), String> {
58        self.write_all(&[EXPLICIT_FILE_PROTOCOL_BYTE]).await
59    }
60
61    pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), String> {
62        match self {
63            Self::Plain(stream) => stream
64                .write_all(buf)
65                .await
66                .map_err(|error| error.to_string()),
67            Self::Encrypted(stream) => stream
68                .write_all(buf)
69                .await
70                .map_err(|error| error.to_string()),
71        }
72    }
73
74    pub fn finish(self) -> Result<(), String> {
75        match self {
76            Self::Plain(mut stream) => stream.finish().map_err(|error| error.to_string()),
77            Self::Encrypted(stream) => stream.finish().map_err(|error| error.to_string()),
78        }
79    }
80}
81
82pub fn transfer_sender(
83    stream: SendStream,
84    key: Option<[u8; APPLICATION_KEY_BYTES]>,
85) -> TransferSender {
86    match key {
87        Some(key) => TransferSender::Encrypted(CryptoSendStream::new(stream, key)),
88        None => TransferSender::Plain(stream),
89    }
90}
91
92pub struct TransferReceiver {
93    inner: CryptoRecvStream,
94}
95
96impl TransferReceiver {
97    pub fn new(stream: RecvStream, key: [u8; APPLICATION_KEY_BYTES]) -> Result<Self, String> {
98        CryptoRecvStream::new(stream, key)
99            .map(|inner| Self { inner })
100            .map_err(|error| error.to_string())
101    }
102
103    pub async fn read(&mut self, buf: &mut [u8]) -> Result<usize, String> {
104        self.inner
105            .read(buf)
106            .await
107            .map_err(|error| error.to_string())
108    }
109
110    pub async fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), String> {
111        let mut offset = 0;
112        while offset < buf.len() {
113            let read_bytes = self.read(&mut buf[offset..]).await?;
114            if read_bytes == 0 {
115                return Err("unexpected end of encrypted explicit transfer stream".to_string());
116            }
117            offset += read_bytes;
118        }
119        Ok(())
120    }
121}
122
123pub async fn read_transfer_header(receiver: &mut TransferReceiver) -> Result<Vec<u8>, String> {
124    let mut len_buf = [0u8; 4];
125    receiver.read_exact(&mut len_buf).await?;
126    let len = u32::from_be_bytes(len_buf) as usize;
127    if len == 0 {
128        return Err("invalid explicit transfer header length: 0 bytes".to_string());
129    }
130    let mut header_buf = vec![0u8; len];
131    receiver.read_exact(&mut header_buf).await?;
132    Ok(header_buf)
133}
134
135pub fn header_is_encrypted(len_buf: [u8; 4], available: &[u8]) -> bool {
136    let frame_len = u32::from_be_bytes(len_buf) as usize;
137    if frame_len < 32 || frame_len > crate::application_crypto::DEFAULT_MAX_RAW_FRAME_BYTES {
138        return false;
139    }
140    if available.len() < 4 + 6 {
141        return false;
142    }
143    is_encrypted_payload(&available[4..])
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::application_crypto::protect_frame;
150
151    #[test]
152    fn webrtc_round_trip_matches_application_payload_type() {
153        let key = [7u8; APPLICATION_KEY_BYTES];
154        let payload = br#"{"transfer_id":"tx-1","filename":"a.txt","size":3,"mime_type":"application/octet-stream"}"#;
155        let protected = protect_payload(&key, payload).expect("protect");
156        let opened = open_payload(&key, &protected).expect("open");
157        assert_eq!(opened, payload);
158    }
159
160    #[test]
161    fn iroh_header_frame_round_trip_uses_raw_stream_type() {
162        let key = [9u8; APPLICATION_KEY_BYTES];
163        let header_json = br#"{"transfer_id":"tx-2","filename":"b.bin","size":10,"mime_type":"application/octet-stream"}"#;
164        let mut header_frame = Vec::with_capacity(4 + header_json.len());
165        header_frame.extend_from_slice(&(header_json.len() as u32).to_be_bytes());
166        header_frame.extend_from_slice(header_json);
167        let wire = protect_frame(&key, &header_frame).expect("frame");
168        let opened = open_crypto_payload(&key, RAW_STREAM_TYPE_ID, &wire[4..], true)
169            .expect("open frame body");
170        assert_eq!(opened, header_frame);
171    }
172}