Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 04c17bec authored by Ricky Wai's avatar Ricky Wai Committed by Android (Google) Code Review
Browse files

Merge "Add differential privacy library and algorithms"

parents 8ca1e41a 0d1bc332
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -670,6 +670,7 @@ java_library {
        "libphonenumber-platform",
        "nist-sip",
        "tagsoup",
        "rappor",
    ],
    dxflags: ["--core-library"],
}
+34 −0
Original line number Diff line number Diff line
/*
 * 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 −0
Original line number Diff line number Diff line
/*
 * 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();
}
+107 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+170 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
Loading