| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.privacy; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.privacy.internal.longitudinalreporting.LongitudinalReportingConfig; |
| import android.privacy.internal.longitudinalreporting.LongitudinalReportingEncoder; |
| |
| import android.support.test.filters.SmallTest; |
| import android.support.test.runner.AndroidJUnit4; |
| |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.charset.StandardCharsets; |
| import java.security.MessageDigest; |
| |
| /** |
| * Unit test for the {@link LongitudinalReportingEncoder}. |
| * |
| * As {@link LongitudinalReportingEncoder} is based on Rappor, |
| * most cases are covered by Rappor tests already. |
| */ |
| @RunWith(AndroidJUnit4.class) |
| @SmallTest |
| public class LongitudinalReportingEncoderTest { |
| |
| @Test |
| public void testLongitudinalReportingEncoder_config() throws Exception { |
| final LongitudinalReportingConfig config = new LongitudinalReportingConfig( |
| "Foo", // encoderId |
| 0.4, // probabilityF |
| 0.25, // probabilityP |
| 1); // probabilityQ |
| final LongitudinalReportingEncoder encoder = |
| LongitudinalReportingEncoder.createInsecureEncoderForTest( |
| config); |
| assertEquals("LongitudinalReporting", encoder.getConfig().getAlgorithm()); |
| assertEquals( |
| "EncoderId: Foo, ProbabilityF: 0.400, ProbabilityP: 0.250, ProbabilityQ: 1.000", |
| encoder.getConfig().toString()); |
| } |
| |
| @Test |
| public void testLongitudinalReportingEncoder_basicIRRTest() throws Exception { |
| // Test if IRR can generate expected result when seed is fixed (insecure encoder) |
| final LongitudinalReportingConfig config = new LongitudinalReportingConfig( |
| "Foo", // encoderId |
| 0.4, // probabilityF |
| 0, // probabilityP |
| 0); // probabilityQ |
| // Use insecure encoder here to make sure seed is set. |
| final LongitudinalReportingEncoder encoder = |
| LongitudinalReportingEncoder.createInsecureEncoderForTest( |
| config); |
| assertEquals(1, encoder.encodeBoolean(true)[0]); |
| assertEquals(0, encoder.encodeBoolean(true)[0]); |
| assertEquals(1, encoder.encodeBoolean(true)[0]); |
| assertEquals(1, encoder.encodeBoolean(true)[0]); |
| assertEquals(1, encoder.encodeBoolean(true)[0]); |
| assertEquals(1, encoder.encodeBoolean(true)[0]); |
| assertEquals(0, encoder.encodeBoolean(true)[0]); |
| assertEquals(1, encoder.encodeBoolean(true)[0]); |
| assertEquals(1, encoder.encodeBoolean(true)[0]); |
| assertEquals(1, encoder.encodeBoolean(true)[0]); |
| |
| assertEquals(0, encoder.encodeBoolean(false)[0]); |
| assertEquals(1, encoder.encodeBoolean(false)[0]); |
| assertEquals(1, encoder.encodeBoolean(false)[0]); |
| assertEquals(0, encoder.encodeBoolean(false)[0]); |
| assertEquals(0, encoder.encodeBoolean(false)[0]); |
| assertEquals(0, encoder.encodeBoolean(false)[0]); |
| assertEquals(1, encoder.encodeBoolean(false)[0]); |
| assertEquals(0, encoder.encodeBoolean(false)[0]); |
| assertEquals(0, encoder.encodeBoolean(false)[0]); |
| assertEquals(1, encoder.encodeBoolean(false)[0]); |
| |
| // Test if IRR returns original result when f = 0 |
| final LongitudinalReportingConfig config2 = new LongitudinalReportingConfig( |
| "Foo", // encoderId |
| 0, // probabilityF |
| 0, // probabilityP |
| 0); // probabilityQ |
| final LongitudinalReportingEncoder encoder2 |
| = LongitudinalReportingEncoder.createEncoder( |
| config2, makeTestingUserSecret("secret2")); |
| for (int i = 0; i < 10; i++) { |
| assertEquals(1, encoder2.encodeBoolean(true)[0]); |
| } |
| for (int i = 0; i < 10; i++) { |
| assertEquals(0, encoder2.encodeBoolean(false)[0]); |
| } |
| |
| // Test if IRR returns opposite result when f = 1 |
| final LongitudinalReportingConfig config3 = new LongitudinalReportingConfig( |
| "Foo", // encoderId |
| 1, // probabilityF |
| 0, // probabilityP |
| 0); // probabilityQ |
| final LongitudinalReportingEncoder encoder3 |
| = LongitudinalReportingEncoder.createEncoder( |
| config3, makeTestingUserSecret("secret3")); |
| for (int i = 0; i < 10; i++) { |
| assertEquals(1, encoder3.encodeBoolean(false)[0]); |
| } |
| for (int i = 0; i < 10; i++) { |
| assertEquals(0, encoder3.encodeBoolean(true)[0]); |
| } |
| } |
| |
| @Test |
| public void testLongitudinalReportingEncoder_basicPRRTest() throws Exception { |
| // Should always return original value when p = 0 |
| for (int i = 0; i < 10; i++) { |
| for (int j = 0; j < 10; j++) { |
| final LongitudinalReportingConfig config1 = new LongitudinalReportingConfig( |
| "Foo" + i, // encoderId |
| 0, // probabilityF |
| 0, // probabilityP |
| 0); // probabilityQ |
| final LongitudinalReportingEncoder encoder1 |
| = LongitudinalReportingEncoder.createEncoder( |
| config1, makeTestingUserSecret("encoder" + j)); |
| assertEquals(0, encoder1.encodeBoolean(false)[0]); |
| assertEquals(0, encoder1.encodeBoolean(false)[0]); |
| assertEquals(0, encoder1.encodeBoolean(false)[0]); |
| assertEquals(0, encoder1.encodeBoolean(false)[0]); |
| assertEquals(0, encoder1.encodeBoolean(false)[0]); |
| assertEquals(1, encoder1.encodeBoolean(true)[0]); |
| assertEquals(1, encoder1.encodeBoolean(true)[0]); |
| assertEquals(1, encoder1.encodeBoolean(true)[0]); |
| assertEquals(1, encoder1.encodeBoolean(true)[0]); |
| assertEquals(1, encoder1.encodeBoolean(true)[0]); |
| } |
| } |
| |
| // Should always return false when p = 1, q = 0 |
| for (int i = 0; i < 10; i++) { |
| for (int j = 0; j < 10; j++) { |
| final LongitudinalReportingConfig config2 = new LongitudinalReportingConfig( |
| "Foo" + i, // encoderId |
| 0, // probabilityF |
| 1, // probabilityP |
| 0); // probabilityQ |
| final LongitudinalReportingEncoder encoder2 |
| = LongitudinalReportingEncoder.createEncoder( |
| config2, makeTestingUserSecret("encoder" + j)); |
| assertEquals(0, encoder2.encodeBoolean(false)[0]); |
| assertEquals(0, encoder2.encodeBoolean(false)[0]); |
| assertEquals(0, encoder2.encodeBoolean(false)[0]); |
| assertEquals(0, encoder2.encodeBoolean(false)[0]); |
| assertEquals(0, encoder2.encodeBoolean(false)[0]); |
| assertEquals(0, encoder2.encodeBoolean(true)[0]); |
| assertEquals(0, encoder2.encodeBoolean(true)[0]); |
| assertEquals(0, encoder2.encodeBoolean(true)[0]); |
| assertEquals(0, encoder2.encodeBoolean(true)[0]); |
| assertEquals(0, encoder2.encodeBoolean(true)[0]); |
| } |
| } |
| |
| // Should always return true when p = 1, q = 1 |
| for (int i = 0; i < 10; i++) { |
| for (int j = 0; j < 10; j++) { |
| final LongitudinalReportingConfig config3 = new LongitudinalReportingConfig( |
| "Foo" + i, // encoderId |
| 0, // probabilityF |
| 1, // probabilityP |
| 1); // probabilityQ |
| final LongitudinalReportingEncoder encoder3 |
| = LongitudinalReportingEncoder.createEncoder( |
| config3, makeTestingUserSecret("encoder" + j)); |
| assertEquals(1, encoder3.encodeBoolean(false)[0]); |
| assertEquals(1, encoder3.encodeBoolean(false)[0]); |
| assertEquals(1, encoder3.encodeBoolean(false)[0]); |
| assertEquals(1, encoder3.encodeBoolean(false)[0]); |
| assertEquals(1, encoder3.encodeBoolean(false)[0]); |
| assertEquals(1, encoder3.encodeBoolean(true)[0]); |
| assertEquals(1, encoder3.encodeBoolean(true)[0]); |
| assertEquals(1, encoder3.encodeBoolean(true)[0]); |
| assertEquals(1, encoder3.encodeBoolean(true)[0]); |
| assertEquals(1, encoder3.encodeBoolean(true)[0]); |
| } |
| } |
| |
| // PRR should return different value when encoder id is changed |
| boolean hasFalseResult1 = false; |
| boolean hasTrueResult1 = false; |
| for (int i = 0; i < 50; i++) { |
| boolean firstResult = false; |
| for (int j = 0; j < 10; j++) { |
| final LongitudinalReportingConfig config4 = new LongitudinalReportingConfig( |
| "Foo" + i, // encoderId |
| 0, // probabilityF |
| 1, // probabilityP |
| 0.5); // probabilityQ |
| final LongitudinalReportingEncoder encoder4 |
| = LongitudinalReportingEncoder.createEncoder( |
| config4, makeTestingUserSecret("encoder4")); |
| boolean encodedFalse = encoder4.encodeBoolean(false)[0] > 0; |
| boolean encodedTrue = encoder4.encodeBoolean(true)[0] > 0; |
| // PRR should always give the same value when all parameters are the same |
| assertEquals(encodedTrue, encodedFalse); |
| if (j == 0) { |
| firstResult = encodedTrue; |
| } else { |
| assertEquals(firstResult, encodedTrue); |
| } |
| if (encodedTrue) { |
| hasTrueResult1 = true; |
| } else { |
| hasFalseResult1 = true; |
| } |
| } |
| } |
| // Ensure it has both true and false results when encoder id is different |
| assertTrue(hasTrueResult1); |
| assertTrue(hasFalseResult1); |
| |
| // PRR should give different value when secret is changed |
| boolean hasFalseResult2 = false; |
| boolean hasTrueResult2 = false; |
| for (int i = 0; i < 50; i++) { |
| boolean firstResult = false; |
| for (int j = 0; j < 10; j++) { |
| final LongitudinalReportingConfig config5 = new LongitudinalReportingConfig( |
| "Foo", // encoderId |
| 0, // probabilityF |
| 1, // probabilityP |
| 0.5); // probabilityQ |
| final LongitudinalReportingEncoder encoder5 |
| = LongitudinalReportingEncoder.createEncoder( |
| config5, makeTestingUserSecret("encoder" + i)); |
| boolean encodedFalse = encoder5.encodeBoolean(false)[0] > 0; |
| boolean encodedTrue = encoder5.encodeBoolean(true)[0] > 0; |
| // PRR should always give the same value when parameters are the same |
| assertEquals(encodedTrue, encodedFalse); |
| if (j == 0) { |
| firstResult = encodedTrue; |
| } else { |
| assertEquals(firstResult, encodedTrue); |
| } |
| if (encodedTrue) { |
| hasTrueResult2 = true; |
| } else { |
| hasFalseResult2 = true; |
| } |
| } |
| } |
| // Ensure it has both true and false results when encoder id is different |
| assertTrue(hasTrueResult2); |
| assertTrue(hasFalseResult2); |
| |
| // Confirm if PRR randomizer is working correctly |
| final int n1 = 1000; |
| final double p1 = 0.8; |
| final double expectedTrueSum1 = n1 * p1; |
| final double valueRange1 = 5 * Math.sqrt(n1 * p1 * (1 - p1)); |
| int trueSum1 = 0; |
| for (int i = 0; i < n1; i++) { |
| final LongitudinalReportingConfig config6 = new LongitudinalReportingConfig( |
| "Foo", // encoderId |
| 0, // probabilityF |
| p1, // probabilityP |
| 1); // probabilityQ |
| final LongitudinalReportingEncoder encoder6 |
| = LongitudinalReportingEncoder.createEncoder( |
| config6, makeTestingUserSecret("encoder" + i)); |
| boolean encodedFalse = encoder6.encodeBoolean(false)[0] > 0; |
| if (encodedFalse) { |
| trueSum1 += 1; |
| } |
| } |
| // Total number of true(s) should be around the mean (1000 * 0.8) |
| assertTrue(trueSum1 < expectedTrueSum1 + valueRange1); |
| assertTrue(trueSum1 > expectedTrueSum1 - valueRange1); |
| |
| // Confirm if PRR randomizer is working correctly |
| final int n2 = 1000; |
| final double p2 = 0.2; |
| final double expectedTrueSum2 = n2 * p2; |
| final double valueRange2 = 5 * Math.sqrt(n2 * p2 * (1 - p2)); |
| int trueSum2 = 0; |
| for (int i = 0; i < n2; i++) { |
| final LongitudinalReportingConfig config7 = new LongitudinalReportingConfig( |
| "Foo", // encoderId |
| 0, // probabilityF |
| p2, // probabilityP |
| 1); // probabilityQ |
| final LongitudinalReportingEncoder encoder7 |
| = LongitudinalReportingEncoder.createEncoder( |
| config7, makeTestingUserSecret("encoder" + i)); |
| boolean encodedFalse = encoder7.encodeBoolean(false)[0] > 0; |
| if (encodedFalse) { |
| trueSum2 += 1; |
| } |
| } |
| // Total number of true(s) should be around the mean (1000 * 0.2) |
| assertTrue(trueSum2 < expectedTrueSum2 + valueRange2); |
| assertTrue(trueSum2 > expectedTrueSum2 - valueRange2); |
| } |
| |
| @Test |
| public void testLongitudinalReportingEncoder_basicIRRwithPRRTest() throws Exception { |
| // Verify PRR result will run IRR |
| boolean hasFalseResult1 = false; |
| boolean hasTrueResult1 = false; |
| for (int i = 0; i < 50; i++) { |
| final LongitudinalReportingConfig config1 = new LongitudinalReportingConfig( |
| "Foo", // encoderId |
| 0.5, // probabilityF |
| 1, // probabilityP |
| 1); // probabilityQ |
| final LongitudinalReportingEncoder encoder1 |
| = LongitudinalReportingEncoder.createEncoder( |
| config1, makeTestingUserSecret("encoder1")); |
| if (encoder1.encodeBoolean(false)[0] > 0) { |
| hasTrueResult1 = true; |
| } else { |
| hasFalseResult1 = true; |
| } |
| } |
| assertTrue(hasTrueResult1); |
| assertTrue(hasFalseResult1); |
| |
| // When secret is different, some device should use PRR result, some should use IRR result |
| boolean hasFalseResult2 = false; |
| boolean hasTrueResult2 = false; |
| for (int i = 0; i < 50; i++) { |
| final LongitudinalReportingConfig config2 = new LongitudinalReportingConfig( |
| "Foo", // encoderId |
| 1, // probabilityF |
| 0.5, // probabilityP |
| 1); // probabilityQ |
| final LongitudinalReportingEncoder encoder2 |
| = LongitudinalReportingEncoder.createEncoder( |
| config2, makeTestingUserSecret("encoder" + i)); |
| if (encoder2.encodeBoolean(false)[0] > 0) { |
| hasTrueResult2 = true; |
| } else { |
| hasFalseResult2 = true; |
| } |
| } |
| assertTrue(hasTrueResult2); |
| assertTrue(hasFalseResult2); |
| } |
| |
| @Test |
| public void testLongTermRandomizedResult() throws Exception { |
| // Verify getLongTermRandomizedResult can return expected result when parameters are fixed. |
| final boolean[] expectedResult = |
| new boolean[]{true, false, true, true, true, |
| false, false, false, true, false, |
| false, false, false, true, true, |
| true, true, false, true, true, |
| true, true, false, true, true}; |
| for (int i = 0; i < 5; i++) { |
| for (int j = 0; j < 5; j++) { |
| boolean result = LongitudinalReportingEncoder.getLongTermRandomizedResult(0.5, |
| true, makeTestingUserSecret("secret" + i), "encoder" + j); |
| assertEquals(expectedResult[i * 5 + j], result); |
| } |
| } |
| } |
| |
| private static byte[] makeTestingUserSecret(String testingSecret) throws Exception { |
| // We generate the fake user secret by concatenating three copies of the |
| // 16 byte MD5 hash of the testingSecret string encoded in UTF 8. |
| MessageDigest md5 = MessageDigest.getInstance("MD5"); |
| byte[] digest = md5.digest(testingSecret.getBytes(StandardCharsets.UTF_8)); |
| assertEquals(16, digest.length); |
| return ByteBuffer.allocate(48).put(digest).put(digest).put(digest).array(); |
| } |
| } |