MDM定制机实现从远程设置锁屏密码

77 阅读10分钟

一、本地密码安全等级通知

1. 大概流程

业务流程大概如下:
云端下发策略给MdmAgent apk,后者把数据中转到MDMExtension apk,MDMExtension把云端策略转化为Settings值,框架监听Settings值,并进行相应的逻辑处理。
此外,当云端或是系统设置Settings成功修改锁屏密码时,也需要判断修改成功后的密码是否密码等级,并进行相应的逻辑处理。

大概流程.png

2. 具体场景处理

2.1 云端推送安全等级策略场景

云端推送int数组类型的安全等级策略,0(不设置密码等级,默认),1(4位数密码),2(6位数密码),3(复杂密码,至少包含1个字母的4-17位数),MDMExtension以按位或的方式,把int数组转化为int类型数据,并写入Settings值password_secure_level


companion object {   
    const val PASSWORD_SECURE_LEVEL: String = "password_secure_level"  
    const val LEVEL_NONE: Int = 0  
    const val LEVEL_4_PIN: Int = 0b1  
    const val LEVEL_6_PIN: Int = 0b10  
    const val LEVEL_PASSWORD: Int = 0b100  
}  

override fun handleCommand(  
    name: String,  
    bean: PasswordSecureLevelBean,  
    callback: ControlCallback?
): Boolean {  
    var level = LEVEL_NONE  
    run loop@ {
        bean.pwSecureLevel?.forEach { i ->
                when(i) {  
                0 -> {  
                    level = LEVEL_NONE  
                    return@loop  
                }  
                1 -> level = level or LEVEL_4_PIN  
                2 -> level = level or LEVEL_6_PIN  
                3 -> level = level or LEVEL_PASSWORD  
            }  
         }
    }  
   Settings.Secure.putInt(context.contentResolver, PASSWORD_SECURE_LEVEL, level)  
   return true
}

框架层LockSettingsService初始化的时候,添加对password_secure_level值的监听,一旦值有所改变,执行发送或是取消通知逻辑。

private void registerPasswordSecureLevelObserver(Context context) {
    if (context == null) {
        return;
    }
    mContentResolver = context.getContentResolver();
    if (mContentResolver == null) {
        return;
    }
    mContentResolver.registerContentObserver(
            Settings.Secure.getUriFor(Settings.Secure.PASSWORD_SECURE_LEVEL),
            false, new ContentObserver(null) {
                @Override
                public void onChange(boolean selfChange) {
                    super.onChange(selfChange);
                    sendOrCancelPwdLevelNotification();
                }
            });
}

发送或是取消通知的逻辑如下

private void sendOrCancelPwdLevelNotification() {  
    int pwdSecureLevel = Settings.Secure.getInt(mContentResolver,  
            Settings.Secure.PASSWORD_SECURE_LEVEL, LEVEL_NONE);  
    if (pwdSecureLevel == LEVEL_NONE) {  
        cancelPwdLevelNotification();  
    } else if (mLockPatternUtils != null && !mLockPatternUtils.isSecure(UserHandle.myUserId())) {  
        sendPwdLevelNotification(pwdSecureLevel);  
    } else if (TYPE_PASSWORD_PIN.equals(SystemProperties.get("persist.sys.password.type", "-1"))) {  
        if ("4".equals(SystemProperties.get("persist.sys.password.length", "-1"))) {  
            if ((pwdSecureLevel & LEVEL_4_PIN) == 0) {  
                sendPwdLevelNotification(pwdSecureLevel);  
            } else {  
                cancelPwdLevelNotification();  
            }  
        } else if ("6".equals(SystemProperties.get("persist.sys.password.length", "-1"))) {  
            if ((pwdSecureLevel & LEVEL_6_PIN) == 0) {  
                sendPwdLevelNotification(pwdSecureLevel);  
            } else {  
                cancelPwdLevelNotification();  
            }  
        } else {  
            sendPwdLevelNotification(pwdSecureLevel);  
        }  
    } else if (TYPE_PASSWORD_PWD.equals(SystemProperties.get("persist.sys.password.type", "-1"))) {  
        if ((pwdSecureLevel & LEVEL_PASSWORD) == 0) {  
            sendPwdLevelNotification(pwdSecureLevel);  
        } else {  
            cancelPwdLevelNotification();  
        }  
    }  
}

