Loading core/java/android/database/RedactingCursor.java 0 → 100644 +181 −0 Original line number Diff line number Diff line /* * 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.database; import android.annotation.NonNull; import android.util.SparseArray; import java.util.Map; /** * Cursor that offers to redact values of requested columns. * * @hide */ public class RedactingCursor extends CrossProcessCursorWrapper { private final SparseArray<Object> mRedactions; private RedactingCursor(@NonNull Cursor cursor, SparseArray<Object> redactions) { super(cursor); mRedactions = redactions; } /** * Create a wrapped instance of the given {@link Cursor} which redacts the * requested columns so they always return specific values when accessed. * <p> * If a redacted column appears multiple times in the underlying cursor, all * instances will be redacted. If none of the redacted columns appear in the * given cursor, the given cursor will be returned untouched to improve * performance. */ public static Cursor create(@NonNull Cursor cursor, @NonNull Map<String, Object> redactions) { final SparseArray<Object> internalRedactions = new SparseArray<>(); final String[] columns = cursor.getColumnNames(); for (int i = 0; i < columns.length; i++) { if (redactions.containsKey(columns[i])) { internalRedactions.put(i, redactions.get(columns[i])); } } if (internalRedactions.size() == 0) { return cursor; } else { return new RedactingCursor(cursor, internalRedactions); } } @Override public void fillWindow(int position, CursorWindow window) { // Fill window directly to ensure data is redacted DatabaseUtils.cursorFillWindow(this, position, window); } @Override public CursorWindow getWindow() { // Returning underlying window risks leaking redacted data return null; } @Override public Cursor getWrappedCursor() { throw new UnsupportedOperationException( "Returning underlying cursor risks leaking redacted data"); } @Override public double getDouble(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (double) mRedactions.valueAt(i); } else { return super.getDouble(columnIndex); } } @Override public float getFloat(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (float) mRedactions.valueAt(i); } else { return super.getFloat(columnIndex); } } @Override public int getInt(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (int) mRedactions.valueAt(i); } else { return super.getInt(columnIndex); } } @Override public long getLong(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (long) mRedactions.valueAt(i); } else { return super.getLong(columnIndex); } } @Override public short getShort(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (short) mRedactions.valueAt(i); } else { return super.getShort(columnIndex); } } @Override public String getString(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (String) mRedactions.valueAt(i); } else { return super.getString(columnIndex); } } @Override public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { buffer.data = ((String) mRedactions.valueAt(i)).toCharArray(); buffer.sizeCopied = buffer.data.length; } else { super.copyStringToBuffer(columnIndex, buffer); } } @Override public byte[] getBlob(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (byte[]) mRedactions.valueAt(i); } else { return super.getBlob(columnIndex); } } @Override public int getType(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return DatabaseUtils.getTypeOfObject(mRedactions.valueAt(i)); } else { return super.getType(columnIndex); } } @Override public boolean isNull(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return mRedactions.valueAt(i) == null; } else { return super.isNull(columnIndex); } } } core/tests/coretests/AndroidManifest.xml +5 −0 Original line number Diff line number Diff line Loading @@ -1301,6 +1301,11 @@ android:process=":MemoryFileProvider"> </provider> <provider android:name="android.content.RedactingProvider" android:authorities="android.content.RedactingProvider" android:process=":RedactingProvider"> </provider> <!-- Application components used for os tests --> <service android:name="android.os.MessengerService" Loading core/tests/coretests/src/android/content/RedactingProvider.java 0 → 100644 +89 −0 Original line number Diff line number Diff line /* * 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.content; import android.database.Cursor; import android.database.MatrixCursor; import android.database.RedactingCursor; import android.net.Uri; import android.util.ArrayMap; public class RedactingProvider extends ContentProvider { @Override public boolean onCreate() { return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { switch (uri.getLastPathSegment()) { case "missing": { final MatrixCursor cursor = new MatrixCursor( new String[] { "name", "size", "_data" }); cursor.addRow(new Object[] { "foo", 10, "/path/to/foo" }); cursor.addRow(new Object[] { "bar", 20, "/path/to/bar" }); final ArrayMap<String, Object> redactions = new ArrayMap<>(); redactions.put("missing", null); return RedactingCursor.create(cursor, redactions); } case "single": { final MatrixCursor cursor = new MatrixCursor( new String[] { "name", "size", "_data" }); cursor.addRow(new Object[] { "foo", 10, "/path/to/foo" }); cursor.addRow(new Object[] { "bar", 20, "/path/to/bar" }); final ArrayMap<String, Object> redactions = new ArrayMap<>(); redactions.put("name", null); redactions.put("_data", "/dev/null"); return RedactingCursor.create(cursor, redactions); } case "multiple": { final MatrixCursor cursor = new MatrixCursor( new String[] { "_data", "name", "_data" }); cursor.addRow(new Object[] { "/path", "foo", "/path" }); final ArrayMap<String, Object> redactions = new ArrayMap<>(); redactions.put("_data", null); return RedactingCursor.create(cursor, redactions); } } throw new UnsupportedOperationException(); } @Override public String getType(Uri uri) { throw new UnsupportedOperationException(); } @Override public Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException(); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } } core/tests/coretests/src/android/database/RedactingCursorTest.java 0 → 100644 +85 −0 Original line number Diff line number Diff line /* * 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.database; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.content.Context; import android.net.Uri; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class RedactingCursorTest { private Context getContext() { return InstrumentationRegistry.getContext(); } @Test public void testMissing() throws Exception { final Cursor redacting = getContext().getContentResolver().query( Uri.parse("content://android.content.RedactingProvider/missing"), null, null, null); redacting.moveToNext(); assertEquals("foo", redacting.getString(0)); assertEquals(10, redacting.getInt(1)); assertEquals("/path/to/foo", redacting.getString(2)); redacting.moveToNext(); assertEquals("bar", redacting.getString(0)); assertEquals(20, redacting.getInt(1)); assertEquals("/path/to/bar", redacting.getString(2)); } @Test public void testSingle() throws Exception { final Cursor redacting = getContext().getContentResolver().query( Uri.parse("content://android.content.RedactingProvider/single"), null, null, null); redacting.moveToNext(); assertEquals(null, redacting.getString(0)); assertEquals(10, redacting.getInt(1)); assertEquals("/dev/null", redacting.getString(2)); assertEquals(Cursor.FIELD_TYPE_NULL, redacting.getType(0)); assertEquals(Cursor.FIELD_TYPE_INTEGER, redacting.getType(1)); assertEquals(Cursor.FIELD_TYPE_STRING, redacting.getType(2)); assertTrue(redacting.isNull(0)); assertFalse(redacting.isNull(1)); assertFalse(redacting.isNull(2)); redacting.moveToNext(); assertEquals(null, redacting.getString(0)); assertEquals(20, redacting.getInt(1)); assertEquals("/dev/null", redacting.getString(2)); } @Test public void testMultiple() throws Exception { final Cursor redacting = getContext().getContentResolver().query( Uri.parse("content://android.content.RedactingProvider/multiple"), null, null, null); redacting.moveToNext(); assertEquals(null, redacting.getString(0)); assertEquals("foo", redacting.getString(1)); assertEquals(null, redacting.getString(2)); } } Loading
core/java/android/database/RedactingCursor.java 0 → 100644 +181 −0 Original line number Diff line number Diff line /* * 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.database; import android.annotation.NonNull; import android.util.SparseArray; import java.util.Map; /** * Cursor that offers to redact values of requested columns. * * @hide */ public class RedactingCursor extends CrossProcessCursorWrapper { private final SparseArray<Object> mRedactions; private RedactingCursor(@NonNull Cursor cursor, SparseArray<Object> redactions) { super(cursor); mRedactions = redactions; } /** * Create a wrapped instance of the given {@link Cursor} which redacts the * requested columns so they always return specific values when accessed. * <p> * If a redacted column appears multiple times in the underlying cursor, all * instances will be redacted. If none of the redacted columns appear in the * given cursor, the given cursor will be returned untouched to improve * performance. */ public static Cursor create(@NonNull Cursor cursor, @NonNull Map<String, Object> redactions) { final SparseArray<Object> internalRedactions = new SparseArray<>(); final String[] columns = cursor.getColumnNames(); for (int i = 0; i < columns.length; i++) { if (redactions.containsKey(columns[i])) { internalRedactions.put(i, redactions.get(columns[i])); } } if (internalRedactions.size() == 0) { return cursor; } else { return new RedactingCursor(cursor, internalRedactions); } } @Override public void fillWindow(int position, CursorWindow window) { // Fill window directly to ensure data is redacted DatabaseUtils.cursorFillWindow(this, position, window); } @Override public CursorWindow getWindow() { // Returning underlying window risks leaking redacted data return null; } @Override public Cursor getWrappedCursor() { throw new UnsupportedOperationException( "Returning underlying cursor risks leaking redacted data"); } @Override public double getDouble(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (double) mRedactions.valueAt(i); } else { return super.getDouble(columnIndex); } } @Override public float getFloat(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (float) mRedactions.valueAt(i); } else { return super.getFloat(columnIndex); } } @Override public int getInt(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (int) mRedactions.valueAt(i); } else { return super.getInt(columnIndex); } } @Override public long getLong(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (long) mRedactions.valueAt(i); } else { return super.getLong(columnIndex); } } @Override public short getShort(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (short) mRedactions.valueAt(i); } else { return super.getShort(columnIndex); } } @Override public String getString(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (String) mRedactions.valueAt(i); } else { return super.getString(columnIndex); } } @Override public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { buffer.data = ((String) mRedactions.valueAt(i)).toCharArray(); buffer.sizeCopied = buffer.data.length; } else { super.copyStringToBuffer(columnIndex, buffer); } } @Override public byte[] getBlob(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return (byte[]) mRedactions.valueAt(i); } else { return super.getBlob(columnIndex); } } @Override public int getType(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return DatabaseUtils.getTypeOfObject(mRedactions.valueAt(i)); } else { return super.getType(columnIndex); } } @Override public boolean isNull(int columnIndex) { final int i = mRedactions.indexOfKey(columnIndex); if (i >= 0) { return mRedactions.valueAt(i) == null; } else { return super.isNull(columnIndex); } } }
core/tests/coretests/AndroidManifest.xml +5 −0 Original line number Diff line number Diff line Loading @@ -1301,6 +1301,11 @@ android:process=":MemoryFileProvider"> </provider> <provider android:name="android.content.RedactingProvider" android:authorities="android.content.RedactingProvider" android:process=":RedactingProvider"> </provider> <!-- Application components used for os tests --> <service android:name="android.os.MessengerService" Loading
core/tests/coretests/src/android/content/RedactingProvider.java 0 → 100644 +89 −0 Original line number Diff line number Diff line /* * 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.content; import android.database.Cursor; import android.database.MatrixCursor; import android.database.RedactingCursor; import android.net.Uri; import android.util.ArrayMap; public class RedactingProvider extends ContentProvider { @Override public boolean onCreate() { return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { switch (uri.getLastPathSegment()) { case "missing": { final MatrixCursor cursor = new MatrixCursor( new String[] { "name", "size", "_data" }); cursor.addRow(new Object[] { "foo", 10, "/path/to/foo" }); cursor.addRow(new Object[] { "bar", 20, "/path/to/bar" }); final ArrayMap<String, Object> redactions = new ArrayMap<>(); redactions.put("missing", null); return RedactingCursor.create(cursor, redactions); } case "single": { final MatrixCursor cursor = new MatrixCursor( new String[] { "name", "size", "_data" }); cursor.addRow(new Object[] { "foo", 10, "/path/to/foo" }); cursor.addRow(new Object[] { "bar", 20, "/path/to/bar" }); final ArrayMap<String, Object> redactions = new ArrayMap<>(); redactions.put("name", null); redactions.put("_data", "/dev/null"); return RedactingCursor.create(cursor, redactions); } case "multiple": { final MatrixCursor cursor = new MatrixCursor( new String[] { "_data", "name", "_data" }); cursor.addRow(new Object[] { "/path", "foo", "/path" }); final ArrayMap<String, Object> redactions = new ArrayMap<>(); redactions.put("_data", null); return RedactingCursor.create(cursor, redactions); } } throw new UnsupportedOperationException(); } @Override public String getType(Uri uri) { throw new UnsupportedOperationException(); } @Override public Uri insert(Uri uri, ContentValues values) { throw new UnsupportedOperationException(); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { throw new UnsupportedOperationException(); } }
core/tests/coretests/src/android/database/RedactingCursorTest.java 0 → 100644 +85 −0 Original line number Diff line number Diff line /* * 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.database; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.content.Context; import android.net.Uri; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class RedactingCursorTest { private Context getContext() { return InstrumentationRegistry.getContext(); } @Test public void testMissing() throws Exception { final Cursor redacting = getContext().getContentResolver().query( Uri.parse("content://android.content.RedactingProvider/missing"), null, null, null); redacting.moveToNext(); assertEquals("foo", redacting.getString(0)); assertEquals(10, redacting.getInt(1)); assertEquals("/path/to/foo", redacting.getString(2)); redacting.moveToNext(); assertEquals("bar", redacting.getString(0)); assertEquals(20, redacting.getInt(1)); assertEquals("/path/to/bar", redacting.getString(2)); } @Test public void testSingle() throws Exception { final Cursor redacting = getContext().getContentResolver().query( Uri.parse("content://android.content.RedactingProvider/single"), null, null, null); redacting.moveToNext(); assertEquals(null, redacting.getString(0)); assertEquals(10, redacting.getInt(1)); assertEquals("/dev/null", redacting.getString(2)); assertEquals(Cursor.FIELD_TYPE_NULL, redacting.getType(0)); assertEquals(Cursor.FIELD_TYPE_INTEGER, redacting.getType(1)); assertEquals(Cursor.FIELD_TYPE_STRING, redacting.getType(2)); assertTrue(redacting.isNull(0)); assertFalse(redacting.isNull(1)); assertFalse(redacting.isNull(2)); redacting.moveToNext(); assertEquals(null, redacting.getString(0)); assertEquals(20, redacting.getInt(1)); assertEquals("/dev/null", redacting.getString(2)); } @Test public void testMultiple() throws Exception { final Cursor redacting = getContext().getContentResolver().query( Uri.parse("content://android.content.RedactingProvider/multiple"), null, null, null); redacting.moveToNext(); assertEquals(null, redacting.getString(0)); assertEquals("foo", redacting.getString(1)); assertEquals(null, redacting.getString(2)); } }