Browse Source

Implement file download and decrypt logic

timvisee 7 years ago
parent
commit
9eb9462c40

+ 1 - 0
Cargo.lock

@@ -242,6 +242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 name = "ffsend-api"
 version = "0.1.0"
 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)",
  "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)",

+ 1 - 0
api/Cargo.toml

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

+ 61 - 57
api/src/action/download.rs

@@ -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 reqwest::{
     Client, 
     Error as ReqwestError,
 };
 use reqwest::header::Authorization;
+use reqwest::header::ContentLength;
 use serde_json;
 
 use crypto::b64;
@@ -14,6 +15,7 @@ use crypto::key_set::KeySet;
 use crypto::sign::signature_encoded;
 use file::file::DownloadFile;
 use file::metadata::Metadata;
+use reader::EncryptedFileWriter;
 
 pub type Result<T> = ::std::result::Result<T, DownloadError>;
 
@@ -42,7 +44,7 @@ impl<'a> Download<'a> {
         client: &Client,
     ) -> Result<()> {
         // Create a key set for the file
-        let key = KeySet::from(self.file);
+        let mut key = KeySet::from(self.file);
 
         // Build the meta cipher
         // let mut metadata_tag = vec![0u8; 16];
@@ -92,7 +94,7 @@ impl<'a> Download<'a> {
         // Compute the cryptographic signature
         // TODO: do not unwrap, return an error
         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
         // TODO: do not unwrap here, return error
@@ -126,14 +128,63 @@ impl<'a> Download<'a> {
                 .skip(1)
                 .next()
                 .expect("missing metadata nonce")
-        );
+        ).expect("failed to decode 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);
+        // Decrypt the metadata, set the input vector
+        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
         // let metadata = self.create_metadata(&key, &file)?;
@@ -160,6 +211,9 @@ impl<'a> Download<'a> {
         // reporter.lock()
         //     .expect("unable to finish progress, failed to get lock")
         //     .finish();
+        
+        // TODO: return the file path
+        // TODO: return the new remote state (does it still exist remote)
 
         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
-//     }
-// }

+ 3 - 3
api/src/action/upload.rs

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

+ 5 - 0
api/src/crypto/key_set.rs

@@ -104,6 +104,11 @@ impl KeySet {
         &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.
     pub fn file_key(&self) -> Option<&Vec<u8>> {
         self.file_key.as_ref()

+ 10 - 0
api/src/file/file.rs

@@ -246,6 +246,16 @@ impl DownloadFile {
 
         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)]

+ 10 - 0
api/src/file/metadata.rs

@@ -46,6 +46,16 @@ impl Metadata {
     pub fn to_json(&self) -> String {
         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

+ 2 - 0
api/src/lib.rs

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

+ 175 - 7
api/src/reader.rs

@@ -1,4 +1,4 @@
-use std::cmp::min;
+use std::cmp::{max, min};
 use std::fs::File;
 use std::io::{
     self,
@@ -6,6 +6,7 @@ use std::io::{
     Cursor,
     Error as IoError,
     Read,
+    Write,
 };
 use std::sync::{Arc, Mutex};
 
@@ -18,6 +19,8 @@ use openssl::symm::{
 /// The length in bytes of crytographic tags that are used.
 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`
 /// 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,
 /// which may output more data than fits in the given buffer while reading.
 /// The excess data is then returned on the next read.
-pub struct EncryptedFileReaderTagged {
+pub struct EncryptedFileReader {
     /// The raw file that is read from.
     file: File,
 
@@ -50,7 +53,7 @@ pub struct EncryptedFileReaderTagged {
     internal_buf: Vec<u8>,
 }
 
-impl EncryptedFileReaderTagged {
+impl EncryptedFileReader {
     /// Construct a new reader for the given `file` with the given `cipher`.
     ///
     /// This method consumes twice the size of the file in memory while
@@ -72,7 +75,7 @@ impl EncryptedFileReaderTagged {
 
         // Construct the encrypted reader
         Ok(
-            EncryptedFileReaderTagged {
+            EncryptedFileReader {
                 file,
                 cipher,
                 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
     /// tag.
     /// Useful in combination with some progress monitor, to determine how much
@@ -191,7 +194,7 @@ impl ExactLengthReader for EncryptedFileReaderTagged {
 }
 
 /// The reader trait implementation.
-impl Read for EncryptedFileReaderTagged {
+impl Read for EncryptedFileReader {
     /// Read from the encrypted file, and then the encryption tag.
     fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
         // 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
-unsafe impl Send for EncryptedFileReaderTagged {}
+unsafe impl Send for EncryptedFileReader {}
 
 /// A reader wrapper, that measures the reading process for a reader with a
 /// known length.
@@ -341,3 +344,168 @@ impl<R: ExactLengthReader> ExactLengthReader for BufReader<R> {
         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()
+    }
+}

+ 3 - 27
cli/src/action/download.rs

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

+ 0 - 1
cli/src/cmd/cmd_download.rs

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