cancelPwdLevelNotification

private void cancelPwdLevelNotification() {
    if (!isPwdLevelNotificationShowing) {
        return;
    }
    NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
    if (notificationManager == null) {
        return;
    }
    isPwdLevelNotificationShowing = false;
    notificationManager.cancel("pwd_level", 99);
}

sendPwdLevelNotification

private void sendPwdLevelNotification(int pwdLevel) {  
    try {  
        if (mContext == null) {  
            return;  
        }  
        NotificationChannel pwChannel = new NotificationChannel("password_level", "password_level", NotificationManager.IMPORTANCE_LOW);  
        NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);  
        if (notificationManager == null) {  
            return;  
        }  
        notificationManager.createNotificationChannel(pwChannel);  
        final Intent deleteIntent = new Intent(*ACTION_DELETE_PWD_LEVEL_NOTIFICATION*);  
        deleteIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);  
        deleteIntent.setPackage(mContext.getPackageName());  
        final Intent contentIntent = Intent.makeRestartActivityTask(new ComponentName("com.android.settings",  
                        "com.android.settings.Settings$SecurityDashboardActivity"));  
        Notification.BigTextStyle bigTextStyle = new Notification.BigTextStyle();  
        bigTextStyle.setBigContentTitle(mContext.getString(  
                com.android.internal.R.string.pwd_level_notification_title))  
                .bigText(getPwdLevelNotificationContent(pwdLevel));  
        Notification.Builder builder = new Notification.Builder(mContext, pwChannel.getId())  
                .setWhen(0)  
                .setOngoing(true)  
                .setTicker(mContext.getString(  
                        com.android.internal.R.string.pwd_level_notification_title))  
                .setSmallIcon(com.android.internal.R.drawable.ic_user_secure)  
                .setDefaults(0)  
                .setContentTitle(mContext.getString(  
                        com.android.internal.R.string.pwd_level_notification_title))  
                .setContentText(getPwdLevelNotificationContent(pwdLevel))  
                .setVisibility(Notification.VISIBILITY_PUBLIC)  
                .setStyle(bigTextStyle)  
                .setColor(mContext.getColor(  
                        com.android.internal.R.color.system_notification_accent_color))  
                .setNotificationIcon(com.android.internal.R.drawable.ic_user_secure)  
                .setDeleteIntent(PendingIntent.getBroadcast(mContext, 0, deleteIntent,  
                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))  
                .setContentIntent(PendingIntent.getActivity(mContext, 0, contentIntent,  
                        PendingIntent.FLAG_IMMUTABLE));  
        notificationManager.notify("pwd_level", 99, builder.build());  
        isPwdLevelNotificationShowing = true;  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
}

安卓较高系统版本,Notification.Builder即使调用了setOngoing(true)方法,也还是可以滑动删除通知。目前是通过调用setDeleteIntent方法,当通知被删除时,发送广播,在广播接收器里面去判断是否要重新发送通知

mContext.registerReceiver(mDeletePwdNotificationReceiver,  
        new IntentFilter(ACTION_DELETE_PWD_LEVEL_NOTIFICATION));  
  
private final BroadcastReceiver mDeletePwdNotificationReceiver = new BroadcastReceiver() {  
    @Override  
    public void onReceive(Context context, Intent intent) {  
        isPwdLevelNotificationShowing = false;  
        sendOrCancelPwdLevelNotification();  
    }  
};  

sendOrCancelPwdLevelNotification

