Implement file download and decrypt logic

This commit is contained in:
timvisee 2018-03-20 22:03:34 +01:00
parent 7e22c07d72
commit 9eb9462c40
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
11 changed files with 271 additions and 95 deletions

1
Cargo.lock generated
View file

@ -242,6 +242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
name = "ffsend-api" name = "ffsend-api"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hkdf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "hkdf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -5,6 +5,7 @@ authors = ["timvisee <timvisee@gmail.com>"]
workspace = ".." workspace = ".."
[dependencies] [dependencies]
arrayref = "0.3"
base64 = "0.9" base64 = "0.9"
chrono = "0.4" chrono = "0.4"
hkdf = "0.3" hkdf = "0.3"

View file

@ -1,12 +1,13 @@
use std::path::Path; use std::fs::File;
use std::io;
use mime_guess::{get_mime_type, Mime};
use openssl::symm::decrypt_aead; use openssl::symm::decrypt_aead;
use reqwest::{ use reqwest::{
Client, Client,
Error as ReqwestError, Error as ReqwestError,
}; };
use reqwest::header::Authorization; use reqwest::header::Authorization;
use reqwest::header::ContentLength;
use serde_json; use serde_json;
use crypto::b64; use crypto::b64;
@ -14,6 +15,7 @@ use crypto::key_set::KeySet;
use crypto::sign::signature_encoded; use crypto::sign::signature_encoded;
use file::file::DownloadFile; use file::file::DownloadFile;
use file::metadata::Metadata; use file::metadata::Metadata;
use reader::EncryptedFileWriter;
pub type Result<T> = ::std::result::Result<T, DownloadError>; pub type Result<T> = ::std::result::Result<T, DownloadError>;
@ -42,7 +44,7 @@ impl<'a> Download<'a> {
client: &Client, client: &Client,
) -> Result<()> { ) -> Result<()> {
// Create a key set for the file // Create a key set for the file
let key = KeySet::from(self.file); let mut key = KeySet::from(self.file);
// Build the meta cipher // Build the meta cipher
// let mut metadata_tag = vec![0u8; 16]; // let mut metadata_tag = vec![0u8; 16];
@ -92,7 +94,7 @@ impl<'a> Download<'a> {
// Compute the cryptographic signature // Compute the cryptographic signature
// TODO: do not unwrap, return an error // TODO: do not unwrap, return an error
let sig = signature_encoded(key.auth_key().unwrap(), &nonce) let sig = signature_encoded(key.auth_key().unwrap(), &nonce)
.expect("failed to compute signature"); .expect("failed to compute metadata signature");
// Get the meta URL, fetch the metadata // Get the meta URL, fetch the metadata
// TODO: do not unwrap here, return error // TODO: do not unwrap here, return error
@ -126,14 +128,63 @@ impl<'a> Download<'a> {
.skip(1) .skip(1)
.next() .next()
.expect("missing metadata nonce") .expect("missing metadata nonce")
); ).expect("failed to decode metadata nonce");
// Parse the metadata response // Parse the metadata response
let meta_response: MetadataResponse = response.json() let meta_response: MetadataResponse = response.json()
.expect("failed to parse metadata response"); .expect("failed to parse metadata response");
// Decrypt the metadata // Decrypt the metadata, set the input vector
let metadata = meta_response.decrypt_metadata(&key); let metadata = meta_response.decrypt_metadata(&key)
.expect("failed to decrypt metadata");
key.set_iv(metadata.iv());
// Compute the cryptographic signature
// TODO: do not unwrap, return an error
let sig = signature_encoded(key.auth_key().unwrap(), &nonce)
.expect("failed to compute file signature");
// Get the download URL, build the download request
// TODO: do not unwrap here, return error
let download_url = self.file.api_download_url();
let mut response = client.get(download_url)
.header(Authorization(
format!("send-v1 {}", sig)
))
.send()
.expect("failed to fetch file, 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 file, request status is not successful");
}
// Get the content length
let response_len = response.headers().get::<ContentLength>()
.expect("failed to fetch file, missing content length header")
.0;
// Open a file to write to
// TODO: this should become a temporary file first
let out = File::create("downloaded.toml")
.expect("failed to open file");
let mut writer = EncryptedFileWriter::new(
out,
response_len as usize,
KeySet::cipher(),
key.file_key().unwrap(),
key.iv(),
).expect("failed to create encrypted writer");
// Write to the output file
io::copy(&mut response, &mut writer)
.expect("failed to download and decrypt file");
// Verify the writer
// TODO: delete the file if verification failed, show a proper error
assert!(writer.verified(), "downloaded and decrypted file could not be verified");
// // Crpate metadata and a file reader // // Crpate metadata and a file reader
// let metadata = self.create_metadata(&key, &file)?; // let metadata = self.create_metadata(&key, &file)?;
@ -160,6 +211,9 @@ impl<'a> Download<'a> {
// reporter.lock() // reporter.lock()
// .expect("unable to finish progress, failed to get lock") // .expect("unable to finish progress, failed to get lock")
// .finish(); // .finish();
// TODO: return the file path
// TODO: return the new remote state (does it still exist remote)
Ok(()) Ok(())
} }
@ -352,53 +406,3 @@ impl MetadataResponse {
) )
} }
} }
// /// A struct that holds various file properties, such as it's name and it's
// /// mime type.
// struct FileData<'a> {
// /// The file name.
// name: &'a str,
// /// The file mime type.
// mime: Mime,
// }
// impl<'a> FileData<'a> {
// /// Create a file data object, from the file at the given path.
// pub fn from(path: Box<&'a Path>) -> Result<Self> {
// // Make sure the given path is a file
// if !path.is_file() {
// return Err(DownloadError::NotAFile);
// }
// // Get the file name
// let name = match path.file_name() {
// Some(name) => name.to_str().expect("failed to convert string"),
// None => return Err(DownloadError::FileError),
// };
// // Get the file extention
// // TODO: handle cases where the file doesn't have an extention
// let ext = match path.extension() {
// Some(ext) => ext.to_str().expect("failed to convert string"),
// None => return Err(DownloadError::FileError),
// };
// Ok(
// Self {
// name,
// mime: get_mime_type(ext),
// }
// )
// }
// /// Get the file name.
// pub fn name(&self) -> &str {
// self.name
// }
// /// Get the file mime type.
// pub fn mime(&self) -> &Mime {
// &self.mime
// }
// }

