From fdb5a5a8ac13f93a309edc4abc02877fb77cb55c Mon Sep 17 00:00:00 2001 From: timvisee Date: Thu, 8 Mar 2018 22:36:57 +0100 Subject: [PATCH] Start implementing upload logic in an update module --- Cargo.lock | 8 -- Cargo.toml | 2 - src/action/upload.rs | 39 -------- src/b64.rs | 10 +- src/main.rs | 108 +------------------- src/send/key_set.rs | 7 ++ src/{ => send}/metadata.rs | 0 src/send/mod.rs | 5 +- src/{ => send}/reader.rs | 3 + src/send/{file.rs => send_file.rs} | 6 +- src/send/upload.rs | 153 +++++++++++++++++++++++++++++ 11 files changed, 180 insertions(+), 161 deletions(-) rename src/{ => send}/metadata.rs (100%) rename src/{ => send}/reader.rs (98%) rename src/send/{file.rs => send_file.rs} (97%) create mode 100644 src/send/upload.rs diff --git a/Cargo.lock b/Cargo.lock index 482e565..bfd18c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,7 +185,6 @@ dependencies = [ "clap 2.31.1 (registry+https://github.com/rust-lang/crates.io-index)", "hkdf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.11.22 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 2.0.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", "open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "openssl 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -195,7 +194,6 @@ dependencies = [ "serde_json 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "version-compare 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -988,11 +986,6 @@ name = "vec_map" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -[[package]] -name = "version-compare" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "version_check" version = "0.1.3" @@ -1155,7 +1148,6 @@ dependencies = [ "checksum uuid 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" "checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b" "checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" -"checksum version-compare 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "78068add8bf1e4d37d13fa5867182fe4c03f8e525c831053733f83aaba942d37" "checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" "checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" diff --git a/Cargo.toml b/Cargo.toml index d7ae975..248aa80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ chrono = "0.4" clap = "2.31" hkdf = "0.3" hyper = "0.11.9" # same as reqwest -lazy_static = "1.0" mime_guess = "2.0.0-alpha.2" open = "1" openssl = "0.10" @@ -19,4 +18,3 @@ serde_derive = "1.0" serde_json = "1.0" sha2 = "0.7" url = "1.7" -version-compare = "0.0" diff --git a/src/action/upload.rs b/src/action/upload.rs index efe9531..e69de29 100644 --- a/src/action/upload.rs +++ b/src/action/upload.rs @@ -1,39 +0,0 @@ -use super::super::url::Url; - -use super::super::send::file::File; - -/// The response from the server after a file has been uploaded. -/// This response contains the file ID and owner key, to manage the file. -/// -/// It also contains the download URL, although an additional secret is -/// required. -/// -/// The download URL can be generated using `download_url()` which will -/// include the required secret in the URL. -#[derive(Debug, Deserialize)] -pub struct UploadResponse { - /// The file ID. - id: String, - - /// The URL the file is reachable at. - /// This includes the file ID, but does not include the secret. - url: String, - - /// The owner key, used to do further file modifications. - owner: String, -} - -impl UploadResponse { - /// Convert this response into a file object. - /// - /// The `host` and `secret` must be given. - pub fn into_file(self, host: Url, secret: Vec) -> File { - File::new_now( - self.id, - host, - self.url, - secret, - self.owner, - ) - } -} diff --git a/src/b64.rs b/src/b64.rs index d02eb04..51bc71c 100644 --- a/src/b64.rs +++ b/src/b64.rs @@ -6,14 +6,14 @@ extern crate base64; -use self::base64::DecodeError; +// use self::base64::DecodeError; /// Encode the given byte slice using base64, in an URL-safe manner. pub fn encode(input: &[u8]) -> String { base64::encode_config(input, base64::URL_SAFE_NO_PAD) } -/// Decode the given string as base64, in an URL-safe manner. -pub fn decode(input: &str) -> Result, DecodeError> { - base64::decode_config(input, base64::URL_SAFE_NO_PAD) -} +// /// Decode the given string as base64, in an URL-safe manner. +// pub fn decode(input: &str) -> Result, DecodeError> { +// base64::decode_config(input, base64::URL_SAFE_NO_PAD) +// } diff --git a/src/main.rs b/src/main.rs index 60d4da8..231807f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ extern crate hyper; -extern crate lazy_static; extern crate mime_guess; extern crate open; extern crate openssl; @@ -13,26 +12,11 @@ mod app; mod b64; mod cmd; mod crypto; -mod metadata; -mod reader; mod send; mod util; -use std::fs::File; -use std::io::BufReader; -use std::path::Path; - -use openssl::symm::{Cipher, encrypt_aead}; -use reqwest::header::Authorization; -use reqwest::mime::APPLICATION_OCTET_STREAM; -use reqwest::multipart::Part; - -use action::upload::UploadResponse; use cmd::Handler; use cmd::cmd_upload::CmdUpload; -use metadata::{Metadata, XFileMetadata}; -use reader::EncryptedFileReaderTagged; -use send::key_set::KeySet; /// Application entrypoint. fn main() { @@ -61,92 +45,10 @@ fn invoke_action(handler: &Handler) { /// The upload action. fn action_upload(cmd_upload: &CmdUpload) { - // Get the path and host - let path = Path::new(cmd_upload.file()); - let host = cmd_upload.host(); + // // Get the path and host + // let path = Path::new(cmd_upload.file()); + // let host = cmd_upload.host(); - // Make sure the path is a file - if !path.is_file() { - panic!("The selected path is not a file"); - } - - // TODO: a fixed path for now, as upload test - let file_ext = path.extension().unwrap().to_str().unwrap(); - let file_name = path.file_name().unwrap().to_str().unwrap().to_owned(); - - // Create a new reqwest client - let client = reqwest::Client::new(); - - // Generate a key - let key = KeySet::generate(true); - - // Guess the mimetype of the file - let file_mime = mime_guess::get_mime_type(file_ext); - - // Construct the metadata - let metadata = Metadata::from(key.iv(), file_name.clone(), file_mime); - - // Convert the metadata to JSON bytes - let metadata = metadata.to_json().into_bytes(); - - // Choose a file and meta cipher type - let cipher = Cipher::aes_128_gcm(); - - // Encrypt the metadata, and append the tag to it - let mut metadata_tag = vec![0u8; 16]; - let mut metadata = encrypt_aead( - cipher, - key.meta_key().unwrap(), - Some(&[0u8; 12]), - &[], - &metadata, - &mut metadata_tag, - ).unwrap(); - metadata.append(&mut metadata_tag); - - // Open the file and create an encrypted file reader - let file = File::open(path).unwrap(); - let reader = EncryptedFileReaderTagged::new( - file, - cipher, - key.file_key().unwrap(), - key.iv(), - ).unwrap(); - - // Buffer the encrypted reader, and determine the length - let reader_len = reader.len().unwrap(); - let reader = BufReader::new(reader); - - // Build the file part, configure the form to send - let part = Part::reader_with_length(reader, reader_len) - .file_name(file_name) - .mime(APPLICATION_OCTET_STREAM); - let form = reqwest::multipart::Form::new() - .part("data", part); - - // Make the request - // TODO: properly format an URL here - let url = host.join("api/upload").expect("invalid host"); - let mut res = client.post(url.as_str()) - .header(Authorization(format!("send-v1 {}", key.auth_key_encoded().unwrap()))) - .header(XFileMetadata::from(&metadata)) - .multipart(form) - .send() - .unwrap(); - - // Parse the response - let upload_res: UploadResponse = res.json().unwrap(); - - // Print the response - let file = upload_res.into_file(host, key.secret().to_vec()); - let url = file.download_url(); - println!("File: {:#?}", file); - println!("Secret key: {}", key.secret_encoded()); - println!("Download URL: {}", url); - - // Open the URL in the browser - open::that(url).expect("failed to open URL"); + // // Open the URL in the browser + // open::that(url).expect("failed to open URL"); } - -// TODO: implement this some other way -unsafe impl Send for EncryptedFileReaderTagged {} diff --git a/src/send/key_set.rs b/src/send/key_set.rs index 0876836..3d0a372 100644 --- a/src/send/key_set.rs +++ b/src/send/key_set.rs @@ -1,3 +1,5 @@ +use super::super::openssl::symm::Cipher; + use b64; use crypto::{derive_auth_key, derive_file_key, derive_meta_key, rand_bytes}; @@ -98,4 +100,9 @@ impl KeySet { pub fn meta_key(&self) -> Option<&Vec> { self.meta_key.as_ref() } + + /// Get the cipher type to use in combination with these keys. + pub fn cipher() -> Cipher { + Cipher::aes_128_gcm() + } } diff --git a/src/metadata.rs b/src/send/metadata.rs similarity index 100% rename from src/metadata.rs rename to src/send/metadata.rs diff --git a/src/send/mod.rs b/src/send/mod.rs index 9c5096c..6b0f12e 100644 --- a/src/send/mod.rs +++ b/src/send/mod.rs @@ -3,5 +3,8 @@ //! This API client may be used to upload, download, modify or delete files //! to and from a secure Firefox Send server. -pub mod file; pub mod key_set; +pub mod metadata; +pub mod reader; +pub mod send_file; +pub mod upload; diff --git a/src/reader.rs b/src/send/reader.rs similarity index 98% rename from src/reader.rs rename to src/send/reader.rs index 65025d7..3a16d5e 100644 --- a/src/reader.rs +++ b/src/send/reader.rs @@ -215,3 +215,6 @@ impl Read for EncryptedFileReaderTagged { Ok(self.read(buf)? + total) } } + +// TODO: implement this some other way +unsafe impl Send for EncryptedFileReaderTagged {} diff --git a/src/send/file.rs b/src/send/send_file.rs similarity index 97% rename from src/send/file.rs rename to src/send/send_file.rs index 846bc59..c611b24 100644 --- a/src/send/file.rs +++ b/src/send/send_file.rs @@ -10,7 +10,7 @@ use super::super::b64; /// The struct contains the file ID, the file URL, the key that is required /// in combination with the file, and the owner key. #[derive(Debug)] -pub struct File { +pub struct SendFile { /// The ID of the file on that server. id: String, @@ -30,7 +30,7 @@ pub struct File { owner_key: String, } -impl File { +impl SendFile { /// Construct a new file. pub fn new( id: String, @@ -40,7 +40,7 @@ impl File { secret: Vec, owner_key: String, ) -> Self { - File { + Self { id, time, host, diff --git a/src/send/upload.rs b/src/send/upload.rs new file mode 100644 index 0000000..d45005d --- /dev/null +++ b/src/send/upload.rs @@ -0,0 +1,153 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +use super::super::mime_guess::get_mime_type; +use super::super::openssl::symm::encrypt_aead; +use super::super::reqwest; +use super::super::reqwest::header::Authorization; +use super::super::reqwest::mime::APPLICATION_OCTET_STREAM; +use super::super::reqwest::multipart::Part; +use super::super::url::Url; + +use super::key_set::KeySet; +use super::metadata::{Metadata, XFileMetadata}; +use super::reader::EncryptedFileReaderTagged; +use super::send_file::SendFile; + +pub type Result = ::std::result::Result; + +/// A file upload action to a Send server. +pub struct Upload { + /// The Send host to upload the file to. + host: Url, + + /// The file to upload. + path: Box, +} + +impl Upload { + /// Construct a new upload action. + pub fn new(host: Url, path: Box) -> Self { + Self { + host, + path, + } + } + + /// Invoke the upload action. + pub fn invoke(self) -> Result { + // Make sure the given path is a file + if !self.path.is_file() { + return Err(UploadError::NotAFile); + } + + // Grab some file details + let file_ext = self.path.extension().unwrap().to_str().unwrap(); + let file_name = self.path.file_name().unwrap().to_str().unwrap().to_owned(); + let file_mime = get_mime_type(file_ext); + + // Generate a key set + let key = KeySet::generate(true); + + // Construct the metadata + let metadata = Metadata::from(key.iv(), file_name.clone(), file_mime) + .to_json() + .into_bytes(); + + // Encrypt the metadata, and append the tag to it + let mut metadata_tag = vec![0u8; 16]; + let mut metadata = encrypt_aead( + KeySet::cipher(), + key.meta_key().unwrap(), + Some(&[0u8; 12]), + &[], + &metadata, + &mut metadata_tag, + ).unwrap(); + metadata.append(&mut metadata_tag); + + // Open the file and create an encrypted file reader + let file = File::open(&self.path).unwrap(); + let reader = EncryptedFileReaderTagged::new( + file, + KeySet::cipher(), + key.file_key().unwrap(), + key.iv(), + ).unwrap(); + + // Buffer the encrypted reader, and determine the length + let reader_len = reader.len().unwrap(); + let reader = BufReader::new(reader); + + // Build the file part, configure the form to send + let part = Part::reader_with_length(reader, reader_len) + .file_name(file_name) + .mime(APPLICATION_OCTET_STREAM); + let form = reqwest::multipart::Form::new() + .part("data", part); + + // Create a new reqwest client + let client = reqwest::Client::new(); + + // Make the request + // TODO: properly format an URL here + let url = self.host.join("api/upload").expect("invalid host"); + let mut res = client.post(url.as_str()) + .header(Authorization(format!("send-v1 {}", key.auth_key_encoded().unwrap()))) + .header(XFileMetadata::from(&metadata)) + .multipart(form) + .send() + .unwrap(); + + // Parse the response + let upload_res: UploadResponse = res.json().unwrap(); + + // Print the response + Ok( + upload_res.into_file(self.host, key.secret().to_vec()) + ) + } +} + +pub enum UploadError { + /// The given file is not not an existing file. + /// Maybe it is a directory, or maybe it doesn't exist. + NotAFile, +} + +/// The response from the server after a file has been uploaded. +/// This response contains the file ID and owner key, to manage the file. +/// +/// It also contains the download URL, although an additional secret is +/// required. +/// +/// The download URL can be generated using `download_url()` which will +/// include the required secret in the URL. +#[derive(Debug, Deserialize)] +pub struct UploadResponse { + /// The file ID. + id: String, + + /// The URL the file is reachable at. + /// This includes the file ID, but does not include the secret. + url: String, + + /// The owner key, used to do further file modifications. + owner: String, +} + +impl UploadResponse { + /// Convert this response into a file object. + /// + /// The `host` and `secret` must be given. + pub fn into_file(self, host: Url, secret: Vec) -> SendFile { + SendFile::new_now( + self.id, + host, + self.url, + secret, + self.owner, + ) + } +}