Create upload command, implement download data decryption logic

This commit is contained in:
Tim Visée 2018-03-20 13:52:59 +01:00
parent dd41dfbacc
commit 5b1724ef9e
No known key found for this signature in database
GPG key ID: A28432A0AE6E6306
10 changed files with 411 additions and 204 deletions

View file

@ -1,39 +1,30 @@
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::path::Path;
use mime_guess::{get_mime_type, Mime};
use openssl::symm::encrypt_aead;
use openssl::symm::{decrypt_aead, encrypt_aead};
use reqwest::{
Client,
Error as ReqwestError,
Request,
};
use reqwest::header::Authorization;
use reqwest::mime::APPLICATION_OCTET_STREAM;
use reqwest::multipart::{Form, Part};
use url::Url;
use serde_json;
use crypto::b64;
use crypto::key_set::KeySet;
use reader::{
EncryptedFileReaderTagged,
ExactLengthReader,
ProgressReader,
ProgressReporter,
};
use file::file::DownloadFile;
use file::metadata::{Metadata, XFileMetadata};
use file::metadata::Metadata;
pub type Result<T> = ::std::result::Result<T, DownloadError>;
/// The name of the header that is used for the authentication nonce.
const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
// TODO: experiment with `iv` of `None` in decrypt logic
/// A file upload action to a Send server.
pub struct Download<'a> {
/// The Send file to download.
file: &DownloadFile,
file: &'a DownloadFile,
}
impl<'a> Download<'a> {
@ -48,7 +39,7 @@ impl<'a> Download<'a> {
pub fn invoke(
self,
client: &Client,
) -> Result<SendFile> {
) -> Result<()> {
// Create a key set for the file
let key = KeySet::from(self.file);
@ -68,7 +59,7 @@ impl<'a> Download<'a> {
// Get the download url, and parse the nonce
// TODO: do not unwrap here, return error
let download_url = file.download_url(false);
let download_url = self.file.download_url(false);
let response = client.get(download_url)
.send()
.expect("failed to get nonce, failed to send file request");
@ -95,158 +86,213 @@ impl<'a> Download<'a> {
.skip(1)
.next()
.expect("missing authentication nonce")
);
).expect("failed to decode authentication nonce");
// TODO: set the input vector
// Determine the signature
// TODO: use a tag length const here
// TODO: do not unwrap, return an error
let mut sig = vec![0u8; 16];
encrypt_aead(
KeySet::cipher(),
key.auth_key().unwrap(),
None,
&[],
&nonce,
&mut sig,
).expect("failed to derive signature");
let sig_encoded = b64::encode(&sig);
// Crpate metadata and a file reader
let metadata = self.create_metadata(&key, &file)?;
let reader = self.create_reader(&key, reporter.clone())?;
let reader_len = reader.len().unwrap();
// Create the request to send
let req = self.create_request(
client,
&key,
metadata,
reader,
);
// Start the reporter
reporter.lock()
.expect("unable to start progress, failed to get lock")
.start(reader_len);
// Execute the request
let result = self.execute_request(req, client, &key);
// Mark the reporter as finished
reporter.lock()
.expect("unable to finish progress, failed to get lock")
.finish();
result
}
/// Create a blob of encrypted metadata.
fn create_metadata(&self, key: &KeySet, file: &FileData)
-> Result<Vec<u8>>
{
// Construct the metadata
let metadata = Metadata::from(
key.iv(),
file.name().to_owned(),
file.mime().clone(),
).to_json().into_bytes();
// Encrypt the metadata
let mut metadata_tag = vec![0u8; 16];
let mut metadata = match encrypt_aead(
KeySet::cipher(),
key.meta_key().unwrap(),
Some(&[0u8; 12]),
&[],
&metadata,
&mut metadata_tag,
) {
Ok(metadata) => metadata,
Err(_) => return Err(DownloadError::EncryptionError),
};
// Append the encryption tag
metadata.append(&mut metadata_tag);
Ok(metadata)
}
/// Create a reader that reads the file as encrypted stream.
fn create_reader(
&self,
key: &KeySet,
reporter: Arc<Mutex<ProgressReporter>>,
) -> Result<EncryptedReader> {
// Open the file
let file = match File::open(self.path.as_path()) {
Ok(file) => file,
Err(_) => return Err(DownloadError::FileError),
};
// Create an encrypted reader
let reader = match EncryptedFileReaderTagged::new(
file,
KeySet::cipher(),
key.file_key().unwrap(),
key.iv(),
) {
Ok(reader) => reader,
Err(_) => return Err(DownloadError::EncryptionError),
};
// Buffer the encrypted reader
let reader = BufReader::new(reader);
// Wrap into the encrypted reader
let mut reader = ProgressReader::new(reader)
.expect("failed to create progress reader");
// Initialize and attach the reporter
reader.set_reporter(reporter);
Ok(reader)
}
/// Build the request that will be send to the server.
fn create_request(
&self,
client: &Client,
key: &KeySet,
metadata: Vec<u8>,
reader: EncryptedReader,
) -> Request {
// Get the reader length
let len = reader.len().expect("failed to get reader length");
// Configure a form to send
let part = Part::reader_with_length(reader, len)
// .file_name(file.name())
.mime(APPLICATION_OCTET_STREAM);
let form = Form::new()
.part("data", part);
// Define the URL to call
let url = self.host.join("api/upload").expect("invalid host");
// Build the request
client.post(url.as_str())
// Get the meta URL, fetch the metadata
// TODO: do not unwrap here, return error
let meta_url = self.file.api_meta_url();
let mut response = client.get(meta_url)
.header(Authorization(
format!("send-v1 {}", key.auth_key_encoded().unwrap())
format!("send-v1 {}", sig_encoded)
))
.header(XFileMetadata::from(&metadata))
.multipart(form)
.build()
.expect("failed to build an API request")
.send()
.expect("failed to fetch metadata, failed to send request");
// Validate the status code
// TODO: allow redirects here?
if !response.status().is_success() {
// TODO: return error here
panic!("failed to fetch metadata, request status is not successful");
}
// Get the metadata nonce
// TODO: don't unwrap here, return an error
let nonce = b64::decode(
response.headers()
.get_raw(HEADER_AUTH_NONCE)
.expect("missing authenticate header")
.one()
.map(|line| String::from_utf8(line.to_vec())
.expect("invalid authentication header contents")
)
.expect("authentication header is empty")
.split_terminator(" ")
.skip(1)
.next()
.expect("missing metadata nonce")
);
// Parse the metadata response
let meta_response: MetadataResponse = response.json()
.expect("failed to parse metadata response");
// Decrypt the metadata
let metadata = meta_response.decrypt_metadata(&key);
println!("GOT METADATA: {:?}", metadata);
// // Crpate metadata and a file reader
// let metadata = self.create_metadata(&key, &file)?;
// let reader = self.create_reader(&key, reporter.clone())?;
// let reader_len = reader.len().unwrap();
// // Create the request to send
// let req = self.create_request(
// client,
// &key,
// metadata,
// reader,
// );
// // Start the reporter
// reporter.lock()
// .expect("unable to start progress, failed to get lock")
// .start(reader_len);
// // Execute the request
// let result = self.execute_request(req, client, &key);
// // Mark the reporter as finished
// reporter.lock()
// .expect("unable to finish progress, failed to get lock")
// .finish();
Ok(())
}
/// Execute the given request, and create a file object that represents the
/// uploaded file.
fn execute_request(&self, req: Request, client: &Client, key: &KeySet)
-> Result<SendFile>
{
// Execute the request
let mut res = match client.execute(req) {
Ok(res) => res,
Err(err) => return Err(DownloadError::RequestError(err)),
};
// /// Create a blob of encrypted metadata.
// fn create_metadata(&self, key: &KeySet, file: &FileData)
// -> Result<Vec<u8>>
// {
// // Construct the metadata
// let metadata = Metadata::from(
// key.iv(),
// file.name().to_owned(),
// file.mime().clone(),
// ).to_json().into_bytes();
// Decode the response
let res: DownloadResponse = match res.json() {
Ok(res) => res,
Err(_) => return Err(DownloadError::DecodeError),
};
// // Encrypt the metadata
// let mut metadata_tag = vec![0u8; 16];
// let mut metadata = match encrypt_aead(
// KeySet::cipher(),
// key.meta_key().unwrap(),
// Some(&[0u8; 12]),
// &[],
// &metadata,
// &mut metadata_tag,
// ) {
// Ok(metadata) => metadata,
// Err(_) => return Err(DownloadError::EncryptionError),
// };
// Transform the responce into a file object
Ok(res.into_file(self.host.clone(), &key))
}
// // Append the encryption tag
// metadata.append(&mut metadata_tag);
// Ok(metadata)
// }
// /// Create a reader that reads the file as encrypted stream.
// fn create_reader(
// &self,
// key: &KeySet,
// reporter: Arc<Mutex<ProgressReporter>>,
// ) -> Result<EncryptedReader> {
// // Open the file
// let file = match File::open(self.path.as_path()) {
// Ok(file) => file,
// Err(_) => return Err(DownloadError::FileError),
// };
// // Create an encrypted reader
// let reader = match EncryptedFileReaderTagged::new(
// file,
// KeySet::cipher(),
// key.file_key().unwrap(),
// key.iv(),
// ) {
// Ok(reader) => reader,
// Err(_) => return Err(DownloadError::EncryptionError),
// };
// // Buffer the encrypted reader
// let reader = BufReader::new(reader);
// // Wrap into the encrypted reader
// let mut reader = ProgressReader::new(reader)
// .expect("failed to create progress reader");
// // Initialize and attach the reporter
// reader.set_reporter(reporter);
// Ok(reader)
// }
// /// Build the request that will be send to the server.
// fn create_request(
// &self,
// client: &Client,
// key: &KeySet,
// metadata: Vec<u8>,
// reader: EncryptedReader,
// ) -> Request {
// // Get the reader length
// let len = reader.len().expect("failed to get reader length");
// // Configure a form to send
// let part = Part::reader_with_length(reader, len)
// // .file_name(file.name())
// .mime(APPLICATION_OCTET_STREAM);
// let form = Form::new()
// .part("data", part);
// // Define the URL to call
// let url = self.host.join("api/upload").expect("invalid host");
// // Build the request
// client.post(url.as_str())
// .header(Authorization(
// format!("send-v1 {}", key.auth_key_encoded().unwrap())
// ))
// .header(XFileMetadata::from(&metadata))
// .multipart(form)
// .build()
// .expect("failed to build an API request")
// }
// /// Execute the given request, and create a file object that represents the
// /// uploaded file.
// fn execute_request(&self, req: Request, client: &Client, key: &KeySet)
// -> Result<SendFile>
// {
// // Execute the request
// let mut res = match client.execute(req) {
// Ok(res) => res,
// Err(err) => return Err(DownloadError::RequestError(err)),
// };
// // Decode the response
// let res: DownloadResponse = match res.json() {
// Ok(res) => res,
// Err(_) => return Err(DownloadError::DecodeError),
// };
// // Transform the responce into a file object
// Ok(res.into_file(self.host.clone(), &key))
// }
}
/// Errors that may occur in the upload action.
@ -270,39 +316,50 @@ pub enum DownloadError {
DecodeError,
}
/// 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.
/// The metadata response from the server, when fetching the data through
/// the API.
///
/// This metadata is required to successfully download and decrypt the
/// corresponding file.
#[derive(Debug, Deserialize)]
struct DownloadResponse {
/// 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,
struct MetadataResponse {
/// The encrypted metadata.
#[serde(rename="metadata")]
meta: String,
}
impl DownloadResponse {
/// Convert this response into a file object.
impl MetadataResponse {
/// Get and decrypt the metadata, based on the raw data in this response.
///
/// The `host` and `key` must be given.
pub fn into_file(self, host: Url, key: &KeySet) -> SendFile {
SendFile::new_now(
self.id,
host,
Url::parse(&self.url)
.expect("upload response URL parse error"),
key.secret().to_vec(),
self.owner,
/// The decrypted data is verified using an included tag.
/// If verification failed, an error is returned.
// TODO: do not unwrap, return a proper error
pub fn decrypt_metadata(&self, key_set: &KeySet) -> Result<Metadata> {
// Decode the metadata
let raw = b64::decode(&self.meta)
.expect("failed to decode metadata from server");
// Get the encrypted metadata, and it's tag
let (encrypted, tag) = raw.split_at(raw.len() - 16);
// TODO: is the tag length correct, remove assert if it is
assert_eq!(tag.len(), 16);
// Decrypt the metadata
// TODO: is the tag verified here?
// TODO: do not unwrap, return an error
let meta = decrypt_aead(
KeySet::cipher(),
key_set.meta_key().unwrap(),
Some(key_set.iv()),
&[],
encrypted,
&tag,
).expect("failed to decrypt metadata");
// Parse the metadata, and return
Ok(
serde_json::from_slice(&meta)
.expect("failed to parse decrypted metadata as JSON")
)
}
}

View file

@ -1,2 +1,2 @@
//pub mod download;
pub mod download;
pub mod upload;

View file

@ -14,11 +14,11 @@ use crypto::b64;
// TODO: match any sub-path?
// TODO: match URL-safe base64 chars for the file ID?
// TODO: constrain the ID length?
const DOWNLOAD_PATH_PATTERN: &'static str = r"$/?download/([[:alnum:]]{8,}={0,3})/?^";
const DOWNLOAD_PATH_PATTERN: &'static str = r"^/?download/([[:alnum:]]{8,}={0,3})/?$";
/// A pattern for Send download URL fragments, capturing the file secret.
// TODO: constrain the secret length?
const DOWNLOAD_FRAGMENT_PATTERN: &'static str = r"$([a-zA-Z0-9-_+\/]+)?\s*^";
const DOWNLOAD_FRAGMENT_PATTERN: &'static str = r"^([a-zA-Z0-9-_+/]+)?\s*$";
/// A struct representing an uploaded file on a Send host.
///
@ -159,20 +159,13 @@ impl DownloadFile {
/// If the URL fragmet contains a file secret, it is also parsed.
/// If it does not, the secret is left empty and must be specified
/// manually.
pub fn parse_url(url: String) -> Result<DownloadFile, FileParseError> {
// Try to parse as an URL
let url = Url::parse(&url)
.map_err(|err| FileParseError::UrlFormatError(err))?;
pub fn parse_url(url: Url) -> Result<DownloadFile, FileParseError> {
// Build the host
let mut host = url.clone();
host.set_fragment(None);
host.set_query(None);
host.set_path("");
// TODO: remove this after debugging
println!("DEBUG: Extracted host: {}", host);
// Validate the path, get the file ID
let re_path = Regex::new(DOWNLOAD_PATH_PATTERN).unwrap();
let id = re_path.captures(url.path())
@ -243,8 +236,19 @@ impl DownloadFile {
url
}
/// Get the API metadata URL of the file.
pub fn api_meta_url(&self) -> Url {
// Get the download URL, and add the secret fragment
let mut url = self.url.clone();
url.set_path(format!("/api/metadata/{}", self.id).as_str());
url.set_fragment(None);
url
}
}
#[derive(Debug)]
pub enum FileParseError {
/// An URL format error.
UrlFormatError(UrlParseError),

View file

@ -14,7 +14,7 @@ use serde_json;
use crypto::b64;
/// File metadata, which is send to the server.
#[derive(Serialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct Metadata {
/// The input vector.
iv: String,