Merge "Add differential privacy library and algorithms"

This commit is contained in:
Ricky Wai
2017-12-19 10:59:00 +00:00
committed by Android (Google) Code Review
11 changed files with 1243 additions and 0 deletions

View File

@@ -670,6 +670,7 @@ java_library {
"libphonenumber-platform",
"nist-sip",
"tagsoup",
"rappor",
],
dxflags: ["--core-library"],
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 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;
/**
* An interface for differential privacy configuration.
* {@link DifferentialPrivacyEncoder} will apply this configuration to do differential privacy
* encoding.
*
* @hide
*/
public interface DifferentialPrivacyConfig {
/**
* Returns the name of the algorithm used in differential privacy config.
*
* @return The name of the algorithm
*/
String getAlgorithm();
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 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;
/**
* An interface for differential privacy encoder.
* Applications can use it to convert privacy sensitive data to privacy protected report.
* There is no decoder implemented in Android as it is not possible decode a single report by
* design.
*
* <p>Each type of log should have its own encoder, otherwise it may leak
* some information about Permanent Randomized Response(PRR, is used to create a “noisy”
* answer which is memoized by the client and permanently reused in place of the real answer).
*
* <p>Some encoders may not support all encoding methods, and it will throw {@link
* UnsupportedOperationException} if you call unsupported encoding method.
*
* <p><b>WARNING:</b> Privacy protection works only when encoder uses a suitable DP configuration,
* and the configuration and algorithm that is suitable is highly dependent on the use case.
* If the configuration is not suitable for the use case, it may hurt privacy or utility or both.
*
* @hide
*/
public interface DifferentialPrivacyEncoder {
/**
* Apply differential privacy to encode a string.
*
* @param original An arbitrary string
* @return Differential privacy encoded bytes derived from the string
*/
byte[] encodeString(String original);
/**
* Apply differential privacy to encode a boolean.
*
* @param original An arbitrary boolean.
* @return Differential privacy encoded bytes derived from the boolean
*/
byte[] encodeBoolean(boolean original);
/**
* Apply differential privacy to encode sequence of bytes.
*
* @param original An arbitrary byte array.
* @return Differential privacy encoded bytes derived from the bytes
*/
byte[] encodeBits(byte[] original);
/**
* Returns the configuration that this encoder is using.
*/
DifferentialPrivacyConfig getConfig();
/**
* Return True if the output from encoder is NOT securely randomized, otherwise encoder should
* be secure to randomize input.
*
* <b> A non-secure encoder is intended only for testing only and must not be used to process
* real data.
* </b>
*/
boolean isInsecureEncoderForTest();
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 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.internal.longitudinalreporting;
import android.privacy.DifferentialPrivacyConfig;
import android.privacy.internal.rappor.RapporConfig;
import android.text.TextUtils;
import com.android.internal.util.Preconditions;
/**
* A class to store {@link LongitudinalReportingEncoder} configuration.
*
* <ul>
* <li> f is probability to flip input value, used in IRR.
* <li> p is probability to override input value, used in PRR1.
* <li> q is probability to set input value as 1 when result of PRR(p) is true, used in PRR2.
* </ul>
*
* @hide
*/
public class LongitudinalReportingConfig implements DifferentialPrivacyConfig {
private static final String ALGORITHM_NAME = "LongitudinalReporting";
// Probability to flip input value.
private final double mProbabilityF;
// Probability to override original value.
private final double mProbabilityP;
// Probability to override value with 1.
private final double mProbabilityQ;
// IRR config to randomize original value
private final RapporConfig mIRRConfig;
private final String mEncoderId;
/**
* Constructor to create {@link LongitudinalReportingConfig} used for {@link
* LongitudinalReportingEncoder}
*
* @param encoderId Unique encoder id.
* @param probabilityF Probability F used in Longitudinal Reporting algorithm.
* @param probabilityP Probability P used in Longitudinal Reporting algorithm. This will be
* quantized to the nearest 1/256.
* @param probabilityQ Probability Q used in Longitudinal Reporting algorithm. This will be
* quantized to the nearest 1/256.
*/
public LongitudinalReportingConfig(String encoderId, double probabilityF,
double probabilityP, double probabilityQ) {
Preconditions.checkArgument(probabilityF >= 0 && probabilityF <= 1,
"probabilityF must be in range [0.0, 1.0]");
this.mProbabilityF = probabilityF;
Preconditions.checkArgument(probabilityP >= 0 && probabilityP <= 1,
"probabilityP must be in range [0.0, 1.0]");
this.mProbabilityP = probabilityP;
Preconditions.checkArgument(probabilityQ >= 0 && probabilityQ <= 1,
"probabilityQ must be in range [0.0, 1.0]");
this.mProbabilityQ = probabilityQ;
Preconditions.checkArgument(!TextUtils.isEmpty(encoderId), "encoderId cannot be empty");
mEncoderId = encoderId;
mIRRConfig = new RapporConfig(encoderId, 1, 0.0, probabilityF, 1 - probabilityF, 1, 1);
}
@Override
public String getAlgorithm() {
return ALGORITHM_NAME;
}
RapporConfig getIRRConfig() {
return mIRRConfig;
}
double getProbabilityP() {
return mProbabilityP;
}
double getProbabilityQ() {
return mProbabilityQ;
}
String getEncoderId() {
return mEncoderId;
}
@Override
public String toString() {
return String.format("EncoderId: %s, ProbabilityF: %.3f, ProbabilityP: %.3f"
+ ", ProbabilityQ: %.3f",
mEncoderId, mProbabilityF, mProbabilityP, mProbabilityQ);
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 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.internal.longitudinalreporting;
import android.privacy.DifferentialPrivacyEncoder;
import android.privacy.internal.rappor.RapporConfig;
import android.privacy.internal.rappor.RapporEncoder;
import com.android.internal.annotations.VisibleForTesting;
/**
* Differential privacy encoder by using Longitudinal Reporting algorithm.
*
* <b>
* Notes: It supports encodeBoolean() only for now.
* </b>
*
* <p>
* Definition:
* PRR = Permanent Randomized Response
* IRR = Instantaneous Randomized response
*
* Algorithm:
* Step 1: Create long-term secrets x(ignoreOriginalInput)=Ber(P), y=Ber(Q), where Ber denotes
* Bernoulli distribution on {0, 1}, and we use it as a long-term secret, we implement Ber(x) by
* using PRR(2x, 0) when x < 1/2, PRR(2(1-x), 1) when x >= 1/2.
* Step 2: If x is 0, report IRR(F, original), otherwise report IRR(F, y)
* </p>
*
* Reference: go/bit-reporting-with-longitudinal-privacy
* TODO: Add a public blog / site to explain how it works.
*
* @hide
*/
public class LongitudinalReportingEncoder implements DifferentialPrivacyEncoder {
// Suffix that will be added to Rappor's encoder id. There's a (relatively) small risk some
// other Rappor encoder may re-use the same encoder id.
private static final String PRR1_ENCODER_ID = "prr1_encoder_id";
private static final String PRR2_ENCODER_ID = "prr2_encoder_id";
private final LongitudinalReportingConfig mConfig;
// IRR encoder to encode input value.
private final RapporEncoder mIRREncoder;
// A value that used to replace original value as input, so there's always a chance we are
// doing IRR on a fake value not actual original value.
// Null if original value does not need to be replaced.
private final Boolean mFakeValue;
// True if encoder is securely randomized.
private final boolean mIsSecure;
/**
* Create {@link LongitudinalReportingEncoder} with
* {@link LongitudinalReportingConfig} provided.
*
* @param config Longitudinal Reporting parameters to encode input
* @param userSecret User generated secret that used to generate PRR
* @return {@link LongitudinalReportingEncoder} instance
*/
public static LongitudinalReportingEncoder createEncoder(LongitudinalReportingConfig config,
byte[] userSecret) {
return new LongitudinalReportingEncoder(config, true, userSecret);
}
/**
* Create <strong>insecure</strong> {@link LongitudinalReportingEncoder} with
* {@link LongitudinalReportingConfig} provided.
* Should not use it to process sensitive data.
*
* @param config Rappor parameters to encode input.
* @return {@link LongitudinalReportingEncoder} instance.
*/
@VisibleForTesting
public static LongitudinalReportingEncoder createInsecureEncoderForTest(
LongitudinalReportingConfig config) {
return new LongitudinalReportingEncoder(config, false, null);
}
private LongitudinalReportingEncoder(LongitudinalReportingConfig config,
boolean secureEncoder, byte[] userSecret) {
mConfig = config;
mIsSecure = secureEncoder;
final boolean ignoreOriginalInput = getLongTermRandomizedResult(config.getProbabilityP(),
secureEncoder, userSecret, config.getEncoderId() + PRR1_ENCODER_ID);
if (ignoreOriginalInput) {
mFakeValue = getLongTermRandomizedResult(config.getProbabilityQ(),
secureEncoder, userSecret, config.getEncoderId() + PRR2_ENCODER_ID);
} else {
// Not using fake value, so IRR will be processed on real input value.
mFakeValue = null;
}
final RapporConfig irrConfig = config.getIRRConfig();
mIRREncoder = secureEncoder
? RapporEncoder.createEncoder(irrConfig, userSecret)
: RapporEncoder.createInsecureEncoderForTest(irrConfig);
}
@Override
public byte[] encodeString(String original) {
throw new UnsupportedOperationException();
}
@Override
public byte[] encodeBoolean(boolean original) {
if (mFakeValue != null) {
// Use the fake value generated in PRR.
original = mFakeValue.booleanValue();
}
return mIRREncoder.encodeBoolean(original);
}
@Override
public byte[] encodeBits(byte[] bits) {
throw new UnsupportedOperationException();
}
@Override
public LongitudinalReportingConfig getConfig() {
return mConfig;
}
@Override
public boolean isInsecureEncoderForTest() {
return !mIsSecure;
}
/**
* Get PRR result that with probability p is 1, probability 1-p is 0.
*/
@VisibleForTesting
public static boolean getLongTermRandomizedResult(double p, boolean secureEncoder,
byte[] userSecret, String encoderId) {
// Use Rappor to get PRR result. Rappor's P and Q are set to 0 and 1 so IRR will not be
// effective.
// As Rappor has rapporF/2 chance returns 0, rapporF/2 chance returns 1, and 1-rapporF
// chance returns original input.
// If p < 0.5, setting rapporF=2p and input=0 will make Rappor has p chance to return 1
// P(output=1 | input=0) = rapporF/2 = 2p/2 = p.
// If p >= 0.5, setting rapporF=2(1-p) and input=1 will make Rappor has p chance
// to return 1.
// P(output=1 | input=1) = rapporF/2 + (1 - rapporF) = 2(1-p)/2 + (1 - 2(1-p)) = p.
final double effectiveF = p < 0.5f ? p * 2 : (1 - p) * 2;
final boolean prrInput = p < 0.5f ? false : true;
final RapporConfig prrConfig = new RapporConfig(encoderId, 1, effectiveF,
0, 1, 1, 1);
final RapporEncoder encoder = secureEncoder
? RapporEncoder.createEncoder(prrConfig, userSecret)
: RapporEncoder.createInsecureEncoderForTest(prrConfig);
return encoder.encodeBoolean(prrInput)[0] > 0;
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 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.internal.rappor;
import android.privacy.DifferentialPrivacyConfig;
import android.text.TextUtils;
import com.android.internal.util.Preconditions;
/**
* A class to store {@link RapporEncoder} config.
*
* @hide
*/
public class RapporConfig implements DifferentialPrivacyConfig {
private static final String ALGORITHM_NAME = "Rappor";
final String mEncoderId;
final int mNumBits;
final double mProbabilityF;
final double mProbabilityP;
final double mProbabilityQ;
final int mNumCohorts;
final int mNumBloomHashes;
/**
* Constructor for {@link RapporConfig}.
*
* @param encoderId Unique id for encoder.
* @param numBits Number of bits to be encoded in Rappor algorithm.
* @param probabilityF Probability F that used in Rappor algorithm. This will be
* quantized to the nearest 1/128.
* @param probabilityP Probability P that used in Rappor algorithm.
* @param probabilityQ Probability Q that used in Rappor algorithm.
* @param numCohorts Number of cohorts that used in Rappor algorithm.
* @param numBloomHashes Number of bloom hashes that used in Rappor algorithm.
*/
public RapporConfig(String encoderId, int numBits, double probabilityF,
double probabilityP, double probabilityQ, int numCohorts, int numBloomHashes) {
Preconditions.checkArgument(!TextUtils.isEmpty(encoderId), "encoderId cannot be empty");
this.mEncoderId = encoderId;
Preconditions.checkArgument(numBits > 0, "numBits needs to be > 0");
this.mNumBits = numBits;
Preconditions.checkArgument(probabilityF >= 0 && probabilityF <= 1,
"probabilityF must be in range [0.0, 1.0]");
this.mProbabilityF = probabilityF;
Preconditions.checkArgument(probabilityP >= 0 && probabilityP <= 1,
"probabilityP must be in range [0.0, 1.0]");
this.mProbabilityP = probabilityP;
Preconditions.checkArgument(probabilityQ >= 0 && probabilityQ <= 1,
"probabilityQ must be in range [0.0, 1.0]");
this.mProbabilityQ = probabilityQ;
Preconditions.checkArgument(numCohorts > 0, "numCohorts needs to be > 0");
this.mNumCohorts = numCohorts;
Preconditions.checkArgument(numBloomHashes > 0, "numBloomHashes needs to be > 0");
this.mNumBloomHashes = numBloomHashes;
}
@Override
public String getAlgorithm() {
return ALGORITHM_NAME;
}
@Override
public String toString() {
return String.format(
"EncoderId: %s, NumBits: %d, ProbabilityF: %.3f, ProbabilityP: %.3f"
+ ", ProbabilityQ: %.3f, NumCohorts: %d, NumBloomHashes: %d",
mEncoderId, mNumBits, mProbabilityF, mProbabilityP, mProbabilityQ,
mNumCohorts, mNumBloomHashes);
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 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.internal.rappor;
import android.privacy.DifferentialPrivacyEncoder;
import com.google.android.rappor.Encoder;
import java.security.SecureRandom;
import java.util.Random;
/**
* Differential privacy encoder by using
* <a href="https://research.google.com/pubs/pub42852.html">RAPPOR</a>
* algorithm.
*
* @hide
*/
public class RapporEncoder implements DifferentialPrivacyEncoder {
// Hard-coded seed and secret for insecure encoder
private static final long INSECURE_RANDOM_SEED = 0x12345678L;
private static final byte[] INSECURE_SECRET = new byte[]{
(byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
(byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
(byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
(byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
(byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
(byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
(byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
(byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
(byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
(byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
(byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
(byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54
};
private static final SecureRandom sSecureRandom = new SecureRandom();
private final RapporConfig mConfig;
// Rappor encoder
private final Encoder mEncoder;
// True if encoder is secure (seed is securely randomized)
private final boolean mIsSecure;
private RapporEncoder(RapporConfig config, boolean secureEncoder, byte[] userSecret) {
mConfig = config;
mIsSecure = secureEncoder;
final Random random;
if (secureEncoder) {
// Use SecureRandom as random generator.
random = sSecureRandom;
} else {
// Hard-coded random generator, to have deterministic result.
random = new Random(INSECURE_RANDOM_SEED);
userSecret = INSECURE_SECRET;
}
mEncoder = new Encoder(random, null, null,
userSecret, config.mEncoderId, config.mNumBits,
config.mProbabilityF, config.mProbabilityP, config.mProbabilityQ,
config.mNumCohorts, config.mNumBloomHashes);
}
/**
* Create {@link RapporEncoder} with {@link RapporConfig} and user secret provided.
*
* @param config Rappor parameters to encode input.
* @param userSecret Per device unique secret key.
* @return {@link RapporEncoder} instance.
*/
public static RapporEncoder createEncoder(RapporConfig config, byte[] userSecret) {
return new RapporEncoder(config, true, userSecret);
}
/**
* Create <strong>insecure</strong> {@link RapporEncoder} with {@link RapporConfig} provided.
* Should not use it to process sensitive data.
*
* @param config Rappor parameters to encode input.
* @return {@link RapporEncoder} instance.
*/
public static RapporEncoder createInsecureEncoderForTest(RapporConfig config) {
return new RapporEncoder(config, false, null);
}
@Override
public byte[] encodeString(String original) {
return mEncoder.encodeString(original);
}
@Override
public byte[] encodeBoolean(boolean original) {
return mEncoder.encodeBoolean(original);
}
@Override
public byte[] encodeBits(byte[] bits) {
return mEncoder.encodeBits(bits);
}
@Override
public RapporConfig getConfig() {
return mConfig;
}
@Override
public boolean isInsecureEncoderForTest() {
return !mIsSecure;
}
}

View File

@@ -0,0 +1,16 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
# We only want this apk build for tests.
LOCAL_MODULE_TAGS := tests
# Include all test java files.
LOCAL_SRC_FILES := \
$(call all-java-files-under, src)
LOCAL_STATIC_JAVA_LIBRARIES := junit rappor-tests android-support-test
LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_PACKAGE_NAME := FrameworksPrivacyLibraryTests
include $(BUILD_PACKAGE)

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.frameworks.coretests.privacy">
<application>
<uses-library android:name="android.test.runner" />
</application>
<instrumentation
android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.frameworks.coretests.privacy"
android:label="Frameworks Privacy Library Tests" />
</manifest>

View File

@@ -0,0 +1,392 @@
/*
* 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();
}
}

View File

@@ -0,0 +1,204 @@
/*
* 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.rappor.RapporConfig;
import android.privacy.internal.rappor.RapporEncoder;
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 RapporEncoder}.
* Most of the tests are done in external/rappor/client/javatest/ already.
* Tests here are just make sure the {@link RapporEncoder} wrap Rappor correctly.
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RapporEncoderTest {
@Test
public void testRapporEncoder_config() throws Exception {
final RapporConfig config = new RapporConfig(
"Foo", // encoderId
8, // numBits,
13.0 / 128.0, // probabilityF
0.25, // probabilityP
0.75, // probabilityQ
1, // numCohorts
2); // numBloomHashes)
final RapporEncoder encoder = RapporEncoder.createEncoder(config,
makeTestingUserSecret("encoder1"));
assertEquals("Rappor", encoder.getConfig().getAlgorithm());
assertEquals("EncoderId: Foo, NumBits: 8, ProbabilityF: 0.102, "
+ "ProbabilityP: 0.250, ProbabilityQ: 0.750, NumCohorts: 1, "
+ "NumBloomHashes: 2", encoder.getConfig().toString());
}
@Test
public void testRapporEncoder_basicIRRTest() throws Exception {
final RapporConfig config = new RapporConfig(
"Foo", // encoderId
12, // numBits,
0, // probabilityF
0, // probabilityP
1, // probabilityQ
1, // numCohorts (so must be cohort 0)
2); // numBloomHashes
// Use insecure encoder here as we want to get the exact output.
final RapporEncoder encoder = RapporEncoder.createInsecureEncoderForTest(config);
assertEquals(768, toLong(encoder.encodeString("Testing")));
}
@Test
public void testRapporEncoder_IRRWithPRR() throws Exception {
int numBits = 8;
final long inputValue = 254L;
final long prrValue = 250L;
final long prrAndIrrValue = 184L;
final RapporConfig config1 = new RapporConfig(
"Foo", // encoderId
numBits, // numBits,
0.25, // probabilityF
0, // probabilityP
1, // probabilityQ
1, // numCohorts
2); // numBloomHashes
// Use insecure encoder here as we want to get the exact output.
final RapporEncoder encoder1 = RapporEncoder.createInsecureEncoderForTest(config1);
// Verify that PRR is working as expected.
assertEquals(prrValue, toLong(encoder1.encodeBits(toBytes(inputValue))));
assertTrue(encoder1.isInsecureEncoderForTest());
// Verify that IRR is working as expected.
final RapporConfig config2 = new RapporConfig(
"Foo", // encoderId
numBits, // numBits,
0, // probabilityF
0.3, // probabilityP
0.7, // probabilityQ
1, // numCohorts
2); // numBloomHashes
// Use insecure encoder here as we want to get the exact output.
final RapporEncoder encoder2 = RapporEncoder.createInsecureEncoderForTest(config2);
assertEquals(prrAndIrrValue, toLong(encoder2.encodeBits(toBytes(prrValue))));
// Test that end-to-end is the result of PRR + IRR.
final RapporConfig config3 = new RapporConfig(
"Foo", // encoderId
numBits, // numBits,
0.25, // probabilityF
0.3, // probabilityP
0.7, // probabilityQ
1, // numCohorts
2); // numBloomHashes
final RapporEncoder encoder3 = RapporEncoder.createInsecureEncoderForTest(config3);
// Verify that PRR is working as expected.
assertEquals(prrAndIrrValue, toLong(encoder3.encodeBits(toBytes(inputValue))));
}
@Test
public void testRapporEncoder_ensureSecureEncoderIsSecure() throws Exception {
int numBits = 8;
final long inputValue = 254L;
final long prrValue = 250L;
final long prrAndIrrValue = 184L;
final RapporConfig config1 = new RapporConfig(
"Foo", // encoderId
numBits, // numBits,
0.25, // probabilityF
0, // probabilityP
1, // probabilityQ
1, // numCohorts
2); // numBloomHashes
final RapporEncoder encoder1 = RapporEncoder.createEncoder(config1,
makeTestingUserSecret("secret1"));
// Verify that PRR is working as expected, not affected by random seed.
assertEquals(prrValue, toLong(encoder1.encodeBits(toBytes(inputValue))));
assertFalse(encoder1.isInsecureEncoderForTest());
boolean hasDifferentResult2 = false;
for (int i = 0; i < 5; i++) {
final RapporConfig config2 = new RapporConfig(
"Foo", // encoderId
numBits, // numBits,
0, // probabilityF
0.3, // probabilityP
0.7, // probabilityQ
1, // numCohorts
2); // numBloomHashes
final RapporEncoder encoder2 = RapporEncoder.createEncoder(config2,
makeTestingUserSecret("secret1"));
hasDifferentResult2 |= (prrAndIrrValue != toLong(
encoder2.encodeBits(toBytes(prrValue))));
}
// Ensure it's not getting same result as it has random seed while encoder id and secret
// is the same.
assertTrue(hasDifferentResult2);
boolean hasDifferentResults3 = false;
for (int i = 0; i < 5; i++) {
final RapporConfig config3 = new RapporConfig(
"Foo", // encoderId
numBits, // numBits,
0.25, // probabilityF
0.3, // probabilityP
0.7, // probabilityQ
1, // numCohorts
2); // numBloomHashes
final RapporEncoder encoder3 = RapporEncoder.createEncoder(config3,
makeTestingUserSecret("secret1"));
hasDifferentResults3 |= (prrAndIrrValue != toLong(
encoder3.encodeBits(toBytes(inputValue))));
}
// Ensure it's not getting same result as it has random seed while encoder id and secret
// is the same.
assertTrue(hasDifferentResults3);
}
private static byte[] toBytes(long value) {
return ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).array();
}
private static long toLong(byte[] bytes) {
ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).put(bytes);
buffer.rewind();
return buffer.getLong();
}
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();
}
}