1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.dynsystem;
18 
19 import android.text.TextUtils;
20 import android.util.Log;
21 
22 import com.android.internal.annotations.VisibleForTesting;
23 
24 import org.json.JSONArray;
25 import org.json.JSONException;
26 import org.json.JSONObject;
27 
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.net.URL;
31 import java.net.URLConnection;
32 import java.util.HashMap;
33 
34 class KeyRevocationList {
35 
36     private static final String TAG = "KeyRevocationList";
37 
38     private static final String JSON_ENTRIES = "entries";
39     private static final String JSON_PUBLIC_KEY = "public_key";
40     private static final String JSON_STATUS = "status";
41     private static final String JSON_REASON = "reason";
42 
43     private static final String STATUS_REVOKED = "REVOKED";
44 
45     @VisibleForTesting
46     HashMap<String, RevocationStatus> mEntries;
47 
48     static class RevocationStatus {
49         final String mStatus;
50         final String mReason;
51 
RevocationStatus(String status, String reason)52         RevocationStatus(String status, String reason) {
53             mStatus = status;
54             mReason = reason;
55         }
56     }
57 
KeyRevocationList()58     KeyRevocationList() {
59         mEntries = new HashMap<String, RevocationStatus>();
60     }
61 
62     /**
63      * Returns the revocation status of a public key.
64      *
65      * @return a RevocationStatus for |publicKey|, null if |publicKey| doesn't exist.
66      */
getRevocationStatusForKey(String publicKey)67     RevocationStatus getRevocationStatusForKey(String publicKey) {
68         return mEntries.get(publicKey);
69     }
70 
71     /** Test if a public key is revoked or not. */
isRevoked(String publicKey)72     boolean isRevoked(String publicKey) {
73         RevocationStatus entry = getRevocationStatusForKey(publicKey);
74         return entry != null && TextUtils.equals(entry.mStatus, STATUS_REVOKED);
75     }
76 
77     @VisibleForTesting
addEntry(String publicKey, String status, String reason)78     void addEntry(String publicKey, String status, String reason) {
79         mEntries.put(publicKey, new RevocationStatus(status, reason));
80     }
81 
82     /**
83      * Creates a KeyRevocationList from a JSON String.
84      *
85      * @param jsonString the revocation list, for example:
86      *     <pre>{@code
87      *      {
88      *        "entries": [
89      *          {
90      *            "public_key": "00fa2c6637c399afa893fe83d85f3569998707d5",
91      *            "status": "REVOKED",
92      *            "reason": "Revocation Reason"
93      *          }
94      *        ]
95      *      }
96      *     }</pre>
97      *
98      * @throws JSONException if |jsonString| is malformed.
99      */
fromJsonString(String jsonString)100     static KeyRevocationList fromJsonString(String jsonString) throws JSONException {
101         JSONObject jsonObject = new JSONObject(jsonString);
102         KeyRevocationList list = new KeyRevocationList();
103         Log.d(TAG, "Begin of revocation list");
104         if (jsonObject.has(JSON_ENTRIES)) {
105             JSONArray entries = jsonObject.getJSONArray(JSON_ENTRIES);
106             for (int i = 0; i < entries.length(); ++i) {
107                 JSONObject entry = entries.getJSONObject(i);
108                 String publicKey = entry.getString(JSON_PUBLIC_KEY);
109                 String status = entry.getString(JSON_STATUS);
110                 String reason = entry.has(JSON_REASON) ? entry.getString(JSON_REASON) : "";
111                 list.addEntry(publicKey, status, reason);
112                 Log.d(TAG, "Revocation entry: " + entry.toString());
113             }
114         }
115         Log.d(TAG, "End of revocation list");
116         return list;
117     }
118 
119     /**
120      * Creates a KeyRevocationList from a URL.
121      *
122      * @throws IOException if |url| is inaccessible.
123      * @throws JSONException if fetched content is malformed.
124      */
fromUrl(URL url)125     static KeyRevocationList fromUrl(URL url) throws IOException, JSONException {
126         Log.d(TAG, "Fetch from URL: " + url.toString());
127         // Force "conditional GET"
128         // Force validate the cached result with server each time, and use the cached result
129         // only if it is validated by server, else fetch new data from server.
130         // Ref: https://developer.android.com/reference/android/net/http/HttpResponseCache#force-a-network-response
131         URLConnection connection = url.openConnection();
132         connection.setUseCaches(true);
133         connection.addRequestProperty("Cache-Control", "max-age=0");
134         try (InputStream stream = connection.getInputStream()) {
135             return fromJsonString(readFully(stream));
136         }
137     }
138 
readFully(InputStream in)139     private static String readFully(InputStream in) throws IOException {
140         int n;
141         byte[] buffer = new byte[4096];
142         StringBuilder builder = new StringBuilder();
143         while ((n = in.read(buffer, 0, 4096)) > -1) {
144             builder.append(new String(buffer, 0, n));
145         }
146         return builder.toString();
147     }
148 }
149