• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 com.android.internal.widget;
18 
19 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
20 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
21 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN;
22 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
23 import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.os.Parcel;
28 import android.os.Parcelable;
29 import android.text.TextUtils;
30 
31 import com.android.internal.util.ArrayUtils;
32 import com.android.internal.util.Preconditions;
33 
34 import libcore.util.HexEncoding;
35 
36 import java.security.MessageDigest;
37 import java.security.NoSuchAlgorithmException;
38 import java.util.Arrays;
39 import java.util.List;
40 import java.util.Objects;
41 
42 /**
43  * A class representing a lockscreen credential, also called a Lock Screen Knowledge Factor (LSKF).
44  * It can be a PIN, pattern, password, or none (a.k.a. empty).
45  *
46  * <p> As required by some security certification, the framework tries its best to
47  * remove copies of the lockscreen credential bytes from memory. In this regard, this class
48  * abuses the {@link AutoCloseable} interface for sanitizing memory. This
49  * presents a nice syntax to auto-zeroize memory with the try-with-resource statement:
50  * <pre>
51  * try {LockscreenCredential credential = LockscreenCredential.createPassword(...) {
52  *     // Process the credential in some way
53  * }
54  * </pre>
55  * With this construct, we can guarantee that there will be no copies of the credential left in
56  * memory when the object goes out of scope. This should help mitigate certain class of attacks
57  * where the attacker gains read-only access to full device memory (cold boot attack, unsecured
58  * software/hardware memory dumping interfaces such as JTAG).
59  */
60 public class LockscreenCredential implements Parcelable, AutoCloseable {
61 
62     private final int mType;
63     // Stores raw credential bytes, or null if credential has been zeroized. A none credential
64     // is represented as a byte array of length 0.
65     private byte[] mCredential;
66 
67     // This indicates that the credential used characters outside ASCII 32–127.
68     //
69     // Such credentials were never intended to be allowed.  However, Android 10–14 had a bug where
70     // conversion from the chars the user entered to the credential bytes used a simple truncation.
71     // Thus, any 'char' whose remainder mod 256 was in the range 32–127 was accepted and was
72     // equivalent to some ASCII character.  For example, ™, which is U+2122, was truncated to ASCII
73     // 0x22 which is the double-quote character ".
74     //
75     // We have to continue to allow a LockscreenCredential to be constructed with this bug, so that
76     // existing devices can be unlocked if their password used this bug.  However, we prevent new
77     // passwords that use this bug from being set.  The boolean below keeps track of the information
78     // needed to do that check, since the conversion to mCredential may have been lossy.
79     private final boolean mHasInvalidChars;
80 
81     /**
82      * Private constructor, use static builder methods instead.
83      *
84      * <p> Builder methods should create a private copy of the credential bytes using a non-movable
85      * array and pass it in here.  LockscreenCredential will only store the reference internally
86      * without copying. This is to minimize the number of extra copies introduced.
87      */
LockscreenCredential(int type, byte[] credential, boolean hasInvalidChars)88     private LockscreenCredential(int type, byte[] credential, boolean hasInvalidChars) {
89         Objects.requireNonNull(credential);
90         if (type == CREDENTIAL_TYPE_NONE) {
91             Preconditions.checkArgument(credential.length == 0);
92         } else {
93             // Do not allow constructing a CREDENTIAL_TYPE_PASSWORD_OR_PIN object.
94             Preconditions.checkArgument(type == CREDENTIAL_TYPE_PIN
95                     || type == CREDENTIAL_TYPE_PASSWORD
96                     || type == CREDENTIAL_TYPE_PATTERN);
97             // Do not validate credential.length yet.  All non-none credentials have a minimum
98             // length requirement; however, one of the uses of LockscreenCredential is to represent
99             // a proposed credential that might be too short.  For example, a LockscreenCredential
100             // with type CREDENTIAL_TYPE_PIN and length 0 represents an attempt to set an empty PIN.
101             // This differs from an actual attempt to set a none credential.  We have to allow the
102             // LockscreenCredential object to be constructed so that the validation logic can run,
103             // even though the validation logic will ultimately reject the credential as too short.
104         }
105         mType = type;
106         mCredential = credential;
107         mHasInvalidChars = hasInvalidChars;
108     }
109 
LockscreenCredential(int type, CharSequence credential)110     private LockscreenCredential(int type, CharSequence credential) {
111         this(type, charsToBytesTruncating(credential), hasInvalidChars(credential));
112     }
113 
114     /**
115      * Creates a LockscreenCredential object representing a none credential.
116      */
createNone()117     public static LockscreenCredential createNone() {
118         return new LockscreenCredential(CREDENTIAL_TYPE_NONE, new byte[0], false);
119     }
120 
121     /**
122      * Creates a LockscreenCredential object representing the given pattern.
123      */
createPattern(@onNull List<LockPatternView.Cell> pattern)124     public static LockscreenCredential createPattern(@NonNull List<LockPatternView.Cell> pattern) {
125         return new LockscreenCredential(CREDENTIAL_TYPE_PATTERN,
126                 LockPatternUtils.patternToByteArray(pattern), /* hasInvalidChars= */ false);
127     }
128 
129     /**
130      * Creates a LockscreenCredential object representing the given alphabetic password.
131      */
createPassword(@onNull CharSequence password)132     public static LockscreenCredential createPassword(@NonNull CharSequence password) {
133         return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD, password);
134     }
135 
136     /**
137      * Creates a LockscreenCredential object representing the system-generated, system-managed
138      * password for a profile with unified challenge. This credential has type {@code
139      * CREDENTIAL_TYPE_PASSWORD} for now. TODO: consider add a new credential type for this. This
140      * can then supersede the isLockTiedToParent argument in various places in LSS.
141      */
createUnifiedProfilePassword(@onNull byte[] password)142     public static LockscreenCredential createUnifiedProfilePassword(@NonNull byte[] password) {
143         return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD,
144                 copyOfArrayNonMovable(password), /* hasInvalidChars= */ false);
145     }
146 
147     /**
148      * Creates a LockscreenCredential object representing the given numeric PIN.
149      */
createPin(@onNull CharSequence pin)150     public static LockscreenCredential createPin(@NonNull CharSequence pin) {
151         return new LockscreenCredential(CREDENTIAL_TYPE_PIN, pin);
152     }
153 
154     /**
155      * Creates a LockscreenCredential object representing the given alphabetic password.
156      * If the supplied password is empty, create a none credential object.
157      */
createPasswordOrNone(@ullable CharSequence password)158     public static LockscreenCredential createPasswordOrNone(@Nullable CharSequence password) {
159         if (TextUtils.isEmpty(password)) {
160             return createNone();
161         } else {
162             return createPassword(password);
163         }
164     }
165 
166     /**
167      * Creates a LockscreenCredential object representing the given numeric PIN.
168      * If the supplied password is empty, create a none credential object.
169      */
createPinOrNone(@ullable CharSequence pin)170     public static LockscreenCredential createPinOrNone(@Nullable CharSequence pin) {
171         if (TextUtils.isEmpty(pin)) {
172             return createNone();
173         } else {
174             return createPin(pin);
175         }
176     }
177 
ensureNotZeroized()178     private void ensureNotZeroized() {
179         Preconditions.checkState(mCredential != null, "Credential is already zeroized");
180     }
181     /**
182      * Returns the type of this credential. Can be one of {@link #CREDENTIAL_TYPE_NONE},
183      * {@link #CREDENTIAL_TYPE_PATTERN}, {@link #CREDENTIAL_TYPE_PIN} or
184      * {@link #CREDENTIAL_TYPE_PASSWORD}.
185      */
getType()186     public int getType() {
187         ensureNotZeroized();
188         return mType;
189     }
190 
191     /**
192      * Returns the credential bytes. This is a direct reference of the internal field so
193      * callers should not modify it.
194      *
195      */
getCredential()196     public byte[] getCredential() {
197         ensureNotZeroized();
198         return mCredential;
199     }
200 
201     /** Returns whether this is a none credential */
isNone()202     public boolean isNone() {
203         ensureNotZeroized();
204         return mType == CREDENTIAL_TYPE_NONE;
205     }
206 
207     /** Returns whether this is a pattern credential */
isPattern()208     public boolean isPattern() {
209         ensureNotZeroized();
210         return mType == CREDENTIAL_TYPE_PATTERN;
211     }
212 
213     /** Returns whether this is a numeric pin credential */
isPin()214     public boolean isPin() {
215         ensureNotZeroized();
216         return mType == CREDENTIAL_TYPE_PIN;
217     }
218 
219     /** Returns whether this is an alphabetic password credential */
isPassword()220     public boolean isPassword() {
221         ensureNotZeroized();
222         return mType == CREDENTIAL_TYPE_PASSWORD;
223     }
224 
225     /** Returns the length of the credential */
size()226     public int size() {
227         ensureNotZeroized();
228         return mCredential.length;
229     }
230 
231     /** Returns true if this credential was constructed with any chars outside the allowed range */
hasInvalidChars()232     public boolean hasInvalidChars() {
233         ensureNotZeroized();
234         return mHasInvalidChars;
235     }
236 
237     /** Create a copy of the credential */
duplicate()238     public LockscreenCredential duplicate() {
239         return new LockscreenCredential(mType,
240                 mCredential != null ? copyOfArrayNonMovable(mCredential) : null,
241                 mHasInvalidChars);
242     }
243 
244     /**
245      * Zeroize the credential bytes.
246      */
zeroize()247     public void zeroize() {
248         if (mCredential != null) {
249             LockPatternUtils.zeroize(mCredential);
250             mCredential = null;
251         }
252     }
253 
254     /**
255      * Copies the given array into a new non-movable array.
256      */
copyOfArrayNonMovable(byte[] array)257     private static byte[] copyOfArrayNonMovable(byte[] array) {
258         byte[] copy = LockPatternUtils.newNonMovableByteArray(array.length);
259         System.arraycopy(array, 0, copy, 0, array.length);
260         return copy;
261     }
262 
263     /**
264      * Checks whether the credential meets basic requirements for setting it as a new credential.
265      *
266      * This is redundant if {@link android.app.admin.PasswordMetrics#validateCredential()}, which
267      * does more comprehensive checks, is correctly called first (which it should be).
268      *
269      * @throws IllegalArgumentException if the credential contains invalid characters or is too
270      * short
271      */
validateBasicRequirements()272     public void validateBasicRequirements() {
273         if (mHasInvalidChars) {
274             throw new IllegalArgumentException("credential contains invalid characters");
275         }
276         switch (getType()) {
277             case CREDENTIAL_TYPE_PATTERN:
278                 if (size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) {
279                     throw new IllegalArgumentException("pattern must be at least "
280                             + LockPatternUtils.MIN_LOCK_PATTERN_SIZE + " dots long.");
281                 }
282                 break;
283             case CREDENTIAL_TYPE_PIN:
284                 if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) {
285                     throw new IllegalArgumentException("PIN must be at least "
286                             + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE + " digits long.");
287                 }
288                 break;
289             case CREDENTIAL_TYPE_PASSWORD:
290                 if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) {
291                     throw new IllegalArgumentException("password must be at least "
292                             + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE + " characters long.");
293                 }
294                 break;
295         }
296     }
297 
298     /**
299      * Check if this credential's type matches one that's retrieved from disk. The nuance here is
300      * that the framework used to not distinguish between PIN and password, so this method will
301      * allow a PIN/Password LockscreenCredential to match against the legacy
302      * {@link #CREDENTIAL_TYPE_PASSWORD_OR_PIN} stored on disk.
303      */
checkAgainstStoredType(int storedCredentialType)304     public boolean checkAgainstStoredType(int storedCredentialType) {
305         if (storedCredentialType == CREDENTIAL_TYPE_PASSWORD_OR_PIN) {
306             return getType() == CREDENTIAL_TYPE_PASSWORD || getType() == CREDENTIAL_TYPE_PIN;
307         }
308         return getType() == storedCredentialType;
309     }
310 
311     /**
312      * Hash the password for password history check purpose.
313      */
passwordToHistoryHash(byte[] salt, byte[] hashFactor)314     public String passwordToHistoryHash(byte[] salt, byte[] hashFactor) {
315         return passwordToHistoryHash(mCredential, salt, hashFactor);
316     }
317 
318     /**
319      * Hash the password for password history check purpose.
320      */
passwordToHistoryHash( byte[] passwordToHash, byte[] salt, byte[] hashFactor)321     public static String passwordToHistoryHash(
322             byte[] passwordToHash, byte[] salt, byte[] hashFactor) {
323         if (passwordToHash == null || passwordToHash.length == 0
324                 || hashFactor == null || salt == null) {
325             return null;
326         }
327         try {
328             MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
329             sha256.update(hashFactor);
330             sha256.update(passwordToHash);
331             sha256.update(salt);
332             return HexEncoding.encodeToString(sha256.digest());
333         } catch (NoSuchAlgorithmException e) {
334             throw new AssertionError("Missing digest algorithm: ", e);
335         }
336     }
337 
338     /**
339      * Hash the given password for the password history, using the legacy algorithm.
340      *
341      * @deprecated This algorithm is insecure because the password can be easily bruteforced, given
342      *             the hash and salt.  Use {@link #passwordToHistoryHash(byte[], byte[], byte[])}
343      *             instead, which incorporates an SP-derived secret into the hash.
344      *
345      * @return the legacy password hash
346      */
347     @Deprecated
legacyPasswordToHash(byte[] password, byte[] salt)348     public static String legacyPasswordToHash(byte[] password, byte[] salt) {
349         if (password == null || password.length == 0 || salt == null) {
350             return null;
351         }
352 
353         try {
354             byte[] saltedPassword = ArrayUtils.concat(password, salt);
355             byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword);
356             byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword);
357 
358             LockPatternUtils.zeroize(saltedPassword);
359             return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5));
360         } catch (NoSuchAlgorithmException e) {
361             throw new AssertionError("Missing digest algorithm: ", e);
362         }
363     }
364 
365     @Override
writeToParcel(Parcel dest, int flags)366     public void writeToParcel(Parcel dest, int flags) {
367         dest.writeInt(mType);
368         dest.writeByteArray(mCredential);
369         dest.writeBoolean(mHasInvalidChars);
370     }
371 
372     public static final Parcelable.Creator<LockscreenCredential> CREATOR =
373             new Parcelable.Creator<LockscreenCredential>() {
374 
375         @Override
376         public LockscreenCredential createFromParcel(Parcel source) {
377             return new LockscreenCredential(source.readInt(), source.createByteArray(),
378                     source.readBoolean());
379         }
380 
381         @Override
382         public LockscreenCredential[] newArray(int size) {
383             return new LockscreenCredential[size];
384         }
385     };
386 
387     @Override
describeContents()388     public int describeContents() {
389         return 0;
390     }
391 
392     @Override
close()393     public void close() {
394         zeroize();
395     }
396 
397     @Override
finalize()398     public void finalize() {
399         zeroize();
400     }
401 
402     @Override
hashCode()403     public int hashCode() {
404         // Effective Java — Always override hashCode when you override equals
405         return Objects.hash(mType, Arrays.hashCode(mCredential), mHasInvalidChars);
406     }
407 
408     @Override
equals(Object o)409     public boolean equals(Object o) {
410         if (o == this) return true;
411         if (!(o instanceof LockscreenCredential)) return false;
412         final LockscreenCredential other = (LockscreenCredential) o;
413         return mType == other.mType && Arrays.equals(mCredential, other.mCredential)
414             && mHasInvalidChars == other.mHasInvalidChars;
415     }
416 
hasInvalidChars(CharSequence chars)417     private static boolean hasInvalidChars(CharSequence chars) {
418         //
419         // Consider the password to have invalid characters if it contains any non-ASCII characters
420         // or control characters.  There are multiple reasons for this restriction:
421         //
422         // - Non-ASCII characters might only be possible to enter on a third-party keyboard app
423         //   (IME) that is available when setting the password but not when verifying it after a
424         //   reboot.  This can happen if the keyboard is not direct boot aware or gets uninstalled.
425         //
426         // - Unicode strings that look identical to the user can map to different byte[].  Yet, only
427         //   one byte[] can be accepted.  Unicode normalization can solve this problem to some
428         //   extent, but still many Unicode characters look similar and could cause confusion.
429         //
430         // - For backwards compatibility reasons, the upper 8 bits of the 16-bit 'chars' are
431         //   discarded by charsToBytesTruncating().  Thus, as-is passwords with characters above
432         //   U+00FF (255) are not as secure as they should be.  IMPORTANT: Do not change the below
433         //   code to allow characters above U+00FF (255) without fixing this issue!
434         //
435         for (int i = 0; i < chars.length(); i++) {
436             char c = chars.charAt(i);
437             if (c < 32 || c > 127) {
438                 return true;
439             }
440         }
441         return false;
442     }
443 
444     /**
445      * Converts a CharSequence to a byte array, intentionally truncating chars greater than 255 for
446      * backwards compatibility reasons.  See {@link #mHasInvalidChars}.
447      *
448      * @param chars The CharSequence to convert
449      * @return A byte array representing the input
450      */
charsToBytesTruncating(CharSequence chars)451     private static byte[] charsToBytesTruncating(CharSequence chars) {
452         byte[] bytes = LockPatternUtils.newNonMovableByteArray(chars.length());
453         for (int i = 0; i < chars.length(); i++) {
454             bytes[i] = (byte) chars.charAt(i);
455         }
456         return bytes;
457     }
458 }
459