blob: 992985528fca5f435b69e1bb6983866a59b7e5bd [file] [log] [blame]
/*
* 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.
*/
package android.app.admin;
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH;
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_LOW;
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_MEDIUM;
import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_NONE;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.admin.DevicePolicyManager.PasswordComplexity;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.internal.widget.LockPatternUtils.CredentialType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A class that represents the metrics of a credential that are used to decide whether or not a
* credential meets the requirements. If the credential is a pattern, only quality matters.
*
* {@hide}
*/
public class PasswordMetrics implements Parcelable {
// Maximum allowed number of repeated or ordered characters in a sequence before we'll
// consider it a complex PIN/password.
public static final int MAX_ALLOWED_SEQUENCE = 3;
public int quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
public int length = 0;
public int letters = 0;
public int upperCase = 0;
public int lowerCase = 0;
public int numeric = 0;
public int symbols = 0;
public int nonLetter = 0;
public PasswordMetrics() {}
public PasswordMetrics(int quality) {
this.quality = quality;
}
public PasswordMetrics(int quality, int length) {
this.quality = quality;
this.length = length;
}
public PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase,
int numeric, int symbols, int nonLetter) {
this(quality, length);
this.letters = letters;
this.upperCase = upperCase;
this.lowerCase = lowerCase;
this.numeric = numeric;
this.symbols = symbols;
this.nonLetter = nonLetter;
}
private PasswordMetrics(Parcel in) {
quality = in.readInt();
length = in.readInt();
letters = in.readInt();
upperCase = in.readInt();
lowerCase = in.readInt();
numeric = in.readInt();
symbols = in.readInt();
nonLetter = in.readInt();
}
/** Returns the min quality allowed by {@code complexityLevel}. */
public static int complexityLevelToMinQuality(@PasswordComplexity int complexityLevel) {
// this would be the quality of the first metrics since mMetrics is sorted in ascending
// order of quality
return PasswordComplexityBucket
.complexityLevelToBucket(complexityLevel).mMetrics[0].quality;
}
/**
* Returns a merged minimum {@link PasswordMetrics} requirements that a new password must meet
* to fulfil {@code requestedQuality}, {@code requiresNumeric} and {@code
* requiresLettersOrSymbols}, which are derived from {@link DevicePolicyManager} requirements,
* and {@code complexityLevel}.
*
* <p>Note that we are taking {@code userEnteredPasswordQuality} into account because there are
* more than one set of metrics to meet the minimum complexity requirement and inspecting what
* the user has entered can help determine whether the alphabetic or alphanumeric set of metrics
* should be used. For example, suppose minimum complexity requires either ALPHABETIC(8+), or
* ALPHANUMERIC(6+). If the user has entered "a", the length requirement displayed on the UI
* would be 8. Then the user appends "1" to make it "a1". We now know the user is entering
* an alphanumeric password so we would update the min complexity required min length to 6.
*/
public static PasswordMetrics getMinimumMetrics(@PasswordComplexity int complexityLevel,
int userEnteredPasswordQuality, int requestedQuality, boolean requiresNumeric,
boolean requiresLettersOrSymbols) {
int targetQuality = Math.max(
userEnteredPasswordQuality,
getActualRequiredQuality(
requestedQuality, requiresNumeric, requiresLettersOrSymbols));
return getTargetQualityMetrics(complexityLevel, targetQuality);
}
/**
* Returns the {@link PasswordMetrics} at {@code complexityLevel} which the metrics quality
* is the same as {@code targetQuality}.
*
* <p>If {@code complexityLevel} does not allow {@code targetQuality}, returns the metrics
* with the min quality at {@code complexityLevel}.
*/
// TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private
@VisibleForTesting
public static PasswordMetrics getTargetQualityMetrics(
@PasswordComplexity int complexityLevel, int targetQuality) {
PasswordComplexityBucket targetBucket =
PasswordComplexityBucket.complexityLevelToBucket(complexityLevel);
for (PasswordMetrics metrics : targetBucket.mMetrics) {
if (targetQuality == metrics.quality) {
return metrics;
}
}
// none of the metrics at complexityLevel has targetQuality, return metrics with min quality
// see test case testGetMinimumMetrics_actualRequiredQualityStricter for an example, where
// min complexity allows at least NUMERIC_COMPLEX, user has not entered anything yet, and
// requested quality is NUMERIC
return targetBucket.mMetrics[0];
}
/**
* Finds out the actual quality requirement based on whether quality is {@link
* DevicePolicyManager#PASSWORD_QUALITY_COMPLEX} and whether digits, letters or symbols are
* required.
*/
@VisibleForTesting
// TODO(bernardchau): update tests to test getMinimumMetrics and change this to be private
public static int getActualRequiredQuality(
int requestedQuality, boolean requiresNumeric, boolean requiresLettersOrSymbols) {
if (requestedQuality != PASSWORD_QUALITY_COMPLEX) {
return requestedQuality;
}
// find out actual password quality from complex requirements
if (requiresNumeric && requiresLettersOrSymbols) {
return PASSWORD_QUALITY_ALPHANUMERIC;
}
if (requiresLettersOrSymbols) {
return PASSWORD_QUALITY_ALPHABETIC;
}
if (requiresNumeric) {
// cannot specify numeric complex using complex quality so this must be numeric
return PASSWORD_QUALITY_NUMERIC;
}
// reaching here means dpm sets quality to complex without specifying any requirements
return PASSWORD_QUALITY_UNSPECIFIED;
}
/**
* Returns {@code complexityLevel} or {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE}
* if {@code complexityLevel} is not valid.
*/
@PasswordComplexity
public static int sanitizeComplexityLevel(@PasswordComplexity int complexityLevel) {
return PasswordComplexityBucket.complexityLevelToBucket(complexityLevel).mComplexityLevel;
}
public boolean isDefault() {
return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
&& length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0
&& numeric == 0 && symbols == 0 && nonLetter == 0;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(quality);
dest.writeInt(length);
dest.writeInt(letters);
dest.writeInt(upperCase);
dest.writeInt(lowerCase);
dest.writeInt(numeric);
dest.writeInt(symbols);
dest.writeInt(nonLetter);
}
public static final @android.annotation.NonNull Parcelable.Creator<PasswordMetrics> CREATOR
= new Parcelable.Creator<PasswordMetrics>() {
public PasswordMetrics createFromParcel(Parcel in) {
return new PasswordMetrics(in);
}
public PasswordMetrics[] newArray(int size) {
return new PasswordMetrics[size];
}
};
/**
* Returnsthe {@code PasswordMetrics} for a given credential.
*
* If the credential is a pin or a password, equivalent to {@link #computeForPassword(byte[])}.
* {@code credential} cannot be null when {@code type} is
* {@link com.android.internal.widget.LockPatternUtils#CREDENTIAL_TYPE_PASSWORD}.
*/
public static PasswordMetrics computeForCredential(
@CredentialType int type, byte[] credential) {
if (type == CREDENTIAL_TYPE_PASSWORD) {
Preconditions.checkNotNull(credential, "credential cannot be null");
return PasswordMetrics.computeForPassword(credential);
} else if (type == CREDENTIAL_TYPE_PATTERN) {
return new PasswordMetrics(PASSWORD_QUALITY_SOMETHING);
} else /* if (type == CREDENTIAL_TYPE_NONE) */ {
return new PasswordMetrics(PASSWORD_QUALITY_UNSPECIFIED);
}
}
/**
* Returns the {@code PasswordMetrics} for a given password
*/
public static PasswordMetrics computeForPassword(@NonNull byte[] password) {
// Analyse the characters used
int letters = 0;
int upperCase = 0;
int lowerCase = 0;
int numeric = 0;
int symbols = 0;
int nonLetter = 0;
final int length = password.length;
for (byte b : password) {
switch (categoryChar((char) b)) {
case CHAR_LOWER_CASE:
letters++;
lowerCase++;
break;
case CHAR_UPPER_CASE:
letters++;
upperCase++;
break;
case CHAR_DIGIT:
numeric++;
nonLetter++;
break;
case CHAR_SYMBOL:
symbols++;
nonLetter++;
break;
}
}
// Determine the quality of the password
final boolean hasNumeric = numeric > 0;
final boolean hasNonNumeric = (letters + symbols) > 0;
final int quality;
if (hasNonNumeric && hasNumeric) {
quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
} else if (hasNonNumeric) {
quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
} else if (hasNumeric) {
quality = maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE
? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
: DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
} else {
quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
}
return new PasswordMetrics(
quality, length, letters, upperCase, lowerCase, numeric, symbols, nonLetter);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof PasswordMetrics)) {
return false;
}
PasswordMetrics o = (PasswordMetrics) other;
return this.quality == o.quality
&& this.length == o.length
&& this.letters == o.letters
&& this.upperCase == o.upperCase
&& this.lowerCase == o.lowerCase
&& this.numeric == o.numeric
&& this.symbols == o.symbols
&& this.nonLetter == o.nonLetter;
}
private boolean satisfiesBucket(PasswordMetrics... bucket) {
for (PasswordMetrics metrics : bucket) {
if (this.quality == metrics.quality) {
return this.length >= metrics.length;
}
}
return false;
}
/**
* Returns the maximum length of a sequential characters. A sequence is defined as
* monotonically increasing characters with a constant interval or the same character repeated.
*
* For example:
* maxLengthSequence("1234") == 4
* maxLengthSequence("13579") == 5
* maxLengthSequence("1234abc") == 4
* maxLengthSequence("aabc") == 3
* maxLengthSequence("qwertyuio") == 1
* maxLengthSequence("@ABC") == 3
* maxLengthSequence(";;;;") == 4 (anything that repeats)
* maxLengthSequence(":;<=>") == 1 (ordered, but not composed of alphas or digits)
*
* @param bytes the pass
* @return the number of sequential letters or digits
*/
public static int maxLengthSequence(@NonNull byte[] bytes) {
if (bytes.length == 0) return 0;
char previousChar = (char) bytes[0];
@CharacterCatagory int category = categoryChar(previousChar); //current sequence category
int diff = 0; //difference between two consecutive characters
boolean hasDiff = false; //if we are currently targeting a sequence
int maxLength = 0; //maximum length of a sequence already found
int startSequence = 0; //where the current sequence started
for (int current = 1; current < bytes.length; current++) {
char currentChar = (char) bytes[current];
@CharacterCatagory int categoryCurrent = categoryChar(currentChar);
int currentDiff = (int) currentChar - (int) previousChar;
if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) {
maxLength = Math.max(maxLength, current - startSequence);
startSequence = current;
hasDiff = false;
category = categoryCurrent;
}
else {
if(hasDiff && currentDiff != diff) {
maxLength = Math.max(maxLength, current - startSequence);
startSequence = current - 1;
}
diff = currentDiff;
hasDiff = true;
}
previousChar = currentChar;
}
maxLength = Math.max(maxLength, bytes.length - startSequence);
return maxLength;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "CHAR_" }, value = {
CHAR_UPPER_CASE,
CHAR_LOWER_CASE,
CHAR_DIGIT,
CHAR_SYMBOL
})
private @interface CharacterCatagory {}
private static final int CHAR_LOWER_CASE = 0;
private static final int CHAR_UPPER_CASE = 1;
private static final int CHAR_DIGIT = 2;
private static final int CHAR_SYMBOL = 3;
@CharacterCatagory
private static int categoryChar(char c) {
if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE;
if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE;
if ('0' <= c && c <= '9') return CHAR_DIGIT;
return CHAR_SYMBOL;
}
private static int maxDiffCategory(@CharacterCatagory int category) {
switch (category) {
case CHAR_LOWER_CASE:
case CHAR_UPPER_CASE:
return 1;
case CHAR_DIGIT:
return 10;
default:
return 0;
}
}
/** Determines the {@link PasswordComplexity} of this {@link PasswordMetrics}. */
@PasswordComplexity
public int determineComplexity() {
for (PasswordComplexityBucket bucket : PasswordComplexityBucket.BUCKETS) {
if (satisfiesBucket(bucket.mMetrics)) {
return bucket.mComplexityLevel;
}
}
return PASSWORD_COMPLEXITY_NONE;
}
/**
* Requirements in terms of {@link PasswordMetrics} for each {@link PasswordComplexity}.
*/
private static class PasswordComplexityBucket {
/**
* Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_HIGH} in terms of
* {@link PasswordMetrics}.
*/
private static final PasswordComplexityBucket HIGH =
new PasswordComplexityBucket(
PASSWORD_COMPLEXITY_HIGH,
new PasswordMetrics(
DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
8),
new PasswordMetrics(
DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 6),
new PasswordMetrics(
DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */
6));
/**
* Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_MEDIUM} in terms of
* {@link PasswordMetrics}.
*/
private static final PasswordComplexityBucket MEDIUM =
new PasswordComplexityBucket(
PASSWORD_COMPLEXITY_MEDIUM,
new PasswordMetrics(
DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX, /* length= */
4),
new PasswordMetrics(
DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC, /* length= */ 4),
new PasswordMetrics(
DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, /* length= */
4));
/**
* Definition of {@link DevicePolicyManager#PASSWORD_COMPLEXITY_LOW} in terms of
* {@link PasswordMetrics}.
*/
private static final PasswordComplexityBucket LOW =
new PasswordComplexityBucket(
PASSWORD_COMPLEXITY_LOW,
new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING),
new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC),
new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX),
new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC),
new PasswordMetrics(DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC));
/**
* A special bucket to represent {@link DevicePolicyManager#PASSWORD_COMPLEXITY_NONE}.
*/
private static final PasswordComplexityBucket NONE =
new PasswordComplexityBucket(PASSWORD_COMPLEXITY_NONE, new PasswordMetrics());
/** Array containing all buckets from high to low. */
private static final PasswordComplexityBucket[] BUCKETS =
new PasswordComplexityBucket[] {HIGH, MEDIUM, LOW};
@PasswordComplexity
private final int mComplexityLevel;
private final PasswordMetrics[] mMetrics;
/**
* @param metricsArray must be sorted in ascending order of {@link #quality}.
*/
private PasswordComplexityBucket(@PasswordComplexity int complexityLevel,
PasswordMetrics... metricsArray) {
int previousQuality = PASSWORD_QUALITY_UNSPECIFIED;
for (PasswordMetrics metrics : metricsArray) {
if (metrics.quality < previousQuality) {
throw new IllegalArgumentException("metricsArray must be sorted in ascending"
+ " order of quality");
}
previousQuality = metrics.quality;
}
this.mMetrics = metricsArray;
this.mComplexityLevel = complexityLevel;
}
/** Returns the bucket that {@code complexityLevel} represents. */
private static PasswordComplexityBucket complexityLevelToBucket(
@PasswordComplexity int complexityLevel) {
for (PasswordComplexityBucket bucket : BUCKETS) {
if (bucket.mComplexityLevel == complexityLevel) {
return bucket;
}
}
return NONE;
}
}
}