1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.os;
18 
19 import android.content.Context;
20 import android.os.storage.StorageManager;
21 import android.system.ErrnoException;
22 import android.system.Os;
23 import android.util.Slog;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 
27 import libcore.io.IoUtils;
28 import libcore.util.EmptyArray;
29 
30 import java.io.File;
31 import java.io.FileDescriptor;
32 import java.io.IOException;
33 import java.io.InterruptedIOException;
34 import java.util.Arrays;
35 
36 /**
37  * Variant of {@link FileDescriptor} that allows its creator to specify regions
38  * that should be redacted.
39  *
40  * @hide
41  */
42 public class RedactingFileDescriptor {
43     private static final String TAG = "RedactingFileDescriptor";
44     private static final boolean DEBUG = true;
45 
46     private volatile long[] mRedactRanges;
47     private volatile long[] mFreeOffsets;
48 
49     private FileDescriptor mInner = null;
50     private ParcelFileDescriptor mOuter = null;
51 
RedactingFileDescriptor( Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)52     private RedactingFileDescriptor(
53             Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)
54             throws IOException {
55         mRedactRanges = checkRangesArgument(redactRanges);
56         mFreeOffsets = freeOffsets;
57 
58         try {
59             try {
60                 mInner = Os.open(file.getAbsolutePath(),
61                         FileUtils.translateModePfdToPosix(mode), 0);
62                 mOuter = context.getSystemService(StorageManager.class)
63                         .openProxyFileDescriptor(mode, mCallback);
64             } catch (ErrnoException e) {
65                 throw e.rethrowAsIOException();
66             }
67         } catch (IOException e) {
68             IoUtils.closeQuietly(mInner);
69             IoUtils.closeQuietly(mOuter);
70             throw e;
71         }
72     }
73 
checkRangesArgument(long[] ranges)74     private static long[] checkRangesArgument(long[] ranges) {
75         if (ranges.length % 2 != 0) {
76             throw new IllegalArgumentException();
77         }
78         for (int i = 0; i < ranges.length - 1; i += 2) {
79             if (ranges[i] > ranges[i + 1]) {
80                 throw new IllegalArgumentException();
81             }
82         }
83         return ranges;
84     }
85 
86     /**
87      * Open the given {@link File} and returns a {@link ParcelFileDescriptor}
88      * that offers a redacted view of the underlying data. If a redacted region
89      * is written to, the newly written data can be read back correctly instead
90      * of continuing to be redacted.
91      *
92      * @param file The underlying file to open.
93      * @param mode The {@link ParcelFileDescriptor} mode to open with.
94      * @param redactRanges List of file ranges that should be redacted, stored
95      *            as {@code [start1, end1, start2, end2, ...]}. Start values are
96      *            inclusive and end values are exclusive.
97      * @param freePositions List of file offsets at which the four byte value 'free' should be
98      *            written instead of zeros within parts of the file covered by {@code redactRanges}.
99      *            Non-redacted bytes will not be modified even if covered by a 'free'. This is
100      *            useful for overwriting boxes in ISOBMFF files with padding data.
101      */
open(Context context, File file, int mode, long[] redactRanges, long[] freePositions)102     public static ParcelFileDescriptor open(Context context, File file, int mode,
103             long[] redactRanges, long[] freePositions) throws IOException {
104         return new RedactingFileDescriptor(context, file, mode, redactRanges, freePositions).mOuter;
105     }
106 
107     /**
108      * Update the given ranges argument to remove any references to the given
109      * offset and length. This is typically used when a caller has written over
110      * a previously redacted region.
111      */
112     @VisibleForTesting
removeRange(long[] ranges, long start, long end)113     public static long[] removeRange(long[] ranges, long start, long end) {
114         if (start == end) {
115             return ranges;
116         } else if (start > end) {
117             throw new IllegalArgumentException();
118         }
119 
120         long[] res = EmptyArray.LONG;
121         for (int i = 0; i < ranges.length; i += 2) {
122             if (start <= ranges[i] && end >= ranges[i + 1]) {
123                 // Range entirely covered; remove it
124             } else if (start >= ranges[i] && end <= ranges[i + 1]) {
125                 // Range partially covered; punch a hole
126                 res = Arrays.copyOf(res, res.length + 4);
127                 res[res.length - 4] = ranges[i];
128                 res[res.length - 3] = start;
129                 res[res.length - 2] = end;
130                 res[res.length - 1] = ranges[i + 1];
131             } else {
132                 // Range might covered; adjust edges if needed
133                 res = Arrays.copyOf(res, res.length + 2);
134                 if (end >= ranges[i] && end <= ranges[i + 1]) {
135                     res[res.length - 2] = Math.max(ranges[i], end);
136                 } else {
137                     res[res.length - 2] = ranges[i];
138                 }
139                 if (start >= ranges[i] && start <= ranges[i + 1]) {
140                     res[res.length - 1] = Math.min(ranges[i + 1], start);
141                 } else {
142                     res[res.length - 1] = ranges[i + 1];
143                 }
144             }
145         }
146         return res;
147     }
148 
149     private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
150         @Override
151         public long onGetSize() throws ErrnoException {
152             return Os.fstat(mInner).st_size;
153         }
154 
155         @Override
156         public int onRead(long offset, int size, byte[] data) throws ErrnoException {
157             int n = 0;
158             while (n < size) {
159                 try {
160                     final int res = Os.pread(mInner, data, n, size - n, offset + n);
161                     if (res == 0) {
162                         break;
163                     } else {
164                         n += res;
165                     }
166                 } catch (InterruptedIOException e) {
167                     n += e.bytesTransferred;
168                 }
169             }
170 
171             // Redact any relevant ranges before returning
172             final long[] ranges = mRedactRanges;
173             for (int i = 0; i < ranges.length; i += 2) {
174                 final long start = Math.max(offset, ranges[i]);
175                 final long end = Math.min(offset + size, ranges[i + 1]);
176                 for (long j = start; j < end; j++) {
177                     data[(int) (j - offset)] = 0;
178                 }
179                 // Overwrite data at 'free' offsets within the redaction ranges.
180                 for (long freeOffset : mFreeOffsets) {
181                     final long freeEnd = freeOffset + 4;
182                     final long redactFreeStart = Math.max(freeOffset, start);
183                     final long redactFreeEnd = Math.min(freeEnd, end);
184                     for (long j = redactFreeStart; j < redactFreeEnd; j++) {
185                         data[(int) (j - offset)] = (byte) "free".charAt((int) (j - freeOffset));
186                     }
187                 }
188             }
189             return n;
190         }
191 
192         @Override
193         public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
194             int n = 0;
195             while (n < size) {
196                 try {
197                     final int res = Os.pwrite(mInner, data, n, size - n, offset + n);
198                     if (res == 0) {
199                         break;
200                     } else {
201                         n += res;
202                     }
203                 } catch (InterruptedIOException e) {
204                     n += e.bytesTransferred;
205                 }
206             }
207 
208             // Clear any relevant redaction ranges before returning, since the
209             // writer should have access to see the data they just overwrote
210             mRedactRanges = removeRange(mRedactRanges, offset, offset + n);
211             return n;
212         }
213 
214         @Override
215         public void onFsync() throws ErrnoException {
216             Os.fsync(mInner);
217         }
218 
219         @Override
220         public void onRelease() {
221             if (DEBUG) Slog.v(TAG, "onRelease()");
222             IoUtils.closeQuietly(mInner);
223         }
224     };
225 }
226