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

Commit cb394993 authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Redact location Exif tags when no permission.

When the caller doesn't hold the ACCESS_MEDIA_LOCATION permission,
any location Exif tags should be redacted for privacy reasons.  We
still allow unredacted raw file access if the media is owned by the
calling app, since they should be able to see data they contributed.

Certain backup apps really want to see the original contents without
any redaction, so provide them a setRequireOriginal() API so they
get a strong exception whenever the original bits can't be provided.

Add the ability to open a redacted file for read/write access by
stopping redaction for any ranges that have been overwritten with
new data, along with tests to verify this behavior.

Extend "content" tool to bind null values.

Bug: 111892141
Test: atest android.os.RedactingFileDescriptorTest
Test: atest cts/tests/tests/provider/src/android/provider/cts/MediaStore*
Change-Id: I47b220036a712d9d49547196b90e031b10760f84
parent bd9669cd
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -37296,6 +37296,7 @@ package android.provider {
    method public static java.lang.String getVolumeName(android.net.Uri);
    method public static android.provider.MediaStore.PendingSession openPending(android.content.Context, android.net.Uri);
    method public static android.net.Uri setIncludePending(android.net.Uri);
    method public static android.net.Uri setRequireOriginal(android.net.Uri);
    field public static final java.lang.String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE";
    field public static final java.lang.String ACTION_IMAGE_CAPTURE_SECURE = "android.media.action.IMAGE_CAPTURE_SECURE";
    field public static final java.lang.String ACTION_REVIEW = "android.provider.action.REVIEW";
+4 −1
Original line number Diff line number Diff line
@@ -77,7 +77,7 @@ public class Content {
                    + "  <BINDING> binds a typed value to a column and is formatted:\n"
                    + "  <COLUMN_NAME>:<TYPE>:<COLUMN_VALUE> where:\n"
                    + "  <TYPE> specifies data type such as:\n"
                    + "  b - boolean, s - string, i - integer, l - long, f - float, d - double\n"
                    + "  b - boolean, s - string, i - integer, l - long, f - float, d - double, n - null\n"
                    + "  Note: Omit the value for passing an empty string, e.g column:s:\n"
                    + "  Example:\n"
                    + "  # Add \"new_setting\" secure setting with value \"new_value\".\n"
@@ -153,6 +153,7 @@ public class Content {
        private static final String TYPE_LONG = "l";
        private static final String TYPE_FLOAT = "f";
        private static final String TYPE_DOUBLE = "d";
        private static final String TYPE_NULL = "n";
        private static final String COLON = ":";
        private static final String ARGUMENT_PREFIX = "--";

@@ -410,6 +411,8 @@ public class Content {
                values.put(column, Long.parseLong(value));
            } else if (TYPE_FLOAT.equalsIgnoreCase(type) || TYPE_DOUBLE.equalsIgnoreCase(type)) {
                values.put(column, Double.parseDouble(value));
            } else if (TYPE_NULL.equalsIgnoreCase(type)) {
                values.putNull(column);
            } else {
                throw new IllegalArgumentException("Unsupported type: " + type);
            }
+10 −8
Original line number Diff line number Diff line
@@ -17,12 +17,6 @@
package android.os;

import static android.system.OsConstants.AF_UNIX;
import static android.system.OsConstants.O_APPEND;
import static android.system.OsConstants.O_CREAT;
import static android.system.OsConstants.O_RDONLY;
import static android.system.OsConstants.O_RDWR;
import static android.system.OsConstants.O_TRUNC;
import static android.system.OsConstants.O_WRONLY;
import static android.system.OsConstants.SEEK_SET;
import static android.system.OsConstants.SOCK_SEQPACKET;
import static android.system.OsConstants.SOCK_STREAM;
@@ -254,8 +248,16 @@ public class ParcelFileDescriptor implements Parcelable, Closeable {
    }

    /** {@hide} */
    public static ParcelFileDescriptor fromFd(
            FileDescriptor fd, Handler handler, final OnCloseListener listener) throws IOException {
    public static ParcelFileDescriptor fromPfd(ParcelFileDescriptor pfd, Handler handler,
            final OnCloseListener listener) throws IOException {
        final FileDescriptor original = new FileDescriptor();
        original.setInt$(pfd.detachFd());
        return fromFd(original, handler, listener);
    }

    /** {@hide} */
    public static ParcelFileDescriptor fromFd(FileDescriptor fd, Handler handler,
            final OnCloseListener listener) throws IOException {
        if (handler == null) {
            throw new IllegalArgumentException("Handler must not be null");
        }
+76 −10
Original line number Diff line number Diff line
@@ -20,15 +20,18 @@ import android.content.Context;
import android.os.storage.StorageManager;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

import libcore.io.IoUtils;
import libcore.util.EmptyArray;

import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.Arrays;

/**
 * Variant of {@link FileDescriptor} that allows its creator to specify regions
@@ -40,20 +43,21 @@ public class RedactingFileDescriptor {
    private static final String TAG = "RedactingFileDescriptor";
    private static final boolean DEBUG = true;

    private final long[] mRedactRanges;
    private volatile long[] mRedactRanges;

    private FileDescriptor mInner = null;
    private ParcelFileDescriptor mOuter = null;

    private RedactingFileDescriptor(Context context, File file, long[] redactRanges)
    private RedactingFileDescriptor(Context context, File file, int mode, long[] redactRanges)
            throws IOException {
        mRedactRanges = checkRangesArgument(redactRanges);

        try {
            try {
                mInner = Os.open(file.getAbsolutePath(), OsConstants.O_RDONLY, 0);
                mInner = Os.open(file.getAbsolutePath(),
                        FileUtils.translateModePfdToPosix(mode), 0);
                mOuter = context.getSystemService(StorageManager.class)
                        .openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_ONLY, mCallback);
                        .openProxyFileDescriptor(mode, mCallback);
            } catch (ErrnoException e) {
                throw e.rethrowAsIOException();
            }
@@ -78,16 +82,61 @@ public class RedactingFileDescriptor {

    /**
     * Open the given {@link File} and returns a {@link ParcelFileDescriptor}
     * that offers a redacted, read-only view of the underlying data.
     * that offers a redacted view of the underlying data. If a redacted region
     * is written to, the newly written data can be read back correctly instead
     * of continuing to be redacted.
     *
     * @param file The underlying file to open.
     * @param mode The {@link ParcelFileDescriptor} mode to open with.
     * @param redactRanges List of file offsets that should be redacted, stored
     *            as {@code [start1, end1, start2, end2, ...]}. Start values are
     *            inclusive and end values are exclusive.
     */
    public static ParcelFileDescriptor open(Context context, File file, long[] redactRanges)
            throws IOException {
        return new RedactingFileDescriptor(context, file, redactRanges).mOuter;
    public static ParcelFileDescriptor open(Context context, File file, int mode,
            long[] redactRanges) throws IOException {
        return new RedactingFileDescriptor(context, file, mode, redactRanges).mOuter;
    }

    /**
     * Update the given ranges argument to remove any references to the given
     * offset and length. This is typically used when a caller has written over
     * a previously redacted region.
     */
    @VisibleForTesting
    public static long[] removeRange(long[] ranges, long start, long end) {
        if (start == end) {
            return ranges;
        } else if (start > end) {
            throw new IllegalArgumentException();
        }

        long[] res = EmptyArray.LONG;
        for (int i = 0; i < ranges.length; i += 2) {
            if (start <= ranges[i] && end >= ranges[i + 1]) {
                // Range entirely covered; remove it
            } else if (start >= ranges[i] && end <= ranges[i + 1]) {
                // Range partially covered; punch a hole
                res = Arrays.copyOf(res, res.length + 4);
                res[res.length - 4] = ranges[i];
                res[res.length - 3] = start;
                res[res.length - 2] = end;
                res[res.length - 1] = ranges[i + 1];
            } else {
                // Range might covered; adjust edges if needed
                res = Arrays.copyOf(res, res.length + 2);
                if (end >= ranges[i] && end <= ranges[i + 1]) {
                    res[res.length - 2] = Math.max(ranges[i], end);
                } else {
                    res[res.length - 2] = ranges[i];
                }
                if (start >= ranges[i] && start <= ranges[i + 1]) {
                    res[res.length - 1] = Math.min(ranges[i + 1], start);
                } else {
                    res[res.length - 1] = ranges[i + 1];
                }
            }
        }
        return res;
    }

    private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
@@ -126,7 +175,24 @@ public class RedactingFileDescriptor {

        @Override
        public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
            throw new ErrnoException(TAG, OsConstants.EBADF);
            int n = 0;
            while (n < size) {
                try {
                    final int res = Os.pwrite(mInner, data, n, size - n, offset + n);
                    if (res == 0) {
                        break;
                    } else {
                        n += res;
                    }
                } catch (InterruptedIOException e) {
                    n += e.bytesTransferred;
                }
            }

            // Clear any relevant redaction ranges before returning, since the
            // writer should have access to see the data they just overwrote
            mRedactRanges = removeRange(mRedactRanges, offset, offset + n);
            return n;
        }

        @Override
+20 −0
Original line number Diff line number Diff line
@@ -121,6 +121,8 @@ public final class MediaStore {
    public static final String PARAM_INCLUDE_PENDING = "includePending";
    /** {@hide} */
    public static final String PARAM_PROGRESS = "progress";
    /** {@hide} */
    public static final String PARAM_REQUIRE_ORIGINAL = "requireOriginal";

    /**
     * Activity Action: Launch a music player.
@@ -477,6 +479,24 @@ public final class MediaStore {
        return uri.buildUpon().appendQueryParameter(PARAM_INCLUDE_PENDING, "1").build();
    }

    /**
     * Update the given {@link Uri} to indicate that the caller requires the
     * original file contents when calling
     * {@link ContentResolver#openFileDescriptor(Uri, String)}.
     * <p>
     * This can be useful when the caller wants to ensure they're backing up the
     * exact bytes of the underlying media, without any Exif redaction being
     * performed.
     * <p>
     * If the original file contents cannot be provided, a
     * {@link UnsupportedOperationException} will be thrown when the returned
     * {@link Uri} is used, such as when the caller doesn't hold
     * {@link android.Manifest.permission#ACCESS_MEDIA_LOCATION}.
     */
    public static @NonNull Uri setRequireOriginal(@NonNull Uri uri) {
        return uri.buildUpon().appendQueryParameter(PARAM_REQUIRE_ORIGINAL, "1").build();
    }

    /**
     * Create a new pending media item using the given parameters. Pending items
     * are expected to have a short lifetime, and owners should either
Loading