mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-06 10:39:57 +02:00
Add API action to change file password
This commit is contained in:
parent
fa4654d31d
commit
f04a6bfc26
6 changed files with 241 additions and 3 deletions
8
IDEAS.md
8
IDEAS.md
|
@ -1,4 +1,5 @@
|
||||||
# Ideas
|
# Ideas
|
||||||
|
- Rename DownloadFile to RemoteFile
|
||||||
- Box errors
|
- Box errors
|
||||||
- Implement error handling everywhere properly
|
- Implement error handling everywhere properly
|
||||||
- `-y` flag for assume yes
|
- `-y` flag for assume yes
|
||||||
|
@ -6,18 +7,25 @@
|
||||||
- Quick upload/download without `upload` or `download` subcommands.
|
- Quick upload/download without `upload` or `download` subcommands.
|
||||||
- Set file password
|
- Set file password
|
||||||
- Set file download count
|
- 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
|
- Download to a temporary location first
|
||||||
- Soft limit uploads to 1GB and 2GB
|
- Soft limit uploads to 1GB and 2GB
|
||||||
- Allow piping input/output files
|
- Allow piping input/output files
|
||||||
- Allow file renaming on upload
|
- Allow file renaming on upload
|
||||||
- Allow file/directory archiving on upload
|
- Allow file/directory archiving on upload
|
||||||
- Allow unarchiving on download
|
- Allow unarchiving on download
|
||||||
|
- Allow hiding the progress bar, and/or showing simple progress
|
||||||
- Enter password through pinetry
|
- Enter password through pinetry
|
||||||
- Remember all uploaded files, make files listable
|
- Remember all uploaded files, make files listable
|
||||||
- Incognito mode, to not remember files `--incognito`
|
- Incognito mode, to not remember files `--incognito`
|
||||||
|
- Document all code components
|
||||||
- Dotfile for default properties
|
- Dotfile for default properties
|
||||||
- Host configuration file for host tags, to easily upload to other hosts
|
- Host configuration file for host tags, to easily upload to other hosts
|
||||||
- Generate manual pages
|
- Generate manual pages
|
||||||
- Automated releases through CI
|
- Automated releases through CI
|
||||||
- Release binaries on GitHub
|
- Release binaries on GitHub
|
||||||
- Ubuntu PPA package
|
- Ubuntu PPA package
|
||||||
|
- Move API URL generator methods out of remote file class
|
||||||
|
|
|
@ -486,7 +486,7 @@ pub enum DownloadError {
|
||||||
#[fail(display = "Failed to download the file")]
|
#[fail(display = "Failed to download the file")]
|
||||||
Download,
|
Download,
|
||||||
|
|
||||||
/// Verifiying the downloaded file failed.
|
/// Verifying the downloaded file failed.
|
||||||
#[fail(display = "File verification failed")]
|
#[fail(display = "File verification failed")]
|
||||||
Verify,
|
Verify,
|
||||||
}
|
}
|
||||||
|
|
209
api/src/action/password.rs
Normal file
209
api/src/action/password.rs
Normal 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),
|
||||||
|
}
|
|
@ -53,7 +53,7 @@ pub fn derive_meta_key(secret: &[u8]) -> Vec<u8> {
|
||||||
///
|
///
|
||||||
/// A `password` and `url` may be given for special key deriving.
|
/// A `password` and `url` may be given for special key deriving.
|
||||||
/// At this time this is not implemented however.
|
/// 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
|
// Nothing, or both a password and URL must be given
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
password.is_none(),
|
password.is_none(),
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use openssl::symm::Cipher;
|
use openssl::symm::Cipher;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use file::file::DownloadFile;
|
use file::file::DownloadFile;
|
||||||
use super::{b64, rand_bytes};
|
use super::{b64, rand_bytes};
|
||||||
|
@ -89,6 +90,16 @@ impl KeySet {
|
||||||
self.meta_key = Some(derive_meta_key(&self.secret));
|
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.
|
/// Get the secret key.
|
||||||
pub fn secret(&self) -> &[u8] {
|
pub fn secret(&self) -> &[u8] {
|
||||||
&self.secret
|
&self.secret
|
||||||
|
|
|
@ -256,6 +256,16 @@ impl DownloadFile {
|
||||||
|
|
||||||
url
|
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)]
|
#[derive(Debug)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue