Add differential privacy library and algorithms
- Created a differential privacy framework interface
- Added 2 DP algorithms in DP framework:
- Rappor, a wrapper based on external/rappor project
- Longitudinal Reporting, DP enhancement based on Rappor
- Created Privacy Tests for testing all privacy libraries
- Added original Rappor test case in privacy test
- Created tests to verify Rappor and Longitudinal Reporting result in DP framework
Test: bit FrameworksPrivacyLibraryTests:android.privacy.LongitudinalReportingEncoderTest
Test: bit FrameworksPrivacyLibraryTests:android.privacy.RapporEncoderTest
Change-Id: Id460665059653924434c141686b5cad3fb697046
diff --git a/Android.bp b/Android.bp
index 979ed1c..80eaa7d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -671,6 +671,7 @@
"libphonenumber-platform",
"nist-sip",
"tagsoup",
+ "rappor",
],
dxflags: ["--core-library"],
}
diff --git a/core/java/android/privacy/DifferentialPrivacyConfig.java b/core/java/android/privacy/DifferentialPrivacyConfig.java
new file mode 100644
index 0000000..e14893e
--- /dev/null
+++ b/core/java/android/privacy/DifferentialPrivacyConfig.java
@@ -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();
+}
diff --git a/core/java/android/privacy/DifferentialPrivacyEncoder.java b/core/java/android/privacy/DifferentialPrivacyEncoder.java
new file mode 100644
index 0000000..9355d6a
--- /dev/null
+++ b/core/java/android/privacy/DifferentialPrivacyEncoder.java
@@ -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();
+}
diff --git a/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java b/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java
new file mode 100644
index 0000000..ee910fc
--- /dev/null
+++ b/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java
@@ -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);
+ }
+}
diff --git a/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java b/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java
new file mode 100644
index 0000000..219868d
--- /dev/null
+++ b/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java
@@ -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;
+ }
+}
diff --git a/core/java/android/privacy/internal/rappor/RapporConfig.java b/core/java/android/privacy/internal/rappor/RapporConfig.java
new file mode 100644
index 0000000..221999b
--- /dev/null
+++ b/core/java/android/privacy/internal/rappor/RapporConfig.java
@@ -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);
+ }
+}
diff --git a/core/java/android/privacy/internal/rappor/RapporEncoder.java b/core/java/android/privacy/internal/rappor/RapporEncoder.java
new file mode 100644
index 0000000..2eca4c98
--- /dev/null
+++ b/core/java/android/privacy/internal/rappor/RapporEncoder.java
@@ -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;
+ }
+}
diff --git a/core/tests/privacytests/Android.mk b/core/tests/privacytests/Android.mk
new file mode 100644
index 0000000..7b11225
--- /dev/null
+++ b/core/tests/privacytests/Android.mk
@@ -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)
diff --git a/core/tests/privacytests/AndroidManifest.xml b/core/tests/privacytests/AndroidManifest.xml
new file mode 100644
index 0000000..a0e5281
--- /dev/null
+++ b/core/tests/privacytests/AndroidManifest.xml
@@ -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>
diff --git a/core/tests/privacytests/src/android/privacy/LongitudinalReportingEncoderTest.java b/core/tests/privacytests/src/android/privacy/LongitudinalReportingEncoderTest.java
new file mode 100644
index 0000000..9166438
--- /dev/null
+++ b/core/tests/privacytests/src/android/privacy/LongitudinalReportingEncoderTest.java
@@ -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();
+ }
+}
diff --git a/core/tests/privacytests/src/android/privacy/RapporEncoderTest.java b/core/tests/privacytests/src/android/privacy/RapporEncoderTest.java
new file mode 100644
index 0000000..dad98b8
--- /dev/null
+++ b/core/tests/privacytests/src/android/privacy/RapporEncoderTest.java
@@ -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();
+ }
+}