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

Commit f4c1cf66 authored by Tianjie's avatar Tianjie
Browse files

Wrap the escrow data with key from keystore

Factor out a class to handle the aes encrypted blob, and
further encrypt the reboot escrow data with a local key
from key store.

Bug: 172780686
Test: atest FrameworksServicesTests:RebootEscrowDataTest \
            FrameworksServicesTests:LockSettingsServiceTests \
            FrameworksServicesTests:RecoverySystemServiceTest \
            FrameworksServicesTests:RebootEscrowManagerTests ;
      atest CtsAppSecurityHostTestCases:ResumeOnRebootHostTest
Change-Id: Id0e18bef4d3b194a254fa1755cd17a43a7b6e5bc
parent 5d33c465
Loading
Loading
Loading
Loading
+109 −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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

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

    private AesEncryptionUtil() {}

    static byte[] decrypt(SecretKey key, DataInputStream cipherStream) throws IOException {
        Objects.requireNonNull(key);
        Objects.requireNonNull(cipherStream);

        int ivSize = cipherStream.readInt();
        if (ivSize < 0 || ivSize > 32) {
            throw new IOException("IV out of range: " + ivSize);
        }
        byte[] iv = new byte[ivSize];
        cipherStream.readFully(iv);

        int rawCipherTextSize = cipherStream.readInt();
        if (rawCipherTextSize < 0) {
            throw new IOException("Invalid cipher text size: " + rawCipherTextSize);
        }

        byte[] rawCipherText = new byte[rawCipherTextSize];
        cipherStream.readFully(rawCipherText);

        final byte[] plainText;
        try {
            Cipher c = Cipher.getInstance(CIPHER_ALGO);
            c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
            plainText = c.doFinal(rawCipherText);
        } catch (NoSuchAlgorithmException | InvalidKeyException | BadPaddingException
                | IllegalBlockSizeException | NoSuchPaddingException
                | InvalidAlgorithmParameterException e) {
            throw new IOException("Could not decrypt cipher text", e);
        }

        return plainText;
    }

    static byte[] decrypt(SecretKey key, byte[] cipherText) throws IOException {
        Objects.requireNonNull(key);
        Objects.requireNonNull(cipherText);

        DataInputStream cipherStream = new DataInputStream(new ByteArrayInputStream(cipherText));
        return decrypt(key, cipherStream);
    }

    static byte[] encrypt(SecretKey key, byte[] plainText) throws IOException {
        Objects.requireNonNull(key);
        Objects.requireNonNull(plainText);

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

        final byte[] cipherText;
        final byte[] iv;
        try {
            Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            cipherText = cipher.doFinal(plainText);
            iv = cipher.getIV();
        } catch (NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException
                | NoSuchPaddingException | InvalidKeyException e) {
            throw new IOException("Could not encrypt input data", e);
        }

        dos.writeInt(iv.length);
        dos.write(iv);
        dos.writeInt(cipherText.length);
        dos.write(cipherText);

        return bos.toByteArray();
    }
}
+22 −72
Original line number Diff line number Diff line
@@ -16,22 +16,14 @@

package com.android.server.locksettings;

import com.android.internal.util.Preconditions;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;

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

