use crate::{
crypto::transform,
internal::{
group_api::requests::{group_list::GroupListResponse, GroupUserEditResponse},
rest::json::TransformedEncryptedValue,
user_api::{self, UserId},
validate_id, validate_name, IronOxideErr, PrivateKey, PublicKey, RequestAuth, TransformKey,
WithKey,
},
};
use chrono::{DateTime, Utc};
use core::convert::identity;
use futures::prelude::*;
use itertools::{Either, Itertools};
use recrypt::prelude::*;
use std::{
collections::HashMap,
convert::{TryFrom, TryInto},
};
mod requests;
pub enum GroupEntity {
Member,
Admin,
}
#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)]
pub struct GroupId(pub(crate) String);
impl GroupId {
pub fn id(&self) -> &String {
&self.0
}
pub fn unsafe_from_string(id: String) -> GroupId {
GroupId(id)
}
}
impl TryFrom<String> for GroupId {
type Error = IronOxideErr;
fn try_from(group_id: String) -> Result<Self, Self::Error> {
group_id.as_str().try_into()
}
}
impl TryFrom<&str> for GroupId {
type Error = IronOxideErr;
fn try_from(group_id: &str) -> Result<Self, Self::Error> {
validate_id(group_id, "group_id").map(GroupId)
}
}
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
pub struct GroupName(pub(crate) String);
impl GroupName {
pub fn name(&self) -> &String {
&self.0
}
}
impl TryFrom<String> for GroupName {
type Error = IronOxideErr;
fn try_from(group_name: String) -> Result<Self, Self::Error> {
group_name.as_str().try_into()
}
}
impl TryFrom<&str> for GroupName {
type Error = IronOxideErr;
fn try_from(group_name: &str) -> Result<Self, Self::Error> {
validate_name(group_name, "group_name").map(GroupName)
}
}
#[derive(Debug)]
pub struct GroupListResult {
result: Vec<GroupMetaResult>,
}
impl GroupListResult {
pub fn new(metas: Vec<GroupMetaResult>) -> GroupListResult {
GroupListResult { result: metas }
}
pub fn result(&self) -> &Vec<GroupMetaResult> {
&self.result
}
}
#[derive(Clone, Debug)]
pub struct GroupMetaResult {
id: GroupId,
name: Option<GroupName>,
group_master_public_key: PublicKey,
is_admin: bool,
is_member: bool,
created: DateTime<Utc>,
updated: DateTime<Utc>,
}
impl GroupMetaResult {
pub fn id(&self) -> &GroupId {
&self.id
}
pub fn name(&self) -> Option<&GroupName> {
self.name.as_ref()
}
pub fn is_admin(&self) -> bool {
self.is_admin
}
pub fn is_member(&self) -> bool {
self.is_member
}
pub fn created(&self) -> &DateTime<Utc> {
&self.created
}
pub fn last_updated(&self) -> &DateTime<Utc> {
&self.updated
}
pub fn group_master_public_key(&self) -> &PublicKey {
&self.group_master_public_key
}
}
#[derive(Debug)]
pub struct GroupGetResult {
id: GroupId,
name: Option<GroupName>,
group_master_public_key: PublicKey,
is_admin: bool,
is_member: bool,
admin_list: Option<Vec<UserId>>,
member_list: Option<Vec<UserId>>,
created: DateTime<Utc>,
updated: DateTime<Utc>,
pub(crate) encrypted_private_key: Option<TransformedEncryptedValue>,
}
impl GroupGetResult {
pub fn id(&self) -> &GroupId {
&self.id
}
pub fn name(&self) -> Option<&GroupName> {
self.name.as_ref()
}
pub fn group_master_public_key(&self) -> &PublicKey {
&self.group_master_public_key
}
pub fn is_admin(&self) -> bool {
self.is_admin
}
pub fn is_member(&self) -> bool {
self.is_member
}
pub fn created(&self) -> &DateTime<Utc> {
&self.created
}
pub fn last_updated(&self) -> &DateTime<Utc> {
&self.updated
}
pub fn admin_list(&self) -> Option<&Vec<UserId>> {
self.admin_list.as_ref()
}
pub fn member_list(&self) -> Option<&Vec<UserId>> {
self.member_list.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct GroupAccessEditErr {
user: UserId,
error: String,
}
impl GroupAccessEditErr {
pub(crate) fn new(user: UserId, error: String) -> GroupAccessEditErr {
GroupAccessEditErr { user, error }
}
pub fn user(&self) -> &UserId {
&self.user
}
pub fn error(&self) -> &String {
&self.error
}
}
#[derive(Debug)]
pub struct GroupAccessEditResult {
succeeded: Vec<UserId>,
failed: Vec<GroupAccessEditErr>,
}
impl GroupAccessEditResult {
pub fn failed(&self) -> &Vec<GroupAccessEditErr> {
&self.failed
}
pub fn succeeded(&self) -> &Vec<UserId> {
&self.succeeded
}
}
pub fn list(
auth: &RequestAuth,
ids: Option<&Vec<GroupId>>,
) -> impl Future<Item = GroupListResult, Error = IronOxideErr> {
let resp = match ids {
Some(group_ids) => requests::group_list::group_limited_list_request(auth, &group_ids),
None => requests::group_list::group_list_request(auth),
};
resp.and_then(|GroupListResponse { result }| {
let group_list = result
.into_iter()
.map(|g| g.try_into())
.collect::<Result<Vec<_>, _>>()?;
Ok(GroupListResult::new(group_list))
})
}
pub(crate) fn get_group_keys<'a>(
auth: &'a RequestAuth,
groups: &'a Vec<GroupId>,
) -> Box<dyn Future<Item = (Vec<GroupId>, Vec<WithKey<GroupId>>), Error = IronOxideErr> + 'a> {
if groups.len() == 0 {
return Box::new(futures::future::ok((vec![], vec![])));
}
let cloned_groups: Vec<GroupId> = groups.clone();
let fetch_groups = list(auth, Some(&groups));
Box::new(
fetch_groups
.map(move |GroupListResult { result }| {
result
.iter()
.fold(HashMap::with_capacity(groups.len()), |mut acc, group| {
let public_key = group.group_master_public_key();
acc.insert(group.id().clone(), public_key.clone());
acc
})
})
.map(move |ids_with_keys| {
cloned_groups.into_iter().partition_map(move |group_id| {
let maybe_public_key = ids_with_keys.get(&group_id).cloned();
match maybe_public_key {
Some(public_key) => Either::Right(WithKey {
id: group_id,
public_key,
}),
None => Either::Left(group_id),
}
})
}),
)
}
pub fn group_create<'a, CR: rand::CryptoRng + rand::RngCore>(
recrypt: &'a Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
auth: &'a RequestAuth,
user_master_pub_key: &'a PublicKey,
group_id: Option<GroupId>,
name: Option<GroupName>,
add_as_member: bool,
) -> impl Future<Item = GroupMetaResult, Error = IronOxideErr> + 'a {
transform::gen_group_keys(recrypt)
.and_then(move |(plaintext, group_priv_key, group_pub_key)| {
Ok({
let encrypted_group_key = recrypt.encrypt(
&plaintext,
&user_master_pub_key.into(),
&auth.signing_keys().into(),
)?;
let transform_key: Option<crate::internal::TransformKey> = if add_as_member {
Some(
recrypt
.generate_transform_key(
&group_priv_key.into(),
&user_master_pub_key.into(),
&auth.signing_keys().into(),
)?
.into(),
)
} else {
None
};
(group_pub_key, encrypted_group_key, transform_key)
})
})
.into_future()
.and_then(move |(group_pub_key, encrypted_group_key, transform_key)| {
requests::group_create::group_create(
&auth,
user_master_pub_key,
group_id,
name,
encrypted_group_key,
group_pub_key,
auth.account_id(),
transform_key,
)
})
.and_then(|resp| resp.try_into())
}
pub fn get_metadata(
auth: &RequestAuth,
id: &GroupId,
) -> impl Future<Item = GroupGetResult, Error = IronOxideErr> {
requests::group_get::group_get_request(auth, id).and_then(|resp| resp.try_into())
}
pub fn group_delete(
auth: &RequestAuth,
group_id: &GroupId,
) -> impl Future<Item = GroupId, Error = IronOxideErr> {
requests::group_delete::group_delete_request(auth, &group_id)
.and_then(|resp| resp.id.try_into())
}
pub fn group_add_members<'a, CR: rand::CryptoRng + rand::RngCore>(
recrypt: &'a Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
auth: &'a RequestAuth,
device_private_key: &'a PrivateKey,
group_id: &'a GroupId,
users: &'a Vec<UserId>,
) -> impl Future<Item = GroupAccessEditResult, Error = IronOxideErr> + 'a {
get_metadata(auth, group_id)
.join(get_user_keys(auth, users))
.and_then(move |(group_get, (mut acc_fails, successes))| {
let encrypted_group_key = group_get
.encrypted_private_key
.ok_or(IronOxideErr::NotGroupAdmin(group_id.clone()))?;
let (plaintext, _) = transform::decrypt_plaintext(
&recrypt,
encrypted_group_key.try_into()?,
&device_private_key.recrypt_key(),
)?;
let group_private_key = recrypt.derive_private_key(&plaintext);
let (mut transform_fails, transform_success) = generate_transform_for_keys(
recrypt,
&group_private_key,
&auth.signing_keys().into(),
successes,
);
acc_fails.append(&mut transform_fails);
Ok((acc_fails, transform_success))
})
.and_then(move |(acc_fails, transforms_to_send)| {
requests::group_add_member::group_add_member_request(
&auth,
&group_id,
transforms_to_send
.into_iter()
.map(|(user_id, pub_key, transform)| {
(user_id, pub_key.into(), transform.into())
})
.collect(),
)
.map(|response| group_access_api_response_to_result(acc_fails, response))
})
}
pub fn group_add_admins<'a, CR: rand::CryptoRng + rand::RngCore>(
recrypt: &'a Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
auth: &'a RequestAuth,
device_private_key: &'a PrivateKey,
group_id: &'a GroupId,
users: &'a Vec<UserId>,
) -> impl Future<Item = GroupAccessEditResult, Error = IronOxideErr> + 'a {
get_metadata(auth, group_id)
.join(get_user_keys(auth, users))
.and_then(move |(group_get, (mut acc_fails, successes))| {
let encrypted_group_key = group_get
.encrypted_private_key
.ok_or(IronOxideErr::NotGroupAdmin(group_id.clone()))?;
let (plaintext, _) = transform::decrypt_plaintext(
&recrypt,
encrypted_group_key.try_into()?,
&device_private_key.recrypt_key(),
)?;
let (recrypt_errors, transform_success) = transform::encrypt_to_with_key(
recrypt,
&plaintext,
&auth.signing_keys().into(),
successes,
);
let mut transform_fails = recrypt_errors
.into_iter()
.map(|(WithKey { id: user_id, .. }, _)| GroupAccessEditErr {
user: user_id,
error: "Transform key could not be generated.".to_string(),
})
.collect();
acc_fails.append(&mut transform_fails);
Ok((acc_fails, transform_success))
})
.and_then(move |(acc_fails, admin_keys_to_send)| {
requests::group_add_admin::group_add_admin_request(
&auth,
&group_id,
admin_keys_to_send
.into_iter()
.map(
|(
WithKey {
id: user_id,
public_key,
},
encrypted_admin_key,
)| {
(user_id, public_key.into(), encrypted_admin_key)
},
)
.collect(),
)
.map(|response| group_access_api_response_to_result(acc_fails, response))
})
}
fn get_user_keys<'a>(
auth: &'a RequestAuth,
users: &'a Vec<UserId>,
) -> impl Future<Item = (Vec<GroupAccessEditErr>, Vec<WithKey<UserId>>), Error = IronOxideErr> + 'a
{
user_api::get_user_keys(auth, &users).map(|(failed_ids, succeeded_ids)| {
(
failed_ids
.into_iter()
.map(|user| GroupAccessEditErr::new(user, "User does not exist".to_string()))
.collect::<Vec<_>>(),
succeeded_ids,
)
})
}
fn group_access_api_response_to_result(
mut other_fails: Vec<GroupAccessEditErr>,
edit_resp: requests::GroupUserEditResponse,
) -> GroupAccessEditResult {
let mut fails_from_req = edit_resp
.failed_ids
.into_iter()
.map(|f| GroupAccessEditErr::new(f.user_id, f.error_message))
.collect();
other_fails.append(&mut fails_from_req);
GroupAccessEditResult {
succeeded: edit_resp
.succeeded_ids
.into_iter()
.map(|r| r.user_id)
.collect(),
failed: other_fails,
}
}
pub fn update_group_name<'a>(
auth: &'a RequestAuth,
id: &'a GroupId,
name: Option<&'a GroupName>,
) -> impl Future<Item = GroupMetaResult, Error = IronOxideErr> + 'a {
requests::group_update::group_update_request(auth, id, name).and_then(|resp| resp.try_into())
}
pub fn group_remove_entity<'a>(
auth: &'a RequestAuth,
id: &'a GroupId,
users: &'a Vec<UserId>,
entity_type: GroupEntity,
) -> impl Future<Item = GroupAccessEditResult, Error = IronOxideErr> + 'a {
requests::group_remove_entity::remove_entity_request(auth, id, users, entity_type).map(
|GroupUserEditResponse {
succeeded_ids,
failed_ids,
}| GroupAccessEditResult {
succeeded: succeeded_ids.into_iter().map(|user| user.user_id).collect(),
failed: failed_ids
.into_iter()
.map(|fail| GroupAccessEditErr::new(fail.user_id, fail.error_message))
.collect(),
},
)
}
fn generate_transform_for_keys<CR: rand::CryptoRng + rand::RngCore>(
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
from_private: &recrypt::api::PrivateKey,
signing_keys: &recrypt::api::SigningKeypair,
users: Vec<WithKey<UserId>>,
) -> (
Vec<GroupAccessEditErr>,
Vec<(UserId, PublicKey, TransformKey)>,
) {
let transform_results_iter = users.into_iter().map(
move |WithKey {
id: user_id,
public_key,
}| {
let group_to_user_transform = recrypt.generate_transform_key(
from_private.into(),
&public_key.clone().into(),
signing_keys,
);
match group_to_user_transform {
Ok(recrypt_transform_key) => {
Either::Right((user_id, public_key, TransformKey(recrypt_transform_key)))
}
Err(_) => Either::Left(GroupAccessEditErr::new(
user_id,
"Transform key could not be generated.".to_string(),
)),
}
},
);
transform_results_iter.partition_map(identity)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn group_id_validate_good() {
let group_id1 = "a_fo_real_good_group_id$";
let group_id2 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
assert_eq!(
GroupId(group_id1.to_string()),
GroupId::try_from(group_id1).unwrap()
);
assert_eq!(
GroupId(group_id2.to_string()),
GroupId::try_from(group_id2).unwrap()
)
}
#[test]
fn group_id_rejects_invalid() {
let group_id1 = GroupId::try_from("not a good ID!");
let group_id2 = GroupId::try_from("!!");
let group_id3 = GroupId::try_from("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567891");
assert_that!(
&group_id1.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(
&group_id2.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(
&group_id3.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
}
#[test]
fn group_id_rejects_empty() {
let group_id = GroupId::try_from("");
assert_that!(&group_id, is_variant!(Err));
assert_that!(
&group_id.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
let group_id = GroupId::try_from("\n \t ");
assert_that!(&group_id, is_variant!(Err));
assert_that!(
&group_id.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
}
#[test]
fn group_name_rejects_empty() {
let group_name = GroupName::try_from("");
assert_that!(&group_name, is_variant!(Err));
assert_that!(
&group_name.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
let group_name = GroupName::try_from("\n \t ");
assert_that!(&group_name, is_variant!(Err));
assert_that!(
&group_name.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
}
#[test]
fn group_name_rejects_too_long() {
let group_name = GroupName::try_from("01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567891");
assert_that!(
&group_name.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
)
}
#[test]
fn group_create() {}
}