diff options
author | Ruslan Tkhakokhov <rthakohov@google.com> | 2019-09-26 22:03:50 +0100 |
---|---|---|
committer | Ruslan Tkhakokhov <rthakohov@google.com> | 2019-09-27 19:28:19 +0000 |
commit | 16857a093605babb8c9e8dc5357a7ee9e5e4f258 (patch) | |
tree | c67e7f1d59237b4e85849b308bf39c81b41bff4c /packages/BackupEncryption/src | |
parent | 4bec4b2fd9e246476be840dc1f751e4cdc861a8d (diff) |
Import RotateSecondaryKeyTask
Bug: 111386661
Test: atest RotateSecondaryKeyTaskTest
atest FakeCryptoBackupServerTest
Change-Id: I7f3c2c901c710863033e397af0b1cd4f76ee03be
Diffstat (limited to 'packages/BackupEncryption/src')
4 files changed, 355 insertions, 0 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java new file mode 100644 index 000000000000..9e31385c9525 --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java @@ -0,0 +1,30 @@ +/* + * 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.client; + +/** + * Error thrown when the user attempts to retrieve a key set from the server, but is asking for keys + * from an inactive secondary. + * + * <p>Although we could just return old keys, there is no good reason to do this. It almost + * certainly indicates a logic error on the client. + */ +public class UnexpectedActiveSecondaryOnServerException extends Exception { + public UnexpectedActiveSecondaryOnServerException(String message) { + super(message); + } +} diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java new file mode 100644 index 000000000000..2e8a61f05970 --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * Error thrown when the server's active secondary key does not exist in the user's recoverable + * keychain. This means the backup data cannot be decrypted, and should be wiped. + */ +public class ActiveSecondaryNotInKeychainException extends Exception { + public ActiveSecondaryNotInKeychainException(String message) { + super(message); + } +} diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java new file mode 100644 index 000000000000..72e8a89f1df3 --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * Error thrown if attempting to rotate key when there is no current active secondary key set + * locally. This means the device needs to re-initialize, asking the backup server what the active + * secondary key is. + */ +public class NoActiveSecondaryKeyException extends Exception { + public NoActiveSecondaryKeyException(String message) { + super(message); + } +} diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java new file mode 100644 index 000000000000..d58cb66ef6b4 --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java @@ -0,0 +1,270 @@ +/* + * 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 static com.android.internal.util.Preconditions.checkNotNull; + +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.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 = checkNotNull(secondaryKeyManager); + mCryptoSettings = checkNotNull(cryptoSettings); + mBackupServer = checkNotNull(backupServer); + mRecoveryController = checkNotNull(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<RecoverableKeyStoreSecondaryKey> 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<RecoverableKeyStoreSecondaryKey> getNextKey() + throws InternalRecoveryServiceException, UnrecoverableKeyException { + Optional<String> 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<String, SecretKey> 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<String, WrappedKeyProto.WrappedKey> 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<String> 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<RecoverableKeyStoreSecondaryKey> 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<String, WrappedKeyProto.WrappedKey> rewrapAll( + RecoverableKeyStoreSecondaryKey newSecondaryKey, Map<String, SecretKey> tertiaryKeys) + throws InvalidKeyException, IllegalBlockSizeException, NoSuchPaddingException, + NoSuchAlgorithmException { + Map<String, WrappedKeyProto.WrappedKey> wrappedKeys = new HashMap<>(); + + for (String packageName : tertiaryKeys.keySet()) { + SecretKey tertiaryKey = tertiaryKeys.get(packageName); + wrappedKeys.put( + packageName, KeyWrapUtils.wrap(newSecondaryKey.getSecretKey(), tertiaryKey)); + } + + return wrappedKeys; + } +} |