1 /*
2  * Copyright (C) 2017 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 android.privacy;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertTrue;
22 
23 import android.privacy.internal.rappor.RapporConfig;
24 import android.privacy.internal.rappor.RapporEncoder;
25 
26 import androidx.test.filters.SmallTest;
27 import androidx.test.runner.AndroidJUnit4;
28 
29 import org.junit.Test;
30 import org.junit.runner.RunWith;
31 
32 import java.nio.ByteBuffer;
33 import java.nio.ByteOrder;
34 import java.nio.charset.StandardCharsets;
35 import java.security.MessageDigest;
36 
37 /**
38  * Unit test for the {@link RapporEncoder}.
39  * Most of the tests are done in external/rappor/client/javatest/ already.
40  * Tests here are just make sure the {@link RapporEncoder} wrap Rappor correctly.
41  */
42 @RunWith(AndroidJUnit4.class)
43 @SmallTest
44 public class RapporEncoderTest {
45 
46     @Test
testRapporEncoder_config()47     public void testRapporEncoder_config() throws Exception {
48         final RapporConfig config = new RapporConfig(
49                 "Foo",  // encoderId
50                 8,  // numBits,
51                 13.0 / 128.0,  // probabilityF
52                 0.25,  // probabilityP
53                 0.75,  // probabilityQ
54                 1,  // numCohorts
55                 2);  // numBloomHashes)
56         final RapporEncoder encoder = RapporEncoder.createEncoder(config,
57                 makeTestingUserSecret("encoder1"));
58         assertEquals("Rappor", encoder.getConfig().getAlgorithm());
59         assertEquals("EncoderId: Foo, NumBits: 8, ProbabilityF: 0.102, "
60                 + "ProbabilityP: 0.250, ProbabilityQ: 0.750, NumCohorts: 1, "
61                 + "NumBloomHashes: 2", encoder.getConfig().toString());
62     }
63 
64     @Test
testRapporEncoder_basicIRRTest()65     public void testRapporEncoder_basicIRRTest() throws Exception {
66         final RapporConfig config = new RapporConfig(
67                 "Foo", // encoderId
68                 12, // numBits,
69                 0, // probabilityF
70                 0, // probabilityP
71                 1, // probabilityQ
72                 1, // numCohorts (so must be cohort 0)
73                 2);  // numBloomHashes
74         // Use insecure encoder here as we want to get the exact output.
75         final RapporEncoder encoder = RapporEncoder.createInsecureEncoderForTest(config);
76         assertEquals(768, toLong(encoder.encodeString("Testing")));
77     }
78 
79     @Test
testRapporEncoder_IRRWithPRR()80     public void testRapporEncoder_IRRWithPRR() throws Exception {
81         int numBits = 8;
82         final long inputValue = 254L;
83         final long expectedPrrValue = 126L;
84         final long expectedPrrAndIrrValue = 79L;
85 
86         final RapporConfig config1 = new RapporConfig(
87                 "Foo2", // encoderId
88                 numBits, // numBits,
89                 0.25, // probabilityF
90                 0, // probabilityP
91                 1, // probabilityQ
92                 1, // numCohorts
93                 2); // numBloomHashes
94         // Use insecure encoder here as we want to get the exact output.
95         final RapporEncoder encoder1 = RapporEncoder.createInsecureEncoderForTest(config1);
96         // Verify that PRR is working as expected.
97         assertEquals(expectedPrrValue, toLong(encoder1.encodeBits(toBytes(inputValue))));
98         assertTrue(encoder1.isInsecureEncoderForTest());
99 
100         // Verify that IRR is working as expected.
101         final RapporConfig config2 = new RapporConfig(
102                 "Foo2", // encoderId
103                 numBits, // numBits,
104                 0, // probabilityF
105                 0.3, // probabilityP
106                 0.7, // probabilityQ
107                 1, // numCohorts
108                 2); // numBloomHashes
109         // Use insecure encoder here as we want to get the exact output.
110         final RapporEncoder encoder2 = RapporEncoder.createInsecureEncoderForTest(config2);
111         assertEquals(expectedPrrAndIrrValue,
112                 toLong(encoder2.encodeBits(toBytes(expectedPrrValue))));
113 
114         // Test that end-to-end is the result of PRR + IRR.
115         final RapporConfig config3 = new RapporConfig(
116                 "Foo2", // encoderId
117                 numBits, // numBits,
118                 0.25, // probabilityF
119                 0.3, // probabilityP
120                 0.7, // probabilityQ
121                 1, // numCohorts
122                 2); // numBloomHashes
123         final RapporEncoder encoder3 = RapporEncoder.createInsecureEncoderForTest(config3);
124         // Verify that PRR is working as expected.
125         assertEquals(expectedPrrAndIrrValue, toLong(encoder3.encodeBits(toBytes(inputValue))));
126     }
127 
128     @Test
testRapporEncoder_ensureSecureEncoderIsSecure()129     public void testRapporEncoder_ensureSecureEncoderIsSecure() throws Exception {
130         int numBits = 8;
131         final long inputValue = 254L;
132         final long prrValue = 250L;
133         final long prrAndIrrValue = 184L;
134 
135         final RapporConfig config1 = new RapporConfig(
136                 "Foo", // encoderId
137                 numBits, // numBits,
138                 0.25, // probabilityF
139                 0, // probabilityP
140                 1, // probabilityQ
141                 1, // numCohorts
142                 2); // numBloomHashes
143         final RapporEncoder encoder1 = RapporEncoder.createEncoder(config1,
144                 makeTestingUserSecret("secret1"));
145         // Verify that PRR is working as expected, not affected by random seed.
146         assertEquals(prrValue, toLong(encoder1.encodeBits(toBytes(inputValue))));
147         assertFalse(encoder1.isInsecureEncoderForTest());
148 
149         boolean hasDifferentResult2 = false;
150         for (int i = 0; i < 5; i++) {
151             final RapporConfig config2 = new RapporConfig(
152                     "Foo", // encoderId
153                     numBits, // numBits,
154                     0, // probabilityF
155                     0.3, // probabilityP
156                     0.7, // probabilityQ
157                     1, // numCohorts
158                     2); // numBloomHashes
159             final RapporEncoder encoder2 = RapporEncoder.createEncoder(config2,
160                     makeTestingUserSecret("secret1"));
161             hasDifferentResult2 |= (prrAndIrrValue != toLong(
162                     encoder2.encodeBits(toBytes(prrValue))));
163         }
164         // Ensure it's not getting same result as it has random seed while encoder id and secret
165         // is the same.
166         assertTrue(hasDifferentResult2);
167 
168         boolean hasDifferentResults3 = false;
169         for (int i = 0; i < 5; i++) {
170             final RapporConfig config3 = new RapporConfig(
171                     "Foo", // encoderId
172                     numBits, // numBits,
173                     0.25, // probabilityF
174                     0.3, // probabilityP
175                     0.7, // probabilityQ
176                     1, // numCohorts
177                     2); // numBloomHashes
178             final RapporEncoder encoder3 = RapporEncoder.createEncoder(config3,
179                     makeTestingUserSecret("secret1"));
180             hasDifferentResults3 |= (prrAndIrrValue != toLong(
181                     encoder3.encodeBits(toBytes(inputValue))));
182         }
183         // Ensure it's not getting same result as it has random seed while encoder id and secret
184         // is the same.
185         assertTrue(hasDifferentResults3);
186     }
187 
toBytes(long value)188     private static byte[] toBytes(long value) {
189         return ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).array();
190     }
191 
toLong(byte[] bytes)192     private static long toLong(byte[] bytes) {
193         ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).put(bytes);
194         buffer.rewind();
195         return buffer.getLong();
196     }
197 
makeTestingUserSecret(String testingSecret)198     private static byte[] makeTestingUserSecret(String testingSecret) throws Exception {
199         // We generate the fake user secret by concatenating three copies of the
200         // 16 byte MD5 hash of the testingSecret string encoded in UTF 8.
201         MessageDigest md5 = MessageDigest.getInstance("MD5");
202         byte[] digest = md5.digest(testingSecret.getBytes(StandardCharsets.UTF_8));
203         assertEquals(16, digest.length);
204         return ByteBuffer.allocate(48).put(digest).put(digest).put(digest).array();
205     }
206 }
207