use crate::internal::{
group_api::GroupId,
rest::{Authorization, SignatureUrlString},
user_api::UserId,
};
use base64::engine::Engine;
use base64::prelude::BASE64_STANDARD;
use futures::Future;
use lazy_static::lazy_static;
use log::error;
use protobuf::{self, Error as ProtobufError};
use quick_error::quick_error;
use recrypt::api::{
CryptoOps, Ed25519, Hashable, KeyGenOps, Plaintext, PrivateKey as RecryptPrivateKey,
PublicKey as RecryptPublicKey, RandomBytes, Recrypt, RecryptErr, Sha256,
SigningKeypair as RecryptSigningKeypair,
};
use regex::Regex;
use reqwest::Method;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::{
convert::{TryFrom, TryInto},
fmt::{Error, Formatter},
result::Result,
sync::{Mutex, MutexGuard},
};
use time::OffsetDateTime;
pub mod document_api;
pub mod group_api;
mod rest;
pub mod user_api;
pub use rest::IronCoreRequest;
lazy_static! {
pub static ref URL_STRING: String = match std::env::var("IRONCORE_ENV") {
Ok(url) => match url.to_lowercase().as_ref() {
"stage" => "https://api-staging.ironcorelabs.com/api/1/",
"prod" => "https://api.ironcorelabs.com/api/1/",
url_choice => url_choice,
}
.to_string(),
_ => "https://api.ironcorelabs.com/api/1/".to_string(),
};
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum RequestErrorCode {
UserVerify,
UserCreate,
UserUpdate,
UserDeviceAdd,
UserDeviceDelete,
UserDeviceList,
UserKeyList,
UserKeyUpdate,
UserGetCurrent,
GroupCreate,
GroupDelete,
GroupList,
GroupGet,
GroupAddMember,
GroupUpdate,
GroupMemberRemove,
GroupAdminRemove,
GroupKeyUpdate,
DocumentList,
DocumentGet,
DocumentCreate,
DocumentUpdate,
DocumentGrantAccess,
DocumentRevokeAccess,
EdekTransform,
PolicyGet,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum SdkOperation {
InitializeSdk,
InitializeSdkCheckRotation,
RotateAll,
DocumentList,
DocumentGetMetadata,
DocumentEncrypt,
DocumentUpdateBytes,
DocumentDecrypt,
DocumentUpdateName,
DocumentGrantAccess,
DocumentRevokeAccess,
DocumentEncryptUnmanaged,
DocumentDecryptUnmanaged,
UserCreate,
UserListDevices,
GenerateNewDevice,
UserDeleteDevice,
UserVerify,
UserGetPublicKey,
UserRotatePrivateKey,
UserChangePassword,
GroupList,
GroupCreate,
GroupGetMetadata,
GroupDelete,
GroupUpdateName,
GroupAddMembers,
GroupRemoveMembers,
GroupAddAdmins,
GroupRemoveAdmins,
GroupRotatePrivateKey,
}
impl std::fmt::Display for SdkOperation {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
write!(f, "'{self:?}'")
}
}
quick_error! {
#[derive(Debug)]
#[non_exhaustive]
pub enum IronOxideErr {
ValidationError(field_name: String, err: String) {
display("'{}' failed validation with the error '{}'", field_name, err)
}
DocumentHeaderParseFailure(message: String) {
display("{}", message)
}
WrongSizeError(actual_size: Option<usize>, expected_size: Option<usize>) {
}
KeyGenerationError {
display("Key generation failed")
}
AesError(err: ring::error::Unspecified) {
source(err)
}
AesEncryptedDocSizeError{
display("Provided document is not long enough to be an encrypted document.")
}
InvalidRecryptEncryptedValue(msg: String) {
display("Got an unexpected Recrypt EncryptedValue: '{}'", msg)
}
RecryptError(msg: String) {
display("Recrypt operation failed with error '{}'", msg)
}
UserDoesNotExist(msg: String) {
display("Operation failed with error '{}'", msg)
}
UserOrGroupDoesNotExist(user_or_group: document_api::UserOrGroup) {
display("User or group {} does not exist.", user_or_group)
}
InitializeError(cause: String) {
display("SDK initialization failed. Underlying cause '{}'", cause)
}
RequestError { message: String, code: RequestErrorCode, http_status: Option<u16> } {
display("Request failed with HTTP status code '{:?}' message '{}' and code '{:?}'", http_status, message, code)
}
RequestServerErrors {errors: Vec<rest::ServerError>, code: RequestErrorCode, http_status: Option<u16> } {
display("Request failed with HTTP status code '{:?}' errors list is '{:?}' and code '{:?}'", http_status, errors, code)
}
MissingTransformBlocks {
display("Expected at least one TransformBlock in transformed value but received none.")
}
NotGroupAdmin(id: GroupId) {
display("You are not an administrator of group '{}'", id.id())
}
PolicyDoesNotExist {
display("No policy is defined. Please visit https://admin.ironcorelabs.com/policy to set a policy")
}
ProtobufSerdeError(err: ProtobufError) {
source(err)
}
ProtobufValidationError(msg: String) {
display("Protobuf validation failed with '{}'", msg)
}
UnmanagedDecryptionError(edek_doc_id: String, edek_segment_id: i32,
edoc_doc_id: String, edoc_segment_id: i32) {
display("Edeks and EncryptedDocument do not match. \
Edeks are for DocumentId({}) and SegmentId({}) and\
Encrypted Document is DocumentId({}) and SegmentId({})",
edek_doc_id, edek_segment_id, edoc_doc_id, edoc_segment_id)
}
UserPrivateKeyRotationError(msg: String) {
display("User private key rotation failed with '{}'", msg)
}
GroupPrivateKeyRotationError(msg: String) {
display("Group private key rotation failed with '{}'", msg)
}
OperationTimedOut{operation: SdkOperation, duration: std::time::Duration} {
display("Operation {} timed out after {}ms", operation, duration.as_millis())
}
JoinError(msg: String) {
display("{}", msg)
}
}
}
impl From<IronOxideErr> for String {
fn from(err: IronOxideErr) -> Self {
err.to_string()
}
}
impl From<RecryptErr> for IronOxideErr {
fn from(recrypt_err: RecryptErr) -> Self {
match recrypt_err {
RecryptErr::InputWrongSize(_, expected_size) => {
IronOxideErr::WrongSizeError(None, Some(expected_size))
}
RecryptErr::InvalidPublicKey(_) => IronOxideErr::KeyGenerationError,
other_recrypt_err => IronOxideErr::RecryptError(other_recrypt_err.to_string()),
}
}
}
impl From<ProtobufError> for IronOxideErr {
fn from(e: ProtobufError) -> Self {
IronOxideErr::ProtobufSerdeError(e)
}
}
impl From<recrypt::nonemptyvec::NonEmptyVecError> for IronOxideErr {
fn from(_: recrypt::nonemptyvec::NonEmptyVecError) -> Self {
IronOxideErr::MissingTransformBlocks
}
}
impl From<tokio::task::JoinError> for IronOxideErr {
fn from(e: tokio::task::JoinError) -> Self {
IronOxideErr::JoinError(e.to_string())
}
}
const NAME_AND_ID_MAX_LEN: usize = 100;
pub fn validate_id(id: &str, id_type: &str) -> Result<String, IronOxideErr> {
let id_regex = Regex::new("^[a-zA-Z0-9_.$#|@/:;=+'-]+$").expect("regex is valid");
let trimmed_id = id.trim();
if trimmed_id.is_empty() || trimmed_id.len() > NAME_AND_ID_MAX_LEN {
Err(IronOxideErr::ValidationError(
id_type.to_string(),
format!("'{trimmed_id}' must have length between 1 and 100"),
))
} else if !id_regex.is_match(trimmed_id) {
Err(IronOxideErr::ValidationError(
id_type.to_string(),
format!("'{trimmed_id}' contains invalid characters"),
))
} else {
Ok(trimmed_id.to_string())
}
}
pub fn validate_name(name: &str, name_type: &str) -> Result<String, IronOxideErr> {
let trimmed_name = name.trim();
if trimmed_name.trim().is_empty() || trimmed_name.len() > NAME_AND_ID_MAX_LEN {
Err(IronOxideErr::ValidationError(
name_type.to_string(),
format!("'{trimmed_name}' must have length between 1 and 100"),
))
} else {
Ok(trimmed_name.trim().to_string())
}
}
pub mod auth_v2 {
use time::OffsetDateTime;
use super::*;
pub struct AuthV2Builder<'a> {
pub(in crate::internal::auth_v2) req_auth: &'a RequestAuth,
pub(in crate::internal::auth_v2) timestamp: OffsetDateTime,
}
impl<'a> AuthV2Builder<'a> {
pub fn new(req_auth: &'a RequestAuth, timestamp: OffsetDateTime) -> AuthV2Builder {
AuthV2Builder {
req_auth,
timestamp,
}
}
pub fn finish_with(
&self,
sig_url: SignatureUrlString,
method: Method,
body_bytes: Option<&'a [u8]>,
) -> Authorization<'a> {
self.req_auth
.create_signature_v2(self.timestamp, sig_url, method, body_bytes)
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestAuth {
account_id: UserId,
segment_id: usize,
signing_private_key: DeviceSigningKeyPair,
#[serde(skip_serializing, skip_deserializing)]
pub(crate) request: IronCoreRequest,
}
impl RequestAuth {
pub fn create_signature_v2<'a>(
&'a self,
current_time: OffsetDateTime,
sig_url: SignatureUrlString,
method: Method,
body: Option<&'a [u8]>,
) -> Authorization<'a> {
Authorization::create_signatures_v2(
current_time,
self.segment_id,
&self.account_id,
method,
sig_url,
body,
&self.signing_private_key,
)
}
pub fn account_id(&self) -> &UserId {
&self.account_id
}
pub fn segment_id(&self) -> usize {
self.segment_id
}
pub fn signing_private_key(&self) -> &DeviceSigningKeyPair {
&self.signing_private_key
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeviceContext {
#[serde(flatten)]
auth: RequestAuth,
device_private_key: PrivateKey,
}
impl DeviceContext {
pub fn new(
account_id: UserId,
segment_id: usize,
device_private_key: PrivateKey,
signing_private_key: DeviceSigningKeyPair,
) -> DeviceContext {
DeviceContext {
auth: RequestAuth {
account_id,
segment_id,
signing_private_key,
request: IronCoreRequest::default(),
},
device_private_key,
}
}
pub(crate) fn auth(&self) -> &RequestAuth {
&self.auth
}
pub fn account_id(&self) -> &UserId {
&self.auth.account_id
}
pub fn segment_id(&self) -> usize {
self.auth.segment_id
}
pub fn signing_private_key(&self) -> &DeviceSigningKeyPair {
&self.auth.signing_private_key
}
pub fn device_private_key(&self) -> &PrivateKey {
&self.device_private_key
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TransformKey(recrypt::api::TransformKey);
impl From<recrypt::api::TransformKey> for TransformKey {
fn from(tk: recrypt::api::TransformKey) -> Self {
TransformKey(tk)
}
}
impl Hashable for TransformKey {
fn to_bytes(&self) -> Vec<u8> {
self.0.to_bytes()
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SchnorrSignature(recrypt::api::SchnorrSignature);
impl From<recrypt::api::SchnorrSignature> for SchnorrSignature {
fn from(s: recrypt::api::SchnorrSignature) -> Self {
SchnorrSignature(s)
}
}
impl From<SchnorrSignature> for Vec<u8> {
fn from(sig: SchnorrSignature) -> Self {
sig.0.bytes().to_vec()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct PublicKey(RecryptPublicKey);
impl PublicKey {
fn to_bytes_x_y(&self) -> (Vec<u8>, Vec<u8>) {
let (x, y) = self.0.bytes_x_y();
(x.to_vec(), y.to_vec())
}
pub fn new_from_slice(bytes: (&[u8], &[u8])) -> Result<Self, IronOxideErr> {
let re_pub = RecryptPublicKey::new_from_slice(bytes)?;
Ok(PublicKey(re_pub))
}
pub fn as_bytes(&self) -> Vec<u8> {
let (mut x, mut y) = self.to_bytes_x_y();
x.append(&mut y);
x
}
}
impl From<RecryptPublicKey> for PublicKey {
fn from(recrypt_pub: RecryptPublicKey) -> Self {
PublicKey(recrypt_pub)
}
}
impl From<PublicKey> for RecryptPublicKey {
fn from(public_key: PublicKey) -> Self {
public_key.0
}
}
impl From<&PublicKey> for RecryptPublicKey {
fn from(public_key: &PublicKey) -> Self {
public_key.0
}
}
impl From<PublicKey> for crate::proto::transform::PublicKey {
fn from(pubk: PublicKey) -> Self {
crate::proto::transform::PublicKey {
x: pubk.to_bytes_x_y().0.into(),
y: pubk.to_bytes_x_y().1.into(),
..Default::default()
}
}
}
impl TryFrom<&[u8]> for PublicKey {
type Error = IronOxideErr;
fn try_from(key_bytes: &[u8]) -> Result<PublicKey, IronOxideErr> {
if key_bytes.len() == RecryptPublicKey::ENCODED_SIZE_BYTES {
PublicKey::new_from_slice(key_bytes.split_at(RecryptPublicKey::ENCODED_SIZE_BYTES / 2))
} else {
Err(IronOxideErr::WrongSizeError(
Some(RecryptPublicKey::ENCODED_SIZE_BYTES),
Some(key_bytes.len()),
))
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct PrivateKey(RecryptPrivateKey);
impl PrivateKey {
const BYTES_SIZE: usize = RecryptPrivateKey::ENCODED_SIZE_BYTES;
pub fn as_bytes(&self) -> &[u8; PrivateKey::BYTES_SIZE] {
self.0.bytes()
}
fn recrypt_key(&self) -> &RecryptPrivateKey {
&self.0
}
fn augment<F: FnOnce(String) -> IronOxideErr>(
&self,
augmenting_key: &AugmentationFactor,
error_fn: F,
) -> Result<PrivateKey, IronOxideErr> {
let zero: RecryptPrivateKey = RecryptPrivateKey::new([0u8; 32]);
if RecryptPrivateKey::from(augmenting_key.clone()) == zero {
Err(error_fn("Augmenting key cannot be zero".into()))
} else if RecryptPrivateKey::from(augmenting_key.clone()) == self.0 {
Err(error_fn(
"PrivateKey augmentation failed with a zero value".into(),
))
} else {
let augmented_key = self.0.augment_minus(&augmenting_key.clone().into());
Ok(augmented_key.into())
}
}
fn augment_user(
&self,
augmenting_key: &AugmentationFactor,
) -> Result<PrivateKey, IronOxideErr> {
self.augment(augmenting_key, IronOxideErr::UserPrivateKeyRotationError)
}
fn augment_group(
&self,
augmenting_key: &AugmentationFactor,
) -> Result<PrivateKey, IronOxideErr> {
self.augment(augmenting_key, IronOxideErr::GroupPrivateKeyRotationError)
}
}
impl From<RecryptPrivateKey> for PrivateKey {
fn from(recrypt_priv: RecryptPrivateKey) -> Self {
PrivateKey(recrypt_priv)
}
}
impl From<PrivateKey> for RecryptPrivateKey {
fn from(priv_key: PrivateKey) -> Self {
priv_key.0
}
}
impl From<[u8; 32]> for PrivateKey {
fn from(bytes: [u8; 32]) -> Self {
PrivateKey(RecryptPrivateKey::new(bytes))
}
}
impl TryFrom<&[u8]> for PrivateKey {
type Error = IronOxideErr;
fn try_from(key_bytes: &[u8]) -> Result<PrivateKey, IronOxideErr> {
RecryptPrivateKey::new_from_slice(key_bytes)
.map(PrivateKey)
.map_err(|e| e.into())
}
}
impl Serialize for PrivateKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&BASE64_STANDARD.encode(self.0.bytes()))
}
}
impl<'de> Deserialize<'de> for PrivateKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
let keys_bytes = BASE64_STANDARD
.decode(s)
.map_err(|e| Error::custom(e.to_string()))?;
PrivateKey::try_from(&keys_bytes[..]).map_err(|e| Error::custom(e.to_string()))
}
}
#[derive(Clone, Debug)]
pub(crate) struct AugmentationFactor(PrivateKey);
impl AugmentationFactor {
pub fn generate_new<R: KeyGenOps>(recrypt: &R) -> AugmentationFactor {
AugmentationFactor(recrypt.random_private_key().into())
}
pub fn as_bytes(&self) -> &[u8; 32] {
self.0.as_bytes()
}
}
impl From<AugmentationFactor> for RecryptPrivateKey {
fn from(aug: AugmentationFactor) -> Self {
(aug.0).0
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DeviceSigningKeyPair(RecryptSigningKeypair);
impl DeviceSigningKeyPair {
pub fn sign(&self, payload: &[u8]) -> [u8; 64] {
self.0.sign(&payload).into()
}
pub fn as_bytes(&self) -> &[u8; 64] {
self.0.bytes()
}
pub fn public_key(&self) -> [u8; 32] {
self.0.public_key().into()
}
}
impl From<&DeviceSigningKeyPair> for RecryptSigningKeypair {
fn from(dsk: &DeviceSigningKeyPair) -> RecryptSigningKeypair {
dsk.0.clone()
}
}
impl From<RecryptSigningKeypair> for DeviceSigningKeyPair {
fn from(rsk: RecryptSigningKeypair) -> DeviceSigningKeyPair {
DeviceSigningKeyPair(rsk)
}
}
impl TryFrom<&[u8]> for DeviceSigningKeyPair {
type Error = IronOxideErr;
fn try_from(signing_key_bytes: &[u8]) -> Result<DeviceSigningKeyPair, Self::Error> {
RecryptSigningKeypair::from_byte_slice(signing_key_bytes)
.map(DeviceSigningKeyPair)
.map_err(|e| {
IronOxideErr::ValidationError("DeviceSigningKeyPair".to_string(), e.to_string())
})
}
}
impl Serialize for DeviceSigningKeyPair {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let base64 = BASE64_STANDARD.encode(self.0.bytes());
serializer.serialize_str(&base64)
}
}
impl<'de> Deserialize<'de> for DeviceSigningKeyPair {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
let s = String::deserialize(deserializer)?;
let keys_bytes = BASE64_STANDARD
.decode(s)
.map_err(|e| Error::custom(e.to_string()))?;
DeviceSigningKeyPair::try_from(&keys_bytes[..]).map_err(|e| Error::custom(e.to_string()))
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Password(String);
impl TryFrom<&str> for Password {
type Error = IronOxideErr;
fn try_from(maybe_password: &str) -> Result<Self, Self::Error> {
if !maybe_password.trim().is_empty() {
Ok(Password(maybe_password.to_string()))
} else {
Err(IronOxideErr::ValidationError(
"maybe_password".to_string(),
"length must be > 0".to_string(),
))
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct WithKey<T> {
pub(crate) id: T,
pub(crate) public_key: PublicKey,
}
impl<T> WithKey<T> {
pub fn new(id: T, public_key: PublicKey) -> WithKey<T> {
WithKey { id, public_key }
}
}
pub(crate) fn take_lock<T>(m: &Mutex<T>) -> MutexGuard<T> {
m.lock().unwrap_or_else(|e| {
let error = format!("Error when acquiring lock: {e}");
error!("{}", error);
panic!("{}", error);
})
}
fn augment_private_key_with_retry<R: KeyGenOps>(
recrypt: &R,
priv_key: &PrivateKey,
) -> Result<(PrivateKey, AugmentationFactor), IronOxideErr> {
let aug_private_key = || {
let aug_factor = AugmentationFactor::generate_new(recrypt);
priv_key.augment_user(&aug_factor).map(|p| (p, aug_factor))
};
aug_private_key().or_else(|_| aug_private_key())
}
fn gen_plaintext_and_aug_with_retry<R: CryptoOps>(
recrypt: &R,
priv_key: &PrivateKey,
) -> Result<(Plaintext, AugmentationFactor), IronOxideErr> {
let aug_private_key = || -> Result<(Plaintext, AugmentationFactor), IronOxideErr> {
let new_plaintext = recrypt.gen_plaintext();
let new_group_private_key = recrypt.derive_private_key(&new_plaintext);
let new_key_aug = AugmentationFactor(new_group_private_key.into());
let aug_factor = priv_key.augment_group(&new_key_aug)?;
Ok((new_plaintext, AugmentationFactor(aug_factor)))
};
aug_private_key().or_else(|_| aug_private_key())
}
pub async fn add_optional_timeout<F: Future>(
f: F,
timeout: Option<std::time::Duration>,
op: SdkOperation,
) -> Result<F::Output, IronOxideErr> {
use futures::future::TryFutureExt;
let result = match timeout {
Some(d) => {
tokio::time::timeout(d, f)
.map_err(|_| IronOxideErr::OperationTimedOut {
operation: op,
duration: d,
})
.await?
}
None => f.await,
};
Ok(result)
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use double::*;
use galvanic_assert::{matchers::*, *};
use std::fmt::Debug;
use tokio::time::Duration;
use vec1::vec1;
pub fn contains<'a>(expected: &'a str) -> Box<dyn Matcher<String> + 'a> {
Box::new(move |actual: &String| {
let builder = MatchResultBuilder::for_("contains");
if actual.contains(expected) {
builder.matched()
} else {
let expected_string: String = expected.to_string();
builder.failed_comparison(actual, &expected_string)
}
})
}
pub fn length<'a, I, T>(expected: &'a usize) -> Box<dyn Matcher<I> + 'a>
where
T: 'a,
&'a I: Debug + Sized + IntoIterator<Item = &'a T> + 'a,
{
Box::new(move |actual: &'a I| {
let actual_list: Vec<_> = actual.into_iter().collect();
let builder = MatchResultBuilder::for_("contains");
if &actual_list.len() == expected {
builder.matched()
} else {
builder.failed_because(&format!(
"Expected '{:?}' to have length of {} but found length of {}",
actual,
expected,
actual_list.len()
))
}
})
}
#[test]
fn serde_devicecontext_roundtrip() -> Result<(), IronOxideErr> {
let priv_key: recrypt::api::PrivateKey = recrypt::api::PrivateKey::new_from_slice(
BASE64_STANDARD
.decode("bzb0Rlg0u7gx9wDuk1ppRI77OH/0ferXleenJ3Ag6Jg=")
.unwrap()
.as_slice(),
)?;
let dev_keys = recrypt::api::SigningKeypair::from_byte_slice(&[
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 138, 136, 227, 221, 116, 9, 241, 149, 253, 82, 219, 45, 60, 186, 93, 114, 202,
103, 9, 191, 29, 148, 18, 27, 243, 116, 136, 1, 180, 15, 111, 92,
])
.unwrap();
let context = DeviceContext::new(
"account_id".try_into()?,
22,
priv_key.into(),
DeviceSigningKeyPair::from(dev_keys),
);
let json = serde_json::to_string(&context).unwrap();
let expect_json = r#"{"accountId":"account_id","segmentId":22,"signingPrivateKey":"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGKiOPddAnxlf1S2y08ul1yymcJvx2UEhvzdIgBtA9vXA==","devicePrivateKey":"bzb0Rlg0u7gx9wDuk1ppRI77OH/0ferXleenJ3Ag6Jg="}"#;
assert_eq!(json, expect_json);
let de: DeviceContext = serde_json::from_str(&json).unwrap();
assert_eq!(context.account_id(), de.account_id());
assert_eq!(
context.auth.signing_private_key.as_bytes().to_vec(),
de.auth.signing_private_key.as_bytes().to_vec()
);
assert_eq!(
context.device_private_key.as_bytes().to_vec(),
de.device_private_key.as_bytes().to_vec()
);
Ok(())
}
#[test]
fn validate_id_success() {
let valid_id = "abcABC012_.$#|@/:;=+'-";
let id = validate_id(valid_id, "id_type");
assert_that!(&id, is_variant!(Ok));
assert_that!(&id.unwrap(), eq(valid_id.to_string()))
}
#[test]
fn valid_id_whitespace() {
let valid_id = " abc212 ";
let id = validate_id(valid_id, "id_type");
assert_that!(&id, is_variant!(Ok));
assert_that!(&id.unwrap(), eq("abc212".to_string()))
}
#[test]
fn validate_id_failure() {
let invalid_id = "with spaces";
let id_type = "id_type";
let id = validate_id(invalid_id, id_type);
assert_that!(&id, is_variant!(Err));
let validation_error = id.unwrap_err();
assert_that!(
&validation_error,
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(&format!("{}", validation_error), contains(id_type));
assert_that!(&format!("{}", validation_error), contains(invalid_id));
}
#[test]
fn validate_id_all_whitespace() {
let invalid_id = " ";
let id_type = "id_type";
let id = validate_id(invalid_id, id_type);
assert_that!(&id, is_variant!(Err));
let validation_error = id.unwrap_err();
assert_that!(
&validation_error,
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(&format!("{}", validation_error), contains(id_type));
}
#[test]
fn validate_name_success() {
let valid_name = "name with any char _.$#|@/:;=+'-";
let id = validate_name(valid_name, "name_type");
assert_that!(&id, is_variant!(Ok));
assert_that!(&id.unwrap(), eq(valid_name.to_string()))
}
#[test]
fn validate_name_surrounding_whitespace() {
let valid_name = " a good name ";
let id = validate_name(valid_name, "name_type");
assert_that!(&id, is_variant!(Ok));
assert_that!(&id.unwrap(), eq("a good name".to_string()))
}
#[test]
fn validate_name_failure() {
let name_type = "name_type";
let invalid_name = "too many chars 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
let name = validate_name(invalid_name, name_type);
assert_that!(&name, is_variant!(Err));
let validation_error = name.unwrap_err();
assert_that!(
&validation_error,
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(&format!("{}", validation_error), contains(invalid_name));
assert_that!(&format!("{}", validation_error), contains(name_type));
}
#[test]
fn validate_name_all_whitespace() {
let invalid_name = " ";
let name_type = "name_type";
let name = validate_name(invalid_name, name_type);
assert_that!(&name, is_variant!(Err));
let validation_error = name.unwrap_err();
assert_that!(
&validation_error,
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(&format!("{}", validation_error), contains(name_type));
}
#[test]
fn passphrase_validation() {
let result = Password::try_from("");
assert!(result.is_err())
}
#[test]
fn encode_proto_public_key() -> Result<(), IronOxideErr> {
let recr = recrypt::api::Recrypt::new();
let (_, re_pubk) = recr.generate_key_pair()?;
let pubk: PublicKey = re_pubk.into();
let proto_pubk: crate::proto::transform::PublicKey = pubk.clone().into();
assert_eq!(
(&pubk.to_bytes_x_y().0, &pubk.to_bytes_x_y().1),
(&proto_pubk.x.to_vec(), &proto_pubk.y.to_vec())
);
Ok(())
}
#[test]
fn public_key_try_from_slice() -> Result<(), IronOxideErr> {
let recr = recrypt::api::Recrypt::new();
let (_, re_pubk) = recr.generate_key_pair()?;
let pubk: PublicKey = re_pubk.into();
let pubk2: PublicKey = pubk.as_bytes().as_slice().try_into()?;
assert_eq!(pubk, pubk2);
Ok(())
}
#[test]
fn public_key_try_from_slice_invalid() {
let bytes = [1u8; 8];
let maybe_public_key: Result<PublicKey, IronOxideErr> = bytes[..].try_into();
assert!(maybe_public_key.is_err())
}
pub fn gen_priv_key() -> PrivateKey {
let recr = recrypt::api::Recrypt::new();
let (re_privk, _) = recr.generate_key_pair().unwrap();
re_privk.into()
}
#[test]
fn private_key_augment_with_self_is_none() {
let privk = gen_priv_key();
let result = privk.augment_user(&AugmentationFactor(privk.clone()));
assert_that!(&result, is_variant!(Err));
assert_that!(
&result.unwrap_err(),
is_variant!(IronOxideErr::UserPrivateKeyRotationError)
)
}
#[test]
fn private_key_augmentation_is_augment_minus() {
let p1 = gen_priv_key();
let p2 = gen_priv_key();
let p3 = p1.0.augment_minus(&p2.0);
let aug_p = p1.augment_user(&AugmentationFactor(p2)).unwrap();
assert_eq!(aug_p.0, p3)
}
#[test]
fn private_key_augmentation_aug_key_of_zero_is_err() {
let priv_key_orig = gen_priv_key();
let zero_aug_factor = AugmentationFactor(PrivateKey(RecryptPrivateKey::new([0u8; 32])));
let new_priv_key = priv_key_orig.augment_user(&zero_aug_factor);
assert_that!(&new_priv_key, is_variant!(Err));
assert_that!(
&new_priv_key.unwrap_err(),
is_variant!(IronOxideErr::UserPrivateKeyRotationError)
)
}
mock_trait!(
MockKeyGenOps,
random_private_key() -> recrypt::api::PrivateKey
);
impl KeyGenOps for MockKeyGenOps {
fn compute_public_key(
&self,
_private_key: &RecryptPrivateKey,
) -> Result<RecryptPublicKey, RecryptErr> {
unimplemented!()
}
mock_method!(random_private_key(&self) -> RecryptPrivateKey);
fn generate_key_pair(&self) -> Result<(RecryptPrivateKey, RecryptPublicKey), RecryptErr> {
unimplemented!()
}
fn generate_transform_key(
&self,
_from_private_key: &RecryptPrivateKey,
_to_public_key: &RecryptPublicKey,
_signing_keypair: &recrypt::api::SigningKeypair,
) -> Result<recrypt::api::TransformKey, RecryptErr> {
unimplemented!()
}
}
mock_trait!(MockCryptoOps,
gen_plaintext() -> recrypt::api::Plaintext
);
impl CryptoOps for MockCryptoOps {
fn derive_symmetric_key(
&self,
_: &recrypt::api::Plaintext,
) -> recrypt::api::DerivedSymmetricKey {
unimplemented!()
}
mock_method!(gen_plaintext(&self) -> recrypt::api::Plaintext);
fn transform(
&self,
_: recrypt::api::EncryptedValue,
_: recrypt::api::TransformKey,
_: &recrypt::api::SigningKeypair,
) -> std::result::Result<recrypt::api::EncryptedValue, RecryptErr> {
unimplemented!()
}
fn decrypt(
&self,
_: recrypt::api::EncryptedValue,
_: &recrypt::api::PrivateKey,
) -> std::result::Result<recrypt::api::Plaintext, RecryptErr> {
unimplemented!()
}
fn encrypt(
&self,
_: &recrypt::api::Plaintext,
_: &recrypt::api::PublicKey,
_: &recrypt::api::SigningKeypair,
) -> std::result::Result<recrypt::api::EncryptedValue, RecryptErr> {
unimplemented!()
}
fn derive_private_key(&self, pt: &recrypt::api::Plaintext) -> recrypt::api::PrivateKey {
let recrypt = recrypt::api::Recrypt::new();
recrypt.derive_private_key(pt)
}
}
#[test]
fn augment_private_key_with_retry_retries_once() {
let recrypt_mock = MockKeyGenOps::default();
let good_re_private_key = RecryptPrivateKey::new([42u8; 32]); recrypt_mock.random_private_key.return_values(vec![
RecryptPrivateKey::new([0u8; 32]), good_re_private_key.clone(), ]);
let curr_priv_key = PrivateKey::from([100u8; 32]);
let expected_priv_key_bytes: [u8; 32] = [
58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58, 58,
58, 58, 58, 58, 58, 58, 58, 58, 58, 58,
];
let result = augment_private_key_with_retry(&recrypt_mock, &curr_priv_key).unwrap();
assert_eq!(
(result.0).0,
RecryptPrivateKey::new(expected_priv_key_bytes)
);
assert_eq!(((result.1).0).0, good_re_private_key)
}
#[test]
fn augment_private_key_with_retry_retries_only_once() {
let recrypt_mock = MockKeyGenOps::default();
recrypt_mock.random_private_key.return_values(vec![
RecryptPrivateKey::new([0u8; 32]), RecryptPrivateKey::new([100u8; 32]), RecryptPrivateKey::new([42u8; 32]), ]);
let curr_priv_key = PrivateKey::from([100u8; 32]);
let result = augment_private_key_with_retry(&recrypt_mock, &curr_priv_key);
assert_that!(
&result.unwrap_err(),
is_variant!(IronOxideErr::UserPrivateKeyRotationError)
);
}
#[test]
fn gen_plaintext_and_diff_with_retry_retries_once() {
let recrypt_mock = MockCryptoOps::default();
let recrypt = recrypt::api::Recrypt::new();
let bad_plaintext = recrypt.gen_plaintext();
let bad_private_key = recrypt.derive_private_key(&bad_plaintext);
let good_plaintext = recrypt.gen_plaintext();
recrypt_mock
.gen_plaintext
.return_values(vec![bad_plaintext, good_plaintext.clone()]);
let result =
gen_plaintext_and_aug_with_retry(&recrypt_mock, &bad_private_key.into()).unwrap();
assert_eq!(result.0, good_plaintext);
}
#[test]
fn gen_plaintext_and_diff_with_retry_retries_only_once() {
let recrypt_mock = MockCryptoOps::default();
let recrypt = recrypt::api::Recrypt::new();
let bad_plaintext = recrypt.gen_plaintext();
let bad_private_key = recrypt.derive_private_key(&bad_plaintext);
let good_plaintext = recrypt.gen_plaintext();
recrypt_mock.gen_plaintext.return_values(vec![
bad_plaintext.clone(),
bad_plaintext,
good_plaintext,
]);
let result = gen_plaintext_and_aug_with_retry(&recrypt_mock, &bad_private_key.into());
assert_that!(
&result.unwrap_err(),
is_variant!(IronOxideErr::GroupPrivateKeyRotationError)
);
}
#[test]
fn init_and_rotation_user_and_groups() -> Result<(), IronOxideErr> {
use crate::{
check_groups_and_collect_rotation,
internal::{
group_api::tests::create_group_meta_result, user_api::tests::create_user_result,
},
InitAndRotationCheck, IronOxide,
};
let recrypt = recrypt::api::Recrypt::new();
let (_, pub_key) = recrypt.generate_key_pair()?;
let time = OffsetDateTime::now_utc();
let create_gmr = |id: GroupId, needs_rotation: Option<bool>| {
create_group_meta_result(
id,
None,
pub_key.into(),
true,
true,
time,
time,
needs_rotation,
)
};
let de_json = r#"{"deviceId":314,"accountId":"account_id","segmentId":22,"signingPrivateKey":"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQGKiOPddAnxlf1S2y08ul1yymcJvx2UEhvzdIgBtA9vXA==","devicePrivateKey":"bzb0Rlg0u7gx9wDuk1ppRI77OH/0ferXleenJ3Ag6Jg="}"#;
let de: DeviceContext = serde_json::from_str(de_json).unwrap();
let user_id = UserId::try_from("account_id")?;
let user = create_user_result(user_id.clone(), 22, pub_key.into(), true);
let io = IronOxide::create(&user, &de, &Default::default());
let good_group_id = GroupId::try_from("group")?;
let gmr_vec = vec![
create_gmr(good_group_id.clone(), Some(true)),
create_gmr(GroupId::try_from("notthisone")?, Some(false)),
create_gmr(GroupId::try_from("northisone")?, None),
];
let init = check_groups_and_collect_rotation(&gmr_vec, true, user_id.clone(), io);
let rotation = match init {
InitAndRotationCheck::NoRotationNeeded(_) => panic!("user and group need rotation"),
InitAndRotationCheck::RotationNeeded(_, rotation) => rotation,
};
assert_eq!(
rotation.group_rotation_needed(),
Some(&vec1![good_group_id])
);
assert_eq!(rotation.user_rotation_needed(), Some(&user_id));
Ok(())
}
#[tokio::test]
async fn run_maybe_timed_sdk_op_no_timeout() -> Result<(), IronOxideErr> {
async fn get_42() -> u8 {
tokio::time::sleep(Duration::from_millis(100)).await;
42
}
let forty_two = get_42();
let result =
add_optional_timeout(forty_two, None, SdkOperation::DocumentRevokeAccess).await?;
assert_eq!(result, 42);
let forty_two = get_42();
let result = add_optional_timeout(
forty_two,
Some(Duration::from_secs(1)),
SdkOperation::DocumentRevokeAccess,
)
.await?;
assert_eq!(result, 42);
async fn get_err() -> Result<(), IronOxideErr> {
tokio::time::sleep(Duration::from_millis(100)).await;
Err(IronOxideErr::MissingTransformBlocks)
}
let err_f = get_err();
let result = add_optional_timeout(err_f, None, SdkOperation::DocumentRevokeAccess).await?;
assert!(result.is_err());
assert_that!(
&result.unwrap_err(),
is_variant!(IronOxideErr::MissingTransformBlocks)
);
let err_f = get_err();
let result = add_optional_timeout(
err_f,
Some(Duration::from_secs(1)),
SdkOperation::DocumentRevokeAccess,
)
.await?;
assert!(result.is_err());
assert_that!(
&result.unwrap_err(),
is_variant!(IronOxideErr::MissingTransformBlocks)
);
Ok(())
}
#[tokio::test]
async fn run_maybe_timed_sdk_op_with_timeout() -> Result<(), IronOxideErr> {
async fn get_42() -> u8 {
tokio::time::sleep(Duration::from_millis(100)).await;
42
}
let forty_two = get_42();
let result = add_optional_timeout(
forty_two,
Some(Duration::from_nanos(1)),
SdkOperation::DocumentRevokeAccess,
)
.await;
assert!(result.is_err());
assert_that!(
&result.unwrap_err(),
is_variant!(IronOxideErr::OperationTimedOut)
);
async fn get_err() -> Result<u8, IronOxideErr> {
tokio::time::sleep(Duration::from_millis(100)).await;
Err(IronOxideErr::MissingTransformBlocks)
}
let err_f = get_err();
let result = add_optional_timeout(
err_f,
Some(Duration::from_millis(1)),
SdkOperation::DocumentRevokeAccess,
)
.await;
assert!(result.is_err());
assert_that!(
&result.unwrap_err(),
is_variant!(IronOxideErr::OperationTimedOut)
);
Ok(())
}
}