Add API action to change file password

This commit is contained in:
timvisee 2018-03-28 20:55:36 +02:00
parent fa4654d31d
commit f04a6bfc26
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
6 changed files with 241 additions and 3 deletions

View file

@ -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
api/src/action/password.rs Normal file
View file

@ -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),
}

View file

@ -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(),

View file

@ -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

View file

@ -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)]