OSDN Git Service

Introduce RedactingFileDescriptor.
authorJeff Sharkey <jsharkey@android.com>
Thu, 14 Jun 2018 22:04:00 +0000 (16:04 -0600)
committerJeff Sharkey <jsharkey@android.com>
Wed, 20 Jun 2018 20:17:51 +0000 (14:17 -0600)
Files on disk can sometimes contain content that should be redacted
based on the permissions of the remote caller.  (For example, EXIF
data containing location information shouldn't be visible to apps
that haven't been granted the location permission.)

This class provides an easy way to "redact" ranges within an
underlying file, without the overhead of making a duplicate copy
of the file, or the limitations of returning a non-seekable FD.

Bug: 110228267
Test: atest android.os.RedactingFileDescriptorTest
Change-Id: I0b3b9f642573f6165e152e7568cdaf55f0af7134

core/java/android/os/RedactingFileDescriptor.java [new file with mode: 0644]
core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java [new file with mode: 0644]

diff --git a/core/java/android/os/RedactingFileDescriptor.java b/core/java/android/os/RedactingFileDescriptor.java
new file mode 100644 (file)
index 0000000..60eb5c3
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 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.os;
+
+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 libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+
+/**
+ * Variant of {@link FileDescriptor} that allows its creator to specify regions
+ * that should be redacted (appearing as zeros to the reader).
+ *
+ * @hide
+ */
+public class RedactingFileDescriptor {
+    private static final String TAG = "RedactingFileDescriptor";
+    private static final boolean DEBUG = true;
+
+    private final long[] mRedactRanges;
+
+    private FileDescriptor mInner = null;
+    private ParcelFileDescriptor mOuter = null;
+
+    private RedactingFileDescriptor(Context context, File file, long[] redactRanges)
+            throws IOException {
+        mRedactRanges = checkRangesArgument(redactRanges);
+
+        try {
+            try {
+                mInner = Os.open(file.getAbsolutePath(), OsConstants.O_RDONLY, 0);
+                mOuter = context.getSystemService(StorageManager.class)
+                        .openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_ONLY, mCallback);
+            } catch (ErrnoException e) {
+                throw e.rethrowAsIOException();
+            }
+        } catch (IOException e) {
+            IoUtils.closeQuietly(mInner);
+            IoUtils.closeQuietly(mOuter);
+            throw e;
+        }
+    }
+
+    private static long[] checkRangesArgument(long[] ranges) {
+        if (ranges.length % 2 != 0) {
+            throw new IllegalArgumentException();
+        }
+        for (int i = 0; i < ranges.length - 1; i += 2) {
+            if (ranges[i] > ranges[i + 1]) {
+                throw new IllegalArgumentException();
+            }
+        }
+        return ranges;
+    }
+
+    /**
+     * Open the given {@link File} and returns a {@link ParcelFileDescriptor}
+     * that offers a redacted, read-only view of the underlying data.
+     *
+     * @param file The underlying file to open.
+     * @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;
+    }
+
+    private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
+        @Override
+        public long onGetSize() throws ErrnoException {
+            return Os.fstat(mInner).st_size;
+        }
+
+        @Override
+        public int onRead(long offset, int size, byte[] data) throws ErrnoException {
+            int n = 0;
+            while (n < size) {
+                try {
+                    final int res = Os.pread(mInner, data, n, size - n, offset + n);
+                    if (res == 0) {
+                        break;
+                    } else {
+                        n += res;
+                    }
+                } catch (InterruptedIOException e) {
+                    n += e.bytesTransferred;
+                }
+            }
+
+            // Redact any relevant ranges before returning
+            final long[] ranges = mRedactRanges;
+            for (int i = 0; i < ranges.length; i += 2) {
+                final long start = Math.max(offset, ranges[i]);
+                final long end = Math.min(offset + size, ranges[i + 1]);
+                for (long j = start; j < end; j++) {
+                    data[(int) (j - offset)] = 0;
+                }
+            }
+            return n;
+        }
+
+        @Override
+        public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
+            throw new ErrnoException(TAG, OsConstants.EBADF);
+        }
+
+        @Override
+        public void onFsync() throws ErrnoException {
+            Os.fsync(mInner);
+        }
+
+        @Override
+        public void onRelease() {
+            if (DEBUG) Slog.v(TAG, "onRelease()");
+            IoUtils.closeQuietly(mInner);
+        }
+    };
+}
diff --git a/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java b/core/tests/coretests/src/android/os/RedactingFileDescriptorTest.java
new file mode 100644 (file)
index 0000000..c8bc35c
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2018 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.os;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.system.Os;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class RedactingFileDescriptorTest {
+    private Context mContext;
+    private File mFile;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mFile = File.createTempFile("redacting", "dat");
+        try (FileOutputStream out = new FileOutputStream(mFile)) {
+            final byte[] buf = new byte[1_000_000];
+            Arrays.fill(buf, (byte) 64);
+            out.write(buf);
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mFile.delete();
+    }
+
+    @Test
+    public void testSingleByte() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor
+                .open(mContext, mFile, new long[] { 10, 11 }).getFileDescriptor();
+
+        final byte[] buf = new byte[1_000];
+        assertEquals(buf.length, Os.read(fd, buf, 0, buf.length));
+        for (int i = 0; i < buf.length; i++) {
+            if (i == 10) {
+                assertEquals(0, buf[i]);
+            } else {
+                assertEquals(64, buf[i]);
+            }
+        }
+    }
+
+    @Test
+    public void testRanges() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor
+                .open(mContext, mFile, new long[] { 100, 200, 300, 400 }).getFileDescriptor();
+
+        final byte[] buf = new byte[10];
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 90));
+        assertArrayEquals(new byte[] { 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 }, buf);
+
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 95));
+        assertArrayEquals(new byte[] { 64, 64, 64, 64, 64, 0, 0, 0, 0, 0 }, buf);
+
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 100));
+        assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, buf);
+
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 195));
+        assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 64, 64, 64, 64, 64 }, buf);
+
+        assertEquals(buf.length, Os.pread(fd, buf, 0, 10, 395));
+        assertArrayEquals(new byte[] { 0, 0, 0, 0, 0, 64, 64, 64, 64, 64 }, buf);
+    }
+
+    @Test
+    public void testEntireFile() throws Exception {
+        final FileDescriptor fd = RedactingFileDescriptor
+                .open(mContext, mFile, new long[] { 0, 5_000_000 }).getFileDescriptor();
+
+        try (FileInputStream in = new FileInputStream(fd)) {
+            int val;
+            while ((val = in.read()) != -1) {
+                assertEquals(0, val);
+            }
+        }
+    }
+}