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  * Caches *unified* work challenge for managed profiles. The cached credential is encrypted using
47  * a keystore key auth-bound to the parent user's lockscreen credential, similar to how unified
48  * work challenge is normally secured.
49  *
50  * <p> The cache is filled whenever the managed profile's unified challenge is created or derived
51  * (as part of the parent user's credential verification flow). It's removed when the profile is
52  * deleted or a (separate) lockscreen credential is explicitly set on the profile. There is also
53  * an ADB command to evict the cache "cmd lock_settings remove-cache --user X", to assist
54  * development and testing.
55 
56  * <p> The encrypted credential is stored in-memory only so the cache does not persist across
57  * reboots.
58  */
59 @VisibleForTesting // public visibility is needed for Mockito
60 public class ManagedProfilePasswordCache {
61 
62     private static final String TAG = "ManagedProfilePasswordCache";
63     private static final int KEY_LENGTH = 256;
64     private static final int CACHE_TIMEOUT_SECONDS = (int) TimeUnit.DAYS.toSeconds(7);
65 
66     private final SparseArray<byte[]> mEncryptedPasswords = new SparseArray<>();
67     private final KeyStore mKeyStore;
68 
ManagedProfilePasswordCache(KeyStore keyStore)69     public ManagedProfilePasswordCache(KeyStore keyStore) {
70         mKeyStore = keyStore;
71     }
72 
73     /**
74      * Encrypt and store the password in the cache. Does NOT overwrite existing password cache
75      * if one for the given user already exists.
76      *
77      * Should only be called on a profile userId.
78      */
storePassword(int userId, LockscreenCredential password, long parentSid)79     public void storePassword(int userId, LockscreenCredential password, long parentSid) {
80         if (parentSid == GateKeeper.INVALID_SECURE_USER_ID) return;
81         synchronized (mEncryptedPasswords) {
82             if (mEncryptedPasswords.contains(userId)) {
83                 return;
84             }
85             String keyName = getEncryptionKeyName(userId);
86             KeyGenerator generator;
87             SecretKey key;
88             try {
89                 generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES,
90                         mKeyStore.getProvider());
91                 generator.init(new KeyGenParameterSpec.Builder(
92                         keyName, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
93                         .setKeySize(KEY_LENGTH)
94                         .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
95                         .setNamespace(SyntheticPasswordCrypto.keyNamespace())
96                         .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
97                         .setUserAuthenticationRequired(true)
98                         .setBoundToSpecificSecureUserId(parentSid)
99                         .setUserAuthenticationValidityDurationSeconds(CACHE_TIMEOUT_SECONDS)
100                         .build());
101                 key = generator.generateKey();
102             } catch (GeneralSecurityException e) {
103                 Slog.e(TAG, "Cannot generate key", e);
104                 return;
105             }
106 
107             Cipher cipher;
108             try {
109                 cipher = Cipher.getInstance("AES/GCM/NoPadding");
110                 cipher.init(Cipher.ENCRYPT_MODE, key);
111                 byte[] ciphertext = cipher.doFinal(password.getCredential());
112                 byte[] iv = cipher.getIV();
113                 byte[] block = ArrayUtils.concat(iv, ciphertext);
114                 mEncryptedPasswords.put(userId, block);
115             } catch (GeneralSecurityException e) {
116                 Slog.d(TAG, "Cannot encrypt", e);
117             }
118         }
119     }
120 
121     /** Attempt to retrieve the password for the given user. Returns {@code null} if it's not in the
122      * cache or if decryption fails.
123      */
retrievePassword(int userId)124     public @Nullable LockscreenCredential retrievePassword(int userId) {
125         synchronized (mEncryptedPasswords) {
126             byte[] block = mEncryptedPasswords.get(userId);
127             if (block == null) {
128                 return null;
129             }
130             Key key;
131             try {
132                 key = mKeyStore.getKey(getEncryptionKeyName(userId), null);
133             } catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) {
134                 Slog.d(TAG, "Cannot get key", e);
135                 return null;
136             }
137             if (key == null) {
138                 return null;
139             }
140             byte[] iv = Arrays.copyOf(block, 12);
141             byte[] ciphertext = Arrays.copyOfRange(block, 12, block.length);
142             byte[] credential;
143             try {
144                 Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
145                 cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
146                 credential = cipher.doFinal(ciphertext);
147             } catch (UserNotAuthenticatedException e) {
148                 Slog.i(TAG, "Device not unlocked for more than 7 days");
149                 return null;
150             } catch (GeneralSecurityException e) {
151                 Slog.d(TAG, "Cannot decrypt", e);
152                 return null;
153             }
154             LockscreenCredential result = LockscreenCredential.createManagedPassword(credential);
155             Arrays.fill(credential, (byte) 0);
156             return result;
157         }
158     }
159 
160     /** Remove the given user's password from cache, if one exists. */
removePassword(int userId)161     public void removePassword(int userId) {
162         synchronized (mEncryptedPasswords) {
163             String keyName = getEncryptionKeyName(userId);
164             String legacyKeyName = getLegacyEncryptionKeyName(userId);
165             try {
166                 if (mKeyStore.containsAlias(keyName)) {
167                     mKeyStore.deleteEntry(keyName);
168                 }
169                 if (mKeyStore.containsAlias(legacyKeyName)) {
170                     mKeyStore.deleteEntry(legacyKeyName);
171                 }
172             } catch (KeyStoreException e) {
173                 Slog.d(TAG, "Cannot delete key", e);
174             }
175             if (mEncryptedPasswords.contains(userId)) {
176                 Arrays.fill(mEncryptedPasswords.get(userId), (byte) 0);
177                 mEncryptedPasswords.remove(userId);
178             }
179         }
180     }
181 
getEncryptionKeyName(int userId)182     private static String getEncryptionKeyName(int userId) {
183         return "com.android.server.locksettings.unified_profile_cache_v2_" + userId;
184     }
185 
186     /**
187      * Returns the legacy keystore key name when setUnlockedDeviceRequired() was set explicitly.
188      * Only existed during Android 11 internal testing period.
189      */
getLegacyEncryptionKeyName(int userId)190     private static String getLegacyEncryptionKeyName(int userId) {
191         return "com.android.server.locksettings.unified_profile_cache_" + userId;
192     }
193 }
194