Add synthetic password to authentication flow
The user password is used to unlock a per-user synthetic password which
serves the purpose of what the user password previsouly achieves (protect
keystore, vold disk encryption, auth token generation).
Test: runtest frameworks-services -c com.android.server.SyntheticPasswordTests
Test: manual
1. Start with fresh device, enable synthetic password with "adb shell cmd lock_settings sp 1"
1.1 add device lock, reboot and verify (positive & negative); change device lock, reboot and verify.
1.2 Inflate a work profile, reboot and verify device lock. check SID with "adb shell dumpsys lock_settings"
1.3 Un-unify and add work challenge, reboot and verify work challenge and SID.
1.4 Re-unify work challenge, reboot and verify.
1.5 Clear device lock, reboot and verify lock and SID.
2. Start with a fresh device, add a device lock and inflate a work profile.
2.1 Enable synthetic password, note current SID
2.2 Reboot and unlock device. Verify synthetic password is generated and SID remains.
2.3 Clear device lock, reboot and verify (SID should be cleared)
3. Start with a fresh device, inflate a work profile, add separate work challenge
3.1 Enable synthetic password, not current SID
3.2 Reboot and unlock device and profile. Verify synthetic password is generated.
3.3 Clear device lock only, reboot and verify (work profile SID should remain)
All steps tested on marlin (FBE) and bullhead (FDE)
Bug: 33126414
Change-Id: Idb9ebfc7bba2fe40670c5fee2189e873d9704540
diff --git a/services/core/java/com/android/server/LockSettingsService.java b/services/core/java/com/android/server/LockSettingsService.java
index a073d8e..c10bcf0 100644
--- a/services/core/java/com/android/server/LockSettingsService.java
+++ b/services/core/java/com/android/server/LockSettingsService.java
@@ -20,6 +20,8 @@
import static android.Manifest.permission.READ_CONTACTS;
import static android.content.Context.KEYGUARD_SERVICE;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_ENABLED_KEY;
+import static com.android.internal.widget.LockPatternUtils.SYNTHETIC_PASSWORD_HANDLE_KEY;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
@@ -61,6 +63,7 @@
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.provider.Settings.SettingNotFoundException;
+import android.security.GateKeeper;
import android.security.KeyStore;
import android.security.keystore.AndroidKeyStoreProvider;
import android.security.keystore.KeyProperties;
@@ -79,6 +82,8 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.server.LockSettingsStorage.CredentialHash;
+import com.android.server.SyntheticPasswordManager.AuthenticationResult;
+import com.android.server.SyntheticPasswordManager.AuthenticationToken;
import libcore.util.HexEncoding;
@@ -86,6 +91,7 @@
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -145,12 +151,14 @@
private boolean mFirstCallToVold;
protected IGateKeeperService mGateKeeperService;
+ private SyntheticPasswordManager mSpManager;
+
/**
* The UIDs that are used for system credential storage in keystore.
*/
private static final int[] SYSTEM_CREDENTIAL_UIDS = {
Process.WIFI_UID, Process.VPN_UID,
- Process.ROOT_UID, Process.SYSTEM_UID };
+ Process.ROOT_UID };
// This class manages life cycle events for encrypted users on File Based Encryption (FBE)
// devices. The most basic of these is to show/hide notifications about missing features until
@@ -335,6 +343,14 @@
}
return null;
}
+
+ public SyntheticPasswordManager getSyntheticPasswordManager(LockSettingsStorage storage) {
+ return new SyntheticPasswordManager(storage);
+ }
+
+ public int binderGetCallingUid() {
+ return Binder.getCallingUid();
+ }
}
public LockSettingsService(Context context) {
@@ -365,6 +381,8 @@
mUserManager = injector.getUserManager();
mStrongAuthTracker = injector.getStrongAuthTracker();
mStrongAuthTracker.register(mStrongAuth);
+
+ mSpManager = injector.getSyntheticPasswordManager(mStorage);
}
/**
@@ -801,17 +819,42 @@
@Override
public boolean havePassword(int userId) throws RemoteException {
+ synchronized (mSpManager) {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ return mSpManager.getCredentialType(handle, userId) ==
+ LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
+ }
+ }
// Do we need a permissions check here?
return mStorage.hasPassword(userId);
}
@Override
public boolean havePattern(int userId) throws RemoteException {
+ synchronized (mSpManager) {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ return mSpManager.getCredentialType(handle, userId) ==
+ LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
+ }
+ }
// Do we need a permissions check here?
return mStorage.hasPattern(userId);
}
private boolean isUserSecure(int userId) {
+ synchronized (mSpManager) {
+ try {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ return mSpManager.getCredentialType(handle, userId) !=
+ LockPatternUtils.CREDENTIAL_TYPE_NONE;
+ }
+ } catch (RemoteException e) {
+ // fall through
+ }
+ }
return mStorage.hasCredential(userId);
}
@@ -1021,6 +1064,13 @@
private void setLockCredentialInternal(String credential, int credentialType,
String savedCredential, int userId) throws RemoteException {
+ synchronized (mSpManager) {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential,
+ userId);
+ return;
+ }
+ }
if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
if (credential != null) {
Slog.wtf(TAG, "CredentialType is none, but credential is non-null.");
@@ -1061,7 +1111,16 @@
savedCredential = null;
}
}
-
+ synchronized (mSpManager) {
+ if (shouldMigrateToSyntheticPasswordLocked(userId)) {
+ initializeSyntheticPasswordLocked(currentHandle.hash, savedCredential,
+ currentHandle.type, userId);
+ spBasedSetLockCredentialInternalLocked(credential, credentialType, savedCredential,
+ userId);
+ return;
+ }
+ }
+ if (DEBUG) Slog.d(TAG, "setLockCredentialInternal: user=" + userId);
byte[] enrolledHandle = enrollCredential(currentHandle.hash, savedCredential, credential,
userId);
if (enrolledHandle != null) {
@@ -1189,6 +1248,11 @@
return hash;
}
+ private void setAuthlessUserKeyProtection(int userId, byte[] key) throws RemoteException {
+ if (DEBUG) Slog.d(TAG, "setAuthlessUserKeyProtectiond: user=" + userId);
+ addUserKeyAuth(userId, null, key);
+ }
+
private void setUserKeyProtection(int userId, String credential, VerifyCredentialResponse vcr)
throws RemoteException {
if (DEBUG) Slog.d(TAG, "setUserKeyProtection: user=" + userId);
@@ -1320,7 +1384,16 @@
if (TextUtils.isEmpty(credential)) {
throw new IllegalArgumentException("Credential can't be null or empty");
}
-
+ synchronized (mSpManager) {
+ if (isSyntheticPasswordBasedCredentialLocked(userId)) {
+ VerifyCredentialResponse response = spBasedDoVerifyCredentialLocked(credential,
+ credentialType, hasChallenge, challenge, userId, progressCallback);
+ if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+ mStrongAuth.reportSuccessfulStrongAuthUnlock(userId);
+ }
+ return response;
+ }
+ }
CredentialHash storedHash = mStorage.readCredentialHash(userId);
if (storedHash.type != credentialType) {
Slog.wtf(TAG, "doVerifyCredential type mismatch with stored credential??"
@@ -1456,8 +1529,8 @@
notifyActivePasswordMetricsAvailable(credential, userId);
unlockKeystore(credential, userId);
- Slog.i(TAG, "Unlocking user " + userId +
- " with token length " + response.getPayload().length);
+ Slog.i(TAG, "Unlocking user " + userId + " with token length "
+ + response.getPayload().length);
unlockUser(userId, response.getPayload(), secretFromCredential(credential));
if (isManagedProfileWithSeparatedLock(userId)) {
@@ -1467,6 +1540,15 @@
}
if (shouldReEnroll) {
setLockCredentialInternal(credential, storedHash.type, credential, userId);
+ } else {
+ // Now that we've cleared of all required GK migration, let's do the final
+ // migration to synthetic password.
+ synchronized (mSpManager) {
+ if (shouldMigrateToSyntheticPasswordLocked(userId)) {
+ initializeSyntheticPasswordLocked(storedHash.hash, credential,
+ storedHash.type, userId);
+ }
+ }
}
} else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
if (response.getTimeout() > 0) {
@@ -1697,7 +1779,7 @@
}
}
- private synchronized IGateKeeperService getGateKeeperService()
+ protected synchronized IGateKeeperService getGateKeeperService()
throws RemoteException {
if (mGateKeeperService != null) {
return mGateKeeperService;
@@ -1713,4 +1795,267 @@
Slog.e(TAG, "Unable to acquire GateKeeperService");
return null;
}
+
+ /**
+ * Precondition: vold and keystore unlocked.
+ *
+ * Create new synthetic password, set up synthetic password blob protected by the supplied
+ * user credential, and make the newly-created SP blob active.
+ *
+ * The invariant under a synthetic password is:
+ * 1. If user credential exists, then both vold and keystore and protected with keys derived
+ * from the synthetic password.
+ * 2. If user credential does not exist, vold and keystore protection are cleared. This is to
+ * make it consistent with current behaviour. It also allows ActivityManager to call
+ * unlockUser() with empty secret.
+ * 3. Once a user is migrated to have synthetic password, its value will never change, no matter
+ * whether the user changes his lockscreen PIN or clear/reset it. When the user clears its
+ * lockscreen PIN, we still maintain the existing synthetic password in a password blob
+ * protected by a default PIN. The only exception is when the DPC performs an untrusted
+ * credential change, in which case we have no way to derive the existing synthetic password
+ * and has to create a new one.
+ * 4. The user SID is linked with synthetic password, but its cleared/re-created when the user
+ * clears/re-creates his lockscreen PIN.
+ *
+ *
+ * Different cases of calling this method:
+ * 1. credentialHash != null
+ * This implies credential != null, a new SP blob will be provisioned, and existing SID
+ * migrated to associate with the new SP.
+ * This happens during a normal migration case when the user currently has password.
+ *
+ * 2. credentialhash == null and credential == null
+ * A new SP blob and a new SID will be created, while the user has no credentials.
+ * This can happens when we are activating an escrow token on a unsecured device, during
+ * which we want to create the SP structure with an empty user credential.
+ *
+ * 3. credentialhash == null and credential != null
+ * This is the untrusted credential reset, OR the user sets a new lockscreen password
+ * FOR THE FIRST TIME on a SP-enabled device. New credential and new SID will be created
+ */
+ private AuthenticationToken initializeSyntheticPasswordLocked(byte[] credentialHash,
+ String credential, int credentialType, int userId) throws RemoteException {
+ Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
+ AuthenticationToken auth = mSpManager.newSyntheticPasswordAndSid(getGateKeeperService(),
+ credentialHash, credential, userId);
+ if (auth == null) {
+ Slog.wtf(TAG, "initializeSyntheticPasswordLocked returns null auth token");
+ return null;
+ }
+ long handle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(),
+ credential, credentialType, auth, userId);
+ if (credential != null) {
+ if (credentialHash == null) {
+ // Since when initializing SP, we didn't provide an existing password handle
+ // for it to migrate SID, we need to create a new SID for the user.
+ mSpManager.newSidForUser(getGateKeeperService(), auth, userId);
+ }
+ mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId);
+ setAuthlessUserKeyProtection(userId, auth.deriveDiskEncryptionKey());
+ setKeystorePassword(auth.deriveKeyStorePassword(), userId);
+ } else {
+ clearUserKeyProtection(userId);
+ setKeystorePassword(null, userId);
+ getGateKeeperService().clearSecureUserId(userId);
+ }
+ fixateNewestUserKeyAuth(userId);
+ setLong(SYNTHETIC_PASSWORD_HANDLE_KEY, handle, userId);
+ return auth;
+ }
+
+ private long getSyntheticPasswordHandleLocked(int userId) {
+ try {
+ return getLong(SYNTHETIC_PASSWORD_HANDLE_KEY, 0, userId);
+ } catch (RemoteException e) {
+ return SyntheticPasswordManager.DEFAULT_HANDLE;
+ }
+ }
+
+ private boolean isSyntheticPasswordBasedCredentialLocked(int userId) throws RemoteException {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ // This is a global setting
+ long enabled = getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM);
+ return enabled != 0 && handle != SyntheticPasswordManager.DEFAULT_HANDLE;
+ }
+
+ private boolean shouldMigrateToSyntheticPasswordLocked(int userId) throws RemoteException {
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ // This is a global setting
+ long enabled = getLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 0, UserHandle.USER_SYSTEM);
+ return enabled != 0 && handle == SyntheticPasswordManager.DEFAULT_HANDLE;
+ }
+
+ private void enableSyntheticPasswordLocked() throws RemoteException {
+ setLong(SYNTHETIC_PASSWORD_ENABLED_KEY, 1, UserHandle.USER_SYSTEM);
+ }
+
+ private VerifyCredentialResponse spBasedDoVerifyCredentialLocked(String userCredential, int
+ credentialType, boolean hasChallenge, long challenge, int userId,
+ ICheckCredentialProgressCallback progressCallback) throws RemoteException {
+ if (DEBUG) Slog.d(TAG, "spBasedDoVerifyCredentialLocked: user=" + userId);
+ if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+ userCredential = null;
+ }
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ AuthenticationResult authResult = mSpManager.unwrapPasswordBasedSyntheticPassword(
+ getGateKeeperService(), handle, userCredential, userId);
+
+ VerifyCredentialResponse response = authResult.gkResponse;
+ if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+ // credential has matched
+ // perform verifyChallenge with synthetic password which generates the real auth
+ // token for the current user
+ response = mSpManager.verifyChallenge(getGateKeeperService(), authResult.authToken,
+ challenge, userId);
+ if (response.getResponseCode() != VerifyCredentialResponse.RESPONSE_OK) {
+ Slog.wtf(TAG, "verifyChallenge with SP failed.");
+ return VerifyCredentialResponse.ERROR;
+ }
+ if (progressCallback != null) {
+ progressCallback.onCredentialVerified();
+ }
+ notifyActivePasswordMetricsAvailable(userCredential, userId);
+ unlockKeystore(authResult.authToken.deriveKeyStorePassword(), userId);
+
+ final byte[] secret = authResult.authToken.deriveDiskEncryptionKey();
+ Slog.i(TAG, "Unlocking user " + userId + " with secret only, length " + secret.length);
+ unlockUser(userId, null, secret);
+
+ if (isManagedProfileWithSeparatedLock(userId)) {
+ TrustManager trustManager =
+ (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
+ trustManager.setDeviceLockedForUser(userId, false);
+ }
+ } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
+ if (response.getTimeout() > 0) {
+ requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId);
+ }
+ }
+
+ return response;
+ }
+
+ /**
+ * Change the user's lockscreen password by creating a new SP blob and update the handle, based
+ * on an existing authentication token. Even though a new SP blob is created, the underlying
+ * synthetic password is never changed.
+ *
+ * When clearing credential, we keep the SP unchanged, but clear its password handle so its
+ * SID is gone. We also clear password from (software-based) keystore and vold, which will be
+ * added back when new password is set in future.
+ */
+ private long setLockCredentialWithAuthTokenLocked(String credential, int credentialType,
+ AuthenticationToken auth, int userId) throws RemoteException {
+ if (DEBUG) Slog.d(TAG, "setLockCredentialWithAuthTokenLocked: user=" + userId);
+ long newHandle = mSpManager.createPasswordBasedSyntheticPassword(getGateKeeperService(),
+ credential, credentialType, auth, userId);
+ final Map<Integer, String> profilePasswords;
+ if (credential != null) {
+ // // not needed by synchronizeUnifiedWorkChallengeForProfiles()
+ profilePasswords = null;
+
+ if (mSpManager.hasSidForUser(userId)) {
+ // We are changing password of a secured device, nothing more needed as
+ // createPasswordBasedSyntheticPassword has already taken care of maintaining
+ // the password handle and SID unchanged.
+
+ //refresh auth token
+ mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId);
+ } else {
+ // A new password is set on a previously-unsecured device, we need to generate
+ // a new SID, and re-add keys to vold and keystore.
+ mSpManager.newSidForUser(getGateKeeperService(), auth, userId);
+ mSpManager.verifyChallenge(getGateKeeperService(), auth, 0L, userId);
+ setAuthlessUserKeyProtection(userId, auth.deriveDiskEncryptionKey());
+ fixateNewestUserKeyAuth(userId);
+ setKeystorePassword(auth.deriveKeyStorePassword(), userId);
+ }
+ } else {
+ // Cache all profile password if they use unified work challenge. This will later be
+ // used to clear the profile's password in synchronizeUnifiedWorkChallengeForProfiles()
+ profilePasswords = getDecryptedPasswordsForAllTiedProfiles(userId);
+
+ // we are clearing password of a secured device, so need to nuke SID as well.
+ mSpManager.clearSidForUser(userId);
+ getGateKeeperService().clearSecureUserId(userId);
+ // Clear key from vold so ActivityManager can just unlock the user with empty secret
+ // during boot.
+ clearUserKeyProtection(userId);
+ fixateNewestUserKeyAuth(userId);
+ setKeystorePassword(null, userId);
+ }
+ setLong(SYNTHETIC_PASSWORD_HANDLE_KEY, newHandle, userId);
+ synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords);
+ return newHandle;
+ }
+
+ private void spBasedSetLockCredentialInternalLocked(String credential, int credentialType,
+ String savedCredential, int userId) throws RemoteException {
+ if (DEBUG) Slog.d(TAG, "spBasedSetLockCredentialInternalLocked: user=" + userId);
+ if (isManagedProfileWithUnifiedLock(userId)) {
+ // get credential from keystore when managed profile has unified lock
+ try {
+ savedCredential = getDecryptedPasswordForTiedProfile(userId);
+ } catch (FileNotFoundException e) {
+ Slog.i(TAG, "Child profile key not found");
+ } catch (UnrecoverableKeyException | InvalidKeyException | KeyStoreException
+ | NoSuchAlgorithmException | NoSuchPaddingException
+ | InvalidAlgorithmParameterException | IllegalBlockSizeException
+ | BadPaddingException | CertificateException | IOException e) {
+ Slog.e(TAG, "Failed to decrypt child profile key", e);
+ }
+ }
+ long handle = getSyntheticPasswordHandleLocked(userId);
+ AuthenticationToken auth = mSpManager.unwrapPasswordBasedSyntheticPassword(
+ getGateKeeperService(), handle, savedCredential, userId).authToken;
+ if (auth != null) {
+ // We are performing a trusted credential change i.e. a correct existing credential
+ // is provided
+ setLockCredentialWithAuthTokenLocked(credential, credentialType, auth, userId);
+ mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
+ } else {
+ // We are performing an untrusted credential change i.e. by DevicePolicyManager.
+ // So provision a new SP and SID. This would invalidate existing escrow tokens.
+ // Still support this for now but this flow will be removed in the next release.
+
+ Slog.w(TAG, "Untrusted credential change invoked");
+ initializeSyntheticPasswordLocked(null, credential, credentialType, userId);
+ synchronizeUnifiedWorkChallengeForProfiles(userId, null);
+ mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
+ }
+ notifyActivePasswordMetricsAvailable(credential, userId);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args){
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+
+ pw.println("Permission Denial: can't dump LockSettingsService from from pid="
+ + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ return;
+ }
+
+ synchronized (this) {
+ pw.println("Current lock settings service state:");
+ pw.println(String.format("SP Enabled = %b",
+ mLockPatternUtils.isSyntheticPasswordEnabled()));
+
+ List<UserInfo> users = mUserManager.getUsers();
+ for (int user = 0; user < users.size(); user++) {
+ final int userId = users.get(user).id;
+ pw.println(" User " + userId);
+ pw.println(String.format(" SP Handle = %x",
+ getSyntheticPasswordHandleLocked(userId)));
+ try {
+ pw.println(String.format(" SID = %x",
+ getGateKeeperService().getSecureUserId(userId)));
+ } catch (RemoteException e) {
+ // ignore.
+ }
+ }
+ }
+ }
+
}
diff --git a/services/core/java/com/android/server/LockSettingsShellCommand.java b/services/core/java/com/android/server/LockSettingsShellCommand.java
index 1ab5303..91bd98e 100644
--- a/services/core/java/com/android/server/LockSettingsShellCommand.java
+++ b/services/core/java/com/android/server/LockSettingsShellCommand.java
@@ -22,12 +22,9 @@
import android.app.ActivityManager;
import android.content.Context;
-import android.os.Binder;
-import android.os.Process;
import android.os.RemoteException;
import android.os.ShellCommand;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockPatternUtils.RequestThrottledException;
@@ -37,6 +34,7 @@
private static final String COMMAND_SET_PIN = "set-pin";
private static final String COMMAND_SET_PASSWORD = "set-password";
private static final String COMMAND_CLEAR = "clear";
+ private static final String COMMAND_SP = "sp";
private int mCurrentUserId;
private final LockPatternUtils mLockPatternUtils;
@@ -71,6 +69,9 @@
case COMMAND_CLEAR:
runClear();
break;
+ case COMMAND_SP:
+ runEnableSp();
+ break;
default:
getErrPrintWriter().println("Unknown command: " + cmd);
break;
@@ -92,6 +93,8 @@
while ((opt = getNextOption()) != null) {
if ("--old".equals(opt)) {
mOld = getNextArgRequired();
+ } else if ("--user".equals(opt)) {
+ mCurrentUserId = Integer.parseInt(getNextArgRequired());
} else {
getErrPrintWriter().println("Unknown option: " + opt);
throw new IllegalArgumentException();
@@ -100,6 +103,15 @@
mNew = getNextArg();
}
+ private void runEnableSp() {
+ if (mNew != null) {
+ mLockPatternUtils.enableSyntheticPassword();
+ getOutPrintWriter().println("Synthetic password enabled");
+ }
+ getOutPrintWriter().println(String.format("SP Enabled = %b",
+ mLockPatternUtils.isSyntheticPasswordEnabled()));
+ }
+
private void runSetPattern() throws RemoteException {
mLockPatternUtils.saveLockPattern(stringToPattern(mNew), mOld, mCurrentUserId);
getOutPrintWriter().println("Pattern set to '" + mNew + "'");
diff --git a/services/core/java/com/android/server/LockSettingsStorage.java b/services/core/java/com/android/server/LockSettingsStorage.java
index 385b1cf..f5bae7c 100644
--- a/services/core/java/com/android/server/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/LockSettingsStorage.java
@@ -66,6 +66,8 @@
private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key";
+ private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/";
+
private static final Object DEFAULT = new Object();
private final DatabaseHelper mOpenHelper;
@@ -412,8 +414,7 @@
}
private String getLockCredentialFilePathForUser(int userId, String basename) {
- String dataSystemDirectory =
- android.os.Environment.getDataDirectory().getAbsolutePath() +
+ String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +
SYSTEM_DIRECTORY;
if (userId == 0) {
// Leave it in the same place for user 0
@@ -423,6 +424,40 @@
}
}
+ public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) {
+ writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data);
+ }
+
+ public byte[] readSyntheticPasswordState(int userId, long handle, String name) {
+ return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name));
+ }
+
+ public void deleteSyntheticPasswordState(int userId, long handle, String name, boolean secure) {
+ String path = getSynthenticPasswordStateFilePathForUser(userId, handle, name);
+ File file = new File(path);
+ if (file.exists()) {
+ //TODO: (b/34600579) invoke secdiscardable
+ file.delete();
+ mCache.putFile(path, null);
+ }
+ }
+
+ @VisibleForTesting
+ protected File getSyntheticPasswordDirectoryForUser(int userId) {
+ return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY);
+ }
+
+ @VisibleForTesting
+ protected String getSynthenticPasswordStateFilePathForUser(int userId, long handle,
+ String name) {
+ File baseDir = getSyntheticPasswordDirectoryForUser(userId);
+ String baseName = String.format("%016x.%s", handle, name);
+ if (!baseDir.exists()) {
+ baseDir.mkdir();
+ }
+ return new File(baseDir, baseName).getAbsolutePath();
+ }
+
public void removeUser(int userId) {
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
@@ -446,15 +481,20 @@
}
}
} else {
- // Manged profile
+ // Managed profile
removeChildProfileLock(userId);
}
+ File spStateDir = getSyntheticPasswordDirectoryForUser(userId);
try {
db.beginTransaction();
db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
db.setTransactionSuccessful();
mCache.removeUser(userId);
+ // The directory itself will be deleted as part of user deletion operation by the
+ // framework, so only need to purge cache here.
+ //TODO: (b/34600579) invoke secdiscardable
+ mCache.purgePath(spStateDir.getAbsolutePath());
} finally {
db.endTransaction();
}
@@ -619,6 +659,16 @@
mVersion++;
}
+ synchronized void purgePath(String path) {
+ for (int i = mCache.size() - 1; i >= 0; i--) {
+ CacheKey entry = mCache.keyAt(i);
+ if (entry.type == CacheKey.TYPE_FILE && entry.key.startsWith(path)) {
+ mCache.removeAt(i);
+ }
+ }
+ mVersion++;
+ }
+
synchronized void clear() {
mCache.clear();
mVersion++;
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 32b7e4d..0415971 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -2922,10 +2922,10 @@
waitForReady();
if (StorageManager.isFileEncryptedNativeOrEmulated()) {
- // When a user has secure lock screen, require a challenge token to
- // actually unlock. This check is mostly in place for emulation mode.
- if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(token)) {
- throw new IllegalStateException("Token required to unlock secure user " + userId);
+ // When a user has secure lock screen, require secret to actually unlock.
+ // This check is mostly in place for emulation mode.
+ if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(secret)) {
+ throw new IllegalStateException("Secret required to unlock secure user " + userId);
}
try {
diff --git a/services/core/java/com/android/server/SyntheticPasswordCrypto.java b/services/core/java/com/android/server/SyntheticPasswordCrypto.java
new file mode 100644
index 0000000..12d91c5
--- /dev/null
+++ b/services/core/java/com/android/server/SyntheticPasswordCrypto.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.security.keystore.KeyProperties;
+import android.security.keystore.KeyProtection;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+
+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.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class SyntheticPasswordCrypto {
+ private static final int PROFILE_KEY_IV_SIZE = 12;
+ private static final int AES_KEY_LENGTH = 32; // 256-bit AES key
+ private static final byte[] APPLICATION_ID_PERSONALIZATION = "application-id".getBytes();
+ // Time between the user credential is verified with GK and the decryption of synthetic password
+ // under the auth-bound key. This should always happen one after the other, but give it 15
+ // seconds just to be sure.
+ private static final int USER_AUTHENTICATION_VALIDITY = 15;
+
+ private static byte[] decrypt(SecretKey key, byte[] blob)
+ throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
+ InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
+ if (blob == null) {
+ return null;
+ }
+ byte[] iv = Arrays.copyOfRange(blob, 0, PROFILE_KEY_IV_SIZE);
+ byte[] ciphertext = Arrays.copyOfRange(blob, PROFILE_KEY_IV_SIZE, blob.length);
+ Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE);
+ cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
+ return cipher.doFinal(ciphertext);
+ }
+
+ private static byte[] encrypt(SecretKey key, byte[] blob)
+ throws IOException, NoSuchAlgorithmException, NoSuchPaddingException,
+ InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+ if (blob == null) {
+ return null;
+ }
+ Cipher cipher = Cipher.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/"
+ + KeyProperties.ENCRYPTION_PADDING_NONE);
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] ciphertext = cipher.doFinal(blob);
+ byte[] iv = cipher.getIV();
+ if (iv.length != PROFILE_KEY_IV_SIZE) {
+ throw new RuntimeException("Invalid iv length: " + iv.length);
+ }
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ outputStream.write(iv);
+ outputStream.write(ciphertext);
+ return outputStream.toByteArray();
+ }
+
+ public static byte[] encrypt(byte[] keyBytes, byte[] personalisation, byte[] message) {
+ byte[] keyHash = personalisedHash(personalisation, keyBytes);
+ SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(keyHash, AES_KEY_LENGTH),
+ KeyProperties.KEY_ALGORITHM_AES);
+ try {
+ return encrypt(key, message);
+ } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
+ | IllegalBlockSizeException | BadPaddingException | IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static byte[] decrypt(byte[] keyBytes, byte[] personalisation, byte[] ciphertext) {
+ byte[] keyHash = personalisedHash(personalisation, keyBytes);
+ SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(keyHash, AES_KEY_LENGTH),
+ KeyProperties.KEY_ALGORITHM_AES);
+ try {
+ return decrypt(key, ciphertext);
+ } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException
+ | IllegalBlockSizeException | BadPaddingException
+ | InvalidAlgorithmParameterException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static byte[] decryptBlob(String keyAlias, byte[] blob, byte[] applicationId) {
+ try {
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+
+ SecretKey decryptionKey = (SecretKey) keyStore.getKey(keyAlias, null);
+ byte[] intermediate = decrypt(applicationId, APPLICATION_ID_PERSONALIZATION, blob);
+ return decrypt(decryptionKey, intermediate);
+ } catch (CertificateException | IOException | BadPaddingException
+ | IllegalBlockSizeException
+ | KeyStoreException | NoSuchPaddingException | NoSuchAlgorithmException
+ | InvalidKeyException | UnrecoverableKeyException
+ | InvalidAlgorithmParameterException e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to decrypt blob", e);
+ }
+ }
+
+ public static byte[] createBlob(String keyAlias, byte[] data, byte[] applicationId, long sid) {
+ try {
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES);
+ keyGenerator.init(new SecureRandom());
+ SecretKey secretKey = keyGenerator.generateKey();
+ KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ KeyProtection.Builder builder = new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE);
+ if (sid != 0) {
+ builder.setUserAuthenticationRequired(true)
+ .setBoundToSpecificSecureUserId(sid)
+ .setUserAuthenticationValidityDurationSeconds(USER_AUTHENTICATION_VALIDITY);
+ }
+ keyStore.setEntry(keyAlias,
+ new KeyStore.SecretKeyEntry(secretKey),
+ builder.build());
+ byte[] intermediate = encrypt(secretKey, data);
+ return encrypt(applicationId, APPLICATION_ID_PERSONALIZATION, intermediate);
+
+ } catch (CertificateException | IOException | BadPaddingException
+ | IllegalBlockSizeException
+ | KeyStoreException | NoSuchPaddingException | NoSuchAlgorithmException
+ | InvalidKeyException e) {
+ e.printStackTrace();
+ throw new RuntimeException("Failed to encrypt blob", e);
+ }
+ }
+
+ public static void destroyBlobKey(String keyAlias) {
+ KeyStore keyStore;
+ try {
+ keyStore = KeyStore.getInstance("AndroidKeyStore");
+ keyStore.load(null);
+ keyStore.deleteEntry(keyAlias);
+ } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException
+ | IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ protected static byte[] personalisedHash(byte[] personalisation, byte[]... message) {
+ try {
+ final int PADDING_LENGTH = 128;
+ MessageDigest digest = MessageDigest.getInstance("SHA-512");
+ if (personalisation.length > PADDING_LENGTH) {
+ throw new RuntimeException("Personalisation too long");
+ }
+ // Personalize the hash
+ // Pad it to the block size of the hash function
+ personalisation = Arrays.copyOf(personalisation, PADDING_LENGTH);
+ digest.update(personalisation);
+ for (byte[] data : message) {
+ digest.update(data);
+ }
+ return digest.digest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("NoSuchAlgorithmException for SHA-512", e);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/SyntheticPasswordManager.java b/services/core/java/com/android/server/SyntheticPasswordManager.java
new file mode 100644
index 0000000..0449d20
--- /dev/null
+++ b/services/core/java/com/android/server/SyntheticPasswordManager.java
@@ -0,0 +1,592 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.RemoteException;
+import android.service.gatekeeper.GateKeeperResponse;
+import android.service.gatekeeper.IGateKeeperService;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.VerifyCredentialResponse;
+
+import libcore.util.HexEncoding;
+
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Set;
+
+
+/**
+ * A class that maintains the wrapping of synthetic password by user credentials or escrow tokens.
+ * It's (mostly) a pure storage for synthetic passwords, providing APIs to creating and destroying
+ * synthetic password blobs which are wrapped by user credentials or escrow tokens.
+ *
+ * Here is the assumptions it makes:
+ * Each user has one single synthetic password at any time.
+ * The SP has an associated password handle, which binds to the SID for that user. The password
+ * handle is persisted by SyntheticPasswordManager internally.
+ * If the user credential is null, it's treated as if the credential is DEFAULT_PASSWORD
+ */
+public class SyntheticPasswordManager {
+ private static final String SP_BLOB_NAME = "spblob";
+ private static final String SP_E0_NAME = "e0";
+ private static final String SP_P1_NAME = "p1";
+ private static final String SP_HANDLE_NAME = "handle";
+ private static final String SECDISCARDABLE_NAME = "secdis";
+ private static final int SECDISCARDABLE_LENGTH = 16 * 1024;
+ private static final String PASSWORD_DATA_NAME = "pwd";
+
+ public static final long DEFAULT_HANDLE = 0;
+ private static final String DEFAULT_PASSWORD = "default-password";
+
+ private static final byte SYNTHETIC_PASSWORD_VERSION = 1;
+ private static final byte SYNTHETIC_PASSWORD_PASSWORD_BASED = 0;
+
+ // 256-bit synthetic password
+ private static final byte SYNTHETIC_PASSWORD_LENGTH = 256 / 8;
+
+ private static final int PASSWORD_SCRYPT_N = 13;
+ private static final int PASSWORD_SCRYPT_R = 3;
+ private static final int PASSWORD_SCRYPT_P = 1;
+ private static final int PASSWORD_SALT_LENGTH = 16;
+ private static final int PASSWORD_TOKEN_LENGTH = 32;
+ private static final String TAG = "SyntheticPasswordManager";
+
+ private static final byte[] PERSONALISATION_SECDISCARDABLE = "secdiscardable-transform".getBytes();
+ private static final byte[] PERSONALIZATION_KEY_STORE_PASSWORD = "keystore-password".getBytes();
+ private static final byte[] PERSONALIZATION_USER_GK_AUTH = "user-gk-authentication".getBytes();
+ private static final byte[] PERSONALIZATION_SP_GK_AUTH = "sp-gk-authentication".getBytes();
+ private static final byte[] PERSONALIZATION_FBE_KEY = "fbe-key".getBytes();
+ private static final byte[] PERSONALIZATION_SP_SPLIT = "sp-split".getBytes();
+ private static final byte[] PERSONALIZATION_E0 = "e0-encryption".getBytes();
+
+ static class AuthenticationResult {
+ public AuthenticationToken authToken;
+ public VerifyCredentialResponse gkResponse;
+ }
+
+ static class AuthenticationToken {
+ /*
+ * Here is the relationship between all three fields:
+ * P0 and P1 are two randomly-generated blocks. P1 is stored on disk but P0 is not.
+ * syntheticPassword = hash(P0 || P1)
+ * E0 = P0 encrypted under syntheticPassword, stored on disk.
+ */
+ private @Nullable byte[] E0;
+ private @Nullable byte[] P1;
+ private @NonNull String syntheticPassword;
+
+ public String deriveKeyStorePassword() {
+ return bytesToHex(SyntheticPasswordCrypto.personalisedHash(
+ PERSONALIZATION_KEY_STORE_PASSWORD, syntheticPassword.getBytes()));
+ }
+
+ public byte[] deriveGkPassword() {
+ return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_SP_GK_AUTH,
+ syntheticPassword.getBytes());
+ }
+
+ public byte[] deriveDiskEncryptionKey() {
+ return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_FBE_KEY,
+ syntheticPassword.getBytes());
+ }
+
+ public void initialize(byte[] P0, byte[] P1) {
+ this.P1 = P1;
+ this.syntheticPassword = String.valueOf(HexEncoding.encode(
+ SyntheticPasswordCrypto.personalisedHash(
+ PERSONALIZATION_SP_SPLIT, P0, P1)));
+ this.E0 = SyntheticPasswordCrypto.encrypt(this.syntheticPassword.getBytes(),
+ PERSONALIZATION_E0, P0);
+ }
+
+ protected static AuthenticationToken create() {
+ AuthenticationToken result = new AuthenticationToken();
+ result.initialize(secureRandom(SYNTHETIC_PASSWORD_LENGTH),
+ secureRandom(SYNTHETIC_PASSWORD_LENGTH));
+ return result;
+ }
+
+ public byte[] computeP0() {
+ if (E0 == null) {
+ return null;
+ }
+ return SyntheticPasswordCrypto.decrypt(syntheticPassword.getBytes(), PERSONALIZATION_E0,
+ E0);
+ }
+ }
+
+ static class PasswordData {
+ byte scryptN;
+ byte scryptR;
+ byte scryptP;
+ public int passwordType;
+ byte[] salt;
+ public byte[] passwordHandle;
+
+ public static PasswordData create(int passwordType) {
+ PasswordData result = new PasswordData();
+ result.scryptN = PASSWORD_SCRYPT_N;
+ result.scryptR = PASSWORD_SCRYPT_R;
+ result.scryptP = PASSWORD_SCRYPT_P;
+ result.passwordType = passwordType;
+ result.salt = secureRandom(PASSWORD_SALT_LENGTH);
+ return result;
+ }
+
+ public static PasswordData fromBytes(byte[] data) {
+ PasswordData result = new PasswordData();
+ ByteBuffer buffer = ByteBuffer.allocate(data.length);
+ buffer.put(data, 0, data.length);
+ buffer.flip();
+ result.passwordType = buffer.getInt();
+ result.scryptN = buffer.get();
+ result.scryptR = buffer.get();
+ result.scryptP = buffer.get();
+ int saltLen = buffer.getInt();
+ result.salt = new byte[saltLen];
+ buffer.get(result.salt);
+ int handleLen = buffer.getInt();
+ result.passwordHandle = new byte[handleLen];
+ buffer.get(result.passwordHandle);
+ return result;
+ }
+
+ public byte[] toBytes() {
+ ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES + 3 * Byte.BYTES
+ + Integer.BYTES + salt.length + Integer.BYTES + passwordHandle.length);
+ buffer.putInt(passwordType);
+ buffer.put(scryptN);
+ buffer.put(scryptR);
+ buffer.put(scryptP);
+ buffer.putInt(salt.length);
+ buffer.put(salt);
+ buffer.putInt(passwordHandle.length);
+ buffer.put(passwordHandle);
+ return buffer.array();
+ }
+ }
+
+ private LockSettingsStorage mStorage;
+
+ public SyntheticPasswordManager(LockSettingsStorage storage) {
+ mStorage = storage;
+ }
+
+
+ public int getCredentialType(long handle, int userId) {
+ byte[] passwordData = loadState(PASSWORD_DATA_NAME, handle, userId);
+ if (passwordData == null) {
+ Log.w(TAG, "getCredentialType: encountered empty password data for user " + userId);
+ return LockPatternUtils.CREDENTIAL_TYPE_NONE;
+ }
+ return PasswordData.fromBytes(passwordData).passwordType;
+ }
+
+ /**
+ * Initializing a new Authentication token, possibly from an existing credential and hash.
+ *
+ * The authentication token would bear a randomly-generated synthetic password.
+ *
+ * This method has the side effect of rebinding the SID of the given user to the
+ * newly-generated SP.
+ *
+ * If the existing credential hash is non-null, the existing SID mill be migrated so
+ * the synthetic password in the authentication token will produce the same SID
+ * (the corresponding synthetic password handle is persisted by SyntheticPasswordManager
+ * in a per-user data storage.
+ *
+ * If the existing credential hash is null, it means the given user should have no SID so
+ * SyntheticPasswordManager will nuke any SP handle previously persisted. In this case,
+ * the supplied credential parameter is also ignored.
+ *
+ * Also saves the escrow information necessary to re-generate the synthetic password under
+ * an escrow scheme. This information can be removed with {@link #destroyEscrowData} if
+ * password escrow should be disabled completely on the given user.
+ *
+ */
+ public AuthenticationToken newSyntheticPasswordAndSid(IGateKeeperService gatekeeper,
+ byte[] hash, String credential, int userId) throws RemoteException {
+ AuthenticationToken result = AuthenticationToken.create();
+ GateKeeperResponse response;
+ if (hash != null) {
+ response = gatekeeper.enroll(userId, hash, credential.getBytes(),
+ result.deriveGkPassword());
+ if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
+ Log.w(TAG, "Fail to migrate SID, assuming no SID, user " + userId);
+ clearSidForUser(userId);
+ } else {
+ saveSyntheticPasswordHandle(response.getPayload(), userId);
+ }
+ } else {
+ clearSidForUser(userId);
+ }
+ saveEscrowData(result, userId);
+ return result;
+ }
+
+ /**
+ * Enroll a new password handle and SID for the given synthetic password and persist it on disk.
+ * Used when adding password to previously-unsecured devices.
+ */
+ public void newSidForUser(IGateKeeperService gatekeeper, AuthenticationToken authToken,
+ int userId) throws RemoteException {
+ GateKeeperResponse response = gatekeeper.enroll(userId, null, null,
+ authToken.deriveGkPassword());
+ if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
+ Log.e(TAG, "Fail to create new SID for user " + userId);
+ return;
+ }
+ saveSyntheticPasswordHandle(response.getPayload(), userId);
+ }
+
+ // Nuke the SP handle (and as a result, its SID) for the given user.
+ public void clearSidForUser(int userId) {
+ destroyState(SP_HANDLE_NAME, true, DEFAULT_HANDLE, userId);
+ }
+
+ public boolean hasSidForUser(int userId) {
+ return hasState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId);
+ }
+
+ // if null, it means there is no SID associated with the user
+ // This can happen if the user is migrated to SP but currently
+ // do not have a lockscreen password.
+ private byte[] loadSyntheticPasswordHandle(int userId) {
+ return loadState(SP_HANDLE_NAME, DEFAULT_HANDLE, userId);
+ }
+
+ private void saveSyntheticPasswordHandle(byte[] spHandle, int userId) {
+ saveState(SP_HANDLE_NAME, spHandle, DEFAULT_HANDLE, userId);
+ }
+
+ private boolean loadEscrowData(AuthenticationToken authToken, int userId) {
+ authToken.E0 = loadState(SP_E0_NAME, DEFAULT_HANDLE, userId);
+ authToken.P1 = loadState(SP_P1_NAME, DEFAULT_HANDLE, userId);
+ return authToken.E0 != null && authToken.P1 != null;
+ }
+
+ private void saveEscrowData(AuthenticationToken authToken, int userId) {
+ saveState(SP_E0_NAME, authToken.E0, DEFAULT_HANDLE, userId);
+ saveState(SP_P1_NAME, authToken.P1, DEFAULT_HANDLE, userId);
+ }
+
+ public void destroyEscrowData(int userId) {
+ destroyState(SP_E0_NAME, true, DEFAULT_HANDLE, userId);
+ destroyState(SP_P1_NAME, true, DEFAULT_HANDLE, userId);
+ }
+
+ /**
+ * Create a new password based SP blob based on the supplied authentication token, such that
+ * a future successful authentication with unwrapPasswordBasedSyntheticPassword() would result
+ * in the same authentication token.
+ *
+ * This method only creates SP blob wrapping around the given synthetic password and does not
+ * handle logic around SID or SP handle. The caller should separately ensure that the user's SID
+ * is consistent with the device state by calling other APIs in this class.
+ *
+ * @see #newSidForUser
+ * @see #clearSidForUser
+ */
+ public long createPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper,
+ String credential, int credentialType, AuthenticationToken authToken, int userId)
+ throws RemoteException {
+ if (credential == null || credentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+ credentialType = LockPatternUtils.CREDENTIAL_TYPE_NONE;
+ credential = DEFAULT_PASSWORD;
+ }
+
+ long handle = generateHandle();
+ PasswordData pwd = PasswordData.create(credentialType);
+ byte[] pwdToken = computePasswordToken(credential, pwd);
+
+ GateKeeperResponse response = gatekeeper.enroll(fakeUid(userId), null, null,
+ passwordTokenToGkInput(pwdToken));
+ if (response.getResponseCode() != GateKeeperResponse.RESPONSE_OK) {
+ Log.e(TAG, "Fail to enroll user password when creating SP for user " + userId);
+ return 0;
+ }
+ pwd.passwordHandle = response.getPayload();
+ long sid = sidFromPasswordHandle(pwd.passwordHandle);
+ saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId);
+
+ byte[] applicationId = transformUnderSecdiscardable(pwdToken,
+ createSecdiscardable(handle, userId));
+ createSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED, authToken,
+ applicationId, sid, userId);
+ return handle;
+ }
+
+ private void createSyntheticPasswordBlob(long handle, byte type, AuthenticationToken authToken,
+ byte[] applicationId, long sid, int userId) {
+ final byte[] secret = authToken.syntheticPassword.getBytes();
+ byte[] content = createSPBlob(getHandleName(handle), secret, applicationId, sid);
+ byte[] blob = new byte[content.length + 1 + 1];
+ blob[0] = SYNTHETIC_PASSWORD_VERSION;
+ blob[1] = type;
+ System.arraycopy(content, 0, blob, 2, content.length);
+ saveState(SP_BLOB_NAME, blob, handle, userId);
+ }
+
+ /**
+ * Decrypt a synthetic password by supplying the user credential and corresponding password
+ * blob handle generated previously. If the decryption is successful, initiate a GateKeeper
+ * verification to referesh the SID & Auth token maintained by the system.
+ */
+ public AuthenticationResult unwrapPasswordBasedSyntheticPassword(IGateKeeperService gatekeeper,
+ long handle, String credential, int userId) throws RemoteException {
+ if (credential == null) {
+ credential = DEFAULT_PASSWORD;
+ }
+ AuthenticationResult result = new AuthenticationResult();
+ PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, handle, userId));
+ byte[] pwdToken = computePasswordToken(credential, pwd);
+ byte[] gkPwdToken = passwordTokenToGkInput(pwdToken);
+
+ GateKeeperResponse response = gatekeeper.verifyChallenge(fakeUid(userId), 0L,
+ pwd.passwordHandle, gkPwdToken);
+ int responseCode = response.getResponseCode();
+ if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+ result.gkResponse = VerifyCredentialResponse.OK;
+ if (response.getShouldReEnroll()) {
+ GateKeeperResponse reenrollResponse = gatekeeper.enroll(fakeUid(userId),
+ pwd.passwordHandle, gkPwdToken, gkPwdToken);
+ if (reenrollResponse.getResponseCode() == GateKeeperResponse.RESPONSE_OK) {
+ pwd.passwordHandle = reenrollResponse.getPayload();
+ saveState(PASSWORD_DATA_NAME, pwd.toBytes(), handle, userId);
+ } else {
+ Log.w(TAG, "Fail to re-enroll user password for user " + userId);
+ // continue the flow anyway
+ }
+ }
+ } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+ result.gkResponse = new VerifyCredentialResponse(response.getTimeout());
+ return result;
+ } else {
+ result.gkResponse = VerifyCredentialResponse.ERROR;
+ return result;
+ }
+
+
+ byte[] applicationId = transformUnderSecdiscardable(pwdToken,
+ loadSecdiscardable(handle, userId));
+ result.authToken = unwrapSyntheticPasswordBlob(handle, SYNTHETIC_PASSWORD_PASSWORD_BASED,
+ applicationId, userId);
+
+ // Perform verifyChallenge to refresh auth tokens for GK if user password exists.
+ result.gkResponse = verifyChallenge(gatekeeper, result.authToken, 0L, userId);
+ return result;
+ }
+
+ private AuthenticationToken unwrapSyntheticPasswordBlob(long handle, byte type,
+ byte[] applicationId, int userId) {
+ byte[] blob = loadState(SP_BLOB_NAME, handle, userId);
+ if (blob == null) {
+ return null;
+ }
+ if (blob[0] != SYNTHETIC_PASSWORD_VERSION) {
+ throw new RuntimeException("Unknown blob version");
+ }
+ if (blob[1] != type) {
+ throw new RuntimeException("Invalid blob type");
+ }
+ byte[] secret = decryptSPBlob(getHandleName(handle),
+ Arrays.copyOfRange(blob, 2, blob.length), applicationId);
+ if (secret == null) {
+ Log.e(TAG, "Fail to decrypt SP for user " + userId);
+ return null;
+ }
+ AuthenticationToken result = new AuthenticationToken();
+ result.syntheticPassword = new String(secret);
+ return result;
+ }
+
+ /**
+ * performs GK verifyChallenge and returns auth token, re-enrolling SP password handle
+ * if required.
+ *
+ * Normally performing verifyChallenge with an AuthenticationToken should always return
+ * RESPONSE_OK, since user authentication failures are detected earlier when trying to
+ * decrypt SP.
+ */
+ public VerifyCredentialResponse verifyChallenge(IGateKeeperService gatekeeper,
+ @NonNull AuthenticationToken auth, long challenge, int userId) throws RemoteException {
+ byte[] spHandle = loadSyntheticPasswordHandle(userId);
+ if (spHandle == null) {
+ // There is no password handle associated with the given user, i.e. the user is not
+ // secured by lockscreen and has no SID, so just return here;
+ return null;
+ }
+ VerifyCredentialResponse result;
+ GateKeeperResponse response = gatekeeper.verifyChallenge(userId, challenge,
+ spHandle, auth.deriveGkPassword());
+ int responseCode = response.getResponseCode();
+ if (responseCode == GateKeeperResponse.RESPONSE_OK) {
+ result = new VerifyCredentialResponse(response.getPayload());
+ if (response.getShouldReEnroll()) {
+ response = gatekeeper.enroll(userId, spHandle,
+ spHandle, auth.deriveGkPassword());
+ if (response.getResponseCode() == GateKeeperResponse.RESPONSE_OK) {
+ spHandle = response.getPayload();
+ saveSyntheticPasswordHandle(spHandle, userId);
+ // Call self again to re-verify with updated handle
+ return verifyChallenge(gatekeeper, auth, challenge, userId);
+ } else {
+ Log.w(TAG, "Fail to re-enroll SP handle for user " + userId);
+ // Fall through, return existing handle
+ }
+ }
+ } else if (responseCode == GateKeeperResponse.RESPONSE_RETRY) {
+ result = new VerifyCredentialResponse(response.getTimeout());
+ } else {
+ result = VerifyCredentialResponse.ERROR;
+ }
+ return result;
+ }
+
+ public boolean existsHandle(long handle, int userId) {
+ return hasState(SP_BLOB_NAME, handle, userId);
+ }
+
+ public void destroyPasswordBasedSyntheticPassword(long handle, int userId) {
+ destroySyntheticPassword(handle, userId);
+ destroyState(SECDISCARDABLE_NAME, true, handle, userId);
+ destroyState(PASSWORD_DATA_NAME, true, handle, userId);
+ }
+
+ private void destroySyntheticPassword(long handle, int userId) {
+ destroyState(SP_BLOB_NAME, true, handle, userId);
+ destroyState(SP_E0_NAME, true, handle, userId);
+ destroyState(SP_P1_NAME, true, handle, userId);
+ destroySPBlobKey(getHandleName(handle));
+ }
+
+ private byte[] transformUnderSecdiscardable(byte[] data, byte[] rawSecdiscardable) {
+ byte[] secdiscardable = SyntheticPasswordCrypto.personalisedHash(
+ PERSONALISATION_SECDISCARDABLE, rawSecdiscardable);
+ byte[] result = new byte[data.length + secdiscardable.length];
+ System.arraycopy(data, 0, result, 0, data.length);
+ System.arraycopy(secdiscardable, 0, result, data.length, secdiscardable.length);
+ return result;
+ }
+
+ private byte[] createSecdiscardable(long handle, int userId) {
+ byte[] data = secureRandom(SECDISCARDABLE_LENGTH);
+ saveState(SECDISCARDABLE_NAME, data, handle, userId);
+ return data;
+ }
+
+ private byte[] loadSecdiscardable(long handle, int userId) {
+ return loadState(SECDISCARDABLE_NAME, handle, userId);
+ }
+
+ private boolean hasState(String stateName, long handle, int userId) {
+ return !ArrayUtils.isEmpty(loadState(stateName, handle, userId));
+ }
+
+ private byte[] loadState(String stateName, long handle, int userId) {
+ return mStorage.readSyntheticPasswordState(userId, handle, stateName);
+ }
+
+ private void saveState(String stateName, byte[] data, long handle, int userId) {
+ mStorage.writeSyntheticPasswordState(userId, handle, stateName, data);
+ }
+
+ private void destroyState(String stateName, boolean secure, long handle, int userId) {
+ mStorage.deleteSyntheticPasswordState(userId, handle, stateName, secure);
+ }
+
+ protected byte[] decryptSPBlob(String blobKeyName, byte[] blob, byte[] applicationId) {
+ return SyntheticPasswordCrypto.decryptBlob(blobKeyName, blob, applicationId);
+ }
+
+ protected byte[] createSPBlob(String blobKeyName, byte[] data, byte[] applicationId, long sid) {
+ return SyntheticPasswordCrypto.createBlob(blobKeyName, data, applicationId, sid);
+ }
+
+ protected void destroySPBlobKey(String keyAlias) {
+ SyntheticPasswordCrypto.destroyBlobKey(keyAlias);
+ }
+
+ public static long generateHandle() {
+ SecureRandom rng = new SecureRandom();
+ long result;
+ do {
+ result = rng.nextLong();
+ } while (result == DEFAULT_HANDLE);
+ return result;
+ }
+
+ private int fakeUid(int uid) {
+ return 100000 + uid;
+ }
+
+ protected static byte[] secureRandom(int length) {
+ try {
+ return SecureRandom.getInstance("SHA1PRNG").generateSeed(length);
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private String getHandleName(long handle) {
+ return String.format("%s%x", LockPatternUtils.SYNTHETIC_PASSWORD_KEY_PREFIX, handle);
+ }
+
+ private byte[] computePasswordToken(String password, PasswordData data) {
+ return scrypt(password, data.salt, 1 << data.scryptN, 1 << data.scryptR, 1 << data.scryptP,
+ PASSWORD_TOKEN_LENGTH);
+ }
+
+ private byte[] passwordTokenToGkInput(byte[] token) {
+ return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_USER_GK_AUTH, token);
+ }
+
+ protected long sidFromPasswordHandle(byte[] handle) {
+ return nativeSidFromPasswordHandle(handle);
+ }
+
+ protected byte[] scrypt(String password, byte[] salt, int N, int r, int p, int outLen) {
+ return nativeScrypt(password.getBytes(), salt, N, r, p, outLen);
+ }
+
+ native long nativeSidFromPasswordHandle(byte[] handle);
+ native byte[] nativeScrypt(byte[] password, byte[] salt, int N, int r, int p, int outLen);
+
+ final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+ public static String bytesToHex(byte[] bytes) {
+ if (bytes == null) {
+ return "null";
+ }
+ char[] hexChars = new char[bytes.length * 2];
+ for ( int j = 0; j < bytes.length; j++ ) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+}
diff --git a/services/core/jni/Android.mk b/services/core/jni/Android.mk
index eab5d8a..2c3cda5 100644
--- a/services/core/jni/Android.mk
+++ b/services/core/jni/Android.mk
@@ -19,6 +19,7 @@
$(LOCAL_REL_DIR)/com_android_server_location_GnssLocationProvider.cpp \
$(LOCAL_REL_DIR)/com_android_server_power_PowerManagerService.cpp \
$(LOCAL_REL_DIR)/com_android_server_SerialService.cpp \
+ $(LOCAL_REL_DIR)/com_android_server_SyntheticPasswordManager.cpp \
$(LOCAL_REL_DIR)/com_android_server_storage_AppFuseBridge.cpp \
$(LOCAL_REL_DIR)/com_android_server_SystemServer.cpp \
$(LOCAL_REL_DIR)/com_android_server_tv_TvUinputBridge.cpp \
@@ -33,12 +34,14 @@
LOCAL_C_INCLUDES += \
$(JNI_H_INCLUDE) \
+ external/scrypt/lib/crypto \
frameworks/base/services \
frameworks/base/libs \
frameworks/base/libs/hwui \
frameworks/base/core/jni \
frameworks/native/services \
system/core/libappfuse/include \
+ system/gatekeeper/include \
system/security/keystore/include \
$(call include-path-for, libhardware)/hardware \
$(call include-path-for, libhardware_legacy)/hardware_legacy \
@@ -50,6 +53,7 @@
libappfuse \
libbinder \
libcutils \
+ libcrypto \
liblog \
libhardware \
libhardware_legacy \
@@ -83,3 +87,5 @@
android.hardware.tv.input@1.0 \
android.hardware.vibrator@1.0 \
android.hardware.vr@1.0 \
+
+LOCAL_STATIC_LIBRARIES += libscrypt_static
\ No newline at end of file
diff --git a/services/core/jni/com_android_server_SyntheticPasswordManager.cpp b/services/core/jni/com_android_server_SyntheticPasswordManager.cpp
new file mode 100644
index 0000000..a9f7b9f
--- /dev/null
+++ b/services/core/jni/com_android_server_SyntheticPasswordManager.cpp
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+#define LOG_TAG "SyntheticPasswordManager"
+
+#include "JNIHelp.h"
+#include "jni.h"
+
+#include <android_runtime/Log.h>
+#include <utils/Timers.h>
+#include <utils/misc.h>
+#include <utils/String8.h>
+#include <utils/Log.h>
+#include <gatekeeper/password_handle.h>
+
+
+extern "C" {
+#include "crypto_scrypt.h"
+}
+
+namespace android {
+
+static jlong android_server_SyntheticPasswordManager_nativeSidFromPasswordHandle(JNIEnv* env, jobject, jbyteArray handleArray) {
+
+ jbyte* data = (jbyte*)env->GetPrimitiveArrayCritical(handleArray, NULL);
+
+ if (data != NULL) {
+ const gatekeeper::password_handle_t *handle =
+ reinterpret_cast<const gatekeeper::password_handle_t *>(data);
+ jlong sid = handle->user_id;
+ env->ReleasePrimitiveArrayCritical(handleArray, data, JNI_ABORT);
+ return sid;
+ } else {
+ return 0;
+ }
+}
+
+static jbyteArray android_server_SyntheticPasswordManager_nativeScrypt(JNIEnv* env, jobject, jbyteArray password, jbyteArray salt, jint N, jint r, jint p, jint outLen) {
+ if (!password || !salt) {
+ return NULL;
+ }
+
+ int passwordLen = env->GetArrayLength(password);
+ int saltLen = env->GetArrayLength(salt);
+ jbyteArray ret = env->NewByteArray(outLen);
+
+ jbyte* passwordPtr = (jbyte*)env->GetByteArrayElements(password, NULL);
+ jbyte* saltPtr = (jbyte*)env->GetByteArrayElements(salt, NULL);
+ jbyte* retPtr = (jbyte*)env->GetByteArrayElements(ret, NULL);
+
+ int rc = crypto_scrypt((const uint8_t *)passwordPtr, passwordLen,
+ (const uint8_t *)saltPtr, saltLen, N, r, p, (uint8_t *)retPtr,
+ outLen);
+ env->ReleaseByteArrayElements(password, passwordPtr, JNI_ABORT);
+ env->ReleaseByteArrayElements(salt, saltPtr, JNI_ABORT);
+ env->ReleaseByteArrayElements(ret, retPtr, 0);
+
+ if (!rc) {
+ return ret;
+ } else {
+ SLOGE("scrypt failed");
+ return NULL;
+ }
+}
+
+static const JNINativeMethod sMethods[] = {
+ /* name, signature, funcPtr */
+ {"nativeSidFromPasswordHandle", "([B)J", (void*)android_server_SyntheticPasswordManager_nativeSidFromPasswordHandle},
+ {"nativeScrypt", "([B[BIIII)[B", (void*)android_server_SyntheticPasswordManager_nativeScrypt},
+};
+
+int register_android_server_SyntheticPasswordManager(JNIEnv* env) {
+ return jniRegisterNativeMethods(env, "com/android/server/SyntheticPasswordManager",
+ sMethods, NELEM(sMethods));
+}
+
+} /* namespace android */
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 6f505d5..899640e 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -45,6 +45,7 @@
int register_android_server_PersistentDataBlockService(JNIEnv* env);
int register_android_server_Watchdog(JNIEnv* env);
int register_android_server_HardwarePropertiesManagerService(JNIEnv* env);
+int register_android_server_SyntheticPasswordManager(JNIEnv* env);
};
using namespace android;
@@ -85,6 +86,7 @@
register_android_server_Watchdog(env);
register_android_server_HardwarePropertiesManagerService(env);
register_android_server_storage_AppFuse(env);
+ register_android_server_SyntheticPasswordManager(env);
return JNI_VERSION_1_4;
}