1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
use std::fs::File;
use std::io::{self, Error as IoError, Read, Write};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use reqwest::header::AUTHORIZATION;
use reqwest::{Client, Response};

use super::metadata::{Error as MetadataError, Metadata as MetadataAction, MetadataResponse};
use crate::api::request::{ensure_success, ResponseError};
use crate::api::url::UrlBuilder;
use crate::api::Version;
use crate::crypto::key_set::KeySet;
use crate::crypto::sig::signature_encoded;
use crate::file::remote_file::RemoteFile;
#[cfg(feature = "send3")]
use crate::pipe::crypto::EceCrypt;
#[cfg(feature = "send2")]
use crate::pipe::crypto::GcmCrypt;
use crate::pipe::{
    progress::{ProgressPipe, ProgressReporter},
    prelude::*,
};

/// A file download action to a Send server.
///
/// This action is compatible with both Firefox Send v2 and v3, but the server API version to use
/// must be explicitly given due to a version specific download method.
pub struct Download<'a> {
    /// The server API version to use when downloading the file.
    version: Version,

    /// The remote file to download.
    file: &'a RemoteFile,

    /// The target file or directory, to download the file to.
    target: PathBuf,

    /// An optional password to decrypt a protected file.
    password: Option<String>,

    /// Check whether the file exists (recommended).
    check_exists: bool,

    /// The metadata response to work with,
    /// which will skip the internal metadata request.
    metadata_response: Option<MetadataResponse>,
}

impl<'a> Download<'a> {
    /// Construct a new download action for the given remote file.
    /// It is recommended to check whether the file exists,
    /// unless that is already done.
    pub fn new(
        version: Version,
        file: &'a RemoteFile,
        target: PathBuf,
        password: Option<String>,
        check_exists: bool,
        metadata_response: Option<MetadataResponse>,
    ) -> Self {
        Self {
            version,
            file,
            target,
            password,
            check_exists,
            metadata_response,
        }
    }

    /// Invoke the download action.
    pub fn invoke(
        mut self,
        client: &Client,
        reporter: Option<Arc<Mutex<ProgressReporter>>>,
    ) -> Result<(), Error> {
        // Create a key set for the file
        let mut key = KeySet::from(self.file, self.password.as_ref());

        // Get the metadata, or fetch the file metadata,
        // then update the input vector in the key set
        let metadata: MetadataResponse = if self.metadata_response.is_some() {
            self.metadata_response.take().unwrap()
        } else {
            MetadataAction::new(self.file, self.password.clone(), self.check_exists)
                .invoke(&client)?
        };

        // Set the input vector if known, depending on the API version
        if let Some(iv) = metadata.metadata().iv() {
            key.set_iv(iv);
        }

        // Decide what actual file target to use
        let path = self.decide_path(metadata.metadata().name());
        let path_str = path.to_str().unwrap_or("?").to_owned();

        // Open the file we will write to
        // TODO: this should become a temporary file first
        // TODO: use the uploaded file name as default
        let out = File::create(path)
            .map_err(|err| Error::File(path_str.clone(), FileError::Create(err)))?;

        // Create the file reader for downloading
        let (reader, len) = self.create_file_reader(&key, &metadata, &client)?;

        // Create the file writer
        let writer = self
            .create_writer(out, len, &key, reporter.clone())
            .map_err(|err| Error::File(path_str.clone(), err))?;

        // Download the file
        self.download(reader, writer, len, reporter)?;

        // TODO: return the file path
        // TODO: return the new remote state (does it still exist remote)

        Ok(())
    }

    /// Decide what path we will download the file to.
    ///
    /// A target file or directory, and a file name hint must be given.
    /// The name hint can be derived from the retrieved metadata on this file.
    ///
    /// The name hint is used as file name, if a directory was given.
    fn decide_path(&self, name_hint: &str) -> PathBuf {
        // Return the target if it is an existing file
        if self.target.is_file() {
            return self.target.clone();
        }

        // Append the name hint if this is a directory
        if self.target.is_dir() {
            return self.target.join(name_hint);
        }

        // Return if the parent is an existing directory
        if self.target.parent().map(|p| p.is_dir()).unwrap_or(false) {
            return self.target.clone();
        }

        // TODO: are these todos below already implemented in CLI client?
        // TODO: canonicalize the path when possible
        // TODO: allow using `file.toml` as target without directory indication
        // TODO: return a nice error here as the path may be invalid
        // TODO: maybe prompt the user to create the directory
        panic!("Invalid (non-existing) output path given, not yet supported");
    }

    /// Make a download request, and create a reader that downloads the
    /// encrypted file.
    ///
    /// The response representing the file reader is returned along with the
    /// length of the reader content.
    fn create_file_reader(
        &self,
        key: &KeySet,
        metadata: &MetadataResponse,
        client: &Client,
    ) -> Result<(Response, u64), DownloadError> {
        // Compute the cryptographic signature
        let sig = signature_encoded(key.auth_key().unwrap(), metadata.nonce())
            .map_err(|_| DownloadError::ComputeSignature)?;

        // Build and send the download request
        let response = client
            .get(UrlBuilder::api_download(self.file))
            .header(AUTHORIZATION.as_str(), format!("send-v1 {}", sig))
            .send()
            .map_err(|_| DownloadError::Request)?;

        // Ensure the response is successful
        ensure_success(&response).map_err(DownloadError::Response)?;

        // Get the content length
        // TODO: make sure there is enough disk space
        let len = metadata.size();

        Ok((response, len))
    }

