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. An empty password
64     // is represented as a byte array of length 0.
65     private byte[] mCredential;
66 
67     /**
68      * Private constructor, use static builder methods instead.
69      *
70      * <p> Builder methods should create a private copy of the credential bytes and pass in here.
71      * LockscreenCredential will only store the reference internally without copying. This is to
72      * minimize the number of extra copies introduced.
73      */
LockscreenCredential(int type, byte[] credential)74     private LockscreenCredential(int type, byte[] credential) {
75         Objects.requireNonNull(credential);
76         if (type == CREDENTIAL_TYPE_NONE) {
77             Preconditions.checkArgument(credential.length == 0);
78         } else {
79             // Do not allow constructing a CREDENTIAL_TYPE_PASSWORD_OR_PIN object.
80             Preconditions.checkArgument(type == CREDENTIAL_TYPE_PIN
81                     || type == CREDENTIAL_TYPE_PASSWORD
82                     || type == CREDENTIAL_TYPE_PATTERN);
83             Preconditions.checkArgument(credential.length > 0);
84         }
85         mType = type;
86         mCredential = credential;
87     }
88 
89     /**
90      * Creates a LockscreenCredential object representing empty password.
91      */
createNone()92     public static LockscreenCredential createNone() {
93         return new LockscreenCredential(CREDENTIAL_TYPE_NONE, new byte[0]);
94     }
95 
96     /**
97      * Creates a LockscreenCredential object representing the given pattern.
98      */
createPattern(@onNull List<LockPatternView.Cell> pattern)99     public static LockscreenCredential createPattern(@NonNull List<LockPatternView.Cell> pattern) {
100         return new LockscreenCredential(CREDENTIAL_TYPE_PATTERN,
101                 LockPatternUtils.patternToByteArray(pattern));
102     }
103 
104     /**
105      * Creates a LockscreenCredential object representing the given alphabetic password.
106      */
createPassword(@onNull CharSequence password)107     public static LockscreenCredential createPassword(@NonNull CharSequence password) {
108         return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD,
109                 charSequenceToByteArray(password));
110     }
111 
112     /**
113      * Creates a LockscreenCredential object representing a managed password for profile with
114      * unified challenge. This credentiall will have type {@code CREDENTIAL_TYPE_PASSWORD} for now.
115      * TODO: consider add a new credential type for this. This can then supersede the
116      * isLockTiedToParent argument in various places in LSS.
117      */
createManagedPassword(@onNull byte[] password)118     public static LockscreenCredential createManagedPassword(@NonNull byte[] password) {
119         return new LockscreenCredential(CREDENTIAL_TYPE_PASSWORD,
120                 Arrays.copyOf(password, password.length));
121     }
122 
123     /**
124      * Creates a LockscreenCredential object representing the given numeric PIN.
125      */
createPin(@onNull CharSequence pin)126     public static LockscreenCredential createPin(@NonNull CharSequence pin) {
127         return new LockscreenCredential(CREDENTIAL_TYPE_PIN,
128                 charSequenceToByteArray(pin));
129     }
130 
131     /**
132      * Creates a LockscreenCredential object representing the given alphabetic password.
133      * If the supplied password is empty, create an empty credential object.
134      */
createPasswordOrNone(@ullable CharSequence password)135     public static LockscreenCredential createPasswordOrNone(@Nullable CharSequence password) {
136         if (TextUtils.isEmpty(password)) {
137             return createNone();
138         } else {
139             return createPassword(password);
140         }
141     }
142 
143     /**
144      * Creates a LockscreenCredential object representing the given numeric PIN.
145      * If the supplied password is empty, create an empty credential object.
146      */
createPinOrNone(@ullable CharSequence pin)147     public static LockscreenCredential createPinOrNone(@Nullable CharSequence pin) {
148         if (TextUtils.isEmpty(pin)) {
149             return createNone();
150         } else {
151             return createPin(pin);
152         }
153     }
154 
ensureNotZeroized()155     private void ensureNotZeroized() {
156         Preconditions.checkState(mCredential != null, "Credential is already zeroized");
157     }
158     /**
159      * Returns the type of this credential. Can be one of {@link #CREDENTIAL_TYPE_NONE},
160      * {@link #CREDENTIAL_TYPE_PATTERN}, {@link #CREDENTIAL_TYPE_PIN} or
161      * {@link #CREDENTIAL_TYPE_PASSWORD}.
162      */
getType()163     public int getType() {
164         ensureNotZeroized();
165         return mType;
166     }
167 
168     /**
169      * Returns the credential bytes. This is a direct reference of the internal field so
170      * callers should not modify it.
171      *
172      */
getCredential()173     public byte[] getCredential() {
174         ensureNotZeroized();
175         return mCredential;
176     }
177 
178     /** Returns whether this is an empty credential */
isNone()179     public boolean isNone() {
180         ensureNotZeroized();
181         return mType == CREDENTIAL_TYPE_NONE;
182     }
183 
184     /** Returns whether this is a pattern credential */
isPattern()185     public boolean isPattern() {
186         ensureNotZeroized();
187         return mType == CREDENTIAL_TYPE_PATTERN;
188     }
189 
190     /** Returns whether this is a numeric pin credential */
isPin()191     public boolean isPin() {
192         ensureNotZeroized();
193         return mType == CREDENTIAL_TYPE_PIN;
194     }
195 
196     /** Returns whether this is an alphabetic password credential */
isPassword()197     public boolean isPassword() {
198         ensureNotZeroized();
199         return mType == CREDENTIAL_TYPE_PASSWORD;
200     }
201 
202     /** Returns the length of the credential */
size()203     public int size() {
204         ensureNotZeroized();
205         return mCredential.length;
206     }
207 
208     /** Create a copy of the credential */
duplicate()209     public LockscreenCredential duplicate() {
210         return new LockscreenCredential(mType,
211                 mCredential != null ? Arrays.copyOf(mCredential, mCredential.length) : null);
212     }
213 
214     /**
215      * Zeroize the credential bytes.
216      */
zeroize()217     public void zeroize() {
218         if (mCredential != null) {
219             Arrays.fill(mCredential, (byte) 0);
220             mCredential = null;
221         }
222     }
223 
224     /**
225      * Check if the credential meets minimal length requirement.
226      *
227      * @throws IllegalArgumentException if the credential is too short.
228      */
checkLength()229     public void checkLength() {
230         if (isNone()) {
231             return;
232         }
233         if (isPattern()) {
234             if (size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) {
235                 throw new IllegalArgumentException("pattern must not be null and at least "
236                         + LockPatternUtils.MIN_LOCK_PATTERN_SIZE + " dots long.");
237             }
238             return;
239         }
240         if (isPassword() || isPin()) {
241             if (size() < LockPatternUtils.MIN_LOCK_PASSWORD_SIZE) {
242                 throw new IllegalArgumentException("password must not be null and at least "
243                         + "of length " + LockPatternUtils.MIN_LOCK_PASSWORD_SIZE);
244             }
245             return;
246         }
247     }
248 
249     /**
250      * Check if this credential's type matches one that's retrieved from disk. The nuance here is
251      * that the framework used to not distinguish between PIN and password, so this method will
252      * allow a PIN/Password LockscreenCredential to match against the legacy
253      * {@link #CREDENTIAL_TYPE_PASSWORD_OR_PIN} stored on disk.
254      */
checkAgainstStoredType(int storedCredentialType)255     public boolean checkAgainstStoredType(int storedCredentialType) {
256         if (storedCredentialType == CREDENTIAL_TYPE_PASSWORD_OR_PIN) {
257             return getType() == CREDENTIAL_TYPE_PASSWORD || getType() == CREDENTIAL_TYPE_PIN;
258         }
259         return getType() == storedCredentialType;
260     }
261 
262     /**
263      * Hash the password for password history check purpose.
264      */
passwordToHistoryHash(byte[] salt, byte[] hashFactor)265     public String passwordToHistoryHash(byte[] salt, byte[] hashFactor) {
266         return passwordToHistoryHash(mCredential, salt, hashFactor);
267     }
268 
269     /**
270      * Hash the password for password history check purpose.
271      */
passwordToHistoryHash( byte[] passwordToHash, byte[] salt, byte[] hashFactor)272     public static String passwordToHistoryHash(
273             byte[] passwordToHash, byte[] salt, byte[] hashFactor) {
274         if (passwordToHash == null || passwordToHash.length == 0
275                 || hashFactor == null || salt == null) {
276             return null;
277         }
278         try {
279             MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
280             sha256.update(hashFactor);
281             sha256.update(passwordToHash);
282             sha256.update(salt);
283             return HexEncoding.encodeToString(sha256.digest());
284         } catch (NoSuchAlgorithmException e) {
285             throw new AssertionError("Missing digest algorithm: ", e);
286         }
287     }
288 
289     /**
290      * Hash the given password for the password history, using the legacy algorithm.
291      *
292      * @deprecated This algorithm is insecure because the password can be easily bruteforced, given
293      *             the hash and salt.  Use {@link #passwordToHistoryHash(byte[], byte[], byte[])}
294      *             instead, which incorporates an SP-derived secret into the hash.
295      *
296      * @return the legacy password hash
297      */
298     @Deprecated
legacyPasswordToHash(byte[] password, byte[] salt)299     public static String legacyPasswordToHash(byte[] password, byte[] salt) {
300         if (password == null || password.length == 0 || salt == null) {
301             return null;
302         }
303 
304         try {
305             byte[] saltedPassword = ArrayUtils.concat(password, salt);
306             byte[] sha1 = MessageDigest.getInstance("SHA-1").digest(saltedPassword);
307             byte[] md5 = MessageDigest.getInstance("MD5").digest(saltedPassword);
308 
309             Arrays.fill(saltedPassword, (byte) 0);
310             return HexEncoding.encodeToString(ArrayUtils.concat(sha1, md5));
311         } catch (NoSuchAlgorithmException e) {
312             throw new AssertionError("Missing digest algorithm: ", e);
313         }
314     }
315 
316     @Override
writeToParcel(Parcel dest, int flags)317     public void writeToParcel(Parcel dest, int flags) {
318         dest.writeInt(mType);
319         dest.writeByteArray(mCredential);
320     }
321 
322     public static final Parcelable.Creator<LockscreenCredential> CREATOR =
323             new Parcelable.Creator<LockscreenCredential>() {
324 
325         @Override
326         public LockscreenCredential createFromParcel(Parcel source) {
327             return new LockscreenCredential(source.readInt(), source.createByteArray());
328         }
329 
330         @Override
331         public LockscreenCredential[] newArray(int size) {
332             return new LockscreenCredential[size];
333         }
334     };
335 
336     @Override
describeContents()337     public int describeContents() {
338         return 0;
339     }
340 
341     @Override
close()342     public void close() {
343         zeroize();
344     }
345 
346     @Override
hashCode()347     public int hashCode() {
348         // Effective Java — Always override hashCode when you override equals
349         return Objects.hash(mType, Arrays.hashCode(mCredential));
350     }
351 
352     @Override
equals(Object o)353     public boolean equals(Object o) {
354         if (o == this) return true;
355         if (!(o instanceof LockscreenCredential)) return false;
356         final LockscreenCredential other = (LockscreenCredential) o;
357         return mType == other.mType && Arrays.equals(mCredential, other.mCredential);
358     }
359 
360     /**
361      * Converts a CharSequence to a byte array without requiring a toString(), which creates an
362      * additional copy.
363      *
364      * @param chars The CharSequence to convert
365      * @return A byte array representing the input
366      */
charSequenceToByteArray(CharSequence chars)367     private static byte[] charSequenceToByteArray(CharSequence chars) {
368         if (chars == null) {
369             return new byte[0];
370         }
371         byte[] bytes = new byte[chars.length()];
372         for (int i = 0; i < chars.length(); i++) {
373             bytes[i] = (byte) chars.charAt(i);
374         }
375         return bytes;
376     }
377 }
378