Do not require reauth when turning on profile with unified challenge
Encrypt a copy of the profile unified challenge with an auth-bound key of 7-day timeout, so that the framework can rederive the challenge as long as the user has unlocked the device in the last 7 days. Bug: 72178550 Test: atest com.android.server.locksettings Test: atest com.android.cts.devicepolicy.QuietModeHostsideTest Change-Id: Icb1335ebce2bd1a29f5a7111b61536578fccedd5
This commit is contained in:
@@ -89,4 +89,6 @@ interface ILockSettings {
|
||||
in List<WrappedApplicationKey> applicationKeys);
|
||||
void closeSession(in String sessionId);
|
||||
boolean hasSecureLockScreen();
|
||||
boolean tryUnlockWithCachedUnifiedChallenge(int userId);
|
||||
void removeCachedUnifiedChallenge(int userId);
|
||||
}
|
||||
|
||||
@@ -55,10 +55,10 @@ import android.util.SparseLongArray;
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.server.LocalServices;
|
||||
|
||||
import libcore.util.HexEncoding;
|
||||
|
||||
import com.google.android.collect.Lists;
|
||||
|
||||
import libcore.util.HexEncoding;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.security.MessageDigest;
|
||||
@@ -1755,4 +1755,33 @@ public class LockPatternUtils {
|
||||
return FRP_CREDENTIAL_ENABLED && context.getResources().getBoolean(
|
||||
com.android.internal.R.bool.config_enableCredentialFactoryResetProtection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to rederive the unified work challenge for the specified profile user and unlock the
|
||||
* user. If successful, this would allow the user to leave quiet mode automatically without
|
||||
* additional user authentication.
|
||||
*
|
||||
* This is made possible by the framework storing an encrypted copy of the unified challenge
|
||||
* auth-bound to the primary user's lockscreen. As long as the primery user has unlocked
|
||||
* recently (7 days), the framework will be able to decrypt it and plug the secret into the
|
||||
* unlock flow.
|
||||
*
|
||||
* @return {@code true} if automatic unlocking is successful, {@code false} otherwise.
|
||||
*/
|
||||
public boolean tryUnlockWithCachedUnifiedChallenge(int userId) {
|
||||
try {
|
||||
return getLockSettings().tryUnlockWithCachedUnifiedChallenge(userId);
|
||||
} catch (RemoteException re) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove cached unified profile challenge, for testing and CTS usage. */
|
||||
public void removeCachedUnifiedChallenge(int userId) {
|
||||
try {
|
||||
getLockSettings().removeCachedUnifiedChallenge(userId);
|
||||
} catch (RemoteException re) {
|
||||
re.rethrowFromSystemServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ import java.io.FileDescriptor;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyStoreException;
|
||||
@@ -224,6 +225,7 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
private final KeyStore mKeyStore;
|
||||
|
||||
private final RecoverableKeyStoreManager mRecoverableKeyStoreManager;
|
||||
private ManagedProfilePasswordCache mManagedProfilePasswordCache;
|
||||
|
||||
private final RebootEscrowManager mRebootEscrowManager;
|
||||
|
||||
@@ -387,6 +389,7 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
setLockCredentialInternal(unifiedProfilePassword, managedUserPassword, managedUserId,
|
||||
/* isLockTiedToParent= */ true);
|
||||
tieProfileLockToParent(managedUserId, unifiedProfilePassword);
|
||||
mManagedProfilePasswordCache.storePassword(managedUserId, unifiedProfilePassword);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +530,16 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
int defaultValue) {
|
||||
return Settings.Global.getInt(contentResolver, keyName, defaultValue);
|
||||
}
|
||||
|
||||
public @NonNull ManagedProfilePasswordCache getManagedProfilePasswordCache() {
|
||||
try {
|
||||
java.security.KeyStore ks = java.security.KeyStore.getInstance("AndroidKeyStore");
|
||||
ks.load(null);
|
||||
return new ManagedProfilePasswordCache(ks, getUserManager());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot load keystore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LockSettingsService(Context context) {
|
||||
@@ -560,6 +573,7 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
mStrongAuthTracker.register(mStrongAuth);
|
||||
|
||||
mSpManager = injector.getSyntheticPasswordManager(mStorage);
|
||||
mManagedProfilePasswordCache = injector.getManagedProfilePasswordCache();
|
||||
|
||||
mRebootEscrowManager = injector.getRebootEscrowManager(new RebootEscrowCallbacks(),
|
||||
mStorage);
|
||||
@@ -706,7 +720,8 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
private void ensureProfileKeystoreUnlocked(int userId) {
|
||||
final KeyStore ks = KeyStore.getInstance();
|
||||
if (ks.state(userId) == KeyStore.State.LOCKED
|
||||
&& tiedManagedProfileReadyToUnlock(mUserManager.getUserInfo(userId))) {
|
||||
&& mUserManager.getUserInfo(userId).isManagedProfile()
|
||||
&& hasUnifiedChallenge(userId)) {
|
||||
Slog.i(TAG, "Managed profile got unlocked, will unlock its keystore");
|
||||
// If boot took too long and the password in vold got expired, parent keystore will
|
||||
// be still locked, we ignore this case since the user will be prompted to unlock
|
||||
@@ -1302,6 +1317,7 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
LockscreenCredential credential = LockscreenCredential.createManagedPassword(
|
||||
decryptionResult);
|
||||
Arrays.fill(decryptionResult, (byte) 0);
|
||||
mManagedProfilePasswordCache.storePassword(userId, credential);
|
||||
return credential;
|
||||
}
|
||||
|
||||
@@ -1381,12 +1397,25 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
}
|
||||
|
||||
for (UserInfo profile : mUserManager.getProfiles(userId)) {
|
||||
// Unlock managed profile with unified lock
|
||||
if (tiedManagedProfileReadyToUnlock(profile)) {
|
||||
// Must pass the challenge on for resetLockout, so it's not over-written, which
|
||||
// causes LockSettingsService to revokeChallenge inappropriately.
|
||||
unlockChildProfile(profile.id, false /* ignoreUserNotAuthenticated */,
|
||||
challengeType, challenge, resetLockouts);
|
||||
if (profile.id == userId) continue;
|
||||
if (!profile.isManagedProfile()) continue;
|
||||
|
||||
if (hasUnifiedChallenge(profile.id)) {
|
||||
if (mUserManager.isUserRunning(profile.id)) {
|
||||
// Unlock managed profile with unified lock
|
||||
// Must pass the challenge on for resetLockout, so it's not over-written, which
|
||||
// causes LockSettingsService to revokeChallenge inappropriately.
|
||||
unlockChildProfile(profile.id, false /* ignoreUserNotAuthenticated */,
|
||||
challengeType, challenge, resetLockouts);
|
||||
} else {
|
||||
try {
|
||||
// Profile not ready for unlock yet, but decrypt the unified challenge now
|
||||
// so it goes into the cache
|
||||
getDecryptedPasswordForTiedProfile(profile.id);
|
||||
} catch (GeneralSecurityException | IOException e) {
|
||||
Slog.d(TAG, "Cache work profile password failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Now we have unlocked the parent user and attempted to unlock the profile we should
|
||||
// show notifications if the profile is still locked.
|
||||
@@ -1417,11 +1446,9 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tiedManagedProfileReadyToUnlock(UserInfo userInfo) {
|
||||
return userInfo.isManagedProfile()
|
||||
&& !getSeparateProfileChallengeEnabledInternal(userInfo.id)
|
||||
&& mStorage.hasChildProfileLock(userInfo.id)
|
||||
&& mUserManager.isUserRunning(userInfo.id);
|
||||
private boolean hasUnifiedChallenge(int userId) {
|
||||
return !getSeparateProfileChallengeEnabledInternal(userId)
|
||||
&& mStorage.hasChildProfileLock(userId);
|
||||
}
|
||||
|
||||
private Map<Integer, LockscreenCredential> getDecryptedPasswordsForAllTiedProfiles(int userId) {
|
||||
@@ -2233,6 +2260,7 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
|
||||
final KeyStore ks = KeyStore.getInstance();
|
||||
ks.onUserRemoved(userId);
|
||||
mManagedProfilePasswordCache.removePassword(userId);
|
||||
|
||||
gateKeeperClearSecureUserId(userId);
|
||||
if (unknownUser || mUserManager.getUserInfo(userId).isManagedProfile()) {
|
||||
@@ -2783,6 +2811,7 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords);
|
||||
|
||||
setUserPasswordMetrics(credential, userId);
|
||||
mManagedProfilePasswordCache.removePassword(userId);
|
||||
|
||||
if (profilePasswords != null) {
|
||||
for (Map.Entry<Integer, LockscreenCredential> entry : profilePasswords.entrySet()) {
|
||||
@@ -3097,6 +3126,22 @@ public class LockSettingsService extends ILockSettings.Stub {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryUnlockWithCachedUnifiedChallenge(int userId) {
|
||||
try (LockscreenCredential cred = mManagedProfilePasswordCache.retrievePassword(userId)) {
|
||||
if (cred == null) {
|
||||
return false;
|
||||
}
|
||||
return doVerifyCredential(cred, CHALLENGE_NONE, 0, userId, null /* progressCallback */)
|
||||
.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCachedUnifiedChallenge(int userId) {
|
||||
mManagedProfilePasswordCache.removePassword(userId);
|
||||
}
|
||||
|
||||
static String timestampToString(long timestamp) {
|
||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(timestamp));
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class LockSettingsShellCommand extends ShellCommand {
|
||||
private static final String COMMAND_SET_DISABLED = "set-disabled";
|
||||
private static final String COMMAND_VERIFY = "verify";
|
||||
private static final String COMMAND_GET_DISABLED = "get-disabled";
|
||||
private static final String COMMAND_REMOVE_CACHE = "remove-cache";
|
||||
private static final String COMMAND_HELP = "help";
|
||||
|
||||
private int mCurrentUserId;
|
||||
@@ -76,6 +77,15 @@ class LockSettingsShellCommand extends ShellCommand {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
switch (cmd) {
|
||||
// Commands that do not require authentication go here.
|
||||
case COMMAND_REMOVE_CACHE:
|
||||
runRemoveCache();
|
||||
return 0;
|
||||
case COMMAND_HELP:
|
||||
onHelp();
|
||||
return 0;
|
||||
}
|
||||
if (!checkCredential()) {
|
||||
return -1;
|
||||
}
|
||||
@@ -105,9 +115,6 @@ class LockSettingsShellCommand extends ShellCommand {
|
||||
case COMMAND_GET_DISABLED:
|
||||
runGetDisabled();
|
||||
break;
|
||||
case COMMAND_HELP:
|
||||
onHelp();
|
||||
break;
|
||||
default:
|
||||
getErrPrintWriter().println("Unknown command: " + cmd);
|
||||
break;
|
||||
@@ -163,6 +170,9 @@ class LockSettingsShellCommand extends ShellCommand {
|
||||
pw.println(" verify [--old <CREDENTIAL>] [--user USER_ID]");
|
||||
pw.println(" Verifies the lock credentials.");
|
||||
pw.println("");
|
||||
pw.println(" remove-cache [--user USER_ID]");
|
||||
pw.println(" Removes cached unified challenge for the managed profile.");
|
||||
pw.println("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,4 +332,9 @@ class LockSettingsShellCommand extends ShellCommand {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void runRemoveCache() {
|
||||
mLockPatternUtils.removeCachedUnifiedChallenge(mCurrentUserId);
|
||||
getOutPrintWriter().println("Password cached removed for user " + mCurrentUserId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.annotation.Nullable;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.security.keystore.AndroidKeyStoreSpi;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import android.security.keystore.UserNotAuthenticatedException;
|
||||
import android.util.Slog;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import com.android.internal.widget.LockscreenCredential;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
/**
|
||||
* Caches *unified* work challenge for user 0's managed profiles. Only user 0's profile is supported
|
||||
* at the moment because the cached credential is encrypted using a keystore key auth-bound to
|
||||
* user 0: this is to match how unified work challenge is similarly auth-bound to its parent user's
|
||||
* lockscreen credential normally. It's possible to extend this class to support managed profiles
|
||||
* for secondary users, that will require generating auth-bound keys to their corresponding parent
|
||||
* user though (which {@link KeyGenParameterSpec} does not support right now).
|
||||
*
|
||||
* <p> The cache is filled whenever the managed profile's unified challenge is created or derived
|
||||
* (as part of the parent user's credential verification flow). It's removed when the profile is
|
||||
* deleted or a (separate) lockscreen credential is explicitly set on the profile. There is also
|
||||
* an ADB command to evict the cache "cmd lock_settings remove-cache --user X", to assist
|
||||
* development and testing.
|
||||
|
||||
* <p> The encrypted credential is stored in-memory only so the cache does not persist across
|
||||
* reboots.
|
||||
*/
|
||||
public class ManagedProfilePasswordCache {
|
||||
|
||||
private static final String TAG = "ManagedProfilePasswordCache";
|
||||
private static final int KEY_LENGTH = 256;
|
||||
private static final int CACHE_TIMEOUT_SECONDS = (int) TimeUnit.DAYS.toSeconds(7);
|
||||
|
||||
private final SparseArray<byte[]> mEncryptedPasswords = new SparseArray<>();
|
||||
private final KeyStore mKeyStore;
|
||||
private final UserManager mUserManager;
|
||||
|
||||
public ManagedProfilePasswordCache(KeyStore keyStore, UserManager userManager) {
|
||||
mKeyStore = keyStore;
|
||||
mUserManager = userManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt and store the password in the cache. Does NOT overwrite existing password cache
|
||||
* if one for the given user already exists.
|
||||
*/
|
||||
public void storePassword(int userId, LockscreenCredential password) {
|
||||
synchronized (mEncryptedPasswords) {
|
||||
if (mEncryptedPasswords.contains(userId)) {
|
||||
return;
|
||||
}
|
||||
UserInfo parent = mUserManager.getProfileParent(userId);
|
||||
if (parent == null || parent.id != UserHandle.USER_SYSTEM) {
|
||||
// Since the cached password is encrypted using a keystore key auth-bound to user 0,
|
||||
// only support caching password for user 0's profile.
|
||||
return;
|
||||
}
|
||||
String keyName = getEncryptionKeyName(userId);
|
||||
KeyGenerator generator;
|
||||
SecretKey key;
|
||||
try {
|
||||
generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
|
||||
AndroidKeyStoreSpi.NAME);
|
||||
generator.init(new KeyGenParameterSpec.Builder(
|
||||
keyName, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||
.setKeySize(KEY_LENGTH)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
// Generate auth-bound key to user 0 (since we the caller is user 0)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setUserAuthenticationValidityDurationSeconds(CACHE_TIMEOUT_SECONDS)
|
||||
// Only accessible after user 0's keyguard is unlocked
|
||||
.setUnlockedDeviceRequired(true)
|
||||
.build());
|
||||
key = generator.generateKey();
|
||||
} catch (GeneralSecurityException e) {
|
||||
Slog.e(TAG, "Cannot generate key", e);
|
||||
return;
|
||||
}
|
||||
|
||||
Cipher cipher;
|
||||
try {
|
||||
cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
byte[] ciphertext = cipher.doFinal(password.getCredential());
|
||||
byte[] iv = cipher.getIV();
|
||||
byte[] block = Arrays.copyOf(iv, ciphertext.length + iv.length);
|
||||
System.arraycopy(ciphertext, 0, block, iv.length, ciphertext.length);
|
||||
mEncryptedPasswords.put(userId, block);
|
||||
} catch (GeneralSecurityException e) {
|
||||
Slog.d(TAG, "Cannot encrypt", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Attempt to retrieve the password for the given user. Returns {@code null} if it's not in the
|
||||
* cache or if decryption fails.
|
||||
*/
|
||||
public @Nullable LockscreenCredential retrievePassword(int userId) {
|
||||
synchronized (mEncryptedPasswords) {
|
||||
byte[] block = mEncryptedPasswords.get(userId);
|
||||
if (block == null) {
|
||||
return null;
|
||||
}
|
||||
Key key;
|
||||
try {
|
||||
key = mKeyStore.getKey(getEncryptionKeyName(userId), null);
|
||||
} catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) {
|
||||
Slog.d(TAG, "Cannot get key", e);
|
||||
return null;
|
||||
}
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] iv = Arrays.copyOf(block, 12);
|
||||
byte[] ciphertext = Arrays.copyOfRange(block, 12, block.length);
|
||||
byte[] credential;
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
|
||||
credential = cipher.doFinal(ciphertext);
|
||||
} catch (UserNotAuthenticatedException e) {
|
||||
Slog.i(TAG, "Device not unlocked for more than 7 days");
|
||||
return null;
|
||||
} catch (GeneralSecurityException e) {
|
||||
Slog.d(TAG, "Cannot decrypt", e);
|
||||
return null;
|
||||
}
|
||||
LockscreenCredential result = LockscreenCredential.createManagedPassword(credential);
|
||||
Arrays.fill(credential, (byte) 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove the given user's password from cache, if one exists. */
|
||||
public void removePassword(int userId) {
|
||||
synchronized (mEncryptedPasswords) {
|
||||
String keyName = getEncryptionKeyName(userId);
|
||||
try {
|
||||
if (mKeyStore.containsAlias(keyName)) {
|
||||
mKeyStore.deleteEntry(keyName);
|
||||
}
|
||||
} catch (KeyStoreException e) {
|
||||
Slog.d(TAG, "Cannot delete key", e);
|
||||
}
|
||||
if (mEncryptedPasswords.contains(userId)) {
|
||||
Arrays.fill(mEncryptedPasswords.get(userId), (byte) 0);
|
||||
mEncryptedPasswords.remove(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getEncryptionKeyName(int userId) {
|
||||
return "com.android.server.locksettings.unified_profile_cache_" + userId;
|
||||
}
|
||||
}
|
||||
@@ -932,6 +932,7 @@ public class UserManagerService extends IUserManager.Stub {
|
||||
userId, true /* enableQuietMode */, target, callingPackage);
|
||||
return true;
|
||||
}
|
||||
mLockPatternUtils.tryUnlockWithCachedUnifiedChallenge(userId);
|
||||
boolean needToShowConfirmCredential =
|
||||
mLockPatternUtils.isSecure(userId)
|
||||
&& !StorageManager.isUserKeyUnlocked(userId);
|
||||
@@ -942,8 +943,7 @@ public class UserManagerService extends IUserManager.Stub {
|
||||
showConfirmCredentialToDisableQuietMode(userId, target);
|
||||
return false;
|
||||
}
|
||||
setQuietModeEnabled(
|
||||
userId, false /* enableQuietMode */, target, callingPackage);
|
||||
setQuietModeEnabled(userId, false /* enableQuietMode */, target, callingPackage);
|
||||
return true;
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(identity);
|
||||
|
||||
@@ -147,6 +147,12 @@ public class LockSettingsServiceTestable extends LockSettingsService {
|
||||
public RecoverableKeyStoreManager getRecoverableKeyStoreManager(KeyStore keyStore) {
|
||||
return mRecoverableKeyStoreManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManagedProfilePasswordCache getManagedProfilePasswordCache() {
|
||||
return mock(ManagedProfilePasswordCache.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public MockInjector mInjector;
|
||||
|
||||
Reference in New Issue
Block a user