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