portal_lib/
lib.rs

1//! A small Protocol Library for [Portal](https://github.com/landhb/portal) - An encrypted file transfer utility
2//!
3//! This crate enables a consumer to:
4//!
5//! - Create/serialize/deserialize Portal request/response messages.
6//! - Negoticate a symmetric key with a peer using [SPAKE2](https://docs.rs/spake2/0.2.0/spake2)
7//! - Encrypt files with [Chacha20-Poly1305](https://blog.cloudflare.com/it-takes-two-to-chacha-poly/) using either the
8//!     [RustCrypto](https://docs.rs/chacha20poly1305) implementation or [Ring's](https://briansmith.org/rustdoc/ring/aead/index.html)
9//! - Send/receive files through a Portal relay
10//!
11//! The library is broken up into two abstractions:
12//!
13//! - A higher level API, exposted via the `Portal` struct, to facilitate automating transfers easily
14//! - A lower level API, exposed via the `protocol::Protocol` struct, if you need access to lower-level facilities
15use memmap::{MmapMut, MmapOptions};
16use std::convert::TryInto;
17use std::error::Error;
18use std::fs::{File, OpenOptions};
19use std::io::{Read, Write};
20use std::path::{Path, PathBuf};
21
22// Key Exchange
23use sha2::{Digest, Sha256};
24use spake2::{Ed25519Group, Identity, Password, Spake2};
25
26#[cfg(test)]
27mod tests;
28
29// Allow users to access errors
30pub mod errors;
31use errors::PortalError::*;
32
33/// Lower level protocol methods. Use these
34/// if the higher-level Portal interface is
35/// too abstract.
36pub mod protocol;
37pub use protocol::*;
38
39/**
40 * Arbitrary port for the Portal protocol
41 */
42pub const DEFAULT_PORT: u16 = 13265;
43
44/**
45 * Default chunk size
46 */
47pub const CHUNK_SIZE: usize = 65536;
48
49/// None constant for optional verify callbacks - Helper
50pub const NO_VERIFY_CALLBACK: Option<fn(&TransferInfo) -> bool> = None::<fn(&TransferInfo) -> bool>;
51
52/// None constant for optional progress callbacks - Helper
53pub const NO_PROGRESS_CALLBACK: Option<fn(usize)> = None::<fn(usize)>;
54
55/**
56 * The primary interface into the library.
57 */
58#[derive(PartialEq, Eq, Debug)]
59pub struct Portal {
60    // Information to correlate
61    // connections on the relay
62    pub id: String,
63    pub direction: Direction,
64
65    // KeyExchange information
66    pub exchange: PortalKeyExchange,
67
68    // A nonce sequence that must be used for
69    // the entire session to ensure no re-use
70    nseq: NonceSequence,
71
72    // Crypto state used to derive the key
73    // once we receive a confirmation msg from the peer
74    pub state: Option<Spake2<Ed25519Group>>,
75
76    // Derived session key
77    key: Option<Vec<u8>>,
78}
79
80impl Portal {
81    /// Initialize a new portal request
82    ///
83    /// # Example
84    ///
85    /// ```
86    /// use portal_lib::{Portal,Direction};
87    ///
88    /// // the shared password should be secret and hard to guess/crack
89    /// // see the portal-client as an example of potential usage
90    /// let id = String::from("my client ID");
91    /// let password = String::from("testpasswd");
92    /// let portal = Portal::init(Direction::Receiver, id, password).unwrap();
93    /// ```
94    pub fn init(
95        direction: Direction,
96        id: String,
97        password: String,
98    ) -> Result<Portal, Box<dyn Error>> {
99        // hash the ID string
100        let mut hasher = Sha256::new();
101        hasher.update(&id);
102        let id_bytes = hasher.finalize();
103        let id_hash = hex::encode(id_bytes);
104
105        // Initialize the state
106        let (s1, outbound_msg) = Spake2::<Ed25519Group>::start_symmetric(
107            &Password::new(password.as_bytes()),
108            &Identity::new(&id_bytes),
109        );
110
111        Ok(Portal {
112            direction,
113            id: id_hash,
114            exchange: outbound_msg.try_into().or(Err(CryptoError))?,
115            nseq: NonceSequence::new(),
116            state: Some(s1),
117            key: None,
118        })
119    }
120
121    /// Negotiate a secure connection over the insecure channel by performing the portal
122    /// handshake. Subsequent communication will be encrypted.
123    ///
124    /// # Example
125    ///
126    /// ```no_run
127    /// use std::net::TcpStream;
128    /// use portal_lib::{Portal,Direction};
129    ///
130    /// let mut portal = Portal::init(Direction::Sender, "id".into(), "password".into()).unwrap();
131    /// let mut stream = TcpStream::connect("127.0.0.1:34254").unwrap();
132    ///
133    /// // conduct the handshake with the peer
134    /// portal.handshake(&mut stream).unwrap();
135    /// ```
136    pub fn handshake<P: Read + Write>(&mut self, peer: &mut P) -> Result<(), Box<dyn Error>> {
137        // Send the connection message. If the relay cannot
138        // match us with a peer this will fail.
139        let confirm =
140            Protocol::connect(peer, &self.id, self.direction, self.exchange).or(Err(NoPeer))?;
141
142        // after calling finish() the SPAKE2 struct will be consumed
143        // so we must replace the value stored in self.state
144        let state = self.state.take().ok_or(BadState)?;
145
146        // Derive the session key
147        let key = Protocol::derive_key(state, &confirm).or(Err(BadMsg))?;
148
149        // confirm that the peer has the same key
150        Protocol::confirm_peer(peer, &self.id, self.direction, &key)?;
151
152        // Set key for further use
153        self.key = Some(key);
154        Ok(())
155    }
156
157    /// As the sender, communicate a TransferInfo struct to the receiver
158    /// so that they may confirm/deny the transfer. Returns an iterator
159    /// over the fullpath + Metadata to pass to send_file(). Allows the user
160    /// to send multiple files in one session.
161    ///
162    /// # Example
163    ///
164    /// ```no_run
165    /// use std::path::Path;
166    /// use std::error::Error;
167    /// use std::net::TcpStream;
168    /// use portal_lib::{Portal, Direction, TransferInfoBuilder};
169    ///
170    /// fn my_send() -> Result<(), Box<dyn Error>> {
171    ///     // Securely generate/exchange ID & Password with peer out-of-band
172    ///     let id = String::from("id");
173    ///     let password = String::from("password");
174    ///
175    ///     // Connect to the relay
176    ///     let mut portal = Portal::init(Direction::Sender,"id".into(), "password".into())?;
177    ///     let mut stream = TcpStream::connect("127.0.0.1:34254")?;
178    ///
179    ///     // The handshake must be performed first, otherwise
180    ///     // there is no shared key to encrypt the file with
181    ///     portal.handshake(&mut stream)?;
182    ///
183    ///     // Add any files/directories
184    ///     let info = TransferInfoBuilder::new()
185    ///         .add_file(Path::new("/etc/passwd"))?
186    ///         .finalize();
187    ///
188    ///     // Optional: implement a custom callback to display how much
189    ///     // has been transferred
190    ///     fn progress(transferred: usize) {
191    ///         println!("sent {:?} bytes", transferred);
192    ///     }
193    ///
194    ///     // Send every file in TransferInfo
195    ///     for (fullpath, metadata) in portal.outgoing(&mut stream, &info)? {
196    ///         portal.send_file(&mut stream, fullpath, Some(progress))?;
197    ///     }
198    ///     Ok(())
199    /// }
200    /// ```
201    pub fn outgoing<'a, W>(
202        &mut self,
203        peer: &mut W,
204        info: &'a TransferInfo,
205    ) -> Result<impl Iterator<Item = (&'a PathBuf, &'a Metadata)>, Box<dyn Error>>
206    where
207        W: Write,
208    {
209        // Check that the key exists to confirm the handshake is complete
210        let key = self.key.as_ref().ok_or(NoPeer)?;
211
212        // Send all TransferInfo for peer to confirm
213        Protocol::encrypt_and_write_object(peer, key, &mut self.nseq, info)?;
214
215        // Return an iterator that returns metadata for each outgoing file
216        Ok(info.localpaths.iter().zip(info.all.iter()))
217    }
218
219    /// As the receiver, receive a TransferInfo struct which will be passed
220    /// to your optional verify callback. And may be used to confirm/deny
221    /// the transfer. Returns an iterator over the Metadata of incoming files.
222    ///
223    /// # Example
224    ///
225    /// ```no_run
226    /// use std::path::Path;
227    /// use std::error::Error;
228    /// use std::net::TcpStream;
229    /// use portal_lib::{Portal, Direction, TransferInfo};
230    ///
231    /// fn my_recv() -> Result<(), Box<dyn Error>> {
232    ///
233    ///     // Securely generate/exchange ID & Password with peer out-of-band
234    ///     let id = String::from("id");
235    ///     let password = String::from("password");
236    ///
237    ///     // Connect to the relay
238    ///     let mut portal = Portal::init(Direction::Sender, id, password)?;
239    ///     let mut stream = TcpStream::connect("127.0.0.1:34254")?;
240    ///
241    ///     // The handshake must be performed first, otherwise
242    ///     // there is no shared key to encrypt the file with
243    ///     portal.handshake(&mut stream)?;
244    ///
245    ///     // Optional: User callback to confirm/deny a transfer. If
246    ///     // none is provided, this will default accept the incoming file.
247    ///     // Return true to accept, false to reject the transfer.
248    ///     fn confirm_download(_info: &TransferInfo) -> bool { true }
249    ///
250    ///     // Optional: implement a custom callback to display how much
251    ///     // has been transferred
252    ///     fn progress(transferred: usize) {
253    ///         println!("received {:?} bytes", transferred);
254    ///     }
255    ///
256    ///     // Decide where downloads should go
257    ///     let my_downloads = Path::new("/tmp");
258    ///
259    ///     // Receive every file in TransferInfo
260    ///     for metadata in portal.incoming(&mut stream, Some(confirm_download))? {
261    ///         portal.recv_file(&mut stream, my_downloads, Some(&metadata), Some(progress))?;
262    ///     }
263    ///     Ok(())
264    /// }
265    /// ```
266    pub fn incoming<R, V>(
267        &mut self,
268        peer: &mut R,
269        verify: Option<V>,
270    ) -> Result<impl Iterator<Item = Metadata>, Box<dyn Error>>
271    where
272        R: Read,
273        V: Fn(&TransferInfo) -> bool,
274    {
275        // Check that the key exists to confirm the handshake is complete
276        let key = self.key.as_ref().ok_or(NoPeer)?;
277
278        // Receive the TransferInfo
279        let info: TransferInfo = Protocol::read_encrypted_from(peer, key)?;
280
281        // Process the verify callback if applicable
282        match verify.as_ref().map_or(true, |c| c(&info)) {
283            true => {}
284            false => return Err(Cancelled.into()),
285        }
286
287        // Return an iterator that returns metadata for each incoming file
288        Ok(info.all.into_iter())
289    }
290
291    /// Send a given file over the portal. Must be called after performing the
292    /// handshake or this method will return an error.
293    ///
294    /// # Example
295    ///
296    /// ```no_run
297    /// use std::path::Path;
298    /// use std::net::TcpStream;
299    /// use portal_lib::{Portal,Direction};
300    ///
301    /// let mut portal = Portal::init(Direction::Sender,"id".into(), "password".into()).unwrap();
302    /// let mut stream = TcpStream::connect("127.0.0.1:34254").unwrap();
303    ///
304    /// // The handshake must be performed first, otherwise
305    /// // there is no shared key to encrypt the file with
306    /// portal.handshake(&mut stream);
307    ///
308    /// // Optional: implement a custom callback to display how much
309    /// // has been transferred
310    /// fn progress(transferred: usize) {
311    ///     println!("sent {:?} bytes", transferred);
312    /// }
313    ///
314    /// // Begin sending the file
315    /// let file = Path::new("/etc/passwd").to_path_buf();
316    /// portal.send_file(&mut stream, &file, Some(progress));
317    /// ```
318    pub fn send_file<W, D>(
319        &mut self,
320        peer: &mut W,
321        path: &PathBuf,
322        callback: Option<D>,
323    ) -> Result<usize, Box<dyn Error>>
324    where
325        W: Write,
326        D: Fn(usize),
327    {
328        // Check that the key exists to confirm the handshake is complete
329        let key = self.key.as_ref().ok_or(NoPeer)?;
330
331        // Obtain the file name stub from the path
332        let filename = path
333            .file_name()
334            .ok_or(BadFileName)?
335            .to_str()
336            .ok_or(BadFileName)?;
337
338        // Map the file into memory
339        let mut mmap = self.map_readable_file(path)?;
340
341        // Create the metatada object
342        let metadata = Metadata {
343            filesize: mmap.len() as u64,
344            filename: filename.to_string(),
345        };
346
347        // Write the file metadata over the encrypted channel
348        Protocol::encrypt_and_write_object(peer, key, &mut self.nseq, &metadata)?;
349
350        // Send the encrypted region in chunks
351        let mut total_sent = 0;
352        for chunk in mmap[..].chunks_mut(CHUNK_SIZE) {
353            // Encrypt the chunk in-place & send the header
354            Protocol::encrypt_and_write_header_only(peer, key, &mut self.nseq, chunk)?;
355
356            // Write the entire chunk
357            peer.write_all(chunk)?;
358
359            // Increment and optionally invoke callback
360            total_sent += chunk.len();
361            if let Some(c) = callback.as_ref() {
362                c(total_sent);
363            }
364        }
365        Ok(total_sent)
366    }
367
368    /// Receive the next file over the portal. Must be called after performing
369    /// the handshake or this method will return an error.
370    ///
371    /// # Example
372    ///
373    /// ```no_run
374    /// use std::path::Path;
375    /// use std::net::TcpStream;
376    /// use portal_lib::{Portal,Direction};
377    ///
378    /// let mut portal = Portal::init(Direction::Sender,"id".into(), "password".into()).unwrap();
379    /// let mut stream = TcpStream::connect("127.0.0.1:34254").unwrap();
380    ///
381    /// // The handshake must be performed first, otherwise
382    /// // there is no shared key to encrypt the file with
383    /// portal.handshake(&mut stream);
384    ///
385    /// // Optional: implement a custom callback to display how much
386    /// // has been transferred
387    /// fn progress(transferred: usize) {
388    ///     println!("received {:?} bytes", transferred);
389    /// }
390    ///
391    /// // Begin receiving the file into /tmp
392    /// portal.recv_file(&mut stream, Path::new("/tmp"), None, Some(progress));
393    /// ```
394    pub fn recv_file<R, D>(
395        &mut self,
396        peer: &mut R,
397        outdir: &Path,
398        expected: Option<&Metadata>,
399        display: Option<D>,
400    ) -> Result<Metadata, Box<dyn Error>>
401    where
402        R: Read,
403        D: Fn(usize),
404    {
405        // Check that the key exists to confirm the handshake is complete
406        let key = self.key.as_ref().ok_or(NoPeer)?;
407
408        // Verify the outdir is valid
409        if !outdir.is_dir() {
410            return Err(BadDirectory.into());
411        }
412
413        // Receive the metadata
414        let metadata: Metadata = Protocol::read_encrypted_from(peer, key)?;
415
416        // Verify the metadata is expected, if a comparison is provided
417        if expected.map_or(false, |exp| metadata != *exp) {
418            return Err(BadMsg.into());
419        }
420
421        // Ensure the filename is only the name component
422        let path = match Path::new(&metadata.filename).file_name() {
423            Some(s) => outdir.join(s),
424            _ => return Err(BadFileName.into()),
425        };
426
427        // Map the region into memory for writing
428        let mut mmap = self.map_writeable_file(&path, metadata.filesize)?;
429
430        let mut total = 0;
431        for chunk in mmap[..].chunks_mut(CHUNK_SIZE) {
432            // Receive the entire chunk in-place
433            Protocol::read_encrypted_zero_copy(peer, key, chunk)?;
434
435            // Increment and optionally invoke callback
436            total += chunk.len();
437            if let Some(c) = display.as_ref() {
438                c(total);
439            }
440        }
441
442        // Check for incomplete transfers
443        if total != metadata.filesize as usize {
444            return Err(Incomplete.into());
445        }
446        Ok(metadata)
447    }
448
449    /// Helper: mmap's a file into memory for reading
450    fn map_readable_file(&self, f: &PathBuf) -> Result<MmapMut, Box<dyn Error>> {
451        let file = File::open(f)?;
452        let mmap = unsafe { MmapOptions::new().map_copy(&file)? };
453        Ok(mmap)
454    }
455
456    /// Helper: mmap's a file into memory for writing
457    fn map_writeable_file(&self, f: &PathBuf, size: u64) -> Result<MmapMut, Box<dyn Error>> {
458        let file = OpenOptions::new()
459            .read(true)
460            .write(true)
461            .create(true)
462            .open(f)?;
463
464        file.set_len(size)?;
465        let mmap = unsafe { MmapOptions::new().map_mut(&file)? };
466        Ok(mmap)
467    }
468
469    /// Returns a copy of the Portal::Direction associated with
470    /// this Portal request
471    pub fn get_direction(&self) -> Direction {
472        self.direction
473    }
474
475    /// Sets the Portal::Direction associated with this Poral request
476    pub fn set_direction(&mut self, direction: Direction) {
477        self.direction = direction;
478    }
479
480    /// Returns a reference to the ID associated with this
481    /// Portal request
482    pub fn get_id(&self) -> &String {
483        &self.id
484    }
485
486    /// Sets the ID associated with this Poral request
487    pub fn set_id(&mut self, id: String) {
488        self.id = id;
489    }
490
491    /// Returns a reference to the key associated with this
492    /// Portal request
493    pub fn get_key(&self) -> &Option<Vec<u8>> {
494        &self.key
495    }
496
497    /// Sets the ID associated with this Poral request
498    pub fn set_key(&mut self, key: Vec<u8>) {
499        self.key = Some(key);
500    }
501}