private void sendOrCancelPwdLevelNotification() {
    int pwdSecureLevel = Settings.Secure.getInt(mContentResolver,
            Settings.Secure.PASSWORD_SECURE_LEVEL, LEVEL_NONE);
    if (pwdSecureLevel == LEVEL_NONE) {
        cancelPwdLevelNotification();
    } else if (mLockPatternUtils != null && !mLockPatternUtils.isSecure(UserHandle.myUserId())) {
        sendPwdLevelNotification(pwdSecureLevel);
    } else if (TYPE_PASSWORD_PIN.equals(SystemProperties.get("persist.sys.password.type", "-1"))) {
        if ("4".equals(SystemProperties.get("persist.sys.password.length", "-1"))) {
            if ((pwdSecureLevel & LEVEL_4_PIN) == 0) {
                sendPwdLevelNotification(pwdSecureLevel);
            } else {
                cancelPwdLevelNotification();
            }
        } else if ("6".equals(SystemProperties.get("persist.sys.password.length", "-1"))) {
            if ((pwdSecureLevel & LEVEL_6_PIN) == 0) {
                sendPwdLevelNotification(pwdSecureLevel);
            } else {
                cancelPwdLevelNotification();
            }
        } else {
            sendPwdLevelNotification(pwdSecureLevel);
        }
    } else if (TYPE_PASSWORD_PWD.equals(SystemProperties.get("persist.sys.password.type", "-1"))) {
        if ((pwdSecureLevel & LEVEL_PASSWORD) == 0) {
            sendPwdLevelNotification(pwdSecureLevel);
        } else {
            cancelPwdLevelNotification();
        }
    }
}

2.2 通知文案根据安全等级动态适配

安全等级Settings值为int类型,刚好可以复用来动态适配通知文案

private static final int LEVEL_4_PIN = 0b1;  
private static final int LEVEL_6_PIN = 0b10;  
private static final int LEVEL_PASSWORD = 0b100;  
private static final int LEVEL_4_6_PIN = 0b11;  
private static final int LEVEL_4_PIN_PASSWORD = 0b101;  
private static final int LEVEL_6_PIN_PASSWORD = 0b110;  
private static final int LEVEL_MASK = 0b111;  
  
private String getPwdLevelNotificationContent(int pwdLevel) {  
    if (mContext == null) {  
        return "";  
    }  
    switch (pwdLevel) {  
        case LEVEL_4_PIN -> {  
            return mContext.getString(R.string.pwd_level_notification_content,  
                    mContext.getString(R.string.pwd_level_notification_4_pin));  
        }  
        case LEVEL_4_6_PIN -> {  
            return mContext.getString(R.string.pwd_level_notification_content,  
                    mContext.getString(R.string.pwd_level_notification_4_6_pin));  
        }  
        case LEVEL_4_PIN_PASSWORD -> {  
            return mContext.getString(R.string.pwd_level_notification_content,  
                    mContext.getString(R.string.pwd_level_notification_4_pin_pwd));  
        }  
        case LEVEL_6_PIN -> {  
            return mContext.getString(R.string.pwd_level_notification_content,  
                    mContext.getString(R.string.pwd_level_notification_6_pin));  
        }  
        case LEVEL_6_PIN_PASSWORD -> {  
            return mContext.getString(R.string.pwd_level_notification_content,  
                    mContext.getString(R.string.pwd_level_notification_6_pin_pwd));  
        }  
        case LEVEL_PASSWORD -> {  
            return mContext.getString(R.string.pwd_level_notification_content,  
                    mContext.getString(R.string.pwd_level_notification_pwd));  
        }  
        case LEVEL_MASK -> {  
            return mContext.getString(R.string.pwd_level_notification_content,  
                    mContext.getString(R.string.pwd_level_notification_4_6_pin_pwd));  
        }  
    }  
    return "";  
}

2.3 云端/系统设置Settings修改锁屏密码场景

