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<String>,
+  pub encryption_key: Option<EncryptionKey>,
 }
 
 /// 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<Option<ReportOutput>> {
+    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<String>,
-) -> Result<ProcessedReport, serde_json::Error> {
+) -> Result<ProcessedReport, ReportsServiceError> {
+  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 })