Merge "DSU to support GSI key revocation list"
This commit is contained in:
@@ -35,4 +35,7 @@
|
||||
<!-- Toast when we fail to launch into Dynamic System [CHAR LIMIT=64] -->
|
||||
<string name="toast_failed_to_reboot_to_dynsystem">Can\u2019t restart or load dynamic system</string>
|
||||
|
||||
<!-- URL of Dynamic System Key Revocation List [DO NOT TRANSLATE] -->
|
||||
<string name="key_revocation_list_url" translatable="false">https://dl.google.com/developers/android/gsi/gsi-keyblacklist.json</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -46,6 +46,7 @@ import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.http.HttpResponseCache;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
@@ -60,6 +61,8 @@ import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -146,10 +149,26 @@ public class DynamicSystemInstallationService extends Service
|
||||
prepareNotification();
|
||||
|
||||
mDynSystem = (DynamicSystemManager) getSystemService(Context.DYNAMIC_SYSTEM_SERVICE);
|
||||
|
||||
// Install an HttpResponseCache in the application cache directory so we can cache
|
||||
// gsi key revocation list. The http(s) protocol handler uses this cache transparently.
|
||||
// The cache size is chosen heuristically. Since we don't have too much traffic right now,
|
||||
// a moderate size of 1MiB should be enough.
|
||||
try {
|
||||
File httpCacheDir = new File(getCacheDir(), "httpCache");
|
||||
long httpCacheSize = 1 * 1024 * 1024; // 1 MiB
|
||||
HttpResponseCache.install(httpCacheDir, httpCacheSize);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "HttpResponseCache.install() failed: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
HttpResponseCache cache = HttpResponseCache.getInstalled();
|
||||
if (cache != null) {
|
||||
cache.flush();
|
||||
}
|
||||
// Cancel the persistent notification.
|
||||
mNM.cancel(NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import android.os.image.DynamicSystemManager;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -100,7 +102,9 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
|
||||
private final Context mContext;
|
||||
private final DynamicSystemManager mDynSystem;
|
||||
private final ProgressListener mListener;
|
||||
private final boolean mIsNetworkUrl;
|
||||
private DynamicSystemManager.Session mInstallationSession;
|
||||
private KeyRevocationList mKeyRevocationList;
|
||||
|
||||
private boolean mIsZip;
|
||||
private boolean mIsCompleted;
|
||||
@@ -123,6 +127,7 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
|
||||
mContext = context;
|
||||
mDynSystem = dynSystem;
|
||||
mListener = listener;
|
||||
mIsNetworkUrl = URLUtil.isNetworkUrl(mUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -152,9 +157,11 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO(yochiang): do post-install public key check (revocation list / boot-ramdisk)
|
||||
|
||||
mDynSystem.finishInstallation();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, e.toString(), e);
|
||||
mDynSystem.remove();
|
||||
return e;
|
||||
} finally {
|
||||
@@ -220,7 +227,7 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
|
||||
String.format(Locale.US, "Unsupported file format: %s", mUrl));
|
||||
}
|
||||
|
||||
if (URLUtil.isNetworkUrl(mUrl)) {
|
||||
if (mIsNetworkUrl) {
|
||||
mStream = new URL(mUrl).openStream();
|
||||
} else if (URLUtil.isFileUrl(mUrl)) {
|
||||
if (mIsZip) {
|
||||
@@ -234,6 +241,25 @@ class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Prog
|
||||
throw new UnsupportedUrlException(
|
||||
String.format(Locale.US, "Unsupported URL: %s", mUrl));
|
||||
}
|
||||
|
||||
// TODO(yochiang): Bypass this check if device is unlocked
|
||||
try {
|
||||
String listUrl = mContext.getString(R.string.key_revocation_list_url);
|
||||
mKeyRevocationList = KeyRevocationList.fromUrl(new URL(listUrl));
|
||||
} catch (IOException | JSONException e) {
|
||||
Log.d(TAG, "Failed to fetch Dynamic System Key Revocation List");
|
||||
mKeyRevocationList = new KeyRevocationList();
|
||||
keyRevocationThrowOrWarning(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void keyRevocationThrowOrWarning(Exception e) throws Exception {
|
||||
if (mIsNetworkUrl) {
|
||||
throw e;
|
||||
} else {
|
||||
// If DSU is being installed from a local file URI, then be permissive
|
||||
Log.w(TAG, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void installUserdata() throws Exception {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 com.android.dynsystem;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.HashMap;
|
||||
|
||||
class KeyRevocationList {
|
||||
|
||||
private static final String TAG = "KeyRevocationList";
|
||||
|
||||
private static final String JSON_ENTRIES = "entries";
|
||||
private static final String JSON_PUBLIC_KEY = "public_key";
|
||||
private static final String JSON_STATUS = "status";
|
||||
private static final String JSON_REASON = "reason";
|
||||
|
||||
private static final String STATUS_REVOKED = "REVOKED";
|
||||
|
||||
@VisibleForTesting
|
||||
HashMap<String, RevocationStatus> mEntries;
|
||||
|
||||
static class RevocationStatus {
|
||||
final String mStatus;
|
||||
final String mReason;
|
||||
|
||||
RevocationStatus(String status, String reason) {
|
||||
mStatus = status;
|
||||
mReason = reason;
|
||||
}
|
||||
}
|
||||
|
||||
KeyRevocationList() {
|
||||
mEntries = new HashMap<String, RevocationStatus>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the revocation status of a public key.
|
||||
*
|
||||
* @return a RevocationStatus for |publicKey|, null if |publicKey| doesn't exist.
|
||||
*/
|
||||
RevocationStatus getRevocationStatusForKey(String publicKey) {
|
||||
return mEntries.get(publicKey);
|
||||
}
|
||||
|
||||
/** Test if a public key is revoked or not. */
|
||||
boolean isRevoked(String publicKey) {
|
||||
RevocationStatus entry = getRevocationStatusForKey(publicKey);
|
||||
return entry != null && TextUtils.equals(entry.mStatus, STATUS_REVOKED);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void addEntry(String publicKey, String status, String reason) {
|
||||
mEntries.put(publicKey, new RevocationStatus(status, reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a KeyRevocationList from a JSON String.
|
||||
*
|
||||
* @param jsonString the revocation list, for example:
|
||||
* <pre>{@code
|
||||
* {
|
||||
* "entries": [
|
||||
* {
|
||||
* "public_key": "00fa2c6637c399afa893fe83d85f3569998707d5",
|
||||
* "status": "REVOKED",
|
||||
* "reason": "Revocation Reason"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @throws JSONException if |jsonString| is malformed.
|
||||
*/
|
||||
static KeyRevocationList fromJsonString(String jsonString) throws JSONException {
|
||||
JSONObject jsonObject = new JSONObject(jsonString);
|
||||
KeyRevocationList list = new KeyRevocationList();
|
||||
Log.d(TAG, "Begin of revocation list");
|
||||
if (jsonObject.has(JSON_ENTRIES)) {
|
||||
JSONArray entries = jsonObject.getJSONArray(JSON_ENTRIES);
|
||||
for (int i = 0; i < entries.length(); ++i) {
|
||||
JSONObject entry = entries.getJSONObject(i);
|
||||
String publicKey = entry.getString(JSON_PUBLIC_KEY);
|
||||
String status = entry.getString(JSON_STATUS);
|
||||
String reason = entry.has(JSON_REASON) ? entry.getString(JSON_REASON) : "";
|
||||
list.addEntry(publicKey, status, reason);
|
||||
Log.d(TAG, "Revocation entry: " + entry.toString());
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "End of revocation list");
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a KeyRevocationList from a URL.
|
||||
*
|
||||
* @throws IOException if |url| is inaccessible.
|
||||
* @throws JSONException if fetched content is malformed.
|
||||
*/
|
||||
static KeyRevocationList fromUrl(URL url) throws IOException, JSONException {
|
||||
Log.d(TAG, "Fetch from URL: " + url.toString());
|
||||
// Force "conditional GET"
|
||||
// Force validate the cached result with server each time, and use the cached result
|
||||
// only if it is validated by server, else fetch new data from server.
|
||||
// Ref: https://developer.android.com/reference/android/net/http/HttpResponseCache#force-a-network-response
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setUseCaches(true);
|
||||
connection.addRequestProperty("Cache-Control", "max-age=0");
|
||||
try (InputStream stream = connection.getInputStream()) {
|
||||
return fromJsonString(readFully(stream));
|
||||
}
|
||||
}
|
||||
|
||||
private static String readFully(InputStream in) throws IOException {
|
||||
int n;
|
||||
byte[] buffer = new byte[4096];
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while ((n = in.read(buffer, 0, 4096)) > -1) {
|
||||
builder.append(new String(buffer, 0, n));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
15
packages/DynamicSystemInstallationService/tests/Android.bp
Normal file
15
packages/DynamicSystemInstallationService/tests/Android.bp
Normal file
@@ -0,0 +1,15 @@
|
||||
android_test {
|
||||
name: "DynamicSystemInstallationServiceTests",
|
||||
|
||||
srcs: ["src/**/*.java"],
|
||||
static_libs: [
|
||||
"androidx.test.runner",
|
||||
"androidx.test.rules",
|
||||
"mockito-target-minus-junit4",
|
||||
],
|
||||
|
||||
resource_dirs: ["res"],
|
||||
platform_apis: true,
|
||||
instrumentation_for: "DynamicSystemInstallationService",
|
||||
certificate: "platform",
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.dynsystem.tests">
|
||||
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
|
||||
android:targetPackage="com.android.dynsystem"
|
||||
android:label="Tests for DynamicSystemInstallationService" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- testFromJsonString -->
|
||||
<string name="blacklist_json_string" translatable="false">
|
||||
{
|
||||
\"entries\":[
|
||||
{
|
||||
\"public_key\":\"00fa2c6637c399afa893fe83d85f3569998707d5\",
|
||||
\"status\":\"REVOKED\",
|
||||
\"reason\":\"Key revocation test key\"
|
||||
},
|
||||
{
|
||||
\"public_key\":\"key2\",
|
||||
\"status\":\"REVOKED\"
|
||||
}
|
||||
]
|
||||
}
|
||||
</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 com.android.dynsystem;
|
||||
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
/**
|
||||
* A test for KeyRevocationList.java
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class KeyRevocationListTest {
|
||||
|
||||
private static final String TAG = "KeyRevocationListTest";
|
||||
|
||||
private static Context sContext;
|
||||
|
||||
private static String sBlacklistJsonString;
|
||||
|
||||
@BeforeClass
|
||||
public static void setUpClass() throws Exception {
|
||||
sContext = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
sBlacklistJsonString =
|
||||
sContext.getString(com.android.dynsystem.tests.R.string.blacklist_json_string);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
public void testFromJsonString() throws JSONException {
|
||||
KeyRevocationList blacklist;
|
||||
blacklist = KeyRevocationList.fromJsonString(sBlacklistJsonString);
|
||||
Assert.assertNotNull(blacklist);
|
||||
Assert.assertFalse(blacklist.mEntries.isEmpty());
|
||||
blacklist = KeyRevocationList.fromJsonString("{}");
|
||||
Assert.assertNotNull(blacklist);
|
||||
Assert.assertTrue(blacklist.mEntries.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
public void testFromUrl() throws IOException, JSONException {
|
||||
URLConnection mockConnection = mock(URLConnection.class);
|
||||
doReturn(new ByteArrayInputStream(sBlacklistJsonString.getBytes()))
|
||||
.when(mockConnection).getInputStream();
|
||||
URL mockUrl = new URL(
|
||||
"http", // protocol
|
||||
"foo.bar", // host
|
||||
80, // port
|
||||
"baz", // file
|
||||
new URLStreamHandler() {
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) {
|
||||
return mockConnection;
|
||||
}
|
||||
});
|
||||
URL mockBadUrl = new URL(
|
||||
"http", // protocol
|
||||
"foo.bar", // host
|
||||
80, // port
|
||||
"baz", // file
|
||||
new URLStreamHandler() {
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
});
|
||||
|
||||
KeyRevocationList blacklist = KeyRevocationList.fromUrl(mockUrl);
|
||||
Assert.assertNotNull(blacklist);
|
||||
Assert.assertFalse(blacklist.mEntries.isEmpty());
|
||||
|
||||
blacklist = null;
|
||||
try {
|
||||
blacklist = KeyRevocationList.fromUrl(mockBadUrl);
|
||||
// Up should throw, down should be unreachable
|
||||
Assert.fail("Expected IOException not thrown");
|
||||
} catch (IOException e) {
|
||||
// This is expected, do nothing
|
||||
}
|
||||
Assert.assertNull(blacklist);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
public void testIsRevoked() {
|
||||
KeyRevocationList blacklist = new KeyRevocationList();
|
||||
blacklist.addEntry("key1", "REVOKED", "reason for key1");
|
||||
|
||||
KeyRevocationList.RevocationStatus revocationStatus =
|
||||
blacklist.getRevocationStatusForKey("key1");
|
||||
Assert.assertNotNull(revocationStatus);
|
||||
Assert.assertEquals(revocationStatus.mReason, "reason for key1");
|
||||
|
||||
revocationStatus = blacklist.getRevocationStatusForKey("key2");
|
||||
Assert.assertNull(revocationStatus);
|
||||
|
||||
Assert.assertTrue(blacklist.isRevoked("key1"));
|
||||
Assert.assertFalse(blacklist.isRevoked("key2"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user