设置锁屏密码会走到LockSettingsService类里面的setLockCredential方法,该方法内部会调用到setLockCredentialInternal(credential, savedCredential,userId, /* isLockTiedToParent= */ false),如果后者返回false,也就是设置密码失败,前者会直接return false。我们只需要在后者返回true,也就是成功设置锁屏密码之后,去判断是否需要发送或是取消通知就好。

发送或是取消通知的逻辑如下:

安全等级.png


private void sendOrCancelPwdLevelNotification(LockscreenCredential credential) {  
    if (credential == null) {  
        return;  
    }  
    int pwdSecureLevel = Settings.Secure.getInt(mContentResolver,  
            Settings.Secure.PASSWORD_SECURE_LEVEL, LEVEL_NONE);  
    if (pwdSecureLevel == LEVEL_NONE) {  
        cancelPwdLevelNotification();  
    } else if (credential.isNone()) {  
        sendPwdLevelNotification(pwdSecureLevel);  
    } else if (credential.isPin()) {  
        int length = new String(credential.getCredential()).length();  
        if (length == 4) {  
            if ((pwdSecureLevel & LEVEL_4_PIN) == 0) {  
                sendPwdLevelNotification(pwdSecureLevel);  
            } else {  
                cancelPwdLevelNotification();  
            }  
        } else if (length == 6) {  
            if ((pwdSecureLevel & LEVEL_6_PIN) == 0) {  
                sendPwdLevelNotification(pwdSecureLevel);  
            } else {  
                cancelPwdLevelNotification();  
            }  
        } else if (length == 5) {  
            sendPwdLevelNotification(pwdSecureLevel);  
        }  
    } else if (credential.isPassword()) {  
        if ((pwdSecureLevel & LEVEL_PASSWORD) == 0) {  
            sendPwdLevelNotification(pwdSecureLevel);  
        } else {  
            cancelPwdLevelNotification();  
        }  
    }  
}

二、云端强制生效特定密码

这部分参考了系统设置 Settings 设置或是修改锁屏密码逻辑

1. 大概流程

云端强制生成密码.png

2. 具体细节

2.1 新密码字符串解密

MDMExtension拿到客户端MdmAgent传递过来的密码字符串后,要先解密

byte[] newPwd = Base64.getDecoder().decode(newPassword);
String newPwdStr;
newPwdStr = decrypt(key.getBytes(StandardCharsets.UTF_8), newPwd);

