diff options
Diffstat (limited to 'toolkit/components/telemetry/dap/ffi')
-rw-r--r-- | toolkit/components/telemetry/dap/ffi/Cargo.toml | 13 | ||||
-rw-r--r-- | toolkit/components/telemetry/dap/ffi/cbindgen.toml | 11 | ||||
-rw-r--r-- | toolkit/components/telemetry/dap/ffi/src/lib.rs | 255 | ||||
-rw-r--r-- | toolkit/components/telemetry/dap/ffi/src/prg.rs | 93 | ||||
-rw-r--r-- | toolkit/components/telemetry/dap/ffi/src/types.rs | 338 |
5 files changed, 710 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/dap/ffi/Cargo.toml b/toolkit/components/telemetry/dap/ffi/Cargo.toml new file mode 100644 index 0000000000..412e98eb80 --- /dev/null +++ b/toolkit/components/telemetry/dap/ffi/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "dap_ffi" +version = "0.1.0" +edition = "2021" +authors = [ + "Simon Friedberger <simon@mozilla.com>", +] +license = "MPL-2.0" + +[dependencies] +prio = {version = "0.9.0", default-features = false } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +rand = "0.8" diff --git a/toolkit/components/telemetry/dap/ffi/cbindgen.toml b/toolkit/components/telemetry/dap/ffi/cbindgen.toml new file mode 100644 index 0000000000..12b3a58a1a --- /dev/null +++ b/toolkit/components/telemetry/dap/ffi/cbindgen.toml @@ -0,0 +1,11 @@ +header = """/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */ +#ifndef DAPTelemetryBindings_h +#error "Don't include this file directly, instead include DAPTelemetryBindings.h" +#endif +""" + +[export.rename] +"ThinVec" = "nsTArray"
\ No newline at end of file diff --git a/toolkit/components/telemetry/dap/ffi/src/lib.rs b/toolkit/components/telemetry/dap/ffi/src/lib.rs new file mode 100644 index 0000000000..6e5601d743 --- /dev/null +++ b/toolkit/components/telemetry/dap/ffi/src/lib.rs @@ -0,0 +1,255 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::io::Cursor; + +use thin_vec::ThinVec; + +pub mod types; +use types::HpkeConfig; +use types::Report; +use types::ReportID; +use types::ReportMetadata; +use types::TaskID; +use types::Time; + +pub mod prg; +use prg::PrgAes128Alt; + +use prio::codec::Encode; +use prio::codec::{encode_u32_items, Decode}; +use prio::field::Field128; +use prio::flp::gadgets::{BlindPolyEval, ParallelSum}; +use prio::flp::types::{CountVec, Sum}; +use prio::vdaf::prio3::Prio3; +use prio::vdaf::Client; +use prio::vdaf::VdafError; + +use crate::types::HpkeCiphertext; + +type Prio3Aes128SumAlt = Prio3<Sum<Field128>, PrgAes128Alt, 16>; +type Prio3Aes128CountVecAlt = + Prio3<CountVec<Field128, ParallelSum<Field128, BlindPolyEval<Field128>>>, PrgAes128Alt, 16>; + +extern "C" { + pub fn dapHpkeEncryptOneshot( + aKey: *const u8, + aKeyLength: u32, + aInfo: *const u8, + aInfoLength: u32, + aAad: *const u8, + aAadLength: u32, + aPlaintext: *const u8, + aPlaintextLength: u32, + aOutputEncapsulatedKey: &mut ThinVec<u8>, + aOutputShare: &mut ThinVec<u8>, + ) -> bool; +} + +pub fn new_prio_u8(num_aggregators: u8, bits: u32) -> Result<Prio3Aes128SumAlt, VdafError> { + if bits > 64 { + return Err(VdafError::Uncategorized(format!( + "bit length ({}) exceeds limit for aggregate type (64)", + bits + ))); + } + + Prio3::new(num_aggregators, Sum::new(bits as usize)?) +} + +pub fn new_prio_vecu16( + num_aggregators: u8, + len: usize, +) -> Result<Prio3Aes128CountVecAlt, VdafError> { + Prio3::new(num_aggregators, CountVec::new(len)) +} + +enum Role { + Leader = 2, + Helper = 3, +} + +/// A minimal wrapper around the FFI function which mostly just converts datatypes. +fn hpke_encrypt_wrapper( + plain_share: &Vec<u8>, + aad: &Vec<u8>, + info: &Vec<u8>, + hpke_config: &HpkeConfig, +) -> Result<HpkeCiphertext, Box<dyn std::error::Error>> { + let mut encrypted_share = ThinVec::<u8>::new(); + let mut encapsulated_key = ThinVec::<u8>::new(); + unsafe { + if !dapHpkeEncryptOneshot( + hpke_config.public_key.as_ptr(), + hpke_config.public_key.len() as u32, + info.as_ptr(), + info.len() as u32, + aad.as_ptr(), + aad.len() as u32, + plain_share.as_ptr(), + plain_share.len() as u32, + &mut encapsulated_key, + &mut encrypted_share, + ) { + return Err(Box::from("Encryption failed.")); + } + } + + Ok(HpkeCiphertext { + config_id: hpke_config.id, + enc: encapsulated_key.to_vec(), + payload: encrypted_share.to_vec(), + }) +} + +trait Shardable { + fn shard(&self) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>>; +} + +impl Shardable for ThinVec<u16> { + fn shard(&self) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>> { + let prio = new_prio_vecu16(2, self.len())?; + + let measurement: Vec<u128> = self.iter().map(|e| (*e as u128)).collect(); + let (public_share, input_shares) = prio.shard(&measurement)?; + + debug_assert_eq!(input_shares.len(), 2); + debug_assert_eq!(public_share, ()); + + let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect(); + Ok(encoded_input_shares) + } +} +impl Shardable for u8 { + fn shard(&self) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>> { + let prio = new_prio_u8(2, 2)?; + + let (public_share, input_shares) = prio.shard(&(*self as u128))?; + + debug_assert_eq!(input_shares.len(), 2); + debug_assert_eq!(public_share, ()); + + let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect(); + Ok(encoded_input_shares) + } +} + +/// Pre-fill the info part of the HPKE sealing with the constants from the standard. +fn make_base_info() -> Vec<u8> { + let mut info = Vec::<u8>::new(); + const START: &[u8] = "dap-02 input share".as_bytes(); + info.extend(START); + const FIXED: u8 = 1; + info.push(FIXED); + + info +} + +/// This function creates a full report - ready to send - for a measurement. +/// +/// To do that it also needs the HPKE configurations for the endpoints and some +/// additional data which is part of the authentication. +fn get_dap_report_internal<T: Shardable>( + leader_hpke_config_encoded: &ThinVec<u8>, + helper_hpke_config_encoded: &ThinVec<u8>, + measurement: &T, + task_id: &[u8; 32], + time_precision: u64, +) -> Result<Report, Box<dyn std::error::Error>> { + let leader_hpke_config = HpkeConfig::decode(&mut Cursor::new(leader_hpke_config_encoded))?; + let helper_hpke_config = HpkeConfig::decode(&mut Cursor::new(helper_hpke_config_encoded))?; + + let encoded_input_shares = measurement.shard()?; + let public_share = Vec::new(); // the encoding wants an empty vector not () + + let metadata = ReportMetadata { + report_id: ReportID::generate(), + time: Time::generate(time_precision), + extensions: vec![], + }; + + // This quote from the standard describes which info and aad to use for the encryption: + // enc, payload = SealBase(pk, + // "dap-02 input share" || 0x01 || server_role, + // task_id || metadata || public_share, input_share) + // https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-request + let mut info = make_base_info(); + + let mut aad = Vec::from(*task_id); + metadata.encode(&mut aad); + encode_u32_items(&mut aad, &(), &public_share); + + info.push(Role::Leader as u8); + + let leader_payload = + hpke_encrypt_wrapper(&encoded_input_shares[0], &aad, &info, &leader_hpke_config)?; + + *info.last_mut().unwrap() = Role::Helper as u8; + + let helper_payload = + hpke_encrypt_wrapper(&encoded_input_shares[1], &aad, &info, &helper_hpke_config)?; + + Ok(Report { + task_id: TaskID(*task_id), + metadata, + public_share, + encrypted_input_shares: vec![leader_payload, helper_payload], + }) +} + +/// Wraps the function above with minor C interop. +/// Mostly it turns any error result into a return value of false. +#[no_mangle] +pub extern "C" fn dapGetReportU8( + leader_hpke_config_encoded: &ThinVec<u8>, + helper_hpke_config_encoded: &ThinVec<u8>, + measurement: u8, + task_id: &ThinVec<u8>, + time_precision: u64, + out_report: &mut ThinVec<u8>, +) -> bool { + assert_eq!(task_id.len(), 32); + + if let Ok(report) = get_dap_report_internal::<u8>( + leader_hpke_config_encoded, + helper_hpke_config_encoded, + &measurement, + &task_id.as_slice().try_into().unwrap(), + time_precision, + ) { + let encoded_report = report.get_encoded(); + out_report.extend(encoded_report); + + true + } else { + false + } +} + +#[no_mangle] +pub extern "C" fn dapGetReportVecU16( + leader_hpke_config_encoded: &ThinVec<u8>, + helper_hpke_config_encoded: &ThinVec<u8>, + measurement: &ThinVec<u16>, + task_id: &ThinVec<u8>, + time_precision: u64, + out_report: &mut ThinVec<u8>, +) -> bool { + assert_eq!(task_id.len(), 32); + + if let Ok(report) = get_dap_report_internal::<ThinVec<u16>>( + leader_hpke_config_encoded, + helper_hpke_config_encoded, + measurement, + &task_id.as_slice().try_into().unwrap(), + time_precision, + ) { + let encoded_report = report.get_encoded(); + out_report.extend(encoded_report); + + true + } else { + false + } +} diff --git a/toolkit/components/telemetry/dap/ffi/src/prg.rs b/toolkit/components/telemetry/dap/ffi/src/prg.rs new file mode 100644 index 0000000000..a7ebeb11cb --- /dev/null +++ b/toolkit/components/telemetry/dap/ffi/src/prg.rs @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::ffi::c_void; + +use prio::vdaf::prg::{Prg, SeedStream}; + +extern "C" { + pub fn dapStartCmac(aSeed: *mut u8) -> *mut c_void; + pub fn dapUpdateCmac(aContext: *mut c_void, aData: *const u8, aDataLen: u32); + pub fn dapFinalizeCmac(aContext: *mut c_void, aMacOutput: *mut u8); + pub fn dapReleaseCmac(aContext: *mut c_void); + + pub fn dapStartAesCtr(aKey: *const u8) -> *mut c_void; + pub fn dapCtrFillBuffer(aContext: *mut c_void, aBuffer: *mut u8, aBufferSize: i32); + pub fn dapReleaseCtrCtx(aContext: *mut c_void); +} + +#[derive(Clone, Debug)] +pub struct PrgAes128Alt { + nss_context: *mut c_void, +} + +impl Prg<16> for PrgAes128Alt { + type SeedStream = SeedStreamAes128Alt; + + fn init(seed_bytes: &[u8; 16]) -> Self { + let mut my_seed_bytes = *seed_bytes; + let ctx = unsafe { dapStartCmac(my_seed_bytes.as_mut_ptr()) }; + assert!(!ctx.is_null()); + + Self { nss_context: ctx } + } + + fn update(&mut self, data: &[u8]) { + unsafe { + dapUpdateCmac( + self.nss_context, + data.as_ptr(), + u32::try_from(data.len()).unwrap(), + ); + } + } + + fn into_seed_stream(self) -> Self::SeedStream { + // finish the MAC and create a new random data stream using the result as key and 0 as IV for AES-CTR + let mut key = [0u8; 16]; + unsafe { + dapFinalizeCmac(self.nss_context, key.as_mut_ptr()); + } + + SeedStreamAes128Alt::new(&mut key, &[0; 16]) + } +} + +impl Drop for PrgAes128Alt { + fn drop(&mut self) { + unsafe { + dapReleaseCmac(self.nss_context); + } + } +} + +pub struct SeedStreamAes128Alt { + nss_context: *mut c_void, +} + +impl SeedStreamAes128Alt { + pub(crate) fn new(key: &mut [u8; 16], iv: &[u8; 16]) -> Self { + debug_assert_eq!(iv, &[0; 16]); + let ctx = unsafe { dapStartAesCtr(key.as_ptr()) }; + Self { nss_context: ctx } + } +} + +impl SeedStream for SeedStreamAes128Alt { + fn fill(&mut self, buf: &mut [u8]) { + unsafe { + dapCtrFillBuffer( + self.nss_context, + buf.as_mut_ptr(), + i32::try_from(buf.len()).unwrap(), + ); + } + } +} + +impl Drop for SeedStreamAes128Alt { + fn drop(&mut self) { + unsafe { dapReleaseCtrCtx(self.nss_context) }; + } +} diff --git a/toolkit/components/telemetry/dap/ffi/src/types.rs b/toolkit/components/telemetry/dap/ffi/src/types.rs new file mode 100644 index 0000000000..bfbf3264c0 --- /dev/null +++ b/toolkit/components/telemetry/dap/ffi/src/types.rs @@ -0,0 +1,338 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This file contains structs for use in the DAP protocol and implements TLS compatible +//! serialization/deserialization as required for the wire protocol. +//! +//! The current draft standard with the definition of these structs is available here: +//! https://github.com/ietf-wg-ppm/draft-ietf-ppm-dap +//! This code is based on version 02 of the standard available here: +//! https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html + +use prio::codec::{ + decode_u16_items, decode_u32_items, encode_u16_items, encode_u32_items, CodecError, Decode, + Encode, +}; +use std::io::{Cursor, Read}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use rand::Rng; + +/// opaque TaskId[32]; +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-task-configuration +#[derive(Debug, PartialEq, Eq)] +pub struct TaskID(pub [u8; 32]); + +impl Decode for TaskID { + fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> { + // this should probably be available in codec...? + let mut data: [u8; 32] = [0; 32]; + bytes.read_exact(&mut data)?; + Ok(TaskID(data)) + } +} + +impl Encode for TaskID { + fn encode(&self, bytes: &mut Vec<u8>) { + bytes.extend_from_slice(&self.0); + } +} + +/// Time uint64; +/// seconds elapsed since start of UNIX epoch +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-protocol-definition +#[derive(Debug, PartialEq, Eq)] +pub struct Time(pub u64); + +impl Decode for Time { + fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> { + Ok(Time(u64::decode(bytes)?)) + } +} + +impl Encode for Time { + fn encode(&self, bytes: &mut Vec<u8>) { + u64::encode(&self.0, bytes); + } +} + +impl Time { + /// Generates a Time for the current system time rounded to the desired precision. + pub fn generate(time_precision: u64) -> Time { + let now_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Failed to get time.") + .as_secs(); + let timestamp = (now_secs / time_precision) * time_precision; + Time(timestamp) + } +} + +/// struct { +/// ExtensionType extension_type; +/// opaque extension_data<0..2^16-1>; +/// } Extension; +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-extensions +#[derive(Debug, PartialEq)] +pub struct Extension { + extension_type: ExtensionType, + extension_data: Vec<u8>, +} + +impl Decode for Extension { + fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> { + let extension_type = ExtensionType::from_u16(u16::decode(bytes)?); + let extension_data: Vec<u8> = decode_u16_items(&(), bytes)?; + + Ok(Extension { + extension_type, + extension_data, + }) + } +} + +impl Encode for Extension { + fn encode(&self, bytes: &mut Vec<u8>) { + (self.extension_type as u16).encode(bytes); + encode_u16_items(bytes, &(), &self.extension_data); + } +} + +/// enum { +/// TBD(0), +/// (65535) +/// } ExtensionType; +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-extensions +#[derive(Debug, PartialEq, Clone, Copy)] +#[repr(u16)] +enum ExtensionType { + Tbd = 0, +} + +impl ExtensionType { + fn from_u16(value: u16) -> ExtensionType { + match value { + 0 => ExtensionType::Tbd, + _ => panic!("Unknown value for Extension Type: {}", value), + } + } +} + +/// Identifier for a server's HPKE configuration +/// uint8 HpkeConfigId; +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-protocol-definition +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct HpkeConfigId(u8); + +impl Decode for HpkeConfigId { + fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> { + Ok(HpkeConfigId(u8::decode(bytes)?)) + } +} + +impl Encode for HpkeConfigId { + fn encode(&self, bytes: &mut Vec<u8>) { + self.0.encode(bytes); + } +} + +/// struct { +/// HpkeConfigId id; +/// HpkeKemId kem_id; +/// HpkeKdfId kdf_id; +/// HpkeAeadId aead_id; +/// HpkePublicKey public_key; +/// } HpkeConfig; +/// opaque HpkePublicKey<1..2^16-1>; +/// uint16 HpkeAeadId; /* Defined in [HPKE] */ +/// uint16 HpkeKemId; /* Defined in [HPKE] */ +/// uint16 HpkeKdfId; /* Defined in [HPKE] */ +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-hpke-configuration-request +#[derive(Debug)] +pub struct HpkeConfig { + pub id: HpkeConfigId, + pub kem_id: u16, + pub kdf_id: u16, + pub aead_id: u16, + pub public_key: Vec<u8>, +} + +impl Decode for HpkeConfig { + fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> { + Ok(HpkeConfig { + id: HpkeConfigId::decode(bytes)?, + kem_id: u16::decode(bytes)?, + kdf_id: u16::decode(bytes)?, + aead_id: u16::decode(bytes)?, + public_key: decode_u16_items(&(), bytes)?, + }) + } +} + +impl Encode for HpkeConfig { + fn encode(&self, bytes: &mut Vec<u8>) { + self.id.encode(bytes); + self.kem_id.encode(bytes); + self.kdf_id.encode(bytes); + self.aead_id.encode(bytes); + encode_u16_items(bytes, &(), &self.public_key); + } +} + +/// An HPKE ciphertext. +/// struct { +/// HpkeConfigId config_id; /* config ID */ +/// opaque enc<1..2^16-1>; /* encapsulated HPKE key */ +/// opaque payload<1..2^32-1>; /* ciphertext */ +/// } HpkeCiphertext; +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-protocol-definition +#[derive(Debug, PartialEq, Eq)] +pub struct HpkeCiphertext { + pub config_id: HpkeConfigId, + pub enc: Vec<u8>, + pub payload: Vec<u8>, +} + +impl Decode for HpkeCiphertext { + fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> { + let config_id = HpkeConfigId::decode(bytes)?; + let enc: Vec<u8> = decode_u16_items(&(), bytes)?; + let payload: Vec<u8> = decode_u32_items(&(), bytes)?; + + Ok(HpkeCiphertext { + config_id, + enc, + payload, + }) + } +} + +impl Encode for HpkeCiphertext { + fn encode(&self, bytes: &mut Vec<u8>) { + self.config_id.encode(bytes); + encode_u16_items(bytes, &(), &self.enc); + encode_u32_items(bytes, &(), &self.payload); + } +} + +/// uint8 ReportID[16]; +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-protocol-definition +#[derive(Debug, PartialEq, Eq)] +pub struct ReportID(pub [u8; 16]); + +impl Decode for ReportID { + fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> { + let mut data: [u8; 16] = [0; 16]; + bytes.read_exact(&mut data)?; + Ok(ReportID(data)) + } +} + +impl Encode for ReportID { + fn encode(&self, bytes: &mut Vec<u8>) { + bytes.extend_from_slice(&self.0); + } +} + +impl ReportID { + pub fn generate() -> ReportID { + ReportID(rand::thread_rng().gen()) + } +} + +/// struct { +/// ReportID report_id; +/// Time time; +/// Extension extensions<0..2^16-1>; +/// } ReportMetadata; +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-request +#[derive(Debug, PartialEq)] +pub struct ReportMetadata { + pub report_id: ReportID, + pub time: Time, + pub extensions: Vec<Extension>, +} + +impl Decode for ReportMetadata { + fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> { + let report_id = ReportID::decode(bytes)?; + let time = Time::decode(bytes)?; + let extensions = decode_u16_items(&(), bytes)?; + + Ok(ReportMetadata { + report_id, + time, + extensions, + }) + } +} + +impl Encode for ReportMetadata { + fn encode(&self, bytes: &mut Vec<u8>) { + self.report_id.encode(bytes); + self.time.encode(bytes); + encode_u16_items(bytes, &(), &self.extensions); + } +} + +/// struct { +/// TaskID task_id; +/// ReportMetadata metadata; +/// opaque public_share<0..2^32-1>; +/// HpkeCiphertext encrypted_input_shares<1..2^32-1>; +/// } Report; +/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-request +#[derive(Debug, PartialEq)] +pub struct Report { + pub task_id: TaskID, + pub metadata: ReportMetadata, + pub public_share: Vec<u8>, + pub encrypted_input_shares: Vec<HpkeCiphertext>, +} + +impl Report { + /// Creates a minimal report for use in tests. + pub fn new_dummy() -> Self { + Report { + task_id: TaskID([0x12; 32]), + metadata: ReportMetadata { + report_id: ReportID::generate(), + time: Time::generate(1), + extensions: vec![], + }, + public_share: vec![], + encrypted_input_shares: vec![], + } + } +} + +impl Decode for Report { + fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> { + let task_id = TaskID::decode(bytes)?; + let metadata = ReportMetadata::decode(bytes)?; + let public_share: Vec<u8> = decode_u32_items(&(), bytes)?; + let encrypted_input_shares: Vec<HpkeCiphertext> = decode_u32_items(&(), bytes)?; + + let remaining_bytes = bytes.get_ref().len() - (bytes.position() as usize); + if remaining_bytes == 0 { + Ok(Report { + task_id, + metadata, + public_share, + encrypted_input_shares, + }) + } else { + Err(CodecError::BytesLeftOver(remaining_bytes)) + } + } +} + +impl Encode for Report { + fn encode(&self, bytes: &mut Vec<u8>) { + self.task_id.encode(bytes); + self.metadata.encode(bytes); + encode_u32_items(bytes, &(), &self.public_share); + encode_u32_items(bytes, &(), &self.encrypted_input_shares); + } +} |