1 /*
2  * Copyright (C) 2022 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.rkpdapp.service;
18 
19 import android.content.Context;
20 import android.os.IBinder;
21 import android.os.RemoteException;
22 import android.util.Log;
23 
24 import androidx.annotation.GuardedBy;
25 
26 import com.android.rkpdapp.GeekResponse;
27 import com.android.rkpdapp.IGetKeyCallback;
28 import com.android.rkpdapp.IRegistration;
29 import com.android.rkpdapp.IStoreUpgradedKeyCallback;
30 import com.android.rkpdapp.RemotelyProvisionedKey;
31 import com.android.rkpdapp.RkpdException;
32 import com.android.rkpdapp.database.ProvisionedKey;
33 import com.android.rkpdapp.database.ProvisionedKeyDao;
34 import com.android.rkpdapp.interfaces.ServerInterface;
35 import com.android.rkpdapp.interfaces.SystemInterface;
36 import com.android.rkpdapp.metrics.ProvisioningAttempt;
37 import com.android.rkpdapp.metrics.RkpdClientOperation;
38 import com.android.rkpdapp.provisioner.Provisioner;
39 import com.android.rkpdapp.utils.Settings;
40 
41 import java.time.Duration;
42 import java.time.Instant;
43 import java.util.Arrays;
44 import java.util.Collections;
45 import java.util.HashMap;
46 import java.util.concurrent.ExecutorService;
47 import java.util.concurrent.Future;
48 
49 import co.nstant.in.cbor.CborException;
50 
51 /**
52  * Implementation of com.android.rkpdapp.IRegistration, which fetches keys for a (caller UID,
53  * IRemotelyProvisionedComponent) tuple.
54  */
55 public final class RegistrationBinder extends IRegistration.Stub {
56     // The minimum amount of time that the registration will consider a key valid. If a key expires
57     // before this time elapses, then the key is considered too stale and will not be used.
58     public static final Duration MIN_KEY_LIFETIME = Duration.ofHours(1);
59 
60     static final String TAG = "RkpdRegistrationBinder";
61 
62     private final Context mContext;
63     private final int mClientUid;
64     private final SystemInterface mSystemInterface;
65     private final ProvisionedKeyDao mProvisionedKeyDao;
66     private final ServerInterface mRkpServer;
67     private final Provisioner mProvisioner;
68     private final ExecutorService mThreadPool;
69     private final Object mTasksLock = new Object();
70     @GuardedBy("mTasksLock")
71     private final HashMap<IBinder, Future<?>> mTasks = new HashMap<>();
72 
RegistrationBinder(Context context, int clientUid, SystemInterface systemInterface, ProvisionedKeyDao provisionedKeyDao, ServerInterface rkpServer, Provisioner provisioner, ExecutorService threadPool)73     public RegistrationBinder(Context context, int clientUid, SystemInterface systemInterface,
74             ProvisionedKeyDao provisionedKeyDao, ServerInterface rkpServer,
75             Provisioner provisioner, ExecutorService threadPool) {
76         mContext = context;
77         mClientUid = clientUid;
78         mSystemInterface = systemInterface;
79         mProvisionedKeyDao = provisionedKeyDao;
80         mRkpServer = rkpServer;
81         mProvisioner = provisioner;
82         mThreadPool = threadPool;
83     }
84 
getKeyWorker(int keyId, IGetKeyCallback callback)85     private void getKeyWorker(int keyId, IGetKeyCallback callback)
86             throws CborException, InterruptedException, RkpdException {
87         Log.i(TAG, "Key requested for : " + mSystemInterface.getServiceName() + ", clientUid: "
88                 + mClientUid + ", keyId: " + keyId + ", callback: "
89                 + callback.asBinder().hashCode());
90         // Use reduced look-ahead to get rid of soon-to-be expired keys, because the periodic
91         // provisioner should be ensuring that old keys are already expired. However, in the
92         // edge case that periodic provisioning didn't work, we want to allow slightly "more stale"
93         // keys to be used. This reduces window of time in which key attestation is not available
94         // (e.g. if there is a provisioning server outage). Note that we must have some look-ahead,
95         // rather than using "now", else we might return a key that expires so soon that the caller
96         // can never successfully use it.
97         final Instant minExpiry = Instant.now().plus(MIN_KEY_LIFETIME);
98         mProvisionedKeyDao.deleteExpiringKeys(minExpiry);
99 
100         ProvisionedKey assignedKey = mProvisionedKeyDao.getKeyForClientAndIrpc(
101                 mSystemInterface.getServiceName(), mClientUid, keyId);
102 
103         if (assignedKey == null) {
104             assignedKey = tryToAssignKey(minExpiry, keyId);
105         }
106 
107         if (assignedKey == null) {
108             // Since provisionKeys goes over the network, this represents our last chancel to cancel
109             // before we go off and hit the network. It's not worth checking for interruption prior
110             // to this, because none of the prior work is long-running.
111             checkForCancel();
112 
113             Log.i(TAG, "No keys are available, kicking off provisioning");
114             checkedCallback(callback::onProvisioningNeeded);
115             try (ProvisioningAttempt metrics = ProvisioningAttempt.createOutOfKeysAttemptMetrics(
116                     mContext, mSystemInterface.getServiceName())) {
117                 fetchGeekAndProvisionKeys(metrics);
118             }
119             assignedKey = tryToAssignKey(minExpiry, keyId);
120         }
121 
122         // Now that we've gotten back from our network round-trip, it's possible an interrupt came
123         // in, so deal with it. However, it's most likely that an InterruptedException came from
124         // the SDK while we were sitting on the socket down in Provisioner.provisionKeys.
125         checkForCancel();
126 
127         if (assignedKey == null) {
128             // This can happen if provisioning is disabled on the device for some reason,
129             // or if we're not connected to the internet.
130             Log.e(TAG, "Unable to provision keys");
131             checkedCallback(() -> callback.onError(IGetKeyCallback.Error.ERROR_UNKNOWN,
132                     "Provisioning failed, no keys available"));
133         } else {
134             Log.i(TAG, "Key successfully assigned to client");
135             RemotelyProvisionedKey key = new RemotelyProvisionedKey();
136             key.keyBlob = assignedKey.keyBlob;
137             key.encodedCertChain = assignedKey.certificateChain;
138             checkedCallback(() -> callback.onSuccess(key));
139         }
140     }
141 
fetchGeekAndProvisionKeys(ProvisioningAttempt metrics)142     private void fetchGeekAndProvisionKeys(ProvisioningAttempt metrics)
143             throws CborException, RkpdException, InterruptedException {
144         GeekResponse response = mRkpServer.fetchGeekAndUpdate(metrics);
145         if (response.numExtraAttestationKeys == 0) {
146             Log.v(TAG, "Provisioning disabled.");
147             metrics.setEnablement(ProvisioningAttempt.Enablement.DISABLED);
148             metrics.setStatus(ProvisioningAttempt.Status.PROVISIONING_DISABLED);
149             return;
150         }
151         mProvisioner.provisionKeys(metrics, mSystemInterface, response);
152     }
153 
tryToAssignKey(Instant minExpiry, int keyId)154     private ProvisionedKey tryToAssignKey(Instant minExpiry, int keyId) {
155         // Since we're going to be assigning a fresh key to the app, we ideally want a key that's
156         // longer-lived than the minimum. We use the server-configured expiration, which is normally
157         // days, as the preferred lifetime for a key. However, if we cannot find a key that is valid
158         // for that long, we'll settle for a shorter-lived key.
159         Instant[] expirations = new Instant[]{
160                 Instant.now().plus(Settings.getExpiringBy(mContext)),
161                 minExpiry
162         };
163         Arrays.sort(expirations, Collections.reverseOrder());
164         for (Instant expiry : expirations) {
165             Log.i(TAG, "No key assigned, looking for an available key with expiry of " + expiry);
166             ProvisionedKey key = mProvisionedKeyDao.getOrAssignKey(
167                     mSystemInterface.getServiceName(),
168                     expiry, mClientUid, keyId);
169             if (key != null) {
170                 provisionKeysOnKeyConsumed();
171                 return key;
172             }
173         }
174         return null;
175     }
176 
provisionKeysOnKeyConsumed()177     private void provisionKeysOnKeyConsumed() {
178         try (ProvisioningAttempt metrics = ProvisioningAttempt.createKeyConsumedAttemptMetrics(
179                 mContext, mSystemInterface.getServiceName())) {
180             if (!mProvisioner.isProvisioningNeeded(metrics, mSystemInterface.getServiceName())) {
181                 metrics.setStatus(ProvisioningAttempt.Status.NO_PROVISIONING_NEEDED);
182                 return;
183             }
184 
185             mThreadPool.execute(() -> {
186                 try {
187                     fetchGeekAndProvisionKeys(metrics);
188                 } catch (CborException | RkpdException | InterruptedException e) {
189                     Log.e(TAG, "Error provisioning keys", e);
190                 }
191             });
192         }
193     }
194 
checkForCancel()195     private void checkForCancel() throws InterruptedException {
196         if (Thread.currentThread().isInterrupted()) {
197             throw new InterruptedException();
198         }
199     }
200 
201     private interface CallbackWrapper {
run()202         void run() throws RemoteException;
203     }
204 
checkedCallback(CallbackWrapper wrapper)205     private void checkedCallback(CallbackWrapper wrapper) {
206         try {
207             wrapper.run();
208         } catch (RemoteException e) {
209             // This should only ever happen if there's a binder issue invoking the callback
210             Log.e(TAG, "Error performing client callback", e);
211         }
212     }
213 
214     @Override
getKey(int keyId, IGetKeyCallback callback)215     public void getKey(int keyId, IGetKeyCallback callback) {
216         synchronized (mTasksLock) {
217             if (mTasks.containsKey(callback.asBinder())) {
218                 throw new IllegalArgumentException("Callback " + callback.asBinder().hashCode()
219                         + " is already associated with a getKey operation that is in-progress");
220             }
221 
222             mTasks.put(callback.asBinder(),
223                     mThreadPool.submit(() -> getKeyThreadWorker(keyId, callback)));
224         }
225     }
226 
getKeyThreadWorker(int keyId, IGetKeyCallback callback)227     private void getKeyThreadWorker(int keyId, IGetKeyCallback callback) {
228         // We don't use a try-with-resources here because the metric may need to be updated
229         // inside an exception handler, but close would have been called prior to that. Therefore,
230         // we explicitly close the metric explicitly in the "finally" block, after all handlers
231         // have had a chance to run.
232         RkpdClientOperation metric = RkpdClientOperation.getKey(mClientUid,
233                 mSystemInterface.getServiceName());
234         try {
235             getKeyWorker(keyId, callback);
236             metric.setResult(RkpdClientOperation.Result.SUCCESS);
237         } catch (InterruptedException e) {
238             Log.i(TAG, "getKey was interrupted");
239             metric.setResult(RkpdClientOperation.Result.CANCELED);
240             checkedCallback(callback::onCancel);
241         } catch (RkpdException e) {
242             Log.e(TAG, "RKPD failed to provision keys", e);
243             final byte mappedError = mapToGetKeyError(e, metric);
244             checkedCallback(
245                     () -> callback.onError(mappedError, e.getMessage()));
246         } catch (Exception e) {
247             // Do our best to inform the callback when the unexpected happens. Otherwise,
248             // the caller is going to wait until they timeout without knowing something like
249             // a RuntimeException occurred.
250             Log.e(TAG, "Unexpected error provisioning keys", e);
251             checkedCallback(() -> callback.onError(IGetKeyCallback.Error.ERROR_UNKNOWN,
252                     e.getMessage()));
253         } finally {
254             metric.close();
255             synchronized (mTasksLock) {
256                 mTasks.remove(callback.asBinder());
257             }
258         }
259     }
260 
261     /** Maps an RkpdException into an IGetKeyCallback.Error value. */
mapToGetKeyError(RkpdException e, RkpdClientOperation metric)262     private byte mapToGetKeyError(RkpdException e, RkpdClientOperation metric) {
263         switch (e.getErrorCode()) {
264             case NO_NETWORK_CONNECTIVITY:
265                 metric.setResult(RkpdClientOperation.Result.ERROR_PENDING_INTERNET_CONNECTIVITY);
266                 return IGetKeyCallback.Error.ERROR_PENDING_INTERNET_CONNECTIVITY;
267 
268             case DEVICE_NOT_REGISTERED:
269                 metric.setResult(RkpdClientOperation.Result.ERROR_PERMANENT);
270                 return IGetKeyCallback.Error.ERROR_PERMANENT;
271 
272             case INTERNAL_ERROR:
273                 metric.setResult(RkpdClientOperation.Result.ERROR_INTERNAL);
274                 return IGetKeyCallback.Error.ERROR_UNKNOWN;
275 
276             case NETWORK_COMMUNICATION_ERROR:
277             case HTTP_CLIENT_ERROR:
278             case HTTP_SERVER_ERROR:
279             case HTTP_UNKNOWN_ERROR:
280             default:
281                 return IGetKeyCallback.Error.ERROR_UNKNOWN;
282         }
283     }
284 
285     @Override
cancelGetKey(IGetKeyCallback callback)286     public void cancelGetKey(IGetKeyCallback callback) throws RemoteException {
287         Log.i(TAG, "cancelGetKey(" + callback.asBinder().hashCode() + ")");
288         synchronized (mTasksLock) {
289             try (RkpdClientOperation metric = RkpdClientOperation.cancelGetKey(mClientUid,
290                     mSystemInterface.getServiceName())) {
291                 Future<?> task = mTasks.get(callback.asBinder());
292 
293                 if (task == null) {
294                     Log.w(TAG, "callback not found, task may have already completed");
295                 } else if (task.isDone()) {
296                     Log.w(TAG, "task already completed, not cancelling");
297                 } else if (task.isCancelled()) {
298                     Log.w(TAG, "task already cancelled, cannot cancel it any further");
299                 } else {
300                     task.cancel(true);
301                 }
302                 metric.setResult(RkpdClientOperation.Result.SUCCESS);
303             }
304         }
305     }
306 
307     @Override
storeUpgradedKeyAsync(byte[] oldKeyBlob, byte[] newKeyBlob, IStoreUpgradedKeyCallback callback)308     public void storeUpgradedKeyAsync(byte[] oldKeyBlob, byte[] newKeyBlob,
309             IStoreUpgradedKeyCallback callback) throws RemoteException {
310         Log.i(TAG, "storeUpgradedKeyAsync");
311         mThreadPool.execute(() -> {
312             try (RkpdClientOperation metric = RkpdClientOperation.storeUpgradedKey(
313                     mClientUid, mSystemInterface.getServiceName())) {
314                 int keysUpgraded = mProvisionedKeyDao.upgradeKeyBlob(mClientUid, oldKeyBlob,
315                         newKeyBlob);
316                 if (keysUpgraded == 1) {
317                     metric.setResult(RkpdClientOperation.Result.SUCCESS);
318                     checkedCallback(callback::onSuccess);
319                 } else if (keysUpgraded == 0) {
320                     metric.setResult(RkpdClientOperation.Result.ERROR_KEY_NOT_FOUND);
321                     checkedCallback(() -> callback.onError("No keys matching oldKeyBlob found"));
322                 } else {
323                     metric.setResult(RkpdClientOperation.Result.ERROR_INTERNAL);
324                     Log.e(TAG, "Multiple keys matched the upgrade (" + keysUpgraded
325                             + "). This should be impossible!");
326                     checkedCallback(() -> callback.onError("Internal error"));
327                 }
328             } catch (Exception e) {
329                 checkedCallback(() -> callback.onError(e.getMessage()));
330             }
331         });
332     }
333 }
334