@@ -64,6 +64,12 @@ 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+
69+ private static final int GCM_TAG_LENGTH = 16 ;
70+ private static final int MIN_DATA_LENGTH = 1 ;
71+ private static final int FORMAT_HEADER_LENGTH = 2 ;
72+
6773 private final String OLD_KEY_ALIAS ;
6874 private final String OLD_KEY_IV_ALIAS ;
6975 private final String KEY_ALIAS ;
@@ -156,7 +162,9 @@ KeyStore.PrivateKeyEntry getRSAKeyEntry() throws CryptoException, IncompatibleDe
156162 generator .generateKeyPair ();
157163
158164 return getKeyEntryCompat (keyStore , KEY_ALIAS );
159- } catch (CertificateException | InvalidAlgorithmParameterException | NoSuchProviderException | NoSuchAlgorithmException | KeyStoreException | ProviderException e ) {
165+ } catch (CertificateException | InvalidAlgorithmParameterException |
166+ NoSuchProviderException | NoSuchAlgorithmException | KeyStoreException |
167+ ProviderException e ) {
160168 /*
161169 * This exceptions are safe to be ignored:
162170 *
@@ -240,7 +248,8 @@ private void deleteRSAKeys() {
240248 keyStore .deleteEntry (KEY_ALIAS );
241249 keyStore .deleteEntry (OLD_KEY_ALIAS );
242250 Log .d (TAG , "Deleting the existing RSA key pair from the KeyStore." );
243- } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e ) {
251+ } catch (KeyStoreException | CertificateException | IOException |
252+ NoSuchAlgorithmException e ) {
244253 Log .e (TAG , "Failed to remove the RSA KeyEntry from the Android KeyStore." , e );
245254 }
246255 }
@@ -403,7 +412,7 @@ byte[] getAESKey() throws IncompatibleDeviceException, CryptoException {
403412
404413
405414 /**
406- * Encrypts the given input bytes using a symmetric key (AES).
415+ * Decrypts the given input bytes using a symmetric key (AES).
407416 * The AES key is stored protected by an asymmetric key pair (RSA).
408417 *
409418 * @param encryptedInput the input bytes to decrypt. There's no limit in size.
@@ -415,18 +424,15 @@ public byte[] decrypt(byte[] encryptedInput) throws CryptoException, Incompatibl
415424 try {
416425 SecretKey key = new SecretKeySpec (getAESKey (), ALGORITHM_AES );
417426 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- }
427+
428+ // Detect format and decrypt accordingly to maintain backward compatibility
429+ if (isNewFormat (encryptedInput )) {
430+ return decryptNewFormat (encryptedInput , cipher , key );
431+ } else {
432+ return decryptLegacyFormat (encryptedInput , cipher , key );
425433 }
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 ) {
434+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
435+ InvalidAlgorithmParameterException e ) {
430436 /*
431437 * This exceptions are safe to be ignored:
432438 *
@@ -456,12 +462,115 @@ public byte[] decrypt(byte[] encryptedInput) throws CryptoException, Incompatibl
456462 }
457463 }
458464
465+ /**
466+ * Checks if the encrypted input uses the new format with bundled IV.
467+ * New format structure: [FORMAT_MARKER][IV_LENGTH][IV][ENCRYPTED_DATA]
468+ *
469+ * @param encryptedInput the encrypted data to check
470+ * @return true if new format, false if legacy format
471+ */
472+ @ VisibleForTesting (otherwise = VisibleForTesting .PRIVATE )
473+ boolean isNewFormat (byte [] encryptedInput ) {
474+
475+ // Boundary check
476+ if (encryptedInput == null || encryptedInput .length < 2 ) {
477+ return false ;
478+ }
479+
480+ if (encryptedInput [0 ] != FORMAT_MARKER ) {
481+ return false ;
482+ }
483+
484+ // Check IV length is valid for AES-GCM (12 or 16 bytes)
485+ // AES is a 128 block size cipher ,which is 16 bytes
486+ // AES in GCM mode the recommended IV length is 12 bytes.
487+ // This 12-byte IV is then combined with a 4-byte internal counter to form the full 16-byte
488+ // input block for the underlying AES block cipher in counter mode (CTR), which GCM utilizes.
489+ // Thus checking for a 12 or 16 byte length
490+ int ivLength = encryptedInput [1 ] & 0xFF ;
491+ if (ivLength != 12 && ivLength != 16 ) {
492+ return false ;
493+ }
494+
495+ // Verify minimum total length
496+ // Need: marker(1) + length(1) + IV(12-16) + GCM tag(16) + data(1+)
497+ int minLength = FORMAT_HEADER_LENGTH + ivLength + GCM_TAG_LENGTH + MIN_DATA_LENGTH ;
498+ return encryptedInput .length >= minLength ;
499+ }
500+
501+ /**
502+ * Decrypts data in the new format (IV bundled with encrypted data).
503+ *
504+ * @param encryptedInput the encrypted input in new format
505+ * @param cipher the cipher instance
506+ * @param key the secret key
507+ * @return the decrypted data
508+ * @throws InvalidKeyException if the key is invalid
509+ * @throws InvalidAlgorithmParameterException if the IV is invalid
510+ * @throws IllegalBlockSizeException if the block size is invalid
511+ * @throws BadPaddingException if padding is incorrect
512+ */
513+ @ VisibleForTesting
514+ private byte [] decryptNewFormat (byte [] encryptedInput , Cipher cipher , SecretKey key )
515+ throws InvalidKeyException , InvalidAlgorithmParameterException ,
516+ IllegalBlockSizeException , BadPaddingException {
517+
518+ // Read IV length (byte 1)
519+ int ivLength = encryptedInput [1 ] & 0xFF ;
520+
521+ // Extract IV (bytes 2 to 2+ivLength)
522+ byte [] iv = new byte [ivLength ];
523+ System .arraycopy (encryptedInput , 2 , iv , 0 , ivLength );
524+
525+ int encryptedDataOffset = 2 + ivLength ;
526+ int encryptedDataLength = encryptedInput .length - encryptedDataOffset ;
527+
528+ cipher .init (Cipher .DECRYPT_MODE , key , new IvParameterSpec (iv ));
529+ return cipher .doFinal (encryptedInput , encryptedDataOffset , encryptedDataLength );
530+ }
531+
532+ /**
533+ * Decrypts data in the legacy format (IV stored separately in storage).
534+ * This maintains backward compatibility with credentials encrypted before the fix.
535+ *
536+ * @param encryptedInput the encrypted input in legacy format
537+ * @param cipher the cipher instance
538+ * @param key the secret key
539+ * @return the decrypted data
540+ * @throws InvalidKeyException if the key is invalid
541+ * @throws InvalidAlgorithmParameterException if the IV is invalid
542+ * @throws IllegalBlockSizeException if the block size is invalid
543+ * @throws BadPaddingException if padding is incorrect
544+ * @throws CryptoException if the IV cannot be found in storage
545+ */
546+ @ VisibleForTesting
547+ private byte [] decryptLegacyFormat (byte [] encryptedInput , Cipher cipher , SecretKey key )
548+ throws InvalidKeyException , InvalidAlgorithmParameterException ,
549+ IllegalBlockSizeException , BadPaddingException , CryptoException {
550+ // Retrieve IV from storage (legacy behavior)
551+ String encodedIV = storage .retrieveString (KEY_IV_ALIAS );
552+ if (TextUtils .isEmpty (encodedIV )) {
553+ encodedIV = storage .retrieveString (OLD_KEY_IV_ALIAS );
554+ if (TextUtils .isEmpty (encodedIV )) {
555+ throw new CryptoException ("The encryption keys changed recently. You need to re-encrypt something first." , null );
556+ }
557+ }
558+
559+ byte [] iv = Base64 .decode (encodedIV , Base64 .DEFAULT );
560+ cipher .init (Cipher .DECRYPT_MODE , key , new IvParameterSpec (iv ));
561+ return cipher .doFinal (encryptedInput );
562+ }
563+
459564 /**
460565 * Encrypts the given input bytes using a symmetric key (AES).
461566 * The AES key is stored protected by an asymmetric key pair (RSA).
567+ * <p>
568+ * The encrypted output uses a new format that bundles the IV with the encrypted data
569+ * to prevent IV collision issues when multiple credentials are stored.
570+ * Format: [FORMAT_MARKER(1)][IV_LENGTH(1)][IV(12-16)][ENCRYPTED_DATA(variable)]
462571 *
463572 * @param decryptedInput the input bytes to encrypt. There's no limit in size.
464- * @return the encrypted output bytes
573+ * @return the encrypted output bytes with bundled IV
465574 * @throws CryptoException if the RSA Key pair was deemed invalid and got deleted. Operation can be retried.
466575 * @throws IncompatibleDeviceException in the event the device can't understand the cryptographic settings required
467576 */
@@ -471,10 +580,17 @@ public byte[] encrypt(byte[] decryptedInput) throws CryptoException, Incompatibl
471580 Cipher cipher = Cipher .getInstance (AES_TRANSFORMATION );
472581 cipher .init (Cipher .ENCRYPT_MODE , key );
473582 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 ;
583+ byte [] iv = cipher .getIV ();
584+
585+ // NEW FORMAT: Bundle IV with encrypted data to prevent collision issues
586+ // Format: [FORMAT_MARKER][IV_LENGTH][IV][ENCRYPTED_DATA]
587+ byte [] output = new byte [1 + 1 + iv .length + encrypted .length ];
588+ output [0 ] = FORMAT_MARKER ;
589+ output [1 ] = (byte ) iv .length ;
590+ System .arraycopy (iv , 0 , output , 2 , iv .length );
591+ System .arraycopy (encrypted , 0 , output , 2 + iv .length , encrypted .length );
592+
593+ return output ;
478594 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e ) {
479595 /*
480596 * This exceptions are safe to be ignored:
0 commit comments