Start implementing upload logic in an update module

This commit is contained in:
timvisee 2018-03-08 22:36:57 +01:00
parent a4ccf395b9
commit fdb5a5a8ac
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
11 changed files with 180 additions and 161 deletions

8
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<u8>) -> File {
File::new_now(
self.id,
host,
self.url,
secret,
self.owner,
)
}
}

View file

@ -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<Vec<u8>, 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<Vec<u8>, DecodeError> {
// base64::decode_config(input, base64::URL_SAFE_NO_PAD)
// }

View file

@ -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 {}

View file

@ -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<u8>> {
self.meta_key.as_ref()
}
/// Get the cipher type to use in combination with these keys.
pub fn cipher() -> Cipher {
Cipher::aes_128_gcm()
}
}

View file

@ -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;

View file

@ -215,3 +215,6 @@ impl Read for EncryptedFileReaderTagged {
Ok(self.read(buf)? + total)
}
}
// TODO: implement this some other way
unsafe impl Send for EncryptedFileReaderTagged {}

View file

@ -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<u8>,
owner_key: String,
) -> Self {
File {
Self {
id,
time,
host,

153
src/send/upload.rs Normal file
View file

@ -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<T> = ::std::result::Result<T, UploadError>;
/// 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<Path>,
}
impl Upload {
/// Construct a new upload action.
pub fn new(host: Url, path: Box<Path>) -> Self {
Self {
host,
path,
}
}
/// Invoke the upload action.
pub fn invoke(self) -> Result<SendFile> {
// 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<u8>) -> SendFile {
SendFile::new_now(
self.id,
host,
self.url,
secret,
self.owner,
)
}
}