View file

@ -17,7 +17,7 @@ use url::Url;
use crypto::key_set::KeySet; use crypto::key_set::KeySet;
use reader::{ use reader::{
EncryptedFileReaderTagged, EncryptedFileReader,
ExactLengthReader, ExactLengthReader,
ProgressReader, ProgressReader,
ProgressReporter, ProgressReporter,
@ -25,7 +25,7 @@ use reader::{
use file::file::File as SendFile; use file::file::File as SendFile;
use file::metadata::{Metadata, XFileMetadata}; use file::metadata::{Metadata, XFileMetadata};
type EncryptedReader = ProgressReader<BufReader<EncryptedFileReaderTagged>>; type EncryptedReader = ProgressReader<BufReader<EncryptedFileReader>>;
pub type Result<T> = ::std::result::Result<T, UploadError>; pub type Result<T> = ::std::result::Result<T, UploadError>;
/// A file upload action to a Send server. /// A file upload action to a Send server.
@ -129,7 +129,7 @@ impl Upload {
}; };
// Create an encrypted reader // Create an encrypted reader
let reader = match EncryptedFileReaderTagged::new( let reader = match EncryptedFileReader::new(
file, file,
KeySet::cipher(), KeySet::cipher(),
key.file_key().unwrap(), key.file_key().unwrap(),

View file

@ -104,6 +104,11 @@ impl KeySet {
&self.iv &self.iv
} }
/// Set the input vector.
pub fn set_iv(&mut self, iv: [u8; KEY_IV_LEN]) {
self.iv = iv;
}
/// Get the file encryption key, if derived. /// Get the file encryption key, if derived.
pub fn file_key(&self) -> Option<&Vec<u8>> { pub fn file_key(&self) -> Option<&Vec<u8>> {
self.file_key.as_ref() self.file_key.as_ref()

View file

@ -246,6 +246,16 @@ impl DownloadFile {
url url
} }
/// Get the API download URL of the file.
pub fn api_download_url(&self) -> Url {
// Get the download URL, and add the secret fragment
let mut url = self.url.clone();
url.set_path(format!("/api/download/{}", self.id).as_str());
url.set_fragment(None);
url
}
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -46,6 +46,16 @@ impl Metadata {
pub fn to_json(&self) -> String { pub fn to_json(&self) -> String {
serde_json::to_string(&self).unwrap() serde_json::to_string(&self).unwrap()
} }
/// Get the input vector
// TODO: use an input vector length from a constant
pub fn iv(&self) -> [u8; 12] {
// Decode the input vector
let decoded = b64::decode_url(&self.iv).unwrap();
// Create a sized array
*array_ref!(decoded, 0, 12)
}
} }
/// A X-File-Metadata header for reqwest, that is used to pass encrypted /// A X-File-Metadata header for reqwest, that is used to pass encrypted

View file

@ -1,3 +1,5 @@
#[macro_use]
extern crate arrayref;
extern crate mime_guess; extern crate mime_guess;
extern crate openssl; extern crate openssl;
pub extern crate reqwest; pub extern crate reqwest;

View file

@ -1,4 +1,4 @@
use std::cmp::min; use std::cmp::{max, min};
use std::fs::File; use std::fs::File;
use std::io::{ use std::io::{
self, self,
@ -6,6 +6,7 @@ use std::io::{
Cursor, Cursor,
Error as IoError, Error as IoError,
Read, Read,
Write,
}; };
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -18,6 +19,8 @@ use openssl::symm::{
/// The length in bytes of crytographic tags that are used. /// The length in bytes of crytographic tags that are used.
const TAG_LEN: usize = 16; const TAG_LEN: usize = 16;
// TODO: create a generic reader/writer wrapper for the the encryptor/decryptor.
/// A lazy file reader, that encrypts the file with the given `cipher` /// A lazy file reader, that encrypts the file with the given `cipher`
/// and appends the cryptographic tag to the end of it. /// and appends the cryptographic tag to the end of it.
/// ///
@ -30,7 +33,7 @@ const TAG_LEN: usize = 16;
/// The reader uses a small internal buffer as data is encrypted in blocks, /// The reader uses a small internal buffer as data is encrypted in blocks,
/// which may output more data than fits in the given buffer while reading. /// which may output more data than fits in the given buffer while reading.
/// The excess data is then returned on the next read. /// The excess data is then returned on the next read.
pub struct EncryptedFileReaderTagged { pub struct EncryptedFileReader {
/// The raw file that is read from. /// The raw file that is read from.
file: File, file: File,
@ -50,7 +53,7 @@ pub struct EncryptedFileReaderTagged {
internal_buf: Vec<u8>, internal_buf: Vec<u8>,
} }
impl EncryptedFileReaderTagged { impl EncryptedFileReader {
/// Construct a new reader for the given `file` with the given `cipher`. /// Construct a new reader for the given `file` with the given `cipher`.
/// ///
/// This method consumes twice the size of the file in memory while /// This method consumes twice the size of the file in memory while
@ -72,7 +75,7 @@ impl EncryptedFileReaderTagged {
// Construct the encrypted reader // Construct the encrypted reader
Ok( Ok(
EncryptedFileReaderTagged { EncryptedFileReader {
file, file,
cipher, cipher,
crypter, crypter,
@ -180,7 +183,7 @@ impl EncryptedFileReaderTagged {
} }
} }
impl ExactLengthReader for EncryptedFileReaderTagged { impl ExactLengthReader for EncryptedFileReader {
/// Calculate the total length of the encrypted file with the appended /// Calculate the total length of the encrypted file with the appended
/// tag. /// tag.
/// Useful in combination with some progress monitor, to determine how much /// Useful in combination with some progress monitor, to determine how much
@ -191,7 +194,7 @@ impl ExactLengthReader for EncryptedFileReaderTagged {
} }
/// The reader trait implementation. /// The reader trait implementation.
impl Read for EncryptedFileReaderTagged { impl Read for EncryptedFileReader {
/// Read from the encrypted file, and then the encryption tag. /// Read from the encrypted file, and then the encryption tag.
fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> { fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
// Read from the internal buffer, return full or splice to empty // Read from the internal buffer, return full or splice to empty
@ -226,7 +229,7 @@ impl Read for EncryptedFileReaderTagged {
} }
// TODO: implement this some other way // TODO: implement this some other way
unsafe impl Send for EncryptedFileReaderTagged {} unsafe impl Send for EncryptedFileReader {}
/// A reader wrapper, that measures the reading process for a reader with a /// A reader wrapper, that measures the reading process for a reader with a
/// known length. /// known length.
@ -341,3 +344,168 @@ impl<R: ExactLengthReader> ExactLengthReader for BufReader<R> {
self.get_ref().len() self.get_ref().len()
} }
} }
/// A lazy file writer, that decrypt the file with the given `cipher`
/// and verifies it with the tag appended to the end of the input data.
///
/// This writer is lazy because the input data is decrypted and written to the
/// specified file on the fly, instead of buffering all the data first.
/// This greatly reduces memory usage for large files.
///
/// The length of the input data (including the appended tag) must be given
/// when this reader is initialized. When all data including the tag is read,
/// the decrypted data is verified with the tag. If the tag doesn't match the
/// decrypted data, a write error is returned on the last write.
/// This writer will never write more bytes than the length initially
/// specified.
///
/// This reader encrypts the input data with the given key and input vector.
///
/// A failed writing implies that no data could be written, or that the data
/// wasn't successfully decrypted because of an decryption or tag matching
/// error. Such a fail means that the file will be incomplete or corrupted,
/// and should therefore be removed from the disk.
///
/// It is highly recommended to invoke the `verified()` method after writing
/// the file, to ensure the written file is indeed complete and fully verified.
pub struct EncryptedFileWriter {
/// The file to write the decrypted data to.
file: File,
/// The number of bytes that have currently been written to this writer.
cur: usize,
/// The length of all the data, which includes the file data and the
/// appended tag.
len: usize,
/// The cipher type used for decrypting.
cipher: Cipher,
/// The crypter used for decrypting the data.
crypter: Crypter,
/// A buffer for the tag.
tag_buf: Vec<u8>,
/// A boolean that defines whether the decrypted data has successfully
/// been verified.
verified: bool,
}
impl EncryptedFileWriter {
/// Construct a new encrypted file writer.
///
/// The file to write to must be given to `file`, which must be open for
/// writing. The total length of the input data in bytes must be given to
/// `len`, which includes both the file bytes and the appended tag.
///
/// For decryption, a `cipher`, `key` and `iv` must also be given.
pub fn new(file: File, len: usize, cipher: Cipher, key: &[u8], iv: &[u8])
-> Result<Self, io::Error>
{
// Build the crypter
let crypter = Crypter::new(
cipher,
CrypterMode::Decrypt,
key,
Some(iv),
)?;
// Construct the encrypted reader
Ok(
EncryptedFileWriter {
file,
cur: 0,
len,
cipher,
crypter,
tag_buf: Vec::with_capacity(TAG_LEN),
verified: false,
}
)
}
/// Check wheher the complete tag is buffered.
pub fn has_tag(&self) -> bool {
self.tag_buf.len() >= TAG_LEN
}
/// Check whether the decrypted data is succesfsully verified.
///
/// If this method returns true the following is implied:
/// - The complete file has been written.
/// - The complete file was successfully decrypted.
/// - The included tag matches the decrypted file.
///
/// It is highly recommended to invoke this method and check the
/// verification after writing the file using this writer.
pub fn verified(&self) -> bool {
self.verified
}
}
/// The writer trait implementation.
impl Write for EncryptedFileWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
// Do not write anything if the tag was already written
if self.verified() || self.has_tag() {
return Ok(0);
}
// Determine how many file and tag bytes we still need to process
let file_bytes = max(self.len - TAG_LEN - self.cur, 0);
let tag_bytes = TAG_LEN - self.tag_buf.len();
// Split the input buffer
let (file_buf, tag_buf) = buf.split_at(min(file_bytes, buf.len()));
// Read from the file buf
if !file_buf.is_empty() {
// Create a decrypted buffer, with the proper size
let block_size = self.cipher.block_size();
let mut decrypted = vec![0u8; file_bytes + block_size];
// Decrypt bytes
// TODO: catch error in below statement
let len = self.crypter.update(
file_buf,
&mut decrypted,
)?;
decrypted.truncate(len);
// Write to the file
self.file.write_all(&decrypted)?;
}
// Read from the tag part to fill the tag buffer
if !tag_buf.is_empty() {
self.tag_buf.extend(tag_buf.iter().take(tag_bytes));
}
// Verify the tag once it has been buffered completely
if self.has_tag() {
// Set the tag
self.crypter.set_tag(&self.tag_buf)?;
// Create a buffer for any remaining data
let block_size = self.cipher.block_size();
let mut extra = vec![0u8; block_size];
// Finalize, write all remaining data
let len = self.crypter.finalize(&mut extra)?;
extra.truncate(len);
self.file.write_all(&extra)?;
// Set the verified flag
self.verified = true;
}
// Compute how many bytes were written
Ok(file_bytes - file_buf.len() + min(tag_buf.len(), tag_bytes))
}
fn flush(&mut self) -> Result<(), io::Error> {
self.file.flush()
}
}

View file

@ -1,15 +1,8 @@
use std::path::Path;
use std::sync::{Arc, Mutex};
use ffsend_api::action::download::Download as ApiDownload; use ffsend_api::action::download::Download as ApiDownload;
use ffsend_api::file::file::DownloadFile; use ffsend_api::file::file::DownloadFile;
use ffsend_api::reqwest::Client; use ffsend_api::reqwest::Client;
use cmd::cmd_download::CmdDownload; use cmd::cmd_download::CmdDownload;
use progress::ProgressBar;
use util::open_url;
#[cfg(feature = "clipboard")]
use util::set_clipboard;
/// A file download action. /// A file download action.
pub struct Download<'a> { pub struct Download<'a> {
@ -41,26 +34,9 @@ impl<'a> Download<'a> {
// TODO: do not unwrap, but return an error // TODO: do not unwrap, but return an error
ApiDownload::new(&file).invoke(&client).unwrap(); ApiDownload::new(&file).invoke(&client).unwrap();
// // Get the download URL, and report it in the console // TODO: open the file, or it's location
// let url = file.download_url(true); // TODO: copy the file location
// println!("Download URL: {}", url);
// // Open the URL in the browser println!("Download complete");
// if self.cmd.open() {
// // TODO: do not expect, but return an error
// open_url(url.clone()).expect("failed to open URL");
// }
// // Copy the URL in the user's clipboard
// #[cfg(feature = "clipboard")]
// {
// if self.cmd.copy() {
// // TODO: do not expect, but return an error
// set_clipboard(url.as_str().to_owned())
// .expect("failed to put download URL in user clipboard");
// }
// }
panic!("DONE");
} }
} }

View file

@ -2,7 +2,6 @@ use ffsend_api::url::{ParseError, Url};
use super::clap::{App, Arg, ArgMatches, SubCommand}; use super::clap::{App, Arg, ArgMatches, SubCommand};
use app::SEND_DEF_HOST;
use util::quit_error; use util::quit_error;
/// The download command. /// The download command.