use crate::{
DeviceSigningKeyPair, PolicyCache,
config::{IronOxideConfig, PolicyCachingConfig},
crypto::{
aes::{self, AesEncryptedValue},
transform,
},
internal::{
self, IronOxideErr, PrivateKey, PublicKey, PublicKeyCache, RequestAuth, WithKey,
document_api::requests::UserOrGroupWithKey,
group_api::{GroupId, GroupName},
take_lock,
user_api::UserId,
validate_id, validate_name,
},
policy::PolicyGrant,
proto::transform::{
EncryptedDek as EncryptedDekP, EncryptedDekData as EncryptedDekDataP,
EncryptedDeks as EncryptedDeksP, user_or_group::UserOrGroupId as UserOrGroupIdP,
},
};
use futures::{Future, try_join};
use hex::encode;
use itertools::{Either, Itertools};
use protobuf::Message;
use rand::{self, CryptoRng};
use recrypt::{api::Plaintext, prelude::*};
use requests::{
DocumentMetaApiResponse, document_create,
document_list::{DocumentListApiResponse, DocumentListApiResponseItem},
policy_get::PolicyResponse,
};
use serde::{Deserialize, Serialize};
use std::{
convert::{TryFrom, TryInto, identity},
fmt::{Debug, Formatter},
ops::DerefMut,
sync::Mutex,
};
use time::OffsetDateTime;
pub mod file_ops;
mod requests;
const DOC_VERSION_HEADER_LENGTH: usize = 1;
const HEADER_META_LENGTH_LENGTH: usize = 2;
const CURRENT_DOCUMENT_ID_VERSION: u8 = 2;
pub(crate) fn parse_header_length(header_prefix: &[u8; 3]) -> Result<usize, IronOxideErr> {
if header_prefix[0] != CURRENT_DOCUMENT_ID_VERSION {
return Err(IronOxideErr::DocumentHeaderParseFailure(
"Document is not a supported version and may not be an encrypted file.".to_string(),
));
}
let encoded_header_size = header_prefix[1] as usize * 256 + header_prefix[2] as usize;
Ok(DOC_VERSION_HEADER_LENGTH + HEADER_META_LENGTH_LENGTH + encoded_header_size)
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct DocumentId(pub(crate) String);
impl DocumentId {
pub fn id(&self) -> &str {
&self.0
}
pub(crate) fn goo_id<R: CryptoRng>(rng: &Mutex<R>) -> DocumentId {
let mut id = [0u8; 16];
take_lock(rng).deref_mut().fill_bytes(&mut id);
DocumentId(encode(id))
}
}
impl TryFrom<&str> for DocumentId {
type Error = IronOxideErr;
fn try_from(id: &str) -> Result<Self, Self::Error> {
validate_id(id, "document_id").map(DocumentId)
}
}
impl TryFrom<String> for DocumentId {
type Error = IronOxideErr;
fn try_from(doc_id: String) -> Result<Self, Self::Error> {
doc_id.as_str().try_into()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct DocumentName(pub(crate) String);
impl DocumentName {
pub fn name(&self) -> &String {
&self.0
}
}
impl TryFrom<&str> for DocumentName {
type Error = IronOxideErr;
fn try_from(name: &str) -> Result<Self, Self::Error> {
validate_name(name, "document_name").map(DocumentName)
}
}
impl TryFrom<String> for DocumentName {
type Error = IronOxideErr;
fn try_from(doc_name: String) -> Result<Self, Self::Error> {
doc_name.as_str().try_into()
}
}
pub(crate) struct DocHeaderPacked(pub Vec<u8>);
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub(crate) struct DocumentHeader {
#[serde(rename = "_did_")]
pub document_id: DocumentId,
#[serde(rename = "_sid_")]
pub segment_id: usize,
}
impl DocumentHeader {
pub(crate) fn new(document_id: DocumentId, segment_id: usize) -> DocumentHeader {
DocumentHeader {
document_id,
segment_id,
}
}
pub(crate) fn pack(&self) -> DocHeaderPacked {
let mut header_json_bytes =
serde_json::to_vec(&self).expect("Serialization of DocumentHeader failed."); let header_json_len = header_json_bytes.len();
let mut header = Vec::with_capacity(header_json_len + 3);
header.push(CURRENT_DOCUMENT_ID_VERSION);
header.push((header_json_len >> 8) as u8);
header.push(header_json_len as u8);
header.append(&mut header_json_bytes);
DocHeaderPacked(header)
}
}
fn parse_document_parts(
encrypted_document: &[u8],
) -> Result<(DocumentHeader, aes::AesEncryptedValue), IronOxideErr> {
let header_len = parse_header_length(encrypted_document[..3].try_into().map_err(|_| {
IronOxideErr::DocumentHeaderParseFailure(
"Document is too short to contain a valid header".to_string(),
)
})?)?;
let header_json_start = DOC_VERSION_HEADER_LENGTH + HEADER_META_LENGTH_LENGTH;
serde_json::from_slice(&encrypted_document[header_json_start..header_len])
.map_err(|_| {
IronOxideErr::DocumentHeaderParseFailure(
"Unable to parse document header. Header value is corrupted.".to_string(),
)
})
.and_then(|header_json| {
Ok((header_json, encrypted_document[header_len..].try_into()?))
})
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AssociationType {
Owner,
FromUser,
FromGroup,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct VisibleUser {
id: UserId,
}
impl VisibleUser {
pub fn id(&self) -> &UserId {
&self.id
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct VisibleGroup {
id: GroupId,
name: Option<GroupName>,
}
impl VisibleGroup {
pub fn id(&self) -> &GroupId {
&self.id
}
pub fn name(&self) -> Option<&GroupName> {
self.name.as_ref()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentListMeta(DocumentListApiResponseItem);
impl DocumentListMeta {
pub fn id(&self) -> &DocumentId {
&self.0.id
}
pub fn name(&self) -> Option<&DocumentName> {
self.0.name.as_ref()
}
pub fn association_type(&self) -> &AssociationType {
&self.0.association.typ
}
pub fn created(&self) -> &OffsetDateTime {
&self.0.created
}
pub fn last_updated(&self) -> &OffsetDateTime {
&self.0.updated
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentListResult {
result: Vec<DocumentListMeta>,
}
impl DocumentListResult {
pub fn result(&self) -> &Vec<DocumentListMeta> {
&self.result
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentMetadataResult(DocumentMetaApiResponse);
impl DocumentMetadataResult {
pub fn id(&self) -> &DocumentId {
&self.0.id
}
pub fn name(&self) -> Option<&DocumentName> {
self.0.name.as_ref()
}
pub fn created(&self) -> &OffsetDateTime {
&self.0.created
}
pub fn last_updated(&self) -> &OffsetDateTime {
&self.0.updated
}
pub fn association_type(&self) -> &AssociationType {
&self.0.association.typ
}
pub fn visible_to_users(&self) -> &Vec<VisibleUser> {
&self.0.visible_to.users
}
pub fn visible_to_groups(&self) -> &Vec<VisibleGroup> {
&self.0.visible_to.groups
}
pub(crate) fn to_encrypted_symmetric_key(
&self,
) -> Result<recrypt::api::EncryptedValue, IronOxideErr> {
self.0.encrypted_symmetric_key.clone().try_into()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentMetadataUnmanagedResult {
id: DocumentId,
user_visibility: Vec<VisibleUser>,
group_visibility: Vec<VisibleGroup>,
}
impl DocumentMetadataUnmanagedResult {
pub fn id(&self) -> &DocumentId {
&self.id
}
pub fn visible_to_users(&self) -> &Vec<VisibleUser> {
&self.user_visibility
}
pub fn visible_to_groups(&self) -> &Vec<VisibleGroup> {
&self.group_visibility
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentEncryptUnmanagedResult {
id: DocumentId,
encrypted_data: Vec<u8>,
encrypted_deks: Vec<u8>,
grants: Vec<UserOrGroup>,
access_errs: Vec<DocAccessEditErr>,
}
impl DocumentEncryptUnmanagedResult {
fn new(
encryption_result: EncryptedDoc,
access_errs: Vec<DocAccessEditErr>,
) -> Result<Self, IronOxideErr> {
let edek_bytes = encryption_result.edek_bytes()?;
let encrypted_data = encryption_result.edoc_bytes().to_vec();
Ok(DocumentEncryptUnmanagedResult {
id: encryption_result.header.document_id,
access_errs,
encrypted_data,
encrypted_deks: edek_bytes,
grants: encryption_result
.recryption_result
.edeks
.into_iter()
.map(|edek| edek.grant_to.id)
.collect(),
})
}
pub fn encrypted_data(&self) -> &[u8] {
&self.encrypted_data
}
pub fn encrypted_deks(&self) -> &[u8] {
&self.encrypted_deks
}
pub fn id(&self) -> &DocumentId {
&self.id
}
pub fn grants(&self) -> &[UserOrGroup] {
&self.grants
}
pub fn access_errs(&self) -> &[DocAccessEditErr] {
&self.access_errs
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentEncryptResult {
id: DocumentId,
name: Option<DocumentName>,
updated: OffsetDateTime,
created: OffsetDateTime,
encrypted_data: Vec<u8>,
grants: Vec<UserOrGroup>,
access_errs: Vec<DocAccessEditErr>,
}
impl DocumentEncryptResult {
pub fn encrypted_data(&self) -> &[u8] {
&self.encrypted_data
}
pub fn id(&self) -> &DocumentId {
&self.id
}
pub fn name(&self) -> Option<&DocumentName> {
self.name.as_ref()
}
pub fn created(&self) -> &OffsetDateTime {
&self.created
}
pub fn last_updated(&self) -> &OffsetDateTime {
&self.updated
}
pub fn grants(&self) -> &[UserOrGroup] {
&self.grants
}
pub fn access_errs(&self) -> &[DocAccessEditErr] {
&self.access_errs
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentDecryptResult {
id: DocumentId,
name: Option<DocumentName>,
updated: OffsetDateTime,
created: OffsetDateTime,
decrypted_data: Vec<u8>,
}
impl DocumentDecryptResult {
pub fn decrypted_data(&self) -> &[u8] {
&self.decrypted_data
}
pub fn id(&self) -> &DocumentId {
&self.id
}
pub fn name(&self) -> Option<&DocumentName> {
self.name.as_ref()
}
pub fn created(&self) -> &OffsetDateTime {
&self.created
}
pub fn last_updated(&self) -> &OffsetDateTime {
&self.updated
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocAccessEditErr {
pub user_or_group: UserOrGroup,
pub err: String,
}
impl DocAccessEditErr {
pub(crate) fn new(user_or_group: UserOrGroup, err_msg: String) -> DocAccessEditErr {
DocAccessEditErr {
user_or_group,
err: err_msg,
}
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentAccessResult {
succeeded: Vec<UserOrGroup>,
failed: Vec<DocAccessEditErr>,
}
impl DocumentAccessResult {
pub(crate) fn new(
succeeded: Vec<UserOrGroup>,
failed: Vec<DocAccessEditErr>,
) -> DocumentAccessResult {
DocumentAccessResult { succeeded, failed }
}
pub fn succeeded(&self) -> &[UserOrGroup] {
&self.succeeded
}
pub fn failed(&self) -> &[DocAccessEditErr] {
&self.failed
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct DecryptedData(Vec<u8>);
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentDecryptUnmanagedResult {
id: DocumentId,
access_via: UserOrGroup,
decrypted_data: DecryptedData,
}
impl DocumentDecryptUnmanagedResult {
pub fn id(&self) -> &DocumentId {
&self.id
}
pub fn access_via(&self) -> &UserOrGroup {
&self.access_via
}
pub fn decrypted_data(&self) -> &[u8] {
&self.decrypted_data.0
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DocumentAccessUnmanagedResult {
access_via: Option<UserOrGroup>,
encrypted_deks: Vec<u8>,
succeeded: Vec<UserOrGroup>,
failed: Vec<DocAccessEditErr>,
}
impl DocumentAccessUnmanagedResult {
pub fn access_via(&self) -> Option<&UserOrGroup> {
self.access_via.as_ref()
}
pub fn encrypted_deks(&self) -> &[u8] {
&self.encrypted_deks
}
pub fn succeeded(&self) -> &[UserOrGroup] {
&self.succeeded
}
pub fn failed(&self) -> &[DocAccessEditErr] {
&self.failed
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum UserOrGroup {
User { id: UserId },
Group { id: GroupId },
}
impl std::fmt::Display for UserOrGroup {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
UserOrGroup::User { id } => write!(f, "'{}' [user]", &id.0),
UserOrGroup::Group { id } => write!(f, "'{}' [group]", &id.0),
}
}
}
impl From<UserId> for UserOrGroup {
fn from(u: UserId) -> Self {
UserOrGroup::User { id: u }
}
}
impl From<GroupId> for UserOrGroup {
fn from(g: GroupId) -> Self {
UserOrGroup::Group { id: g }
}
}
impl From<&UserId> for UserOrGroup {
fn from(u: &UserId) -> Self {
u.to_owned().into()
}
}
impl From<&GroupId> for UserOrGroup {
fn from(g: &GroupId) -> Self {
g.to_owned().into()
}
}
pub async fn document_list(auth: &RequestAuth) -> Result<DocumentListResult, IronOxideErr> {
let DocumentListApiResponse { result } =
requests::document_list::document_list_request(auth).await?;
Ok(DocumentListResult {
result: result.into_iter().map(DocumentListMeta).collect(),
})
}
pub async fn document_get_metadata(
auth: &RequestAuth,
id: &DocumentId,
) -> Result<DocumentMetadataResult, IronOxideErr> {
Ok(DocumentMetadataResult(
requests::document_get::document_get_request(auth, id).await?,
))
}
pub fn get_id_from_bytes(encrypted_document: &[u8]) -> Result<DocumentId, IronOxideErr> {
parse_document_parts(encrypted_document).map(|header| header.0.document_id)
}
pub async fn encrypt_document<R1: rand::CryptoRng, R2: rand::CryptoRng>(
auth: &RequestAuth,
config: &IronOxideConfig,
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<R1>>,
user_master_pub_key: &PublicKey,
rng: &Mutex<R2>,
plaintext: Vec<u8>,
document_id: Option<DocumentId>,
document_name: Option<DocumentName>,
grant_to_author: bool,
user_grants: &[UserId],
group_grants: &[GroupId],
policy_grant: Option<&PolicyGrant>,
policy_cache: &PolicyCache,
public_key_cache: &PublicKeyCache,
) -> Result<DocumentEncryptResult, IronOxideErr> {
let (dek, doc_sym_key) = transform::generate_new_doc_key(recrypt);
let doc_id = document_id.unwrap_or_else(|| DocumentId::goo_id(rng));
let (encrypted_doc, (grants, key_errs)) = try_join!(
aes::encrypt_async(rng, plaintext, *doc_sym_key.bytes()),
resolve_keys_for_grants(
auth,
config,
user_grants,
group_grants,
policy_grant,
if grant_to_author {
Some(user_master_pub_key)
} else {
None
},
policy_cache,
public_key_cache
)
)?;
let r = recrypt_document(&auth.signing_private_key, recrypt, dek, &doc_id, grants)?;
let encryption_errs = r.encryption_errs.clone();
document_create(
auth,
r.into_edoc(
DocumentHeader::new(doc_id.clone(), auth.segment_id),
encrypted_doc,
),
doc_id,
&document_name,
[key_errs, encryption_errs].concat(),
)
.await
}
type UserMasterPublicKey = PublicKey;
pub(crate) async fn resolve_keys_for_grants(
auth: &RequestAuth,
config: &IronOxideConfig,
user_grants: &[UserId],
group_grants: &[GroupId],
policy_grant: Option<&PolicyGrant>,
maybe_user_master_pub_key: Option<&UserMasterPublicKey>,
policy_cache: &PolicyCache,
public_key_cache: &PublicKeyCache,
) -> Result<(Vec<WithKey<UserOrGroup>>, Vec<DocAccessEditErr>), IronOxideErr> {
let get_user_keys_f =
internal::user_api::get_user_keys(auth, user_grants, public_key_cache.user_keys());
let get_group_keys_f =
internal::group_api::get_group_keys(auth, group_grants, public_key_cache.group_keys());
let maybe_policy_grants_f =
policy_grant.map(|p| (p, requests::policy_get::policy_get_request(auth, p)));
let policy_grants_f = async {
if let Some((p, policy_eval_f)) = maybe_policy_grants_f {
get_cached_policy_or(&config.policy_caching, p, policy_cache, policy_eval_f).await
} else {
Ok((vec![], vec![]))
}
};
let (users, groups, policy_result) =
try_join!(get_user_keys_f, get_group_keys_f, policy_grants_f)?;
let (group_errs, groups_with_key) = process_groups(groups);
let (user_errs, users_with_key) = process_users(users);
let explicit_grants = [users_with_key, groups_with_key].concat();
let (policy_errs, applied_policy_grants) = policy_result;
let maybe_self_grant = {
if let Some(user_master_pub_key) = maybe_user_master_pub_key {
vec![WithKey::new(
UserOrGroup::User {
id: auth.account_id.clone(),
},
user_master_pub_key.clone(),
)]
} else {
vec![]
}
};
Ok((
{ [maybe_self_grant, explicit_grants, applied_policy_grants].concat() },
[group_errs, user_errs, policy_errs].concat(),
))
}
async fn get_cached_policy_or<F>(
config: &PolicyCachingConfig,
grant: &PolicyGrant,
policy_cache: &PolicyCache,
get_policy_f: F,
) -> Result<(Vec<DocAccessEditErr>, Vec<WithKey<UserOrGroup>>), IronOxideErr>
where
F: Future<Output = Result<PolicyResponse, IronOxideErr>>,
{
if let Some(cached_policy) = policy_cache.pin_owned().get(grant).cloned() {
Ok((vec![], cached_policy))
} else {
get_policy_f
.await
.map(|policy_resp| {
let (errs, public_keys) = process_policy(&policy_resp);
if errs.is_empty() {
let policy_pin = policy_cache.pin();
if policy_cache.len() >= config.max_entries {
policy_pin.clear()
}
policy_pin.insert(grant.clone(), public_keys.clone());
}
(errs, public_keys)
})
.map_err(|x| match x {
IronOxideErr::RequestError {
http_status: Some(404),
..
} => IronOxideErr::PolicyDoesNotExist,
e => e,
})
}
}
pub async fn encrypt_document_unmanaged<R1, R2>(
auth: &RequestAuth,
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<R1>>,
user_master_pub_key: &PublicKey,
rng: &Mutex<R2>,
plaintext: Vec<u8>,
document_id: Option<DocumentId>,
grant_to_author: bool,
user_grants: &[UserId],
group_grants: &[GroupId],
policy_grant: Option<&PolicyGrant>,
policy_cache: &PolicyCache,
public_key_cache: &PublicKeyCache,
) -> Result<DocumentEncryptUnmanagedResult, IronOxideErr>
where
R1: rand::CryptoRng,
R2: rand::CryptoRng,
{
let config = IronOxideConfig::default();
let (dek, doc_sym_key) = transform::generate_new_doc_key(recrypt);
let doc_id = document_id.unwrap_or_else(|| DocumentId::goo_id(rng));
let (doc, (recryption_result, access_errs)) = try_join!(
aes::encrypt_async(rng, plaintext, *doc_sym_key.bytes()),
encrypt_dek_to(
auth,
recrypt,
user_master_pub_key,
grant_to_author,
user_grants,
group_grants,
policy_grant,
policy_cache,
public_key_cache,
config,
dek,
&doc_id,
)
)?;
let enc_result = EncryptedDoc {
header: DocumentHeader::new(doc_id, auth.segment_id),
doc,
recryption_result,
};
DocumentEncryptUnmanagedResult::new(enc_result, access_errs)
}
async fn encrypt_dek_to<R1>(
auth: &RequestAuth,
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<R1>>,
user_master_pub_key: &PublicKey,
grant_to_author: bool,
user_grants: &[UserId],
group_grants: &[GroupId],
policy_grant: Option<&PolicyGrant>,
policy_cache: &papaya::HashMap<PolicyGrant, Vec<WithKey<UserOrGroup>>>,
public_key_cache: &PublicKeyCache,
config: IronOxideConfig,
dek: Plaintext,
doc_id: &DocumentId,
) -> Result<(RecryptionResult, Vec<DocAccessEditErr>), IronOxideErr>
where
R1: rand::CryptoRng,
{
let (grants, key_errs) = resolve_keys_for_grants(
auth,
&config,
user_grants,
group_grants,
policy_grant,
if grant_to_author {
Some(user_master_pub_key)
} else {
None
},
policy_cache,
public_key_cache,
)
.await?;
let recryption_result =
recrypt_document(&auth.signing_private_key, recrypt, dek, doc_id, grants)?;
let access_errs = key_errs
.into_iter()
.chain(recryption_result.encryption_errs.clone())
.collect();
Ok((recryption_result, access_errs))
}
fn dedupe_grants(grants: &[WithKey<UserOrGroup>]) -> Vec<WithKey<UserOrGroup>> {
grants
.iter()
.unique_by(|i| &i.id)
.map(Clone::clone)
.collect_vec()
}
pub(crate) fn recrypt_document<CR: rand::CryptoRng>(
signing_keys: &DeviceSigningKeyPair,
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
dek: Plaintext,
doc_id: &DocumentId,
grants: Vec<WithKey<UserOrGroup>>,
) -> Result<RecryptionResult, IronOxideErr> {
if grants.is_empty() {
Err(IronOxideErr::ValidationError(
"grants".into(),
format!(
"Access must be granted to document {:?} by explicit grant or via a policy",
&doc_id
),
))
} else {
Ok({
let (encrypt_errs, grants) = transform::encrypt_to_with_key(
recrypt,
&dek,
&signing_keys.into(),
dedupe_grants(&grants),
);
RecryptionResult {
edeks: grants
.into_iter()
.map(|(wk, ev)| EncryptedDek {
grant_to: wk,
encrypted_dek_data: ev,
})
.collect(),
encryption_errs: encrypt_errs.into_iter().map(|e| e.into()).collect(),
}
})
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct EncryptedDek {
pub(crate) grant_to: WithKey<UserOrGroup>,
pub(crate) encrypted_dek_data: recrypt::api::EncryptedValue,
}
impl EncryptedDek {
pub(crate) fn grant_to(&self) -> &UserOrGroup {
&self.grant_to.id
}
}
impl TryFrom<&EncryptedDek> for EncryptedDekP {
type Error = IronOxideErr;
fn try_from(edek: &EncryptedDek) -> Result<Self, Self::Error> {
use crate::proto::transform;
use recrypt::api as re;
let proto_edek_data = match edek.encrypted_dek_data {
re::EncryptedValue::EncryptedOnceValue {
ephemeral_public_key,
encrypted_message,
auth_hash,
public_signing_key,
signature,
} => Ok(EncryptedDekDataP {
encryptedBytes: encrypted_message.bytes().to_vec().into(),
ephemeralPublicKey: Some(PublicKey::from(ephemeral_public_key).into()).into(),
signature: signature.bytes().to_vec().into(),
authHash: auth_hash.bytes().to_vec().into(),
publicSigningKey: public_signing_key.bytes().to_vec().into(),
..Default::default()
}),
re::EncryptedValue::TransformedValue { .. } => Err(
IronOxideErr::InvalidRecryptEncryptedValue("Expected".to_string()),
),
}?;
let proto_uog = match edek.grant_to.clone() {
WithKey {
id:
UserOrGroup::User {
id: UserId(user_string),
},
public_key,
} => {
let mut proto_uog = transform::UserOrGroup::default();
proto_uog.set_userId(user_string.into());
proto_uog.masterPublicKey = Some(public_key.into()).into();
proto_uog
}
WithKey {
id:
UserOrGroup::Group {
id: GroupId(group_string),
},
public_key,
} => {
let mut proto_uog = transform::UserOrGroup::default();
proto_uog.set_groupId(group_string.into());
proto_uog.masterPublicKey = Some(public_key.into()).into();
proto_uog
}
};
Ok(EncryptedDekP {
userOrGroup: Some(proto_uog).into(),
encryptedDekData: Some(proto_edek_data).into(),
..Default::default()
})
}
}
#[derive(Clone, Debug)]
pub(crate) struct RecryptionResult {
pub edeks: Vec<EncryptedDek>,
pub encryption_errs: Vec<DocAccessEditErr>,
}
impl RecryptionResult {
fn into_edoc(self, header: DocumentHeader, doc: AesEncryptedValue) -> EncryptedDoc {
EncryptedDoc {
header,
doc,
recryption_result: self,
}
}
}
#[derive(Debug)]
struct EncryptedDoc {
header: DocumentHeader,
recryption_result: RecryptionResult,
doc: AesEncryptedValue,
}
impl EncryptedDoc {
fn edoc_bytes(&self) -> Vec<u8> {
[&self.header.pack().0[..], &self.doc.bytes()].concat()
}
fn edek_vec(&self) -> Vec<EncryptedDek> {
self.recryption_result.edeks.clone()
}
fn edek_bytes(&self) -> Result<Vec<u8>, IronOxideErr> {
edeks_to_edeks_proto(
&self.recryption_result.edeks,
self.header.document_id.id(),
self.header.segment_id as i32, )
}
}
pub(crate) fn edeks_to_bytes(
edeks: &[EncryptedDek],
document_id: &DocumentId,
segment_id: usize,
) -> Result<Vec<u8>, IronOxideErr> {
edeks_to_edeks_proto(edeks, document_id.id(), segment_id as i32)
}
fn edeks_to_edeks_proto(
edeks: &[EncryptedDek],
document_id: &str,
segment_id: i32,
) -> Result<Vec<u8>, IronOxideErr> {
let proto_edek_vec_results: Result<Vec<_>, _> =
edeks.iter().map(|edek| edek.try_into()).collect();
let proto_edek_vec = proto_edek_vec_results?;
let proto_edeks = EncryptedDeksP {
edeks: proto_edek_vec,
documentId: document_id.into(),
segmentId: segment_id,
..Default::default()
};
let edek_bytes = proto_edeks.write_to_bytes()?;
Ok(edek_bytes)
}
async fn document_create(
auth: &RequestAuth,
edoc: EncryptedDoc,
doc_id: DocumentId,
doc_name: &Option<DocumentName>,
accum_errs: Vec<DocAccessEditErr>,
) -> Result<DocumentEncryptResult, IronOxideErr> {
let api_resp =
document_create::document_create_request(auth, doc_id, doc_name.clone(), edoc.edek_vec())
.await?;
Ok(DocumentEncryptResult {
id: api_resp.id,
name: api_resp.name,
created: api_resp.created,
updated: api_resp.updated,
encrypted_data: edoc.edoc_bytes(),
grants: api_resp
.shared_with
.into_iter()
.map(|sw| sw.into())
.collect(),
access_errs: [accum_errs, edoc.recryption_result.encryption_errs].concat(),
})
}
pub async fn document_update_bytes<R1: rand::CryptoRng, R2: rand::CryptoRng>(
auth: &RequestAuth,
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<R1>>,
device_private_key: &PrivateKey,
rng: &Mutex<R2>,
document_id: &DocumentId,
plaintext: Vec<u8>,
) -> Result<DocumentEncryptResult, IronOxideErr> {
let doc_meta = document_get_metadata(auth, document_id).await?;
let sym_key = transform::decrypt_as_symmetric_key(
recrypt,
doc_meta.0.encrypted_symmetric_key.try_into()?,
device_private_key.recrypt_key(),
)?;
Ok(
aes::encrypt(rng, plaintext, *sym_key.bytes()).map(move |encrypted_doc| {
let mut encrypted_payload =
DocumentHeader::new(document_id.clone(), auth.segment_id()).pack();
encrypted_payload.0.append(&mut encrypted_doc.bytes());
DocumentEncryptResult {
id: doc_meta.0.id,
name: doc_meta.0.name,
created: doc_meta.0.created,
updated: doc_meta.0.updated,
encrypted_data: encrypted_payload.0,
grants: vec![], access_errs: vec![], }
})?,
)
}
pub async fn decrypt_document<CR: rand::CryptoRng + Send + Sync + 'static>(
auth: &RequestAuth,
recrypt: std::sync::Arc<Recrypt<Sha256, Ed25519, RandomBytes<CR>>>,
device_private_key: &PrivateKey,
encrypted_doc: &[u8],
) -> Result<DocumentDecryptResult, IronOxideErr> {
let (doc_header, mut enc_doc) = parse_document_parts(encrypted_doc)?;
let doc_meta = document_get_metadata(auth, &doc_header.document_id).await?;
let device_private_key = device_private_key.clone();
tokio::task::spawn_blocking(move || {
let sym_key = transform::decrypt_as_symmetric_key(
&recrypt,
doc_meta.0.encrypted_symmetric_key.try_into()?,
device_private_key.recrypt_key(),
)?;
Ok(
aes::decrypt(&mut enc_doc, *sym_key.bytes()).map(move |decrypted_doc| {
DocumentDecryptResult {
id: doc_meta.0.id,
name: doc_meta.0.name,
created: doc_meta.0.created,
updated: doc_meta.0.updated,
decrypted_data: decrypted_doc.to_vec(),
}
})?,
)
})
.await?
}
pub async fn decrypt_document_unmanaged<CR: rand::CryptoRng>(
auth: &RequestAuth,
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
device_private_key: &PrivateKey,
encrypted_doc: &[u8],
encrypted_deks: &[u8],
) -> Result<DocumentDecryptUnmanagedResult, IronOxideErr> {
let ((proto_edeks, (doc_meta, mut aes_encrypted_value)), transform_resp) = try_join!(
async {
Ok((
EncryptedDeksP::parse_from_bytes(encrypted_deks).map_err(IronOxideErr::from)?,
parse_document_parts(encrypted_doc)?,
))
},
requests::edek_transform::edek_transform(auth, encrypted_deks)
)?;
edeks_and_header_match_or_err(&proto_edeks, &doc_meta)?;
let requests::edek_transform::EdekTransformResponse {
user_or_group,
encrypted_symmetric_key,
} = transform_resp;
let sym_key = transform::decrypt_as_symmetric_key(
recrypt,
encrypted_symmetric_key.try_into()?,
device_private_key.recrypt_key(),
)?;
aes::decrypt(&mut aes_encrypted_value, *sym_key.bytes())
.map_err(|e| e.into())
.map(move |decrypted_doc| DocumentDecryptUnmanagedResult {
id: doc_meta.document_id,
access_via: user_or_group,
decrypted_data: DecryptedData(decrypted_doc.to_vec()),
})
}
pub async fn document_grant_access_unmanaged<CR: rand::CryptoRng>(
auth: &RequestAuth,
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
user_master_pub_key: &PublicKey,
device_private_key: &PrivateKey,
encrypted_deks: &[u8],
users: &[UserId],
groups: &[GroupId],
public_key_cache: &PublicKeyCache,
config: IronOxideConfig,
) -> Result<DocumentAccessUnmanagedResult, IronOxideErr> {
let (proto_edeks, transform_resp) = try_join!(
async { EncryptedDeksP::parse_from_bytes(encrypted_deks).map_err(IronOxideErr::from) },
requests::edek_transform::edek_transform(auth, encrypted_deks)
)?;
let document_id = (*proto_edeks.documentId).try_into()?;
let requests::edek_transform::EdekTransformResponse {
user_or_group,
encrypted_symmetric_key: actually_recrypt_encrypted_plaintext,
} = transform_resp;
let dek = recrypt.decrypt(
actually_recrypt_encrypted_plaintext.try_into()?,
device_private_key.recrypt_key(),
)?;
let (recryption_result, access_errs) = encrypt_dek_to(
auth,
recrypt,
user_master_pub_key,
false,
users,
groups,
None,
&Default::default(),
public_key_cache,
config,
dek,
&document_id,
)
.await?;
let succeeded: Vec<UserOrGroup> = recryption_result
.edeks
.iter()
.map(|edek| edek.grant_to.id.clone())
.collect();
let new_proto_edeks: Vec<EncryptedDekP> = recryption_result
.edeks
.iter()
.map(|edek| edek.try_into())
.collect::<Result<Vec<_>, _>>()?;
let mut proto_edeks = proto_edeks;
proto_edeks.edeks.retain(|edek| {
let proto_id = edek
.userOrGroup
.as_ref()
.and_then(|uog| uog.UserOrGroupId.as_ref());
match proto_id {
Some(UserOrGroupIdP::UserId(u)) => {
let Ok(uid) = UserId::try_from(u.to_string()) else {
return true;
};
!succeeded.contains(&UserOrGroup::User { id: uid })
}
Some(UserOrGroupIdP::GroupId(g)) => {
let Ok(gid) = GroupId::try_from(g.to_string()) else {
return true;
};
!succeeded.contains(&UserOrGroup::Group { id: gid })
}
None => true,
}
});
proto_edeks.edeks.extend(new_proto_edeks);
let encrypted_deks = proto_edeks.write_to_bytes()?;
Ok(DocumentAccessUnmanagedResult {
access_via: Some(user_or_group),
encrypted_deks,
succeeded,
failed: access_errs,
})
}
pub fn document_get_metadata_unmanaged(
encrypted_deks: &[u8],
) -> Result<DocumentMetadataUnmanagedResult, IronOxideErr> {
let proto_edeks =
EncryptedDeksP::parse_from_bytes(encrypted_deks).map_err(IronOxideErr::from)?;
let doc_id: DocumentId = (*proto_edeks.documentId).try_into()?;
let (users, groups): (Vec<UserId>, Vec<GroupId>) = proto_edeks
.edeks
.iter()
.filter_map(|edek| edek.userOrGroup.as_ref()?.UserOrGroupId.as_ref())
.filter_map(|proto_user_or_group| match proto_user_or_group {
UserOrGroupIdP::UserId(u) => UserId::try_from(u.to_string()).ok().map(Either::Left),
UserOrGroupIdP::GroupId(g) => GroupId::try_from(g.to_string()).ok().map(Either::Right),
})
.partition_map(identity);
let user_visibility: Vec<_> = users.into_iter().map(|id| VisibleUser { id }).collect();
let group_visibility: Vec<_> = groups
.into_iter()
.map(|id| VisibleGroup { id, name: None })
.collect();
Ok(DocumentMetadataUnmanagedResult {
id: doc_id,
user_visibility,
group_visibility,
})
}
pub fn document_revoke_access_unmanaged(
encrypted_deks: &[u8],
revoke_list: &[UserOrGroup],
) -> Result<DocumentAccessUnmanagedResult, IronOxideErr> {
let mut proto_edeks =
EncryptedDeksP::parse_from_bytes(encrypted_deks).map_err(IronOxideErr::from)?;
let should_revoke = |edek: &EncryptedDekP| -> Option<UserOrGroup> {
let uog = edek.userOrGroup.as_ref()?.UserOrGroupId.as_ref()?;
match uog {
UserOrGroupIdP::UserId(u) => {
let uid = UserId::try_from(u.to_string()).ok()?;
revoke_list
.iter()
.find(|revocation| matches!(revocation, UserOrGroup::User { id } if id == &uid))
.cloned()
}
UserOrGroupIdP::GroupId(g) => {
let gid = GroupId::try_from(g.to_string()).ok()?;
revoke_list
.iter()
.find(
|revocation| matches!(revocation, UserOrGroup::Group { id } if id == &gid),
)
.cloned()
}
}
};
let (revoked, kept): (Vec<_>, Vec<_>) =
proto_edeks
.edeks
.into_iter()
.partition_map(|edek| match should_revoke(&edek) {
Some(revoked_identity) => Either::Left(revoked_identity),
None => Either::Right(edek),
});
let failed: Vec<DocAccessEditErr> = revoke_list
.iter()
.filter(|r| !revoked.contains(r))
.map(|r| {
DocAccessEditErr::new(
r.clone(),
"User or group not found in EDEK grants.".to_string(),
)
})
.collect();
proto_edeks.edeks = kept;
let new_edek_bytes = proto_edeks.write_to_bytes()?;
Ok(DocumentAccessUnmanagedResult {
access_via: None,
encrypted_deks: new_edek_bytes,
succeeded: revoked,
failed,
})
}
pub(crate) fn edeks_and_header_match_or_err(
edeks: &EncryptedDeksP,
doc_meta: &DocumentHeader,
) -> Result<(), IronOxideErr> {
if doc_meta.document_id.id() != edeks.documentId.to_string()
|| doc_meta.segment_id as i32 != edeks.segmentId
{
Err(IronOxideErr::UnmanagedDecryptionError(
edeks.documentId.to_string(),
edeks.segmentId,
doc_meta.document_id.clone().0,
doc_meta.segment_id as i32,
))
} else {
Ok(())
}
}
pub async fn update_document_name(
auth: &RequestAuth,
id: &DocumentId,
name: Option<&DocumentName>,
) -> Result<DocumentMetadataResult, IronOxideErr> {
requests::document_update::document_update_request(auth, id, name)
.await
.map(DocumentMetadataResult)
}
pub async fn document_grant_access<CR: rand::CryptoRng>(
auth: &RequestAuth,
recrypt: &Recrypt<Sha256, Ed25519, RandomBytes<CR>>,
id: &DocumentId,
user_master_pub_key: &PublicKey,
priv_device_key: &PrivateKey,
user_grants: &[UserId],
group_grants: &[GroupId],
public_key_cache: &PublicKeyCache,
) -> Result<DocumentAccessResult, IronOxideErr> {
let (doc_meta, users, groups) = try_join!(
document_get_metadata(auth, id),
internal::user_api::get_user_keys(auth, user_grants, public_key_cache.user_keys()),
internal::group_api::get_group_keys(auth, group_grants, public_key_cache.group_keys()),
)?;
let (grants, other_errs) = {
let edek = doc_meta.to_encrypted_symmetric_key()?;
let dek = recrypt.decrypt(edek, priv_device_key.recrypt_key())?;
let (group_errs, groups_with_key) = process_groups(groups);
let (user_errs, users_with_key) = process_users(users);
let users_and_groups = dedupe_grants(&[&users_with_key[..], &groups_with_key[..]].concat());
let (grant_errs, grants) = transform::encrypt_to_with_key(
recrypt,
&dek,
&auth.signing_private_key().into(),
users_and_groups,
);
let other_errs = group_errs
.into_iter()
.chain(user_errs)
.chain(grant_errs.into_iter().map(|e| e.into()))
.collect_vec();
(grants, other_errs)
};
let resp =
requests::document_access::grant_access_request(auth, id, user_master_pub_key, grants)
.await?;
Ok(requests::document_access::resp::document_access_api_resp_to_result(resp, other_errs))
}
pub async fn document_revoke_access(
auth: &RequestAuth,
id: &DocumentId,
revoke_list: &[UserOrGroup],
) -> Result<DocumentAccessResult, IronOxideErr> {
use requests::document_access::{self, resp};
let revoke_request_list: Vec<_> = revoke_list
.iter()
.map(|entity| match entity {
UserOrGroup::User { id } => resp::UserOrGroupAccess::User { id: id.0.clone() },
UserOrGroup::Group { id } => resp::UserOrGroupAccess::Group { id: id.0.clone() },
})
.collect();
let resp = document_access::revoke_access_request(auth, id, revoke_request_list).await?;
Ok(resp::document_access_api_resp_to_result(resp, vec![]))
}
fn process_groups(
(group_errs, groups_with_key): (Vec<GroupId>, Vec<WithKey<GroupId>>),
) -> (Vec<DocAccessEditErr>, Vec<WithKey<UserOrGroup>>) {
let group_errs = group_errs
.into_iter()
.map(|gid| {
DocAccessEditErr::new(
UserOrGroup::Group { id: gid },
"Group could not be found".to_string(),
)
})
.collect();
let groups_with_key: Vec<WithKey<UserOrGroup>> = groups_with_key
.into_iter()
.map(|WithKey { id, public_key }| WithKey {
id: UserOrGroup::Group { id },
public_key,
})
.collect();
(group_errs, groups_with_key)
}
fn process_users(
(user_errs, users_with_key): (Vec<UserId>, Vec<WithKey<UserId>>),
) -> (Vec<DocAccessEditErr>, Vec<WithKey<UserOrGroup>>) {
let users_with_key: Vec<WithKey<UserOrGroup>> = users_with_key
.into_iter()
.map(|WithKey { id, public_key }| WithKey {
id: UserOrGroup::User { id },
public_key,
})
.collect();
let user_errs: Vec<DocAccessEditErr> = user_errs
.into_iter()
.map(|uid| {
DocAccessEditErr::new(
UserOrGroup::User { id: uid },
"User could not be found".to_string(),
)
})
.collect();
(user_errs, users_with_key)
}
fn process_policy(
policy_result: &PolicyResponse,
) -> (Vec<DocAccessEditErr>, Vec<WithKey<UserOrGroup>>) {
let (pubkey_errs, policy_eval_results): (Vec<DocAccessEditErr>, Vec<WithKey<UserOrGroup>>) =
policy_result
.users_and_groups
.iter()
.partition_map(|uog| match uog {
UserOrGroupWithKey::User {
id,
master_public_key: Some(key),
} => {
let user = UserOrGroup::User {
id: UserId::unsafe_from_string(id.clone()),
};
Either::from(
key.clone()
.try_into()
.map(|k| WithKey::new(user.clone(), k))
.map_err(|_e| {
DocAccessEditErr::new(
user,
format!("Error parsing user public key {:?}", &key),
)
}),
)
}
UserOrGroupWithKey::Group {
id,
master_public_key: Some(key),
} => {
let group = UserOrGroup::Group {
id: GroupId::unsafe_from_string(id.clone()),
};
Either::from(
key.clone()
.try_into()
.map(|k| WithKey::new(group.clone(), k))
.map_err(|_e| {
DocAccessEditErr::new(
group,
format!("Error parsing group public key {:?}", &key),
)
}),
)
}
any => {
let uog: UserOrGroup = any.clone().into();
let err_msg = format!("{} does not have associated public key", &uog);
Either::Left(DocAccessEditErr::new(uog, err_msg))
}
});
(
pubkey_errs
.into_iter()
.chain(policy_result.invalid_users_and_groups.iter().map(|uog| {
DocAccessEditErr::new(
uog.clone(),
format!("Policy refers to unknown user or group '{}'", &uog),
)
}))
.collect(),
policy_eval_results,
)
}
#[cfg(test)]
mod tests {
use crate::internal::tests::contains;
use base64::engine::Engine;
use base64::prelude::BASE64_STANDARD;
use galvanic_assert::{
matchers::{collection::*, *},
*,
};
use super::*;
use crate::internal::RequestErrorCode;
use papaya::HashMap;
use std::borrow::Borrow;
#[tokio::test]
async fn get_policy_or() -> Result<(), IronOxideErr> {
let policy_json = r#"{ "usersAndGroups": [ { "type": "group", "id": "data_recovery_abcABC012_.$#|@/:;=+'-f1e11a54-8aa9-4641-aaf3-fb92079499f0", "masterPublicKey": { "x": "GE5XQYcRDRhBcyDpNwlu79x6tshNi111ym1IfxOTIxk=", "y": "amgLgcCEYIPQ4oxinLoAvsO3VG7XTFdRfkG/3tooaZE=" } } ], "invalidUsersAndGroups": [] }"#;
let policy_grant = PolicyGrant::default();
let policy_cache = HashMap::new();
let config = PolicyCachingConfig::default();
let policy_resp: PolicyResponse =
serde_json::from_str(policy_json).expect("json should parse");
let err_result = get_cached_policy_or(&config, &policy_grant, &policy_cache, async {
Err(IronOxideErr::InitializeError("".into()))
})
.await;
assert!(err_result.is_err());
let policy = get_cached_policy_or(&config, &policy_grant, &policy_cache, async {
Ok(policy_resp.clone())
})
.await?;
assert_eq!(1, policy_cache.len());
assert_eq!(
policy.1,
policy_cache.pin().get(&policy_grant).unwrap().clone()
);
get_cached_policy_or(&config, &policy_grant, &policy_cache, async {
Err(IronOxideErr::InitializeError("".into()))
})
.await?;
assert_eq!(1, policy_cache.len());
Ok(())
}
#[tokio::test]
async fn policy_404_gives_nice_error() -> Result<(), IronOxideErr> {
let policy_grant = PolicyGrant::default();
let policy_cache = HashMap::new();
let config = PolicyCachingConfig::default();
let err_result = get_cached_policy_or(&config, &policy_grant, &policy_cache, async {
Err(IronOxideErr::RequestError {
message: "".into(),
code: RequestErrorCode::PolicyGet,
http_status: Some(404),
})
})
.await;
assert!(err_result.is_err());
assert_that!(
&err_result.unwrap_err(),
is_variant!(IronOxideErr::PolicyDoesNotExist)
);
Ok(())
}
#[tokio::test]
async fn policy_cache_max_size_honored() -> Result<(), IronOxideErr> {
let policy_json = r#"{ "usersAndGroups": [ { "type": "group", "id": "data_recovery_abcABC012_.$#|@/:;=+'-f1e11a54-8aa9-4641-aaf3-fb92079499f0", "masterPublicKey": { "x": "GE5XQYcRDRhBcyDpNwlu79x6tshNi111ym1IfxOTIxk=", "y": "amgLgcCEYIPQ4oxinLoAvsO3VG7XTFdRfkG/3tooaZE=" } } ], "invalidUsersAndGroups": [] }"#;
let policy_grant = PolicyGrant::default();
let policy_cache = HashMap::new();
let config = PolicyCachingConfig { max_entries: 3 };
let policy_resp: PolicyResponse =
serde_json::from_str(policy_json).expect("json should parse");
get_cached_policy_or(&config, &policy_grant, &policy_cache, async {
Ok(policy_resp.clone())
})
.await?;
assert_eq!(1, policy_cache.len());
let policy_grant2 = PolicyGrant::new(Some("foo".try_into()?), None, None, None);
get_cached_policy_or(&config, &policy_grant2, &policy_cache, async {
Ok(policy_resp.clone())
})
.await?;
assert_eq!(2, policy_cache.len());
let policy_grant3 = PolicyGrant::new(Some("bar".try_into()?), None, None, None);
get_cached_policy_or(&config, &policy_grant3, &policy_cache, async {
Ok(policy_resp.clone())
})
.await?;
assert_eq!(3, policy_cache.len());
let policy_grant4 = PolicyGrant::new(Some("baz".try_into()?), None, None, None);
get_cached_policy_or(&config, &policy_grant4, &policy_cache, async {
Ok(policy_resp.clone())
})
.await?;
assert_eq!(1, policy_cache.len());
Ok(())
}
#[tokio::test]
async fn policy_cache_unclean_entries_not_cached() -> Result<(), IronOxideErr> {
let policy_json = r#"{ "usersAndGroups": [ { "type": "group", "id": "data_recovery_abcABC012_.$#|@/:;=+'-f1e11a54-8aa9-4641-aaf3-fb92079499f0", "masterPublicKey": { "x": "GE5XQYcRDRhBcyDpNwlu79x6tshNi111ym1IfxOTIxk=", "y": "amgLgcCEYIPQ4oxinLoAvsO3VG7XTFdRfkG/3tooaZE=" } } ], "invalidUsersAndGroups": [{ "type": "group", "id": "group-that-does-not-exist" }] }"#;
let policy_grant = PolicyGrant::default();
let policy_cache = HashMap::new();
let config = PolicyCachingConfig::default();
let policy_resp: PolicyResponse =
serde_json::from_str(policy_json).expect("json should parse");
get_cached_policy_or(&config, &policy_grant, &policy_cache, async {
Ok(policy_resp.clone())
})
.await?;
assert_eq!(0, policy_cache.len());
Ok(())
}
#[test]
fn document_id_validate_good() {
let doc_id1 = "an_actual_good_doc_id$";
let doc_id2 = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789";
assert_eq!(
DocumentId(doc_id1.to_string()),
DocumentId::try_from(doc_id1).unwrap()
);
assert_eq!(
DocumentId(doc_id2.to_string()),
DocumentId::try_from(doc_id2).unwrap()
)
}
#[test]
fn document_id_rejects_invalid() {
let doc_id1 = DocumentId::try_from("not a good ID!");
let doc_id2 = DocumentId::try_from("!!");
let doc_id3 = DocumentId::try_from(
"01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567891",
);
assert_that!(
&doc_id1.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(
&doc_id2.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
assert_that!(
&doc_id3.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
}
#[test]
fn doc_id_rejects_empty() {
let doc_id = DocumentId::try_from("");
assert_that!(&doc_id, is_variant!(Err));
assert_that!(
&doc_id.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
let doc_id = DocumentId::try_from("\n \t ");
assert_that!(&doc_id, is_variant!(Err));
assert_that!(
&doc_id.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
}
#[test]
fn doc_name_rejects_empty() {
let doc_name = DocumentName::try_from("");
assert_that!(&doc_name, is_variant!(Err));
assert_that!(
&doc_name.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
let doc_name = DocumentName::try_from("\n \t ");
assert_that!(&doc_name, is_variant!(Err));
assert_that!(
&doc_name.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
);
}
#[test]
fn doc_name_rejects_too_long() {
let doc_name = DocumentName::try_from(
"01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567891",
);
assert_that!(
&doc_name.unwrap_err(),
is_variant!(IronOxideErr::ValidationError)
)
}
#[test]
fn err_on_bad_doc_header() {
let doc_with_wrong_version = BASE64_STANDARD.decode("AQA4eyJfZGlkXyI6ImNjOTIyZTA3NzRhM2MwZWViZTI2NDM2Yzk2ZjdiYzkzIiwiX3NpZF8iOjYwOH1ciL4su5SPZh4eFGuG+5rJ+/I2gDSZAs+2dXw097gU8fBkMWzRo0dDIW0dOxHg/1mio1yMRdDZDA==").unwrap();
let doc_with_invalid_json = BASE64_STANDARD.decode("AgA4Z2NfZGlkXyI6ImNjOTIyZTA3NzRhM2MwZWViZTI2NDM2Yzk2ZjdiYzkzIiwiX3NpZF8iOjYwOH1ciL4su5SPZh4eFGuG+5rJ+/I2gDSZAs+2dXw097gU8fBkMWzRo0dDIW0dOxHg/1mio1yMRdDZDA==").unwrap();
assert_that!(
&get_id_from_bytes(&doc_with_wrong_version).unwrap_err(),
has_structure!(
IronOxideErr::DocumentHeaderParseFailure[contains("not a supported version")]
)
);
assert_that!(
&get_id_from_bytes(&doc_with_invalid_json).unwrap_err(),
has_structure!(
IronOxideErr::DocumentHeaderParseFailure[contains("Header value is corrupted")]
)
);
}
#[test]
fn read_good_document_header_test() {
let enc_doc = BASE64_STANDARD.decode("AgA4eyJfZGlkXyI6ImNjOTIyZTA3NzRhM2MwZWViZTI2NDM2Yzk2ZjdiYzkzIiwiX3NpZF8iOjYwOH1ciL4su5SPZh4eFGuG+5rJ+/I2gDSZAs+2dXw097gU8fBkMWzRo0dDIW0dOxHg/1mio1yMRdDZDA==").unwrap();
let doc_id = get_id_from_bytes(&enc_doc).unwrap();
assert_that!(
&doc_id,
has_structure!(DocumentId[eq("cc922e0774a3c0eebe26436c96f7bc93".to_string())])
);
let doc_parts = parse_document_parts(&enc_doc).unwrap();
assert_that!(
&doc_parts.0,
has_structure!(DocumentHeader {
document_id: eq(DocumentId("cc922e0774a3c0eebe26436c96f7bc93".to_string())),
segment_id: eq(608),
})
);
assert_that!(
&doc_parts.1.bytes(),
eq(vec![
92, 136, 190, 44, 187, 148, 143, 102, 30, 30, 20, 107, 134, 251, 154, 201, 251,
242, 54, 128, 52, 153, 2, 207, 182, 117, 124, 52, 247, 184, 20, 241, 240, 100, 49,
108, 209, 163, 71, 67, 33, 109, 29, 59, 17, 224, 255, 89, 162, 163, 92, 140, 69,
208, 217, 12
])
)
}
#[test]
fn generate_document_header_test() {
let header = DocumentHeader::new("123abc".try_into().unwrap(), 18usize);
assert_that!(
&header.pack().0,
eq(vec![
2, 0, 29, 123, 34, 95, 100, 105, 100, 95, 34, 58, 34, 49, 50, 51, 97, 98, 99, 34,
44, 34, 95, 115, 105, 100, 95, 34, 58, 49, 56, 125
])
);
}
#[test]
fn process_policy_good() {
let recrypt = recrypt::api::Recrypt::new();
let (_, pubk) = recrypt.generate_key_pair().unwrap();
let policy = PolicyResponse {
users_and_groups: vec![
UserOrGroupWithKey::User {
id: "userid1".to_string(),
master_public_key: Some(pubk.into()),
},
UserOrGroupWithKey::Group {
id: "groupid1".to_string(),
master_public_key: Some(pubk.into()),
},
],
invalid_users_and_groups: vec![],
};
let (errs, results) = process_policy(&policy);
assert_that!(results.len() == 2);
assert_that!(errs.is_empty());
let ex_user = WithKey {
id: UserOrGroup::User {
id: UserId::unsafe_from_string("userid1".to_string()),
},
public_key: pubk.into(),
};
let ex_group = WithKey {
id: UserOrGroup::Group {
id: GroupId::unsafe_from_string("groupid1".to_string()),
},
public_key: pubk.into(),
};
assert_that!(&results, contains_in_any_order(vec![ex_user, ex_group]));
}
#[test]
fn dedupe_grants_removes_dupes() {
let recrypt = recrypt::api::Recrypt::new();
let (_, pubk) = recrypt.generate_key_pair().unwrap();
let u1 = &UserId::unsafe_from_string("user1".into());
let g1 = &GroupId::unsafe_from_string("group1".into());
let grants_w_dupes: Vec<WithKey<UserOrGroup>> = vec![
WithKey::new(u1.into(), pubk.into()),
WithKey::new(g1.into(), pubk.into()),
WithKey::new(u1.into(), pubk.into()),
WithKey::new(g1.into(), pubk.into()),
];
let deduplicated_grants = dedupe_grants(&grants_w_dupes);
assert_that!(&deduplicated_grants.len(), eq(2))
}
#[test]
fn encode_encrypted_dek_proto() {
use recrypt::{api::Hashable, prelude::*};
let recrypt_api = recrypt::api::Recrypt::new();
let (_, pubk) = recrypt_api.generate_key_pair().unwrap();
let signing_keys = recrypt_api.generate_ed25519_key_pair();
let plaintext = recrypt_api.gen_plaintext();
let encrypted_value = recrypt_api
.encrypt(&plaintext, &pubk, &signing_keys)
.unwrap();
let user_str = "userid".to_string();
let edek = EncryptedDek {
encrypted_dek_data: encrypted_value.clone(),
grant_to: WithKey {
public_key: pubk.into(),
id: UserId::unsafe_from_string(user_str.clone()).borrow().into(),
},
};
let proto_edek: EncryptedDekP = edek.borrow().try_into().unwrap();
assert_eq!(&user_str, &proto_edek.userOrGroup.userId());
let (x, y) = pubk.bytes_x_y();
assert_eq!(
(x.to_vec(), y.to_vec()),
(
proto_edek.userOrGroup.masterPublicKey.x.to_vec(),
proto_edek.userOrGroup.masterPublicKey.y.to_vec()
)
);
if let recrypt::api::EncryptedValue::EncryptedOnceValue {
ephemeral_public_key,
encrypted_message,
auth_hash,
public_signing_key,
signature,
} = encrypted_value
{
assert_eq!(
(
ephemeral_public_key.bytes_x_y().0.to_vec(),
ephemeral_public_key.bytes_x_y().1.to_vec()
),
(
proto_edek.encryptedDekData.ephemeralPublicKey.x.to_vec(),
proto_edek.encryptedDekData.ephemeralPublicKey.y.to_vec()
)
);
assert_eq!(
encrypted_message.bytes().to_vec(),
proto_edek.encryptedDekData.encryptedBytes.to_vec()
);
assert_eq!(
auth_hash.bytes().to_vec(),
proto_edek.encryptedDekData.authHash.to_vec()
);
assert_eq!(
public_signing_key.to_bytes(),
proto_edek.encryptedDekData.publicSigningKey.to_vec()
);
assert_eq!(
signature.bytes().to_vec(),
proto_edek.encryptedDekData.signature.to_vec()
);
} else {
panic!("Should be EncryptedOnceValue");
}
}
#[test]
pub fn unmanaged_edoc_header_properly_encoded() -> Result<(), IronOxideErr> {
use recrypt::prelude::*;
let recr = recrypt::api::Recrypt::new();
let signingkeys = DeviceSigningKeyPair::from(recr.generate_ed25519_key_pair());
let aes_value = AesEncryptedValue::try_from(&[42u8; 32][..])?;
let uid = UserId::unsafe_from_string("userid".into());
let gid = GroupId::unsafe_from_string("groupid".into());
let user: UserOrGroup = uid.borrow().into();
let group: UserOrGroup = gid.borrow().into();
let (_, pubk) = recr.generate_key_pair()?;
let with_keys = vec![
WithKey::new(user, pubk.into()),
WithKey::new(group, pubk.into()),
];
let doc_id = DocumentId("docid".into());
let seg_id = 33;
let encryption_result = recrypt_document(
&signingkeys,
&recr,
recr.gen_plaintext(),
&doc_id,
with_keys,
)?
.into_edoc(DocumentHeader::new(doc_id.clone(), seg_id), aes_value);
assert_eq!(&encryption_result.header.document_id, &doc_id);
assert_eq!(&encryption_result.header.segment_id, &seg_id);
let edoc_bytes = encryption_result.edoc_bytes();
let (parsed_header, _) = parse_document_parts(&edoc_bytes)?;
assert_eq!(&encryption_result.header, &parsed_header);
Ok(())
}
#[test]
pub fn unmanaged_edoc_compare_grants() -> Result<(), IronOxideErr> {
use crate::proto::transform::{
UserOrGroup as UserOrGroupP, user_or_group::UserOrGroupId as UserOrGroupIdP,
};
use recrypt::prelude::*;
let recr = recrypt::api::Recrypt::new();
let signingkeys = DeviceSigningKeyPair::from(recr.generate_ed25519_key_pair());
let aes_value = AesEncryptedValue::try_from(&[42u8; 32][..])?;
let uid = UserId::unsafe_from_string("userid".into());
let gid = GroupId::unsafe_from_string("groupid".into());
let user: UserOrGroup = uid.borrow().into();
let group: UserOrGroup = gid.borrow().into();
let (_, pubk) = recr.generate_key_pair()?;
let with_keys = vec![
WithKey::new(user, pubk.into()),
WithKey::new(group, pubk.into()),
];
let doc_id = DocumentId("docid".into());
let seg_id = 33;
let encryption_result = recrypt_document(
&signingkeys,
&recr,
recr.gen_plaintext(),
&doc_id,
with_keys,
)?
.into_edoc(DocumentHeader::new(doc_id.clone(), seg_id), aes_value);
let doc_encrypt_unmanaged_result =
DocumentEncryptUnmanagedResult::new(encryption_result, vec![])?;
let proto_edeks =
EncryptedDeksP::parse_from_bytes(doc_encrypt_unmanaged_result.encrypted_deks())?;
let result: Result<Vec<UserOrGroup>, IronOxideErr> = proto_edeks
.edeks
.as_slice()
.iter()
.map(|edek| {
if let Some(UserOrGroupP {
UserOrGroupId: Some(proto_uog),
..
}) = edek.userOrGroup.as_ref()
{
match proto_uog {
UserOrGroupIdP::UserId(user_chars) => Ok(UserOrGroup::User {
id: user_chars.to_string().try_into()?,
}),
UserOrGroupIdP::GroupId(group_chars) => Ok(UserOrGroup::Group {
id: group_chars.to_string().try_into()?,
}),
}
} else {
Err(IronOxideErr::ProtobufValidationError(format!(
"EncryptedDek does not have a valid user or group: {:?}",
&edek
)))
}
})
.collect();
assert_that!(
&result?,
contains_in_any_order(vec![
UserOrGroup::Group { id: gid.clone() },
UserOrGroup::User { id: uid.clone() }
])
);
assert_that!(
&doc_encrypt_unmanaged_result.grants().to_vec(),
contains_in_any_order(vec![
UserOrGroup::Group { id: gid },
UserOrGroup::User { id: uid }
])
);
Ok(())
}
#[test]
pub fn edek_edoc_no_match() -> Result<(), IronOxideErr> {
use recrypt::prelude::*;
let recr = recrypt::api::Recrypt::new();
let signingkeys = DeviceSigningKeyPair::from(recr.generate_ed25519_key_pair());
let aes_value = AesEncryptedValue::try_from(&[42u8; 32][..])?;
let uid = UserId::unsafe_from_string("userid".into());
let gid = GroupId::unsafe_from_string("groupid".into());
let user: UserOrGroup = uid.borrow().into();
let group: UserOrGroup = gid.borrow().into();
let (_, pubk) = recr.generate_key_pair()?;
let with_keys = vec![
WithKey::new(user, pubk.into()),
WithKey::new(group, pubk.into()),
];
let doc_id = DocumentId("docid".into());
let seg_id = 33;
let encryption_result = recrypt_document(
&signingkeys,
&recr,
recr.gen_plaintext(),
&doc_id,
with_keys,
)?;
let edoc1 = encryption_result.clone().into_edoc(
DocumentHeader::new(doc_id.clone(), seg_id),
aes_value.clone(),
);
let edoc2 = encryption_result.clone().into_edoc(
DocumentHeader::new(DocumentId("other_docid".into()), seg_id),
aes_value.clone(),
);
let edoc3 = encryption_result.into_edoc(DocumentHeader::new(doc_id.clone(), 42), aes_value);
let edoc1_bytes = edoc1.edoc_bytes();
let edek2_bytes = edoc2.edek_bytes()?;
let edek3_bytes = edoc3.edek_bytes()?;
{
let proto_edeks =
EncryptedDeksP::parse_from_bytes(&edek2_bytes).map_err(IronOxideErr::from)?;
let (doc_meta, _) = parse_document_parts(&edoc1_bytes)?;
let err = edeks_and_header_match_or_err(&proto_edeks, &doc_meta).unwrap_err();
assert_that!(&err, is_variant!(IronOxideErr::UnmanagedDecryptionError));
if let IronOxideErr::UnmanagedDecryptionError(
edek_doc_id,
edek_seg_id,
edoc_doc_id,
edoc_seg_id,
) = err
{
assert_eq!(&edek_doc_id, "other_docid");
assert_eq!(edek_seg_id, seg_id as i32);
assert_eq!(&edoc_doc_id, doc_id.id());
assert_eq!(edoc_seg_id, seg_id as i32);
}
}
{
let proto_edeks =
EncryptedDeksP::parse_from_bytes(&edek3_bytes).map_err(IronOxideErr::from)?;
let (doc_meta, _) = parse_document_parts(&edoc1_bytes)?;
let err = edeks_and_header_match_or_err(&proto_edeks, &doc_meta).unwrap_err();
assert_that!(&err, is_variant!(IronOxideErr::UnmanagedDecryptionError));
if let IronOxideErr::UnmanagedDecryptionError(
edek_doc_id,
edek_seg_id,
edoc_doc_id,
edoc_seg_id,
) = err
{
assert_eq!(&edek_doc_id, doc_id.id());
assert_eq!(edek_seg_id, 42i32);
assert_eq!(&edoc_doc_id, doc_id.id());
assert_eq!(edoc_seg_id, seg_id as i32);
}
}
Ok(())
}
fn build_test_edek_bytes(
user_ids: &[&str],
group_ids: &[&str],
doc_id: &str,
seg_id: i32,
) -> Result<Vec<u8>, IronOxideErr> {
use recrypt::prelude::*;
let recr = recrypt::api::Recrypt::new();
let signingkeys = DeviceSigningKeyPair::from(recr.generate_ed25519_key_pair());
let (_, pubk) = recr.generate_key_pair()?;
let mut with_keys: Vec<WithKey<UserOrGroup>> = Vec::new();
for uid_str in user_ids {
let uid = UserId::unsafe_from_string(uid_str.to_string());
with_keys.push(WithKey::new(uid.borrow().into(), pubk.into()));
}
for gid_str in group_ids {
let gid = GroupId::unsafe_from_string(gid_str.to_string());
with_keys.push(WithKey::new(gid.borrow().into(), pubk.into()));
}
let document_id = DocumentId(doc_id.into());
let recryption_result = recrypt_document(
&signingkeys,
&recr,
recr.gen_plaintext(),
&document_id,
with_keys,
)?;
edeks_to_edeks_proto(&recryption_result.edeks, doc_id, seg_id)
}
mod revoke_access_unmanaged {
use super::*;
#[test]
fn revoke_user_succeeds() {
let edek_bytes = build_test_edek_bytes(&["user1", "user2"], &[], "doc1", 1).unwrap();
let revoke_list = vec![UserOrGroup::User {
id: UserId::unsafe_from_string("user1".into()),
}];
let result = document_revoke_access_unmanaged(&edek_bytes, &revoke_list).unwrap();
assert!(result.access_via().is_none());
assert_eq!(result.succeeded().len(), 1);
assert_eq!(
result.succeeded()[0],
UserOrGroup::User {
id: UserId::unsafe_from_string("user1".into())
}
);
assert!(result.failed().is_empty());
let remaining = EncryptedDeksP::parse_from_bytes(result.encrypted_deks()).unwrap();
assert_eq!(remaining.edeks.len(), 1);
}
#[test]
fn revoke_group_succeeds() {
let edek_bytes = build_test_edek_bytes(&[], &["group1", "group2"], "doc1", 1).unwrap();
let revoke_list = vec![UserOrGroup::Group {
id: GroupId::unsafe_from_string("group2".into()),
}];
let result = document_revoke_access_unmanaged(&edek_bytes, &revoke_list).unwrap();
assert_eq!(result.succeeded().len(), 1);
assert_eq!(
result.succeeded()[0],
UserOrGroup::Group {
id: GroupId::unsafe_from_string("group2".into())
}
);
assert!(result.failed().is_empty());
let remaining = EncryptedDeksP::parse_from_bytes(result.encrypted_deks()).unwrap();
assert_eq!(remaining.edeks.len(), 1);
}
#[test]
fn revoke_nonexistent_user_reports_failure() {
let edek_bytes = build_test_edek_bytes(&["user1"], &[], "doc1", 1).unwrap();
let revoke_list = vec![UserOrGroup::User {
id: UserId::unsafe_from_string("no_such_user".into()),
}];
let result = document_revoke_access_unmanaged(&edek_bytes, &revoke_list).unwrap();
assert!(result.succeeded().is_empty());
assert_eq!(result.failed().len(), 1);
assert_eq!(
result.failed()[0].user_or_group,
UserOrGroup::User {
id: UserId::unsafe_from_string("no_such_user".into())
}
);
assert!(result.failed()[0].err.contains("not found in EDEK grants"));
let remaining = EncryptedDeksP::parse_from_bytes(result.encrypted_deks()).unwrap();
assert_eq!(remaining.edeks.len(), 1);
}
#[test]
fn revoke_mixed_success_and_failure() {
let edek_bytes = build_test_edek_bytes(&["user1"], &["group1"], "doc1", 1).unwrap();
let revoke_list = vec![
UserOrGroup::User {
id: UserId::unsafe_from_string("user1".into()),
},
UserOrGroup::Group {
id: GroupId::unsafe_from_string("nonexistent_group".into()),
},
];
let result = document_revoke_access_unmanaged(&edek_bytes, &revoke_list).unwrap();
assert_eq!(result.succeeded().len(), 1);
assert_eq!(
result.succeeded()[0],
UserOrGroup::User {
id: UserId::unsafe_from_string("user1".into())
}
);
assert_eq!(result.failed().len(), 1);
assert_eq!(
result.failed()[0].user_or_group,
UserOrGroup::Group {
id: GroupId::unsafe_from_string("nonexistent_group".into())
}
);
let remaining = EncryptedDeksP::parse_from_bytes(result.encrypted_deks()).unwrap();
assert_eq!(remaining.edeks.len(), 1); }
#[test]
fn revoke_all_grants_leaves_empty_edeks() {
let edek_bytes = build_test_edek_bytes(&["user1"], &["group1"], "doc1", 1).unwrap();
let revoke_list = vec![
UserOrGroup::User {
id: UserId::unsafe_from_string("user1".into()),
},
UserOrGroup::Group {
id: GroupId::unsafe_from_string("group1".into()),
},
];
let result = document_revoke_access_unmanaged(&edek_bytes, &revoke_list).unwrap();
assert_eq!(result.succeeded().len(), 2);
assert!(result.failed().is_empty());
let remaining = EncryptedDeksP::parse_from_bytes(result.encrypted_deks()).unwrap();
assert_eq!(remaining.edeks.len(), 0);
}
#[test]
fn revoke_empty_list_is_noop() {
let edek_bytes = build_test_edek_bytes(&["user1"], &["group1"], "doc1", 1).unwrap();
let result = document_revoke_access_unmanaged(&edek_bytes, &[]).unwrap();
assert!(result.succeeded().is_empty());
assert!(result.failed().is_empty());
let remaining = EncryptedDeksP::parse_from_bytes(result.encrypted_deks()).unwrap();
assert_eq!(remaining.edeks.len(), 2);
}
#[test]
fn revoke_preserves_document_id_and_segment() {
let edek_bytes =
build_test_edek_bytes(&["user1", "user2"], &[], "my_doc_42", 99).unwrap();
let revoke_list = vec![UserOrGroup::User {
id: UserId::unsafe_from_string("user1".into()),
}];
let result = document_revoke_access_unmanaged(&edek_bytes, &revoke_list).unwrap();
let remaining = EncryptedDeksP::parse_from_bytes(result.encrypted_deks()).unwrap();
assert_eq!(remaining.documentId.to_string(), "my_doc_42");
assert_eq!(remaining.segmentId, 99);
}
#[test]
fn revoke_garbage_bytes_fails() {
let result = document_revoke_access_unmanaged(b"not_valid_proto", &[]);
assert!(result.is_err());
}
#[test]
fn revoke_empty_bytes_is_noop() {
let result = document_revoke_access_unmanaged(&[], &[]).unwrap();
assert!(result.succeeded().is_empty());
assert!(result.failed().is_empty());
}
}
mod get_id_from_edeks {
use super::*;
use crate::document::advanced::DocumentAdvancedOps;
use crate::internal::tests::create_test_sdk;
#[test]
fn extracts_document_id_from_valid_edeks() {
let edek_bytes = build_test_edek_bytes(&["user1"], &[], "test_doc_id", 1).unwrap();
let sdk = create_test_sdk().unwrap();
let doc_id = sdk
.document_get_id_from_edeks_unmanaged(&edek_bytes)
.unwrap();
assert_eq!(doc_id.id(), "test_doc_id");
}
#[test]
fn empty_bytes_returns_err_for_missing_doc_id() {
let sdk = create_test_sdk().unwrap();
let result = sdk.document_get_id_from_edeks_unmanaged(&[]);
assert!(result.is_err());
}
}
mod edeks_to_proto_roundtrip {
use super::*;
#[test]
fn roundtrip_preserves_doc_id_segment_and_edek_count() {
let recr = recrypt::api::Recrypt::new();
let signingkeys = DeviceSigningKeyPair::from(recr.generate_ed25519_key_pair());
let (_, pubk) = recr.generate_key_pair().unwrap();
let uid = UserId::unsafe_from_string("user1".into());
let gid = GroupId::unsafe_from_string("group1".into());
let with_keys = vec![
WithKey::new(UserOrGroup::from(uid), pubk.into()),
WithKey::new(UserOrGroup::from(gid), pubk.into()),
];
let doc_id = DocumentId("roundtrip_doc".into());
let recryption_result = recrypt_document(
&signingkeys,
&recr,
recr.gen_plaintext(),
&doc_id,
with_keys,
)
.unwrap();
let bytes =
edeks_to_edeks_proto(&recryption_result.edeks, "roundtrip_doc", 77).unwrap();
let parsed = EncryptedDeksP::parse_from_bytes(&bytes).unwrap();
assert_eq!(parsed.documentId.to_string(), "roundtrip_doc");
assert_eq!(parsed.segmentId, 77);
assert_eq!(parsed.edeks.len(), 2);
}
#[test]
fn empty_edeks_produces_valid_proto() {
let bytes = edeks_to_edeks_proto(&[], "empty_doc", 5).unwrap();
let parsed = EncryptedDeksP::parse_from_bytes(&bytes).unwrap();
assert_eq!(parsed.documentId.to_string(), "empty_doc");
assert_eq!(parsed.segmentId, 5);
assert_eq!(parsed.edeks.len(), 0);
}
}
}