• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 android.security.keystore;
18 
19 import android.annotation.FlaggedApi;
20 import android.annotation.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.SystemService;
23 import android.content.Context;
24 import android.hardware.security.keymint.TagType;
25 import android.security.KeyStore2;
26 import android.security.KeyStoreException;
27 import android.security.keystore2.AndroidKeyStoreProvider;
28 import android.security.keystore2.AndroidKeyStorePublicKey;
29 import android.system.keystore2.Domain;
30 import android.system.keystore2.KeyDescriptor;
31 import android.system.keystore2.KeyPermission;
32 import android.util.Log;
33 
34 import com.android.internal.annotations.GuardedBy;
35 
36 import java.io.ByteArrayInputStream;
37 import java.lang.annotation.Retention;
38 import java.lang.annotation.RetentionPolicy;
39 import java.security.Key;
40 import java.security.KeyPair;
41 import java.security.PublicKey;
42 import java.security.UnrecoverableKeyException;
43 import java.security.cert.CertificateFactory;
44 import java.security.cert.X509Certificate;
45 import java.util.ArrayList;
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.List;
49 
50 /**
51  * This class provides methods for interacting with keys stored within the
52  * <a href="/privacy-and-security/keystore">Android Keystore</a>.
53  */
54 @FlaggedApi(android.security.Flags.FLAG_KEYSTORE_GRANT_API)
55 @SystemService(Context.KEYSTORE_SERVICE)
56 public final class KeyStoreManager {
57     private static final String TAG = "KeyStoreManager";
58 
59     private static final Object sInstanceLock = new Object();
60     @GuardedBy("sInstanceLock")
61     private static KeyStoreManager sInstance;
62 
63     private final KeyStore2 mKeyStore2;
64 
65     /**
66      * Private constructor to ensure only a single instance is created.
67      */
KeyStoreManager()68     private KeyStoreManager() {
69         mKeyStore2 = KeyStore2.getInstance();
70     }
71 
72     /**
73      * Returns the single instance of the {@code KeyStoreManager}.
74      *
75      * @hide
76      */
getInstance()77     public static KeyStoreManager getInstance() {
78         synchronized (sInstanceLock) {
79             if (sInstance == null) {
80                 sInstance = new KeyStoreManager();
81             }
82             return sInstance;
83         }
84     }
85 
86     /**
87      * Grants access to the key owned by the calling app stored under the specified {@code alias}
88      * to another app on the device with the provided {@code uid}.
89      *
90      * <p>This method supports granting access to instances of both {@link javax.crypto.SecretKey}
91      * and {@link java.security.PrivateKey}. The resulting ID will persist across reboots and can be
92      * used by the grantee app for the life of the key or until access is revoked with {@link
93      * #revokeKeyAccess(String, int)}.
94      *
95      * <p>If the provided {@code alias} does not correspond to a key in the Android KeyStore, then
96      * an {@link UnrecoverableKeyException} is thrown.
97      *
98      * @param alias the alias of the key to be granted to another app
99      * @param uid   the uid of the app to which the key should be granted
100      * @return the ID of the granted key; this can be shared with the specified app, and that
101      * app can use {@link #getGrantedKeyFromId(long)} to access the key
102      * @throws UnrecoverableKeyException if the specified key cannot be recovered
103      * @throws KeyStoreException if an error is encountered when attempting to grant access to
104      * the key
105      * @see #getGrantedKeyFromId(long)
106      */
grantKeyAccess(@onNull String alias, int uid)107     public long grantKeyAccess(@NonNull String alias, int uid)
108             throws KeyStoreException, UnrecoverableKeyException {
109         KeyDescriptor keyDescriptor = createKeyDescriptorFromAlias(alias);
110         final int grantAccessVector = KeyPermission.USE | KeyPermission.GET_INFO;
111         // When a key is in the GRANT domain, the nspace field of the KeyDescriptor contains its ID.
112         KeyDescriptor result = null;
113         try {
114             result = mKeyStore2.grant(keyDescriptor, uid, grantAccessVector);
115         } catch (KeyStoreException e) {
116             // If the provided alias does not correspond to a valid key in the KeyStore, then throw
117             // an UnrecoverableKeyException to remain consistent with other APIs in this class.
118             if (e.getNumericErrorCode() == KeyStoreException.ERROR_KEY_DOES_NOT_EXIST) {
119                 throw new UnrecoverableKeyException("No key found by the given alias");
120             }
121             throw e;
122         }
123         if (result == null) {
124             Log.e(TAG, "Received a null KeyDescriptor from grant");
125             throw new KeyStoreException(KeyStoreException.ERROR_INTERNAL_SYSTEM_ERROR,
126                     "No ID was returned for the grant request for alias " + alias + " to uid "
127                             + uid);
128         } else if (result.domain != Domain.GRANT) {
129             Log.e(TAG, "Received a result outside the grant domain: " + result.domain);
130             throw new KeyStoreException(KeyStoreException.ERROR_INTERNAL_SYSTEM_ERROR,
131                     "Unable to obtain a grant ID for alias " + alias + " to uid " + uid);
132         }
133         return result.nspace;
134     }
135 
136     /**
137      * Revokes access to the key in the app's namespace stored under the specified {@code
138      * alias} that was previously granted to another app on the device with the provided
139      * {@code uid}.
140      *
141      * <p>If the provided {@code alias} does not correspond to a key in the Android KeyStore, then
142      * an {@link UnrecoverableKeyException} is thrown.
143      *
144      * @param alias the alias of the key to be revoked from another app
145      * @param uid   the uid of the app from which the key access should be revoked
146      * @throws UnrecoverableKeyException if the specified key cannot be recovered
147      * @throws KeyStoreException if an error is encountered when attempting to revoke access
148      * to the key
149      */
revokeKeyAccess(@onNull String alias, int uid)150     public void revokeKeyAccess(@NonNull String alias, int uid)
151             throws KeyStoreException, UnrecoverableKeyException {
152         KeyDescriptor keyDescriptor = createKeyDescriptorFromAlias(alias);
153         try {
154             mKeyStore2.ungrant(keyDescriptor, uid);
155         } catch (KeyStoreException e) {
156             // If the provided alias does not correspond to a valid key in the KeyStore, then throw
157             // an UnrecoverableKeyException to remain consistent with other APIs in this class.
158             if (e.getNumericErrorCode() == KeyStoreException.ERROR_KEY_DOES_NOT_EXIST) {
159                 throw new UnrecoverableKeyException("No key found by the given alias");
160             }
161             throw e;
162         }
163     }
164 
165     /**
166      * Returns the key with the specified {@code id} that was previously shared with the
167      * app.
168      *
169      * <p>This method can return instances of both {@link javax.crypto.SecretKey} and {@link
170      * java.security.PrivateKey}. If a key with the provide {@code id} has not been granted to the
171      * caller, then an {@link UnrecoverableKeyException} is thrown.
172      *
173      * @param id the ID of the key that was shared with the app
174      * @return the {@link Key} that was shared with the app
175      * @throws UnrecoverableKeyException          if the specified key cannot be recovered
176      * @throws KeyPermanentlyInvalidatedException if the specified key was authorized to only
177      *                                            be used if the user has been authenticated and a
178      *                                            change has been made to the users
179      *                                            lockscreen or biometric enrollment that
180      *                                            permanently invalidates the key
181      * @see #grantKeyAccess(String, int)
182      */
getGrantedKeyFromId(long id)183     public @NonNull Key getGrantedKeyFromId(long id)
184             throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
185         Key result = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore2, null,
186                 id, Domain.GRANT);
187         if (result == null) {
188             throw new UnrecoverableKeyException("No key found by the given alias");
189         }
190         return result;
191     }
192 
193     /**
194      * Returns a {@link KeyPair} containing the public and private key associated with
195      * the key that was previously shared with the app under the provided {@code id}.
196      *
197      * <p>If a {@link java.security.PrivateKey} has not been granted to the caller with the
198      * specified {@code id}, then an {@link UnrecoverableKeyException} is thrown.
199      *
200      * @param id the ID of the private key that was shared with the app
201      * @return a KeyPair containing the public and private key shared with the app
202      * @throws UnrecoverableKeyException          if the specified key cannot be recovered
203      * @throws KeyPermanentlyInvalidatedException if the specified key was authorized to only
204      *                                            be used if the user has been authenticated and a
205      *                                            change has been made to the users
206      *                                            lockscreen or biometric enrollment that
207      *                                            permanently invalidates the key
208      */
getGrantedKeyPairFromId(long id)209     public @NonNull KeyPair getGrantedKeyPairFromId(long id)
210             throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
211         KeyDescriptor keyDescriptor = createKeyDescriptorFromId(id, Domain.GRANT);
212         return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyPairFromKeystore(mKeyStore2,
213                 keyDescriptor);
214     }
215 
216     /**
217      * Returns a {@link List} of {@link X509Certificate} instances representing the certificate
218      * chain for the key that was previously shared with the app under the provided {@code id}.
219      *
220      * <p>If a {@link java.security.PrivateKey} has not been granted to the caller with the
221      * specified {@code id}, then an {@link UnrecoverableKeyException} is thrown.
222      *
223      * @param id the ID of the asymmetric key that was shared with the app
224      * @return a List of X509Certificates with the certificate at index 0 corresponding to
225      * the private key shared with the app
226      * @throws UnrecoverableKeyException          if the specified key cannot be recovered
227      * @throws KeyPermanentlyInvalidatedException if the specified key was authorized to only
228      *                                            be used if the user has been authenticated and a
229      *                                            change has been made to the users
230      *                                            lockscreen or biometric enrollment that
231      *                                            permanently invalidates the key
232      * @see #grantKeyAccess(String, int)
233      */
234     // Java APIs should prefer mutable collection return types with the exception being
235     // Collection.empty return types.
236     @SuppressWarnings("MixedMutabilityReturnType")
getGrantedCertificateChainFromId(long id)237     public @NonNull List<X509Certificate> getGrantedCertificateChainFromId(long id)
238             throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
239         KeyDescriptor keyDescriptor = createKeyDescriptorFromId(id, Domain.GRANT);
240         KeyPair keyPair = AndroidKeyStoreProvider.loadAndroidKeyStoreKeyPairFromKeystore(mKeyStore2,
241                 keyDescriptor);
242         PublicKey keyStoreKey = keyPair.getPublic();
243         if (keyStoreKey instanceof AndroidKeyStorePublicKey) {
244             AndroidKeyStorePublicKey androidKeyStorePublicKey =
245                     (AndroidKeyStorePublicKey) keyStoreKey;
246             byte[] certBytes = androidKeyStorePublicKey.getCertificate();
247             X509Certificate cert = getCertificate(certBytes);
248             // If the leaf certificate is null, then a chain should not exist either
249             if (cert == null) {
250                 return Collections.emptyList();
251             }
252             List<X509Certificate> result = new ArrayList<>();
253             result.add(cert);
254             byte[] certificateChain = androidKeyStorePublicKey.getCertificateChain();
255             Collection<X509Certificate> certificates = getCertificates(certificateChain);
256             result.addAll(certificates);
257             return result;
258         } else {
259             Log.e(TAG, "keyStoreKey is not of the expected type: " + keyStoreKey);
260         }
261         return Collections.emptyList();
262     }
263 
264     /**
265      * Returns an {@link X509Certificate} instance from the provided {@code certificate} byte
266      * encoding of the certificate, or null if the provided encoding is null.
267      */
getCertificate(byte[] certificate)268     private static X509Certificate getCertificate(byte[] certificate) {
269         X509Certificate result = null;
270         if (certificate != null) {
271             try {
272                 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
273                 result = (X509Certificate) certificateFactory.generateCertificate(
274                         new ByteArrayInputStream(certificate));
275             } catch (Exception e) {
276                 Log.e(TAG, "Caught an exception parsing the certificate: ", e);
277             }
278         }
279         return result;
280     }
281 
282     /**
283      * Returns a {@link Collection} of {@link X509Certificate} instances from the provided
284      * {@code certificateChain} byte encoding of the certificates, or null if the provided
285      * encoding is null.
286      */
getCertificates(byte[] certificateChain)287     private static Collection<X509Certificate> getCertificates(byte[] certificateChain) {
288         if (certificateChain != null) {
289             try {
290                 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
291                 Collection<X509Certificate> certificates =
292                         (Collection<X509Certificate>) certificateFactory.generateCertificates(
293                                 new ByteArrayInputStream(certificateChain));
294                 if (certificates == null) {
295                     Log.e(TAG, "Received null certificates from a non-null certificateChain");
296                     return Collections.emptyList();
297                 }
298                 return certificates;
299             } catch (Exception e) {
300                 Log.e(TAG, "Caught an exception parsing the certs: ", e);
301             }
302         }
303         return Collections.emptyList();
304     }
305 
306     /** @hide */
307     @Retention(RetentionPolicy.SOURCE)
308     @IntDef(value = {MODULE_HASH})
309     public @interface SupplementaryAttestationInfoTagEnum {}
310 
311     /**
312      * When passed into getSupplementaryAttestationInfo, getSupplementaryAttestationInfo returns the
313      * DER-encoded structure corresponding to the `Modules` schema described in the KeyMint HAL's
314      * KeyCreationResult.aidl. The SHA-256 hash of this encoded structure is what's included with
315      * the tag in attestations. To ensure the returned encoded structure is the one attested to,
316      * clients should verify its SHA-256 hash matches the one in the attestation. Note that the
317      * returned structure can vary between boots.
318      */
319     // TODO(b/380020528): Replace with Tag.MODULE_HASH when KeyMint V4 is frozen.
320     public static final int MODULE_HASH = TagType.BYTES | 724;
321 
322     /**
323      * Returns tag-specific data required to interpret a tag's attested value.
324      *
325      * When performing key attestation, the obtained attestation certificate contains a list of tags
326      * and their corresponding attested values. For some tags, additional information about the
327      * attested value can be queried via this API. See individual tags for specifics.
328      *
329      * @param tag tag for which info is being requested
330      * @return tag-specific info
331      * @throws KeyStoreException if the requested info is not available
332      */
333     @FlaggedApi(android.security.keystore2.Flags.FLAG_ATTEST_MODULES)
getSupplementaryAttestationInfo( @upplementaryAttestationInfoTagEnum int tag)334     public @NonNull byte[] getSupplementaryAttestationInfo(
335             @SupplementaryAttestationInfoTagEnum int tag) throws KeyStoreException {
336         return mKeyStore2.getSupplementaryAttestationInfo(tag);
337     }
338 
339     /**
340      * Returns a new {@link KeyDescriptor} instance in the app domain / namespace with the {@code
341      * alias} set to the provided value.
342      */
createKeyDescriptorFromAlias(String alias)343     private static KeyDescriptor createKeyDescriptorFromAlias(String alias) {
344         KeyDescriptor keyDescriptor = new KeyDescriptor();
345         keyDescriptor.domain = Domain.APP;
346         keyDescriptor.nspace = KeyProperties.NAMESPACE_APPLICATION;
347         keyDescriptor.alias = alias;
348         keyDescriptor.blob = null;
349         return keyDescriptor;
350     }
351 
352     /**
353      * Returns a new {@link KeyDescriptor} instance in the provided {@code domain} with the nspace
354      * field set to the provided {@code id}.
355      */
createKeyDescriptorFromId(long id, int domain)356     private static KeyDescriptor createKeyDescriptorFromId(long id, int domain) {
357         KeyDescriptor keyDescriptor = new KeyDescriptor();
358         keyDescriptor.domain = domain;
359         keyDescriptor.nspace = id;
360         keyDescriptor.alias = null;
361         keyDescriptor.blob = null;
362         return keyDescriptor;
363     }
364 }
365