Merge changes Id0e18bef,Ie2b5f559
* changes: Wrap the escrow data with key from keystore Load and generate key from keystore in reboot escrow
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
private static final int CURRENT_VERSION = 2;
|
||||
|
||||
/** 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,
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
return new RebootEscrowData(spVersion, syntheticPassword, blob, ks);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 android.security.keystore.AndroidKeyStoreSpi;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import android.security.keystore2.AndroidKeyStoreLoadStoreParameter;
|
||||
import android.security.keystore2.AndroidKeyStoreProvider;
|
||||
import android.util.Slog;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
/**
|
||||
* This class loads and generates the key used for resume on reboot from android keystore.
|
||||
*/
|
||||
public class RebootEscrowKeyStoreManager {
|
||||
private static final String TAG = "RebootEscrowKeyStoreManager";
|
||||
|
||||
/**
|
||||
* The key alias in keystore. This key is used to wrap both escrow key and escrow data.
|
||||
*/
|
||||
public static final String REBOOT_ESCROW_KEY_STORE_ENCRYPTION_KEY_NAME =
|
||||
"reboot_escrow_key_store_encryption_key";
|
||||
|
||||
public static final int KEY_LENGTH = 256;
|
||||
|
||||
/**
|
||||
* Use keystore2 once it's installed.
|
||||
*/
|
||||
private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeystore";
|
||||
|
||||
/**
|
||||
* The selinux namespace for resume_on_reboot_key
|
||||
*/
|
||||
private static final int KEY_STORE_NAMESPACE = 120;
|
||||
|
||||
/**
|
||||
* Hold this lock when getting or generating the encryption key in keystore.
|
||||
*/
|
||||
private final Object mKeyStoreLock = new Object();
|
||||
|
||||
@GuardedBy("mKeyStoreLock")
|
||||
private SecretKey getKeyStoreEncryptionKeyLocked() {
|
||||
try {
|
||||
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER);
|
||||
KeyStore.LoadStoreParameter loadStoreParameter = null;
|
||||
// Load from the specific namespace if keystore2 is enabled.
|
||||
if (AndroidKeyStoreProvider.isInstalled()) {
|
||||
loadStoreParameter = new AndroidKeyStoreLoadStoreParameter(KEY_STORE_NAMESPACE);
|
||||
}
|
||||
keyStore.load(loadStoreParameter);
|
||||
return (SecretKey) keyStore.getKey(REBOOT_ESCROW_KEY_STORE_ENCRYPTION_KEY_NAME,
|
||||
null);
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
Slog.e(TAG, "Unable to get encryption key from keystore.", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected SecretKey getKeyStoreEncryptionKey() {
|
||||
synchronized (mKeyStoreLock) {
|
||||
return getKeyStoreEncryptionKeyLocked();
|
||||
}
|
||||
}
|
||||
|
||||
protected void clearKeyStoreEncryptionKey() {
|
||||
synchronized (mKeyStoreLock) {
|
||||
try {
|
||||
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER);
|
||||
KeyStore.LoadStoreParameter loadStoreParameter = null;
|
||||
// Load from the specific namespace if keystore2 is enabled.
|
||||
if (AndroidKeyStoreProvider.isInstalled()) {
|
||||
loadStoreParameter = new AndroidKeyStoreLoadStoreParameter(KEY_STORE_NAMESPACE);
|
||||
}
|
||||
keyStore.load(loadStoreParameter);
|
||||
keyStore.deleteEntry(REBOOT_ESCROW_KEY_STORE_ENCRYPTION_KEY_NAME);
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
Slog.e(TAG, "Unable to delete encryption key in keystore.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected SecretKey generateKeyStoreEncryptionKeyIfNeeded() {
|
||||
synchronized (mKeyStoreLock) {
|
||||
SecretKey kk = getKeyStoreEncryptionKeyLocked();
|
||||
if (kk != null) {
|
||||
return kk;
|
||||
}
|
||||
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStoreSpi.NAME);
|
||||
KeyGenParameterSpec.Builder parameterSpecBuilder = new KeyGenParameterSpec.Builder(
|
||||
REBOOT_ESCROW_KEY_STORE_ENCRYPTION_KEY_NAME,
|
||||
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||
.setKeySize(KEY_LENGTH)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE);
|
||||
// Generate the key with the correct namespace if keystore2 is enabled.
|
||||
if (AndroidKeyStoreProvider.isInstalled()) {
|
||||
parameterSpecBuilder.setNamespace(KEY_STORE_NAMESPACE);
|
||||
}
|
||||
generator.init(parameterSpecBuilder.build());
|
||||
return generator.generateKey();
|
||||
} catch (GeneralSecurityException e) {
|
||||
// Should never happen.
|
||||
Slog.e(TAG, "Unable to generate key from keystore.", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,18 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
/**
|
||||
* This class aims to persists the synthetic password(SP) across reboot in a secure way. In
|
||||
* particular, it manages the encryption of the sp before reboot, and decryption of the sp after
|
||||
* reboot. Here are the meaning of some terms.
|
||||
* SP: synthetic password
|
||||
* K_s: The RebootEscrowKey, i.e. AES-GCM key stored in memory
|
||||
* K_k: AES-GCM key in android keystore
|
||||
* RebootEscrowData: The synthetic password and its encrypted blob. We encrypt SP with K_s first,
|
||||
* then with K_k, i.e. E(K_k, E(K_s, SP))
|
||||
*/
|
||||
class RebootEscrowManager {
|
||||
private static final String TAG = "RebootEscrowManager";
|
||||
|
||||
@@ -101,6 +113,8 @@ class RebootEscrowManager {
|
||||
|
||||
private final Callbacks mCallbacks;
|
||||
|
||||
private final RebootEscrowKeyStoreManager mKeyStoreManager;
|
||||
|
||||
interface Callbacks {
|
||||
boolean isUserSecure(int userId);
|
||||
|
||||
@@ -109,11 +123,13 @@ class RebootEscrowManager {
|
||||
|
||||
static class Injector {
|
||||
protected Context mContext;
|
||||
|
||||
private final RebootEscrowKeyStoreManager mKeyStoreManager;
|
||||
private final RebootEscrowProviderInterface mRebootEscrowProvider;
|
||||
|
||||
Injector(Context context) {
|
||||
mContext = context;
|
||||
mKeyStoreManager = new RebootEscrowKeyStoreManager();
|
||||
|
||||
RebootEscrowProviderInterface rebootEscrowProvider = null;
|
||||
// TODO(xunchang) add implementation for server based ror.
|
||||
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_OTA,
|
||||
@@ -138,6 +154,10 @@ class RebootEscrowManager {
|
||||
return (UserManager) mContext.getSystemService(Context.USER_SERVICE);
|
||||
}
|
||||
|
||||
public RebootEscrowKeyStoreManager getKeyStoreManager() {
|
||||
return mKeyStoreManager;
|
||||
}
|
||||
|
||||
public RebootEscrowProviderInterface getRebootEscrowProvider() {
|
||||
return mRebootEscrowProvider;
|
||||
}
|
||||
@@ -168,6 +188,7 @@ class RebootEscrowManager {
|
||||
mStorage = storage;
|
||||
mUserManager = injector.getUserManager();
|
||||
mEventLog = injector.getEventLog();
|
||||
mKeyStoreManager = injector.getKeyStoreManager();
|
||||
}
|
||||
|
||||
void loadRebootEscrowDataIfAvailable() {
|
||||
@@ -183,8 +204,12 @@ class RebootEscrowManager {
|
||||
return;
|
||||
}
|
||||
|
||||
RebootEscrowKey escrowKey = getAndClearRebootEscrowKey();
|
||||
if (escrowKey == null) {
|
||||
// Fetch the key from keystore to decrypt the escrow data & escrow key; this key is
|
||||
// generated before reboot. Note that we will clear the escrow key even if the keystore key
|
||||
// is null.
|
||||
SecretKey kk = mKeyStoreManager.getKeyStoreEncryptionKey();
|
||||
RebootEscrowKey escrowKey = getAndClearRebootEscrowKey(kk);
|
||||
if (kk == null || escrowKey == null) {
|
||||
Slog.w(TAG, "Had reboot escrow data for users, but no key; removing escrow storage.");
|
||||
for (UserInfo user : users) {
|
||||
mStorage.removeRebootEscrow(user.id);
|
||||
@@ -197,7 +222,7 @@ class RebootEscrowManager {
|
||||
|
||||
boolean allUsersUnlocked = true;
|
||||
for (UserInfo user : rebootEscrowUsers) {
|
||||
allUsersUnlocked &= restoreRebootEscrowForUser(user.id, escrowKey);
|
||||
allUsersUnlocked &= restoreRebootEscrowForUser(user.id, escrowKey, kk);
|
||||
}
|
||||
onEscrowRestoreComplete(allUsersUnlocked);
|
||||
}
|
||||
@@ -212,7 +237,7 @@ class RebootEscrowManager {
|
||||
}
|
||||
}
|
||||
|
||||
private RebootEscrowKey getAndClearRebootEscrowKey() {
|
||||
private RebootEscrowKey getAndClearRebootEscrowKey(SecretKey kk) {
|
||||
RebootEscrowProviderInterface rebootEscrowProvider = mInjector.getRebootEscrowProvider();
|
||||
if (rebootEscrowProvider == null) {
|
||||
Slog.w(TAG,
|
||||
@@ -220,14 +245,16 @@ class RebootEscrowManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
RebootEscrowKey key = rebootEscrowProvider.getAndClearRebootEscrowKey(null);
|
||||
// The K_s blob maybe encrypted by K_k as well.
|
||||
RebootEscrowKey key = rebootEscrowProvider.getAndClearRebootEscrowKey(kk);
|
||||
if (key != null) {
|
||||
mEventLog.addEntry(RebootEscrowEvent.RETRIEVED_STORED_KEK);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
private boolean restoreRebootEscrowForUser(@UserIdInt int userId, RebootEscrowKey key) {
|
||||
private boolean restoreRebootEscrowForUser(@UserIdInt int userId, RebootEscrowKey ks,
|
||||
SecretKey kk) {
|
||||
if (!mStorage.hasRebootEscrow(userId)) {
|
||||
return false;
|
||||
}
|
||||
@@ -236,7 +263,7 @@ class RebootEscrowManager {
|
||||
byte[] blob = mStorage.readRebootEscrow(userId);
|
||||
mStorage.removeRebootEscrow(userId);
|
||||
|
||||
RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(key, blob);
|
||||
RebootEscrowData escrowData = RebootEscrowData.fromEncryptedData(ks, blob, kk);
|
||||
|
||||
mCallbacks.onRebootEscrowRestored(escrowData.getSpVersion(),
|
||||
escrowData.getSyntheticPassword(), userId);
|
||||
@@ -246,6 +273,9 @@ class RebootEscrowManager {
|
||||
} catch (IOException e) {
|
||||
Slog.w(TAG, "Could not load reboot escrow data for user " + userId, e);
|
||||
return false;
|
||||
} finally {
|
||||
// Clear the old key in keystore. A new key will be generated by new RoR requests.
|
||||
mKeyStoreManager.clearKeyStoreEncryptionKey();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,11 +297,16 @@ class RebootEscrowManager {
|
||||
return;
|
||||
}
|
||||
|
||||
SecretKey kk = mKeyStoreManager.generateKeyStoreEncryptionKeyIfNeeded();
|
||||
if (kk == null) {
|
||||
Slog.e(TAG, "Failed to generate encryption key from keystore.");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -348,7 +383,13 @@ class RebootEscrowManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean armedRebootEscrow = rebootEscrowProvider.storeRebootEscrowKey(escrowKey, null);
|
||||
// We will use the same key from keystore to encrypt the escrow key and escrow data blob.
|
||||
SecretKey kk = mKeyStoreManager.getKeyStoreEncryptionKey();
|
||||
if (kk == null) {
|
||||
Slog.e(TAG, "Failed to get encryption key from keystore.");
|
||||
return false;
|
||||
}
|
||||
boolean armedRebootEscrow = rebootEscrowProvider.storeRebootEscrowKey(escrowKey, kk);
|
||||
if (armedRebootEscrow) {
|
||||
mStorage.setInt(REBOOT_ESCROW_ARMED_KEY, mInjector.getBootCount(), USER_SYSTEM);
|
||||
mEventLog.addEntry(RebootEscrowEvent.SET_ARMED_STATUS);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ import org.mockito.ArgumentCaptor;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
@SmallTest
|
||||
@Presubmit
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@@ -77,15 +80,25 @@ public class RebootEscrowManagerTests {
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
};
|
||||
|
||||
// Hex encoding of a randomly generated AES key for test.
|
||||
private static final byte[] TEST_AES_KEY = new byte[] {
|
||||
0x48, 0x19, 0x12, 0x54, 0x13, 0x13, 0x52, 0x31,
|
||||
0x44, 0x74, 0x61, 0x54, 0x29, 0x74, 0x37, 0x61,
|
||||
0x70, 0x70, 0x75, 0x25, 0x27, 0x31, 0x49, 0x09,
|
||||
0x26, 0x52, 0x72, 0x63, 0x63, 0x61, 0x78, 0x23,
|
||||
};
|
||||
|
||||
private Context mContext;
|
||||
private UserManager mUserManager;
|
||||
private RebootEscrowManager.Callbacks mCallbacks;
|
||||
private IRebootEscrow mRebootEscrow;
|
||||
private RebootEscrowKeyStoreManager mKeyStoreManager;
|
||||
|
||||
LockSettingsStorageTestable mStorage;
|
||||
|
||||
private MockableRebootEscrowInjected mInjected;
|
||||
private RebootEscrowManager mService;
|
||||
private SecretKey mAesKey;
|
||||
|
||||
public interface MockableRebootEscrowInjected {
|
||||
int getBootCount();
|
||||
@@ -98,9 +111,11 @@ public class RebootEscrowManagerTests {
|
||||
private final RebootEscrowProviderInterface mRebootEscrowProvider;
|
||||
private final UserManager mUserManager;
|
||||
private final MockableRebootEscrowInjected mInjected;
|
||||
private final RebootEscrowKeyStoreManager mKeyStoreManager;
|
||||
|
||||
MockInjector(Context context, UserManager userManager,
|
||||
IRebootEscrow rebootEscrow,
|
||||
RebootEscrowKeyStoreManager keyStoreManager,
|
||||
MockableRebootEscrowInjected injected) {
|
||||
super(context);
|
||||
mRebootEscrow = rebootEscrow;
|
||||
@@ -114,6 +129,7 @@ public class RebootEscrowManagerTests {
|
||||
};
|
||||
mRebootEscrowProvider = new RebootEscrowProviderHalImpl(halInjector);
|
||||
mUserManager = userManager;
|
||||
mKeyStoreManager = keyStoreManager;
|
||||
mInjected = injected;
|
||||
}
|
||||
|
||||
@@ -127,6 +143,11 @@ public class RebootEscrowManagerTests {
|
||||
return mRebootEscrowProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RebootEscrowKeyStoreManager getKeyStoreManager() {
|
||||
return mKeyStoreManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBootCount() {
|
||||
return mInjected.getBootCount();
|
||||
@@ -144,6 +165,11 @@ public class RebootEscrowManagerTests {
|
||||
mUserManager = mock(UserManager.class);
|
||||
mCallbacks = mock(RebootEscrowManager.Callbacks.class);
|
||||
mRebootEscrow = mock(IRebootEscrow.class);
|
||||
mKeyStoreManager = mock(RebootEscrowKeyStoreManager.class);
|
||||
mAesKey = new SecretKeySpec(TEST_AES_KEY, "AES");
|
||||
|
||||
when(mKeyStoreManager.getKeyStoreEncryptionKey()).thenReturn(mAesKey);
|
||||
when(mKeyStoreManager.generateKeyStoreEncryptionKeyIfNeeded()).thenReturn(mAesKey);
|
||||
|
||||
mStorage = new LockSettingsStorageTestable(mContext,
|
||||
new File(InstrumentationRegistry.getContext().getFilesDir(), "locksettings"));
|
||||
@@ -160,7 +186,7 @@ public class RebootEscrowManagerTests {
|
||||
when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true);
|
||||
mInjected = mock(MockableRebootEscrowInjected.class);
|
||||
mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager, mRebootEscrow,
|
||||
mInjected), mCallbacks, mStorage);
|
||||
mKeyStoreManager, mInjected), mCallbacks, mStorage);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -213,6 +239,7 @@ public class RebootEscrowManagerTests {
|
||||
assertNotNull(
|
||||
mStorage.getString(RebootEscrowManager.REBOOT_ESCROW_ARMED_KEY, null, USER_SYSTEM));
|
||||
verify(mRebootEscrow).storeKey(any());
|
||||
verify(mKeyStoreManager).getKeyStoreEncryptionKey();
|
||||
|
||||
assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
|
||||
assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID));
|
||||
@@ -300,6 +327,7 @@ public class RebootEscrowManagerTests {
|
||||
ArgumentCaptor<byte[]> keyByteCaptor = ArgumentCaptor.forClass(byte[].class);
|
||||
assertTrue(mService.armRebootEscrowIfNeeded());
|
||||
verify(mRebootEscrow).storeKey(keyByteCaptor.capture());
|
||||
verify(mKeyStoreManager).getKeyStoreEncryptionKey();
|
||||
|
||||
assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
|
||||
assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID));
|
||||
@@ -314,6 +342,7 @@ public class RebootEscrowManagerTests {
|
||||
mService.loadRebootEscrowDataIfAvailable();
|
||||
verify(mRebootEscrow).retrieveKey();
|
||||
assertTrue(metricsSuccessCaptor.getValue());
|
||||
verify(mKeyStoreManager).clearKeyStoreEncryptionKey();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user