File negotiation and crypto

This commit is contained in:
rictorlome 2022-02-12 14:59:04 -05:00
parent f4c7f1361e
commit ec5034cb62
8 changed files with 211 additions and 54 deletions

View file

@ -1,13 +1,17 @@
https://github.com/mcginty/snow/tree/master/examples - https://github.com/mcginty/snow/tree/master/examples
https://github.com/black-binary/snowstorm/tree/master/examples - https://github.com/black-binary/snowstorm/tree/master/examples
https://cryptoservices.github.io/cryptography/protocols/2016/04/27/noise-protocol.html - https://cryptoservices.github.io/cryptography/protocols/2016/04/27/noise-protocol.html
https://github.com/libp2p - https://github.com/libp2p
https://docs.rs/spake2/latest/spake2/ - https://docs.rs/spake2/latest/spake2/
https://crates.io/crates/chacha20poly1305 - https://crates.io/crates/chacha20poly1305
https://briansmith.org/rustdoc/ring/aead/index.html - https://briansmith.org/rustdoc/ring/aead/index.html
https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream - https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream
https://kerkour.com/rust-symmetric-encryption-aead-benchmark/ - https://kerkour.com/rust-symmetric-encryption-aead-benchmark/
https://docs.rs/aes-gcm/latest/aes_gcm/ - https://docs.rs/aes-gcm/latest/aes_gcm/
- https://github.com/rust-lang/flate2-rs
- https://crates.io/crates/async-compression

View file

