/* * 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.server.backup.encryption.tasks; import static android.os.Build.VERSION_CODES.P; import android.content.Context; import android.security.keystore.recovery.InternalRecoveryServiceException; import android.security.keystore.recovery.RecoveryController; import android.util.Slog; import com.android.server.backup.encryption.CryptoSettings; import com.android.server.backup.encryption.client.CryptoBackupServer; import com.android.server.backup.encryption.keys.KeyWrapUtils; import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey; import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager; import com.android.server.backup.encryption.keys.TertiaryKeyStore; import com.android.server.backup.encryption.protos.nano.WrappedKeyProto; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; /** * Finishes a rotation for a {@link * com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey}. */ public class RotateSecondaryKeyTask { private static final String TAG = "RotateSecondaryKeyTask"; private final Context mContext; private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager; private final CryptoBackupServer mBackupServer; private final CryptoSettings mCryptoSettings; private final RecoveryController mRecoveryController; /** * A new instance. * * @param secondaryKeyManager For loading the currently active and next secondary key. * @param backupServer For loading and storing tertiary keys and for setting active secondary * key. * @param cryptoSettings For checking the stored aliases for the next and active key. * @param recoveryController For communicating with the Framework apis. */ public RotateSecondaryKeyTask( Context context, RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager, CryptoBackupServer backupServer, CryptoSettings cryptoSettings, RecoveryController recoveryController) { mContext = context; mSecondaryKeyManager = Objects.requireNonNull(secondaryKeyManager); mCryptoSettings = Objects.requireNonNull(cryptoSettings); mBackupServer = Objects.requireNonNull(backupServer); mRecoveryController = Objects.requireNonNull(recoveryController); } /** Runs the task. */ public void run() { // Never run more than one of these at the same time. synchronized (RotateSecondaryKeyTask.class) { runInternal(); } } private void runInternal() { Optional maybeNextKey; try { maybeNextKey = getNextKey(); } catch (Exception e) { Slog.e(TAG, "Error checking for next key", e); return; } if (!maybeNextKey.isPresent()) { Slog.d(TAG, "No secondary key rotation task pending. Exiting."); return; } RecoverableKeyStoreSecondaryKey nextKey = maybeNextKey.get(); boolean isReady; try { isReady = isSecondaryKeyRotationReady(nextKey); } catch (InternalRecoveryServiceException e) { Slog.e(TAG, "Error encountered checking whether next secondary key is synced", e); return; } if (!isReady) { return; } try { rotateToKey(nextKey); } catch (Exception e) { Slog.e(TAG, "Error trying to rotate to new secondary key", e); } } private Optional getNextKey() throws InternalRecoveryServiceException, UnrecoverableKeyException { Optional maybeNextAlias = mCryptoSettings.getNextSecondaryKeyAlias(); if (!maybeNextAlias.isPresent()) { return Optional.empty(); } return mSecondaryKeyManager.get(maybeNextAlias.get()); } private boolean isSecondaryKeyRotationReady(RecoverableKeyStoreSecondaryKey nextKey) throws InternalRecoveryServiceException { String nextAlias = nextKey.getAlias(); Slog.i(TAG, "Key rotation to " + nextAlias + " is pending. Checking key sync status."); int status = mRecoveryController.getRecoveryStatus(nextAlias); if (status == RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE) { Slog.e( TAG, "Permanent failure to sync " + nextAlias + ". Cannot possibly rotate to it."); mCryptoSettings.removeNextSecondaryKeyAlias(); return false; } if (status == RecoveryController.RECOVERY_STATUS_SYNCED) { Slog.i(TAG, "Secondary key " + nextAlias + " has now synced! Commencing rotation."); } else { Slog.i(TAG, "Sync still pending for " + nextAlias); } return status == RecoveryController.RECOVERY_STATUS_SYNCED; } /** * @throws ActiveSecondaryNotInKeychainException if the currently active secondary key is not in * the keychain. * @throws IOException if there is an IO issue communicating with the server or loading from * disk. * @throws NoActiveSecondaryKeyException if there is no active key set. * @throws IllegalBlockSizeException if there is an issue decrypting a tertiary key. * @throws InvalidKeyException if any of the secondary keys cannot be used for wrapping or * unwrapping tertiary keys. */ private void rotateToKey(RecoverableKeyStoreSecondaryKey newSecondaryKey) throws ActiveSecondaryNotInKeychainException, IOException, NoActiveSecondaryKeyException, IllegalBlockSizeException, InvalidKeyException, InternalRecoveryServiceException, UnrecoverableKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException { RecoverableKeyStoreSecondaryKey activeSecondaryKey = getActiveSecondaryKey(); String activeSecondaryKeyAlias = activeSecondaryKey.getAlias(); String newSecondaryKeyAlias = newSecondaryKey.getAlias(); if (newSecondaryKeyAlias.equals(activeSecondaryKeyAlias)) { Slog.i(TAG, activeSecondaryKeyAlias + " was already the active alias."); return; } TertiaryKeyStore tertiaryKeyStore = TertiaryKeyStore.newInstance(mContext, activeSecondaryKey); Map tertiaryKeys = tertiaryKeyStore.getAll(); if (tertiaryKeys.isEmpty()) { Slog.i( TAG, "No tertiary keys for " + activeSecondaryKeyAlias + ". No need to rewrap. "); mBackupServer.setActiveSecondaryKeyAlias( newSecondaryKeyAlias, /*tertiaryKeys=*/ Collections.emptyMap()); } else { Map rewrappedTertiaryKeys = rewrapAll(newSecondaryKey, tertiaryKeys); TertiaryKeyStore.newInstance(mContext, newSecondaryKey).putAll(rewrappedTertiaryKeys); Slog.i( TAG, "Successfully rewrapped " + rewrappedTertiaryKeys.size() + " tertiary keys"); mBackupServer.setActiveSecondaryKeyAlias(newSecondaryKeyAlias, rewrappedTertiaryKeys); Slog.i( TAG, "Successfully uploaded new set of tertiary keys to " + newSecondaryKeyAlias + " alias"); } mCryptoSettings.setActiveSecondaryKeyAlias(newSecondaryKeyAlias); mCryptoSettings.removeNextSecondaryKeyAlias(); try { mRecoveryController.removeKey(activeSecondaryKeyAlias); } catch (InternalRecoveryServiceException e) { Slog.e(TAG, "Error removing old secondary key from RecoverableKeyStoreLoader", e); } } private RecoverableKeyStoreSecondaryKey getActiveSecondaryKey() throws NoActiveSecondaryKeyException, ActiveSecondaryNotInKeychainException, InternalRecoveryServiceException, UnrecoverableKeyException { Optional activeSecondaryAlias = mCryptoSettings.getActiveSecondaryKeyAlias(); if (!activeSecondaryAlias.isPresent()) { Slog.i( TAG, "Was asked to rotate secondary key, but local config did not have a secondary " + "key alias set."); throw new NoActiveSecondaryKeyException("No local active secondary key set."); } String activeSecondaryKeyAlias = activeSecondaryAlias.get(); Optional secondaryKey = mSecondaryKeyManager.get(activeSecondaryKeyAlias); if (!secondaryKey.isPresent()) { throw new ActiveSecondaryNotInKeychainException( String.format( Locale.US, "Had local active recoverable key alias of %s but key was not in" + " user's keychain.", activeSecondaryKeyAlias)); } return secondaryKey.get(); } /** * Rewraps all the tertiary keys. * * @param newSecondaryKey The secondary key with which to rewrap the tertiaries. * @param tertiaryKeys The tertiary keys, by package name. * @return The newly wrapped tertiary keys, by package name. * @throws InvalidKeyException if any key is unusable. * @throws IllegalBlockSizeException if could not decrypt. */ private Map rewrapAll( RecoverableKeyStoreSecondaryKey newSecondaryKey, Map tertiaryKeys) throws InvalidKeyException, IllegalBlockSizeException, NoSuchPaddingException, NoSuchAlgorithmException { Map wrappedKeys = new HashMap<>(); for (String packageName : tertiaryKeys.keySet()) { SecretKey tertiaryKey = tertiaryKeys.get(packageName); wrappedKeys.put( packageName, KeyWrapUtils.wrap(newSecondaryKey.getSecretKey(), tertiaryKey)); } return wrappedKeys; } }