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