Add API action to fetch remote file metadata

This commit is contained in:
timvisee 2018-04-02 18:12:42 +02:00
parent 6ccba568dd
commit 2e4803848b
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
3 changed files with 316 additions and 0 deletions

View file

@ -40,3 +40,4 @@
- Rename host to server?
- Read and write files from and to stdin and stdout with `-` as file
- Ask to add MIME extension to downloaded files without one on Windows
- Fetch max file size from `server/jsconfig.js`

314
api/src/action/metadata.rs Normal file
View file

@ -0,0 +1,314 @@
// TODO: define redirect policy
use failure::Error as FailureError;
use openssl::symm::decrypt_aead;
use reqwest::{Client, StatusCode};
use reqwest::header::Authorization;
use serde_json;
use crypto::b64;
use crypto::key_set::KeySet;
use crypto::sig::signature_encoded;
use ext::status_code::StatusCodeExt;
use file::metadata::Metadata as MetadataData;
use file::remote_file::RemoteFile;
/// The name of the header that is used for the authentication nonce.
const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
/// The HTTP status code that is returned for expired files.
const FILE_EXPIRED_STATUS: StatusCode = StatusCode::NotFound;
/// An action to fetch file metadata.
pub struct Metadata<'a> {
/// The remote file to fetch the metadata for.
file: &'a RemoteFile,
/// An optional password to decrypt a protected file.
password: Option<String>,
}
impl<'a> Metadata<'a> {
/// Construct a new metadata action.
pub fn new(file: &'a RemoteFile, password: Option<String>) -> Self {
Self {
file,
password,
}
}
/// Invoke the metadata action.
pub fn invoke(self, client: &Client) -> Result<MetadataResponse, Error> {
// Create a key set for the file
let mut key = KeySet::from(self.file, self.password.as_ref());
// Fetch the authentication nonce
let auth_nonce = self.fetch_auth_nonce(client)?;
// Fetch the metadata and the metadata nonce, return the result
self.fetch_metadata(&client, &mut key, auth_nonce)
.map_err(|err| err.into())
}
/// Fetch the authentication nonce for the file from the Send server.
fn fetch_auth_nonce(&self, client: &Client)
-> Result<Vec<u8>, Error>
{
// 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() {
// 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())
}
/// Create a metadata nonce, and fetch the metadata for the file from the
/// Send server.
///
/// The key set, along with the authentication nonce must be given.
///
/// The metadata, with the meta nonce is returned.
fn fetch_metadata(
&self,
client: &Client,
key: &KeySet,
auth_nonce: Vec<u8>,
) -> Result<MetadataResponse, MetaError> {
// Compute the cryptographic signature for authentication
let sig = signature_encoded(key.auth_key().unwrap(), &auth_nonce)
.map_err(|_| MetaError::ComputeSignature)?;
// Build the request, fetch the encrypted metadata
let mut response = client.get(self.file.api_meta_url())
.header(Authorization(
format!("send-v1 {}", sig)
))
.send()
.map_err(|_| MetaError::NonceReq)?;
// Validate the status code
let status = response.status();
if !status.is_success() {
return Err(MetaError::NonceReqStatus(status, status.err_text()));
}
// Get the metadata nonce
let nonce = b64::decode(
response.headers()
.get_raw(HEADER_AUTH_NONCE)
.ok_or(MetaError::NoNonceHeader)?
.one()
.ok_or(MetaError::MalformedNonce)
.and_then(|line| String::from_utf8(line.to_vec())
.map_err(|_| MetaError::MalformedNonce)
)?
.split_terminator(" ")
.skip(1)
.next()
.ok_or(MetaError::MalformedNonce)?
).map_err(|_| MetaError::MalformedNonce)?;
// Parse the metadata response, and decrypt it
Ok(MetadataResponse::from(
response.json::<RawMetadataResponse>()
.map_err(|_| MetaError::Malformed)?
.decrypt_metadata(&key)
.map_err(|_| MetaError::Decrypt)?,
nonce,
))
}
}
/// The metadata response from the server, when fetching the data through
/// the API.
/// This response contains raw metadata, which is still encrypted.
#[derive(Debug, Deserialize)]
pub struct RawMetadataResponse {
/// The encrypted metadata.
#[serde(rename = "metadata")]
meta: String,
}
impl RawMetadataResponse {
/// Get and decrypt the metadata, based on the raw data in this response.
///
/// The decrypted data is verified using an included tag.
/// If verification failed, an error is returned.
pub fn decrypt_metadata(&self, key_set: &KeySet) -> Result<MetadataData, FailureError> {
// Decode the metadata
let raw = b64::decode(&self.meta)?;
// Get the encrypted metadata, and it's tag
let (encrypted, tag) = raw.split_at(raw.len() - 16);
// TODO: is the tag length correct, remove assert if it is
assert_eq!(tag.len(), 16);
// Decrypt the metadata
let meta = decrypt_aead(
KeySet::cipher(),
key_set.meta_key().unwrap(),
Some(key_set.iv()),
&[],
encrypted,
&tag,
)?;
// Parse the metadata, and return
Ok(serde_json::from_slice(&meta)?)
}
}
/// The decoded and decrypted metadata response, holding all the properties.
/// This response object is returned from this action.
pub struct MetadataResponse {
/// The actual metadata.
metadata: MetadataData,
/// The metadata nonce.
nonce: Vec<u8>,
}
impl<'a> MetadataResponse {
/// Construct a new response with the given metadata and nonce.
pub fn from(metadata: MetadataData, nonce: Vec<u8>) -> Self {
MetadataResponse {
metadata,
nonce,
}
}
/// Get the metadata.
pub fn metadata(&self) -> &MetadataData {
&self.metadata
}
/// Get the nonce.
pub fn nonce(&self) -> &Vec<u8> {
&self.nonce
}
}
#[derive(Fail, Debug)]
pub enum Error {
/// A general error occurred while requesting the file data.
/// This may be because authentication failed, because decrypting the
/// file metadata didn't succeed, or due to some other reason.
#[fail(display = "Failed to request file data")]
Request(#[cause] RequestError),
/// 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,
}
impl From<AuthError> for Error {
fn from(err: AuthError) -> Error {
Error::Request(RequestError::Auth(err))
}
}
impl From<MetaError> for Error {
fn from(err: MetaError) -> Error {
Error::Request(RequestError::Meta(err))
}
}
#[derive(Fail, Debug)]
pub enum RequestError {
/// Failed authenticating, in order to fetch the file data.
#[fail(display = "Failed to authenticate")]
Auth(#[cause] AuthError),
/// Failed to retrieve the file metadata.
#[fail(display = "Failed to retrieve file metadata")]
Meta(#[cause] MetaError),
}
#[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 MetaError {
/// An error occurred while computing the cryptographic signature used for
/// decryption.
#[fail(display = "Failed to compute cryptographic signature")]
ComputeSignature,
/// Sending the request to gather the metadata encryption nonce failed.
#[fail(display = "Failed to request metadata nonce")]
NonceReq,
/// The response for fetching the metadata encryption nonce indicated an
/// error and wasn't successful.
#[fail(display = "Bad HTTP response '{}' while requesting metadata nonce", _1)]
NonceReqStatus(StatusCode, String),
/// No metadata encryption nonce was included in the response from the
/// server, it was missing.
#[fail(display = "Missing metadata nonce in server response")]
NoNonceHeader,
/// The metadata 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 metadata nonce")]
MalformedNonce,
/// The received metadata is malformed, and couldn't be decoded or
/// interpreted.
#[fail(display = "Received malformed metadata")]
Malformed,
/// Failed to decrypt the received metadata.
#[fail(display = "Failed to decrypt received metadata")]
Decrypt,
}

View file

@ -1,5 +1,6 @@
pub mod download;
pub mod info;
pub mod metadata;
pub mod params;
pub mod password;
pub mod upload;