@@ -53,10 +53,11 @@ class CryptoUtil {
53
53
54
54
// Transformations available since API 18
55
55
// https://developer.android.com/training/articles/keystore.html#SupportedCiphers
56
- private static final String RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding " ;
56
+ private static final String RSA_TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding " ;
57
57
// https://developer.android.com/reference/javax/crypto/Cipher.html
58
58
@ SuppressWarnings ("SpellCheckingInspection" )
59
59
private static final String AES_TRANSFORMATION = "AES/GCM/NOPADDING" ;
60
+ private static final String OLD_PKCS1_RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding" ;
60
61
61
62
private static final String ANDROID_KEY_STORE = "AndroidKeyStore" ;
62
63
private static final String ALGORITHM_RSA = "RSA" ;
@@ -124,7 +125,8 @@ KeyStore.PrivateKeyEntry getRSAKeyEntry() throws CryptoException, IncompatibleDe
124
125
.setCertificateNotBefore (start .getTime ())
125
126
.setCertificateNotAfter (end .getTime ())
126
127
.setKeySize (RSA_KEY_SIZE )
127
- .setEncryptionPaddings (KeyProperties .ENCRYPTION_PADDING_RSA_PKCS1 )
128
+ .setEncryptionPaddings (KeyProperties .ENCRYPTION_PADDING_RSA_OAEP )
129
+ .setDigests (KeyProperties .DIGEST_SHA256 , KeyProperties .DIGEST_SHA1 )
128
130
.setBlockModes (KeyProperties .BLOCK_MODE_ECB )
129
131
.build ();
130
132
} else {
@@ -355,6 +357,7 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry
355
357
356
358
/**
357
359
* Attempts to recover the existing AES Key or generates a new one if none is found.
360
+ * Handles migration from PKCS1Padding-encrypted AES keys to OAEP-encrypted ones.
358
361
*
359
362
* @return a valid AES Key bytes
360
363
* @throws IncompatibleDeviceException in the event the device can't understand the cryptographic settings required
@@ -366,39 +369,114 @@ byte[] getAESKey() throws IncompatibleDeviceException, CryptoException {
366
369
if (TextUtils .isEmpty (encodedEncryptedAES )) {
367
370
encodedEncryptedAES = storage .retrieveString (OLD_KEY_ALIAS );
368
371
}
372
+ byte [] decryptedAESKey = null ;
373
+ boolean migrationNeeded = false ;
369
374
if (encodedEncryptedAES != null ) {
370
375
//Return existing key
371
- byte [] encryptedAES = Base64 .decode (encodedEncryptedAES , Base64 .DEFAULT );
372
- byte [] existingAES = RSADecrypt (encryptedAES );
373
- final int aesExpectedLengthInBytes = AES_KEY_SIZE / 8 ;
374
- //Prevent returning an 'Empty key' (invalid/corrupted) that was mistakenly saved
375
- if (existingAES != null && existingAES .length == aesExpectedLengthInBytes ) {
376
- //Key exists and has the right size
377
- return existingAES ;
376
+ byte [] encryptedAESBytes = Base64 .decode (encodedEncryptedAES , Base64 .DEFAULT );
377
+ try {
378
+ // Attempt 1: Decrypt with new OAEP configuration
379
+ Log .d (TAG , "Attempting to decrypt AES key with OAEP (" + RSA_TRANSFORMATION + ")." );
380
+ decryptedAESKey = RSADecrypt (encryptedAESBytes ); // RSADecrypt uses the new RSA_TRANSFORMATION (OAEP)
381
+ Log .d (TAG , "AES key successfully decrypted with OAEP." );
382
+ }catch (IncompatibleDeviceException e ){
383
+ Log .w (TAG , "Failed to decrypt AES key with OAEP due to " +
384
+ "IncompatibleDeviceException. Cause: "
385
+ + (e .getCause () != null ? e .getCause ().getClass ().getSimpleName () : "N/A" )
386
+ + ". Attempting PKCS1 fallback for migration." , e );
387
+ Throwable cause = e .getCause ();
388
+ if (cause instanceof InvalidKeyException ) { // Specifically if key was not compatible with OAEP
389
+ try {
390
+ // Attempt 2: Decrypt with old PKCS1Padding configuration
391
+ Log .d (TAG , "Attempting to decrypt AES key with PKCS1Padding " +
392
+ "(" + OLD_PKCS1_RSA_TRANSFORMATION +") for migration due to " +
393
+ "InvalidKeyException with OAEP." );
394
+ KeyStore .PrivateKeyEntry rsaKeyEntry = getRSAKeyEntry ();
395
+ Cipher rsaPkcs1Cipher = Cipher .getInstance (OLD_PKCS1_RSA_TRANSFORMATION );
396
+ rsaPkcs1Cipher .init (Cipher .DECRYPT_MODE , rsaKeyEntry .getPrivateKey ());
397
+ decryptedAESKey = rsaPkcs1Cipher .doFinal (encryptedAESBytes );
398
+ Log .i (TAG , "Successfully decrypted legacy AES key using PKCS1Padding. Migration is needed." );
399
+ migrationNeeded = true ;
400
+ } catch (Exception pkcs1Exception ) {
401
+ Log .e (TAG , "Failed to decrypt AES key with PKCS1Padding fallback." ,
402
+ pkcs1Exception );
403
+ decryptedAESKey = null ; // Ensure it's null after failed fallback
404
+ }
405
+ } else {
406
+ // Not an InvalidKeyException cause we're handling for PKCS1 fallback for IncompatibleDeviceException, so rethrow
407
+ throw e ;
408
+ }
409
+ }catch (CryptoException e ) {
410
+ Log .w (TAG , "Failed to decrypt AES key with OAEP. Cause: " +
411
+ (e .getCause () != null ? e .getCause ().getClass ().getSimpleName () : "N/A" ) +
412
+ ". Attempting PKCS1 fallback for migration." , e );
413
+ Throwable cause = e .getCause ();
414
+ if (cause instanceof BadPaddingException || cause instanceof
415
+ IllegalBlockSizeException || cause instanceof InvalidKeyException ) {
416
+ try {
417
+ // Attempt 2: Decrypt with old PKCS1Padding configuration
418
+ Log .d (TAG , "Attempting to decrypt AES key with PKCS1Padding (" +
419
+ OLD_PKCS1_RSA_TRANSFORMATION +") for migration." );
420
+ KeyStore .PrivateKeyEntry rsaKeyEntry = getRSAKeyEntry ();
421
+ Cipher rsaPkcs1Cipher = Cipher .getInstance (OLD_PKCS1_RSA_TRANSFORMATION );
422
+ rsaPkcs1Cipher .init (Cipher .DECRYPT_MODE , rsaKeyEntry .getPrivateKey ());
423
+ decryptedAESKey = rsaPkcs1Cipher .doFinal (encryptedAESBytes );
424
+ Log .i (TAG , "Successfully decrypted legacy AES key using PKCS1Padding."
425
+ + " Migration is needed." );
426
+ migrationNeeded = true ;
427
+ } catch (Exception pkcs1Exception ) {
428
+ Log .e (TAG , "Failed to decrypt AES key with PKCS1Padding fallback."
429
+ , pkcs1Exception );
430
+ decryptedAESKey = null ;
431
+ }
432
+ } else {
433
+ // Not a known migration-related exception cause from CryptoException, rethrow.
434
+ throw e ;
435
+ }
436
+ }
437
+ if (decryptedAESKey != null ) {
438
+ final int aesExpectedLengthInBytes = AES_KEY_SIZE / 8 ;
439
+ if (decryptedAESKey .length != aesExpectedLengthInBytes ) {
440
+ Log .w (TAG , "Decrypted AES key has incorrect length (" +
441
+ decryptedAESKey .length + " bytes, expected " + aesExpectedLengthInBytes
442
+ + "). Discarding." );
443
+ decryptedAESKey = null ;
444
+ migrationNeeded = false ;
445
+ }
378
446
}
379
447
}
380
- //Key doesn't exist. Generate new AES
381
- try {
382
- KeyGenerator keyGen = KeyGenerator .getInstance (ALGORITHM_AES );
383
- keyGen .init (AES_KEY_SIZE );
384
- byte [] aes = keyGen .generateKey ().getEncoded ();
385
- //Save encrypted encoded version
386
- byte [] encryptedAES = RSAEncrypt (aes );
387
- String encodedEncryptedAESText = new String (Base64 .encode (encryptedAES , Base64 .DEFAULT ), StandardCharsets .UTF_8 );
388
- storage .store (KEY_ALIAS , encodedEncryptedAESText );
389
- return aes ;
390
- } catch (NoSuchAlgorithmException e ) {
391
- /*
392
- * This exceptions are safe to be ignored:
393
- *
394
- * - NoSuchAlgorithmException:
395
- * Thrown if the Algorithm implementation is not available. AES was introduced in API 1
396
- *
397
- * Read more in https://developer.android.com/reference/javax/crypto/KeyGenerator
398
- */
399
- Log .e (TAG , "Error while creating the AES key." , e );
400
- throw new IncompatibleDeviceException (e );
448
+
449
+ if (migrationNeeded && decryptedAESKey != null ) {
450
+ try {
451
+ Log .d (TAG , "AES key was from legacy PKCS1. Deleting old RSA key pair to ensure new OAEP-compatible RSA key is used for re-encryption." );
452
+ deleteRSAKeys ();
453
+
454
+ byte [] encryptedAESWithOAEP = RSAEncrypt (decryptedAESKey );
455
+ String newEncodedEncryptedAES = new String (Base64 .encode (encryptedAESWithOAEP , Base64 .DEFAULT ), StandardCharsets .UTF_8 );
456
+ storage .store (KEY_ALIAS , newEncodedEncryptedAES );
457
+ Log .i (TAG , "AES key successfully migrated and re-encrypted with OAEP." );
458
+ } catch (Exception reEncryptEx ) {
459
+ Log .e (TAG , "Failed to re-encrypt AES key with OAEP during migration. A new AES key will be generated." , reEncryptEx );
460
+ decryptedAESKey = null ;
461
+ }
462
+ }
463
+ if (decryptedAESKey == null ) {
464
+ Log .d (TAG , "Generating new AES key." );
465
+ try {
466
+ KeyGenerator keyGen = KeyGenerator .getInstance (ALGORITHM_AES );
467
+ keyGen .init (AES_KEY_SIZE );
468
+ decryptedAESKey = keyGen .generateKey ().getEncoded ();
469
+
470
+ byte [] encryptedNewAES = RSAEncrypt (decryptedAESKey );
471
+ String encodedEncryptedNewAESText = new String (Base64 .encode (encryptedNewAES , Base64 .DEFAULT ), StandardCharsets .UTF_8 );
472
+ storage .store (KEY_ALIAS , encodedEncryptedNewAESText );
473
+ Log .d (TAG , "New AES key generated, encrypted with OAEP, and stored." );
474
+ } catch (NoSuchAlgorithmException e ) {
475
+ Log .e (TAG , "Error while creating the new AES key." , e );
476
+ throw new IncompatibleDeviceException (e );
477
+ }
401
478
}
479
+ return decryptedAESKey ;
402
480
}
403
481
404
482
0 commit comments