@@ -53,10 +53,11 @@ class CryptoUtil {
5353
5454 // Transformations available since API 18
5555 // 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 " ;
5757 // https://developer.android.com/reference/javax/crypto/Cipher.html
5858 @ SuppressWarnings ("SpellCheckingInspection" )
5959 private static final String AES_TRANSFORMATION = "AES/GCM/NOPADDING" ;
60+ private static final String OLD_PKCS1_RSA_TRANSFORMATION = "RSA/ECB/PKCS1Padding" ;
6061
6162 private static final String ANDROID_KEY_STORE = "AndroidKeyStore" ;
6263 private static final String ALGORITHM_RSA = "RSA" ;
@@ -124,7 +125,8 @@ KeyStore.PrivateKeyEntry getRSAKeyEntry() throws CryptoException, IncompatibleDe
124125 .setCertificateNotBefore (start .getTime ())
125126 .setCertificateNotAfter (end .getTime ())
126127 .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 )
128130 .setBlockModes (KeyProperties .BLOCK_MODE_ECB )
129131 .build ();
130132 } else {
@@ -355,6 +357,7 @@ byte[] RSAEncrypt(byte[] decryptedInput) throws IncompatibleDeviceException, Cry
355357
356358 /**
357359 * 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.
358361 *
359362 * @return a valid AES Key bytes
360363 * @throws IncompatibleDeviceException in the event the device can't understand the cryptographic settings required
@@ -366,39 +369,114 @@ byte[] getAESKey() throws IncompatibleDeviceException, CryptoException {
366369 if (TextUtils .isEmpty (encodedEncryptedAES )) {
367370 encodedEncryptedAES = storage .retrieveString (OLD_KEY_ALIAS );
368371 }
372+ byte [] decryptedAESKey = null ;
373+ boolean migrationNeeded = false ;
369374 if (encodedEncryptedAES != null ) {
370375 //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+ }
378446 }
379447 }
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+ }
401478 }
479+ return decryptedAESKey ;
402480 }
403481
404482
0 commit comments