Merge "Add differential privacy library and algorithms"
This commit is contained in:
@@ -670,6 +670,7 @@ java_library {
|
||||
"libphonenumber-platform",
|
||||
"nist-sip",
|
||||
"tagsoup",
|
||||
"rappor",
|
||||
],
|
||||
dxflags: ["--core-library"],
|
||||
}
|
||||
|
||||
34
core/java/android/privacy/DifferentialPrivacyConfig.java
Normal file
34
core/java/android/privacy/DifferentialPrivacyConfig.java
Normal 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();
|
||||
}
|
||||
78
core/java/android/privacy/DifferentialPrivacyEncoder.java
Normal file
78
core/java/android/privacy/DifferentialPrivacyEncoder.java
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
87
core/java/android/privacy/internal/rappor/RapporConfig.java
Normal file
87
core/java/android/privacy/internal/rappor/RapporConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
125
core/java/android/privacy/internal/rappor/RapporEncoder.java
Normal file
125
core/java/android/privacy/internal/rappor/RapporEncoder.java
Normal 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;
|
||||
}
|
||||
}
|
||||
16
core/tests/privacytests/Android.mk
Normal file
16
core/tests/privacytests/Android.mk
Normal 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)
|
||||
29
core/tests/privacytests/AndroidManifest.xml
Normal file
29
core/tests/privacytests/AndroidManifest.xml
Normal 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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user