瀏覽代碼

Add API action to change file password

timvisee 7 年之前
父節點
當前提交
f04a6bfc26
共有 6 個文件被更改,包括 241 次插入3 次删除
  1. 8 0
      IDEAS.md
  2. 2 2
      api/src/action/download.rs
  3. 209 0
      api/src/action/password.rs
  4. 1 1
      api/src/crypto/hdkf.rs
  5. 11 0
      api/src/crypto/key_set.rs
  6. 10 0
      api/src/file/file.rs

+ 8 - 0
IDEAS.md

@@ -1,4 +1,5 @@
 # Ideas
+- Rename DownloadFile to RemoteFile
 - Box errors
 - Implement error handling everywhere properly
 - `-y` flag for assume yes
@@ -6,18 +7,25 @@
 - Quick upload/download without `upload` or `download` subcommands.
 - Set file password
 - Set file download count
+- Flag to explicitly delete file after download
+- Check remote version and heartbeat using `/__version__`
+- Check whether the file still exists everywhere
+- API actions contain duplicate code, create centralized functions
 - Download to a temporary location first
 - Soft limit uploads to 1GB and 2GB
 - Allow piping input/output files
 - Allow file renaming on upload
 - Allow file/directory archiving on upload
 - Allow unarchiving on download 
+- Allow hiding the progress bar, and/or showing simple progress
 - Enter password through pinetry
 - Remember all uploaded files, make files listable
 - Incognito mode, to not remember files `--incognito`
+- Document all code components
 - Dotfile for default properties
 - Host configuration file for host tags, to easily upload to other hosts
 - Generate manual pages
 - Automated releases through CI
 - Release binaries on GitHub
 - Ubuntu PPA package
+- Move API URL generator methods out of remote file class

+ 2 - 2
api/src/action/download.rs

