• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2019 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.identity;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 
23 import java.io.ByteArrayInputStream;
24 import java.io.IOException;
25 import java.nio.ByteBuffer;
26 import java.security.InvalidAlgorithmParameterException;
27 import java.security.InvalidKeyException;
28 import java.security.KeyPair;
29 import java.security.KeyStore;
30 import java.security.KeyStoreException;
31 import java.security.NoSuchAlgorithmException;
32 import java.security.PrivateKey;
33 import java.security.PublicKey;
34 import java.security.UnrecoverableKeyException;
35 import java.security.cert.Certificate;
36 import java.security.cert.CertificateEncodingException;
37 import java.security.cert.CertificateException;
38 import java.security.cert.CertificateFactory;
39 import java.security.cert.X509Certificate;
40 import java.time.Instant;
41 import java.util.Collection;
42 import java.util.LinkedList;
43 import java.util.Map;
44 
45 import javax.crypto.BadPaddingException;
46 import javax.crypto.Cipher;
47 import javax.crypto.IllegalBlockSizeException;
48 import javax.crypto.KeyAgreement;
49 import javax.crypto.NoSuchPaddingException;
50 import javax.crypto.SecretKey;
51 import javax.crypto.spec.GCMParameterSpec;
52 import javax.crypto.spec.SecretKeySpec;
53 
54 class CredstoreIdentityCredential extends IdentityCredential {
55 
56     private static final String TAG = "CredstoreIdentityCredential";
57     private String mCredentialName;
58     private @IdentityCredentialStore.Ciphersuite int mCipherSuite;
59     private Context mContext;
60     private ICredential mBinder;
61 
CredstoreIdentityCredential(Context context, String credentialName, @IdentityCredentialStore.Ciphersuite int cipherSuite, ICredential binder)62     CredstoreIdentityCredential(Context context, String credentialName,
63             @IdentityCredentialStore.Ciphersuite int cipherSuite,
64             ICredential binder) {
65         mContext = context;
66         mCredentialName = credentialName;
67         mCipherSuite = cipherSuite;
68         mBinder = binder;
69     }
70 
71     private KeyPair mEphemeralKeyPair = null;
72     private SecretKey mSecretKey = null;
73     private SecretKey mReaderSecretKey = null;
74     private int mEphemeralCounter;
75     private int mReadersExpectedEphemeralCounter;
76 
ensureEphemeralKeyPair()77     private void ensureEphemeralKeyPair() {
78         if (mEphemeralKeyPair != null) {
79             return;
80         }
81         try {
82             // This PKCS#12 blob is generated in credstore, using BoringSSL.
83             //
84             // The main reason for this convoluted approach and not just sending the decomposed
85             // key-pair is that this would require directly using (device-side) BouncyCastle which
86             // is tricky due to various API hiding efforts. So instead we have credstore generate
87             // this PKCS#12 blob. The blob is encrypted with no password (sadly, also, BoringSSL
88             // doesn't support not using encryption when building a PKCS#12 blob).
89             //
90             byte[] pkcs12 = mBinder.createEphemeralKeyPair();
91             String alias = "ephemeralKey";
92             char[] password = {};
93 
94             KeyStore ks = KeyStore.getInstance("PKCS12");
95             ByteArrayInputStream bais = new ByteArrayInputStream(pkcs12);
96             ks.load(bais, password);
97             PrivateKey privKey = (PrivateKey) ks.getKey(alias, password);
98 
99             Certificate cert = ks.getCertificate(alias);
100             PublicKey pubKey = cert.getPublicKey();
101 
102             mEphemeralKeyPair = new KeyPair(pubKey, privKey);
103         } catch (android.os.RemoteException e) {
104             throw new RuntimeException("Unexpected RemoteException ", e);
105         } catch (android.os.ServiceSpecificException e) {
106             throw new RuntimeException("Unexpected ServiceSpecificException with code "
107                     + e.errorCode, e);
108         } catch (KeyStoreException
109                 | CertificateException
110                 | UnrecoverableKeyException
111                 | NoSuchAlgorithmException
112                 | IOException e) {
113             throw new RuntimeException("Unexpected exception ", e);
114         }
115     }
116 
117     @Override
createEphemeralKeyPair()118     public @NonNull KeyPair createEphemeralKeyPair() {
119         ensureEphemeralKeyPair();
120         return mEphemeralKeyPair;
121     }
122 
123     @Override
setReaderEphemeralPublicKey(@onNull PublicKey readerEphemeralPublicKey)124     public void setReaderEphemeralPublicKey(@NonNull PublicKey readerEphemeralPublicKey)
125             throws InvalidKeyException {
126         try {
127             byte[] uncompressedForm =
128                     Util.publicKeyEncodeUncompressedForm(readerEphemeralPublicKey);
129             mBinder.setReaderEphemeralPublicKey(uncompressedForm);
130         } catch (android.os.RemoteException e) {
131             throw new RuntimeException("Unexpected RemoteException ", e);
132         } catch (android.os.ServiceSpecificException e) {
133             throw new RuntimeException("Unexpected ServiceSpecificException with code "
134                     + e.errorCode, e);
135         }
136 
137         ensureEphemeralKeyPair();
138 
139         try {
140             KeyAgreement ka = KeyAgreement.getInstance("ECDH");
141             ka.init(mEphemeralKeyPair.getPrivate());
142             ka.doPhase(readerEphemeralPublicKey, true);
143             byte[] sharedSecret = ka.generateSecret();
144 
145             byte[] salt = new byte[1];
146             byte[] info = new byte[0];
147 
148             salt[0] = 0x01;
149             byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32);
150             mSecretKey = new SecretKeySpec(derivedKey, "AES");
151 
152             salt[0] = 0x00;
153             derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32);
154             mReaderSecretKey = new SecretKeySpec(derivedKey, "AES");
155 
156             mEphemeralCounter = 1;
157             mReadersExpectedEphemeralCounter = 1;
158 
159         } catch (NoSuchAlgorithmException e) {
160             throw new RuntimeException("Error performing key agreement", e);
161         }
162     }
163 
164     @Override
encryptMessageToReader(@onNull byte[] messagePlaintext)165     public @NonNull byte[] encryptMessageToReader(@NonNull byte[] messagePlaintext) {
166         byte[] messageCiphertextAndAuthTag = null;
167         try {
168             ByteBuffer iv = ByteBuffer.allocate(12);
169             iv.putInt(0, 0x00000000);
170             iv.putInt(4, 0x00000001);
171             iv.putInt(8, mEphemeralCounter);
172             Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
173             GCMParameterSpec encryptionParameterSpec = new GCMParameterSpec(128, iv.array());
174             cipher.init(Cipher.ENCRYPT_MODE, mSecretKey, encryptionParameterSpec);
175             messageCiphertextAndAuthTag = cipher.doFinal(messagePlaintext);
176         } catch (BadPaddingException
177                 | IllegalBlockSizeException
178                 | NoSuchPaddingException
179                 | InvalidKeyException
180                 | NoSuchAlgorithmException
181                 | InvalidAlgorithmParameterException e) {
182             throw new RuntimeException("Error encrypting message", e);
183         }
184         mEphemeralCounter += 1;
185         return messageCiphertextAndAuthTag;
186     }
187 
188     @Override
decryptMessageFromReader(@onNull byte[] messageCiphertext)189     public @NonNull byte[] decryptMessageFromReader(@NonNull byte[] messageCiphertext)
190             throws MessageDecryptionException {
191         ByteBuffer iv = ByteBuffer.allocate(12);
192         iv.putInt(0, 0x00000000);
193         iv.putInt(4, 0x00000000);
194         iv.putInt(8, mReadersExpectedEphemeralCounter);
195         byte[] plainText = null;
196         try {
197             final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
198             cipher.init(Cipher.DECRYPT_MODE, mReaderSecretKey,
199                     new GCMParameterSpec(128, iv.array()));
200             plainText = cipher.doFinal(messageCiphertext);
201         } catch (BadPaddingException
202                 | IllegalBlockSizeException
203                 | InvalidAlgorithmParameterException
204                 | InvalidKeyException
205                 | NoSuchAlgorithmException
206                 | NoSuchPaddingException e) {
207             throw new MessageDecryptionException("Error decrypting message", e);
208         }
209         mReadersExpectedEphemeralCounter += 1;
210         return plainText;
211     }
212 
213     @Override
getCredentialKeyCertificateChain()214     public @NonNull Collection<X509Certificate> getCredentialKeyCertificateChain() {
215         try {
216             byte[] certsBlob = mBinder.getCredentialKeyCertificateChain();
217             ByteArrayInputStream bais = new ByteArrayInputStream(certsBlob);
218 
219             Collection<? extends Certificate> certs = null;
220             try {
221                 CertificateFactory factory = CertificateFactory.getInstance("X.509");
222                 certs = factory.generateCertificates(bais);
223             } catch (CertificateException e) {
224                 throw new RuntimeException("Error decoding certificates", e);
225             }
226 
227             LinkedList<X509Certificate> x509Certs = new LinkedList<>();
228             for (Certificate cert : certs) {
229                 x509Certs.add((X509Certificate) cert);
230             }
231             return x509Certs;
232         } catch (android.os.RemoteException e) {
233             throw new RuntimeException("Unexpected RemoteException ", e);
234         } catch (android.os.ServiceSpecificException e) {
235             throw new RuntimeException("Unexpected ServiceSpecificException with code "
236                     + e.errorCode, e);
237         }
238     }
239 
240     private boolean mAllowUsingExhaustedKeys = true;
241     private boolean mAllowUsingExpiredKeys = false;
242 
243     @Override
setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys)244     public void setAllowUsingExhaustedKeys(boolean allowUsingExhaustedKeys) {
245         mAllowUsingExhaustedKeys = allowUsingExhaustedKeys;
246     }
247 
248     @Override
setAllowUsingExpiredKeys(boolean allowUsingExpiredKeys)249     public void setAllowUsingExpiredKeys(boolean allowUsingExpiredKeys) {
250         mAllowUsingExpiredKeys = allowUsingExpiredKeys;
251     }
252 
253     private boolean mOperationHandleSet = false;
254     private long mOperationHandle = 0;
255 
256     /**
257      * Called by android.hardware.biometrics.CryptoObject#getOpId() to get an
258      * operation handle.
259      *
260      * @hide
261      */
262     @Override
getCredstoreOperationHandle()263     public long getCredstoreOperationHandle() {
264         if (!mOperationHandleSet) {
265             try {
266                 mOperationHandle = mBinder.selectAuthKey(mAllowUsingExhaustedKeys,
267                         mAllowUsingExpiredKeys);
268                 mOperationHandleSet = true;
269             } catch (android.os.RemoteException e) {
270                 throw new RuntimeException("Unexpected RemoteException ", e);
271             } catch (android.os.ServiceSpecificException e) {
272                 if (e.errorCode == ICredentialStore.ERROR_NO_AUTHENTICATION_KEY_AVAILABLE) {
273                     // The NoAuthenticationKeyAvailableException will be thrown when
274                     // the caller proceeds to call getEntries().
275                 }
276                 throw new RuntimeException("Unexpected ServiceSpecificException with code "
277                         + e.errorCode, e);
278             }
279         }
280         return mOperationHandle;
281     }
282 
283     @NonNull
284     @Override
getEntries( @ullable byte[] requestMessage, @NonNull Map<String, Collection<String>> entriesToRequest, @Nullable byte[] sessionTranscript, @Nullable byte[] readerSignature)285     public ResultData getEntries(
286             @Nullable byte[] requestMessage,
287             @NonNull Map<String, Collection<String>> entriesToRequest,
288             @Nullable byte[] sessionTranscript,
289             @Nullable byte[] readerSignature)
290             throws SessionTranscriptMismatchException, NoAuthenticationKeyAvailableException,
291             InvalidReaderSignatureException, EphemeralPublicKeyNotFoundException,
292             InvalidRequestMessageException {
293 
294         RequestNamespaceParcel[] rnsParcels = new RequestNamespaceParcel[entriesToRequest.size()];
295         int n = 0;
296         for (String namespaceName : entriesToRequest.keySet()) {
297             Collection<String> entryNames = entriesToRequest.get(namespaceName);
298             rnsParcels[n] = new RequestNamespaceParcel();
299             rnsParcels[n].namespaceName = namespaceName;
300             rnsParcels[n].entries = new RequestEntryParcel[entryNames.size()];
301             int m = 0;
302             for (String entryName : entryNames) {
303                 rnsParcels[n].entries[m] = new RequestEntryParcel();
304                 rnsParcels[n].entries[m].name = entryName;
305                 m++;
306             }
307             n++;
308         }
309 
310         GetEntriesResultParcel resultParcel = null;
311         try {
312             resultParcel = mBinder.getEntries(
313                 requestMessage != null ? requestMessage : new byte[0],
314                 rnsParcels,
315                 sessionTranscript != null ? sessionTranscript : new byte[0],
316                 readerSignature != null ? readerSignature : new byte[0],
317                 mAllowUsingExhaustedKeys,
318                 mAllowUsingExpiredKeys);
319         } catch (android.os.RemoteException e) {
320             throw new RuntimeException("Unexpected RemoteException ", e);
321         } catch (android.os.ServiceSpecificException e) {
322             if (e.errorCode == ICredentialStore.ERROR_EPHEMERAL_PUBLIC_KEY_NOT_FOUND) {
323                 throw new EphemeralPublicKeyNotFoundException(e.getMessage(), e);
324             } else if (e.errorCode == ICredentialStore.ERROR_INVALID_READER_SIGNATURE) {
325                 throw new InvalidReaderSignatureException(e.getMessage(), e);
326             } else if (e.errorCode == ICredentialStore.ERROR_NO_AUTHENTICATION_KEY_AVAILABLE) {
327                 throw new NoAuthenticationKeyAvailableException(e.getMessage(), e);
328             } else if (e.errorCode == ICredentialStore.ERROR_INVALID_ITEMS_REQUEST_MESSAGE) {
329                 throw new InvalidRequestMessageException(e.getMessage(), e);
330             } else if (e.errorCode == ICredentialStore.ERROR_SESSION_TRANSCRIPT_MISMATCH) {
331                 throw new SessionTranscriptMismatchException(e.getMessage(), e);
332             } else {
333                 throw new RuntimeException("Unexpected ServiceSpecificException with code "
334                         + e.errorCode, e);
335             }
336         }
337 
338         byte[] mac = resultParcel.mac;
339         if (mac != null && mac.length == 0) {
340             mac = null;
341         }
342         CredstoreResultData.Builder resultDataBuilder = new CredstoreResultData.Builder(
343                 resultParcel.staticAuthenticationData, resultParcel.deviceNameSpaces, mac);
344 
345         for (ResultNamespaceParcel resultNamespaceParcel : resultParcel.resultNamespaces) {
346             for (ResultEntryParcel resultEntryParcel : resultNamespaceParcel.entries) {
347                 if (resultEntryParcel.status == ICredential.STATUS_OK) {
348                     resultDataBuilder.addEntry(resultNamespaceParcel.namespaceName,
349                             resultEntryParcel.name, resultEntryParcel.value);
350                 } else {
351                     resultDataBuilder.addErrorStatus(resultNamespaceParcel.namespaceName,
352                             resultEntryParcel.name,
353                             resultEntryParcel.status);
354                 }
355             }
356         }
357         return resultDataBuilder.build();
358     }
359 
360     @Override
setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey)361     public void setAvailableAuthenticationKeys(int keyCount, int maxUsesPerKey) {
362         try {
363             mBinder.setAvailableAuthenticationKeys(keyCount, maxUsesPerKey);
364         } catch (android.os.RemoteException e) {
365             throw new RuntimeException("Unexpected RemoteException ", e);
366         } catch (android.os.ServiceSpecificException e) {
367             throw new RuntimeException("Unexpected ServiceSpecificException with code "
368                     + e.errorCode, e);
369         }
370     }
371 
372     @Override
getAuthKeysNeedingCertification()373     public @NonNull Collection<X509Certificate> getAuthKeysNeedingCertification() {
374         try {
375             AuthKeyParcel[] authKeyParcels = mBinder.getAuthKeysNeedingCertification();
376             LinkedList<X509Certificate> x509Certs = new LinkedList<>();
377             CertificateFactory factory = CertificateFactory.getInstance("X.509");
378             for (AuthKeyParcel authKeyParcel : authKeyParcels) {
379                 Collection<? extends Certificate> certs = null;
380                 ByteArrayInputStream bais = new ByteArrayInputStream(authKeyParcel.x509cert);
381                 certs = factory.generateCertificates(bais);
382                 if (certs.size() != 1) {
383                     throw new RuntimeException("Returned blob yields more than one X509 cert");
384                 }
385                 X509Certificate authKeyCert = (X509Certificate) certs.iterator().next();
386                 x509Certs.add(authKeyCert);
387             }
388             return x509Certs;
389         } catch (CertificateException e) {
390             throw new RuntimeException("Error decoding authenticationKey", e);
391         } catch (android.os.RemoteException e) {
392             throw new RuntimeException("Unexpected RemoteException ", e);
393         } catch (android.os.ServiceSpecificException e) {
394             throw new RuntimeException("Unexpected ServiceSpecificException with code "
395                     + e.errorCode, e);
396         }
397     }
398 
399     @Override
storeStaticAuthenticationData(X509Certificate authenticationKey, byte[] staticAuthData)400     public void storeStaticAuthenticationData(X509Certificate authenticationKey,
401             byte[] staticAuthData)
402             throws UnknownAuthenticationKeyException {
403         try {
404             AuthKeyParcel authKeyParcel = new AuthKeyParcel();
405             authKeyParcel.x509cert = authenticationKey.getEncoded();
406             mBinder.storeStaticAuthenticationData(authKeyParcel, staticAuthData);
407         } catch (CertificateEncodingException e) {
408             throw new RuntimeException("Error encoding authenticationKey", e);
409         } catch (android.os.RemoteException e) {
410             throw new RuntimeException("Unexpected RemoteException ", e);
411         } catch (android.os.ServiceSpecificException e) {
412             if (e.errorCode == ICredentialStore.ERROR_AUTHENTICATION_KEY_NOT_FOUND) {
413                 throw new UnknownAuthenticationKeyException(e.getMessage(), e);
414             } else {
415                 throw new RuntimeException("Unexpected ServiceSpecificException with code "
416                         + e.errorCode, e);
417             }
418         }
419     }
420 
421     @Override
storeStaticAuthenticationData(X509Certificate authenticationKey, Instant expirationDate, byte[] staticAuthData)422     public void storeStaticAuthenticationData(X509Certificate authenticationKey,
423             Instant expirationDate,
424             byte[] staticAuthData)
425             throws UnknownAuthenticationKeyException {
426         try {
427             AuthKeyParcel authKeyParcel = new AuthKeyParcel();
428             authKeyParcel.x509cert = authenticationKey.getEncoded();
429             long millisSinceEpoch = (expirationDate.getEpochSecond() * 1000)
430                                     + (expirationDate.getNano() / 1000000);
431             mBinder.storeStaticAuthenticationDataWithExpiration(authKeyParcel,
432                     millisSinceEpoch, staticAuthData);
433         } catch (CertificateEncodingException e) {
434             throw new RuntimeException("Error encoding authenticationKey", e);
435         } catch (android.os.RemoteException e) {
436             throw new RuntimeException("Unexpected RemoteException ", e);
437         } catch (android.os.ServiceSpecificException e) {
438             if (e.errorCode == ICredentialStore.ERROR_NOT_SUPPORTED) {
439                 throw new UnsupportedOperationException("Not supported", e);
440             } else if (e.errorCode == ICredentialStore.ERROR_AUTHENTICATION_KEY_NOT_FOUND) {
441                 throw new UnknownAuthenticationKeyException(e.getMessage(), e);
442             } else {
443                 throw new RuntimeException("Unexpected ServiceSpecificException with code "
444                         + e.errorCode, e);
445             }
446         }
447     }
448 
449     @Override
getAuthenticationDataUsageCount()450     public @NonNull int[] getAuthenticationDataUsageCount() {
451         try {
452             int[] usageCount = mBinder.getAuthenticationDataUsageCount();
453             return usageCount;
454         } catch (android.os.RemoteException e) {
455             throw new RuntimeException("Unexpected RemoteException ", e);
456         } catch (android.os.ServiceSpecificException e) {
457             throw new RuntimeException("Unexpected ServiceSpecificException with code "
458                     + e.errorCode, e);
459         }
460     }
461 
462     @Override
proveOwnership(@onNull byte[] challenge)463     public @NonNull byte[] proveOwnership(@NonNull byte[] challenge) {
464         try {
465             byte[] proofOfOwnership = mBinder.proveOwnership(challenge);
466             return proofOfOwnership;
467         } catch (android.os.RemoteException e) {
468             throw new RuntimeException("Unexpected RemoteException ", e);
469         } catch (android.os.ServiceSpecificException e) {
470             if (e.errorCode == ICredentialStore.ERROR_NOT_SUPPORTED) {
471                 throw new UnsupportedOperationException("Not supported", e);
472             } else {
473                 throw new RuntimeException("Unexpected ServiceSpecificException with code "
474                         + e.errorCode, e);
475             }
476         }
477     }
478 
479     @Override
delete(@onNull byte[] challenge)480     public @NonNull byte[] delete(@NonNull byte[] challenge) {
481         try {
482             byte[] proofOfDeletion = mBinder.deleteWithChallenge(challenge);
483             return proofOfDeletion;
484         } catch (android.os.RemoteException e) {
485             throw new RuntimeException("Unexpected RemoteException ", e);
486         } catch (android.os.ServiceSpecificException e) {
487             throw new RuntimeException("Unexpected ServiceSpecificException with code "
488                     + e.errorCode, e);
489         }
490     }
491 
492     @Override
update(@onNull PersonalizationData personalizationData)493     public @NonNull byte[] update(@NonNull PersonalizationData personalizationData) {
494         try {
495             IWritableCredential binder = mBinder.update();
496             byte[] proofOfProvision =
497                     CredstoreWritableIdentityCredential.personalize(binder, personalizationData);
498             return proofOfProvision;
499         } catch (android.os.RemoteException e) {
500             throw new RuntimeException("Unexpected RemoteException ", e);
501         } catch (android.os.ServiceSpecificException e) {
502             throw new RuntimeException("Unexpected ServiceSpecificException with code "
503                     + e.errorCode, e);
504         }
505     }
506 }
507