@ -1,14 +1,17 @@
use crate::crypto::handshake; use crate::crypto::handshake;
use crate::file::FileHandle; use crate::file::{to_size_string, FileHandle, FileInfo};
use crate::message::{Message, MessageStream}; use crate::message::{EncryptedMessage, FileNegotiationPayload, Message, MessageStream};
use anyhow::Result; use aes_gcm::Aes256Gcm;
use anyhow::{anyhow, Result};
use blake2::{Blake2s256, Digest}; use blake2::{Blake2s256, Digest};
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures::future::join_all; use futures::future::try_join_all;
use futures::prelude::*; use futures::prelude::*;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::io;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tokio_util::codec::{FramedRead, LinesCodec};
fn pass_to_bytes(password: &String) -> Bytes { fn pass_to_bytes(password: &String) -> Bytes {
let mut hasher = Blake2s256::new(); let mut hasher = Blake2s256::new();
@ -18,17 +21,15 @@ fn pass_to_bytes(password: &String) -> Bytes {
} }
pub async fn send(file_paths: &Vec<PathBuf>, password: &String) -> Result<()> { pub async fn send(file_paths: &Vec<PathBuf>, password: &String) -> Result<()> {
let tasks = file_paths // Fail early if there are problems generating file handles
.into_iter() let handles = get_file_handles(file_paths).await?;
.map(|path| FileHandle::new(path.to_path_buf()).map(|f| f.map(|s| s.to_file_info())));
let metadatas = join_all(tasks).await;
println!("mds: {:?}", metadatas);
return Ok(());
// Establish connection to server
let socket = TcpStream::connect("127.0.0.1:8080").await?; let socket = TcpStream::connect("127.0.0.1:8080").await?;
let mut stream = Message::to_stream(socket); let mut stream = Message::to_stream(socket);
let (stream, key) = handshake( // Complete handshake, returning cipher used for encryption
let (stream, cipher) = handshake(
&mut stream, &mut stream,
true, true,
Bytes::from(password.to_string()), Bytes::from(password.to_string()),
@ -36,22 +37,102 @@ pub async fn send(file_paths: &Vec<PathBuf>, password: &String) -> Result<()> {
) )
.await?; .await?;
// return upload_encrypted_files(stream, file_paths, key).await; // Complete file negotiation
let handles = negotiate_files_up(handles, stream, &cipher).await?;
// Upload negotiated files
// Exit
Ok(())
} }
pub async fn receive(password: &String) -> Result<()> { pub async fn receive(password: &String) -> Result<()> {
let socket = TcpStream::connect("127.0.0.1:8080").await?; let socket = TcpStream::connect("127.0.0.1:8080").await?;
let mut stream = Message::to_stream(socket); let mut stream = Message::to_stream(socket);
let (stream, key) = handshake( let (stream, cipher) = handshake(
&mut stream, &mut stream,
false, false,
Bytes::from(password.to_string()), Bytes::from(password.to_string()),
pass_to_bytes(password), pass_to_bytes(password),
) )
.await?; .await?;
let files = negotiate_files_down(stream, &cipher).await?;
return Ok(()); return Ok(());
} }
pub async fn get_file_handles(file_paths: &Vec<PathBuf>) -> Result<Vec<FileHandle>> {
let tasks = file_paths
.into_iter()
.map(|path| FileHandle::new(path.to_path_buf()));
let handles = try_join_all(tasks).await?;
Ok(handles)
}
pub async fn negotiate_files_up(
file_handles: Vec<FileHandle>,
stream: &mut MessageStream,
cipher: &Aes256Gcm,
) -> Result<Vec<FileHandle>> {
let files = file_handles.iter().map(|fh| fh.to_file_info()).collect();
let msg = EncryptedMessage::FileNegotiationMessage(FileNegotiationPayload { files });
let server_msg = msg.to_encrypted_message(cipher)?;
stream.send(server_msg).await?;
let reply_payload = match stream.next().await {
Some(Ok(msg)) => match msg {
Message::EncryptedMessage(response) => response,
_ => return Err(anyhow!("Expecting encrypted message back")),
},
_ => {
return Err(anyhow!("No response to negotiation message"));
}
};
let plaintext_reply = EncryptedMessage::from_encrypted_message(cipher, &reply_payload)?;
let requested_paths: Vec<PathBuf> = match plaintext_reply {
EncryptedMessage::FileNegotiationMessage(fnm) => {
fnm.files.into_iter().map(|f| f.path).collect()
}
_ => return Err(anyhow!("Expecting file negotiation message back")),
};
Ok(file_handles
.into_iter()
.filter(|fh| requested_paths.contains(&fh.path))
.collect())
}
pub async fn negotiate_files_down(stream: &mut MessageStream, cipher: &Aes256Gcm) -> Result<()> {
let file_offer = match stream.next().await {
Some(Ok(msg)) => match msg {
Message::EncryptedMessage(response) => response,
_ => return Err(anyhow!("Expecting encrypted message back")),
},
_ => {
return Err(anyhow!("No response to negotiation message"));
}
};
let plaintext_offer = EncryptedMessage::from_encrypted_message(cipher, &file_offer)?;
let requested_infos: Vec<FileInfo> = match plaintext_offer {
EncryptedMessage::FileNegotiationMessage(fnm) => fnm.files,
_ => return Err(anyhow!("Expecting file negotiation message back")),
};
let mut stdin = FramedRead::new(io::stdin(), LinesCodec::new());
let mut files = vec![];
for path in requested_infos.into_iter() {
let mut reply = prompt_user_input(&mut stdin, &path).await;
while reply.is_none() {
reply = prompt_user_input(&mut stdin, &path).await;
}
match reply {
Some(true) => files.push(path),
_ => {}
}
}
let msg = EncryptedMessage::FileNegotiationMessage(FileNegotiationPayload { files });
let server_msg = msg.to_encrypted_message(cipher)?;
stream.send(server_msg).await?;
Ok(())
}
pub async fn upload_encrypted_files( pub async fn upload_encrypted_files(
stream: &mut MessageStream, stream: &mut MessageStream,
file_paths: &Vec<PathBuf>, file_paths: &Vec<PathBuf>,
@ -59,3 +140,26 @@ pub async fn upload_encrypted_files(
) -> Result<()> { ) -> Result<()> {
return Ok(()); return Ok(());
} }
pub async fn prompt_user_input(
stdin: &mut FramedRead<io::Stdin, LinesCodec>,
file_info: &FileInfo,
) -> Option<bool> {
let prompt_name = file_info.path.file_name().unwrap();
println!(
"Do you want to download {:?}? It's {:?}. (Y/n)",
prompt_name,
to_size_string(file_info.size)
);
match stdin.next().await {
Some(Ok(line)) => match line.as_str() {
"" | "Y" | "y" | "yes" | "Yes" | "YES" => Some(true),
"N" | "n" | "NO" | "no" | "No" => Some(false),
_ => {
println!("Invalid input. Please enter one of the following characters: [YyNn]");
return None;
}
},
_ => None,
}
}

View file

@ -13,7 +13,7 @@ pub async fn handshake(
up: bool, up: bool,
password: Bytes, password: Bytes,
id: Bytes, id: Bytes,
) -> Result<(&mut MessageStream, Bytes)> { ) -> Result<(&mut MessageStream, Aes256Gcm)> {
let (s1, outbound_msg) = let (s1, outbound_msg) =
Spake2::<Ed25519Group>::start_symmetric(&Password::new(password), &Identity::new(&id)); Spake2::<Ed25519Group>::start_symmetric(&Password::new(password), &Identity::new(&id));
println!("client - sending handshake msg"); println!("client - sending handshake msg");
@ -39,34 +39,33 @@ pub async fn handshake(
Err(e) => return Err(anyhow!(e.to_string())), Err(e) => return Err(anyhow!(e.to_string())),
}; };
println!("Handshake successful. Key is {:?}", key); println!("Handshake successful. Key is {:?}", key);
return Ok((stream, Bytes::from(key))); return Ok((stream, new_cipher(&key)));
} }
pub fn new_cypher(key: Bytes) -> Aes256Gcm { pub fn new_cipher(key: &Vec<u8>) -> Aes256Gcm {
let key = Key::from_slice(&key[..]); let key = Key::from_slice(&key[..]);
Aes256Gcm::new(key) Aes256Gcm::new(key)
} }
const NONCE_SIZE_IN_BYTES: usize = 96 / 8; pub const NONCE_SIZE_IN_BYTES: usize = 96 / 8;
pub fn encrypt(cipher: &Aes256Gcm, body: &Vec<u8>) -> Result<EncryptedPayload> {
pub fn encrypt(cipher: &Aes256Gcm, body: Bytes) -> Result<EncryptedPayload> {
let mut arr = [0u8; NONCE_SIZE_IN_BYTES]; let mut arr = [0u8; NONCE_SIZE_IN_BYTES];
thread_rng().try_fill(&mut arr[..])?; thread_rng().try_fill(&mut arr[..])?;
let nonce = Nonce::from_slice(&arr); let nonce = Nonce::from_slice(&arr);
let plaintext = body.as_ref(); let plaintext = body.as_ref();
match cipher.encrypt(nonce, plaintext) { match cipher.encrypt(nonce, plaintext) {
Ok(ciphertext) => Ok(EncryptedPayload { Ok(body) => Ok(EncryptedPayload {
nonce: Bytes::from(&arr[..]), nonce: arr.to_vec(),
body: Bytes::from(ciphertext.as_ref()), body,
}), }),
Err(_) => anyhow!("Encryption error"), Err(_) => Err(anyhow!("Encryption error")),
} }
} }
pub fn decrypt(cipher: &Aes256Gcm, payload: EncryptedPayload) -> Result<Bytes> { pub fn decrypt(cipher: &Aes256Gcm, payload: &EncryptedPayload) -> Result<Bytes> {
let nonce = Nonce::from_slice(payload.nonce.as_ref()); let nonce = Nonce::from_slice(payload.nonce.as_ref());
match cipher.decrypt(nonce, payload.body.as_ref()) { match cipher.decrypt(nonce, payload.body.as_ref()) {
Ok(_) => Ok(Bytes::from("hello")), Ok(payload) => Ok(Bytes::from(payload)),
Err(_) => anyhow!("Decryption error"), Err(_) => Err(anyhow!("Decryption error")),
} }
} }

View file

@ -1,20 +1,19 @@
use anyhow::Result; use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ffi::OsString;
use std::fs::Metadata; use std::fs::Metadata;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::fs::File; use tokio::fs::File;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FileInfo { pub struct FileInfo {
name: OsString, pub path: PathBuf,
size: u64, pub size: u64,
} }
pub struct FileHandle { pub struct FileHandle {
file: File, pub file: File,
md: Metadata, pub md: Metadata,
path: PathBuf, pub path: PathBuf,
} }
impl FileHandle { impl FileHandle {
@ -27,10 +26,7 @@ impl FileHandle {
pub fn to_file_info(&self) -> FileInfo { pub fn to_file_info(&self) -> FileInfo {
FileInfo { FileInfo {
name: match self.path.file_name() { path: self.path.clone(),
Some(s) => s.to_os_string(),
None => OsString::from("Unknown"),
},
size: self.md.len(), size: self.md.len(),
} }
} }
@ -38,7 +34,8 @@ impl FileHandle {
const SUFFIX: [&'static str; 9] = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const SUFFIX: [&'static str; 9] = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
// Stolen: https://gitlab.com/forkbomb9/human_bytes-rs/-/blob/master/src/lib.rs // Stolen: https://gitlab.com/forkbomb9/human_bytes-rs/-/blob/master/src/lib.rs
pub fn to_size_string(size: f64) -> String { pub fn to_size_string(size: u64) -> String {
let size = size as f64;
let base = size.log10() / 1024_f64.log10(); let base = size.log10() / 1024_f64.log10();
let mut result = format!("{:.1}", 1024_f64.powf(base - base.floor()),) let mut result = format!("{:.1}", 1024_f64.powf(base - base.floor()),)
.trim_end_matches(".0") .trim_end_matches(".0")

View file

@ -5,6 +5,9 @@ mod file;
mod message; mod message;
mod server; mod server;
#[cfg(test)]
mod tests;
use clap::Parser; use clap::Parser;
use cli::{Cli, Commands}; use cli::{Cli, Commands};
use client::{receive, send}; use client::{receive, send};

View file

@ -1,3 +1,9 @@
use crate::crypto::{decrypt, encrypt};
use crate::file::FileInfo;
use aes_gcm::Aes256Gcm; // Or `Aes128Gcm`
use anyhow::{anyhow, Result};
use bincode::config;
use bytes::Bytes; use bytes::Bytes;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::error::Error; use std::error::Error;
@ -9,6 +15,7 @@ use tokio_util::codec::{Framed, LengthDelimitedCodec};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Message { pub enum Message {
HandshakeMessage(HandshakePayload), HandshakeMessage(HandshakePayload),
EncryptedMessage(EncryptedPayload),
ErrorMessage(RuckError), ErrorMessage(RuckError),
} }
@ -21,8 +28,43 @@ pub struct HandshakePayload {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EncryptedPayload { pub struct EncryptedPayload {
pub nonce: Bytes, pub nonce: Vec<u8>,
pub body: Bytes, pub body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EncryptedMessage {
FileNegotiationMessage(FileNegotiationPayload),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FileNegotiationPayload {
pub files: Vec<FileInfo>,
}
impl EncryptedMessage {
pub fn from_encrypted_message(cipher: &Aes256Gcm, payload: &EncryptedPayload) -> Result<Self> {
let raw = decrypt(cipher, payload)?;
let res = match bincode::deserialize(raw.as_ref()) {
Ok(result) => result,
Err(e) => {
println!("deserialize error {:?}", e);
return Err(anyhow!("deser error"));
}
};
Ok(res)
}
pub fn to_encrypted_message(&self, cipher: &Aes256Gcm) -> Result<Message> {
let raw = match bincode::serialize(&self) {
Ok(result) => result,
Err(e) => {
println!("serialize error {:?}", e);
return Err(anyhow!("serialize error"));
}
};
let payload = encrypt(cipher, &raw)?;
Ok(Message::EncryptedMessage(payload))
}
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View file

@ -156,9 +156,6 @@ pub async fn handle_connection(
return Ok(()); return Ok(());
} }
}; };
// How do I get this handshake message to the peer
// If it's the sender, the recipient hasn't arrived yet
// If it's the recipient, the sender was created before
println!("server - received msg from {:?}", addr); println!("server - received msg from {:?}", addr);
let mut client = Client::new( let mut client = Client::new(
handshake_payload.up, handshake_payload.up,

11
src/tests.rs Normal file
View file

@ -0,0 +1,11 @@
use crate::crypto::new_cipher;
use crate::file::FileHandle;
use std::path::PathBuf;
#[tokio::test]
async fn test_file_handle_nonexistent_file() {
let pb = PathBuf::new();
let fh = FileHandle::new(pb).await;
assert!(fh.is_err());
}