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

Commit 0fc353ce authored by Automerger Merge Worker's avatar Automerger Merge Worker
Browse files

Merge "DSU to support GSI key revocation list" am: 739c45ea

Change-Id: Ie2fd14d1fe4029a2c8178c36630167cea0e65af5
parents 04be13bb 739c45ea
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