summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2018-12-01 18:26:43 -0700
committerJeff Sharkey <jsharkey@android.com>2018-12-03 12:57:09 -0700
commitcb394993547697cfc45053f8a7fc656dffea8630 (patch)
tree1069b9c0c22f171b59dd5075d724fe7ce7871948
parentbd9669cd6a7cd47dabf050479bdf8b9b0e2b6669 (diff)
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
-rw-r--r--api/current.txt1
-rw-r--r--cmds/content/src/com/android/commands/content/Content.java5
-rw-r--r--core/java/android/os/ParcelFileDescriptor.java18
-rw-r--r--core/java/android/os/RedactingFileDescriptor.java86
-rw-r--r--core/java/android/provider/MediaStore.java20
-rw-r--r--core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java73
6 files changed, 178 insertions, 25 deletions
diff --git a/api/current.txt b/api/current.txt
index 079539e3c5f4..69fd37269944 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -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";
diff --git a/cmds/content/src/com/android/commands/content/Content.java b/cmds/content/src/com/android/commands/content/Content.java
index 1597c8c9c2b2..52a2ab407f91 100644
--- a/cmds/content/src/com/android/commands/content/Content.java
+++ b/cmds/content/src/com/android/commands/content/Content.java
@@ -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);
}
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 44b9e311dc0b..6de1ff4bc097 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -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");
}
diff --git a/core/java/android/os/RedactingFileDescriptor.java b/core/java/android/os/RedactingFileDescriptor.java
index 60eb5c3c4a89..4e5eaac3442f 100644
--- a/core/java/android/os/RedactingFileDescriptor.java
+++ b/core/java/android/os/RedactingFileDescriptor.java
@@ -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
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index e0e4fe29f48a..ec8db1ca580e 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -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.
@@ -478,6 +480,24 @@ public final class MediaStore {
}
/**
+ * 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
* {@link PendingSession#publish()} or {@link PendingSession#abandon()} a
diff --git a/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java b/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java
index c8bc35c976a2..9e1523165925 100644
--- a/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java
+++ b/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java
@@ -16,6 +16,10 @@
package android.os;
+import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
+import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
+import static android.os.RedactingFileDescriptor.removeRange;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -58,8 +62,8 @@ public class RedactingFileDescriptorTest {
@Test
public void testSingleByte() throws Exception {
- final FileDescriptor fd = RedactingFileDescriptor
- .open(mContext, mFile, new long[] { 10, 11 }).getFileDescriptor();
+ final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_ONLY,
+ new long[] { 10, 11 }).getFileDescriptor();
final byte[] buf = new byte[1_000];
assertEquals(buf.length, Os.read(fd, buf, 0, buf.length));
@@ -74,8 +78,8 @@ public class RedactingFileDescriptorTest {
@Test
public void testRanges() throws Exception {
- final FileDescriptor fd = RedactingFileDescriptor
- .open(mContext, mFile, new long[] { 100, 200, 300, 400 }).getFileDescriptor();
+ final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_ONLY,
+ new long[] { 100, 200, 300, 400 }).getFileDescriptor();
final byte[] buf = new byte[10];
assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 90));
@@ -96,8 +100,8 @@ public class RedactingFileDescriptorTest {
@Test
public void testEntireFile() throws Exception {
- final FileDescriptor fd = RedactingFileDescriptor
- .open(mContext, mFile, new long[] { 0, 5_000_000 }).getFileDescriptor();
+ final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_ONLY,
+ new long[] { 0, 5_000_000 }).getFileDescriptor();
try (FileInputStream in = new FileInputStream(fd)) {
int val;
@@ -106,4 +110,61 @@ public class RedactingFileDescriptorTest {
}
}
}
+
+ @Test
+ public void testReadWrite() throws Exception {
+ final FileDescriptor fd = RedactingFileDescriptor.open(mContext, mFile, MODE_READ_WRITE,
+ new long[] { 100, 200, 300, 400 }).getFileDescriptor();
+
+ // Redacted at first
+ final byte[] buf = new byte[10];
+ assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 95));
+ assertArrayEquals(new byte[] { 64, 64, 64, 64, 64, 0, 0, 0, 0, 0 }, buf);
+
+ // But we can see data that we've written
+ Os.pwrite(fd, new byte[] { 32, 32 }, 0, 2, 102);
+ assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 95));
+ assertArrayEquals(new byte[] { 64, 64, 64, 64, 64, 0, 0, 32, 32, 0 }, buf);
+ }
+
+ @Test
+ public void testRemoveRange() throws Exception {
+ // Removing outside ranges should have no changes
+ assertArrayEquals(new long[] { 100, 200, 300, 400 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 0, 100));
+ assertArrayEquals(new long[] { 100, 200, 300, 400 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 200, 300));
+ assertArrayEquals(new long[] { 100, 200, 300, 400 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 400, 500));
+
+ // Removing full regions
+ assertArrayEquals(new long[] { 100, 200 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 300, 400));
+ assertArrayEquals(new long[] { 100, 200 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 250, 450));
+ assertArrayEquals(new long[] { 300, 400 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 50, 250));
+ assertArrayEquals(new long[] { },
+ removeRange(new long[] { 100, 200, 300, 400 }, 0, 5_000_000));
+ }
+
+ @Test
+ public void testRemoveRange_Partial() throws Exception {
+ assertArrayEquals(new long[] { 150, 200, 300, 400 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 50, 150));
+ assertArrayEquals(new long[] { 100, 150, 300, 400 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 150, 250));
+ assertArrayEquals(new long[] { 100, 150, 350, 400 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 150, 350));
+ assertArrayEquals(new long[] { 100, 150 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 150, 500));
+ }
+
+ @Test
+ public void testRemoveRange_Hole() throws Exception {
+ assertArrayEquals(new long[] { 100, 125, 175, 200, 300, 400 },
+ removeRange(new long[] { 100, 200, 300, 400 }, 125, 175));
+ assertArrayEquals(new long[] { 100, 200 },
+ removeRange(new long[] { 100, 200 }, 150, 150));
+ }
}