Home | History | Annotate | Download | only in admin
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.app.admin;
     18 
     19 import android.annotation.IntDef;
     20 import android.annotation.NonNull;
     21 import android.os.Parcel;
     22 import android.os.Parcelable;
     23 
     24 import java.lang.annotation.Retention;
     25 import java.lang.annotation.RetentionPolicy;
     26 
     27 /**
     28  * A class that represents the metrics of a password that are used to decide whether or not a
     29  * password meets the requirements.
     30  *
     31  * {@hide}
     32  */
     33 public class PasswordMetrics implements Parcelable {
     34     // Maximum allowed number of repeated or ordered characters in a sequence before we'll
     35     // consider it a complex PIN/password.
     36     public static final int MAX_ALLOWED_SEQUENCE = 3;
     37 
     38     public int quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
     39     public int length = 0;
     40     public int letters = 0;
     41     public int upperCase = 0;
     42     public int lowerCase = 0;
     43     public int numeric = 0;
     44     public int symbols = 0;
     45     public int nonLetter = 0;
     46 
     47     public PasswordMetrics() {}
     48 
     49     public PasswordMetrics(int quality, int length) {
     50         this.quality = quality;
     51         this.length = length;
     52     }
     53 
     54     public PasswordMetrics(int quality, int length, int letters, int upperCase, int lowerCase,
     55             int numeric, int symbols, int nonLetter) {
     56         this(quality, length);
     57         this.letters = letters;
     58         this.upperCase = upperCase;
     59         this.lowerCase = lowerCase;
     60         this.numeric = numeric;
     61         this.symbols = symbols;
     62         this.nonLetter = nonLetter;
     63     }
     64 
     65     private PasswordMetrics(Parcel in) {
     66         quality = in.readInt();
     67         length = in.readInt();
     68         letters = in.readInt();
     69         upperCase = in.readInt();
     70         lowerCase = in.readInt();
     71         numeric = in.readInt();
     72         symbols = in.readInt();
     73         nonLetter = in.readInt();
     74     }
     75 
     76     public boolean isDefault() {
     77         return quality == DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
     78                 && length == 0 && letters == 0 && upperCase == 0 && lowerCase == 0
     79                 && numeric == 0 && symbols == 0 && nonLetter == 0;
     80     }
     81 
     82     @Override
     83     public int describeContents() {
     84         return 0;
     85     }
     86 
     87     @Override
     88     public void writeToParcel(Parcel dest, int flags) {
     89         dest.writeInt(quality);
     90         dest.writeInt(length);
     91         dest.writeInt(letters);
     92         dest.writeInt(upperCase);
     93         dest.writeInt(lowerCase);
     94         dest.writeInt(numeric);
     95         dest.writeInt(symbols);
     96         dest.writeInt(nonLetter);
     97     }
     98 
     99     public static final Parcelable.Creator<PasswordMetrics> CREATOR
    100             = new Parcelable.Creator<PasswordMetrics>() {
    101         public PasswordMetrics createFromParcel(Parcel in) {
    102             return new PasswordMetrics(in);
    103         }
    104 
    105         public PasswordMetrics[] newArray(int size) {
    106             return new PasswordMetrics[size];
    107         }
    108     };
    109 
    110     public static PasswordMetrics computeForPassword(@NonNull String password) {
    111         // Analyse the characters used
    112         int letters = 0;
    113         int upperCase = 0;
    114         int lowerCase = 0;
    115         int numeric = 0;
    116         int symbols = 0;
    117         int nonLetter = 0;
    118         final int length = password.length();
    119         for (int i = 0; i < length; i++) {
    120             switch (categoryChar(password.charAt(i))) {
    121                 case CHAR_LOWER_CASE:
    122                     letters++;
    123                     lowerCase++;
    124                     break;
    125                 case CHAR_UPPER_CASE:
    126                     letters++;
    127                     upperCase++;
    128                     break;
    129                 case CHAR_DIGIT:
    130                     numeric++;
    131                     nonLetter++;
    132                     break;
    133                 case CHAR_SYMBOL:
    134                     symbols++;
    135                     nonLetter++;
    136                     break;
    137             }
    138         }
    139 
    140         // Determine the quality of the password
    141         final boolean hasNumeric = numeric > 0;
    142         final boolean hasNonNumeric = (letters + symbols) > 0;
    143         final int quality;
    144         if (hasNonNumeric && hasNumeric) {
    145             quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC;
    146         } else if (hasNonNumeric) {
    147             quality = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
    148         } else if (hasNumeric) {
    149             quality = maxLengthSequence(password) > MAX_ALLOWED_SEQUENCE
    150                     ? DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
    151                     : DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX;
    152         } else {
    153             quality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
    154         }
    155 
    156         return new PasswordMetrics(
    157                 quality, length, letters, upperCase, lowerCase, numeric, symbols, nonLetter);
    158     }
    159 
    160     @Override
    161     public boolean equals(Object other) {
    162         if (!(other instanceof PasswordMetrics)) {
    163             return false;
    164         }
    165         PasswordMetrics o = (PasswordMetrics) other;
    166         return this.quality == o.quality
    167                 && this.length == o.length
    168                 && this.letters == o.letters
    169                 && this.upperCase == o.upperCase
    170                 && this.lowerCase == o.lowerCase
    171                 && this.numeric == o.numeric
    172                 && this.symbols == o.symbols
    173                 && this.nonLetter == o.nonLetter;
    174     }
    175 
    176     /*
    177      * Returns the maximum length of a sequential characters. A sequence is defined as
    178      * monotonically increasing characters with a constant interval or the same character repeated.
    179      *
    180      * For example:
    181      * maxLengthSequence("1234") == 4
    182      * maxLengthSequence("13579") == 5
    183      * maxLengthSequence("1234abc") == 4
    184      * maxLengthSequence("aabc") == 3
    185      * maxLengthSequence("qwertyuio") == 1
    186      * maxLengthSequence("@ABC") == 3
    187      * maxLengthSequence(";;;;") == 4 (anything that repeats)
    188      * maxLengthSequence(":;<=>") == 1  (ordered, but not composed of alphas or digits)
    189      *
    190      * @param string the pass
    191      * @return the number of sequential letters or digits
    192      */
    193     public static int maxLengthSequence(@NonNull String string) {
    194         if (string.length() == 0) return 0;
    195         char previousChar = string.charAt(0);
    196         @CharacterCatagory int category = categoryChar(previousChar); //current sequence category
    197         int diff = 0; //difference between two consecutive characters
    198         boolean hasDiff = false; //if we are currently targeting a sequence
    199         int maxLength = 0; //maximum length of a sequence already found
    200         int startSequence = 0; //where the current sequence started
    201         for (int current = 1; current < string.length(); current++) {
    202             char currentChar = string.charAt(current);
    203             @CharacterCatagory int categoryCurrent = categoryChar(currentChar);
    204             int currentDiff = (int) currentChar - (int) previousChar;
    205             if (categoryCurrent != category || Math.abs(currentDiff) > maxDiffCategory(category)) {
    206                 maxLength = Math.max(maxLength, current - startSequence);
    207                 startSequence = current;
    208                 hasDiff = false;
    209                 category = categoryCurrent;
    210             }
    211             else {
    212                 if(hasDiff && currentDiff != diff) {
    213                     maxLength = Math.max(maxLength, current - startSequence);
    214                     startSequence = current - 1;
    215                 }
    216                 diff = currentDiff;
    217                 hasDiff = true;
    218             }
    219             previousChar = currentChar;
    220         }
    221         maxLength = Math.max(maxLength, string.length() - startSequence);
    222         return maxLength;
    223     }
    224 
    225     @Retention(RetentionPolicy.SOURCE)
    226     @IntDef(prefix = { "CHAR_" }, value = {
    227             CHAR_UPPER_CASE,
    228             CHAR_LOWER_CASE,
    229             CHAR_DIGIT,
    230             CHAR_SYMBOL
    231     })
    232     private @interface CharacterCatagory {}
    233     private static final int CHAR_LOWER_CASE = 0;
    234     private static final int CHAR_UPPER_CASE = 1;
    235     private static final int CHAR_DIGIT = 2;
    236     private static final int CHAR_SYMBOL = 3;
    237 
    238     @CharacterCatagory
    239     private static int categoryChar(char c) {
    240         if ('a' <= c && c <= 'z') return CHAR_LOWER_CASE;
    241         if ('A' <= c && c <= 'Z') return CHAR_UPPER_CASE;
    242         if ('0' <= c && c <= '9') return CHAR_DIGIT;
    243         return CHAR_SYMBOL;
    244     }
    245 
    246     private static int maxDiffCategory(@CharacterCatagory int category) {
    247         switch (category) {
    248             case CHAR_LOWER_CASE:
    249             case CHAR_UPPER_CASE:
    250                 return 1;
    251             case CHAR_DIGIT:
    252                 return 10;
    253             default:
    254                 return 0;
    255         }
    256     }
    257 }
    258