From 5620696b2586f1b96a714580d954db473fd7f870 Mon Sep 17 00:00:00 2001 From: Clint Frederickson Date: Wed, 14 Aug 2019 12:22:13 -0600 Subject: [PATCH 1/9] #[22] first pass at end to end unmanaged decryption --- .gitignore | 1 + src/document/advanced.rs | 30 ++++- src/document/mod.rs | 4 +- src/internal/document_api/mod.rs | 170 +++++++++++++++++++------- src/internal/document_api/requests.rs | 26 +++- src/internal/mod.rs | 1 + src/internal/rest.rs | 77 ++++++++++++ tests/document_ops.rs | 19 ++- 8 files changed, 277 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 98b0159d..ef0c98be 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ java/scala/src/test/resources/service-keys.conf java/scala/src/test/resources/service-keys.conf.stage java/scala/src/test/resources/service-keys.conf.local .vscode +src/proto/transform.rs diff --git a/src/document/advanced.rs b/src/document/advanced.rs index 107cf609..a2405c10 100644 --- a/src/document/advanced.rs +++ b/src/document/advanced.rs @@ -1,6 +1,8 @@ use crate::document::{partition_user_or_group, DocumentEncryptOpts}; use crate::internal; -use crate::internal::document_api::DocumentEncryptUnmanagedResult; +pub use crate::internal::document_api::{ + DocumentDecryptUnmanagedResult, DocumentEncryptUnmanagedResult, +}; use crate::Result; use itertools::EitherOrBoth; use tokio::runtime::current_thread::Runtime; @@ -25,6 +27,12 @@ pub trait DocumentAdvancedOps { data: &[u8], encrypt_opts: &DocumentEncryptOpts, ) -> Result; + + fn document_decrypt_unmanaged( + &self, + encrypted_data: &[u8], + encrypted_deks: &[u8], + ) -> Result; } impl DocumentAdvancedOps for crate::IronOxide { @@ -66,4 +74,24 @@ impl DocumentAdvancedOps for crate::IronOxide { policy_grants, )) } + + fn document_decrypt_unmanaged( + &self, + encrypted_data: &[u8], + encrypted_deks: &[u8], + ) -> Result { + let deks: crate::proto::transform::EncryptedDeks = + protobuf::parse_from_bytes(&encrypted_deks)?; + dbg!(&deks); + + let mut rt = Runtime::new().unwrap(); + + rt.block_on(internal::document_api::decrypt_document_unmanaged( + self.device.auth(), + &self.recrypt, + self.device().private_device_key(), + encrypted_data, + encrypted_deks, + )) + } } diff --git a/src/document/mod.rs b/src/document/mod.rs index 0dbafe95..41fb274c 100644 --- a/src/document/mod.rs +++ b/src/document/mod.rs @@ -1,7 +1,7 @@ pub use crate::internal::document_api::{ AssociationType, DocAccessEditErr, DocumentAccessResult, DocumentDecryptResult, - DocumentEncryptResult, DocumentEncryptUnmanagedResult, DocumentListMeta, DocumentListResult, - DocumentMetadataResult, UserOrGroup, VisibleGroup, VisibleUser, + DocumentEncryptResult, DocumentListMeta, DocumentListResult, DocumentMetadataResult, + UserOrGroup, VisibleGroup, VisibleUser, }; use crate::{ internal::{ diff --git a/src/internal/document_api/mod.rs b/src/internal/document_api/mod.rs index 59843782..3837484b 100644 --- a/src/internal/document_api/mod.rs +++ b/src/internal/document_api/mod.rs @@ -95,9 +95,9 @@ struct DocumentHeader { pub segment_id: usize, } -// Take an encrypted document and extract out the header metadata. Return that metadata as well as the AESEncryptedValue -// that contains the AES IV and encrypted content. Will fail if the provided document doesn't contain the latest version -// which contains the header bytes. +/// Take an encrypted document and extract out the header metadata. Return that metadata as well as the AESEncryptedValue +/// that contains the AES IV and encrypted content. Will fail if the provided document doesn't contain the latest version +/// which contains the header bytes. fn parse_document_parts( encrypted_document: &[u8], ) -> Result<(DocumentHeader, aes::AesEncryptedValue), IronOxideErr> { @@ -131,9 +131,10 @@ fn parse_document_parts( } } -// Generate a documents header given its ID and internal segment ID that is is associated with. Generates -//a Vec which includes the document version, header size, and header JSON as bytes. -fn generate_document_header(document_id: DocumentId, segment_id: usize) -> Vec { +/// Generate a documents header given its ID and internal segment ID that is is associated with. Generates +/// a Vec which includes the document version, header size, and header JSON as bytes. +// TODO hand this off of DocHeaderPacked? +fn generate_document_header(document_id: DocumentId, segment_id: usize) -> DocHeaderPacked { let mut header_json_bytes = serde_json::to_vec(&DocumentHeader { document_id, segment_id, @@ -147,7 +148,7 @@ fn generate_document_header(document_id: DocumentId, segment_id: usize) -> Vec> 8) as u8); header.push(header_json_len as u8); header.append(&mut header_json_bytes); - header + DocHeaderPacked(header) } /// Represents the reason a document can be viewed by the requesting user. @@ -276,10 +277,11 @@ impl DocumentEncryptUnmanagedResult { fn new( doc_id: &DocumentId, segment_id: usize, - encryption_result: EncryptionResult, + encryption_result: EncryptionResultWithHeader, access_errs: Vec, ) -> Result { let proto_edek_vec_results: Result, _> = encryption_result + .value .edeks .iter() .map(|edek| edek.try_into()) @@ -296,9 +298,10 @@ impl DocumentEncryptUnmanagedResult { Ok(DocumentEncryptUnmanagedResult { id: doc_id.clone(), access_errs, - encrypted_data: encryption_result.encrypted_data.bytes(), + encrypted_data: encryption_result.to_bytes(), encrypted_deks: edek_bytes, grants: encryption_result + .value .edeks .iter() .map(|edek| edek.grant_to.id.clone()) @@ -535,9 +538,13 @@ pub fn encrypt_document< .into_future() .and_then(move |r| { let encryption_errs = r.encryption_errs.clone(); + let enc_result = EncryptionResultWithHeader { + header: generate_document_header(doc_id.clone(), auth.segment_id), + value: r, + }; document_create( &auth, - r, + enc_result, doc_id, &document_name, [key_errs, encryption_errs].concat(), @@ -638,7 +645,7 @@ where )) .and_then(move |(encryption_result, (grants, key_errs))| { Ok({ - let encryption_result = recrypt_document( + let r = recrypt_document( &auth.signing_keys, recrypt, dek, @@ -646,11 +653,15 @@ where &doc_id, grants, )?; - let access_errs = [&key_errs[..], &encryption_result.encryption_errs[..]].concat(); + let enc_result = EncryptionResultWithHeader { + header: generate_document_header(doc_id.clone(), auth.segment_id), + value: r, + }; + let access_errs = [&key_errs[..], &enc_result.value.encryption_errs[..]].concat(); DocumentEncryptUnmanagedResult::new( &doc_id, auth.segment_id, - encryption_result, + enc_result, access_errs, )? }) @@ -803,10 +814,34 @@ pub struct EncryptionResult { encryption_errs: Vec, } +impl EncryptionResult { + pub fn with_header(self, header: DocHeaderPacked) -> EncryptionResultWithHeader { + EncryptionResultWithHeader { + value: self, + header, + } + } +} + +pub struct DocHeaderPacked(Vec); + +// TODO struct EncryptedDoc? +pub struct EncryptionResultWithHeader { + header: DocHeaderPacked, + value: EncryptionResult, +} + +// TODO to_bytes isn't right +impl EncryptionResultWithHeader { + fn to_bytes(&self) -> Vec { + [self.header.0.clone(), self.value.encrypted_data.bytes()].concat() //TODO + } +} + /// Creates an encrypted document entry in the IronCore webservice. pub fn document_create<'a>( auth: &'a RequestAuth, - encryption_result: EncryptionResult, + encryption_result: EncryptionResultWithHeader, doc_id: DocumentId, doc_name: &Option, accum_errs: Vec, @@ -815,32 +850,16 @@ pub fn document_create<'a>( auth, doc_id.clone(), doc_name.clone(), - encryption_result.edeks.to_vec(), + encryption_result.value.edeks.to_vec(), ) - .map(move |resp| { - ( - doc_id, - resp, - encryption_result.encrypted_data.clone(), - encryption_result.encryption_errs.clone(), - ) - }) - .map(move |(doc_id, api_resp, encrypted_data, encrypt_errs)| { - //Generate and prepend the document header to the encrypted document - let encrypted_payload = [ - generate_document_header(doc_id.clone(), auth.segment_id()), - encrypted_data.bytes(), - ] - .concat(); - DocumentEncryptResult { - id: api_resp.id, - name: api_resp.name, - created: api_resp.created, - updated: api_resp.updated, - encrypted_data: encrypted_payload, - grants: api_resp.shared_with.iter().map(|sw| sw.into()).collect(), - access_errs: [accum_errs, encrypt_errs].concat(), - } + .map(move |api_resp| DocumentEncryptResult { + id: api_resp.id, + name: api_resp.name, + created: api_resp.created, + updated: api_resp.updated, + encrypted_data: encryption_result.to_bytes(), + grants: api_resp.shared_with.iter().map(|sw| sw.into()).collect(), + access_errs: [accum_errs, encryption_result.value.encryption_errs].concat(), }) } @@ -869,13 +888,13 @@ pub fn document_update_bytes< move |encrypted_doc| { let mut encrypted_payload = generate_document_header(document_id.clone(), auth.segment_id()); - encrypted_payload.append(&mut encrypted_doc.bytes()); + 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, + encrypted_data: encrypted_payload.0, grants: vec![], // grants can't currently change via update access_errs: vec![], // no grants, no access errs } @@ -885,8 +904,8 @@ pub fn document_update_bytes< }) } -//Decrypt the provided document with the provided device private key. Return metadata about the document -//that was decrypted along with it's decrypted bytes. +/// Decrypt the provided document with the provided device private key. Return metadata about the document +/// that was decrypted along with it's decrypted bytes. pub fn decrypt_document<'a, CR: rand::CryptoRng + rand::RngCore>( auth: &'a RequestAuth, recrypt: &'a Recrypt>, @@ -915,6 +934,62 @@ pub fn decrypt_document<'a, CR: rand::CryptoRng + rand::RngCore>( }) } +/// Decrypt the unmanaged document. The caller must provide both the encrypted data as well as the +/// encrypted DEKs. Most use cases would want `decrypt_document` instead. +pub fn decrypt_document_unmanaged<'a, CR: rand::CryptoRng + rand::RngCore>( + auth: &'a RequestAuth, + recrypt: &'a Recrypt>, + device_private_key: &'a PrivateKey, + encrypted_doc: &'a [u8], + encrypted_deks: &'a [u8], +) -> impl Future + 'a { + requests::edek_transform::edek_transform(&auth, encrypted_deks) + .join(parse_document_parts(encrypted_doc).into_future()) + .and_then( + move |( + requests::edek_transform::EdekTransformResponse { + user_or_group, + encrypted_symmetric_key, + }, + (DocumentHeader { document_id, .. }, mut enc_data), + )| { + let (_, sym_key) = transform::decrypt_plaintext( + &recrypt, + encrypted_symmetric_key.try_into()?, + &device_private_key.recrypt_key(), + )?; + aes::decrypt(&mut enc_data, *sym_key.bytes()) + .map_err(|e| e.into()) + .map(move |decrypted_doc| DocumentDecryptUnmanagedResult { + id: document_id, + access_via: user_or_group, + decrypted_data: decrypted_doc.to_vec(), + }) + }, + ) +} + +pub struct DocumentDecryptUnmanagedResult { + id: DocumentId, + access_via: UserOrGroup, + decrypted_data: Vec, +} + +impl DocumentDecryptUnmanagedResult { + pub fn id(&self) -> &DocumentId { + &self.id + } + + /// user or group that allowed the caller to decrypt the data + pub fn access_via(&self) -> &UserOrGroup { + &self.access_via + } + + pub fn decrypted_data(&self) -> &[u8] { + &self.decrypted_data + } +} + // Update a documents name. Value can be updated to either a new name with a Some or the name value can be cleared out // by providing a None. pub fn update_document_name<'a>( @@ -1277,7 +1352,7 @@ mod tests { let header = generate_document_header("123abc".try_into().unwrap(), 18usize); assert_that!( - &header, + &header.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 @@ -1442,6 +1517,9 @@ mod tests { panic!("Should be EncryptedOnceValue"); } } + + //TODO test that shows edoc header is properly generated + #[test] pub fn compare_grants_to_edek_encoded() -> Result<(), IronOxideErr> { use crate::proto::transform::UserOrGroup as UserOrGroupP; @@ -1461,6 +1539,7 @@ mod tests { WithKey::new(group, pubk.into()), ]; let doc_id = DocumentId("docid".into()); + let seg_id = 33; let encryption_result = recrypt_document( &signingkeys, @@ -1469,7 +1548,8 @@ mod tests { aes_value, &doc_id, with_keys, - )?; + )? + .with_header(generate_document_header(doc_id.clone(), seg_id)); // create an unmanged result, which does the proto serialization let doc_encrypt_unmanaged_result = diff --git a/src/internal/document_api/requests.rs b/src/internal/document_api/requests.rs index 9400af2b..5112b5b2 100644 --- a/src/internal/document_api/requests.rs +++ b/src/internal/document_api/requests.rs @@ -35,7 +35,7 @@ pub enum UserOrGroupWithKey { User { id: String, // optional because the resp on document create does not return a public key - master_public_key: Option, + master_public_key: Option, //TODO can I replace the None usages of this with UserOrGroup }, #[serde(rename_all = "camelCase")] Group { @@ -179,6 +179,30 @@ pub mod document_get { } } +pub mod edek_transform { + use super::*; + + pub fn edek_transform( + auth: &RequestAuth, + edek_bytes: &[u8], + ) -> impl Future { + auth.request.post_raw( + "edeks/transform", + edek_bytes, + RequestErrorCode::EdekTransform, + &auth.create_signature(Utc::now()), + ) + } + + #[derive(Serialize, Debug, Clone, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + pub struct EdekTransformResponse { + pub(in crate::internal::document_api) user_or_group: UserOrGroup, + pub(in crate::internal::document_api) encrypted_symmetric_key: TransformedEncryptedValue, + } + +} + pub mod document_create { use super::*; use crate::internal::document_api::{DocumentName, EncryptedDek}; diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 2461a32a..6cc05d06 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -56,6 +56,7 @@ pub enum RequestErrorCode { DocumentUpdate, DocumentGrantAccess, DocumentRevokeAccess, + EdekTransform, PolicyGet, } diff --git a/src/internal/rest.rs b/src/internal/rest.rs index 3bb8ed9d..107a127b 100644 --- a/src/internal/rest.rs +++ b/src/internal/rest.rs @@ -133,6 +133,24 @@ impl<'a> IronCoreRequest<'a> { ) } + pub fn post_raw( + &self, + relative_url: &str, + body: &[u8], + error_code: RequestErrorCode, + auth: &Authorization, + ) -> impl Future { + self.request_raw::<_, String, _>( + relative_url, + Method::POST, + Some(body), + None, + error_code, + auth.to_header(), + move |server_resp| IronCoreRequest::deserialize_body(server_resp, error_code), + ) + } + ///PUT body to the resource at relative_url using auth for authorization. ///If the request fails a RequestError will be raised. pub fn put( @@ -239,6 +257,65 @@ impl<'a> IronCoreRequest<'a> { ) } + // TODO combine with `request` + pub fn request_raw( + &self, + relative_url: &str, + method: Method, + maybe_body: Option<&[u8]>, + maybe_query_params: Option<&Q>, + error_code: RequestErrorCode, + headers: HeaderMap, + resp_handler: F, + ) -> impl Future + where + B: DeserializeOwned, + Q: Serialize + ?Sized, + F: FnOnce(&Chunk) -> Result, + { + let client = RClient::new(); + let mut builder = client.request( + method, + format!("{}{}", self.base_url, relative_url).as_str(), + ); + // add query params, if any + builder = maybe_query_params + .iter() + .fold(builder, |build, q| build.query(q)); + + //We want to add the body as json if it was specified + builder = maybe_body + .iter() + .fold(builder, |build, body| build.body(body.to_vec())); + + let req = builder.headers(DEFAULT_HEADERS.clone()).headers(headers); + req.send() + //Parse the body content into bytes + .and_then(|res| { + let status_code = res.status(); + res.into_body() + .concat2() + .map(move |body| (status_code, body)) + }) + //Now make the error type into the IronOxideErr and run the resp_handler which was passed to us. + .then(move |resp| { + //Map the generic error from reqwest to our error type. + let (status, server_resp) = resp.map_err(|err| { + IronCoreRequest::create_request_err(err.to_string(), error_code, err.status()) + })?; + //If the status code is a 5xx, return a fixed error code message + if status.is_server_error() || status.is_client_error() { + Err(IronCoreRequest::request_failure_to_error( + status, + error_code, + &server_resp, + )) + } else { + resp_handler(&server_resp) + } + }) + } + ///Make a request to the url using the specified method. DEFAULT_HEADERS will be used as well as whatever headers are passed /// in. The response will be sent to `resp_handler` so the caller can make the received bytes however they want. pub fn request( diff --git a/tests/document_ops.rs b/tests/document_ops.rs index 9584445c..69a0107d 100644 --- a/tests/document_ops.rs +++ b/tests/document_ops.rs @@ -548,6 +548,21 @@ fn doc_decrypt_roundtrip() { assert_eq!(doc.to_vec(), decrypted.decrypted_data()); } +#[test] +fn doc_decrypt_unmanaged_roundtrip() -> Result<(), IronOxideErr> { + let sdk = init_sdk(); + let encrypt_opts = Default::default(); + let doc = [0u8; 42]; + + let encrypt_result = sdk.document_encrypt_unmanaged(&doc, &encrypt_opts)?; + let decrypt_result = sdk.document_decrypt_unmanaged( + &encrypt_result.encrypted_data(), + &encrypt_result.encrypted_deks(), + )?; + assert_eq!(&doc[..], decrypt_result.decrypted_data()); + Ok(()) +} + #[test] fn doc_encrypt_update_and_decrypt() { let sdk = init_sdk(); @@ -580,7 +595,7 @@ fn doc_grant_access() { // create a second user to grant access to the document let user = create_second_user(); - // group user is a memeber of + // group user is a member of let group_result = sdk.group_create(&Default::default()); assert!(group_result.is_ok()); let group_id = group_result.unwrap().id().clone(); @@ -606,7 +621,7 @@ fn doc_grant_access() { }, ], ); - dbg!(&grant_result); + // dbg!(&grant_result); assert!(grant_result.is_ok()); let grants = grant_result.unwrap(); assert_eq!(3, grants.succeeded().len()); From 6fbe766835ffc697300e3ec93dd70f28867613a0 Mon Sep 17 00:00:00 2001 From: Clint Frederickson Date: Thu, 15 Aug 2019 11:00:06 -0600 Subject: [PATCH 2/9] #[22] attempt to make byte array handling safer --- src/internal/document_api/mod.rs | 125 +++++++++++++++++-------------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/src/internal/document_api/mod.rs b/src/internal/document_api/mod.rs index 3837484b..3cf85bc8 100644 --- a/src/internal/document_api/mod.rs +++ b/src/internal/document_api/mod.rs @@ -86,13 +86,40 @@ impl TryFrom<&str> for DocumentName { } } +/// Binary version of the document header. Appropriate for using in edoc serialization. +struct DocHeaderPacked(Vec); + /// Represents a parsed document header which is decoded from JSON #[derive(Debug, Serialize, Deserialize, PartialEq)] struct DocumentHeader { #[serde(rename = "_did_")] - pub document_id: DocumentId, + document_id: DocumentId, #[serde(rename = "_sid_")] - pub segment_id: usize, + segment_id: usize, +} + +impl DocumentHeader { + fn new(document_id: DocumentId, segment_id: usize) -> DocumentHeader { + DocumentHeader { + document_id, + segment_id, + } + } + /// Generate a documents header given its ID and internal segment ID that is is associated with. Generates + /// a Vec which includes the document version, header size, and header JSON as bytes. + fn pack(&self) -> DocHeaderPacked { + let mut header_json_bytes = + serde_json::to_vec(&self).expect("Serialization of DocumentHeader failed."); //Serializing a string and number shouldn't fail + let header_json_len = header_json_bytes.len(); + //Make header vector with size of header plus 1 byte for version and 2 bytes for header length + let mut header = Vec::with_capacity(header_json_len + 3); + header.push(CURRENT_DOCUMENT_ID_VERSION); + //Push the header length representation as two bytes, most significant digit first (BigEndian) + header.push((header_json_len >> 8) as u8); + header.push(header_json_len as u8); + header.append(&mut header_json_bytes); + DocHeaderPacked(header) + } } /// Take an encrypted document and extract out the header metadata. Return that metadata as well as the AESEncryptedValue @@ -131,26 +158,6 @@ fn parse_document_parts( } } -/// Generate a documents header given its ID and internal segment ID that is is associated with. Generates -/// a Vec which includes the document version, header size, and header JSON as bytes. -// TODO hand this off of DocHeaderPacked? -fn generate_document_header(document_id: DocumentId, segment_id: usize) -> DocHeaderPacked { - let mut header_json_bytes = serde_json::to_vec(&DocumentHeader { - document_id, - segment_id, - }) - .expect("Serialization of DocumentHeader failed."); //Serializing a string and number shouldn't fail - let header_json_len = header_json_bytes.len(); - //Make header vector with size of header plus 1 byte for version and 2 bytes for header length - let mut header = Vec::with_capacity(header_json_len + 3); - header.push(CURRENT_DOCUMENT_ID_VERSION); - //Push the header length representation as two bytes, most significant digit first (BigEndian) - header.push((header_json_len >> 8) as u8); - header.push(header_json_len as u8); - header.append(&mut header_json_bytes); - DocHeaderPacked(header) -} - /// Represents the reason a document can be viewed by the requesting user. #[derive(Serialize, Deserialize, Debug, Clone, Hash, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -277,7 +284,7 @@ impl DocumentEncryptUnmanagedResult { fn new( doc_id: &DocumentId, segment_id: usize, - encryption_result: EncryptionResultWithHeader, + encryption_result: EncryptedDocUnmanaged, access_errs: Vec, ) -> Result { let proto_edek_vec_results: Result, _> = encryption_result @@ -298,7 +305,7 @@ impl DocumentEncryptUnmanagedResult { Ok(DocumentEncryptUnmanagedResult { id: doc_id.clone(), access_errs, - encrypted_data: encryption_result.to_bytes(), + encrypted_data: encryption_result.edoc_bytes().to_vec(), encrypted_deks: edek_bytes, grants: encryption_result .value @@ -538,13 +545,9 @@ pub fn encrypt_document< .into_future() .and_then(move |r| { let encryption_errs = r.encryption_errs.clone(); - let enc_result = EncryptionResultWithHeader { - header: generate_document_header(doc_id.clone(), auth.segment_id), - value: r, - }; document_create( &auth, - enc_result, + r.into_edoc_unmanaged(DocumentHeader::new(doc_id.clone(), auth.segment_id)), doc_id, &document_name, [key_errs, encryption_errs].concat(), @@ -653,8 +656,8 @@ where &doc_id, grants, )?; - let enc_result = EncryptionResultWithHeader { - header: generate_document_header(doc_id.clone(), auth.segment_id), + let enc_result = EncryptedDocUnmanaged { + header: DocumentHeader::new(doc_id.clone(), auth.segment_id), value: r, }; let access_errs = [&key_errs[..], &enc_result.value.encryption_errs[..]].concat(); @@ -693,7 +696,7 @@ fn recrypt_document<'a, CR: rand::CryptoRng + rand::RngCore>( encrypted_doc: AesEncryptedValue, doc_id: &DocumentId, grants: Vec>, -) -> Result { +) -> Result { // check to make sure that we are granting to something if grants.is_empty() { Err(IronOxideErr::ValidationError( @@ -713,7 +716,7 @@ fn recrypt_document<'a, CR: rand::CryptoRng + rand::RngCore>( dedupe_grants(&grants), ); - EncryptionResult { + RecryptionResult { edeks: grants .into_iter() .map(|(wk, ev)| EncryptedDek { @@ -808,40 +811,45 @@ impl TryFrom<&EncryptedDek> for EncryptedDekP { } #[derive(Debug, Clone)] -pub struct EncryptionResult { +struct RecryptionResult { edeks: Vec, encrypted_data: AesEncryptedValue, encryption_errs: Vec, } -impl EncryptionResult { - pub fn with_header(self, header: DocHeaderPacked) -> EncryptionResultWithHeader { - EncryptionResultWithHeader { +impl RecryptionResult { + fn into_edoc_unmanaged(self, header: DocumentHeader) -> EncryptedDocUnmanaged { + EncryptedDocUnmanaged { value: self, header, } } } -pub struct DocHeaderPacked(Vec); - -// TODO struct EncryptedDoc? -pub struct EncryptionResultWithHeader { - header: DocHeaderPacked, - value: EncryptionResult, +struct EncryptedDocUnmanaged { + header: DocumentHeader, + value: RecryptionResult, } -// TODO to_bytes isn't right -impl EncryptionResultWithHeader { - fn to_bytes(&self) -> Vec { - [self.header.0.clone(), self.value.encrypted_data.bytes()].concat() //TODO +// TODO is this a reasonable name? +impl EncryptedDocUnmanaged { + fn edoc_bytes(&self) -> Vec { + [ + &self.header.pack().0[..], + &self.value.encrypted_data.bytes(), + ] + .concat() //TODO } + + // fn edek_bytes(&self) -> &[u8] { + // &self.value.edeks //TODO I could move the proto serialization here... + // } } /// Creates an encrypted document entry in the IronCore webservice. -pub fn document_create<'a>( +fn document_create<'a>( auth: &'a RequestAuth, - encryption_result: EncryptionResultWithHeader, + encryption_result: EncryptedDocUnmanaged, doc_id: DocumentId, doc_name: &Option, accum_errs: Vec, @@ -857,7 +865,7 @@ pub fn document_create<'a>( name: api_resp.name, created: api_resp.created, updated: api_resp.updated, - encrypted_data: encryption_result.to_bytes(), + encrypted_data: encryption_result.edoc_bytes().to_vec(), grants: api_resp.shared_with.iter().map(|sw| sw.into()).collect(), access_errs: [accum_errs, encryption_result.value.encryption_errs].concat(), }) @@ -887,7 +895,7 @@ pub fn document_update_bytes< aes::encrypt(&rng, &plaintext.to_vec(), *sym_key.bytes()).map( move |encrypted_doc| { let mut encrypted_payload = - generate_document_header(document_id.clone(), auth.segment_id()); + DocumentHeader::new(document_id.clone(), auth.segment_id()).pack(); encrypted_payload.0.append(&mut encrypted_doc.bytes()); DocumentEncryptResult { id: doc_meta.0.id, @@ -943,7 +951,7 @@ pub fn decrypt_document_unmanaged<'a, CR: rand::CryptoRng + rand::RngCore>( encrypted_doc: &'a [u8], encrypted_deks: &'a [u8], ) -> impl Future + 'a { - requests::edek_transform::edek_transform(&auth, encrypted_deks) + requests::edek_transform::edek_transform(&auth, encrypted_deks) //TODO proto decode here? .join(parse_document_parts(encrypted_doc).into_future()) .and_then( move |( @@ -963,16 +971,17 @@ pub fn decrypt_document_unmanaged<'a, CR: rand::CryptoRng + rand::RngCore>( .map(move |decrypted_doc| DocumentDecryptUnmanagedResult { id: document_id, access_via: user_or_group, - decrypted_data: decrypted_doc.to_vec(), + decrypted_data: DecryptedData(decrypted_doc.to_vec()), }) }, ) } +struct DecryptedData(Vec); pub struct DocumentDecryptUnmanagedResult { id: DocumentId, access_via: UserOrGroup, - decrypted_data: Vec, + decrypted_data: DecryptedData, } impl DocumentDecryptUnmanagedResult { @@ -986,7 +995,7 @@ impl DocumentDecryptUnmanagedResult { } pub fn decrypted_data(&self) -> &[u8] { - &self.decrypted_data + &self.decrypted_data.0 } } @@ -1349,10 +1358,10 @@ mod tests { #[test] fn generate_document_header_test() { - let header = generate_document_header("123abc".try_into().unwrap(), 18usize); + let header = DocumentHeader::new("123abc".try_into().unwrap(), 18usize); assert_that!( - &header.0, + &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 @@ -1549,7 +1558,7 @@ mod tests { &doc_id, with_keys, )? - .with_header(generate_document_header(doc_id.clone(), seg_id)); + .into_edoc_unmanaged(DocumentHeader::new(doc_id.clone(), seg_id)); // create an unmanged result, which does the proto serialization let doc_encrypt_unmanaged_result = From f0e5916a6f07eb18ef2723591fd56811b15f3f61 Mon Sep 17 00:00:00 2001 From: Clint Frederickson Date: Thu, 15 Aug 2019 12:23:32 -0600 Subject: [PATCH 3/9] #[22] refactor post_raw --- src/document/advanced.rs | 5 +- src/internal/document_api/mod.rs | 49 ++++++++++++-- src/internal/document_api/requests.rs | 2 +- src/internal/group_api/mod.rs | 1 - src/internal/rest.rs | 93 +++++++++------------------ tests/document_ops.rs | 4 +- 6 files changed, 77 insertions(+), 77 deletions(-) diff --git a/src/document/advanced.rs b/src/document/advanced.rs index a2405c10..ef46eaef 100644 --- a/src/document/advanced.rs +++ b/src/document/advanced.rs @@ -28,6 +28,7 @@ pub trait DocumentAdvancedOps { encrypt_opts: &DocumentEncryptOpts, ) -> Result; + /// (Advanced) TODO fn document_decrypt_unmanaged( &self, encrypted_data: &[u8], @@ -80,10 +81,6 @@ impl DocumentAdvancedOps for crate::IronOxide { encrypted_data: &[u8], encrypted_deks: &[u8], ) -> Result { - let deks: crate::proto::transform::EncryptedDeks = - protobuf::parse_from_bytes(&encrypted_deks)?; - dbg!(&deks); - let mut rt = Runtime::new().unwrap(); rt.block_on(internal::document_api::decrypt_document_unmanaged( diff --git a/src/internal/document_api/mod.rs b/src/internal/document_api/mod.rs index 3cf85bc8..23c51c11 100644 --- a/src/internal/document_api/mod.rs +++ b/src/internal/document_api/mod.rs @@ -809,7 +809,7 @@ impl TryFrom<&EncryptedDek> for EncryptedDekP { Ok(proto_edek) } } - +//TODO #[derive(Debug, Clone)] struct RecryptionResult { edeks: Vec, @@ -826,19 +826,19 @@ impl RecryptionResult { } } +//TODO struct EncryptedDocUnmanaged { header: DocumentHeader, value: RecryptionResult, } -// TODO is this a reasonable name? impl EncryptedDocUnmanaged { fn edoc_bytes(&self) -> Vec { [ &self.header.pack().0[..], &self.value.encrypted_data.bytes(), ] - .concat() //TODO + .concat() } // fn edek_bytes(&self) -> &[u8] { @@ -1527,10 +1527,49 @@ mod tests { } } - //TODO test that shows edoc header is properly generated + #[test] + pub fn unmanged_edoc_header_properly_encoded() -> Result<(), IronOxideErr> { + use crate::proto::transform::UserOrGroup as UserOrGroupP; + use crate::proto::transform::UserOrGroup_oneof_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.clone().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(), + aes_value, + &doc_id, + with_keys, + )? + .into_edoc_unmanaged(DocumentHeader::new(doc_id.clone(), seg_id)); + + 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 compare_grants_to_edek_encoded() -> Result<(), IronOxideErr> { + pub fn unmanaged_edec_compare_grants() -> Result<(), IronOxideErr> { use crate::proto::transform::UserOrGroup as UserOrGroupP; use crate::proto::transform::UserOrGroup_oneof_UserOrGroupId as UserOrGroupIdP; use recrypt::prelude::*; diff --git a/src/internal/document_api/requests.rs b/src/internal/document_api/requests.rs index 5112b5b2..bc637e54 100644 --- a/src/internal/document_api/requests.rs +++ b/src/internal/document_api/requests.rs @@ -35,7 +35,7 @@ pub enum UserOrGroupWithKey { User { id: String, // optional because the resp on document create does not return a public key - master_public_key: Option, //TODO can I replace the None usages of this with UserOrGroup + master_public_key: Option, }, #[serde(rename_all = "camelCase")] Group { diff --git a/src/internal/group_api/mod.rs b/src/internal/group_api/mod.rs index 4f4412f4..061cb6b7 100644 --- a/src/internal/group_api/mod.rs +++ b/src/internal/group_api/mod.rs @@ -570,7 +570,6 @@ fn generate_transform_for_keys( Ok(recrypt_transform_key) => { Either::Right((user_id, public_key, TransformKey(recrypt_transform_key))) } - //TODO: Logging the error might be nice? Err(_) => Either::Left(GroupAccessEditErr::new( user_id, "Transform key could not be generated.".to_string(), diff --git a/src/internal/rest.rs b/src/internal/rest.rs index 107a127b..f2c91098 100644 --- a/src/internal/rest.rs +++ b/src/internal/rest.rs @@ -14,6 +14,7 @@ use serde::{de::DeserializeOwned, Serialize}; use crate::internal::{ user_api::UserId, DeviceSigningKeyPair, IronOxideErr, Jwt, RequestErrorCode, OUR_REQUEST, }; +use reqwest::r#async::RequestBuilder; lazy_static! { static ref DEFAULT_HEADERS: HeaderMap = { @@ -21,6 +22,11 @@ lazy_static! { headers.append("Content-Type", "application/json".parse().unwrap()); headers }; + static ref RAW_BYTES_HEADERS: HeaderMap = { + let mut headers: HeaderMap = Default::default(); + headers.append("Content-Type", "application/x-protobuf".parse().unwrap()); + headers + }; } #[derive(Debug, Serialize, Deserialize, PartialEq)] @@ -140,15 +146,21 @@ impl<'a> IronCoreRequest<'a> { error_code: RequestErrorCode, auth: &Authorization, ) -> impl Future { - self.request_raw::<_, String, _>( - relative_url, + let client = RClient::new(); + let mut builder = client.request( Method::POST, - Some(body), - None, - error_code, - auth.to_header(), - move |server_resp| IronCoreRequest::deserialize_body(server_resp, error_code), - ) + format!("{}{}", self.base_url, relative_url).as_str(), + ); + + //We want to add the body as raw bytes + builder = builder.body(body.to_vec()); + + let req = builder + .headers(RAW_BYTES_HEADERS.clone()) + .headers(auth.to_header()); + IronCoreRequest::send_req(req, error_code.clone(), move |server_resp| { + IronCoreRequest::deserialize_body(server_resp, error_code.clone()) + }) } ///PUT body to the resource at relative_url using auth for authorization. @@ -257,18 +269,20 @@ impl<'a> IronCoreRequest<'a> { ) } - // TODO combine with `request` - pub fn request_raw( + ///Make a request to the url using the specified method. DEFAULT_HEADERS will be used as well as whatever headers are passed + /// in. The response will be sent to `resp_handler` so the caller can make the received bytes however they want. + pub fn request( &self, relative_url: &str, method: Method, - maybe_body: Option<&[u8]>, + maybe_body: Option<&A>, maybe_query_params: Option<&Q>, error_code: RequestErrorCode, headers: HeaderMap, resp_handler: F, ) -> impl Future where + A: Serialize, B: DeserializeOwned, Q: Serialize + ?Sized, F: FnOnce(&Chunk) -> Result, @@ -286,70 +300,21 @@ impl<'a> IronCoreRequest<'a> { //We want to add the body as json if it was specified builder = maybe_body .iter() - .fold(builder, |build, body| build.body(body.to_vec())); + .fold(builder, |build, body| build.json(body)); let req = builder.headers(DEFAULT_HEADERS.clone()).headers(headers); - req.send() - //Parse the body content into bytes - .and_then(|res| { - let status_code = res.status(); - res.into_body() - .concat2() - .map(move |body| (status_code, body)) - }) - //Now make the error type into the IronOxideErr and run the resp_handler which was passed to us. - .then(move |resp| { - //Map the generic error from reqwest to our error type. - let (status, server_resp) = resp.map_err(|err| { - IronCoreRequest::create_request_err(err.to_string(), error_code, err.status()) - })?; - //If the status code is a 5xx, return a fixed error code message - if status.is_server_error() || status.is_client_error() { - Err(IronCoreRequest::request_failure_to_error( - status, - error_code, - &server_resp, - )) - } else { - resp_handler(&server_resp) - } - }) + IronCoreRequest::send_req(req, error_code, resp_handler) } - ///Make a request to the url using the specified method. DEFAULT_HEADERS will be used as well as whatever headers are passed - /// in. The response will be sent to `resp_handler` so the caller can make the received bytes however they want. - pub fn request( - &self, - relative_url: &str, - method: Method, - maybe_body: Option<&A>, - maybe_query_params: Option<&Q>, + fn send_req( + req: RequestBuilder, error_code: RequestErrorCode, - headers: HeaderMap, resp_handler: F, ) -> impl Future where - A: Serialize, B: DeserializeOwned, - Q: Serialize + ?Sized, F: FnOnce(&Chunk) -> Result, { - let client = RClient::new(); - let mut builder = client.request( - method, - format!("{}{}", self.base_url, relative_url).as_str(), - ); - // add query params, if any - builder = maybe_query_params - .iter() - .fold(builder, |build, q| build.query(q)); - - //We want to add the body as json if it was specified - builder = maybe_body - .iter() - .fold(builder, |build, body| build.json(body)); - - let req = builder.headers(DEFAULT_HEADERS.clone()).headers(headers); req.send() //Parse the body content into bytes .and_then(|res| { diff --git a/tests/document_ops.rs b/tests/document_ops.rs index 69a0107d..a8c17684 100644 --- a/tests/document_ops.rs +++ b/tests/document_ops.rs @@ -534,7 +534,7 @@ fn doc_create_and_adjust_name() { } #[test] -fn doc_decrypt_roundtrip() { +fn doc_encrypt_decrypt_roundtrip() { let sdk = init_sdk(); let doc = [43u8; 64]; let encrypted_doc = sdk.document_encrypt(&doc, &Default::default()).unwrap(); @@ -549,7 +549,7 @@ fn doc_decrypt_roundtrip() { } #[test] -fn doc_decrypt_unmanaged_roundtrip() -> Result<(), IronOxideErr> { +fn doc_encrypt_decrypt_unmanaged_roundtrip() -> Result<(), IronOxideErr> { let sdk = init_sdk(); let encrypt_opts = Default::default(); let doc = [0u8; 42]; From 51c1233ae466424ce6b91c544ce959353418b033 Mon Sep 17 00:00:00 2001 From: Clint Frederickson Date: Thu, 15 Aug 2019 15:42:48 -0600 Subject: [PATCH 4/9] #[22] moved edek proto encoding --- src/internal/document_api/mod.rs | 80 ++++++++++++++++---------------- src/internal/rest.rs | 3 +- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/internal/document_api/mod.rs b/src/internal/document_api/mod.rs index 23c51c11..5c31d71f 100644 --- a/src/internal/document_api/mod.rs +++ b/src/internal/document_api/mod.rs @@ -282,28 +282,13 @@ pub struct DocumentEncryptUnmanagedResult { impl DocumentEncryptUnmanagedResult { fn new( - doc_id: &DocumentId, - segment_id: usize, - encryption_result: EncryptedDocUnmanaged, + encryption_result: EncryptedDoc, access_errs: Vec, ) -> Result { - let proto_edek_vec_results: Result, _> = encryption_result - .value - .edeks - .iter() - .map(|edek| edek.try_into()) - .collect(); - let proto_edek_vec = proto_edek_vec_results?; - - let mut proto_edeks = EncryptedDeksP::default(); - proto_edeks.edeks = RepeatedField::from_vec(proto_edek_vec); - proto_edeks.documentId = doc_id.id().as_str().into(); - proto_edeks.segmentId = segment_id as i32; // okay since the ironcore-ws defines this to be an i32 - - let edek_bytes = proto_edeks.write_to_bytes()?; + let edek_bytes = encryption_result.edek_bytes()?; Ok(DocumentEncryptUnmanagedResult { - id: doc_id.clone(), + id: encryption_result.header.document_id.clone(), access_errs, encrypted_data: encryption_result.edoc_bytes().to_vec(), encrypted_deks: edek_bytes, @@ -656,17 +641,12 @@ where &doc_id, grants, )?; - let enc_result = EncryptedDocUnmanaged { + let enc_result = EncryptedDoc { header: DocumentHeader::new(doc_id.clone(), auth.segment_id), value: r, }; let access_errs = [&key_errs[..], &enc_result.value.encryption_errs[..]].concat(); - DocumentEncryptUnmanagedResult::new( - &doc_id, - auth.segment_id, - enc_result, - access_errs, - )? + DocumentEncryptUnmanagedResult::new(enc_result, access_errs)? }) }) } @@ -809,7 +789,9 @@ impl TryFrom<&EncryptedDek> for EncryptedDekP { Ok(proto_edek) } } -//TODO +/// Result of recrypt encryption. Contains the encrypted DEKs and the encrypted (user) data. +/// `RecryptionResult` is an intermediate value as it cannot be serialized to bytes directly. +/// To serialize to bytes, first construct an `EncryptedDoc` #[derive(Debug, Clone)] struct RecryptionResult { edeks: Vec, @@ -818,21 +800,22 @@ struct RecryptionResult { } impl RecryptionResult { - fn into_edoc_unmanaged(self, header: DocumentHeader) -> EncryptedDocUnmanaged { - EncryptedDocUnmanaged { + fn into_edoc_unmanaged(self, header: DocumentHeader) -> EncryptedDoc { + EncryptedDoc { value: self, header, } } } -//TODO -struct EncryptedDocUnmanaged { +/// An ironoxide encrypted document +struct EncryptedDoc { header: DocumentHeader, value: RecryptionResult, } -impl EncryptedDocUnmanaged { +impl EncryptedDoc { + /// bytes of the encrypted data with the edoc header prepended fn edoc_bytes(&self) -> Vec { [ &self.header.pack().0[..], @@ -841,15 +824,34 @@ impl EncryptedDocUnmanaged { .concat() } - // fn edek_bytes(&self) -> &[u8] { - // &self.value.edeks //TODO I could move the proto serialization here... - // } + /// vector of EDEKs + fn edek_vec(&self) -> Vec { + self.value.edeks.clone() + } + + fn edek_bytes(&self) -> Result, IronOxideErr> { + let proto_edek_vec_results: Result, _> = self + .value + .edeks + .iter() + .map(|edek| edek.try_into()) + .collect(); + let proto_edek_vec = proto_edek_vec_results?; + + let mut proto_edeks = EncryptedDeksP::default(); + proto_edeks.edeks = RepeatedField::from_vec(proto_edek_vec); + proto_edeks.documentId = self.header.document_id.id().as_str().into(); + proto_edeks.segmentId = self.header.segment_id as i32; // okay since the ironcore-ws defines this to be an i32 + + let edek_bytes = proto_edeks.write_to_bytes()?; + Ok(edek_bytes) + } } /// Creates an encrypted document entry in the IronCore webservice. fn document_create<'a>( auth: &'a RequestAuth, - encryption_result: EncryptedDocUnmanaged, + edoc: EncryptedDoc, doc_id: DocumentId, doc_name: &Option, accum_errs: Vec, @@ -858,16 +860,16 @@ fn document_create<'a>( auth, doc_id.clone(), doc_name.clone(), - encryption_result.value.edeks.to_vec(), + edoc.edek_vec(), ) .map(move |api_resp| DocumentEncryptResult { id: api_resp.id, name: api_resp.name, created: api_resp.created, updated: api_resp.updated, - encrypted_data: encryption_result.edoc_bytes().to_vec(), + encrypted_data: edoc.edoc_bytes().to_vec(), grants: api_resp.shared_with.iter().map(|sw| sw.into()).collect(), - access_errs: [accum_errs, encryption_result.value.encryption_errs].concat(), + access_errs: [accum_errs, edoc.value.encryption_errs].concat(), }) } @@ -1601,7 +1603,7 @@ mod tests { // create an unmanged result, which does the proto serialization let doc_encrypt_unmanaged_result = - DocumentEncryptUnmanagedResult::new(&doc_id, 1, encryption_result, vec![])?; + DocumentEncryptUnmanagedResult::new(encryption_result, vec![])?; // then deserialize and extract the user/groups from the edeks let proto_edeks: EncryptedDeksP = diff --git a/src/internal/rest.rs b/src/internal/rest.rs index f2c91098..70b1d473 100644 --- a/src/internal/rest.rs +++ b/src/internal/rest.rs @@ -24,7 +24,8 @@ lazy_static! { }; static ref RAW_BYTES_HEADERS: HeaderMap = { let mut headers: HeaderMap = Default::default(); - headers.append("Content-Type", "application/x-protobuf".parse().unwrap()); + // this works with cloudflare. tried `application/x-protobuf` and `application/protobuf` and both were flagged as potentially malicious + headers.append("Content-Type", "application/octet-stream".parse().unwrap()); headers }; } From 70ccd9f32985e10f3401952134659201c190330f Mon Sep 17 00:00:00 2001 From: Clint Frederickson Date: Thu, 15 Aug 2019 15:59:45 -0600 Subject: [PATCH 5/9] #[22] decode proto as fail-fast validation --- src/internal/document_api/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/internal/document_api/mod.rs b/src/internal/document_api/mod.rs index 5c31d71f..d67aaa73 100644 --- a/src/internal/document_api/mod.rs +++ b/src/internal/document_api/mod.rs @@ -953,10 +953,17 @@ pub fn decrypt_document_unmanaged<'a, CR: rand::CryptoRng + rand::RngCore>( encrypted_doc: &'a [u8], encrypted_deks: &'a [u8], ) -> impl Future + 'a { - requests::edek_transform::edek_transform(&auth, encrypted_deks) //TODO proto decode here? - .join(parse_document_parts(encrypted_doc).into_future()) + // attempt to parse the proto as fail-fast validation. If it fails decrypt will fail + protobuf::parse_from_bytes::(encrypted_deks) + .map_err(IronOxideErr::from) + .into_future() + .join3( + requests::edek_transform::edek_transform(&auth, encrypted_deks), + parse_document_parts(encrypted_doc).into_future(), + ) .and_then( move |( + _, requests::edek_transform::EdekTransformResponse { user_or_group, encrypted_symmetric_key, From 15337964b9c67e03fd30d3e150707302d73f6568 Mon Sep 17 00:00:00 2001 From: Clint Frederickson Date: Thu, 15 Aug 2019 16:08:55 -0600 Subject: [PATCH 6/9] #[22] additional documentation --- src/document/advanced.rs | 10 +++++++++- src/internal/document_api/mod.rs | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/document/advanced.rs b/src/document/advanced.rs index ef46eaef..d75e4e5b 100644 --- a/src/document/advanced.rs +++ b/src/document/advanced.rs @@ -28,7 +28,15 @@ pub trait DocumentAdvancedOps { encrypt_opts: &DocumentEncryptOpts, ) -> Result; - /// (Advanced) TODO + /// (Advanced) Decrypt a document not managed by the ironcore service. Both the encrypted + /// data and the encrypted deks need to be provided. + /// + /// The webservice is still needed to transform a chosen encrypted dek so it can be decrypted + /// by the caller's private key. + /// + /// # Arguments + /// - `encrypted_data` - Encrypted document + /// - `encrypted_deks` - Associated encrypted DEKs for the `encrypted_data` fn document_decrypt_unmanaged( &self, encrypted_data: &[u8], diff --git a/src/internal/document_api/mod.rs b/src/internal/document_api/mod.rs index d67aaa73..04883bd9 100644 --- a/src/internal/document_api/mod.rs +++ b/src/internal/document_api/mod.rs @@ -1003,6 +1003,7 @@ impl DocumentDecryptUnmanagedResult { &self.access_via } + /// plaintext user data pub fn decrypted_data(&self) -> &[u8] { &self.decrypted_data.0 } From 5f30d0c9f05f4cfef4190776241fbac0062861c6 Mon Sep 17 00:00:00 2001 From: Clint Frederickson Date: Fri, 16 Aug 2019 13:59:44 -0600 Subject: [PATCH 7/9] #[22] better validation, more docs, more tests --- src/internal/document_api/mod.rs | 114 ++++++++++++++++++++++++++++--- src/internal/mod.rs | 9 ++- tests/document_ops.rs | 33 ++++++++- 3 files changed, 143 insertions(+), 13 deletions(-) diff --git a/src/internal/document_api/mod.rs b/src/internal/document_api/mod.rs index 04883bd9..2f221866 100644 --- a/src/internal/document_api/mod.rs +++ b/src/internal/document_api/mod.rs @@ -532,7 +532,7 @@ pub fn encrypt_document< let encryption_errs = r.encryption_errs.clone(); document_create( &auth, - r.into_edoc_unmanaged(DocumentHeader::new(doc_id.clone(), auth.segment_id)), + r.into_edoc(DocumentHeader::new(doc_id.clone(), auth.segment_id)), doc_id, &document_name, [key_errs, encryption_errs].concat(), @@ -800,7 +800,7 @@ struct RecryptionResult { } impl RecryptionResult { - fn into_edoc_unmanaged(self, header: DocumentHeader) -> EncryptedDoc { + fn into_edoc(self, header: DocumentHeader) -> EncryptedDoc { EncryptedDoc { value: self, header, @@ -809,6 +809,7 @@ impl RecryptionResult { } /// An ironoxide encrypted document +#[derive(Debug)] struct EncryptedDoc { header: DocumentHeader, value: RecryptionResult, @@ -824,11 +825,12 @@ impl EncryptedDoc { .concat() } - /// vector of EDEKs + /// associated EncryptedDeks for this EncryptedDoc fn edek_vec(&self) -> Vec { self.value.edeks.clone() } + /// binary blob for associated edeks, or error if encoding the edeks failed fn edek_bytes(&self) -> Result, IronOxideErr> { let proto_edek_vec_results: Result, _> = self .value @@ -957,18 +959,43 @@ pub fn decrypt_document_unmanaged<'a, CR: rand::CryptoRng + rand::RngCore>( protobuf::parse_from_bytes::(encrypted_deks) .map_err(IronOxideErr::from) .into_future() - .join3( - requests::edek_transform::edek_transform(&auth, encrypted_deks), - parse_document_parts(encrypted_doc).into_future(), + .join(parse_document_parts(encrypted_doc)) + .and_then( + |( + proto_edeks, + ( + DocumentHeader { + document_id, + segment_id, + }, + aes_encrypted_value, + ), + )| { + if document_id.id() == proto_edeks.get_documentId() + && segment_id as i32 == proto_edeks.get_segmentId() + { + Ok((document_id, aes_encrypted_value)) + } else { + Err(IronOxideErr::UnmanagedDecryptionError( + proto_edeks.get_documentId().into(), + proto_edeks.get_segmentId(), + document_id.0, + segment_id as i32, + )) + } + }, ) + .join(requests::edek_transform::edek_transform( + &auth, + encrypted_deks, + )) .and_then( move |( - _, + (document_id, mut enc_data), requests::edek_transform::EdekTransformResponse { user_or_group, encrypted_symmetric_key, }, - (DocumentHeader { document_id, .. }, mut enc_data), )| { let (_, sym_key) = transform::decrypt_plaintext( &recrypt, @@ -986,7 +1013,11 @@ pub fn decrypt_document_unmanaged<'a, CR: rand::CryptoRng + rand::RngCore>( ) } +#[derive(Clone, PartialEq, Debug)] struct DecryptedData(Vec); + +/// Result of successful unmanaged decryption +#[derive(Clone, PartialEq, Debug)] pub struct DocumentDecryptUnmanagedResult { id: DocumentId, access_via: UserOrGroup, @@ -1231,7 +1262,9 @@ mod tests { use galvanic_assert::matchers::{collection::*, *}; use super::*; + use crate::internal::rest::IronCoreRequest; use std::borrow::Borrow; + use tokio::runtime::current_thread::Runtime; #[test] fn document_id_validate_good() { @@ -1538,7 +1571,7 @@ mod tests { } #[test] - pub fn unmanged_edoc_header_properly_encoded() -> Result<(), IronOxideErr> { + pub fn unmanaged_edoc_header_properly_encoded() -> Result<(), IronOxideErr> { use crate::proto::transform::UserOrGroup as UserOrGroupP; use crate::proto::transform::UserOrGroup_oneof_UserOrGroupId as UserOrGroupIdP; use recrypt::prelude::*; @@ -1566,7 +1599,7 @@ mod tests { &doc_id, with_keys, )? - .into_edoc_unmanaged(DocumentHeader::new(doc_id.clone(), seg_id)); + .into_edoc(DocumentHeader::new(doc_id.clone(), seg_id)); assert_eq!(&encryption_result.header.document_id, &doc_id); assert_eq!(&encryption_result.header.segment_id, &seg_id); @@ -1607,7 +1640,7 @@ mod tests { &doc_id, with_keys, )? - .into_edoc_unmanaged(DocumentHeader::new(doc_id.clone(), seg_id)); + .into_edoc(DocumentHeader::new(doc_id.clone(), seg_id)); // create an unmanged result, which does the proto serialization let doc_encrypt_unmanaged_result = @@ -1665,4 +1698,63 @@ mod tests { Ok(()) } + + #[test] + pub fn unmanaged_decrypt_edek_edoc_no_match() -> Result<(), IronOxideErr> { + use crate::proto::transform::UserOrGroup as UserOrGroupP; + use crate::proto::transform::UserOrGroup_oneof_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 (priv_key, pubk) = recr.generate_key_pair()?; + let with_keys = vec![ + WithKey::new(user, pubk.clone().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(), + aes_value, + &doc_id, + with_keys, + )?; + + let edoc1 = encryption_result + .clone() + .into_edoc(DocumentHeader::new(doc_id, seg_id)); + let edoc2 = + encryption_result.into_edoc(DocumentHeader::new(DocumentId("other_docid".into()), 99)); + + let auth = RequestAuth { + account_id: uid, + segment_id: seg_id, + signing_keys: signingkeys, + request: IronCoreRequest::default(), + }; + + let decrypt_priv = priv_key.into(); + let decrypt_edoc = edoc1.edoc_bytes(); + let decrypt_edek = edoc2.edek_bytes()?; + + let decrypt_result_f = + decrypt_document_unmanaged(&auth, &recr, &decrypt_priv, &decrypt_edoc, &decrypt_edek); + + let result = Runtime::new().unwrap().block_on(decrypt_result_f); + assert_that!(&result, is_variant!(Result::Err)); + assert_that!( + &result.unwrap_err(), + is_variant!(IronOxideErr::UnmanagedDecryptionError) + ); + Ok(()) + } } diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 6cc05d06..b1516739 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -82,7 +82,7 @@ quick_error! { display("Provided document is not long enough to be an encrypted document.") } InvalidRecryptEncryptedValue(msg: String) { - display("Got an unexpcted Recrypt EncryptedValue: '{}'", msg) + display("Got an unexpected Recrypt EncryptedValue: '{}'", msg) } RecryptError(msg: String) { display("Recrypt operation failed with error '{}'", msg) @@ -115,6 +115,13 @@ quick_error! { 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) + } } } diff --git a/tests/document_ops.rs b/tests/document_ops.rs index a8c17684..d5f742eb 100644 --- a/tests/document_ops.rs +++ b/tests/document_ops.rs @@ -536,6 +536,7 @@ fn doc_create_and_adjust_name() { #[test] fn doc_encrypt_decrypt_roundtrip() { let sdk = init_sdk(); + let doc = [43u8; 64]; let encrypted_doc = sdk.document_encrypt(&doc, &Default::default()).unwrap(); @@ -548,6 +549,37 @@ fn doc_encrypt_decrypt_roundtrip() { assert_eq!(doc.to_vec(), decrypted.decrypted_data()); } +#[test] +fn doc_decrypt_unmanaged_no_access() { + use std::borrow::Borrow; + + let sdk = init_sdk(); + + let user2 = create_second_user(); + + let doc = [43u8; 64]; + let encrypted_doc = sdk + .document_encrypt_unmanaged( + &doc, + &DocumentEncryptOpts::with_explicit_grants( + None, + None, + false, + vec![user2.account_id().borrow().into()], + ), + ) + .unwrap(); + + let decrypt_err = sdk + .document_decrypt_unmanaged( + &encrypted_doc.encrypted_data(), + &encrypted_doc.encrypted_deks(), + ) + .unwrap_err(); + + assert_that!(&decrypt_err, is_variant!(IronOxideErr::RequestServerErrors)); +} + #[test] fn doc_encrypt_decrypt_unmanaged_roundtrip() -> Result<(), IronOxideErr> { let sdk = init_sdk(); @@ -621,7 +653,6 @@ fn doc_grant_access() { }, ], ); - // dbg!(&grant_result); assert!(grant_result.is_ok()); let grants = grant_result.unwrap(); assert_eq!(3, grants.succeeded().len()); From 2f6fcb238fe19a2eb59a8f36d962608ccaf13f13 Mon Sep 17 00:00:00 2001 From: Clint Frederickson Date: Sun, 18 Aug 2019 16:33:03 -0600 Subject: [PATCH 8/9] #[22] corrected two spelling errors --- src/internal/document_api/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/internal/document_api/mod.rs b/src/internal/document_api/mod.rs index 2f221866..82001666 100644 --- a/src/internal/document_api/mod.rs +++ b/src/internal/document_api/mod.rs @@ -917,7 +917,7 @@ pub fn document_update_bytes< } /// Decrypt the provided document with the provided device private key. Return metadata about the document -/// that was decrypted along with it's decrypted bytes. +/// that was decrypted along with its decrypted bytes. pub fn decrypt_document<'a, CR: rand::CryptoRng + rand::RngCore>( auth: &'a RequestAuth, recrypt: &'a Recrypt>, @@ -1612,7 +1612,7 @@ mod tests { } #[test] - pub fn unmanaged_edec_compare_grants() -> Result<(), IronOxideErr> { + pub fn unmanaged_edoc_compare_grants() -> Result<(), IronOxideErr> { use crate::proto::transform::UserOrGroup as UserOrGroupP; use crate::proto::transform::UserOrGroup_oneof_UserOrGroupId as UserOrGroupIdP; use recrypt::prelude::*; From 0b85ada7bc3ff7b997a5a0c4f61e041d3e010da1 Mon Sep 17 00:00:00 2001 From: Clint Frederickson Date: Mon, 19 Aug 2019 11:46:31 -0600 Subject: [PATCH 9/9] #[22] moved a couple of types to more appropriate locations --- src/internal/document_api/mod.rs | 67 +++++++++++++------------------- src/internal/mod.rs | 7 ++++ 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/src/internal/document_api/mod.rs b/src/internal/document_api/mod.rs index 82001666..2fab6e25 100644 --- a/src/internal/document_api/mod.rs +++ b/src/internal/document_api/mod.rs @@ -21,7 +21,7 @@ use chrono::{DateTime, Utc}; use futures::prelude::*; use hex::encode; use itertools::{Either, Itertools}; -use protobuf::{Message, ProtobufError, RepeatedField}; +use protobuf::{Message, RepeatedField}; use rand::{self, CryptoRng, RngCore}; use recrypt::{api::Plaintext, prelude::*}; pub use requests::policy_get::PolicyResult; @@ -431,6 +431,32 @@ impl DocumentAccessResult { &self.failed } } +#[derive(Clone, PartialEq, Debug)] +struct DecryptedData(Vec); + +/// Result of successful unmanaged decryption +#[derive(Clone, PartialEq, Debug)] +pub struct DocumentDecryptUnmanagedResult { + id: DocumentId, + access_via: UserOrGroup, + decrypted_data: DecryptedData, +} + +impl DocumentDecryptUnmanagedResult { + pub fn id(&self) -> &DocumentId { + &self.id + } + + /// user or group that allowed the caller to decrypt the data + pub fn access_via(&self) -> &UserOrGroup { + &self.access_via + } + + /// plaintext user data + pub fn decrypted_data(&self) -> &[u8] { + &self.decrypted_data.0 + } +} /// Either a user or a group. Allows for containing both. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -481,7 +507,7 @@ pub fn document_get_metadata( requests::document_get::document_get_request(auth, id).map(DocumentMetadataResult) } -// Attempt to parse the provided encrypted document header and extract out the ID if present +/// Attempt to parse the provided encrypted document header and extract out the ID if present pub fn get_id_from_bytes(encrypted_document: &[u8]) -> Result { parse_document_parts(&encrypted_document).map(|header| header.0.document_id) } @@ -651,12 +677,6 @@ where }) } -impl From for IronOxideErr { - fn from(e: ProtobufError) -> Self { - internal::IronOxideErr::ProtobufSerdeError(e) - } -} - /// Remove any duplicates in the grant list. Uses ids (not keys) for comparison. fn dedupe_grants(grants: &[WithKey]) -> Vec> { grants @@ -1013,33 +1033,6 @@ pub fn decrypt_document_unmanaged<'a, CR: rand::CryptoRng + rand::RngCore>( ) } -#[derive(Clone, PartialEq, Debug)] -struct DecryptedData(Vec); - -/// Result of successful unmanaged decryption -#[derive(Clone, PartialEq, Debug)] -pub struct DocumentDecryptUnmanagedResult { - id: DocumentId, - access_via: UserOrGroup, - decrypted_data: DecryptedData, -} - -impl DocumentDecryptUnmanagedResult { - pub fn id(&self) -> &DocumentId { - &self.id - } - - /// user or group that allowed the caller to decrypt the data - pub fn access_via(&self) -> &UserOrGroup { - &self.access_via - } - - /// plaintext user data - pub fn decrypted_data(&self) -> &[u8] { - &self.decrypted_data.0 - } -} - // Update a documents name. Value can be updated to either a new name with a Some or the name value can be cleared out // by providing a None. pub fn update_document_name<'a>( @@ -1572,8 +1565,6 @@ mod tests { #[test] pub fn unmanaged_edoc_header_properly_encoded() -> Result<(), IronOxideErr> { - use crate::proto::transform::UserOrGroup as UserOrGroupP; - use crate::proto::transform::UserOrGroup_oneof_UserOrGroupId as UserOrGroupIdP; use recrypt::prelude::*; let recr = recrypt::api::Recrypt::new(); @@ -1701,8 +1692,6 @@ mod tests { #[test] pub fn unmanaged_decrypt_edek_edoc_no_match() -> Result<(), IronOxideErr> { - use crate::proto::transform::UserOrGroup as UserOrGroupP; - use crate::proto::transform::UserOrGroup_oneof_UserOrGroupId as UserOrGroupIdP; use recrypt::prelude::*; let recr = recrypt::api::Recrypt::new(); diff --git a/src/internal/mod.rs b/src/internal/mod.rs index b1516739..f77d3f96 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -9,6 +9,7 @@ use crate::internal::{ use chrono::{DateTime, Utc}; use log::error; use protobuf; +use protobuf::ProtobufError; use recrypt::api::{ Hashable, PrivateKey as RecryptPrivateKey, PublicKey as RecryptPublicKey, RecryptErr, SigningKeypair as RecryptSigningKeypair, @@ -138,6 +139,12 @@ impl From for IronOxideErr { } } +impl From for IronOxideErr { + fn from(e: ProtobufError) -> Self { + IronOxideErr::ProtobufSerdeError(e) + } +} + impl From for IronOxideErr { fn from(_: recrypt::nonemptyvec::NonEmptyVecError) -> Self { IronOxideErr::MissingTransformBlocks