Password security for FBE disk encryption keys
Add the means to protect FBE keys with a combination of an auth token
from Gatekeeper, and a hash of the password. Both of these must be
passed to unlock_user_key. Keys are created unprotected, and
change_user_key changes the way they are protected.
Bug: 22950892
Change-Id: Ie13bc6f82059ce941b0e664a5b60355e52b45f30
diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java
index ecba0a4..4dbb490 100644
--- a/services/core/java/com/android/server/LockSettingsService.java
+++ b/services/core/java/com/android/server/LockSettingsService.java
@@ -62,6 +62,10 @@
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.server.LockSettingsStorage.CredentialHash;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
import java.util.Arrays;
import java.util.List;
@@ -510,9 +514,9 @@
}
}
- private void unlockUser(int userId, byte[] token) {
+ private void unlockUser(int userId, byte[] token, byte[] secret) {
try {
- ActivityManagerNative.getDefault().unlockUser(userId, token);
+ ActivityManagerNative.getDefault().unlockUser(userId, token, secret);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
@@ -560,6 +564,7 @@
getGateKeeperService().clearSecureUserId(userId);
mStorage.writePatternHash(null, userId);
setKeystorePassword(null, userId);
+ clearUserKeyProtection(userId);
return;
}
@@ -573,6 +578,7 @@
byte[] enrolledHandle = enrollCredential(currentHandle, savedCredential, pattern, userId);
if (enrolledHandle != null) {
mStorage.writePatternHash(enrolledHandle, userId);
+ setUserKeyProtection(userId, pattern, verifyPattern(pattern, 0, userId));
} else {
throw new RemoteException("Failed to enroll pattern");
}
@@ -588,6 +594,7 @@
getGateKeeperService().clearSecureUserId(userId);
mStorage.writePasswordHash(null, userId);
setKeystorePassword(null, userId);
+ clearUserKeyProtection(userId);
return;
}
@@ -601,6 +608,7 @@
byte[] enrolledHandle = enrollCredential(currentHandle, savedCredential, password, userId);
if (enrolledHandle != null) {
mStorage.writePasswordHash(enrolledHandle, userId);
+ setUserKeyProtection(userId, password, verifyPassword(password, 0, userId));
} else {
throw new RemoteException("Failed to enroll password");
}
@@ -633,6 +641,48 @@
return hash;
}
+ private void setUserKeyProtection(int userId, String credential, VerifyCredentialResponse vcr)
+ throws RemoteException {
+ if (vcr == null) {
+ throw new RemoteException("Null response verifying a credential we just set");
+ }
+ if (vcr.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK) {
+ throw new RemoteException("Non-OK response verifying a credential we just set: "
+ + vcr.getResponseCode());
+ }
+ byte[] token = vcr.getPayload();
+ if (token == null) {
+ throw new RemoteException("Empty payload verifying a credential we just set");
+ }
+ changeUserKey(userId, token, secretFromCredential(credential));
+ }
+
+ private void clearUserKeyProtection(int userId) throws RemoteException {
+ changeUserKey(userId, null, null);
+ }
+
+ private static byte[] secretFromCredential(String credential) throws RemoteException {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-512");
+ // Personalize the hash
+ byte[] personalization = "Android FBE credential hash"
+ .getBytes(StandardCharsets.UTF_8);
+ // Pad it to the block size of the hash function
+ personalization = Arrays.copyOf(personalization, 128);
+ digest.update(personalization);
+ digest.update(credential.getBytes(StandardCharsets.UTF_8));
+ return digest.digest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("NoSuchAlgorithmException for SHA-512");
+ }
+ }
+
+ private void changeUserKey(int userId, byte[] token, byte[] secret)
+ throws RemoteException {
+ final UserInfo userInfo = UserManager.get(mContext).getUserInfo(userId);
+ getMountService().changeUserKey(userId, userInfo.serialNumber, token, null, secret);
+ }
+
@Override
public VerifyCredentialResponse checkPattern(String pattern, int userId) throws RemoteException {
return doVerifyPattern(pattern, false, 0, userId);
@@ -742,11 +792,11 @@
if (Arrays.equals(hash, storedHash.hash)) {
unlockKeystore(credentialUtil.adjustForKeystore(credential), userId);
- // TODO: pass through a meaningful token from gatekeeper to
- // unlock credential keys; for now pass through a stub value to
- // indicate that we came from a user challenge.
- final byte[] token = String.valueOf(userId).getBytes();
- unlockUser(userId, token);
+ // Users with legacy credentials don't have credential-backed
+ // FBE keys, so just pass through a fake token/secret
+ Slog.i(TAG, "Unlocking user with fake token: " + userId);
+ final byte[] fakeToken = String.valueOf(userId).getBytes();
+ unlockUser(userId, fakeToken, fakeToken);
// migrate credential to GateKeeper
credentialUtil.setCredential(credential, null, userId);
@@ -786,11 +836,9 @@
// credential has matched
unlockKeystore(credential, userId);
- // TODO: pass through a meaningful token from gatekeeper to
- // unlock credential keys; for now pass through a stub value to
- // indicate that we came from a user challenge.
- final byte[] token = String.valueOf(userId).getBytes();
- unlockUser(userId, token);
+ Slog.i(TAG, "Unlocking user " + userId +
+ " with token length " + response.getPayload().length);
+ unlockUser(userId, response.getPayload(), secretFromCredential(credential));
UserInfo info = UserManager.get(mContext).getUserInfo(userId);
if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)) {
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index 5120e1b..cbd477a 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -2742,8 +2742,30 @@
}
}
+ private SensitiveArg encodeBytes(byte[] bytes) {
+ if (ArrayUtils.isEmpty(bytes)) {
+ return new SensitiveArg("!");
+ } else {
+ return new SensitiveArg(HexDump.toHexString(bytes));
+ }
+ }
+
@Override
- public void unlockUserKey(int userId, int serialNumber, byte[] token) {
+ public void changeUserKey(int userId, int serialNumber,
+ byte[] token, byte[] oldSecret, byte[] newSecret) {
+ enforcePermission(android.Manifest.permission.STORAGE_INTERNAL);
+ waitForReady();
+
+ try {
+ mCryptConnector.execute("cryptfs", "change_user_key", userId, serialNumber,
+ encodeBytes(token), encodeBytes(oldSecret), encodeBytes(newSecret));
+ } catch (NativeDaemonConnectorException e) {
+ throw e.rethrowAsParcelableException();
+ }
+ }
+
+ @Override
+ public void unlockUserKey(int userId, int serialNumber, byte[] token, byte[] secret) {
enforcePermission(android.Manifest.permission.STORAGE_INTERNAL);
waitForReady();
@@ -2753,16 +2775,9 @@
throw new IllegalStateException("Token required to unlock secure user " + userId);
}
- final String encodedToken;
- if (ArrayUtils.isEmpty(token)) {
- encodedToken = "!";
- } else {
- encodedToken = HexDump.toHexString(token);
- }
-
try {
mCryptConnector.execute("cryptfs", "unlock_user_key", userId, serialNumber,
- new SensitiveArg(encodedToken));
+ encodeBytes(token), encodeBytes(secret));
} catch (NativeDaemonConnectorException e) {
throw e.rethrowAsParcelableException();
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 5125133..9dae740 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -20340,8 +20340,8 @@
}
@Override
- public boolean unlockUser(int userId, byte[] token) {
- return mUserController.unlockUser(userId, token);
+ public boolean unlockUser(int userId, byte[] token, byte[] secret) {
+ return mUserController.unlockUser(userId, token, secret);
}
@Override
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 2f63b2d3..a355fa4 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -783,7 +783,7 @@
return result;
}
- boolean unlockUser(final int userId, byte[] token) {
+ boolean unlockUser(final int userId, byte[] token, byte[] secret) {
if (mService.checkCallingPermission(INTERACT_ACROSS_USERS_FULL)
!= PackageManager.PERMISSION_GRANTED) {
String msg = "Permission Denial: unlockUser() from pid="
@@ -796,7 +796,7 @@
final long binderToken = Binder.clearCallingIdentity();
try {
- return unlockUserCleared(userId, token);
+ return unlockUserCleared(userId, token, secret);
} finally {
Binder.restoreCallingIdentity(binderToken);
}
@@ -810,10 +810,10 @@
*/
boolean maybeUnlockUser(final int userId) {
// Try unlocking storage using empty token
- return unlockUserCleared(userId, null);
+ return unlockUserCleared(userId, null, null);
}
- boolean unlockUserCleared(final int userId, byte[] token) {
+ boolean unlockUserCleared(final int userId, byte[] token, byte[] secret) {
synchronized (mService) {
// Bail if already running unlocked
final UserState uss = mStartedUsers.get(userId);
@@ -824,7 +824,7 @@
final UserInfo userInfo = getUserInfo(userId);
final IMountService mountService = getMountService();
try {
- mountService.unlockUserKey(userId, userInfo.serialNumber, token);
+ mountService.unlockUserKey(userId, userInfo.serialNumber, token, secret);
} catch (RemoteException | RuntimeException e) {
Slog.w(TAG, "Failed to unlock: " + e.getMessage());
return false;