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}