use crate::{
crypto, queries, utils,
v1::{response_payload, PlainResponsePayload, METADATA_VERSION},
FilenSettings,
};
use secstr::{SecUtf8, SecVec};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use snafu::{ensure, Backtrace, ResultExt, Snafu};
type Result<T, E = Error> = std::result::Result<T, E>;
const USER_KEY_PAIR_INFO_PATH: &str = "/v1/user/keyPair/info";
const USER_KEY_PAIR_UPDATE_PATH: &str = "/v1/user/keyPair/update";
const USER_MASTER_KEYS_PATH: &str = "/v1/user/masterKeys";
const USER_PUBLIC_KEY_GET_PATH: &str = "/v1/user/publicKey/get";
#[derive(Snafu, Debug)]
pub enum Error {
#[snafu(display("Caller provided invalid argument: {}", message))]
BadArgument { message: String, backtrace: Backtrace },
#[snafu(display("Public key was not a valid base64-encoded string"))]
DecodePublicKeyFailed { source: base64::DecodeError },
#[snafu(display("Failed to decrypt master keys: {}", source))]
DecryptMasterKeysFailed { source: crypto::Error },
#[snafu(display("Failed to decrypt private key: {}", source))]
DecryptPrivateKeyFailed { source: crypto::Error },
#[snafu(display("Failed to encrypt master keys: {}", source))]
EncryptMasterKeysFailed { source: crypto::Error },
#[snafu(display("Failed to encrypt private key: {}", source))]
EncryptPrivateKeyFailed { source: crypto::Error },
#[snafu(display("{} query failed: {}", USER_KEY_PAIR_INFO_PATH, source))]
UserKeyPairInfoQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", USER_KEY_PAIR_UPDATE_PATH, source))]
UserKeyPairUpdateQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", USER_MASTER_KEYS_PATH, source))]
UserMasterKeysQueryFailed { source: queries::Error },
#[snafu(display("{} query failed: {}", USER_PUBLIC_KEY_GET_PATH, source))]
UserPublicKeyGetQueryFailed { source: queries::Error },
}
pub trait HasMasterKeys {
fn master_keys_metadata_ref(&self) -> Option<&str>;
fn decrypt_master_keys_metadata(&self, last_master_key: &SecUtf8) -> Result<Vec<SecUtf8>> {
match self.master_keys_metadata_ref() {
Some(metadata) => {
crypto::decrypt_master_keys_metadata(metadata, last_master_key).context(DecryptMasterKeysFailedSnafu {})
}
None => BadArgumentSnafu {
message: "master keys metadata is absent, cannot decrypt None",
}
.fail(),
}
}
}
pub trait HasPrivateKey {
fn private_key_metadata_ref(&self) -> Option<&str>;
fn decrypt_private_key(&self, master_keys: &[SecUtf8]) -> Result<SecVec<u8>> {
match self.private_key_metadata_ref() {
Some(metadata) => {
crypto::decrypt_private_key_metadata(metadata, master_keys).context(DecryptPrivateKeyFailedSnafu {})
}
None => BadArgumentSnafu {
message: "private key metadata is absent, cannot decrypt None",
}
.fail(),
}
}
}
pub trait HasPublicKey {
fn public_key_ref(&self) -> Option<&str>;
fn decode_public_key(&self) -> Result<Vec<u8>> {
match self.public_key_ref() {
Some(key) => base64::decode(key).context(DecodePublicKeyFailedSnafu {}),
None => BadArgumentSnafu {
message: "public key is absent, cannot decode None",
}
.fail(),
}
}
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct UserKeyPairInfoResponseData {
#[serde(rename = "publicKey")]
pub public_key: Option<String>,
#[serde(rename = "privateKey")]
pub private_key_metadata: Option<String>,
}
utils::display_from_json!(UserKeyPairInfoResponseData);
impl HasPrivateKey for UserKeyPairInfoResponseData {
fn private_key_metadata_ref(&self) -> Option<&str> {
self.private_key_metadata.as_deref()
}
}
impl HasPublicKey for UserKeyPairInfoResponseData {
fn public_key_ref(&self) -> Option<&str> {
self.public_key.as_deref()
}
}
response_payload!(
UserKeyPairInfoResponsePayload<UserKeyPairInfoResponseData>
);
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct UserKeyPairUpdateRequestPayload<'user_key_pair_update> {
#[serde(rename = "apiKey")]
pub api_key: &'user_key_pair_update SecUtf8,
#[serde(rename = "privateKey")]
pub private_key: SecUtf8,
#[serde(rename = "publicKey")]
pub public_key: String,
}
utils::display_from_json_with_lifetime!('user_key_pair_update, UserKeyPairUpdateRequestPayload);
impl<'user_key_pair_update> UserKeyPairUpdateRequestPayload<'user_key_pair_update> {
pub fn new(
api_key: &'user_key_pair_update SecUtf8,
private_key_bytes: &SecVec<u8>,
public_key_bytes: &[u8],
last_master_key: &SecUtf8,
) -> Result<Self> {
let private_key = crypto::encrypt_metadata_str(
&base64::encode(private_key_bytes.unsecure()),
last_master_key,
METADATA_VERSION,
)
.map(SecUtf8::from)
.context(EncryptPrivateKeyFailedSnafu {})?;
let public_key = base64::encode(public_key_bytes);
Ok(Self {
api_key,
private_key,
public_key,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct MasterKeysFetchRequestPayload<'master_keys_fetch> {
#[serde(rename = "apiKey")]
pub api_key: &'master_keys_fetch SecUtf8,
#[serde(rename = "masterKeys")]
pub master_keys_metadata: String,
}
utils::display_from_json_with_lifetime!('master_keys_fetch, MasterKeysFetchRequestPayload);
impl<'master_keys_fetch> MasterKeysFetchRequestPayload<'master_keys_fetch> {
pub fn new(api_key: &'master_keys_fetch SecUtf8, raw_master_keys: &[SecUtf8]) -> Result<Self> {
let empty_key = SecUtf8::from("");
let last_master_key = raw_master_keys.last().unwrap_or(&empty_key);
ensure!(
!last_master_key.unsecure().is_empty(),
BadArgumentSnafu {
message: "given raw master keys should not be empty or last master key should not be empty"
}
);
let master_keys_metadata =
crypto::encrypt_master_keys_metadata(raw_master_keys, last_master_key, super::METADATA_VERSION)
.context(EncryptMasterKeysFailedSnafu {})?;
Ok(Self {
api_key,
master_keys_metadata,
})
}
}
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct MasterKeysFetchResponseData {
#[serde(rename = "keys")]
pub keys_metadata: Option<String>,
}
utils::display_from_json!(MasterKeysFetchResponseData);
impl HasMasterKeys for MasterKeysFetchResponseData {
fn master_keys_metadata_ref(&self) -> Option<&str> {
self.keys_metadata.as_deref()
}
}
response_payload!(
MasterKeysFetchResponsePayload<MasterKeysFetchResponseData>
);
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct UserPublicKeyGetRequestPayload<'user_public_key_get> {
pub email: &'user_public_key_get str,
}
utils::display_from_json_with_lifetime!('user_public_key_get, UserPublicKeyGetRequestPayload);
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct UserPublicKeyGetResponseData {
#[serde(rename = "publicKey")]
pub public_key: Option<String>,
}
utils::display_from_json!(UserPublicKeyGetResponseData);
impl HasPublicKey for UserPublicKeyGetResponseData {
fn public_key_ref(&self) -> Option<&str> {
self.public_key.as_deref()
}
}
response_payload!(
UserPublicKeyGetResponsePayload<UserPublicKeyGetResponseData>
);
pub fn user_key_pair_info_request(
api_key: &SecUtf8,
filen_settings: &FilenSettings,
) -> Result<UserKeyPairInfoResponsePayload> {
queries::query_filen_api(USER_KEY_PAIR_INFO_PATH, &utils::api_key_json(api_key), filen_settings)
.context(UserKeyPairInfoQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn user_key_pair_info_request_async(
api_key: &SecUtf8,
filen_settings: &FilenSettings,
) -> Result<UserKeyPairInfoResponsePayload> {
queries::query_filen_api_async(USER_KEY_PAIR_INFO_PATH, &utils::api_key_json(api_key), filen_settings)
.await
.context(UserKeyPairInfoQueryFailedSnafu {})
}
pub fn user_key_pair_update_request(
payload: &UserKeyPairUpdateRequestPayload,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api(USER_KEY_PAIR_UPDATE_PATH, payload, filen_settings)
.context(UserKeyPairUpdateQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn user_key_pair_update_request_async(
payload: &UserKeyPairUpdateRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<PlainResponsePayload> {
queries::query_filen_api_async(USER_KEY_PAIR_UPDATE_PATH, payload, filen_settings)
.await
.context(UserKeyPairUpdateQueryFailedSnafu {})
}
pub fn user_master_keys_request(
payload: &MasterKeysFetchRequestPayload,
filen_settings: &FilenSettings,
) -> Result<MasterKeysFetchResponsePayload> {
queries::query_filen_api(USER_MASTER_KEYS_PATH, payload, filen_settings).context(UserMasterKeysQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn user_master_keys_request_async(
payload: &MasterKeysFetchRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<MasterKeysFetchResponsePayload> {
queries::query_filen_api_async(USER_MASTER_KEYS_PATH, payload, filen_settings)
.await
.context(UserMasterKeysQueryFailedSnafu {})
}
pub fn user_public_key_get_request(
payload: &UserPublicKeyGetRequestPayload,
filen_settings: &FilenSettings,
) -> Result<UserPublicKeyGetResponsePayload> {
queries::query_filen_api(USER_PUBLIC_KEY_GET_PATH, payload, filen_settings)
.context(UserPublicKeyGetQueryFailedSnafu {})
}
#[cfg(feature = "async")]
pub async fn user_public_key_get_request_async(
payload: &UserPublicKeyGetRequestPayload<'_>,
filen_settings: &FilenSettings,
) -> Result<UserPublicKeyGetResponsePayload> {
queries::query_filen_api_async(USER_PUBLIC_KEY_GET_PATH, payload, filen_settings)
.await
.context(UserPublicKeyGetQueryFailedSnafu {})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "async")]
use crate::test_utils::validate_contract_async;
use crate::test_utils::{read_project_file, validate_contract};
use once_cell::sync::Lazy;
use pretty_assertions::assert_eq;
static API_KEY: Lazy<SecUtf8> =
Lazy::new(|| SecUtf8::from("bYZmrwdVEbHJSqeA1RfnPtKiBcXzUpRdKGRkjw9m1o1eqSGP1s6DM11CDnklpFq6"));
#[test]
fn decode_public_key_should_return_decoded_bytes() {
let public_key_base64 = "MIICIjA";
let expected_public_key_bytes = base64::decode(public_key_base64).unwrap();
let user_key_pair = UserKeyPairInfoResponseData {
public_key: Some(public_key_base64.to_owned()),
private_key_metadata: None,
};
let decoded_public_key_bytes = user_key_pair.decode_public_key().unwrap();
assert_eq!(decoded_public_key_bytes, expected_public_key_bytes);
}
#[test]
fn decrypt_private_key_should_return_decrypted_and_decoded_key_bytes() {
let private_key_file_contents = read_project_file("tests/resources/filen_private_key.txt");
let private_key_metadata_encrypted = String::from_utf8_lossy(&private_key_file_contents).to_string();
let m_key = SecUtf8::from("ed8d39b6c2d00ece398199a3e83988f1c4942b24");
let expected = crypto::decrypt_metadata_str(&private_key_metadata_encrypted, &m_key)
.map(|str| SecVec::from(base64::decode(str).unwrap()))
.unwrap();
let user_key_pair = UserKeyPairInfoResponseData {
public_key: None,
private_key_metadata: Some(private_key_metadata_encrypted),
};
let decrypted_private_key = user_key_pair.decrypt_private_key(&[m_key]).unwrap();
assert_eq!(decrypted_private_key.unsecure(), expected.unsecure());
}
#[test]
fn user_key_pair_info_request_should_be_correctly_typed() {
let request_payload = utils::api_key_json(&API_KEY);
validate_contract(
USER_KEY_PAIR_INFO_PATH,
request_payload,
"tests/resources/responses/user_keyPair_info.json",
|_, filen_settings| user_key_pair_info_request(&API_KEY, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn user_key_pair_info_request_async_should_be_correctly_typed() {
let request_payload = utils::api_key_json(&API_KEY);
validate_contract_async(
USER_KEY_PAIR_INFO_PATH,
request_payload,
"tests/resources/responses/user_keyPair_info.json",
|_, filen_settings| async move { user_key_pair_info_request_async(&API_KEY, &filen_settings).await },
)
.await;
}
#[test]
fn master_keys_request_should_be_correctly_typed() {
let request_payload = MasterKeysFetchRequestPayload {
api_key: &SecUtf8::from("bYZmrwdVEbHJSqeA1RfnPtKiBcXzUpRdKGRkjw9m1o1eqSGP1s6DM11CDnklpFq6"),
master_keys_metadata:
"U2FsdGVkX1/P4QDMaiaanx8kpL7fY+v/f3dSzC9Ajl58gQg5bffqGUbOIzROwGQn8m5NAZa0tRnVya84aJnf1w==".to_owned(),
};
validate_contract(
USER_MASTER_KEYS_PATH,
request_payload,
"tests/resources/responses/user_masterKeys.json",
|request_payload, filen_settings| user_master_keys_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn master_keys_request_async_should_be_correctly_typed() {
let request_payload = MasterKeysFetchRequestPayload {
api_key: &SecUtf8::from("bYZmrwdVEbHJSqeA1RfnPtKiBcXzUpRdKGRkjw9m1o1eqSGP1s6DM11CDnklpFq6"),
master_keys_metadata:
"U2FsdGVkX1/P4QDMaiaanx8kpL7fY+v/f3dSzC9Ajl58gQg5bffqGUbOIzROwGQn8m5NAZa0tRnVya84aJnf1w==".to_owned(),
};
validate_contract_async(
USER_MASTER_KEYS_PATH,
request_payload,
"tests/resources/responses/user_masterKeys.json",
|request_payload, filen_settings| async move {
user_master_keys_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
#[test]
fn user_public_key_get_request_should_be_correctly_typed() {
let request_payload = UserPublicKeyGetRequestPayload { email: "test@test.com" };
validate_contract(
USER_PUBLIC_KEY_GET_PATH,
request_payload,
"tests/resources/responses/user_public_key_get.json",
|request_payload, filen_settings| user_public_key_get_request(&request_payload, &filen_settings),
);
}
#[cfg(feature = "async")]
#[tokio::test]
async fn user_public_key_get_request_async_should_be_correctly_typed() {
let request_payload = UserPublicKeyGetRequestPayload { email: "test@test.com" };
validate_contract_async(
USER_PUBLIC_KEY_GET_PATH,
request_payload,
"tests/resources/responses/user_public_key_get.json",
|request_payload, filen_settings| async move {
user_public_key_get_request_async(&request_payload, &filen_settings).await
},
)
.await;
}
}