@@ -64,6 +64,8 @@ class CryptoUtil {
6464 private static final int AES_KEY_SIZE = 256 ;
6565 private static final int RSA_KEY_SIZE = 2048 ;
6666
67+ private static final byte FORMAT_MARKER = 0x01 ;
68+
6769 private final String OLD_KEY_ALIAS ;
6870 private final String OLD_KEY_IV_ALIAS ;
6971 private final String KEY_ALIAS ;
@@ -156,7 +158,9 @@ KeyStore.PrivateKeyEntry getRSAKeyEntry() throws CryptoException, IncompatibleDe
156158 generator .generateKeyPair ();
157159
158160 return getKeyEntryCompat (keyStore , KEY_ALIAS );
159- } catch (CertificateException | InvalidAlgorithmParameterException | NoSuchProviderException | NoSuchAlgorithmException | KeyStoreException | ProviderException e ) {
161+ } catch (CertificateException | InvalidAlgorithmParameterException |
162+ NoSuchProviderException | NoSuchAlgorithmException | KeyStoreException |
163+ ProviderException e ) {
160164 /*
161165 * This exceptions are safe to be ignored:
162166 *
@@ -240,7 +244,8 @@ private void deleteRSAKeys() {
240244 keyStore .deleteEntry (KEY_ALIAS );
241245 keyStore .deleteEntry (OLD_KEY_ALIAS );
242246 Log .d (TAG , "Deleting the existing RSA key pair from the KeyStore." );
243- } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e ) {
247+ } catch (KeyStoreException | CertificateException | IOException |
248+ NoSuchAlgorithmException e ) {
244249 Log .e (TAG , "Failed to remove the RSA KeyEntry from the Android KeyStore." , e );
245250 }
246251 }
@@ -403,7 +408,7 @@ byte[] getAESKey() throws IncompatibleDeviceException, CryptoException {
403408
404409
405410 /**
406- * Encrypts the given input bytes using a symmetric key (AES).
411+ * Decrypts the given input bytes using a symmetric key (AES).
407412 * The AES key is stored protected by an asymmetric key pair (RSA).
408413 *
409414 * @param encryptedInput the input bytes to decrypt. There's no limit in size.
@@ -415,18 +420,15 @@ public byte[] decrypt(byte[] encryptedInput) throws CryptoException, Incompatibl
415420 try {
416421 SecretKey key = new SecretKeySpec (getAESKey (), ALGORITHM_AES );
417422 Cipher cipher = Cipher .getInstance (AES_TRANSFORMATION );
418- String encodedIV = storage .retrieveString (KEY_IV_ALIAS );
419- if (TextUtils .isEmpty (encodedIV )) {
420- encodedIV = storage .retrieveString (OLD_KEY_IV_ALIAS );
421- if (TextUtils .isEmpty (encodedIV )) {
422- //AES key was JUST generated. If anything existed before, should be encrypted again first.
423- throw new CryptoException ("The encryption keys changed recently. You need to re-encrypt something first." , null );
424- }
423+
424+ // Detect format and decrypt accordingly to maintain backward compatibility
425+ if (isNewFormat (encryptedInput )) {
426+ return decryptNewFormat (encryptedInput , cipher , key );
427+ } else {
428+ return decryptLegacyFormat (encryptedInput , cipher , key );
425429 }
426- byte [] iv = Base64 .decode (encodedIV , Base64 .DEFAULT );
427- cipher .init (Cipher .DECRYPT_MODE , key , new IvParameterSpec (iv ));
428- return cipher .doFinal (encryptedInput );
429- } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e ) {
430+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
431+ InvalidAlgorithmParameterException e ) {
430432 /*
431433 * This exceptions are safe to be ignored:
432434 *
@@ -456,12 +458,104 @@ public byte[] decrypt(byte[] encryptedInput) throws CryptoException, Incompatibl
456458 }
457459 }
458460
461+ /**
462+ * Checks if the encrypted input uses the new format with bundled IV.
463+ * New format structure: [FORMAT_MARKER][IV_LENGTH][IV][ENCRYPTED_DATA]
464+ *
465+ * @param encryptedInput the encrypted data to check
466+ * @return true if new format, false if legacy format
467+ */
468+ @ VisibleForTesting
469+ boolean isNewFormat (byte [] encryptedInput ) {
470+ if (encryptedInput [0 ] != FORMAT_MARKER ) {
471+ return false ;
472+ }
473+
474+ // Check IV length is valid for AES-GCM (12 or 16 bytes)
475+ int ivLength = encryptedInput [1 ] & 0xFF ;
476+ if (ivLength != 12 && ivLength != 16 ) {
477+ return false ;
478+ }
479+
480+ // Verify minimum total length
481+ // Need: marker(1) + length(1) + IV(12-16) + GCM tag(16) + data(1+)
482+ int minLength = 2 + ivLength + 16 + 1 ;
483+ return encryptedInput .length >= minLength ;
484+ }
485+
486+ /**
487+ * Decrypts data in the new format (IV bundled with encrypted data).
488+ *
489+ * @param encryptedInput the encrypted input in new format
490+ * @param cipher the cipher instance
491+ * @param key the secret key
492+ * @return the decrypted data
493+ * @throws InvalidKeyException if the key is invalid
494+ * @throws InvalidAlgorithmParameterException if the IV is invalid
495+ * @throws IllegalBlockSizeException if the block size is invalid
496+ * @throws BadPaddingException if padding is incorrect
497+ */
498+ @ VisibleForTesting
499+ private byte [] decryptNewFormat (byte [] encryptedInput , Cipher cipher , SecretKey key )
500+ throws InvalidKeyException , InvalidAlgorithmParameterException ,
501+ IllegalBlockSizeException , BadPaddingException {
502+
503+ // Read IV length (byte 1)
504+ int ivLength = encryptedInput [1 ] & 0xFF ;
505+
506+ // Extract IV (bytes 2 to 2+ivLength)
507+ byte [] iv = new byte [ivLength ];
508+ System .arraycopy (encryptedInput , 2 , iv , 0 , ivLength );
509+
510+ int encryptedDataOffset = 2 + ivLength ;
511+ int encryptedDataLength = encryptedInput .length - encryptedDataOffset ;
512+
513+ cipher .init (Cipher .DECRYPT_MODE , key , new IvParameterSpec (iv ));
514+ return cipher .doFinal (encryptedInput , encryptedDataOffset , encryptedDataLength );
515+ }
516+
517+ /**
518+ * Decrypts data in the legacy format (IV stored separately in storage).
519+ * This maintains backward compatibility with credentials encrypted before the fix.
520+ *
521+ * @param encryptedInput the encrypted input in legacy format
522+ * @param cipher the cipher instance
523+ * @param key the secret key
524+ * @return the decrypted data
525+ * @throws InvalidKeyException if the key is invalid
526+ * @throws InvalidAlgorithmParameterException if the IV is invalid
527+ * @throws IllegalBlockSizeException if the block size is invalid
528+ * @throws BadPaddingException if padding is incorrect
529+ * @throws CryptoException if the IV cannot be found in storage
530+ */
531+ @ VisibleForTesting
532+ private byte [] decryptLegacyFormat (byte [] encryptedInput , Cipher cipher , SecretKey key )
533+ throws InvalidKeyException , InvalidAlgorithmParameterException ,
534+ IllegalBlockSizeException , BadPaddingException , CryptoException {
535+ // Retrieve IV from storage (legacy behavior)
536+ String encodedIV = storage .retrieveString (KEY_IV_ALIAS );
537+ if (TextUtils .isEmpty (encodedIV )) {
538+ encodedIV = storage .retrieveString (OLD_KEY_IV_ALIAS );
539+ if (TextUtils .isEmpty (encodedIV )) {
540+ throw new CryptoException ("The encryption keys changed recently. You need to re-encrypt something first." , null );
541+ }
542+ }
543+
544+ byte [] iv = Base64 .decode (encodedIV , Base64 .DEFAULT );
545+ cipher .init (Cipher .DECRYPT_MODE , key , new IvParameterSpec (iv ));
546+ return cipher .doFinal (encryptedInput );
547+ }
548+
459549 /**
460550 * Encrypts the given input bytes using a symmetric key (AES).
461551 * The AES key is stored protected by an asymmetric key pair (RSA).
552+ * <p>
553+ * The encrypted output uses a new format that bundles the IV with the encrypted data
554+ * to prevent IV collision issues when multiple credentials are stored.
555+ * Format: [FORMAT_MARKER(1)][IV_LENGTH(1)][IV(12-16)][ENCRYPTED_DATA(variable)]
462556 *
463557 * @param decryptedInput the input bytes to encrypt. There's no limit in size.
464- * @return the encrypted output bytes
558+ * @return the encrypted output bytes with bundled IV
465559 * @throws CryptoException if the RSA Key pair was deemed invalid and got deleted. Operation can be retried.
466560 * @throws IncompatibleDeviceException in the event the device can't understand the cryptographic settings required
467561 */
@@ -471,10 +565,17 @@ public byte[] encrypt(byte[] decryptedInput) throws CryptoException, Incompatibl
471565 Cipher cipher = Cipher .getInstance (AES_TRANSFORMATION );
472566 cipher .init (Cipher .ENCRYPT_MODE , key );
473567 byte [] encrypted = cipher .doFinal (decryptedInput );
474- byte [] encodedIV = Base64 .encode (cipher .getIV (), Base64 .DEFAULT );
475- //Save IV for Decrypt stage
476- storage .store (KEY_IV_ALIAS , new String (encodedIV , StandardCharsets .UTF_8 ));
477- return encrypted ;
568+ byte [] iv = cipher .getIV ();
569+
570+ // NEW FORMAT: Bundle IV with encrypted data to prevent collision issues
571+ // Format: [FORMAT_MARKER][IV_LENGTH][IV][ENCRYPTED_DATA]
572+ byte [] output = new byte [1 + 1 + iv .length + encrypted .length ];
573+ output [0 ] = FORMAT_MARKER ;
574+ output [1 ] = (byte ) iv .length ;
575+ System .arraycopy (iv , 0 , output , 2 , iv .length );
576+ System .arraycopy (encrypted , 0 , output , 2 + iv .length , encrypted .length );
577+
578+ return output ;
478579 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e ) {
479580 /*
480581 * This exceptions are safe to be ignored:
0 commit comments