#![recursion_limit = "128"]
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate base64_serde;
#[macro_use]
extern crate quick_error;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate vec1;
#[cfg(test)]
#[macro_use]
extern crate galvanic_assert;
#[cfg(test)]
#[macro_use]
extern crate double;
#[macro_use]
extern crate async_trait;
#[macro_use]
extern crate percent_encoding;
mod crypto;
mod internal;
include!(concat!(env!("OUT_DIR"), "/transform.rs"));
#[cfg(feature = "beta")]
pub mod search;
pub mod document;
pub mod group;
pub mod user;
#[cfg(feature = "blocking")]
pub mod blocking;
pub mod policy;
pub mod prelude;
pub use crate::internal::{
DeviceAddResult, DeviceContext, DeviceSigningKeyPair, IronOxideErr, KeyPair, PrivateKey,
PublicKey, SdkOperation,
};
use crate::{
config::IronOxideConfig,
internal::{
add_optional_timeout,
document_api::UserOrGroup,
group_api::{GroupId, GroupUpdatePrivateKeyResult},
user_api::{UserId, UserResult, UserUpdatePrivateKeyResult},
WithKey,
},
policy::PolicyGrant,
};
use dashmap::DashMap;
use itertools::EitherOrBoth;
use rand::{
rngs::{adapter::ReseedingRng, OsRng},
SeedableRng,
};
use rand_chacha::ChaChaCore;
use recrypt::api::{Ed25519, RandomBytes, Recrypt, Sha256};
use std::{convert::TryInto, fmt, sync::Mutex};
use vec1::Vec1;
pub type Result<T> = std::result::Result<T, IronOxideErr>;
type PolicyCache = DashMap<PolicyGrant, Vec<WithKey<UserOrGroup>>>;
pub mod config {
use std::time::Duration;
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct IronOxideConfig {
pub policy_caching: PolicyCachingConfig,
pub sdk_operation_timeout: Option<Duration>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct PolicyCachingConfig {
pub max_entries: usize,
}
impl Default for IronOxideConfig {
fn default() -> Self {
IronOxideConfig {
policy_caching: PolicyCachingConfig::default(),
sdk_operation_timeout: Some(Duration::from_secs(30)),
}
}
}
impl Default for PolicyCachingConfig {
fn default() -> Self {
PolicyCachingConfig { max_entries: 128 }
}
}
}
pub struct IronOxide {
pub(crate) config: IronOxideConfig,
pub(crate) recrypt: Recrypt<Sha256, Ed25519, RandomBytes<recrypt::api::DefaultRng>>,
pub(crate) user_master_pub_key: PublicKey,
pub(crate) device: DeviceContext,
pub(crate) rng: Mutex<ReseedingRng<ChaChaCore, OsRng>>,
pub(crate) policy_eval_cache: PolicyCache,
}
impl fmt::Debug for IronOxide {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("IronOxide")
.field("config", &self.config)
.field("user_master_pub_key", &self.user_master_pub_key)
.field("device", &self.device)
.field("policy_eval_cache", &self.policy_eval_cache)
.finish()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum InitAndRotationCheck<T> {
NoRotationNeeded(T),
RotationNeeded(T, PrivateKeyRotationCheckResult),
}
impl<T> InitAndRotationCheck<T> {
pub fn discard_check(self) -> T {
match self {
InitAndRotationCheck::NoRotationNeeded(io)
| InitAndRotationCheck::RotationNeeded(io, _) => io,
}
}
pub fn new_rotation_needed(
io: T,
rotations_needed: EitherOrBoth<UserId, Vec1<GroupId>>,
) -> InitAndRotationCheck<T> {
InitAndRotationCheck::RotationNeeded(io, PrivateKeyRotationCheckResult { rotations_needed })
}
}
const BYTES_BEFORE_RESEEDING: u64 = 1024 * 1024;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct PrivateKeyRotationCheckResult {
pub rotations_needed: EitherOrBoth<UserId, Vec1<GroupId>>,
}
impl PrivateKeyRotationCheckResult {
pub fn user_rotation_needed(&self) -> Option<&UserId> {
match &self.rotations_needed {
EitherOrBoth::Left(u) | EitherOrBoth::Both(u, _) => Some(u),
_ => None,
}
}
pub fn group_rotation_needed(&self) -> Option<&Vec1<GroupId>> {
match &self.rotations_needed {
EitherOrBoth::Right(groups) | EitherOrBoth::Both(_, groups) => Some(groups),
_ => None,
}
}
}
pub async fn initialize(
device_context: &DeviceContext,
config: &IronOxideConfig,
) -> Result<IronOxide> {
internal::add_optional_timeout(
internal::user_api::user_get_current(device_context.auth()),
config.sdk_operation_timeout,
SdkOperation::InitializeSdk,
)
.await?
.map(|current_user| IronOxide::create(¤t_user, device_context, config))
.map_err(|_| IronOxideErr::InitializeError)
}
fn check_groups_and_collect_rotation<T>(
groups: &Vec<internal::group_api::GroupMetaResult>,
user_needs_rotation: bool,
account_id: UserId,
ironoxide: T,
) -> InitAndRotationCheck<T> {
use EitherOrBoth::{Both, Left, Right};
let groups_needing_rotation = groups
.iter()
.filter(|meta_result| meta_result.needs_rotation() == Some(true))
.map(|meta_result| meta_result.id().to_owned())
.collect::<Vec<_>>();
let maybe_groups_needing_rotation = Vec1::try_from_vec(groups_needing_rotation).ok();
match (user_needs_rotation, maybe_groups_needing_rotation) {
(false, None) => InitAndRotationCheck::NoRotationNeeded(ironoxide),
(true, None) => InitAndRotationCheck::new_rotation_needed(ironoxide, Left(account_id)),
(false, Some(groups)) => {
InitAndRotationCheck::new_rotation_needed(ironoxide, Right(groups))
}
(true, Some(groups)) => {
InitAndRotationCheck::new_rotation_needed(ironoxide, Both(account_id, groups))
}
}
}
pub async fn initialize_check_rotation(
device_context: &DeviceContext,
config: &IronOxideConfig,
) -> Result<InitAndRotationCheck<IronOxide>> {
let (curr_user, group_list_result) = add_optional_timeout(
futures::future::try_join(
internal::user_api::user_get_current(device_context.auth()),
internal::group_api::list(device_context.auth(), None),
),
config.sdk_operation_timeout,
SdkOperation::InitializeSdkCheckRotation,
)
.await??;
let ironoxide = IronOxide::create(&curr_user, device_context, config);
let user_groups = group_list_result.result();
Ok(check_groups_and_collect_rotation(
user_groups,
curr_user.needs_rotation(),
curr_user.account_id().to_owned(),
ironoxide,
))
}
impl IronOxide {
pub fn device(&self) -> &DeviceContext {
&self.device
}
pub fn clear_policy_cache(&self) -> usize {
let size = self.policy_eval_cache.len();
self.policy_eval_cache.clear();
size
}
fn create(
curr_user: &UserResult,
device_context: &DeviceContext,
config: &IronOxideConfig,
) -> IronOxide {
IronOxide {
config: config.clone(),
recrypt: Recrypt::new(),
device: device_context.clone(),
user_master_pub_key: curr_user.user_public_key().to_owned(),
rng: Mutex::new(ReseedingRng::new(
rand_chacha::ChaChaCore::from_entropy(),
BYTES_BEFORE_RESEEDING,
OsRng::default(),
)),
policy_eval_cache: DashMap::new(),
}
}
pub async fn rotate_all(
&self,
rotations: &PrivateKeyRotationCheckResult,
password: &str,
timeout: Option<std::time::Duration>,
) -> Result<(
Option<UserUpdatePrivateKeyResult>,
Option<Vec<GroupUpdatePrivateKeyResult>>,
)> {
let valid_password: internal::Password = password.try_into()?;
let user_future = rotations.user_rotation_needed().map(|_| {
internal::user_api::user_rotate_private_key(
&self.recrypt,
valid_password,
self.device().auth(),
)
});
let group_futures = rotations.group_rotation_needed().map(|groups| {
let group_futures = groups
.into_iter()
.map(|group_id| {
internal::group_api::group_rotate_private_key(
&self.recrypt,
self.device().auth(),
group_id,
self.device().device_private_key(),
)
})
.collect::<Vec<_>>();
futures::future::join_all(group_futures)
});
let user_opt_future: futures::future::OptionFuture<_> = user_future.into();
let group_opt_future: futures::future::OptionFuture<_> = group_futures.into();
let (user_opt_result, group_opt_vec_result) = add_optional_timeout(
futures::future::join(user_opt_future, group_opt_future),
timeout,
SdkOperation::RotateAll,
)
.await?;
let group_opt_result_vec = group_opt_vec_result.map(|g| g.into_iter().collect());
Ok((
user_opt_result.transpose()?,
group_opt_result_vec.transpose()?,
))
}
}