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