private String decrypt(byte[] key, byte[] input) {
    if (input == null || input.length <= 16) {
        return null;
    }
    // 把input分割成IV和密文:
    byte[] iv = new byte[16];
    byte[] data = new byte[input.length - 16];
    System.arraycopy(input, 0, iv, 0, 16);
    System.arraycopy(input, 16, data, 0, data.length);
    // 解密:
    try {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
        IvParameterSpec ivps = new IvParameterSpec(iv);
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivps);
        byte[] result = cipher.doFinal(data);
        return new String(result, StandardCharsets.UTF_8);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

2.2 新密码格式校验

解密得到真正的新密码后,还要进行校验,具体的校验标准参考系统设置Settings(字符值在32-127范围内,PIN码为4-6位纯数字,复杂密码至少包含一位字母,长度为4-17位)。校验完后生成新密码对应的LockscreenCredential对象newCredential。

LockscreenCredential newCredential = createCredential(newPwdStr, mdmInfo);

private LockscreenCredential createCredential(String str, MdmInfo mdmInfo) {
    int len = str.length();
    if (len > 17) {
        mdmInfo.code = CODE_TOO_LONG_PWD;
        mdmInfo.msg = "pwd is too long";
        return null;
    } else if (len < 4) {
        mdmInfo.code = CODE_TOO_SHORT_PWD;
        mdmInfo.msg = "pwd is too short";
        return null;
    }
    boolean hasLetter = false;
    boolean hasSymbol = false;
    for (int i = 0; i < len; i++) {
        char c = str.charAt(i);
        if (c < 32 || c > 127) {
            mdmInfo.code = CODE_HAS_ILLEGAL_CHAR;
            mdmInfo.msg = "pwd has illegal char";
            return null;
        } else if ((c >= 65 && c <= 90) || (c >= 97 && c <= 122)) {
            if (!hasLetter) {
                hasLetter = true;
            }
        } else if (c >= 48 && c <= 57) {
            // [0-9] do nothing
        } else {
            if (!hasSymbol) {
                hasSymbol = true;
            }
        }
    }
    if (hasLetter) {
        return LockscreenCredential.createPassword(str);
    } else if (hasSymbol) {
        mdmInfo.code = CODE_NEITHER_PIN_NOR_PWD;
        mdmInfo.msg = "neither pin nor pwd";
        return null;
    } else if (len > 6){
        mdmInfo.code = CODE_TOO_LONG_PIN;
        mdmInfo.msg = "pin is too long";
        return null;
    }
    return LockscreenCredential.createPin(str);
}

2.3 获取原有密码

接着需要根据当前锁屏密码状态(无密码/复杂密码/PIN),生成旧密码对应的LockscreenCredential对象oldCredential。
当前锁屏密码是PIN或复杂密码时,要先拿到原有密码,才能去生成对应的LockscreenCredential对象oldCredential。MDM项目在TEE区域额外备份了当前锁屏密码,可以直接调用方法获取

private String readOldPw() {
    try {
        synchronized (MdmModifyPasswdControl.class) {
            mIQseeStore = IQseeStore.getService();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    if (mIQseeStore == null) {
        return null;
    }
    try {
        ArrayList<Byte> pdBytes = mIQseeStore.readData(ID_QSEE_STORE_READ_CMD, PW_FILE_NAME);
        if (pdBytes == null || pdBytes.isEmpty()) {
            return null;
        }
        byte[] pdTempBytes = new byte[pdBytes.size()];
        for (int i = 0; i < pdBytes.size(); i++) {
            pdTempBytes[i] = pdBytes.get(i);
        }
        return new String(pdTempBytes);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

那么问题来了,TEE区域里面的锁屏密码,是什么时候备份的呢?
系统设置Settings以及adb指令设置或是修改密码时,都会调用到LockPatternUtils里面的setLockCredential,我们通过MDMExtension去设置或修改密码也一样会调用到。所以,我们在这个地方去进行备份。

 /**
* Save a new lockscreen credential.
*
* <p> This method will fail (returning {  @code  false}) if the previously saved credential
* provided is incorrect, or if the lockscreen verification is still being throttled.
*
*  @param  newCredential The new credential to save
*  @param  savedCredential The current credential
*  @param  userHandle the user whose lockscreen credential is to be changed
*
*  @return  whether this method saved the new password successfully or not. This flow will fail
* and return false if the given credential is wrong.
*  @throws  RuntimeException if password change encountered an unrecoverable error.
*  @throws  UnsupportedOperationException secure lockscreen is not supported on this device.
*  @throws  IllegalArgumentException if new credential is too short.
*/
public boolean setLockCredential(@NonNull LockscreenCredential newCredential,
        @NonNull LockscreenCredential savedCredential, int userHandle) {
    if (!hasSecureLockScreen() && newCredential.getType() != CREDENTIAL_TYPE_NONE) {
        throw new UnsupportedOperationException(
                "This operation requires the lock screen feature.");
    }
    newCredential.checkLength();

    try {
        if (!getLockSettings().setLockCredential(newCredential, savedCredential, userHandle)) {
            return false;
        }
    } catch (RemoteException e) {
        throw new RuntimeException("Unable to save lock password", e);
    }

    try {
        LockPatternUtilsInject.get().savePasswordType(newCredential.getType());
    } catch (Exception exception) {
        exception.printStackTrace();
    }
    if (newCredential.getType() == CREDENTIAL_TYPE_NONE) {
        try {
            getLockSettings().ProcessLeftTimes(true);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    } else {
        try {
            LockPatternUtilsInject.get().savePasswordLength(this, newCredential.getCredential(), userHandle);
        } catch (Exception exception) {
            exception.printStackTrace();
        }

    }
    // 这里写入
    writePwdToTee(newCredential);
    
    return true;
}

writePwdToTee

private void writePwdToTee(LockscreenCredential lockscreenCredential) {
    try {
        if (lockscreenCredential == null) {
            return;
        }
        byte[] credential = lockscreenCredential.getCredential();
        if (credential == null) {
            return;
        }
        ArrayList<Byte> arrayList = new ArrayList(credential.length);
        for (byte mByte : credential) {
            arrayList.add(mByte);
        }
        IQseeStore iQseeStore = IQseeStore.getService();
        if (iQseeStore != null) {
            iQseeStore.writeData(ID_QSEE_STORE_WRITE_CMD, PW_FILE_NAME, arrayList);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

云端要设置的密码和当前原有密码都拿到后,就可以按部就班去设置新密码了

try {
    if (!mLockPatternUtils.setLockCredential(newCredential, oldCredential, UserHandle.myUserId())) {
        mdmInfo.code = CODE_SET_NEW_PDW_FAIL;
        mdmInfo.msg = "check oldCredential false or force set new password failed";
        return mdmInfo;
    }
    mLockPatternUtils.setPassword(TYPE_SCREEN_PASSWORD_FRONT_FOUR, newPwdStr.substring(0, 4));
    mLockSettings.setLong(Settings.Secure.MEIZU_PASSWORD_LENGTH, newPwdStr.length(),
            UserHandle.myUserId());
    int pwdType = mLockPatternUtils.getKeyguardStoredPasswordQuality(UserHandle.myUserId());
    mLockSettings.setLong(LockPatternUtils.PASSWORD_TYPE_KEY + "_backup", pwdType,
            UserHandle.myUserId());
} catch (Exception e) {
    mdmInfo.code = CODE_FAIL_DUE_TO_EXCEPTION;
    mdmInfo.msg = "force set new password failed with exception";
    e.printStackTrace();
}

三、云端密码生效后移除指纹和人脸识别数据

系统设置Settings里面移除锁屏密码时会同步移除指纹和人脸数据,主要是在LockSettingsService里面的setLockCredentialWithSpLocked方法进行处理的,当credential为空,就执行removeBiometricsForUser方法移除生物识别数据。那么,当credential不为空时,我们只要能判断当前是云端修改密码场景,然后执行removeBiometricsForUser方法,就可以在移除指纹和人脸数据了。

 /**
* Changes the user's LSKF by creating an LSKF-based protector that uses the new LSKF (which may
* be empty) and replacing the old LSKF-based protector with it.  The SP itself is not changed.
*
* Also maintains the invariants described in {  @link  SyntheticPasswordManager} by
* setting/clearing the protection (by the SP) on the user's auth-bound Keystore keys when the
* LSKF is added/removed, respectively.  If the new LSKF is nonempty, then the Gatekeeper auth
* token is also refreshed.
*/
@GuardedBy("mSpManager")
private long setLockCredentialWithSpLocked(LockscreenCredential credential,
        SyntheticPassword sp, int userId) {
    Slogf.i(TAG, "Changing lockscreen credential of user %d; newCredentialType=%s\n",
            userId, LockPatternUtils.credentialTypeToString(credential.getType()));
    final int savedCredentialType = getCredentialTypeInternal(userId);
    final long oldProtectorId = getCurrentLskfBasedProtectorId(userId);
    final long newProtectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(),
            credential, sp, userId);
    final Map<Integer, LockscreenCredential> profilePasswords;
    if (!credential.isNone()) {
        // not needed by synchronizeUnifiedWorkChallengeForProfiles()
        profilePasswords = null;

        if (mSpManager.hasSidForUser(userId)) {
            mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId);
        } else {
            mSpManager.newSidForUser(getGateKeeperService(), sp, userId);
            mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId);
            setKeystorePassword(sp.deriveKeyStorePassword(), userId);
        }
        // 这里通过Settings值判断是否为云端修改密码场景
        if (Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.ENFORCE_MODIFY_PASSWORD, 0) == 1) {
            Settings.Secure.putInt(mContext.getContentResolver(), Settings.Secure.ENFORCE_MODIFY_PASSWORD, 0);
            removeBiometricsForUser(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);

        mSpManager.clearSidForUser(userId);
        gateKeeperClearSecureUserId(userId);
        unlockUserKey(userId, sp);
        unlockKeystore(sp.deriveKeyStorePassword(), userId);
        setKeystorePassword(null, userId);
        removeBiometricsForUser(userId);
    }
    setCurrentLskfBasedProtectorId(newProtectorId, userId);
    LockPatternUtils.invalidateCredentialTypeCache();
    synchronizeUnifiedWorkChallengeForProfiles(userId, profilePasswords);

    setUserPasswordMetrics(credential, userId);
    mManagedProfilePasswordCache.removePassword(userId);
    if (savedCredentialType != CREDENTIAL_TYPE_NONE) {
        mSpManager.destroyAllWeakTokenBasedProtectors(userId);
    }

    if (profilePasswords != null) {
        for (Map.Entry<Integer, LockscreenCredential> entry : profilePasswords.entrySet()) {
            entry.getValue().zeroize();
        }
    }
    mSpManager.destroyLskfBasedProtector(oldProtectorId, userId);
    Slogf.i(TAG, "Successfully changed lockscreen credential of user %d", userId);
    return newProtectorId;
}

private void removeBiometricsForUser(int userId) {
    removeAllFingerprintForUser(userId);
    removeAllFaceForUser(userId);
}

四、系统设置Settings 密码页面“移除锁屏密码”选项置灰

云端有配置密码安全等级的话,要置灰“移除锁屏密码”选项,否则不置灰。

当回调onResume时根据云端配置的密码安全等级去决定显示还是隐藏“移除锁屏密码”选项,我们只需要在显示的时候根据密码安全等级去判断是否该置灰选项就好了

private void updatePreferenceScreenLock() {
    PreferenceCategory screenLockCategory = getPreferenceScreen().findPreference(KEY_SCREEN_LOCK_CATEGORY);
    if (!mScreenLockRemoved && mLockPasswordUtils.hasPassword()) {
        mScreenLockPreference.setTitle(R.string.modify_lock_screen_password);
        mScreenLockPreference.setSummary("");
        if (screenLockCategory.findPreference(KEY_REMOVE_SCREEN_LOCK) == null) {
            screenLockCategory.addPreference(mRemoveScreenLockPreference);
        }
        // 更新选项状态
        updateRemoveScreenLockPreference();  
    } else {
        mScreenLockPreference.setTitle(R.string.lock_screen_password);
        mScreenLockPreference.setSummary(R.string.screen_lock_summary);
        screenLockCategory.removePreference(mRemoveScreenLockPreference);
    }

    mScreenLockRemoved = false;
}

private void updateRemoveScreenLockPreference() {
    if (!BuildExt.IS_MDM_CHANNEL || mRemoveScreenLockPreference == null) {
        return;
    }
    ContentResolver contentResolver = getContentResolver();
    if (contentResolver == null) {
        return;
    }
    final int secureLevel = Settings.Secure.getInt(contentResolver,
            Settings.Secure.PASSWORD_SECURE_LEVEL, PASSWORD_SECURE_LEVEL_NONE);
    if (secureLevel != PASSWORD_SECURE_LEVEL_NONE) {
        mRemoveScreenLockPreference.setEnabled(false);
    } else {
        mRemoveScreenLockPreference.setEnabled(true);
    }
}

当云端设置或取消密码安全等级时,用户刚好停留在该页面的话,我们还需要刷新UI,防止用户点击选项。为此,我们需要在onCreate方法里面初始化mSecureLevelObserver,在onStart方法里面registerSecureLevelObserver,在onStop方法里面unregisterSecureLevelObserver

private void initSecureLevelObserver() {
    if (!BuildExt.IS_MDM_CHANNEL) {
        return;
    }
    mSecureLevelObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            updateRemoveScreenLockPreference();
        }
    };
}

private void registerSecureLevelObserver() {
    if (!BuildExt.IS_MDM_CHANNEL || mSecureLevelObserver == null) {
        return;
    }
    ContentResolver contentResolver = getContentResolver();
    if (contentResolver == null) {
        return;
    }
    contentResolver.registerContentObserver(Settings.Secure.getUriFor(Settings.Secure.PASSWORD_SECURE_LEVEL),
            false, mSecureLevelObserver);
}

private void unregisterSecureLevelObserver() {
    if (!BuildExt.IS_MDM_CHANNEL || mSecureLevelObserver == null) {
        return;
    }
    ContentResolver contentResolver = getContentResolver();
    if (contentResolver == null) {
        return;
    }
    contentResolver.unregisterContentObserver(mSecureLevelObserver);
}

五、云端修改密码后手机息屏

声明"android.permission.DEVICE_POWER"权限,同时在mdmextention里面调用PowerManager的goToSleep方法

AndroidManifest.xml
 <uses-permission android:name="android.permission.DEVICE_POWER" tools:ignore="ProtectedPermissions" />

ModifyPasswdControl.kt
try {
    val mPowerManager: PowerManager? = mContext.getSystemService(Context.POWER_SERVICE) as PowerManager?
    mPowerManager?.goToSleep(SystemClock.uptimeMillis())
} catch (e: Exception) {
    e.printStackTrace()
}

六、设置过锁屏密码的非MDM定制机固件,不清除数据升级为MDM定制机固件后,如何拿到原有锁屏密码?

当在锁屏上面输入密码解锁时,会执行LockPatternUtils里面的checkCredential方法。当在系统设置Settings里面修改密码,进入指纹识别或是人脸识别相关页面时,会要求输入锁屏密码,此时也会执行改方法。所以,当密码校验成功时,我们会通过判断新增的Settings值HAS_PWD_BACKUP,把锁屏密码写入TEE区域,等云端要修改密码时,就可以直接拿来用了。

 /**
* Check to see if a credential matches the saved one.
*
*  @param  credential The credential to check.
*  @param  userId The user whose credential is being checked
*  @param  progressCallback callback to deliver early signal that the credential matches
*  @return  {  @code true } if credential matches, {  @code  false} otherwise
*  @throws  RequestThrottledException if credential verification is being throttled due to
*         to many incorrect attempts.
*  @throws  IllegalStateException if called on the main thread.
*/
public boolean checkCredential(@NonNull LockscreenCredential credential, int userId,
        @Nullable CheckCredentialProgressCallback progressCallback)
        throws RequestThrottledException {
    throwIfCalledOnMainThread();
    try {
        VerifyCredentialResponse response = getLockSettings().checkCredential(
                credential, userId, wrapCallback(progressCallback));
        if (response == null) {
            return false;
        } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
            
            LockPatternUtilsInject.get().savePasswordIfNeed(this, credential, userId);
            
            
            if (mContentResolver != null && credential.getCredential() != null) {
                // 这里判断Settings值
                int hasPwdBackup = Settings.Secure.getInt(mContentResolver, Settings.Secure.HAS_PWD_BACKUP, 0);
                // 如果之前没有备份过,就执行备份
                if (hasPwdBackup == 0) {
                    writePwdToTee(credential);
                    Settings.Secure.putInt(mContentResolver, Settings.Secure.HAS_PWD_BACKUP, 1);
                }
            }
            
            return true;
        } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
            throw new RequestThrottledException(response.getTimeout());
        } else {
            return false;
        }
    } catch (RemoteException re) {
        Log.e(TAG, "failed to check credential", re);
        return false;
    }
}