diff --git a/services/reports/src/config.rs b/services/reports/src/config.rs --- a/services/reports/src/config.rs +++ b/services/reports/src/config.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use clap::Parser; +use clap::{ArgAction, Parser}; use comm_services_lib::blob::client::Url; use once_cell::sync::Lazy; use tracing::{info, warn}; @@ -26,9 +26,16 @@ /// HTTP server listening port #[arg(long, default_value_t = 50056)] pub http_port: u16, + #[arg(env = ENV_BLOB_SERVICE_URL)] #[arg(long, default_value = "http://localhost:50053")] pub blob_service_url: Url, + + /// Should reports be encrypted? Note that this flag disables encryption + /// which is enabled by default. + #[arg(long = "no-encrypt", action = ArgAction::SetFalse)] + pub encrypt_reports: bool, + /// AWS Localstack service URL #[arg(env = ENV_LOCALSTACK_ENDPOINT)] #[arg(long)] @@ -72,6 +79,10 @@ } } + if !cfg.encrypt_reports { + warn!("Encryption disabled. Reports will be stored in plaintext!"); + } + Ok(cfg) } diff --git a/services/reports/src/database/item.rs b/services/reports/src/database/item.rs --- a/services/reports/src/database/item.rs +++ b/services/reports/src/database/item.rs @@ -7,6 +7,7 @@ }, bytes::Bytes, constants::DDB_ITEM_SIZE_LIMIT, + crypto::aes256::EncryptionKey, database::{ self, AttributeExtractor, AttributeMap, DBItemError, TryFromAttribute, }, @@ -35,7 +36,7 @@ #[serde(skip_serializing)] pub content: ReportContent, #[serde(skip_serializing)] - pub encryption_key: Option, + pub encryption_key: Option, } /// contains some redundancy as not all keys are always present @@ -73,7 +74,7 @@ attrs.insert(content_attr_name, content_attr); if let Some(key) = self.encryption_key { - attrs.insert(ATTR_ENCRYPTION_KEY.to_string(), AttributeValue::S(key)); + attrs.insert(ATTR_ENCRYPTION_KEY.to_string(), key.into()); } attrs } @@ -111,7 +112,7 @@ } }; if let Some(key) = self.encryption_key.as_ref() { - size += key.as_bytes().len(); + size += key.as_ref().len(); } size } @@ -130,7 +131,7 @@ let content = ReportContent::parse_from_attrs(&mut row)?; let encryption_key = row .remove(ATTR_ENCRYPTION_KEY) - .map(|attr| String::try_from_attr(ATTR_ENCRYPTION_KEY, Some(attr))) + .map(|attr| EncryptionKey::try_from_attr(ATTR_ENCRYPTION_KEY, Some(attr))) .transpose()?; Ok(ReportItem { diff --git a/services/reports/src/http/mod.rs b/services/reports/src/http/mod.rs --- a/services/reports/src/http/mod.rs +++ b/services/reports/src/http/mod.rs @@ -86,7 +86,6 @@ ErrorInternalServerError("internal error") } }, - #[allow(unreachable_patterns)] err => { error!("Received an unexpected error: {0:?} - {0}", err); ErrorInternalServerError("server error") diff --git a/services/reports/src/service.rs b/services/reports/src/service.rs --- a/services/reports/src/service.rs +++ b/services/reports/src/service.rs @@ -3,6 +3,7 @@ use comm_services_lib::{ auth::UserIdentity, blob::client::{BlobServiceClient, BlobServiceError}, + crypto::aes256, database, }; use derive_more::{Display, Error, From}; @@ -11,9 +12,10 @@ future::{ready, Ready}, sync::Arc, }; -use tracing::error; +use tracing::{error, trace}; use crate::{ + config::CONFIG, database::{ client::{DatabaseClient, ReportsPage}, item::{ReportContent, ReportItem}, @@ -38,6 +40,9 @@ /// Returned when trying to perform an operation on an incompatible report type /// e.g. create a Redux Devtools import from a media mission report UnsupportedReportType, + /// Error during encryption or decryption + #[display(fmt = "Encryption error")] + EncryptionError, /// Unexpected error Unexpected, } @@ -88,8 +93,7 @@ let blob_client = self.blob_client.clone(); let user_id = self.requesting_user_id.clone(); tasks.spawn(async move { - let mut report = process_report(input, user_id) - .map_err(ReportsServiceError::SerdeError)?; + let mut report = process_report(input, user_id)?; report.db_item.ensure_size_constraints(&blob_client).await?; Ok(report) }); @@ -126,6 +130,7 @@ &self, report_id: ReportID, ) -> ServiceResult> { + use ReportsServiceError::{EncryptionError, SerdeError}; let Some(report_item) = self.db.get_report(&report_id).await? else { return Ok(None); }; @@ -135,12 +140,21 @@ platform, creation_time, content, + encryption_key, .. } = report_item; - let report_data = content.fetch_bytes(&self.blob_client).await?; - let report_json = serde_json::from_slice(report_data.as_slice()) - .map_err(ReportsServiceError::SerdeError)?; + let mut report_data = content.fetch_bytes(&self.blob_client).await?; + if let Some(key) = encryption_key { + trace!("Encryption key present. Decrypting report data"); + report_data = aes256::decrypt(&report_data, &key).map_err(|_| { + error!("Failed to decrypt report"); + EncryptionError + })?; + } + + let report_json = + serde_json::from_slice(report_data.as_slice()).map_err(SerdeError)?; let output = ReportOutput { id: report_id, @@ -222,7 +236,9 @@ fn process_report( input: ReportInput, user_id: Option, -) -> Result { +) -> Result { + use ReportsServiceError::*; + let id = ReportID::default(); let email = crate::email::prepare_email(&input, &id, user_id.as_deref()); @@ -235,11 +251,26 @@ // Add "platformDetails" back to report content. // It was deserialized into a separate field. - let platform_details_value = serde_json::to_value(&platform_details)?; + let platform_details_value = + serde_json::to_value(&platform_details).map_err(SerdeError)?; report_content.insert("platformDetails".to_string(), platform_details_value); // serialize report JSON to bytes - let content_bytes = serde_json::to_vec(&report_content)?; + let content_bytes = + serde_json::to_vec(&report_content).map_err(SerdeError)?; + + // possibly encrypt report + let (content, encryption_key) = if CONFIG.encrypt_reports { + trace!(?id, "Encrypting report"); + let key = aes256::generate_key(); + let data = aes256::encrypt(&content_bytes, &key).map_err(|_| { + error!("Failed to encrypt report"); + EncryptionError + })?; + (data, Some(key)) + } else { + (content_bytes, None) + }; let db_item = ReportItem { id: id.clone(), @@ -247,8 +278,8 @@ platform: platform_details.platform.clone(), report_type, creation_time: time.unwrap_or_else(Utc::now), - encryption_key: None, - content: ReportContent::Database(content_bytes), + encryption_key, + content: ReportContent::Database(content), }; Ok(ProcessedReport { id, db_item, email })