@@ -306,29 +306,38 @@ public void testDecryptionAfterKeyLoss() {
306306 public void testEncryptionAcrossApiLevels () {
307307 String testData = "test data for cross-version compatibility" ;
308308
309- // Test API 16 (Legacy)
310- setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .JELLY_BEAN );
311- String encryptedOnApi16 = encryptor .encrypt (testData );
312-
313- // Test API 18 (Legacy)
314- setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .JELLY_BEAN_MR2 );
315- String encryptedOnApi18 = encryptor .encrypt (testData );
316- assertEquals ("Legacy decryption should work on API 18" , testData , encryptor .decrypt (encryptedOnApi16 ));
309+ // Create legacy-encrypted data using encryptLegacy directly for backward compat testing
310+ byte [] testBytes = testData .getBytes (java .nio .charset .StandardCharsets .UTF_8 );
311+ IterableDataEncryptor .EncryptedData legacyEncrypted = encryptor .encryptLegacy (testBytes );
312+ // Manually build the combined format: flag(0) + ivLength + iv + data
313+ byte [] combined = new byte [1 + 1 + legacyEncrypted .getIv ().length + legacyEncrypted .getData ().length ];
314+ combined [0 ] = 0 ; // legacy flag
315+ combined [1 ] = (byte ) legacyEncrypted .getIv ().length ;
316+ System .arraycopy (legacyEncrypted .getIv (), 0 , combined , 2 , legacyEncrypted .getIv ().length );
317+ System .arraycopy (legacyEncrypted .getData (), 0 , combined , 2 + legacyEncrypted .getIv ().length , legacyEncrypted .getData ().length );
318+ String legacyEncryptedStr = Base64 .encodeToString (combined , Base64 .NO_WRAP );
317319
318320 // Test API 19 (Modern - First version with GCM support)
319321 setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .KITKAT );
320322 String encryptedOnApi19 = encryptor .encrypt (testData );
321- assertEquals ("Should decrypt legacy data on API 19" , testData , encryptor .decrypt (encryptedOnApi16 ));
322- assertEquals ("Should decrypt legacy data on API 19" , testData , encryptor .decrypt (encryptedOnApi18 ));
323+ assertEquals ("Should decrypt legacy data on API 19" , testData , encryptor .decrypt (legacyEncryptedStr ));
323324
324325 // Test API 23 (Modern with KeyStore)
325326 setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .M );
326327 String encryptedOnApi23 = encryptor .encrypt (testData );
327- assertEquals ("Should decrypt legacy data on API 23" , testData , encryptor .decrypt (encryptedOnApi16 ));
328+ assertEquals ("Should decrypt legacy data on API 23" , testData , encryptor .decrypt (legacyEncryptedStr ));
328329 assertEquals ("Should decrypt API 19 data on API 23" , testData , encryptor .decrypt (encryptedOnApi19 ));
329330
330- // Test that modern encryption fails on legacy devices
331+ // Test that encryption on pre-KitKat throws UnsupportedOperationException
331332 setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .JELLY_BEAN );
333+ try {
334+ encryptor .encrypt (testData );
335+ fail ("Should throw UnsupportedOperationException on pre-KitKat" );
336+ } catch (UnsupportedOperationException e ) {
337+ assertEquals ("Encryption requires Android API 19 (KitKat) or higher" , e .getMessage ());
338+ }
339+
340+ // Test that modern encryption fails to decrypt on legacy devices
332341 try {
333342 encryptor .decrypt (encryptedOnApi19 );
334343 fail ("Should not be able to decrypt modern encryption on legacy device" );
@@ -349,12 +358,6 @@ public void testEncryptionAcrossApiLevels() {
349358 public void testEncryptionMethodFlag () {
350359 String testData = "test data for encryption method verification" ;
351360
352- // Test legacy encryption flag (API 16)
353- setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .JELLY_BEAN );
354- String legacyEncrypted = encryptor .encrypt (testData );
355- byte [] legacyBytes = Base64 .decode (legacyEncrypted , Base64 .NO_WRAP );
356- assertEquals ("Legacy encryption should have flag 0" , 0 , legacyBytes [0 ]);
357-
358361 // Test modern encryption flag (API 19)
359362 setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .KITKAT );
360363 String modernEncrypted = encryptor .encrypt (testData );
@@ -402,17 +405,22 @@ public void testDecryptManipulatedIV() {
402405
403406 @ Test
404407 public void testDecryptManipulatedVersionFlag () {
405- // Test on API 16 device
406- setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .JELLY_BEAN );
407-
408+ // Create legacy-encrypted data using encryptLegacy directly
408409 String testData = "test data" ;
409- String encrypted = encryptor .encrypt (testData );
410- byte [] bytes = Base64 .decode (encrypted , Base64 .NO_WRAP );
410+ byte [] testBytes = testData .getBytes (java .nio .charset .StandardCharsets .UTF_8 );
411+ IterableDataEncryptor .EncryptedData legacyEncrypted = encryptor .encryptLegacy (testBytes );
412+ byte [] combined = new byte [1 + 1 + legacyEncrypted .getIv ().length + legacyEncrypted .getData ().length ];
413+ combined [0 ] = 0 ; // legacy flag
414+ combined [1 ] = (byte ) legacyEncrypted .getIv ().length ;
415+ System .arraycopy (legacyEncrypted .getIv (), 0 , combined , 2 , legacyEncrypted .getIv ().length );
416+ System .arraycopy (legacyEncrypted .getData (), 0 , combined , 2 + legacyEncrypted .getIv ().length , legacyEncrypted .getData ().length );
411417
412418 // Change version flag from legacy (0) to modern (1)
413- bytes [0 ] = 1 ;
414- String manipulated = Base64 .encodeToString (bytes , Base64 .NO_WRAP );
419+ combined [0 ] = 1 ;
420+ String manipulated = Base64 .encodeToString (combined , Base64 .NO_WRAP );
415421
422+ // Test on API 16 device - should fail because modern decryption is not available
423+ setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .JELLY_BEAN );
416424 try {
417425 encryptor .decrypt (manipulated );
418426 fail ("Should throw exception for manipulated version flag" );
@@ -424,31 +432,42 @@ public void testDecryptManipulatedVersionFlag() {
424432
425433 @ Test
426434 public void testLegacyEncryptionAndDecryption () {
427- // Set to API 16 (Legacy)
428- setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .JELLY_BEAN );
429-
430435 String testData = "test data for legacy encryption" ;
431- String encrypted = encryptor .encrypt (testData );
432- String decrypted = encryptor .decrypt (encrypted );
433436
434- assertEquals ("Legacy encryption/decryption should work on API 16" , testData , decrypted );
437+ // Verify encrypt() throws on pre-KitKat
438+ setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .JELLY_BEAN );
439+ try {
440+ encryptor .encrypt (testData );
441+ fail ("Should throw UnsupportedOperationException on pre-KitKat" );
442+ } catch (UnsupportedOperationException e ) {
443+ assertEquals ("Encryption requires Android API 19 (KitKat) or higher" , e .getMessage ());
444+ }
435445
436- // Verify it's using legacy encryption
437- byte [] encryptedBytes = Base64 .decode (encrypted , Base64 .NO_WRAP );
446+ // Create legacy-encrypted data using encryptLegacy directly
447+ byte [] testBytes = testData .getBytes (java .nio .charset .StandardCharsets .UTF_8 );
448+ IterableDataEncryptor .EncryptedData legacyEncrypted = encryptor .encryptLegacy (testBytes );
449+ byte [] combined = new byte [1 + 1 + legacyEncrypted .getIv ().length + legacyEncrypted .getData ().length ];
450+ combined [0 ] = 0 ; // legacy flag
451+ combined [1 ] = (byte ) legacyEncrypted .getIv ().length ;
452+ System .arraycopy (legacyEncrypted .getIv (), 0 , combined , 2 , legacyEncrypted .getIv ().length );
453+ System .arraycopy (legacyEncrypted .getData (), 0 , combined , 2 + legacyEncrypted .getIv ().length , legacyEncrypted .getData ().length );
454+ String legacyEncryptedStr = Base64 .encodeToString (combined , Base64 .NO_WRAP );
455+
456+ // Verify it has legacy encryption flag
457+ byte [] encryptedBytes = Base64 .decode (legacyEncryptedStr , Base64 .NO_WRAP );
438458 assertEquals ("Should use legacy encryption flag" , 0 , encryptedBytes [0 ]);
439459
440- // Test on API 18
441- setFinalStatic (Build .VERSION .class , "SDK_INT" , Build .VERSION_CODES .JELLY_BEAN_MR2 );
442- String decryptedOnApi18 = encryptor .decrypt (encrypted );
443- assertEquals ("Legacy data should be decryptable on API 18" , testData , decryptedOnApi18 );
460+ // Verify legacy data can still be decrypted on any API level
461+ String decryptedOnLegacy = encryptor .decrypt (legacyEncryptedStr );
462+ assertEquals ("Legacy data should be decryptable on legacy device" , testData , decryptedOnLegacy );
444463
445- String encryptedOnApi18 = encryptor . encrypt ( testData );
446- String decryptedFromApi18 = encryptor .decrypt (encryptedOnApi18 );
447- assertEquals ("API 18 encryption/decryption should work " , testData , decryptedFromApi18 );
464+ setFinalStatic ( Build . VERSION . class , "SDK_INT" , Build . VERSION_CODES . KITKAT );
465+ String decryptedOnApi19 = encryptor .decrypt (legacyEncryptedStr );
466+ assertEquals ("Legacy data should be decryptable on API 19 " , testData , decryptedOnApi19 );
448467
449- // Verify API 18 also uses legacy encryption
450- byte [] api18EncryptedBytes = Base64 . decode ( encryptedOnApi18 , Base64 . NO_WRAP );
451- assertEquals ("Should use legacy encryption flag on API 18 " , 0 , api18EncryptedBytes [ 0 ] );
468+ setFinalStatic ( Build . VERSION . class , "SDK_INT" , Build . VERSION_CODES . M );
469+ String decryptedOnApi23 = encryptor . decrypt ( legacyEncryptedStr );
470+ assertEquals ("Legacy data should be decryptable on API 23 " , testData , decryptedOnApi23 );
452471 }
453472
454473 @ Test
0 commit comments