Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 739c45ea authored by Yo Chiang's avatar Yo Chiang Committed by Gerrit Code Review
Browse files

Merge "DSU to support GSI key revocation list"

parents 05cf3e89 61fc692d
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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>
+19 −0
Original line number Diff line number Diff line
@@ -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);
    }
+28 −2
Original line number Diff line number Diff line
@@ -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 {
+148 −0
Original line number Diff line number Diff line
/*
 * 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 −0
Original line number Diff line number Diff line
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",
}
Loading