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