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