/**
 * Holds the data necessary to complete a reboot escrow of the Synthetic Password.
@@ -41,22 +33,17 @@ class RebootEscrowData {
     * This is the current version of the escrow data format. This should be incremented if the
     * format on disk is changed.
     */
    private static final int CURRENT_VERSION = 1;

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

    private RebootEscrowData(byte spVersion, byte[] iv, byte[] syntheticPassword, byte[] blob,
    private RebootEscrowData(byte spVersion, byte[] syntheticPassword, byte[] blob,
            RebootEscrowKey key) {
        mSpVersion = spVersion;
        mIv = iv;
        mSyntheticPassword = syntheticPassword;
        mBlob = blob;
        mKey = key;
    }

    private final byte mSpVersion;
    private final byte[] mIv;
    private final byte[] mSyntheticPassword;
    private final byte[] mBlob;
    private final RebootEscrowKey mKey;
@@ -65,10 +52,6 @@ class RebootEscrowData {
        return mSpVersion;
    }

    public byte[] getIv() {
        return mIv;
    }

    public byte[] getSyntheticPassword() {
        return mSyntheticPassword;
    }
@@ -81,76 +64,43 @@ class RebootEscrowData {
        return mKey;
    }

    static RebootEscrowData fromEncryptedData(RebootEscrowKey key, byte[] blob)
    static RebootEscrowData fromEncryptedData(RebootEscrowKey ks, byte[] blob, SecretKey kk)
            throws IOException {
        Preconditions.checkNotNull(key);
        Preconditions.checkNotNull(blob);
        Objects.requireNonNull(ks);
        Objects.requireNonNull(blob);

        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(blob));
        int version = dis.readInt();
        if (version != CURRENT_VERSION) {
            throw new IOException("Unsupported version " + version);
        }

        byte spVersion = dis.readByte();

        int ivSize = dis.readInt();
        if (ivSize < 0 || ivSize > 32) {
            throw new IOException("IV out of range: " + ivSize);
        }
        byte[] iv = new byte[ivSize];
        dis.readFully(iv);
        // Decrypt the blob with the key from keystore first, then decrypt again with the reboot
        // escrow key.
        byte[] ksEncryptedBlob = AesEncryptionUtil.decrypt(kk, dis);
        final byte[] syntheticPassword = AesEncryptionUtil.decrypt(ks.getKey(), ksEncryptedBlob);

        int cipherTextSize = dis.readInt();
        if (cipherTextSize < 0) {
            throw new IOException("Invalid cipher text size: " + cipherTextSize);
        return new RebootEscrowData(spVersion, syntheticPassword, blob, ks);
    }

        byte[] cipherText = new byte[cipherTextSize];
        dis.readFully(cipherText);

        final byte[] syntheticPassword;
        try {
            Cipher c = Cipher.getInstance(CIPHER_ALGO);
            c.init(Cipher.DECRYPT_MODE, key.getKey(), new IvParameterSpec(iv));
            syntheticPassword = c.doFinal(cipherText);
        } catch (NoSuchAlgorithmException | InvalidKeyException | BadPaddingException
                | IllegalBlockSizeException | NoSuchPaddingException
                | InvalidAlgorithmParameterException e) {
            throw new IOException("Could not decrypt ciphertext", e);
        }

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

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

        // Encrypt synthetic password with the escrow key first; then encrypt the blob again with
        // the key from keystore.
        byte[] ksEncryptedBlob = AesEncryptionUtil.encrypt(ks.getKey(), syntheticPassword);
        byte[] kkEncryptedBlob = AesEncryptionUtil.encrypt(kk, ksEncryptedBlob);

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

        final byte[] cipherText;
        final byte[] iv;
        try {
            Cipher cipher = Cipher.getInstance(CIPHER_ALGO);
            cipher.init(Cipher.ENCRYPT_MODE, key.getKey());
            cipherText = cipher.doFinal(syntheticPassword);
            iv = cipher.getIV();
        } catch (NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException
                | NoSuchPaddingException | InvalidKeyException e) {
            throw new IOException("Could not encrypt reboot escrow data", e);
        }

        dos.writeInt(CURRENT_VERSION);
        dos.writeByte(spVersion);
        dos.writeInt(iv.length);
        dos.write(iv);
        dos.writeInt(cipherText.length);
        dos.write(cipherText);
        dos.write(kkEncryptedBlob);

        return new RebootEscrowData(spVersion, iv, syntheticPassword, bos.toByteArray(),
                key);
        return new RebootEscrowData(spVersion, syntheticPassword, bos.toByteArray(), ks);
    }
}
+2 −3
Original line number Diff line number Diff line
@@ -263,7 +263,7 @@ class RebootEscrowManager {
            byte[] blob = mStorage.readRebootEscrow(userId);
            mStorage.removeRebootEscrow(userId);

            RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(ks, blob);
            RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(ks, blob, kk);

            mCallbacks.onRebootEscrowRestored(escrowData.getSpVersion(),
                    escrowData.getSyntheticPassword(), userId);
@@ -305,9 +305,8 @@ class RebootEscrowManager {

        final RebootEscrowData escrowData;
        try {
            // TODO(xunchang) further wrap the escrowData with a key from keystore.
            escrowData = RebootEscrowData.fromSyntheticPassword(escrowKey, spVersion,
                    syntheticPassword);
                    syntheticPassword, kk);
        } catch (IOException e) {
            setRebootEscrowReady(false);
            Slog.w(TAG, "Could not escrow reboot data", e);
+43 −8
Original line number Diff line number Diff line
@@ -19,22 +19,44 @@ package com.android.server.locksettings;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;

import androidx.test.runner.AndroidJUnit4;

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

import java.security.GeneralSecurityException;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

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

    private SecretKey generateNewRebootEscrowEncryptionKey() throws GeneralSecurityException {
        KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
        generator.init(new KeyGenParameterSpec.Builder(
                "reboot_escrow_data_test_key",
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setKeySize(256)
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .build());
        return generator.generateKey();
    }

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

    private static byte[] getTestSp() {
@@ -47,36 +69,49 @@ public class RebootEscrowDataTest {

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

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

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

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

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

        assertThat(actual.getSpVersion(), is(expected.getSpVersion()));
        assertThat(actual.getIv(), is(expected.getIv()));
        assertThat(actual.getKey().getKeyBytes(), is(expected.getKey().getKeyBytes()));
        assertThat(actual.getBlob(), is(expected.getBlob()));
        assertThat(actual.getSyntheticPassword(), is(expected.getSyntheticPassword()));
    }

    @Test
    public void aesEncryptedBlob_loopback_success() throws Exception {
        byte[] testSp = getTestSp();
        byte [] encrypted = AesEncryptionUtil.encrypt(mKeyStoreEncryptionKey, testSp);
        byte [] decrypted = AesEncryptionUtil.decrypt(mKeyStoreEncryptionKey, encrypted);

        assertThat(decrypted, is(testSp));
    }

}