Skip to main content

ironoxide/
lib.rs

1//! IronOxide - IronCore Labs Rust SDK
2//!
3//! The IronOxide Rust SDK is a pure Rust library that integrates IronCore's privacy, security, and data control solution into
4//! your Rust application. Operations in the IronOxide SDK are performed in the context of a user or backend service account. This
5//! SDK supports all possible operations that work in the IronCore platform including creating and managing users and groups, encrypting
6//! and decrypting document bytes, and granting and revoking access to documents to users and groups.
7//!
8//! # User Operations
9//!
10//! Users are the basis of IronOxide's functionality. Each user is a unique identity that has its own public/private key-pair. Users must always act
11//! through devices. A device is authorized using a user's private encryption key and is therefore tightly bound to that user. Data can be never be encrypted
12//! directly to a device, so devices can be considered ephemeral as there is no penalty for deleting a device and creating a new one.
13//!
14//! This SDK provides all the necessary functionality to manage users and devices. Users can be created, fetched, listed, and updated, while devices can be created
15//! and deleted all using IronOxide's [UserOps](user/trait.UserOps.html).
16//!
17//! ### Creating a User
18//!
19//! Creating a user with [IronOxide::user_create](user/trait.UserOps.html#tymethod.user_create) requires a valid IronCore or Auth0 JWT as well as
20//! the desired password that will be used to encrypt and escrow the user's private key.
21//!
22//! ```
23//! # fn get_jwt() -> &'static str {
24//! #     unimplemented!()
25//! # }
26//! # async fn run() -> Result<(), ironoxide::IronOxideErr> {
27//! # use ironoxide::prelude::*;
28//! // Assuming an external function to get the jwt
29//! let jwt_str = get_jwt();
30//! let jwt = Jwt::new(jwt_str)?;
31//! let password = "foobar";
32//! let opts = UserCreateOpts::new(false);
33//! let user_result = IronOxide::user_create(&jwt, password, &opts, None).await?;
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! Until they generate a device, this user will be unable to make any SDK calls.
39//!
40//! ### Generating a Device
41//!
42//! Generating a device with [IronOxide::generate_new_device](user/trait.UserOps.html#tymethod.generate_new_device) requires a valid IronCore or Auth0 JWT
43//! corresponding to the desired user, as well as the user's password (needed to decrypt the user's escrowed private key).
44//!
45//! ```
46//! # fn get_jwt() -> &'static str {
47//! #     unimplemented!()
48//! # }
49//! # async fn run() -> Result<(), ironoxide::IronOxideErr> {
50//! # use ironoxide::prelude::*;
51//! // Assuming an external function to get the jwt
52//! let jwt_str = get_jwt();
53//! let jwt = Jwt::new(jwt_str)?;
54//! let password = "foobar";
55//! let opts = DeviceCreateOpts::new(None);
56//! let device_result = IronOxide::generate_new_device(&jwt, password, &opts, None).await?;
57//! // A `DeviceAddResult` can be converted into a `DeviceContext` used to initialize the SDK
58//! let device_context: DeviceContext = device_result.into();
59//! # Ok(())
60//! # }
61//! ```
62//!
63//! This `DeviceContext` can now be used to initialize the SDK.
64//!
65//! ### Initializing the SDK
66//!
67//! With [ironoxide::initialize](fn.initialize.html), you can use a `DeviceContext` to create an instance of the `IronOxide` SDK object
68//! that can be used to make calls using the provided device.
69//!
70//! ```
71//! # async fn run() -> Result<(), ironoxide::IronOxideErr> {
72//! # use ironoxide::prelude::*;
73//! # let device_context: DeviceContext = unimplemented!();
74//! let config = IronOxideConfig::default();
75//! let sdk = ironoxide::initialize(&device_context, &config).await?;
76//! # Ok(())
77//! # }
78//! ```
79//!
80//! All calls made with `sdk` will use the user's provided device.
81//!
82//! # Group Operations
83//!
84//! Groups are one of the many differentiating features of the DataControl platform. Groups are collections of users who share access permissions.
85//! Group members are able to encrypt and decrypt documents using the group, and group administrators are able to update the group and modify its membership.
86//! Members can be dynamically added and removed without the need to re-encrypt the data. This requires a series of cryptographic operations
87//! involving the administrator's keys, the group’s keys, and the new member’s public key. By making it simple to control group membership,
88//! we provide efficient and precise control over who has access to what information!
89//!
90//! This SDK allows for easy management of your cryptographic groups. Groups can be created, fetched, updated, and deleted using IronOxide's
91//! [GroupOps](group/trait.GroupOps.html).
92//!
93//! ### Creating a Group
94//!
95//! For simple group creation, the [group_create](group/trait.GroupOps.html#tymethod.group_create) function can be
96//! called with default values.
97//!
98//! ```
99//! # async fn run() -> Result<(), ironoxide::IronOxideErr> {
100//! # use ironoxide::prelude::*;
101//! # let sdk: IronOxide = unimplemented!();
102//! use ironoxide::group::GroupCreateOpts;
103//! let group_result = sdk.group_create(&GroupCreateOpts::default()).await?;
104//! // Group ID used for future calls to this group
105//! let group_id: &GroupId = group_result.id();
106//! # Ok(())
107//! # }
108//! ```
109//!
110//! # Document Operations
111//!
112//! All secret data that is encrypted using the IronCore platform are referred to as documents. Documents wrap the raw bytes of
113//! secret data to encrypt along with various metadata that helps convey access information to that data. Documents can be encrypted,
114//! decrypted, updated, granted to users and groups, and revoked from users and groups using IronOxide's
115//! [DocumentOps](document/trait.DocumentOps.html).
116//!
117//! ### Encrypting a Document
118//!
119//! For simple encryption to self, the [document_encrypt](document/trait.DocumentOps.html#tymethod.document_encrypt) function can be
120//! called with default values.
121//!
122//!```
123//! # async fn run() -> Result<(), ironoxide::IronOxideErr> {
124//! # use ironoxide::prelude::*;
125//! # let sdk: IronOxide = unimplemented!();
126//! use ironoxide::document::DocumentEncryptOpts;
127//! let data = "secret data".to_string().into_bytes();
128//! let encrypted = sdk.document_encrypt(data, &DocumentEncryptOpts::default()).await?;
129//! let encrypted_bytes = encrypted.encrypted_data();
130//! # Ok(())
131//! # }
132//! ```
133//!
134//! ### Decrypting a Document
135//!
136//! Decrypting a document is even simpler, as the only thing required by
137//! [document_decrypt](document/trait.DocumentOps.html#tymethod.document_decrypt) is the bytes of the encrypted document.
138//!
139//!```
140//! # async fn run() -> Result<(), ironoxide::IronOxideErr> {
141//! # use ironoxide::prelude::*;
142//! # let sdk: IronOxide = unimplemented!();
143//! # let encrypted_bytes: &[u8] = &[1;1];
144//! let document = sdk.document_decrypt(encrypted_bytes).await?;
145//! let decrypted_data = document.decrypted_data();
146//! # Ok(())
147//! # }
148//! ```
149
150#![allow(clippy::too_many_arguments)]
151#![allow(clippy::type_complexity)]
152// required by quick_error or IronOxideErr
153#![recursion_limit = "128"]
154// required as of rust 1.46.0
155#![type_length_limit = "2000000"]
156
157// include generated proto code as a proto module
158mod proto {
159    include!(concat!(env!("OUT_DIR"), "/proto/mod.rs"));
160}
161
162mod crypto {
163    pub mod aes;
164    pub mod transform;
165}
166mod internal;
167
168pub mod document;
169pub mod group;
170pub mod policy;
171pub mod prelude;
172pub mod user;
173
174#[cfg(feature = "beta")]
175pub mod search;
176
177#[cfg(feature = "blocking")]
178pub mod blocking;
179
180pub use crate::internal::{IronCoreRequest, IronOxideErr};
181
182use crate::{
183    common::{DeviceContext, DeviceSigningKeyPair, PublicKey, SdkOperation},
184    config::IronOxideConfig,
185    document::UserOrGroup,
186    group::{GroupId, GroupUpdatePrivateKeyResult},
187    internal::{WithKey, add_optional_timeout},
188    policy::PolicyGrant,
189    user::{UserId, UserResult, UserUpdatePrivateKeyResult},
190};
191use itertools::EitherOrBoth;
192use papaya::HashMap;
193use rand::{
194    SeedableRng,
195    rngs::{OsRng, adapter::ReseedingRng},
196};
197use rand_chacha::ChaChaCore;
198use recrypt::api::{Ed25519, RandomBytes, Recrypt, Sha256};
199use std::{
200    convert::TryInto,
201    fmt,
202    sync::{Arc, Mutex},
203};
204use vec1::Vec1;
205
206/// A `Result` alias where the Err case is `IronOxideErr`
207pub type Result<T> = std::result::Result<T, IronOxideErr>;
208type PolicyCache = HashMap<PolicyGrant, Vec<WithKey<UserOrGroup>>>;
209
210// This is where we export structs that don't fit into a single module.
211// They were previously exported at the top level, but added clutter to the docs landing page.
212/// Types useful in multiple modules
213pub mod common {
214    pub use crate::internal::{
215        DeviceContext, DeviceSigningKeyPair, PrivateKey, PublicKey, SdkOperation,
216    };
217    pub use itertools::EitherOrBoth;
218}
219
220/// IronOxide SDK configuration
221pub mod config {
222    use serde::{Deserialize, Serialize};
223    use std::time::Duration;
224
225    /// Top-level configuration object for IronOxide
226    #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
227    pub struct IronOxideConfig {
228        /// See [PolicyCachingConfig](struct.PolicyCachingConfig.html)
229        pub policy_caching: PolicyCachingConfig,
230        /// Timeout for all SDK methods. Will return IronOxideErr::OperationTimedOut on timeout.
231        pub sdk_operation_timeout: Option<Duration>,
232    }
233
234    impl Default for IronOxideConfig {
235        fn default() -> Self {
236            IronOxideConfig {
237                policy_caching: PolicyCachingConfig::default(),
238                sdk_operation_timeout: Some(Duration::from_secs(30)),
239            }
240        }
241    }
242
243    /// Policy evaluation caching config
244    ///
245    /// The lifetime of the cache is the lifetime of the `IronOxide` struct.
246    ///
247    /// Since policies are evaluated by the webservice, caching the result can greatly speed
248    /// up encrypting a document with a [PolicyGrant](../policy/struct.PolicyGrant.html). There is no expiration of the cache, so
249    /// if you want to clear it at runtime, call [IronOxide::clear_policy_cache](../struct.IronOxide.html#method.clear_policy_cache).
250    #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
251    pub struct PolicyCachingConfig {
252        /// maximum number of policy evaluations that will be cached by the SDK.
253        /// If the maximum number is exceeded, the cache will be cleared prior to storing the next entry
254        pub max_entries: usize,
255    }
256
257    impl Default for PolicyCachingConfig {
258        fn default() -> Self {
259            PolicyCachingConfig { max_entries: 128 }
260        }
261    }
262}
263
264/// Primary SDK Object
265///
266/// Struct that is used to make authenticated requests to the IronCore API. Instantiated with the details
267/// of an account's various ids, device, and signing keys. Once instantiated all operations will be
268/// performed in the context of the account provided.
269pub struct IronOxide {
270    pub(crate) config: IronOxideConfig,
271    pub(crate) recrypt: Arc<Recrypt<Sha256, Ed25519, RandomBytes<recrypt::api::DefaultRng>>>,
272    /// Master public key for the user identified by `account_id`
273    pub(crate) user_master_pub_key: PublicKey,
274    pub(crate) device: DeviceContext,
275    pub(crate) rng: Mutex<ReseedingRng<ChaChaCore, OsRng>>,
276    pub(crate) policy_eval_cache: PolicyCache,
277}
278
279/// Manual implementation of Debug without the `recrypt` or `rng` fields
280impl fmt::Debug for IronOxide {
281    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282        f.debug_struct("IronOxide")
283            .field("config", &self.config)
284            .field("user_master_pub_key", &self.user_master_pub_key)
285            .field("device", &self.device)
286            .field("policy_eval_cache", &self.policy_eval_cache)
287            .finish()
288    }
289}
290
291/// Result of calling `initialize_check_rotation`
292#[derive(Clone, Debug, Eq, Hash, PartialEq)]
293pub enum InitAndRotationCheck<T> {
294    /// Initialization succeeded, and no requests for private key rotations were present
295    NoRotationNeeded(T),
296    /// Initialization succeeded, but some keys should be rotated
297    RotationNeeded(T, PrivateKeyRotationCheckResult),
298}
299
300impl<T> InitAndRotationCheck<T> {
301    /// Caller asked to check rotation on initialize, but doesn't want to handle the result.
302    /// Consider using [initialize](fn.initialize.html) instead.
303    pub fn discard_check(self) -> T {
304        match self {
305            InitAndRotationCheck::NoRotationNeeded(io)
306            | InitAndRotationCheck::RotationNeeded(io, _) => io,
307        }
308    }
309
310    /// Convenience constructor to make an InitAndRotationCheck::RotationNeeded from an IronOxide
311    /// and an `EitherOrBoth<UserId, Vec1<GroupId>>` directly.
312    pub fn new_rotation_needed(
313        io: T,
314        rotations_needed: EitherOrBoth<UserId, Vec1<GroupId>>,
315    ) -> InitAndRotationCheck<T> {
316        InitAndRotationCheck::RotationNeeded(io, PrivateKeyRotationCheckResult { rotations_needed })
317    }
318}
319
320/// number of bytes that can be read from `IronOxide.rng` before it is reseeded. 1 MB
321const BYTES_BEFORE_RESEEDING: u64 = 1024 * 1024;
322
323/// Provides soft rotation capabilities for user and group keys
324#[derive(Clone, Debug, Eq, Hash, PartialEq)]
325pub struct PrivateKeyRotationCheckResult {
326    pub rotations_needed: EitherOrBoth<UserId, Vec1<GroupId>>,
327}
328
329impl PrivateKeyRotationCheckResult {
330    pub fn user_rotation_needed(&self) -> Option<&UserId> {
331        match &self.rotations_needed {
332            EitherOrBoth::Left(u) | EitherOrBoth::Both(u, _) => Some(u),
333            _ => None,
334        }
335    }
336
337    pub fn group_rotation_needed(&self) -> Option<&Vec1<GroupId>> {
338        match &self.rotations_needed {
339            EitherOrBoth::Right(groups) | EitherOrBoth::Both(_, groups) => Some(groups),
340            _ => None,
341        }
342    }
343}
344
345/// Initializes the IronOxide SDK with a device.
346///
347/// Verifies that the provided user/segment exists and the provided device keys are valid and
348/// exist for the provided account.
349pub async fn initialize(
350    device_context: &DeviceContext,
351    config: &IronOxideConfig,
352) -> Result<IronOxide> {
353    internal::add_optional_timeout(
354        internal::user_api::user_get_current(device_context.auth()),
355        config.sdk_operation_timeout,
356        SdkOperation::InitializeSdk,
357    )
358    .await?
359    .map(|current_user| IronOxide::create(&current_user, device_context, config))
360    .map_err(|e: IronOxideErr| IronOxideErr::InitializeError(e.to_string()))
361}
362
363/// Finds the groups that the caller is an admin of that need rotation and
364/// forms an InitAndRotationCheck from the user/groups needing rotation.
365fn check_groups_and_collect_rotation<T>(
366    groups: &[internal::group_api::GroupMetaResult],
367    user_needs_rotation: bool,
368    account_id: UserId,
369    ironoxide: T,
370) -> InitAndRotationCheck<T> {
371    use EitherOrBoth::{Both, Left, Right};
372    let groups_needing_rotation = groups
373        .iter()
374        .filter(|meta_result| meta_result.needs_rotation() == Some(true))
375        .map(|meta_result| meta_result.id().to_owned())
376        .collect::<Vec<_>>();
377    // If this is a Some, there are groups needing rotation
378    let maybe_groups_needing_rotation = Vec1::try_from_vec(groups_needing_rotation).ok();
379    match (user_needs_rotation, maybe_groups_needing_rotation) {
380        (false, None) => InitAndRotationCheck::NoRotationNeeded(ironoxide),
381        (true, None) => InitAndRotationCheck::new_rotation_needed(ironoxide, Left(account_id)),
382        (false, Some(groups)) => {
383            InitAndRotationCheck::new_rotation_needed(ironoxide, Right(groups))
384        }
385        (true, Some(groups)) => {
386            InitAndRotationCheck::new_rotation_needed(ironoxide, Both(account_id, groups))
387        }
388    }
389}
390
391/// Initializes the IronOxide SDK with a device and checks for necessary private key rotations
392///
393/// Checks to see if the user that owns this `DeviceContext` is marked for private key rotation,
394/// or if any of the groups that the user is an admin of are marked for private key rotation.
395pub async fn initialize_check_rotation(
396    device_context: &DeviceContext,
397    config: &IronOxideConfig,
398) -> Result<InitAndRotationCheck<IronOxide>> {
399    let (curr_user, group_list_result) = add_optional_timeout(
400        futures::future::try_join(
401            internal::user_api::user_get_current(device_context.auth()),
402            internal::group_api::list(device_context.auth(), None),
403        ),
404        config.sdk_operation_timeout,
405        SdkOperation::InitializeSdkCheckRotation,
406    )
407    .await??;
408
409    let ironoxide = IronOxide::create(&curr_user, device_context, config);
410    let user_groups = group_list_result.result();
411
412    Ok(check_groups_and_collect_rotation(
413        user_groups,
414        curr_user.needs_rotation(),
415        curr_user.account_id().to_owned(),
416        ironoxide,
417    ))
418}
419
420impl IronOxide {
421    /// DeviceContext that was used to create this SDK instance
422    pub fn device(&self) -> &DeviceContext {
423        &self.device
424    }
425
426    /// Clears all entries from the policy cache.
427    ///
428    /// Returns the number of entries cleared from the cache.
429    pub fn clear_policy_cache(&self) -> usize {
430        let size = self.policy_eval_cache.len();
431        self.policy_eval_cache.pin().clear();
432        size
433    }
434
435    /// Create an IronOxide instance. Depends on the system having enough entropy to seed a RNG.
436    fn create(
437        curr_user: &UserResult,
438        device_context: &DeviceContext,
439        config: &IronOxideConfig,
440    ) -> IronOxide {
441        IronOxide {
442            config: config.clone(),
443            recrypt: Arc::new(Recrypt::new()),
444            device: device_context.clone(),
445            user_master_pub_key: curr_user.user_public_key().to_owned(),
446            rng: Mutex::new(ReseedingRng::new(
447                rand_chacha::ChaChaCore::from_entropy(),
448                BYTES_BEFORE_RESEEDING,
449                OsRng,
450            )),
451            policy_eval_cache: HashMap::new(),
452        }
453    }
454
455    /// Rotate the private key of the calling user and all groups they are an administrator of where needs_rotation is true.
456    /// Note that this function has the potential to take much longer than other functions, as rotation will be done
457    /// individually on each user/group. If rotation is only needed for a specific group, it is strongly recommended
458    /// to call [user_rotate_private_key](user\/trait.UserOps.html#tymethod.user_rotate_private_key) or
459    /// [group_rotate_private_key](group\/trait.GroupOps.html#tymethod.group_rotate_private_key) instead.
460    /// # Arguments
461    /// - `rotations` - PrivateKeyRotationCheckResult that holds all users and groups to be rotated
462    /// - `password` - Password to unlock the current user's user master key
463    /// - `timeout` - timeout for rotate_all. This is a separate timeout from the SDK-wide timeout as it is
464    ///   expected that this operation might take significantly longer than other operations.
465    pub async fn rotate_all(
466        &self,
467        rotations: &PrivateKeyRotationCheckResult,
468        password: &str,
469        timeout: Option<std::time::Duration>,
470    ) -> Result<(
471        Option<UserUpdatePrivateKeyResult>,
472        Option<Vec<GroupUpdatePrivateKeyResult>>,
473    )> {
474        let valid_password: internal::Password = password.try_into()?;
475        let user_future = rotations.user_rotation_needed().map(|_| {
476            internal::user_api::user_rotate_private_key(
477                &self.recrypt,
478                valid_password,
479                self.device().auth(),
480            )
481        });
482        let group_futures = rotations.group_rotation_needed().map(|groups| {
483            let group_futures = groups
484                .into_iter()
485                .map(|group_id| {
486                    internal::group_api::group_rotate_private_key(
487                        &self.recrypt,
488                        self.device().auth(),
489                        group_id,
490                        self.device().device_private_key(),
491                    )
492                })
493                .collect::<Vec<_>>();
494            futures::future::join_all(group_futures)
495        });
496        let user_opt_future: futures::future::OptionFuture<_> = user_future.into();
497        let group_opt_future: futures::future::OptionFuture<_> = group_futures.into();
498        let (user_opt_result, group_opt_vec_result) = add_optional_timeout(
499            futures::future::join(user_opt_future, group_opt_future),
500            timeout,
501            SdkOperation::RotateAll,
502        )
503        .await?;
504        let group_opt_result_vec = group_opt_vec_result.map(|g| g.into_iter().collect());
505        Ok((
506            user_opt_result.transpose()?,
507            group_opt_result_vec.transpose()?,
508        ))
509    }
510}