• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.server.locksettings;
18 
19 import android.annotation.Nullable;
20 import android.security.GateKeeper;
21 import android.security.keystore.KeyGenParameterSpec;
22 import android.security.keystore.KeyProperties;
23 import android.security.keystore.UserNotAuthenticatedException;
24 import android.util.Slog;
25 import android.util.SparseArray;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.internal.util.ArrayUtils;
29 import com.android.internal.widget.LockPatternUtils;
30 import com.android.internal.widget.LockscreenCredential;
31 
32 import java.security.GeneralSecurityException;
33 import java.security.Key;
34 import java.security.KeyStore;
35 import java.security.KeyStoreException;
36 import java.security.NoSuchAlgorithmException;
37 import java.security.UnrecoverableKeyException;
38 import java.util.Arrays;
39 import java.util.concurrent.TimeUnit;
40 
41 import javax.crypto.Cipher;
42 import javax.crypto.KeyGenerator;
43 import javax.crypto.SecretKey;
44 import javax.crypto.spec.GCMParameterSpec;
45 
46 /**
47  * An in-memory cache for unified profile passwords.  A "unified profile password" is the random
48  * password that the system automatically generates and manages for each profile that uses a unified
49  * challenge and where the parent user has a secure lock screen.
50  * <p>
51  * Each password in this cache is encrypted by a Keystore key that is auth-bound to the parent user.
52  * This is very similar to how the password is protected on-disk, but the in-memory cache uses a
53  * much longer timeout on the keys: 7 days instead of 30 seconds.  This enables use cases like
54  * unpausing work apps without requiring authentication as frequently.
55  * <p>
56  * Unified profile passwords are cached when they are created, or when they are decrypted as part of
57  * the parent user's LSKF verification flow.  They are removed when the profile is deleted or when a
58  * separate challenge is explicitly set on the profile.  There is also an ADB command to evict a
59  * cached password, "locksettings remove-cache --user X", to assist development and testing.
60  */
61 @VisibleForTesting // public visibility is needed for Mockito
62 public class UnifiedProfilePasswordCache {
63 
64     private static final String TAG = "UnifiedProfilePasswordCache";
65     private static final int KEY_LENGTH = 256;
66     private static final int CACHE_TIMEOUT_SECONDS = (int) TimeUnit.DAYS.toSeconds(7);
67 
68     private final SparseArray<byte[]> mEncryptedPasswords = new SparseArray<>();
69     private final KeyStore mKeyStore;
70 
UnifiedProfilePasswordCache(KeyStore keyStore)71     public UnifiedProfilePasswordCache(KeyStore keyStore) {
72         mKeyStore = keyStore;
73     }
74 
75     /**
76      * Encrypt and store the password in the cache. Does NOT overwrite existing password cache
77      * if one for the given user already exists.
78      *
79      * Should only be called on a profile userId.
80      */
storePassword(int userId, LockscreenCredential password, long parentSid)81     public void storePassword(int userId, LockscreenCredential password, long parentSid) {
82         if (parentSid == GateKeeper.INVALID_SECURE_USER_ID) return;
83         synchronized (mEncryptedPasswords) {
84             if (mEncryptedPasswords.contains(userId)) {
85                 return;
86             }
87             String keyName = getEncryptionKeyName(userId);
88             KeyGenerator generator;
89             SecretKey key;
90             try {
91                 generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
92                         mKeyStore.getProvider());
93                 generator.init(new KeyGenParameterSpec.Builder(
94                         keyName, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
95                         .setKeySize(KEY_LENGTH)
96                         .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
97                         .setNamespace(SyntheticPasswordCrypto.keyNamespace())
98                         .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
99                         .setUserAuthenticationRequired(true)
100                         .setBoundToSpecificSecureUserId(parentSid)
101                         .setUserAuthenticationValidityDurationSeconds(CACHE_TIMEOUT_SECONDS)
102                         .build());
103                 key = generator.generateKey();
104             } catch (GeneralSecurityException e) {
105                 Slog.e(TAG, "Cannot generate key", e);
106                 return;
107             }
108 
109             Cipher cipher;
110             try {
111                 cipher = Cipher.getInstance("AES/GCM/NoPadding");
112                 cipher.init(Cipher.ENCRYPT_MODE, key);
113                 byte[] ciphertext = cipher.doFinal(password.getCredential());
114                 byte[] iv = cipher.getIV();
115                 byte[] block = ArrayUtils.concat(iv, ciphertext);
116                 mEncryptedPasswords.put(userId, block);
117             } catch (GeneralSecurityException e) {
118                 Slog.d(TAG, "Cannot encrypt", e);
119             }
120         }
121     }
122 
123     /** Attempt to retrieve the password for the given user. Returns {@code null} if it's not in the
124      * cache or if decryption fails.
125      */
retrievePassword(int userId)126     public @Nullable LockscreenCredential retrievePassword(int userId) {
127         synchronized (mEncryptedPasswords) {
128             byte[] block = mEncryptedPasswords.get(userId);
129             if (block == null) {
130                 return null;
131             }
132             Key key;
133             try {
134                 key = mKeyStore.getKey(getEncryptionKeyName(userId), null);
135             } catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) {
136                 Slog.d(TAG, "Cannot get key", e);
137                 return null;
138             }
139             if (key == null) {
140                 return null;
141             }
142             byte[] iv = Arrays.copyOf(block, 12);
143             byte[] ciphertext = Arrays.copyOfRange(block, 12, block.length);
144             byte[] credential;
145             try {
146                 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
147                 cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
148                 credential = cipher.doFinal(ciphertext);
149             } catch (UserNotAuthenticatedException e) {
150                 Slog.i(TAG, "Device not unlocked for more than 7 days");
151                 return null;
152             } catch (GeneralSecurityException e) {
153                 Slog.d(TAG, "Cannot decrypt", e);
154                 return null;
155             }
156             LockscreenCredential result =
157                     LockscreenCredential.createUnifiedProfilePassword(credential);
158             LockPatternUtils.zeroize(credential);
159             return result;
160         }
161     }
162 
163     /** Remove the given user's password from cache, if one exists. */
removePassword(int userId)164     public void removePassword(int userId) {
165         synchronized (mEncryptedPasswords) {
166             String keyName = getEncryptionKeyName(userId);
167             String legacyKeyName = getLegacyEncryptionKeyName(userId);
168             try {
169                 if (mKeyStore.containsAlias(keyName)) {
170                     mKeyStore.deleteEntry(keyName);
171                 }
172                 if (mKeyStore.containsAlias(legacyKeyName)) {
173                     mKeyStore.deleteEntry(legacyKeyName);
174                 }
175             } catch (KeyStoreException e) {
176                 Slog.d(TAG, "Cannot delete key", e);
177             }
178             if (mEncryptedPasswords.contains(userId)) {
179                 LockPatternUtils.zeroize(mEncryptedPasswords.get(userId));
180                 mEncryptedPasswords.remove(userId);
181             }
182         }
183     }
184 
getEncryptionKeyName(int userId)185     private static String getEncryptionKeyName(int userId) {
186         return "com.android.server.locksettings.unified_profile_cache_v2_" + userId;
187     }
188 
189     /**
190      * Returns the legacy keystore key name when setUnlockedDeviceRequired() was set explicitly.
191      * Only existed during Android 11 internal testing period.
192      */
getLegacyEncryptionKeyName(int userId)193     private static String getLegacyEncryptionKeyName(int userId) {
194         return "com.android.server.locksettings.unified_profile_cache_" + userId;
195     }
196 }
197