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(¤t_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}