mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-05 10:19:23 +02:00
Implement file download and decrypt logic
This commit is contained in:
parent
7e22c07d72
commit
9eb9462c40
11 changed files with 271 additions and 95 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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)",
|
||||
|
|
|
@ -5,6 +5,7 @@ authors = ["timvisee <timvisee@gmail.com>"]
|
|||
workspace = ".."
|
||||
|
||||
[dependencies]
|
||||
arrayref = "0.3"
|
||||
base64 = "0.9"
|
||||
chrono = "0.4"
|
||||
hkdf = "0.3"
|
||||
|
|
|
@ -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)?;
|
||||
|
@ -161,6 +212,9 @@ impl<'a> Download<'a> {
|
|||
// .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
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#[macro_use]
|
||||
extern crate arrayref;
|
||||
extern crate mime_guess;
|
||||
extern crate openssl;
|
||||
pub extern crate reqwest;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
// TODO: open the file, or it's location
|
||||
// TODO: copy the file location
|
||||
|
||||
// // 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");
|
||||
// }
|
||||
|
||||
// // 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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue