Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 16857a09 authored by Ruslan Tkhakokhov's avatar Ruslan Tkhakokhov
Browse files

Import RotateSecondaryKeyTask

Bug: 111386661
Test: atest RotateSecondaryKeyTaskTest
      atest FakeCryptoBackupServerTest

Change-Id: I7f3c2c901c710863033e397af0b1cd4f76ee03be
parent 4bec4b2f
Loading
Loading
Loading
Loading
+30 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+27 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+28 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+270 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+363 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading