2222// THE SOFTWARE.
2323package com .microsoft .identity .common .crypto ;
2424
25- import android .annotation .TargetApi ;
2625import android .content .Context ;
2726import android .os .Build ;
2827import android .security .KeyPairGeneratorSpec ;
3231import androidx .annotation .RequiresApi ;
3332
3433import com .microsoft .identity .common .internal .util .AndroidKeyStoreUtil ;
34+ import com .microsoft .identity .common .java .controllers .ExceptionAdapter ;
3535import com .microsoft .identity .common .java .crypto .key .AES256KeyLoader ;
3636import com .microsoft .identity .common .java .crypto .key .KeyUtil ;
3737import com .microsoft .identity .common .java .exception .ClientException ;
4343import com .microsoft .identity .common .java .opentelemetry .SpanName ;
4444import com .microsoft .identity .common .java .util .CachedData ;
4545import com .microsoft .identity .common .java .util .FileUtil ;
46+ import com .microsoft .identity .common .java .util .StringUtil ;
4647import com .microsoft .identity .common .logging .Logger ;
4748
4849import java .io .File ;
4950import java .math .BigInteger ;
5051import java .security .KeyPair ;
5152import java .security .KeyPairGenerator ;
5253import java .security .KeyStore ;
53- import java .security .ProviderException ;
5454import java .security .spec .AlgorithmParameterSpec ;
5555import java .util .Calendar ;
5656import java .util .Locale ;
@@ -258,83 +258,153 @@ private void saveSecretKeyToStorage(@NonNull final SecretKey unencryptedKey) thr
258258 KeyPair keyPair = AndroidKeyStoreUtil .readKey (mAlias );
259259 if (keyPair == null ) {
260260 Logger .info (methodTag , "No existing keypair. Generating a new one." );
261- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .P && CommonFlightsManager .INSTANCE .getFlightsProvider ().isFlightEnabled (CommonFlight .ENABLE_NEW_KEY_GEN_SPEC_FOR_WRAP )) {
262- final long keypairGenStartTime = System .currentTimeMillis ();
263- final Span span = OTelUtility .createSpanFromParent (SpanName .KeyPairGeneration .name (), SpanExtension .current ().getSpanContext ());
264- try (final Scope scope = SpanExtension .makeCurrentSpan (span )) {
265- keyPair = attemptKeyPairGeneration (mAlias , true , keypairGenStartTime );
266- Logger .info (methodTag , "Successfully generated keypair with new KeyPairGeneratorSpec with wrap purpose." );
267- span .setAttribute (AttributeName .key_pair_gen_successful_method .name (), "new_key_gen_spec_with_wrap" );
268- span .setStatus (StatusCode .OK );
269- } catch (final ProviderException e ) {
270- if ("SecureKeyImportUnavailableException" .equals (e .getClass ().getSimpleName ())) {
271- Logger .warn (methodTag , "Wrap purpose may not be supported. Retrying without wrap." );
272- try {
273- keyPair = attemptKeyPairGeneration (mAlias , false , keypairGenStartTime );
274- Logger .info (methodTag , "Successfully generated keypair with new KeyPairGeneratorSpec without wrap purpose." );
275- span .setAttribute (AttributeName .key_pair_gen_successful_method .name (), "new_key_gen_spec_without_wrap" );
276- span .setStatus (StatusCode .OK );
277- } catch (final Exception ex ) {
278- // 2nd fallback to legacy keygen spec
279- Logger .warn (methodTag , "Second attempt without wrap also failed. Falling back to legacy spec." + ex );
280- keyPair = generateKeyPairWithLegacySpec (mAlias , keypairGenStartTime );
281- if (e .getMessage () != null ) {
282- span .setAttribute (AttributeName .keypair_gen_exception .name (), e .getMessage ());
283- }
284- span .setAttribute (AttributeName .key_pair_gen_successful_method .name (), "legacy_key_gen_spec" );
285- span .setStatus (StatusCode .OK );
286- }
287- } else {
288- Logger .warn (methodTag , "Some unknown exception occurred. Running legacy keygen spec logic." + e );
289- keyPair = generateKeyPairWithLegacySpec (mAlias , keypairGenStartTime );
290- span .setAttribute (AttributeName .key_pair_gen_successful_method .name (), "legacy_key_gen_spec" );
291- span .setStatus (StatusCode .OK );
292- }
293- } catch (final Throwable throwable ) {
294- Logger .warn (methodTag , "Unexpected error with new KeyPairGeneratorSpec. Falling back to legacy spec. " + throwable );
295- keyPair = generateKeyPairWithLegacySpec (mAlias , keypairGenStartTime );
296- if (throwable .getMessage () != null ) {
297- span .setAttribute (AttributeName .keypair_gen_exception .name (), throwable .getMessage ());
298- }
299- span .setAttribute (AttributeName .key_pair_gen_successful_method .name (), "legacy_key_gen_spec" );
300- span .setStatus (StatusCode .OK );
301- } finally {
302- span .end ();
303- }
304- }
305- else {
306- // If flight for using new keygen spec is not enabled, use the legacy spec.
307- Logger .info (methodTag , "Using legacy spec for keypair generation directly." );
308- keyPair = AndroidKeyStoreUtil .generateKeyPair (
309- WRAP_KEY_ALGORITHM , getLegacySpecForKeyStoreKey (mContext , mAlias ));
261+ final Span span = OTelUtility .createSpanFromParent (SpanName .KeyPairGeneration .name (), SpanExtension .current ().getSpanContext ());
262+ try (final Scope scope = SpanExtension .makeCurrentSpan (span )) {
263+ keyPair = generateNewKeyPair ();
264+ span .setStatus (StatusCode .OK );
265+ } catch (final ClientException e ) {
266+ span .setStatus (StatusCode .ERROR );
267+ span .recordException (e );
268+ throw e ;
269+ } finally {
270+ span .end ();
310271 }
311272 }
312273 final byte [] keyWrapped = AndroidKeyStoreUtil .wrap (unencryptedKey , keyPair , WRAP_ALGORITHM );
313274 FileUtil .writeDataToFile (keyWrapped , getKeyFile ());
314275 }
315276
316- @ RequiresApi (api = Build .VERSION_CODES .P )
317- private KeyPair attemptKeyPairGeneration (@ NonNull final String alias , boolean useWrapPurpose , long keypairGenStartTime ) throws ClientException {
318- KeyPair keyPair = AndroidKeyStoreUtil .generateKeyPair (
319- WRAP_KEY_ALGORITHM , getSpecForKeyStoreKey (alias , useWrapPurpose ));
320- recordKeyGenerationTime (keypairGenStartTime );
321- return keyPair ;
277+ /**
278+ * Generate a new key pair wrapping key, based on API level uses different spec to generate
279+ * the key pair.
280+ * @return a key pair
281+ */
282+ @ NonNull
283+ private KeyPair generateNewKeyPair () throws ClientException {
284+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .P ) {
285+ return generateNewKeyPairAPI28AndAbove ();
286+ } else if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
287+ return generateNewKeyPairAPI23AndAbove ();
288+ } else {
289+ return generateKeyPairWithLegacySpec ();
290+ }
322291 }
323292
324- private KeyPair generateKeyPairWithLegacySpec (@ NonNull final String alias , long keypairGenStartTime ) throws ClientException {
293+ /**
294+ * Call this for API level >= 28. Starting level API 28 PURPOSE_WRAP_KEY is added. Based on flights
295+ * this method may or may not use the PURPOSE_WRAP_KEY along with PURPOSE_ENCRYPT and PURPOSE_DECRYPT. The logic
296+ * if (wrap key flight enabled) use all three purposes
297+ * else if (new key gen flight enabled) use only encrypt and decrypt purposes
298+ * else use legacy spec.
299+ * @return key pair
300+ */
301+ @ RequiresApi (Build .VERSION_CODES .P )
302+ @ NonNull
303+ private KeyPair generateNewKeyPairAPI28AndAbove () throws ClientException {
304+ if (CommonFlightsManager .INSTANCE .getFlightsProvider ().isFlightEnabled (CommonFlight .ENABLE_NEW_KEY_GEN_SPEC_FOR_WRAP_WITH_PURPOSE_WRAP_KEY )) {
305+ return generateWrappingKeyPair_WithPurposeWrapKey ();
306+ } else if (CommonFlightsManager .INSTANCE .getFlightsProvider ().isFlightEnabled (CommonFlight .ENABLE_NEW_KEY_GEN_SPEC_FOR_WRAP_WITHOUT_PURPOSE_WRAP_KEY )) {
307+ return generateWrappingKeyPair ();
308+ } else {
309+ return generateKeyPairWithLegacySpec ();
310+ }
311+ }
312+
313+ /**
314+ * Call this for API level >= 23. Based on flight new key gen spec is used else legacy which
315+ * is deprecated starting API 23.
316+ * @return key pair
317+ */
318+ @ RequiresApi (Build .VERSION_CODES .M )
319+ @ NonNull
320+ private KeyPair generateNewKeyPairAPI23AndAbove () throws ClientException {
321+ if (CommonFlightsManager .INSTANCE .getFlightsProvider ().isFlightEnabled (CommonFlight .ENABLE_NEW_KEY_GEN_SPEC_FOR_WRAP_WITHOUT_PURPOSE_WRAP_KEY )) {
322+ return generateWrappingKeyPair ();
323+ } else {
324+ return generateKeyPairWithLegacySpec ();
325+ }
326+ }
327+
328+ /**
329+ * Generate a new key pair wrapping key based on legacy logic. Call this for API < 23 or as fallback
330+ * until new key gen specs are stable.
331+ * @return key pair generated with legacy spec
332+ * @throws ClientException
333+ */
334+ @ NonNull
335+ private KeyPair generateKeyPairWithLegacySpec () throws ClientException {
336+ final Span span = SpanExtension .current ();
325337 try {
326- final KeyPair keyPair = AndroidKeyStoreUtil . generateKeyPair (
327- WRAP_KEY_ALGORITHM , getLegacySpecForKeyStoreKey ( mContext , alias ) );
328- recordKeyGenerationTime ( keypairGenStartTime );
338+ final AlgorithmParameterSpec keyPairGenSpec = getLegacySpecForKeyStoreKey ();
339+ final KeyPair keyPair = attemptKeyPairGeneration ( keyPairGenSpec );
340+ span . setAttribute ( AttributeName . key_pair_gen_successful_method . name (), "legacy_key_gen_spec" );
329341 return keyPair ;
330- } catch (final ClientException e ) {
331- SpanExtension .current ().recordException (e );
332- SpanExtension .current ().setStatus (StatusCode .ERROR );
342+ } catch (final Throwable e ) {
333343 Logger .error (TAG + ":generateKeyPairWithLegacySpec" , "Error generating keypair with legacy spec." , e );
334- throw e ;
344+ throw ExceptionAdapter . clientExceptionFromException ( e ) ;
335345 }
336346 }
337347
348+ /**
349+ * Generate a new key pair wrapping key, based on API level >= 28. This method uses new key gen spec
350+ * with PURPOSE_WRAP_KEY. If this fails, it will fallback to generateWrappingKeyPair() which does not use
351+ * PURPOSE_WRAP_KEY (still uses new key gen spec).
352+ */
353+ @ RequiresApi (Build .VERSION_CODES .P )
354+ private KeyPair generateWrappingKeyPair_WithPurposeWrapKey () throws ClientException {
355+ final String methodTag = TAG + ":generateWrappingKeyPair_WithPurposeWrapKey" ;
356+ final Span span = SpanExtension .current ();
357+ try {
358+ Logger .info (methodTag , "Generating new keypair with new spec with purpose_wrap_key" );
359+ int purposes = KeyProperties .PURPOSE_ENCRYPT | KeyProperties .PURPOSE_DECRYPT | KeyProperties .PURPOSE_WRAP_KEY ;
360+ final AlgorithmParameterSpec keyPairGenSpec = getSpecForWrappingKey (purposes );
361+ final KeyPair keyPair = attemptKeyPairGeneration (keyPairGenSpec );
362+ span .setAttribute (AttributeName .key_pair_gen_successful_method .name (), "new_key_gen_spec_with_wrap" );
363+ return keyPair ;
364+ } catch (final Throwable e ) {
365+ Logger .error (methodTag , "Error generating keypair with new spec with purpose_wrap_key." +
366+ "Attempting without purpose_wrap_key." , e );
367+ if (!StringUtil .isNullOrEmpty (e .getMessage ())) {
368+ span .setAttribute (AttributeName .keypair_gen_exception .name (), e .getMessage ());
369+ }
370+ return generateWrappingKeyPair ();
371+ }
372+ }
373+
374+ /**
375+ * Generate a new key pair wrapping key, based on API level >= 23. This method uses new key gen spec
376+ * with purposes PURPOSE_ENCRYPT and PURPOSE_DECRYPT. If this fails, it will fallback to generateKeyPairWithLegacySpec()
377+ * which uses olg key gen spec.
378+ */
379+ @ RequiresApi (Build .VERSION_CODES .M )
380+ private KeyPair generateWrappingKeyPair () throws ClientException {
381+ final String methodTag = TAG + ":generateWrappingKeyPair" ;
382+ final Span span = SpanExtension .current ();
383+ try {
384+ Logger .info (methodTag , "Generating new keypair with new spec without wrap key" );
385+ int purposes = KeyProperties .PURPOSE_ENCRYPT | KeyProperties .PURPOSE_DECRYPT ;
386+ final AlgorithmParameterSpec keyPairGenSpec = getSpecForWrappingKey (purposes );
387+ final KeyPair keyPair = attemptKeyPairGeneration (keyPairGenSpec );
388+ span .setAttribute (AttributeName .key_pair_gen_successful_method .name (), "new_key_gen_spec_without_wrap" );
389+ return keyPair ;
390+ } catch (final Throwable e ) {
391+ Logger .error (methodTag , "Error generating keypair with new spec." +
392+ "Attempting with legacy spec." , e );
393+ if (!StringUtil .isNullOrEmpty (e .getMessage ())) {
394+ span .setAttribute (AttributeName .keypair_gen_exception .name (), e .getMessage ());
395+ }
396+ return generateKeyPairWithLegacySpec ();
397+ }
398+ }
399+
400+ private KeyPair attemptKeyPairGeneration (@ NonNull final AlgorithmParameterSpec keyPairGenSpec ) throws ClientException {
401+ final long keypairGenStartTime = System .currentTimeMillis ();
402+ final KeyPair keyPair = AndroidKeyStoreUtil .generateKeyPair (
403+ WRAP_KEY_ALGORITHM , keyPairGenSpec );
404+ recordKeyGenerationTime (keypairGenStartTime );
405+ return keyPair ;
406+ }
407+
338408 private void recordKeyGenerationTime (long keypairGenStartTime ) {
339409 long elapsedTime = System .currentTimeMillis () - keypairGenStartTime ;
340410 SpanExtension .current ().setAttribute (AttributeName .elapsed_time_keypair_generation .name (), elapsedTime );
@@ -353,47 +423,33 @@ public void deleteSecretKeyFromStorage() throws ClientException {
353423 /**
354424 * Generate a self-signed cert and derive an AlgorithmParameterSpec from that.
355425 * This is for the key to be generated in {@link KeyStore} via {@link KeyPairGenerator}
356- * Note : This is now only for API level < 28
357- *
358- * @param context an Android {@link Context} object.
426+ * Note : This is now only for API level < 23 or as fallback.
427+
359428 * @return a {@link AlgorithmParameterSpec} for the keystore key (that we'll use to wrap the secret key).
360429 */
361- private static AlgorithmParameterSpec getLegacySpecForKeyStoreKey (@ NonNull final Context context ,
362- @ NonNull final String alias ) {
430+ private AlgorithmParameterSpec getLegacySpecForKeyStoreKey () {
363431 // Generate a self-signed cert.
364432 final String certInfo = String .format (Locale .ROOT , "CN=%s, OU=%s" ,
365- alias ,
366- context .getPackageName ());
433+ mAlias ,
434+ mContext .getPackageName ());
367435
368436 final Calendar start = Calendar .getInstance ();
369437 final Calendar end = Calendar .getInstance ();
370438 final int certValidYears = 100 ;
371439 end .add (Calendar .YEAR , certValidYears );
372440
373- return new KeyPairGeneratorSpec .Builder (context )
374- .setAlias (alias )
441+ return new KeyPairGeneratorSpec .Builder (mContext )
442+ .setAlias (mAlias )
375443 .setSubject (new X500Principal (certInfo ))
376444 .setSerialNumber (BigInteger .ONE )
377445 .setStartDate (start .getTime ())
378446 .setEndDate (end .getTime ())
379447 .build ();
380448 }
381449
382- /**
383- * Generate a self-signed cert and derive an AlgorithmParameterSpec from that.
384- * This is for the key to be generated in {@link KeyStore} via {@link KeyPairGenerator}
385- *
386- * @param alias the alias for the key.
387- * @param tryPurposeWrap whether to try to use the wrap purpose in the key generation spec.
388- * @return a {@link AlgorithmParameterSpec} for the keystore key (that we'll use to wrap the secret key).
389- */
390- @ RequiresApi (api = Build .VERSION_CODES .P )
391- private static AlgorithmParameterSpec getSpecForKeyStoreKey (@ NonNull final String alias , boolean tryPurposeWrap ) {
392- int purposes = KeyProperties .PURPOSE_ENCRYPT | KeyProperties .PURPOSE_DECRYPT ;
393- if (tryPurposeWrap ) {
394- purposes |= KeyProperties .PURPOSE_WRAP_KEY ;
395- }
396- return new KeyGenParameterSpec .Builder (alias , purposes )
450+ @ RequiresApi (api = Build .VERSION_CODES .M )
451+ private AlgorithmParameterSpec getSpecForWrappingKey (int purposes ) {
452+ return new KeyGenParameterSpec .Builder (mAlias , purposes )
397453 .setKeySize (2048 )
398454 .setDigests (KeyProperties .DIGEST_SHA256 , KeyProperties .DIGEST_SHA512 )
399455 .setEncryptionPaddings (KeyProperties .ENCRYPTION_PADDING_RSA_PKCS1 )
0 commit comments