    /// Create a file writer.
    ///
    /// This writer will will decrypt the input on the fly, and writes the
    /// decrypted data to the given file.
    fn create_writer(
        &self,
        file: File,
        len: u64,
        key: &KeySet,
        reporter: Option<Arc<Mutex<ProgressReporter>>>,
    ) -> Result<impl Write, FileError> {
        // Build the decrypting file writer for the selected server API version
        let writer: Box<dyn Write> = match self.version {
            #[cfg(feature = "send2")]
            Version::V2 => {
                let decrypt = GcmCrypt::decrypt(len as usize, key.file_key().unwrap(), key.iv());
                let writer = decrypt.writer(Box::new(file));
                Box::new(writer)
            }
            #[cfg(feature = "send3")]
            Version::V3 => {
                let ikm = key.secret().to_vec();
                let decrypt = EceCrypt::decrypt(len as usize, ikm);
                let writer = decrypt.writer(Box::new(file));
                Box::new(writer)
            }
        };

        // Build the progress pipe file writer
        let progress = ProgressPipe::zero(len as u64, reporter);
        let writer = progress.writer(writer);

        Ok(writer)
    }

    /// Download the file from the reader, and write it to the writer.
    /// The length of the file must also be given.
    /// The status will be reported to the given progress reporter.
    fn download<R, W>(
        &self,
        mut reader: R,
        mut writer: W,
        len: u64,
        reporter: Option<Arc<Mutex<ProgressReporter>>>,
    ) -> Result<(), DownloadError>
        where R: Read,
              W: Write,
    {
        // Start the writer
        if let Some(reporter) = reporter.as_ref() {
            reporter
                .lock()
                .map_err(|_| DownloadError::Progress)?
                .start(len);
        }

        // Write to the output file
        io::copy(&mut reader, &mut writer).map_err(|_| DownloadError::Download)?;

        // Finish
        if let Some(reporter) = reporter.as_ref() {
            reporter
                .lock()
                .map_err(|_| DownloadError::Progress)?
                .finish();
        }

        Ok(())
    }
}

#[derive(Fail, Debug)]
pub enum Error {
    /// An error occurred while fetching the metadata of the file.
    /// This step is required in order to succsessfully decrypt the
    /// file that will be downloaded.
    #[fail(display = "failed to fetch file metadata")]
    Meta(#[cause] MetadataError),

    /// The given Send file has expired, or did never exist in the first place.
    /// Therefore the file could not be downloaded.
    #[fail(display = "the file has expired or did never exist")]
    Expired,

    /// A password is required, but was not given.
    #[fail(display = "missing password, password required")]
    PasswordRequired,

    /// An error occurred while downloading the file.
    #[fail(display = "failed to download the file")]
    Download(#[cause] DownloadError),

    /// An error occurred while decrypting the downloaded file.
    #[fail(display = "failed to decrypt the downloaded file")]
    Decrypt,

    /// An error occurred while opening or writing to the target file.
    // TODO: show what file this is about
    #[fail(display = "couldn't use the target file at '{}'", _0)]
    File(String, #[cause] FileError),
}

impl From<MetadataError> for Error {
    fn from(err: MetadataError) -> Error {
        match err {
            MetadataError::Expired => Error::Expired,
            MetadataError::PasswordRequired => Error::PasswordRequired,
            err => Error::Meta(err),
        }
    }
}

impl From<DownloadError> for Error {
    fn from(err: DownloadError) -> Error {
        Error::Download(err)
    }
}

#[derive(Fail, Debug)]
pub enum DownloadError {
    /// An error occurred while computing the cryptographic signature used for
    /// downloading the file.
    #[fail(display = "failed to compute cryptographic signature")]
    ComputeSignature,

    /// Sending the request to download the file failed.
    #[fail(display = "failed to request file download")]
    Request,

    /// The server responded with an error while requesting the file download.
    #[fail(display = "bad response from server while requesting download")]
    Response(#[cause] ResponseError),

    /// Failed to start or update the downloading progress, because of this the
    /// download can't continue.
    #[fail(display = "failed to update download progress")]
    Progress,

    /// The actual download and decryption process the server.
    /// This covers reading the file from the server, decrypting the file,
    /// and writing it to the file system.
    #[fail(display = "failed to download the file")]
    Download,

    // /// Verifying the downloaded file failed.
    // #[fail(display = "file verification failed")]
    // Verify,
}

#[derive(Fail, Debug)]
pub enum FileError {
    /// An error occurred while creating or opening the file to write to.
    #[fail(display = "failed to create or replace the file")]
    Create(#[cause] IoError),

    /// Failed to create an encrypted writer for the file, which is used to
    /// decrypt the downloaded file.
    #[fail(display = "failed to create file decryptor")]
    EncryptedWriter,
}