diff options
author | Yo Chiang <yochiang@google.com> | 2020-02-05 19:01:54 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2020-02-05 19:01:54 +0000 |
commit | 739c45ea2364d880d08bfc9a27fdc3cec0f7eab6 (patch) | |
tree | c69711e66c727a15a10302ab1765985751623237 /packages/DynamicSystemInstallationService/src | |
parent | 05cf3e896c6d3e40c11ca3767bde535433636a6d (diff) | |
parent | 61fc692d6675a6b2f35c7ff53ea8fa88728a1e39 (diff) |
Merge "DSU to support GSI key revocation list"
Diffstat (limited to 'packages/DynamicSystemInstallationService/src')
3 files changed, 195 insertions, 2 deletions
diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java index 9ccb837cf613..9bae223a0a3e 100644 --- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java +++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/DynamicSystemInstallationService.java @@ -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); } diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java index 9aea0e713179..438c435ef0e4 100644 --- a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java +++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/InstallationAsyncTask.java @@ -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 { diff --git a/packages/DynamicSystemInstallationService/src/com/android/dynsystem/KeyRevocationList.java b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/KeyRevocationList.java new file mode 100644 index 000000000000..522bc547325b --- /dev/null +++ b/packages/DynamicSystemInstallationService/src/com/android/dynsystem/KeyRevocationList.java @@ -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(); + } +} |