#file-transfer #authenticated-encryption #file-encryption #chacha20 #chacha20-poly1305 #wormhole #spake2

portal-lib

A Secure file transfer library, written in Rust. The library utilizes SPAKE2 for key negotiation over an insecure channel, and ChaCha20Poly1305 Authenticated Encryption to encrypt the file with the derived shared symmetric key. This enables two peers to transfer a file over any channel without needing to trust the intermediary relay.

4 releases (breaking)

0.5.0 Oct 14, 2023
0.4.0 Apr 2, 2022
0.3.0 Mar 28, 2022
0.2.0 Oct 8, 2020
0.1.0 Oct 2, 2020

#4 in #wormhole

Download history 22/week @ 2024-09-20 2/week @ 2024-09-27

141 downloads per month
Used in 2 crates

Apache-2.0 OR MIT

75KB
1.5K SLoC

portal-lib

A small Protocol Library for Portal - An encrypted file transfer utility

This crate enables a consumer to:

  • Create/serialize/deserialize Portal request/response messages.
  • Negoticate a symmetric key with a peer using SPAKE2
  • Encrypt files with Chacha20-Poly1305 using either the RustCrypto implementation or Ring's
  • Send/receive files through a Portal relay

The library is broken up into two abstractions:

  • A higher level API, exposted via the Portal struct, to facilitate automating transfers easily
  • A lower level API, exposed via the Protocol struct, if you need access to lower-level facilities

Higher Level API - Example of Sending a file:

use std::path::Path;
use std::error::Error;
use std::net::TcpStream;
use portal_lib::{Portal, Direction, TransferInfoBuilder};

fn my_send() -> Result<(), Box<dyn Error>> {

    // Securely generate/exchange ID & Password with peer out-of-band
    let id = String::from("id");
    let password = String::from("password");

    // Connect to the relay - the ID will be used to connect the peers
    let mut portal = Portal::init(Direction::Sender, id, password)?;
    let mut stream = TcpStream::connect("127.0.0.1:34254")?;

    // The handshake must be performed first, otherwise
    // there is no shared key to encrypt the file with
    portal.handshake(&mut stream)?;

    // Add any files/directories
    let info = TransferInfoBuilder::new()
        .add_file(Path::new("/etc/passwd"))?
        .finalize();

    // Optional: implement a custom callback to display how much
    // has been transferred
    fn progress(transferred: usize) {
       println!("sent {:?} bytes", transferred);
    }

    // Send every file in TransferInfo
    for (fullpath, metadata) in portal.outgoing(&mut stream, &info)? {
        portal.send_file(&mut stream, fullpath, Some(progress))?;
    }
    Ok(())
}

Higher Level API - Example of Receiving a file:

use std::path::Path;
use std::error::Error;
use std::net::TcpStream;
use portal_lib::{Portal, Direction, TransferInfo};

fn my_recv() -> Result<(), Box<dyn Error>> {

    // Securely generate/exchange ID & Password with peer out-of-band
    let id = String::from("id");
    let password = String::from("password");

    // Connect to the relay - the ID will be used to connect the peers
    let mut portal = Portal::init(Direction::Sender, id, password)?;
    let mut stream = TcpStream::connect("127.0.0.1:34254")?;

    // The handshake must be performed first, otherwise
    // there is no shared key to encrypt the file with
    portal.handshake(&mut stream)?;

    // Optional: User callback to confirm/deny a transfer. If
    // none is provided, this will default accept the incoming file.
    // Return true to accept, false to reject the transfer.
    fn confirm_download(_info: &TransferInfo) -> bool { true }

    // Optional: implement a custom callback to display how much
    // has been transferred
    fn progress(transferred: usize) {
        println!("received {:?} bytes", transferred);
    }

    // Decide where downloads should go
    let my_downloads = Path::new("/tmp");

    // Receive every file in TransferInfo
    for metadata in portal.incoming(&mut stream, Some(confirm_download))? {
        portal.recv_file(&mut stream, my_downloads, Some(&metadata), Some(progress))?;
    }
    Ok(())
}

Lower Level API - Example of SPAKE2 key negotiation:

use spake2::{Ed25519Group, Identity, Password, Spake2};

// Securely receive/derive your id & password for this session
let channel_id = String::from("myid");
let password = String::from("mysecurepassword");

// Init a Spake2 context
let (state, outbound_msg) = Spake2::<Ed25519Group>::start_symmetric(
    &Password::new(&password.as_bytes()),
    &Identity::new(&channel_id.as_bytes()),
);

// Connect to the relay
let mut stream = TcpStream::connect("127.0.0.1:34254").unwrap();

// Send the connection message to the relay. If the relay cannot
// match us with a peer this will fail.
let confirm =
    Protocol::connect(&mut stream, &channel_id, Direction::Sender, outbound_msg).unwrap();

// Derive the shared session key
let key = Protocol::derive_key(state, &confirm).unwrap();

// confirm that the peer has the same key
Protocol::confirm_peer(&mut stream, &channel_id, Direction::Sender, &key)?;

You can use the confirm_peer() method to verify that a remote peer has derived the same key as you, as long as the communication stream implements the std::io::Read and std::io::Write traits.

Dependencies

~4–13MB
~176K SLoC