@@ -298,7 +298,7 @@ impl<'a> Download<'a> {
 
 /// 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)]
@@ -486,7 +486,7 @@ pub enum DownloadError {
     #[fail(display = "Failed to download the file")]
     Download,
 
-    /// Verifiying the downloaded file failed.
+    /// Verifying the downloaded file failed.
     #[fail(display = "File verification failed")]
     Verify,
 }

+ 209 - 0
api/src/action/password.rs

@@ -0,0 +1,209 @@
+// TODO: define redirect policy
+
+use std::io::{
+    self,
+    Error as IoError,
+    Read,
+};
+use std::sync::{Arc, Mutex};
+
+use failure::Error as FailureError;
+use openssl::symm::decrypt_aead;
+use reqwest::{Client, Response, StatusCode};
+use reqwest::header::Authorization;
+use reqwest::header::ContentLength;
+use serde_json;
+
+use crypto::b64;
+use crypto::key_set::KeySet;
+use crypto::sig::signature_encoded;
+use ext::status_code::StatusCodeExt;
+use file::file::DownloadFile;
+use file::metadata::Metadata;
+use reader::{EncryptedFileWriter, ProgressReporter, ProgressWriter};
+
+/// An action to change a password of an uploaded Send file.
+pub struct Password<'a> {
+    /// The uploaded file to change the password for.
+    file: &'a DownloadFile,
+
+    /// The new password.
+    password: &'a str,
+}
+
+impl<'a> Password<'a> {
+    /// Construct a new password action for the given file.
+    pub fn new(file: &'a DownloadFile, password: &'a str) -> Self {
+        Self {
+            file,
+            password,
+        }
+    }
+
+    /// Invoke the password action.
+    // TODO: allow passing an optional existing authentication nonce
+    pub fn invoke(self, client: &Client) -> Result<(), Error> {
+        // Create a key set for the file
+        let mut key = KeySet::from(self.file);
+
+        // Fetch the authentication nonce
+        let auth_nonce = self.fetch_auth_nonce(client)?;
+
+        // Compute a signature
+        let sig = signature_encoded(key.auth_key().unwrap(), &auth_nonce)
+            .map_err(|_| PrepareError::ComputeSignature)?;
+
+        // Derive a new authentication key
+        key.derive_auth_password(self.password, self.file.download_url(true));
+
+        // Send the request to change the password
+        change_password(client, &key, sig)
+    }
+
+    /// Fetch the authentication nonce for the file from the Send server.
+    fn fetch_auth_nonce(&self, client: &Client)
+        -> Result<Vec<u8>, AuthError>
+    {
+        // Get the download URL, and parse the nonce
+        let download_url = self.file.download_url(false);
+        let response = client.get(download_url)
+            .send()
+            .map_err(|_| AuthError::NonceReq)?;
+
+        // Validate the status code
+        let status = response.status();
+        if !status.is_success() {
+            // TODO: should we check here whether a 404 is returned?
+            // // Handle expired files
+            // if status == FILE_EXPIRED_STATUS {
+            //     return Err(Error::Expired);
+            // } else {
+            return Err(AuthError::NonceReqStatus(status, status.err_text()).into());
+            // }
+        }
+
+        // Get the authentication nonce
+        b64::decode(
+            response.headers()
+                .get_raw(HEADER_AUTH_NONCE)
+                .ok_or(AuthError::NoNonceHeader)?
+                .one()
+                .ok_or(AuthError::MalformedNonce)
+                .and_then(|line| String::from_utf8(line.to_vec())
+                    .map_err(|_| AuthError::MalformedNonce)
+                )?
+                .split_terminator(" ")
+                .skip(1)
+                .next()
+                .ok_or(AuthError::MalformedNonce)?
+        ).map_err(|_| AuthError::MalformedNonce.into())
+    }
+
+    /// Send the request for changing the file password.
+    fn change_password(
+        &self,
+        client: &Client,
+        key: &KeySet,
+        sig: String,
+    ) -> Result<Vec<u8>, ChangeError> {
+        // Get the password URL, and send the change
+        let url = self.file.api_password_url();
+        let response = client.post(url)
+            .json(PasswordData::from(&key))
+            .header(Authorization(
+                format!("send-v1 {}", sig)
+            ))
+            .send()
+            .map_err(|_| ChangeError::Request)?;
+
+        // Validate the status code
+        let status = response.status();
+        if !status.is_success() {
+            return Err(ChangeError::RequestStatus(status, status.err_text()).into());
+        }
+
+        Ok(())
+    }
+}
+
+/// The data object to send to the password endpoint,
+/// which sets the file password.
+#[derive(Debug, Serializable)]
+struct PasswordData {
+    /// The authentication key
+    auth: String,
+}
+
+impl PasswordData {
+    /// Create the password data object from the given key set.
+    pub fn from(key: &KeySet) -> PasswordData {
+        PasswordData {
+            // TODO: do not unwrap here
+            auth: key.auth_key_encoded().unwrap(),
+        }
+    }
+}
+
+#[derive(Fail, Debug)]
+pub enum Error {
+    /// An error occurred while preparing the action.
+    #[fail(display = "Failed to prepare setting the password")]
+    Prepare(#[cause] PrepareError),
+
+    // /// 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,
+
+    /// An error has occurred while sending the password change request to
+    /// the server.
+    #[fail(display = "Failed to send the password change request")]
+    Change(#[cause] ChangeError),
+}
+
+#[derive(Fail, Debug)]
+pub enum PrepareError {
+    /// Failed authenticating, needed to set a new password.
+    #[fail(display = "Failed to authenticate")]
+    Auth(#[cause] AuthError),
+
+    /// An error occurred while computing the cryptographic signature.
+    #[fail(display = "Failed to compute cryptographic signature")]
+    ComputeSignature,
+}
+
+#[derive(Fail, Debug)]
+pub enum AuthError {
+    /// Sending the request to gather the authentication encryption nonce
+    /// failed.
+    #[fail(display = "Failed to request authentication nonce")]
+    NonceReq,
+
+    /// The response for fetching the authentication encryption nonce
+    /// indicated an error and wasn't successful.
+    #[fail(display = "Bad HTTP response '{}' while requesting authentication nonce", _1)]
+    NonceReqStatus(StatusCode, String),
+
+    /// No authentication encryption nonce was included in the response
+    /// from the server, it was missing.
+    #[fail(display = "Missing authentication nonce in server response")]
+    NoNonceHeader,
+
+    /// The authentication encryption nonce from the response malformed or
+    /// empty.
+    /// Maybe the server responded with a new format that isn't supported yet
+    /// by this client.
+    #[fail(display = "Received malformed authentication nonce")]
+    MalformedNonce,
+}
+
+#[derive(Fail, Debug)]
+pub enum ChangeError {
+    /// Sending the request to change the password failed.
+    #[fail(display = "Failed to send password change request")]
+    Request,
+
+    /// The response for changing the password indicated an error and wasn't successful.
+    #[fail(display = "Bad HTTP response '{}' while changing the password", _1)]
+    RequestStatus(StatusCode, String),
+}

+ 1 - 1
api/src/crypto/hdkf.rs

@@ -53,7 +53,7 @@ pub fn derive_meta_key(secret: &[u8]) -> Vec<u8> {
 ///
 /// A `password` and `url` may be given for special key deriving.
 /// At this time this is not implemented however.
-pub fn derive_auth_key(secret: &[u8], password: Option<String>, url: Option<Url>) -> Vec<u8> {
+pub fn derive_auth_key(secret: &[u8], password: Option<&str>, url: Option<&Url>) -> Vec<u8> {
     // Nothing, or both a password and URL must be given
     assert_eq!(
         password.is_none(),

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

@@ -1,4 +1,5 @@
 use openssl::symm::Cipher;
+use url::Url;
 
 use file::file::DownloadFile;
 use super::{b64, rand_bytes};
@@ -89,6 +90,16 @@ impl KeySet {
         self.meta_key = Some(derive_meta_key(&self.secret));
     }
 
+    /// Derive an authentication key, with the given password and file URL.
+    /// This method does not derive a (new) file and metadata key.
+    pub fn derive_auth_password(&mut self, pass: &str, url: &Url) {
+        self.auth_key = Some(derive_auth_key(
+            &self.secret,
+            Some(pass),
+            Some(url),
+        ));
+    }
+
     /// Get the secret key.
     pub fn secret(&self) -> &[u8] {
         &self.secret

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

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