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.provisioner;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.util.Log;
22 
23 import androidx.work.WorkManager;
24 import androidx.work.Worker;
25 import androidx.work.WorkerParameters;
26 
27 import com.android.rkpdapp.GeekResponse;
28 import com.android.rkpdapp.RkpdException;
29 import com.android.rkpdapp.database.ProvisionedKeyDao;
30 import com.android.rkpdapp.database.RkpdDatabase;
31 import com.android.rkpdapp.interfaces.ServerInterface;
32 import com.android.rkpdapp.interfaces.ServiceManagerInterface;
33 import com.android.rkpdapp.interfaces.SystemInterface;
34 import com.android.rkpdapp.metrics.ProvisioningAttempt;
35 import com.android.rkpdapp.metrics.RkpdStatsLog;
36 import com.android.rkpdapp.utils.Settings;
37 
38 import java.time.Instant;
39 import java.util.Arrays;
40 import java.util.concurrent.atomic.AtomicBoolean;
41 import java.util.concurrent.locks.ReentrantLock;
42 
43 import co.nstant.in.cbor.CborException;
44 
45 /**
46  * A class that extends Worker in order to be scheduled to maintain the attestation key pool at
47  * regular intervals. If the job determines that more keys need to be generated and signed, it would
48  * drive that process.
49  */
50 public class PeriodicProvisioner extends Worker {
51     public static final String UNIQUE_WORK_NAME = "ProvisioningJob";
52     private static final String TAG = "RkpdPeriodicProvisioner";
53     private static final boolean IS_ASYNC = true;
54 
55     private final Context mContext;
56     private final ProvisionedKeyDao mKeyDao;
57     private static final ReentrantLock sLock = new ReentrantLock();
58 
PeriodicProvisioner(@onNull Context context, @NonNull WorkerParameters params)59     public PeriodicProvisioner(@NonNull Context context, @NonNull WorkerParameters params) {
60         super(context, params);
61         mContext = context;
62         mKeyDao = RkpdDatabase.getDatabase(context).provisionedKeyDao();
63     }
64 
65     /**
66      * Holds a lock, preventing any work from proceeding.
67      * The returned object must be closed for PeriodicProvisioner to perform any future work.
68      */
lock()69     public static AutoCloseable lock() {
70         sLock.lock();
71         return new AutoCloseable() {
72             @Override
73             public void close() {
74                 sLock.unlock();
75             }
76         };
77     }
78 
79     /**
80      * Overrides the default doWork method to handle checking and provisioning the device.
81      */
82     @Override
83     public Result doWork() {
84         sLock.lock();
85         try {
86             return doSynchronizedWork();
87         } finally {
88             sLock.unlock();
89         }
90     }
91 
92     private Result doSynchronizedWork() {
93         Log.i(TAG, "Waking up; checking provisioning state.");
94 
95         SystemInterface[] irpcs = ServiceManagerInterface.getAllInstances();
96         if (irpcs.length == 0) {
97             Log.i(TAG, "Stopping periodic provisioner: there are no IRPC HALs");
98             WorkManager.getInstance(mContext).cancelWorkById(getId());
99             return Result.success();
100         }
101 
102         if (Settings.getDefaultUrl().isEmpty() || Settings.getUrl(mContext).isEmpty()) {
103             Log.i(TAG, "Stopping periodic provisioner: system has no configured server endpoint");
104             WorkManager.getInstance(mContext).cancelWorkById(getId());
105             return Result.success();
106         }
107 
108         try (ProvisioningAttempt metrics = ProvisioningAttempt.createScheduledAttemptMetrics(
109                 mContext)) {
110             // Clean up the expired keys
111             mKeyDao.deleteExpiringKeys(Instant.now());
112 
113             // Fetch geek from the server and figure out whether provisioning needs to be stopped.
114             GeekResponse response;
115             try {
116                 response = new ServerInterface(mContext, IS_ASYNC).fetchGeekAndUpdate(metrics);
117             } catch (InterruptedException | RkpdException e) {
118                 Log.e(TAG, "Error fetching configuration from the RKP server", e);
119                 return Result.failure();
120             }
121 
122             if (response.numExtraAttestationKeys == 0) {
123                 Log.i(TAG, "Disable provisioning and delete all keys.");
124                 metrics.setEnablement(ProvisioningAttempt.Enablement.DISABLED);
125                 metrics.setStatus(ProvisioningAttempt.Status.PROVISIONING_DISABLED);
126 
127                 mKeyDao.deleteAllKeys();
128                 metrics.setIsKeyPoolEmpty(true);
129                 return Result.success();
130             }
131 
132             Log.i(TAG, "Total services found implementing IRPC: " + irpcs.length);
133             Provisioner provisioner = new Provisioner(mContext, mKeyDao, IS_ASYNC);
134             provisioner.clearBadAttestationKeys(response);
135 
136             final AtomicBoolean result = new AtomicBoolean(true);
137             Arrays.stream(irpcs).parallel().forEach(irpc -> {
138                 Log.i(TAG, "Starting provisioning for " + irpc);
139                 try {
140                     provisioner.provisionKeys(metrics, irpc, response);
141                     recordKeyPoolStatsAtom(irpc);
142                     Log.i(TAG, "Successfully provisioned " + irpc);
143                 } catch (CborException e) {
144                     Log.e(TAG, "Error parsing CBOR for " + irpc, e);
145                     result.set(false);
146                 } catch (InterruptedException | RkpdException e) {
147                     Log.e(TAG, "Error provisioning keys for " + irpc, e);
148                     result.set(false);
149                 }
150             });
151             return result.get() ? Result.success() : Result.failure();
152         }
153     }
154 
155     private void recordKeyPoolStatsAtom(SystemInterface irpc) {
156         String halName = irpc.getServiceName();
157         final int numExpiring = mKeyDao.getTotalExpiringKeysForIrpc(halName,
158                 Settings.getExpirationTime(mContext));
159         final int numUnassigned = mKeyDao.getTotalUnassignedKeysForIrpc(halName);
160         final int total = mKeyDao.getTotalKeysForIrpc(halName);
161         Log.i(TAG, "Logging atom metric for pool status, total: " + total + ", numExpiring: "
162                 + numExpiring + ", numUnassigned: " + numUnassigned);
163         RkpdStatsLog.write(RkpdStatsLog.RKPD_POOL_STATS, irpc.getServiceName(), numExpiring,
164                 numUnassigned, total);
165     }
166 }
167