diff --git a/services/reports/Cargo.lock b/services/reports/Cargo.lock --- a/services/reports/Cargo.lock +++ b/services/reports/Cargo.lock @@ -264,6 +264,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "bytes", + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.3" @@ -846,6 +882,16 @@ "winapi", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.4.0" @@ -901,6 +947,8 @@ "actix-multipart", "actix-web", "actix-web-httpauth", + "aead", + "aes-gcm", "anyhow", "aws-config", "aws-sdk-dynamodb", @@ -979,9 +1027,19 @@ checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.20.3" @@ -1220,6 +1278,16 @@ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.28.0" @@ -1413,6 +1481,15 @@ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1667,6 +1744,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.56" @@ -1802,6 +1885,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "postmark" version = "0.8.1" @@ -2665,6 +2760,16 @@ "tinyvec", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" diff --git a/services/reports/Cargo.toml b/services/reports/Cargo.toml --- a/services/reports/Cargo.toml +++ b/services/reports/Cargo.toml @@ -16,6 +16,7 @@ comm-services-lib = { path = "../comm-services-lib", features = [ "blob-client", "http", + "crypto", ] } derive_more = "0.99" hex = "0.4" 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::EncryptionKey::new(); + 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 })