diff --git a/ipa-core/src/error.rs b/ipa-core/src/error.rs index 168827c8e..24fdd6851 100644 --- a/ipa-core/src/error.rs +++ b/ipa-core/src/error.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::{ helpers::{Role, ZeroRecordsError}, protocol::RecordId, - report::InvalidReportError, + report::{hybrid::InvalidHybridReportError, InvalidReportError}, sharding::ShardIndex, task::JoinError, }; @@ -69,6 +69,8 @@ pub enum Error { InvalidQueryParameter(BoxError), #[error("invalid report: {0}")] InvalidReport(#[from] InvalidReportError), + #[error("invalid hybrid report: {0}")] + InvalidHybridReport(#[from] InvalidHybridReportError), #[error("unsupported: {0}")] Unsupported(String), #[error("Decompressing invalid elliptic curve point: {0}")] diff --git a/ipa-core/src/query/runner/hybrid.rs b/ipa-core/src/query/runner/hybrid.rs index 06cc2da4a..3cfdcf4ff 100644 --- a/ipa-core/src/query/runner/hybrid.rs +++ b/ipa-core/src/query/runner/hybrid.rs @@ -5,7 +5,7 @@ use futures::{stream::iter, StreamExt, TryStreamExt}; use crate::{ error::Error, ff::{ - boolean_array::{BooleanArray, BA20, BA3, BA8}, + boolean_array::{BooleanArray, BA3, BA8}, U128Conversions, }, helpers::{ @@ -20,31 +20,40 @@ use crate::{ step::ProtocolStep::Hybrid, }, query::runner::reshard_tag::reshard_aad, - report::hybrid::{ - EncryptedHybridReport, IndistinguishableHybridReport, UniqueTag, UniqueTagValidator, + report::{ + hybrid::{ + EncryptedHybridReport, IndistinguishableHybridReport, UniqueTag, UniqueTagValidator, + }, + hybrid_info::HybridInfo, }, secret_sharing::replicated::semi_honest::AdditiveShare as Replicated, }; #[allow(dead_code)] -pub struct Query { +pub struct Query<'a, C, HV, R: PrivateKeyRegistry> { config: HybridQueryParams, key_registry: Arc, + hybrid_info: HybridInfo<'a>, phantom_data: PhantomData<(C, HV)>, } #[allow(dead_code)] -impl Query { - pub fn new(query_params: HybridQueryParams, key_registry: Arc) -> Self { +impl<'a, C, HV, R: PrivateKeyRegistry> Query<'a, C, HV, R> { + pub fn new( + query_params: HybridQueryParams, + key_registry: Arc, + hybrid_info: HybridInfo<'a>, + ) -> Self { Self { config: query_params, key_registry, + hybrid_info, phantom_data: PhantomData, } } } -impl Query +impl<'a, C, HV, R> Query<'a, C, HV, R> where C: UpgradableContext + Shuffle + ShardedContext, HV: BooleanArray + U128Conversions, @@ -60,6 +69,7 @@ where let Self { config, key_registry, + hybrid_info, phantom_data: _, } = self; @@ -73,13 +83,13 @@ where )); } - let stream = LengthDelimitedStream::::new(input_stream) + let stream = LengthDelimitedStream::, _>::new(input_stream) .map_err(Into::::into) .map_ok(|enc_reports| { iter(enc_reports.into_iter().map({ |enc_report| { let dec_report = enc_report - .decrypt::(key_registry.as_ref()) + .decrypt(key_registry.as_ref(), &hybrid_info) .map_err(Into::::into); let unique_tag = UniqueTag::from_unique_bytes(&enc_report); dec_report.map(|dec_report1| (dec_report1, unique_tag)) @@ -142,7 +152,7 @@ mod tests { use crate::{ ff::{ - boolean_array::{BA16, BA20, BA3, BA8}, + boolean_array::{BA16, BA3, BA8}, U128Conversions, }, helpers::{ @@ -151,61 +161,41 @@ mod tests { }, hpke::{KeyPair, KeyRegistry}, query::runner::hybrid::Query as HybridQuery, - report::{OprfReport, DEFAULT_KEY_ID}, + report::{hybrid::HybridReport, hybrid_info::HybridInfo, DEFAULT_KEY_ID}, secret_sharing::{replicated::semi_honest::AdditiveShare, IntoShares}, test_fixture::{ - flatten3v, ipa::TestRawDataRecord, Reconstruct, RoundRobinInputDistribution, TestWorld, - TestWorldConfig, WithShards, + flatten3v, hybrid::TestHybridRecord, Reconstruct, RoundRobinInputDistribution, + TestWorld, TestWorldConfig, WithShards, }, }; const EXPECTED: &[u128] = &[0, 8, 5]; - fn build_records() -> Vec { - // TODO: When Encryption/Decryption exists for HybridReports - // update these to use that, rather than generating OprfReports + fn build_records() -> Vec { vec![ - TestRawDataRecord { - timestamp: 0, - user_id: 12345, - is_trigger_report: false, + TestHybridRecord::TestImpression { + match_key: 12345, breakdown_key: 2, - trigger_value: 0, }, - TestRawDataRecord { - timestamp: 4, - user_id: 68362, - is_trigger_report: false, + TestHybridRecord::TestImpression { + match_key: 68362, breakdown_key: 1, - trigger_value: 0, }, - TestRawDataRecord { - timestamp: 10, - user_id: 12345, - is_trigger_report: true, - breakdown_key: 0, - trigger_value: 5, + TestHybridRecord::TestConversion { + match_key: 12345, + value: 5, }, - TestRawDataRecord { - timestamp: 12, - user_id: 68362, - is_trigger_report: true, - breakdown_key: 0, - trigger_value: 2, + TestHybridRecord::TestConversion { + match_key: 68362, + value: 2, }, - TestRawDataRecord { - timestamp: 20, - user_id: 68362, - is_trigger_report: false, + TestHybridRecord::TestImpression { + match_key: 68362, breakdown_key: 1, - trigger_value: 0, }, - TestRawDataRecord { - timestamp: 30, - user_id: 68362, - is_trigger_report: true, - breakdown_key: 1, - trigger_value: 7, + TestHybridRecord::TestConversion { + match_key: 68362, + value: 7, }, ] } @@ -216,17 +206,27 @@ mod tests { query_sizes: Vec, } - fn build_buffers_from_records(records: &[TestRawDataRecord], s: usize) -> BufferAndKeyRegistry { + fn build_buffers_from_records( + records: &[TestHybridRecord], + s: usize, + info: &HybridInfo, + ) -> BufferAndKeyRegistry { let mut rng = StdRng::seed_from_u64(42); let key_id = DEFAULT_KEY_ID; let key_registry = Arc::new(KeyRegistry::::random(1, &mut rng)); let mut buffers: [_; 3] = std::array::from_fn(|_| vec![Vec::new(); s]); - let shares: [Vec>; 3] = records.iter().cloned().share(); + let shares: [Vec>; 3] = records.iter().cloned().share(); for (buf, shares) in zip(&mut buffers, shares) { for (i, share) in shares.into_iter().enumerate() { share - .delimited_encrypt_to(key_id, key_registry.as_ref(), &mut rng, &mut buf[i % s]) + .delimited_encrypt_to( + key_id, + key_registry.as_ref(), + info, + &mut rng, + &mut buf[i % s], + ) .unwrap(); } } @@ -265,11 +265,14 @@ mod tests { const SHARDS: usize = 2; let records = build_records(); + let hybrid_info = + HybridInfo::new(0, "HELPER_ORIGIN", "meta.com", 1_729_707_432, 5.0, 1.1).unwrap(); + let BufferAndKeyRegistry { buffers, key_registry, query_sizes, - } = build_buffers_from_records(&records, SHARDS); + } = build_buffers_from_records(&records, SHARDS, &hybrid_info); let world: TestWorld> = TestWorld::with_shards(TestWorldConfig::default()); @@ -295,6 +298,7 @@ mod tests { HybridQuery::<_, BA16, KeyRegistry>::new( query_params, Arc::clone(&key_registry), + hybrid_info.clone(), ) .execute(ctx, query_size, input) }) @@ -329,11 +333,14 @@ mod tests { const SHARDS: usize = 2; let records = build_records(); + let hybrid_info = + HybridInfo::new(0, "HELPER_ORIGIN", "meta.com", 1_729_707_432, 5.0, 1.1).unwrap(); + let BufferAndKeyRegistry { mut buffers, key_registry, query_sizes, - } = build_buffers_from_records(&records, SHARDS); + } = build_buffers_from_records(&records, SHARDS, &hybrid_info); // this is double, since we duplicate the data below let query_sizes = query_sizes @@ -381,6 +388,7 @@ mod tests { HybridQuery::<_, BA16, KeyRegistry>::new( query_params, Arc::clone(&key_registry), + hybrid_info.clone(), ) .execute(ctx, query_size, input) }) @@ -400,11 +408,14 @@ mod tests { const SHARDS: usize = 2; let records = build_records(); + let hybrid_info = + HybridInfo::new(0, "HELPER_ORIGIN", "meta.com", 1_729_707_432, 5.0, 1.1).unwrap(); + let BufferAndKeyRegistry { buffers, key_registry, query_sizes, - } = build_buffers_from_records(&records, SHARDS); + } = build_buffers_from_records(&records, SHARDS, &hybrid_info); let world: TestWorld> = TestWorld::with_shards(TestWorldConfig::default()); @@ -430,6 +441,7 @@ mod tests { HybridQuery::<_, BA16, KeyRegistry>::new( query_params, Arc::clone(&key_registry), + hybrid_info.clone(), ) .execute(ctx, query_size, input) }) diff --git a/ipa-core/src/report/hybrid.rs b/ipa-core/src/report/hybrid.rs index 95604e2fe..d8179a75d 100644 --- a/ipa-core/src/report/hybrid.rs +++ b/ipa-core/src/report/hybrid.rs @@ -27,14 +27,9 @@ //! all secret sharings (including the sharings of zero), making the collection of reports //! cryptographically indistinguishable. -use std::{ - collections::HashSet, - convert::Infallible, - marker::PhantomData, - ops::{Add, Deref}, -}; +use std::{collections::HashSet, convert::Infallible, iter::once, marker::PhantomData, ops::Add}; -use bytes::{BufMut, Bytes}; +use bytes::{Buf, BufMut, Bytes}; use generic_array::{ArrayLength, GenericArray}; use hpke::Serializable as _; use rand_core::{CryptoRng, RngCore}; @@ -49,14 +44,15 @@ use crate::{ PublicKeyRegistry, TagSize, }, report::{ - hybrid_info::HybridImpressionInfo, EncryptedOprfReport, EventType, InvalidReportError, - KeyIdentifier, + hybrid_info::{HybridConversionInfo, HybridImpressionInfo, HybridInfo}, + EncryptedOprfReport, EventType as OprfEventType, KeyIdentifier, }, secret_sharing::{replicated::semi_honest::AdditiveShare as Replicated, SharedValue}, sharding::ShardIndex, }; // TODO(679): This needs to come from configuration. +#[allow(dead_code)] static HELPER_ORIGIN: &str = "github.com/private-attribution"; #[derive(Debug, thiserror::Error)] @@ -79,6 +75,33 @@ pub enum InvalidHybridReportError { DeserializationError(&'static str, #[source] BoxError), #[error("report is too short: {0}, expected length at least: {1}")] Length(usize, usize), + #[error("unknown event type: {0}. Only 0 and 1 are allowed")] + UnknownEventType(u8), + #[error("Incorrect hybrid info type: Expected {0}")] + WrongInfoType(&'static str), +} + +/// Event type as described [`ipa-issue`] +/// Initially we will just support trigger vs source event types but could extend to others in +/// the future. +/// +/// ['ipa-issue']: https://github.com/patcg-individual-drafts/ipa/issues/38 +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum HybridEventType { + Impression, + Conversion, +} + +impl TryFrom for HybridEventType { + type Error = InvalidHybridReportError; + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Impression), + 1 => Ok(Self::Conversion), + _ => Err(InvalidHybridReportError::UnknownEventType(value)), + } + } } /// Reports for impression events are represented here. @@ -87,8 +110,8 @@ pub struct HybridImpressionReport where BK: SharedValue, { - match_key: Replicated, - breakdown_key: Replicated, + pub match_key: Replicated, + pub breakdown_key: Replicated, } impl Serializable for HybridImpressionReport @@ -115,8 +138,7 @@ where let mk_sz = as Serializable>::Size::USIZE; let bk_sz = as Serializable>::Size::USIZE; let match_key = - Replicated::::deserialize(GenericArray::from_slice(&buf[..mk_sz])) - .map_err(|e| InvalidHybridReportError::DeserializationError("match_key", e.into()))?; + Replicated::::deserialize_infallible(GenericArray::from_slice(&buf[..mk_sz])); let breakdown_key = Replicated::::deserialize(GenericArray::from_slice(&buf[mk_sz..mk_sz + bk_sz])) .map_err(|e| InvalidHybridReportError::DeserializationError("breakdown_key", e.into()))?; @@ -136,19 +158,35 @@ where /// # Panics /// If report length does not fit in `u16`. pub fn encrypted_len(&self) -> u16 { - let len = EncryptedHybridImpressionReport::::SITE_DOMAIN_OFFSET; + let len = EncryptedHybridImpressionReport::::SITE_DOMAIN_OFFSET; len.try_into().unwrap() } + + /// # Errors + /// If there is a problem encrypting the report. + pub fn delimited_encrypt_to( + &self, + key_id: KeyIdentifier, + key_registry: &impl PublicKeyRegistry, + info: &HybridImpressionInfo, + rng: &mut R, + out: &mut B, + ) -> Result<(), InvalidHybridReportError> { + out.put_u16_le(self.encrypted_len()); + self.encrypt_to(key_id, key_registry, info, rng, out) + } + /// # Errors /// If there is a problem encrypting the report. pub fn encrypt( &self, key_id: KeyIdentifier, key_registry: &impl PublicKeyRegistry, + info: &HybridImpressionInfo, rng: &mut R, ) -> Result, InvalidHybridReportError> { let mut out = Vec::with_capacity(usize::from(self.encrypted_len())); - self.encrypt_to(key_id, key_registry, rng, &mut out)?; + self.encrypt_to(key_id, key_registry, info, rng, &mut out)?; debug_assert_eq!(out.len(), usize::from(self.encrypted_len())); Ok(out) } @@ -159,11 +197,10 @@ where &self, key_id: KeyIdentifier, key_registry: &impl PublicKeyRegistry, + info: &HybridImpressionInfo, rng: &mut R, out: &mut B, ) -> Result<(), InvalidHybridReportError> { - let info = HybridImpressionInfo::new(key_id, HELPER_ORIGIN)?; - let mut plaintext_mk = GenericArray::default(); self.match_key.serialize(&mut plaintext_mk); @@ -205,8 +242,132 @@ pub struct HybridConversionReport where V: SharedValue, { - match_key: Replicated, - value: Replicated, + pub match_key: Replicated, + pub value: Replicated, +} + +impl Serializable for HybridConversionReport +where + V: SharedValue, + Replicated: Serializable, + as Serializable>::Size: Add, + < as Serializable>::Size as Add< as Serializable>::Size>>:: Output: ArrayLength, +{ + type Size = < as Serializable>::Size as Add< as Serializable>::Size>>:: Output; + type DeserializationError = InvalidHybridReportError; + + fn serialize(&self, buf: &mut GenericArray) { + let mk_sz = as Serializable>::Size::USIZE; + let v_sz = as Serializable>::Size::USIZE; + + self.match_key + .serialize(GenericArray::from_mut_slice(&mut buf[..mk_sz])); + + self.value + .serialize(GenericArray::from_mut_slice(&mut buf[mk_sz..mk_sz + v_sz])); + } + fn deserialize(buf: &GenericArray) -> Result { + let mk_sz = as Serializable>::Size::USIZE; + let v_sz = as Serializable>::Size::USIZE; + let match_key = + Replicated::::deserialize(GenericArray::from_slice(&buf[..mk_sz])) + .map_err(|e| InvalidHybridReportError::DeserializationError("match_key", e.into()))?; + let value = + Replicated::::deserialize(GenericArray::from_slice(&buf[mk_sz..mk_sz + v_sz])) + .map_err(|e| InvalidHybridReportError::DeserializationError("breakdown_key", e.into()))?; + Ok(Self { match_key, value }) + } +} + +impl HybridConversionReport +where + V: SharedValue, + Replicated: Serializable, + as Serializable>::Size: Add, + < as Serializable>::Size as Add< as Serializable>::Size>>:: Output: ArrayLength, +{ + const BTT_END: usize = as Serializable>::Size::USIZE; + + /// # Panics + /// If report length does not fit in `u16`. + pub fn encrypted_len(&self) -> u16 { + let len = EncryptedHybridConversionReport::::SITE_DOMAIN_OFFSET; + len.try_into().unwrap() + } + + /// # Errors + /// If there is a problem encrypting the report. + pub fn delimited_encrypt_to( + &self, + key_id: KeyIdentifier, + key_registry: &impl PublicKeyRegistry, + info: &HybridConversionInfo, + rng: &mut R, + out: &mut B, + ) -> Result<(), InvalidHybridReportError> { + out.put_u16_le(self.encrypted_len()); + self.encrypt_to(key_id, key_registry, info, rng, out) + } + + /// # Errors + /// If there is a problem encrypting the report. + pub fn encrypt( + &self, + key_id: KeyIdentifier, + key_registry: &impl PublicKeyRegistry, + info: &HybridConversionInfo, + rng: &mut R, + ) -> Result, InvalidHybridReportError> { + let mut out = Vec::with_capacity(usize::from(self.encrypted_len())); + self.encrypt_to(key_id, key_registry, info, rng, &mut out)?; + debug_assert_eq!(out.len(), usize::from(self.encrypted_len())); + Ok(out) + } + + /// # Errors + /// If there is a problem encrypting the report. + pub fn encrypt_to( + &self, + key_id: KeyIdentifier, + key_registry: &impl PublicKeyRegistry, + info: &HybridConversionInfo, + rng: &mut R, + out: &mut B, + ) -> Result<(), InvalidHybridReportError> { + + let mut plaintext_mk = GenericArray::default(); + self.match_key.serialize(&mut plaintext_mk); + + let mut plaintext_btt = vec![0u8; Self::BTT_END]; + self.value + .serialize(GenericArray::from_mut_slice(&mut plaintext_btt[..])); + + let pk = key_registry.public_key(key_id).ok_or(CryptError::NoSuchKey(key_id))?; + + let (encap_key_mk, ciphertext_mk, tag_mk) = seal_in_place( + pk, + plaintext_mk.as_mut(), + &info.to_bytes(), + rng, + )?; + + let (encap_key_btt, ciphertext_btt, tag_btt) = seal_in_place( + pk, + plaintext_btt.as_mut(), + &info.to_bytes(), + rng, + )?; + + out.put_slice(&encap_key_mk.to_bytes()); + out.put_slice(ciphertext_mk); + out.put_slice(&tag_mk.to_bytes()); + out.put_slice(&encap_key_btt.to_bytes()); + out.put_slice(ciphertext_btt); + out.put_slice(&tag_btt.to_bytes()); + out.put_slice(&[key_id]); + + Ok(()) + } } /// This enum contains both report types, impression and conversion. @@ -224,35 +385,106 @@ impl HybridReport where BK: SharedValue, V: SharedValue, + Replicated: Serializable, + Replicated: Serializable, + as Serializable>::Size: Add, + as Serializable>::Size: Add, + < as Serializable>::Size as Add< as Serializable>::Size>>:: Output: ArrayLength, + < as Serializable>::Size as Add< as Serializable>::Size>>:: Output: ArrayLength, { + /// # Panics + /// If report length does not fit in `u16`. + pub fn encrypted_len(&self) -> u16 { + match self { + HybridReport::Impression(impression_report) => { + impression_report.encrypted_len() +1 + } + HybridReport::Conversion(conversion_report) => { + conversion_report.encrypted_len() +1 + } + } + } + + /// # Errors + /// If there is a problem encrypting the report. + pub fn delimited_encrypt_to( + &self, + key_id: KeyIdentifier, + key_registry: &impl PublicKeyRegistry, + info: &HybridInfo, + rng: &mut R, + out: &mut B, + ) -> Result<(), InvalidHybridReportError> { + match self { + HybridReport::Impression(impression_report) => { + out.put_u16_le(self.encrypted_len()); + out.put_u8(HybridEventType::Impression as u8); + impression_report.encrypt_to(key_id, key_registry, &info.impression, rng, out) + }, + HybridReport::Conversion(conversion_report) => { + out.put_u16_le(self.encrypted_len()); + out.put_u8(HybridEventType::Conversion as u8); + conversion_report.encrypt_to(key_id, key_registry, &info.conversion, rng, out) + }, + } + } + /// # Errors /// If there is a problem encrypting the report. pub fn encrypt( &self, - _key_id: KeyIdentifier, - _key_registry: &impl PublicKeyRegistry, - _rng: &mut R, - ) -> Result, InvalidReportError> { - unimplemented!() + key_id: KeyIdentifier, + key_registry: &impl PublicKeyRegistry, + info: &HybridInfo, + rng: &mut R, + ) -> Result, InvalidHybridReportError> { + match self { + HybridReport::Impression(impression_report) => { + impression_report.encrypt(key_id, key_registry, &info.impression, rng).map(|v| once(HybridEventType::Impression as u8).chain(v).collect()) + }, + HybridReport::Conversion(conversion_report) => { + conversion_report.encrypt(key_id, key_registry, &info.conversion, rng).map(|v| once(HybridEventType::Conversion as u8).chain(v).collect()) + }, + } + } + + /// # Errors + /// If there is a problem encrypting the report. + pub fn encrypt_to( + &self, + key_id: KeyIdentifier, + key_registry: &impl PublicKeyRegistry, + info: &HybridInfo, + rng: &mut R, + out: &mut B, + ) -> Result<(), InvalidHybridReportError> { + match self { + HybridReport::Impression(impression_report) =>{ + out.put_u8(HybridEventType::Impression as u8); + impression_report.encrypt_to(key_id, key_registry, &info.impression, rng, out) + }, + HybridReport::Conversion(conversion_report) => { + out.put_u8(HybridEventType::Conversion as u8); + conversion_report.encrypt_to(key_id, key_registry, &info.conversion, rng, out) + }, + } } } /// `HybridImpressionReport`s are encrypted when they arrive to the helpers, /// which is represented here. A `EncryptedHybridImpressionReport` decrypts /// into a `HybridImpressionReport`. -#[derive(Copy, Clone, Eq, PartialEq)] -pub struct EncryptedHybridImpressionReport +#[derive(Clone, Eq, PartialEq)] +pub struct EncryptedHybridImpressionReport where - B: Deref, BK: SharedValue, { - data: B, + data: Bytes, phantom_data: PhantomData, } -impl EncryptedHybridImpressionReport +impl EncryptedHybridImpressionReport where - B: Deref, BK: SharedValue, Replicated: Serializable, as Serializable>::Size: Add, @@ -260,14 +492,12 @@ where { const ENCAP_KEY_MK_OFFSET: usize = 0; const CIPHERTEXT_MK_OFFSET: usize = Self::ENCAP_KEY_MK_OFFSET + EncapsulationSize::USIZE; - const ENCAP_KEY_BTT_OFFSET: usize = (Self::CIPHERTEXT_MK_OFFSET - + TagSize::USIZE - + as Serializable>::Size::USIZE); + const ENCAP_KEY_BTT_OFFSET: usize = + (Self::CIPHERTEXT_MK_OFFSET + TagSize::USIZE + Replicated::::size()); const CIPHERTEXT_BTT_OFFSET: usize = Self::ENCAP_KEY_BTT_OFFSET + EncapsulationSize::USIZE; - const KEY_IDENTIFIER_OFFSET: usize = (Self::CIPHERTEXT_BTT_OFFSET - + TagSize::USIZE - + as Serializable>::Size::USIZE); + const KEY_IDENTIFIER_OFFSET: usize = + (Self::CIPHERTEXT_BTT_OFFSET + TagSize::USIZE + Replicated::::size()); const SITE_DOMAIN_OFFSET: usize = Self::KEY_IDENTIFIER_OFFSET + 1; pub fn encap_key_mk(&self) -> &[u8] { @@ -292,7 +522,7 @@ where /// ## Errors /// If the report contents are invalid. - pub fn from_bytes(bytes: B) -> Result { + pub fn from_bytes(bytes: Bytes) -> Result { if bytes.len() < Self::SITE_DOMAIN_OFFSET { return Err(InvalidHybridReportError::Length( bytes.len(), @@ -314,12 +544,11 @@ where pub fn decrypt( &self, key_registry: &P, + info: &HybridImpressionInfo, ) -> Result, InvalidHybridReportError> { type CTMKLength = Sum< as Serializable>::Size, TagSize>; type CTBTTLength = < as Serializable>::Size as Add>::Output; - let info = HybridImpressionInfo::new(self.key_id(), HELPER_ORIGIN).unwrap(); // validated on construction - let mut ct_mk: GenericArray = *GenericArray::from_slice(self.mk_ciphertext()); let sk = key_registry @@ -343,6 +572,103 @@ where } } +#[derive(Clone, Eq, PartialEq)] +pub struct EncryptedHybridConversionReport +where + V: SharedValue, +{ + data: Bytes, + phantom_data: PhantomData, +} + +impl EncryptedHybridConversionReport +where + V: SharedValue, + Replicated: Serializable, + as Serializable>::Size: Add, + < as Serializable>::Size as Add>::Output: ArrayLength, +{ + const ENCAP_KEY_MK_OFFSET: usize = 0; + const CIPHERTEXT_MK_OFFSET: usize = Self::ENCAP_KEY_MK_OFFSET + EncapsulationSize::USIZE; + const ENCAP_KEY_BTT_OFFSET: usize = + (Self::CIPHERTEXT_MK_OFFSET + TagSize::USIZE + Replicated::::size()); + const CIPHERTEXT_BTT_OFFSET: usize = Self::ENCAP_KEY_BTT_OFFSET + EncapsulationSize::USIZE; + + const KEY_IDENTIFIER_OFFSET: usize = + (Self::CIPHERTEXT_BTT_OFFSET + TagSize::USIZE + Replicated::::size()); + const SITE_DOMAIN_OFFSET: usize = Self::KEY_IDENTIFIER_OFFSET + 1; + + pub fn encap_key_mk(&self) -> &[u8] { + &self.data[Self::ENCAP_KEY_MK_OFFSET..Self::CIPHERTEXT_MK_OFFSET] + } + + pub fn mk_ciphertext(&self) -> &[u8] { + &self.data[Self::CIPHERTEXT_MK_OFFSET..Self::ENCAP_KEY_BTT_OFFSET] + } + + pub fn encap_key_btt(&self) -> &[u8] { + &self.data[Self::ENCAP_KEY_BTT_OFFSET..Self::CIPHERTEXT_BTT_OFFSET] + } + + pub fn btt_ciphertext(&self) -> &[u8] { + &self.data[Self::CIPHERTEXT_BTT_OFFSET..Self::KEY_IDENTIFIER_OFFSET] + } + + pub fn key_id(&self) -> KeyIdentifier { + self.data[Self::KEY_IDENTIFIER_OFFSET] + } + + /// ## Errors + /// If the report contents are invalid. + pub fn from_bytes(bytes: Bytes) -> Result { + if bytes.len() < Self::SITE_DOMAIN_OFFSET { + return Err(InvalidHybridReportError::Length( + bytes.len(), + Self::SITE_DOMAIN_OFFSET, + )); + } + Ok(Self { + data: bytes, + phantom_data: PhantomData, + }) + } + + /// ## Errors + /// If the match key shares in the report cannot be decrypted (e.g. due to a + /// failure of the authenticated encryption). + /// ## Panics + /// Should not panic. Only panics if a `Report` constructor failed to validate the + /// contents properly, which would be a bug. + pub fn decrypt( + &self, + key_registry: &P, + info: &HybridConversionInfo, + ) -> Result, InvalidHybridReportError> { + type CTMKLength = Sum< as Serializable>::Size, TagSize>; + type CTBTTLength = < as Serializable>::Size as Add>::Output; + + let mut ct_mk: GenericArray = + *GenericArray::from_slice(self.mk_ciphertext()); + let sk = key_registry + .private_key(self.key_id()) + .ok_or(CryptError::NoSuchKey(self.key_id()))?; + let plaintext_mk = open_in_place(sk, self.encap_key_mk(), &mut ct_mk, &info.to_bytes())?; + let mut ct_btt: GenericArray> = + GenericArray::from_slice(self.btt_ciphertext()).clone(); + + let plaintext_btt = open_in_place(sk, self.encap_key_btt(), &mut ct_btt, &info.to_bytes())?; + + Ok(HybridConversionReport:: { + match_key: Replicated::::deserialize_infallible(GenericArray::from_slice( + plaintext_mk, + )), + value: Replicated::::deserialize(GenericArray::from_slice(plaintext_btt)).map_err( + |e| InvalidHybridReportError::DeserializationError("trigger_value", e.into()), + )?, + }) + } +} + /// This struct is designed to fit both `HybridConversionReport`s /// and `HybridImpressionReport`s so that they can be made indistingushable. /// Note: these need to be shuffled (and secret shares need to be rerandomized) @@ -425,25 +751,97 @@ where } } -#[derive(Clone)] -pub struct EncryptedHybridReport { - bytes: Bytes, +#[derive(Clone, Eq, PartialEq)] +pub enum EncryptedHybridReport +where + BK: SharedValue, + V: SharedValue, +{ + Impression(EncryptedHybridImpressionReport), + Conversion(EncryptedHybridConversionReport), } - -impl EncryptedHybridReport { +impl EncryptedHybridReport +where + V: SharedValue, + BK: SharedValue, + Replicated: Serializable, + Replicated: Serializable, + as Serializable>::Size: Add, + < as Serializable>::Size as Add>::Output: ArrayLength, + as Serializable>::Size: Add, + < as Serializable>::Size as Add>::Output: ArrayLength, +{ + pub fn encap_key_mk(&self) -> &[u8] { + match self { + EncryptedHybridReport::Impression(impression_report) => { + impression_report.encap_key_mk() + } + EncryptedHybridReport::Conversion(conversion_report) => { + conversion_report.encap_key_mk() + } + } + } + pub fn mk_ciphertext(&self) -> &[u8] { + match self { + EncryptedHybridReport::Impression(impression_report) => { + impression_report.mk_ciphertext() + } + EncryptedHybridReport::Conversion(conversion_report) => { + conversion_report.mk_ciphertext() + } + } + } + pub fn encap_key_btt(&self) -> &[u8] { + match self { + EncryptedHybridReport::Impression(impression_report) => { + impression_report.encap_key_btt() + } + EncryptedHybridReport::Conversion(conversion_report) => { + conversion_report.encap_key_btt() + } + } + } + pub fn btt_ciphertext(&self) -> &[u8] { + match self { + EncryptedHybridReport::Impression(impression_report) => { + impression_report.btt_ciphertext() + } + EncryptedHybridReport::Conversion(conversion_report) => { + conversion_report.btt_ciphertext() + } + } + } + pub fn key_id(&self) -> u8 { + match self { + EncryptedHybridReport::Impression(impression_report) => impression_report.key_id(), + EncryptedHybridReport::Conversion(conversion_report) => conversion_report.key_id(), + } + } /// ## Errors - /// If the report fails to decrypt - pub fn decrypt( - &self, + /// If the report contents are invalid. + pub fn from_bytes(mut bytes: Bytes) -> Result { + match HybridEventType::try_from(bytes[0])? { + HybridEventType::Impression => { + bytes.advance(1); + let impression_report = EncryptedHybridImpressionReport::::from_bytes(bytes)?; + Ok(EncryptedHybridReport::Impression(impression_report)) + } + HybridEventType::Conversion => { + bytes.advance(1); + let conversion_report = EncryptedHybridConversionReport::::from_bytes(bytes)?; + Ok(EncryptedHybridReport::Conversion(conversion_report)) + } + } + } + /// ## Errors + /// If decryption of the provided oprf report fails. + pub fn decrypt_from_oprf_report_bytes( + bytes: Bytes, key_registry: &P, - ) -> Result, InvalidReportError> + ) -> Result, InvalidHybridReportError> where P: PrivateKeyRegistry, - BK: SharedValue, - V: SharedValue, TS: SharedValue, - Replicated: Serializable, - Replicated: Serializable, Replicated: Serializable, as Serializable>::Size: Add< as Serializable>::Size>, Sum< as Serializable>::Size, as Serializable>::Size>: @@ -460,38 +858,64 @@ impl EncryptedHybridReport { U16, >: ArrayLength, { - let encrypted_oprf_report = - EncryptedOprfReport::::try_from(self.bytes.clone())?; - let oprf_report = encrypted_oprf_report.decrypt(key_registry)?; + let encrypted_oprf_report = EncryptedOprfReport::::try_from(bytes) + .map_err(|e| { + InvalidHybridReportError::DeserializationError("EncryptedOprfReport", e.into()) + })?; + let oprf_report = encrypted_oprf_report.decrypt(key_registry).map_err(|e| { + InvalidHybridReportError::DeserializationError( + "EncryptedOprfReport Decryption Failure", + e.into(), + ) + })?; match oprf_report.event_type { - EventType::Source => Ok(HybridReport::Impression(HybridImpressionReport { + OprfEventType::Source => Ok(HybridReport::Impression(HybridImpressionReport { match_key: oprf_report.match_key, breakdown_key: oprf_report.breakdown_key, })), - EventType::Trigger => Ok(HybridReport::Conversion(HybridConversionReport { + OprfEventType::Trigger => Ok(HybridReport::Conversion(HybridConversionReport { match_key: oprf_report.match_key, value: oprf_report.trigger_value, })), } } - - /// TODO: update these when we produce a proper encapsulation of - /// `EncryptedHybridReport`, rather than pigggybacking on `EncryptedOprfReport` - pub fn mk_ciphertext(&self) -> &[u8] { - let encap_key_mk_offset: usize = 0; - let ciphertext_mk_offset: usize = encap_key_mk_offset + EncapsulationSize::USIZE; - let encap_key_btt_offset: usize = - ciphertext_mk_offset + TagSize::USIZE + as Serializable>::Size::USIZE; - - &self.bytes[ciphertext_mk_offset..encap_key_btt_offset] + /// ## Errors + /// If the match key shares in the report cannot be decrypted (e.g. due to a + /// failure of the authenticated encryption). + /// ## Panics + /// Should not panic. Only panics if a `Report` constructor failed to validate the + /// contents properly, which would be a bug. + pub fn decrypt( + &self, + key_registry: &P, + info: &HybridInfo, + ) -> Result, InvalidHybridReportError> { + match self { + EncryptedHybridReport::Impression(impression_report) => Ok(HybridReport::Impression( + impression_report.decrypt(key_registry, &info.impression)?, + )), + EncryptedHybridReport::Conversion(conversion_report) => Ok(HybridReport::Conversion( + conversion_report.decrypt(key_registry, &info.conversion)?, + )), + } } } -impl TryFrom for EncryptedHybridReport { - type Error = InvalidReportError; +impl TryFrom for EncryptedHybridReport +where + V: SharedValue, + BK: SharedValue, + Replicated: Serializable, + Replicated: Serializable, + as Serializable>::Size: Add, + < as Serializable>::Size as Add>::Output: ArrayLength, + as Serializable>::Size: Add, + < as Serializable>::Size as Add>::Output: ArrayLength, +{ + type Error = InvalidHybridReportError; - fn try_from(bytes: Bytes) -> Result { - Ok(EncryptedHybridReport { bytes }) + fn try_from(bytes: Bytes) -> Result { + Self::from_bytes(bytes) } } @@ -512,7 +936,17 @@ impl UniqueBytes for UniqueTag { } } -impl UniqueBytes for EncryptedHybridReport { +impl UniqueBytes for EncryptedHybridReport +where + V: SharedValue, + BK: SharedValue, + Replicated: Serializable, + Replicated: Serializable, + as Serializable>::Size: Add, + < as Serializable>::Size as Add>::Output: ArrayLength, + as Serializable>::Size: Add, + < as Serializable>::Size as Add>::Output: ArrayLength, +{ /// We use the `TagSize` (the first 16 bytes of the ciphertext) for collision-detection /// See [analysis here for uniqueness](https://eprint.iacr.org/2019/624) fn unique_bytes(&self) -> [u8; TAG_SIZE] { @@ -609,7 +1043,7 @@ mod test { use super::{ EncryptedHybridImpressionReport, EncryptedHybridReport, GenericArray, HybridConversionReport, HybridImpressionReport, HybridReport, - IndistinguishableHybridReport, UniqueTag, UniqueTagValidator, + IndistinguishableHybridReport, UniqueTag, UniqueTagValidator, HELPER_ORIGIN, }; use crate::{ error::Error, @@ -619,14 +1053,17 @@ mod test { }, hpke::{KeyPair, KeyRegistry}, report::{ - hybrid::{NonAsciiStringError, BA64}, - hybrid_info::HybridImpressionInfo, - EventType, OprfReport, + hybrid::{EncryptedHybridConversionReport, HybridEventType, NonAsciiStringError, BA64}, + hybrid_info::{HybridConversionInfo, HybridImpressionInfo, HybridInfo}, + EventType as OprfEventType, OprfReport, }, secret_sharing::replicated::{semi_honest::AdditiveShare, ReplicatedSecretSharing}, }; - fn build_oprf_report(event_type: EventType, rng: &mut ThreadRng) -> OprfReport { + fn build_oprf_report( + event_type: OprfEventType, + rng: &mut ThreadRng, + ) -> OprfReport { OprfReport:: { match_key: AdditiveShare::new(rng.gen(), rng.gen()), timestamp: AdditiveShare::new(rng.gen(), rng.gen()), @@ -653,7 +1090,7 @@ mod test { fn convert_to_hybrid_impression_report() { let mut rng = thread_rng(); - let b = EventType::Source; + let b = OprfEventType::Source; let oprf_report = build_oprf_report(b, &mut rng); let hybrid_report = HybridReport::Impression::(HybridImpressionReport:: { @@ -667,13 +1104,12 @@ mod test { let enc_report_bytes = oprf_report .encrypt(key_id, &key_registry, &mut rng) .unwrap(); - let enc_report = EncryptedHybridReport { - bytes: enc_report_bytes.into(), - }; - let hybrid_report2 = enc_report - .decrypt::<_, BA8, BA3, BA20>(&key_registry) - .unwrap(); + let hybrid_report2 = EncryptedHybridReport::::decrypt_from_oprf_report_bytes::< + _, + BA20, + >(enc_report_bytes.into(), &key_registry) + .unwrap(); assert_eq!(hybrid_report, hybrid_report2); } @@ -682,7 +1118,7 @@ mod test { fn convert_to_hybrid_conversion_report() { let mut rng = thread_rng(); - let b = EventType::Trigger; + let b = OprfEventType::Trigger; let oprf_report = build_oprf_report(b, &mut rng); let hybrid_report = HybridReport::Conversion::(HybridConversionReport:: { @@ -696,12 +1132,11 @@ mod test { let enc_report_bytes = oprf_report .encrypt(key_id, &key_registry, &mut rng) .unwrap(); - let enc_report = EncryptedHybridReport { - bytes: enc_report_bytes.into(), - }; - let hybrid_report2 = enc_report - .decrypt::<_, BA8, BA3, BA20>(&key_registry) - .unwrap(); + let hybrid_report2 = EncryptedHybridReport::::decrypt_from_oprf_report_bytes::< + _, + BA20, + >(enc_report_bytes.into(), &key_registry) + .unwrap(); assert_eq!(hybrid_report, hybrid_report2); } @@ -791,7 +1226,7 @@ mod test { #[test] fn serialization_hybrid_impression() { let mut rng = thread_rng(); - let b = EventType::Source; + let b = OprfEventType::Source; let oprf_report = build_oprf_report(b, &mut rng); let hybrid_impression_report = HybridImpressionReport:: { @@ -811,7 +1246,29 @@ mod test { } #[test] - fn deserialzation_from_constant() { + fn serialization_hybrid_conversion() { + let mut rng = thread_rng(); + let b = OprfEventType::Source; + let oprf_report = build_oprf_report(b, &mut rng); + + let hybrid_conversion_report = HybridConversionReport:: { + match_key: oprf_report.match_key.clone(), + value: oprf_report.trigger_value.clone(), + }; + let mut hybrid_conversion_report_bytes = + [0u8; as Serializable>::Size::USIZE]; + hybrid_conversion_report.serialize(GenericArray::from_mut_slice( + &mut hybrid_conversion_report_bytes[..], + )); + let hybrid_conversion_report2 = HybridConversionReport::::deserialize( + GenericArray::from_mut_slice(&mut hybrid_conversion_report_bytes[..]), + ) + .unwrap(); + assert_eq!(hybrid_conversion_report, hybrid_conversion_report2); + } + + #[test] + fn constant_serialization_hybrid_impression() { let hybrid_report = HybridImpressionReport::::deserialize(GenericArray::from_slice( &hex::decode("4123a6e38ef1d6d9785c948797cb744d38f4").unwrap(), )) @@ -846,10 +1303,43 @@ mod test { ); } + #[test] + fn constant_serialization_hybrid_conversion() { + let hybrid_report = HybridConversionReport::::deserialize(GenericArray::from_slice( + &hex::decode("4123a6e38ef1d6d9785c948797cb744d0203").unwrap(), + )) + .unwrap(); + + let match_key = AdditiveShare::::deserialize(GenericArray::from_slice( + &hex::decode("4123a6e38ef1d6d9785c948797cb744d").unwrap(), + )) + .unwrap(); + let value = AdditiveShare::::deserialize(GenericArray::from_slice( + &hex::decode("0203").unwrap(), + )) + .unwrap(); + + assert_eq!( + hybrid_report, + HybridConversionReport:: { match_key, value } + ); + + let mut hybrid_conversion_report_bytes = + [0u8; as Serializable>::Size::USIZE]; + hybrid_report.serialize(GenericArray::from_mut_slice( + &mut hybrid_conversion_report_bytes[..], + )); + + assert_eq!( + hybrid_conversion_report_bytes.to_vec(), + hex::decode("4123a6e38ef1d6d9785c948797cb744d0203").unwrap() + ); + } + #[test] fn enc_dec_roundtrip_hybrid_impression() { let mut rng = thread_rng(); - let b = EventType::Source; + let b = OprfEventType::Source; let oprf_report = build_oprf_report(b, &mut rng); let hybrid_impression_report = HybridImpressionReport:: { @@ -860,18 +1350,104 @@ mod test { let key_registry = KeyRegistry::::random(1, &mut rng); let key_id = 0; + let info = HybridImpressionInfo::new(key_id, HELPER_ORIGIN).unwrap(); + let enc_report_bytes = hybrid_impression_report - .encrypt(key_id, &key_registry, &mut rng) + .encrypt(key_id, &key_registry, &info, &mut rng) .unwrap(); let enc_report = - EncryptedHybridImpressionReport::::from_bytes(enc_report_bytes.as_slice()) - .unwrap(); - let dec_report: HybridImpressionReport = enc_report.decrypt(&key_registry).unwrap(); + EncryptedHybridImpressionReport::::from_bytes(enc_report_bytes.into()).unwrap(); + let dec_report: HybridImpressionReport = + enc_report.decrypt(&key_registry, &info).unwrap(); assert_eq!(dec_report, hybrid_impression_report); } + #[test] + fn enc_dec_roundtrip_hybrid_conversion() { + let mut rng = thread_rng(); + let b = OprfEventType::Trigger; + let oprf_report = build_oprf_report(b, &mut rng); + + let hybrid_conversion_report = HybridConversionReport:: { + match_key: oprf_report.match_key.clone(), + value: oprf_report.trigger_value.clone(), + }; + + let key_registry = KeyRegistry::::random(1, &mut rng); + let key_id = 0; + + let info = + HybridConversionInfo::new(key_id, HELPER_ORIGIN, "meta.com", 1_729_707_432, 5.0, 1.1) + .unwrap(); + + let enc_report_bytes = hybrid_conversion_report + .encrypt(key_id, &key_registry, &info, &mut rng) + .unwrap(); + + let enc_report = + EncryptedHybridConversionReport::::from_bytes(enc_report_bytes.into()).unwrap(); + let dec_report: HybridConversionReport = + enc_report.decrypt(&key_registry, &info).unwrap(); + + assert_eq!(dec_report, hybrid_conversion_report); + } + + #[test] + fn enc_report_serialization() { + let mut rng = thread_rng(); + let b = OprfEventType::Trigger; + let oprf_report = build_oprf_report(b, &mut rng); + + let hybrid_conversion_report = HybridConversionReport:: { + match_key: oprf_report.match_key.clone(), + value: oprf_report.trigger_value.clone(), + }; + + let key_registry = KeyRegistry::::random(1, &mut rng); + let key_id = 0; + + let info = + HybridInfo::new(0, "HELPER_ORIGIN", "meta.com", 1_729_707_432, 5.0, 1.1).unwrap(); + + let enc_report_bytes = hybrid_conversion_report + .encrypt(key_id, &key_registry, &info.conversion, &mut rng) + .unwrap(); + + let mut enc_report_bytes2 = enc_report_bytes.clone(); + + let enc_report = + EncryptedHybridConversionReport::::from_bytes(enc_report_bytes.into()).unwrap(); + let dec_report: HybridConversionReport = + enc_report.decrypt(&key_registry, &info.conversion).unwrap(); + assert_eq!(dec_report, hybrid_conversion_report); + + // Prepend a byte to the ciphertext to mark it as a ConversionReport + enc_report_bytes2.splice(0..0, [HybridEventType::Conversion as u8]); + + let enc_report2 = + EncryptedHybridReport::::from_bytes(enc_report_bytes2.into()).unwrap(); + let enc_report3 = enc_report2.clone(); + + // Case 1: Match first, then decrypt + match enc_report2 { + EncryptedHybridReport::Impression(_) => panic!("Expected conversion report"), + EncryptedHybridReport::Conversion(enc_report_conv) => { + let dec_report2: HybridConversionReport = enc_report_conv + .decrypt(&key_registry, &info.conversion) + .unwrap(); + assert_eq!(dec_report2, hybrid_conversion_report); + } + } + // Case 2: Decrypt directly + let dec_report3 = enc_report3.decrypt(&key_registry, &info).unwrap(); + assert_eq!( + dec_report3, + HybridReport::Conversion(hybrid_conversion_report) + ); + } + #[test] fn non_ascii_string() { let non_ascii_string = "☃️☃️☃️"; diff --git a/ipa-core/src/report/hybrid_info.rs b/ipa-core/src/report/hybrid_info.rs index c41849121..5dc2950f2 100644 --- a/ipa-core/src/report/hybrid_info.rs +++ b/ipa-core/src/report/hybrid_info.rs @@ -2,43 +2,95 @@ use crate::report::{hybrid::NonAsciiStringError, KeyIdentifier}; const DOMAIN: &str = "private-attribution"; -#[derive(Debug)] -pub struct HybridImpressionInfo<'a> { +#[derive(Clone, Debug)] +pub struct HybridImpressionInfo { pub key_id: KeyIdentifier, - pub helper_origin: &'a str, + pub helper_origin: &'static str, } -#[allow(dead_code)] +impl HybridImpressionInfo { + /// Creates a new instance. + /// + /// ## Errors + /// if helper or site origin is not a valid ASCII string. + pub fn new( + key_id: KeyIdentifier, + helper_origin: &'static str, + ) -> Result { + // If the types of errors returned from this function change, then the validation in + // `EncryptedReport::from_bytes` may need to change as well. + if !helper_origin.is_ascii() { + return Err(helper_origin.into()); + } + + Ok(Self { + key_id, + helper_origin, + }) + } + + // Converts this instance into an owned byte slice that can further be used to create HPKE + // sender or receiver context. + pub(super) fn to_bytes(&self) -> Box<[u8]> { + let info_len = DOMAIN.len() + + self.helper_origin.len() + + 2 // delimiters(?) + + std::mem::size_of_val(&self.key_id); + let mut r = Vec::with_capacity(info_len); + + r.extend_from_slice(DOMAIN.as_bytes()); + r.push(0); + r.extend_from_slice(self.helper_origin.as_bytes()); + r.push(0); + + r.push(self.key_id); + + debug_assert_eq!(r.len(), info_len, "HPKE Info length estimation is incorrect and leads to extra allocation or wasted memory"); + + r.into_boxed_slice() + } +} + +#[derive(Clone, Debug)] pub struct HybridConversionInfo<'a> { pub key_id: KeyIdentifier, - pub helper_origin: &'a str, - pub converion_site_domain: &'a str, + pub helper_origin: &'static str, + pub conversion_site_domain: &'a str, pub timestamp: u64, pub epsilon: f64, pub sensitivity: f64, } -#[allow(dead_code)] -pub enum HybridInfo<'a> { - Impression(HybridImpressionInfo<'a>), - Conversion(HybridConversionInfo<'a>), -} - -impl<'a> HybridImpressionInfo<'a> { +impl<'a> HybridConversionInfo<'a> { /// Creates a new instance. /// /// ## Errors /// if helper or site origin is not a valid ASCII string. - pub fn new(key_id: KeyIdentifier, helper_origin: &'a str) -> Result { + pub fn new( + key_id: KeyIdentifier, + helper_origin: &'static str, + conversion_site_domain: &'a str, + timestamp: u64, + epsilon: f64, + sensitivity: f64, + ) -> Result { // If the types of errors returned from this function change, then the validation in // `EncryptedReport::from_bytes` may need to change as well. if !helper_origin.is_ascii() { return Err(helper_origin.into()); } + if !conversion_site_domain.is_ascii() { + return Err(conversion_site_domain.into()); + } + Ok(Self { key_id, helper_origin, + conversion_site_domain, + timestamp, + epsilon, + sensitivity, }) } @@ -47,19 +99,62 @@ impl<'a> HybridImpressionInfo<'a> { pub(super) fn to_bytes(&self) -> Box<[u8]> { let info_len = DOMAIN.len() + self.helper_origin.len() - + 2 // delimiters(?) - + std::mem::size_of_val(&self.key_id); + + self.conversion_site_domain.len() + + 3 // delimiters + + std::mem::size_of_val(&self.key_id) + + std::mem::size_of_val(&self.timestamp) + + std::mem::size_of_val(&self.epsilon) + + std::mem::size_of_val(&self.sensitivity); let mut r = Vec::with_capacity(info_len); r.extend_from_slice(DOMAIN.as_bytes()); r.push(0); r.extend_from_slice(self.helper_origin.as_bytes()); r.push(0); + r.extend_from_slice(self.conversion_site_domain.as_bytes()); + r.push(0); r.push(self.key_id); + r.extend_from_slice(&self.timestamp.to_be_bytes()); + r.extend_from_slice(&self.epsilon.to_be_bytes()); + r.extend_from_slice(&self.sensitivity.to_be_bytes()); debug_assert_eq!(r.len(), info_len, "HPKE Info length estimation is incorrect and leads to extra allocation or wasted memory"); r.into_boxed_slice() } } + +#[derive(Clone, Debug)] +pub struct HybridInfo<'a> { + pub impression: HybridImpressionInfo, + pub conversion: HybridConversionInfo<'a>, +} + +impl HybridInfo<'_> { + /// Creates a new instance. + /// ## Errors + /// if helper or site origin is not a valid ASCII string. + pub fn new( + key_id: KeyIdentifier, + helper_origin: &'static str, + conversion_site_domain: &'static str, + timestamp: u64, + epsilon: f64, + sensitivity: f64, + ) -> Result { + let impression = HybridImpressionInfo::new(key_id, helper_origin)?; + let conversion = HybridConversionInfo::new( + key_id, + helper_origin, + conversion_site_domain, + timestamp, + epsilon, + sensitivity, + )?; + Ok(Self { + impression, + conversion, + }) + } +} diff --git a/ipa-core/src/test_fixture/hybrid.rs b/ipa-core/src/test_fixture/hybrid.rs index 3b8cc2460..a28fa7232 100644 --- a/ipa-core/src/test_fixture/hybrid.rs +++ b/ipa-core/src/test_fixture/hybrid.rs @@ -1,8 +1,17 @@ -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + iter::zip, +}; use crate::{ - ff::{boolean_array::BooleanArray, U128Conversions}, - report::hybrid::IndistinguishableHybridReport, + ff::{ + boolean_array::{BooleanArray, BA64}, + U128Conversions, + }, + rand::Rng, + report::hybrid::{ + HybridConversionReport, HybridImpressionReport, HybridReport, IndistinguishableHybridReport, + }, secret_sharing::{replicated::semi_honest::AdditiveShare as Replicated, IntoShares}, test_fixture::sharing::Reconstruct, }; @@ -51,6 +60,54 @@ where } } +impl IntoShares> for TestHybridRecord +where + BK: BooleanArray + U128Conversions + IntoShares>, + V: BooleanArray + U128Conversions + IntoShares>, +{ + fn share_with(self, rng: &mut R) -> [HybridReport; 3] { + match self { + TestHybridRecord::TestImpression { + match_key, + breakdown_key, + } => { + let ba_match_key = BA64::try_from(u128::from(match_key)) + .unwrap() + .share_with(rng); + let ba_breakdown_key = BK::try_from(u128::from(breakdown_key)) + .unwrap() + .share_with(rng); + zip(ba_match_key, ba_breakdown_key) + .map(|(match_key_share, breakdown_key_share)| { + HybridReport::Impression::(HybridImpressionReport { + match_key: match_key_share, + breakdown_key: breakdown_key_share, + }) + }) + .collect::>() + .try_into() + .unwrap() + } + TestHybridRecord::TestConversion { match_key, value } => { + let ba_match_key = BA64::try_from(u128::from(match_key)) + .unwrap() + .share_with(rng); + let ba_value = V::try_from(u128::from(value)).unwrap().share_with(rng); + zip(ba_match_key, ba_value) + .map(|(match_key_share, value_share)| { + HybridReport::Conversion::(HybridConversionReport { + match_key: match_key_share, + value: value_share, + }) + }) + .collect::>() + .try_into() + .unwrap() + } + } + } +} + struct HashmapEntry { breakdown_key: u32, total_value: u32,