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

Commit 15620adb authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Resume-on-Reboot: allow multiple users"

parents 9f7ea2df 12f4648a
Loading
Loading
Loading
Loading
+11 −33
Original line number Diff line number Diff line
@@ -26,16 +26,12 @@ import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * Holds the data necessary to complete a reboot escrow of the Synthetic Password.
@@ -47,17 +43,11 @@ class RebootEscrowData {
     */
    private static final int CURRENT_VERSION = 1;

    /** The secret key will be of this format. */
    private static final String KEY_ALGO = "AES";

    /** The key size used for encrypting the reboot escrow data. */
    private static final int KEY_SIZE_BITS = 256;

    /** The algorithm used for the encryption of the key blob. */
    private static final String CIPHER_ALGO = "AES/GCM/NoPadding";

    private RebootEscrowData(byte spVersion, byte[] iv, byte[] syntheticPassword, byte[] blob,
            byte[] key) {
            RebootEscrowKey key) {
        mSpVersion = spVersion;
        mIv = iv;
        mSyntheticPassword = syntheticPassword;
@@ -69,7 +59,7 @@ class RebootEscrowData {
    private final byte[] mIv;
    private final byte[] mSyntheticPassword;
    private final byte[] mBlob;
    private final byte[] mKey;
    private final RebootEscrowKey mKey;

    public byte getSpVersion() {
        return mSpVersion;
@@ -87,17 +77,13 @@ class RebootEscrowData {
        return mBlob;
    }

    public byte[] getKey() {
    public RebootEscrowKey getKey() {
        return mKey;
    }

    static SecretKeySpec fromKeyBytes(byte[] keyBytes) {
        return new SecretKeySpec(keyBytes, KEY_ALGO);
    }

    static RebootEscrowData fromEncryptedData(SecretKeySpec keySpec, byte[] blob)
    static RebootEscrowData fromEncryptedData(RebootEscrowKey key, byte[] blob)
            throws IOException {
        Preconditions.checkNotNull(keySpec);
        Preconditions.checkNotNull(key);
        Preconditions.checkNotNull(blob);

        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(blob));
@@ -126,7 +112,7 @@ class RebootEscrowData {
        final byte[] syntheticPassword;
        try {
            Cipher c = Cipher.getInstance(CIPHER_ALGO);
            c.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
            c.init(Cipher.DECRYPT_MODE, key.getKey(), new IvParameterSpec(iv));
            syntheticPassword = c.doFinal(cipherText);
        } catch (NoSuchAlgorithmException | InvalidKeyException | BadPaddingException
                | IllegalBlockSizeException | NoSuchPaddingException
@@ -134,30 +120,22 @@ class RebootEscrowData {
            throw new IOException("Could not decrypt ciphertext", e);
        }

        return new RebootEscrowData(spVersion, iv, syntheticPassword, blob, keySpec.getEncoded());
        return new RebootEscrowData(spVersion, iv, syntheticPassword, blob, key);
    }

    static RebootEscrowData fromSyntheticPassword(byte spVersion, byte[] syntheticPassword)
    static RebootEscrowData fromSyntheticPassword(RebootEscrowKey key, byte spVersion,
            byte[] syntheticPassword)
            throws IOException {
        Preconditions.checkNotNull(syntheticPassword);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);

        final SecretKey secretKey;
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGO);
            keyGenerator.init(KEY_SIZE_BITS, new SecureRandom());
            secretKey = keyGenerator.generateKey();
        } catch (NoSuchAlgorithmException e) {
            throw new IOException("Could not generate new secret key", e);
        }

        final byte[] cipherText;
        final byte[] iv;
        try {
            Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            cipher.init(Cipher.ENCRYPT_MODE, key.getKey());
            cipherText = cipher.doFinal(syntheticPassword);
            iv = cipher.getIV();
        } catch (NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException
@@ -173,6 +151,6 @@ class RebootEscrowData {
        dos.write(cipherText);

        return new RebootEscrowData(spVersion, iv, syntheticPassword, bos.toByteArray(),
                secretKey.getEncoded());
                key);
    }
}
+67 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.locksettings;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 * Key used to encrypt and decrypt the {@link RebootEscrowData}.
 */
class RebootEscrowKey {

    /** The secret key will be of this format. */
    private static final String KEY_ALGO = "AES";

    /** The key size used for encrypting the reboot escrow data. */
    private static final int KEY_SIZE_BITS = 256;

    private final SecretKey mKey;

    private RebootEscrowKey(SecretKey key) {
        mKey = key;
    }

    static RebootEscrowKey fromKeyBytes(byte[] keyBytes) {
        return new RebootEscrowKey(new SecretKeySpec(keyBytes, KEY_ALGO));
    }

    static RebootEscrowKey generate() throws IOException {
        final SecretKey secretKey;
        try {
            KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGO);
            keyGenerator.init(KEY_SIZE_BITS, new SecureRandom());
            secretKey = keyGenerator.generateKey();
        } catch (NoSuchAlgorithmException e) {
            throw new IOException("Could not generate new secret key", e);
        }
        return new RebootEscrowKey(secretKey);
    }

    SecretKey getKey() {
        return mKey;
    }

    byte[] getKeyBytes() {
        return mKey.getEncoded();
    }
}
+57 −20
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.os.UserManager;
import android.util.Slog;
import android.util.StatsLog;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.RebootEscrowListener;

@@ -34,18 +35,16 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.crypto.spec.SecretKeySpec;

class RebootEscrowManager {
    private static final String TAG = "RebootEscrowManager";

    /**
     * Used to track when the reboot escrow is wanted. Set to false when mRebootEscrowReady is
     * true.
     * Used to track when the reboot escrow is wanted. Should stay true once escrow is requested
     * unless clearRebootEscrow is called. This will allow all the active users to be unlocked
     * after reboot.
     */
    private final AtomicBoolean mRebootEscrowWanted = new AtomicBoolean(false);
    private boolean mRebootEscrowWanted;

    /** Used to track when reboot escrow is ready. */
    private boolean mRebootEscrowReady;
@@ -53,11 +52,17 @@ class RebootEscrowManager {
    /** Notified when mRebootEscrowReady changes. */
    private RebootEscrowListener mRebootEscrowListener;

    /**
     * Hold this lock when checking or generating the reboot escrow key.
     */
    private final Object mKeyGenerationLock = new Object();

    /**
     * Stores the reboot escrow data between when it's supplied and when
     * {@link #armRebootEscrowIfNeeded()} is called.
     */
    private RebootEscrowData mPendingRebootEscrowData;
    @GuardedBy("mKeyGenerationLock")
    private RebootEscrowKey mPendingRebootEscrowKey;

    private final UserManager mUserManager;

@@ -82,6 +87,7 @@ class RebootEscrowManager {
        public Context getContext() {
            return mContext;
        }

        public UserManager getUserManager() {
            return (UserManager) mContext.getSystemService(Context.USER_SERVICE);
        }
@@ -123,7 +129,7 @@ class RebootEscrowManager {
            return;
        }

        SecretKeySpec escrowKey = getAndClearRebootEscrowKey();
        RebootEscrowKey escrowKey = getAndClearRebootEscrowKey();
        if (escrowKey == null) {
            Slog.w(TAG, "Had reboot escrow data for users, but no key; removing escrow storage.");
            for (UserInfo user : users) {
@@ -140,7 +146,7 @@ class RebootEscrowManager {
        StatsLog.write(StatsLog.REBOOT_ESCROW_RECOVERY_REPORTED, allUsersUnlocked);
    }

    private SecretKeySpec getAndClearRebootEscrowKey() {
    private RebootEscrowKey getAndClearRebootEscrowKey() {
        IRebootEscrow rebootEscrow = mInjector.getRebootEscrow();
        if (rebootEscrow == null) {
            return null;
@@ -170,14 +176,14 @@ class RebootEscrowManager {
            // Overwrite the existing key with the null key
            rebootEscrow.storeKey(new byte[32]);

            return RebootEscrowData.fromKeyBytes(escrowKeyBytes);
            return RebootEscrowKey.fromKeyBytes(escrowKeyBytes);
        } catch (RemoteException e) {
            Slog.w(TAG, "Could not retrieve escrow data");
            return null;
        }
    }

    private boolean restoreRebootEscrowForUser(@UserIdInt int userId, SecretKeySpec escrowKey) {
    private boolean restoreRebootEscrowForUser(@UserIdInt int userId, RebootEscrowKey key) {
        if (!mStorage.hasRebootEscrow(userId)) {
            return false;
        }
@@ -186,7 +192,7 @@ class RebootEscrowManager {
            byte[] blob = mStorage.readRebootEscrow(userId);
            mStorage.removeRebootEscrow(userId);

            RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(escrowKey, blob);
            RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(key, blob);

            mCallbacks.onRebootEscrowRestored(escrowData.getSpVersion(),
                    escrowData.getSyntheticPassword(), userId);
@@ -199,33 +205,60 @@ class RebootEscrowManager {

    void callToRebootEscrowIfNeeded(@UserIdInt int userId, byte spVersion,
            byte[] syntheticPassword) {
        if (!mRebootEscrowWanted.compareAndSet(true, false)) {
        if (!mRebootEscrowWanted) {
            return;
        }

        IRebootEscrow rebootEscrow = mInjector.getRebootEscrow();
        if (rebootEscrow == null) {
            mRebootEscrowWanted = false;
            setRebootEscrowReady(false);
            return;
        }

        RebootEscrowKey escrowKey = generateEscrowKeyIfNeeded();
        if (escrowKey == null) {
            Slog.e(TAG, "Could not generate escrow key");
            mRebootEscrowWanted = false;
            setRebootEscrowReady(false);
            return;
        }

        final RebootEscrowData escrowData;
        try {
            escrowData = RebootEscrowData.fromSyntheticPassword(spVersion, syntheticPassword);
            escrowData = RebootEscrowData.fromSyntheticPassword(escrowKey, spVersion,
                    syntheticPassword);
        } catch (IOException e) {
            setRebootEscrowReady(false);
            Slog.w(TAG, "Could not escrow reboot data", e);
            return;
        }

        mPendingRebootEscrowData = escrowData;
        mStorage.writeRebootEscrow(userId, escrowData.getBlob());

        setRebootEscrowReady(true);
    }

    private RebootEscrowKey generateEscrowKeyIfNeeded() {
        synchronized (mKeyGenerationLock) {
            if (mPendingRebootEscrowKey != null) {
                return mPendingRebootEscrowKey;
            }

            RebootEscrowKey key;
            try {
                key = RebootEscrowKey.generate();
            } catch (IOException e) {
                return null;
            }

            mPendingRebootEscrowKey = key;
            return key;
        }
    }

    private void clearRebootEscrowIfNeeded() {
        mRebootEscrowWanted.set(false);
        mRebootEscrowWanted = false;
        setRebootEscrowReady(false);

        IRebootEscrow rebootEscrow = mInjector.getRebootEscrow();
@@ -255,14 +288,18 @@ class RebootEscrowManager {
            return false;
        }

        RebootEscrowData escrowData = mPendingRebootEscrowData;
        if (escrowData == null) {
        RebootEscrowKey escrowKey;
        synchronized (mKeyGenerationLock) {
            escrowKey = mPendingRebootEscrowKey;
        }

        if (escrowKey == null) {
            return false;
        }

        boolean armedRebootEscrow = false;
        try {
            rebootEscrow.storeKey(escrowData.getKey());
            rebootEscrow.storeKey(escrowKey.getKeyBytes());
            armedRebootEscrow = true;
        } catch (RemoteException e) {
            Slog.w(TAG, "Failed escrow secret to RebootEscrow HAL", e);
@@ -283,7 +320,7 @@ class RebootEscrowManager {
        }

        clearRebootEscrowIfNeeded();
        mRebootEscrowWanted.set(true);
        mRebootEscrowWanted = true;
        return true;
    }

+14 −8
Original line number Diff line number Diff line
@@ -21,16 +21,22 @@ import static org.junit.Assert.assertThat;

import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import javax.crypto.spec.SecretKeySpec;

/**
 * atest FrameworksServicesTests:RebootEscrowDataTest
 */
@RunWith(AndroidJUnit4.class)
public class RebootEscrowDataTest {
    private RebootEscrowKey mKey;

    @Before
    public void generateKey() throws Exception {
        mKey = RebootEscrowKey.generate();
    }

    private static byte[] getTestSp() {
        byte[] testSp = new byte[10];
        for (int i = 0; i < testSp.length; i++) {
@@ -41,30 +47,30 @@ public class RebootEscrowDataTest {

    @Test(expected = NullPointerException.class)
    public void fromEntries_failsOnNull() throws Exception {
        RebootEscrowData.fromSyntheticPassword((byte) 2, null);
        RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, null);
    }

    @Test(expected = NullPointerException.class)
    public void fromEncryptedData_failsOnNullData() throws Exception {
        byte[] testSp = getTestSp();
        RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword((byte) 2, testSp);
        SecretKeySpec key = RebootEscrowData.fromKeyBytes(expected.getKey());
        RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp);
        RebootEscrowKey key = RebootEscrowKey.fromKeyBytes(expected.getKey().getKeyBytes());
        RebootEscrowData.fromEncryptedData(key, null);
    }

    @Test(expected = NullPointerException.class)
    public void fromEncryptedData_failsOnNullKey() throws Exception {
        byte[] testSp = getTestSp();
        RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword((byte) 2, testSp);
        RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp);
        RebootEscrowData.fromEncryptedData(null, expected.getBlob());
    }

    @Test
    public void fromEntries_loopback_success() throws Exception {
        byte[] testSp = getTestSp();
        RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword((byte) 2, testSp);
        RebootEscrowData expected = RebootEscrowData.fromSyntheticPassword(mKey, (byte) 2, testSp);

        SecretKeySpec key = RebootEscrowData.fromKeyBytes(expected.getKey());
        RebootEscrowKey key = RebootEscrowKey.fromKeyBytes(expected.getKey().getKeyBytes());
        RebootEscrowData actual = RebootEscrowData.fromEncryptedData(key, expected.getBlob());

        assertThat(actual.getSpVersion(), is(expected.getSpVersion()));
+35 −3
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.locksettings;

import static android.content.pm.UserInfo.FLAG_FULL;
import static android.content.pm.UserInfo.FLAG_PRIMARY;
import static android.content.pm.UserInfo.FLAG_PROFILE;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -27,6 +28,7 @@ import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -56,7 +58,9 @@ import java.util.ArrayList;
@RunWith(AndroidJUnit4.class)
public class RebootEscrowManagerTests {
    protected static final int PRIMARY_USER_ID = 0;
    protected static final int NONSECURE_USER_ID = 10;
    protected static final int WORK_PROFILE_USER_ID = 10;
    protected static final int NONSECURE_SECONDARY_USER_ID = 20;
    protected static final int SECURE_SECONDARY_USER_ID = 21;
    private static final byte FAKE_SP_VERSION = 1;
    private static final byte[] FAKE_AUTH_TOKEN = new byte[] {
            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
@@ -107,10 +111,14 @@ public class RebootEscrowManagerTests {

        ArrayList<UserInfo> users = new ArrayList<>();
        users.add(new UserInfo(PRIMARY_USER_ID, "primary", FLAG_PRIMARY));
        users.add(new UserInfo(NONSECURE_USER_ID, "non-secure", FLAG_FULL));
        users.add(new UserInfo(WORK_PROFILE_USER_ID, "work", FLAG_PROFILE));
        users.add(new UserInfo(NONSECURE_SECONDARY_USER_ID, "non-secure", FLAG_FULL));
        users.add(new UserInfo(SECURE_SECONDARY_USER_ID, "secure", FLAG_FULL));
        when(mUserManager.getUsers()).thenReturn(users);
        when(mCallbacks.isUserSecure(PRIMARY_USER_ID)).thenReturn(true);
        when(mCallbacks.isUserSecure(NONSECURE_USER_ID)).thenReturn(false);
        when(mCallbacks.isUserSecure(WORK_PROFILE_USER_ID)).thenReturn(true);
        when(mCallbacks.isUserSecure(NONSECURE_SECONDARY_USER_ID)).thenReturn(false);
        when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true);
        mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager, mRebootEscrow),
                mCallbacks, mStorage);
    }
@@ -154,6 +162,30 @@ public class RebootEscrowManagerTests {

        assertTrue(mService.armRebootEscrowIfNeeded());
        verify(mRebootEscrow).storeKey(any());

        assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
        assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID));
    }

    @Test
    public void armService_MultipleUsers_Success() throws Exception {
        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
        mService.setRebootEscrowListener(mockListener);
        mService.prepareRebootEscrow();

        clearInvocations(mRebootEscrow);
        mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
        verify(mockListener).onPreparedForReboot(eq(true));
        mService.callToRebootEscrowIfNeeded(SECURE_SECONDARY_USER_ID, FAKE_SP_VERSION,
                FAKE_AUTH_TOKEN);
        verify(mRebootEscrow, never()).storeKey(any());

        assertTrue(mService.armRebootEscrowIfNeeded());
        verify(mRebootEscrow, times(1)).storeKey(any());

        assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
        assertTrue(mStorage.hasRebootEscrow(SECURE_SECONDARY_USER_ID));
        assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID));
    }

    @Test