1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.cobalt.observations; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.annotation.NonNull; 22 23 import com.android.cobalt.data.EventRecordAndSystemProfile; 24 import com.android.cobalt.data.EventVector; 25 import com.android.cobalt.data.ObservationGenerator; 26 import com.android.cobalt.system.SystemData; 27 28 import com.google.cobalt.AggregateValue; 29 import com.google.cobalt.MetricDefinition; 30 import com.google.cobalt.Observation; 31 import com.google.cobalt.ObservationMetadata; 32 import com.google.cobalt.ObservationToEncrypt; 33 import com.google.cobalt.PrivateIndexObservation; 34 import com.google.cobalt.ReportDefinition; 35 import com.google.cobalt.ReportParticipationObservation; 36 import com.google.cobalt.SystemProfile; 37 import com.google.cobalt.UnencryptedObservationBatch; 38 import com.google.common.collect.ImmutableList; 39 import com.google.common.collect.ImmutableListMultimap; 40 41 import java.security.SecureRandom; 42 43 /** Generates private observations from event data and report privacy parameters. */ 44 final class PrivateObservationGenerator implements ObservationGenerator { 45 /** 46 * Interface to encode an aggregated value as a private index observation for private reports. 47 */ 48 interface Encoder { 49 /** 50 * Encodes one event and aggregated value as a single private observation. 51 * 52 * <p>Note, retuning a single private observation implies that report types that have 53 * multiple values in their {@link AggregateValue}, like histograms, aren't supported. 54 * 55 * @param eventVector the event vector to encode 56 * @param aggregateValue the aggregated value to encode 57 * @return the privacy encoded observation 58 */ encode(EventVector eventVector, AggregateValue aggregateValue)59 PrivateIndexObservation encode(EventVector eventVector, AggregateValue aggregateValue); 60 } 61 62 private final SystemData mSystemData; 63 private final PrivacyGenerator mPrivacyGenerator; 64 private final SecureRandom mSecureRandom; 65 private final Encoder mEncoder; 66 private final int mCustomerId; 67 private final int mProjectId; 68 private final MetricDefinition mMetric; 69 private final ReportDefinition mReport; 70 PrivateObservationGenerator( @onNull SystemData systemData, @NonNull PrivacyGenerator privacyGenerator, @NonNull SecureRandom secureRandom, @NonNull Encoder encoder, int customerId, int projectId, @NonNull MetricDefinition metric, @NonNull ReportDefinition report)71 PrivateObservationGenerator( 72 @NonNull SystemData systemData, 73 @NonNull PrivacyGenerator privacyGenerator, 74 @NonNull SecureRandom secureRandom, 75 @NonNull Encoder encoder, 76 int customerId, 77 int projectId, 78 @NonNull MetricDefinition metric, 79 @NonNull ReportDefinition report) { 80 this.mSystemData = requireNonNull(systemData); 81 this.mPrivacyGenerator = requireNonNull(privacyGenerator); 82 this.mSecureRandom = requireNonNull(secureRandom); 83 this.mEncoder = requireNonNull(encoder); 84 this.mCustomerId = customerId; 85 this.mProjectId = projectId; 86 this.mMetric = requireNonNull(metric); 87 this.mReport = requireNonNull(report); 88 } 89 90 /** 91 * Generate the private observations that for a report and day. 92 * 93 * @param dayIndex the day index to generate observations for 94 * @param allEventData the data for events that occurred that are relevant to the day and Report 95 * @return the observations to store in the DB for later sending, contained in 96 * UnencryptedObservationBatches with their metadata 97 */ 98 @Override generateObservations( int dayIndex, ImmutableListMultimap<SystemProfile, EventRecordAndSystemProfile> allEventData)99 public ImmutableList<UnencryptedObservationBatch> generateObservations( 100 int dayIndex, 101 ImmutableListMultimap<SystemProfile, EventRecordAndSystemProfile> allEventData) { 102 if (allEventData.isEmpty()) { 103 return ImmutableList.of( 104 generateObservations( 105 dayIndex, 106 // Use the current system profile since none is provided. 107 mSystemData.filteredSystemProfile(mReport), 108 ImmutableList.of())); 109 } 110 111 ImmutableList.Builder<UnencryptedObservationBatch> batches = ImmutableList.builder(); 112 for (SystemProfile systemProfile : allEventData.keySet()) { 113 batches.add( 114 generateObservations(dayIndex, systemProfile, allEventData.get(systemProfile))); 115 } 116 117 return batches.build(); 118 } 119 120 /** 121 * Generate an observation batch from events for a report, day, and system profile. 122 * 123 * @param dayIndex the day observations are being generated for 124 * @param systemProfile the system profile of the observations 125 * @param events the events 126 * @return an UnencryptedObservation batch holding the generated observations 127 */ generateObservations( int dayIndex, SystemProfile systemProfile, ImmutableList<EventRecordAndSystemProfile> events)128 private UnencryptedObservationBatch generateObservations( 129 int dayIndex, 130 SystemProfile systemProfile, 131 ImmutableList<EventRecordAndSystemProfile> events) { 132 if (mReport.getEventVectorBufferMax() != 0 133 && events.size() > mReport.getEventVectorBufferMax()) { 134 // Each EventRecordAndSystemProfile contains a unique event vector for the system 135 // profile and day so the number of events can be compared to the event vector 136 // buffer max of the report. 137 events = events.subList(0, (int) mReport.getEventVectorBufferMax()); 138 } 139 140 ImmutableList.Builder<Observation> observations = ImmutableList.builder(); 141 for (EventRecordAndSystemProfile event : events) { 142 observations.add( 143 Observation.newBuilder() 144 .setPrivateIndex( 145 mEncoder.encode(event.eventVector(), event.aggregateValue())) 146 .setRandomId(RandomId.generate(mSecureRandom)) 147 .build()); 148 } 149 for (PrivateIndexObservation privateIndex : 150 mPrivacyGenerator.generateNoise(maxIndexForReport(), mReport)) { 151 observations.add( 152 Observation.newBuilder() 153 .setPrivateIndex(privateIndex) 154 .setRandomId(RandomId.generate(mSecureRandom)) 155 .build()); 156 } 157 observations.add( 158 Observation.newBuilder() 159 .setReportParticipation(ReportParticipationObservation.getDefaultInstance()) 160 .setRandomId(RandomId.generate(mSecureRandom)) 161 .build()); 162 163 ImmutableList.Builder<ObservationToEncrypt> toEncrypt = ImmutableList.builder(); 164 boolean setContributionId = true; 165 for (Observation observation : observations.build()) { 166 ObservationToEncrypt.Builder builder = ObservationToEncrypt.newBuilder(); 167 builder.setObservation(observation); 168 if (setContributionId) { 169 builder.setContributionId(RandomId.generate(mSecureRandom)); 170 } 171 172 // Reports with privacy enabled split a single contribution across multiple 173 // observations, both private and participation. However, only 1 needs the contribution 174 // id set. 175 toEncrypt.add(builder.build()); 176 setContributionId = false; 177 } 178 179 return UnencryptedObservationBatch.newBuilder() 180 .setMetadata( 181 ObservationMetadata.newBuilder() 182 .setCustomerId(mCustomerId) 183 .setProjectId(mProjectId) 184 .setMetricId(mMetric.getId()) 185 .setReportId(mReport.getId()) 186 .setDayIndex(dayIndex) 187 .setSystemProfile(systemProfile)) 188 .addAllUnencryptedObservations(toEncrypt.build()) 189 .build(); 190 } 191 maxIndexForReport()192 private int maxIndexForReport() { 193 return PrivateIndexCalculations.getNumEventVectors(mMetric.getMetricDimensionsList()) 194 * mReport.getNumIndexPoints() 195 - 1; 196 } 197 } 198