简体   繁体   中英

Why are my ByteArrays different after writing to a Room database?

Description

I'm attempting to encrypt a token along with its IV to a pair of ByteArrays, serialize it, then write it to a Room database. The steps are obviously reversed when attempting to decrypt and read it.

When repeating the encryption/serialization/deserialization/decryption steps, but without writing it to a database, the given ByteArray decrypts just fine. Writing it gives me the following error on decryption:

java.io.StreamCorruptedException: invalid stream header

I'm struggling to understand why this happens, and I'd appreciate the help.

Code

ByteArray Functions

@Suppress("UNCHECKED_CAST")
fun <T : Serializable> fromByteArray(byteArray: ByteArray): T {
    val inputStream = ByteArrayInputStream(byteArray)
    val objectInput = ObjectInputStream(inputStream)
    val result = objectInput.readObject() as T
    
    objectInput.close()
    inputStream.close()
    return result
}

fun Serializable.toByteArray(): ByteArray {
    val outputStream = ByteArrayOutputStream()
    val objectOutput = ObjectOutputStream(outputStream)
    
    objectOutput.writeObject(this)
    objectOutput.flush()
    
    val result = outputStream.toByteArray()
    
    outputStream.close()
    objectOutput.close()
    
    return result
}

Encryption Functions

    override fun <T : Serializable> encryptData(data: T): Pair<ByteArray, ByteArray> {
        var temp = data.toByteArray()
        
        if (temp.size % 16 != 0) {
            temp = temp.copyOf(
                (temp.size + 16) - (temp.size % 16)
            )
        }
        
        cipher.init(Cipher.ENCRYPT_MODE, getKey())

        val ivBytes = cipher.iv
        val encryptedArray = cipher.doFinal(temp)
        
        return Pair(ivBytes, encryptedArray)
    }
    
    @Suppress("UNCHECKED_CAST")
    override fun <T> decryptData(ivBytes: ByteArray, data: ByteArray): T {
        val ivSpec = IvParameterSpec(ivBytes)
        
        cipher.init(Cipher.DECRYPT_MODE, getKey(), ivSpec)
        val tempArray: ByteArray = cipher.doFinal(data)
        return fromByteArray(tempArray) as T
    }

Room Data Class

data class UserData(
    val profilePictureId: Long?,
    val savedTimestamp: Long = System.currentTimeMillis(),
    @PrimaryKey
    val username: String = "",
    val userToken: Pair<ByteArray, ByteArray>?
)

Database Class

@Database(entities = [UserData::class], version = 1)
@TypeConverters(UserDataConverters::class)
abstract class UserDataDatabase : RoomDatabase() {
    abstract val userDataDao: UserDataDao
    
    companion object {
        const val DB_NAME = "user_data_db"
    }
}

Database DAO

@Dao
interface UserDataDao {
    
    @Query("SELECT * FROM UserData")
    fun loadUserData(): Flow<UserData>
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun updateUserData(userData: UserData)
}

Database Type Converters

class UserDataConverters {
    @TypeConverter
    fun fromTokenPair(pair: Pair<ByteArray, ByteArray>): String {
        return Json.encodeToString(pair)
    }
    
    @TypeConverter
    fun toTokenPair(serializedPair: String): Pair<ByteArray, ByteArray> {
        return Json.decodeFromString(serializedPair)
    }
}

So this isn't actually related to Room. My mistake.

I didn't realise that serializing objects to ByteArrays with ObjectInputStream also writes a header for later serialization with ObjectOutputStream.

When encrypting the serialized data, I was using CDC block mode, which requires padding to a block size divisble by 16. That extra padding caused the aforementioned header to become invalid for accompanying data.

Removing the padding raises issues with detecting when padding stops and content starts (copyOf adds zeroes). With that in mind, and after later finding out that CBC is less secure than GCM (which requires no padding), I changed the block mode to GCM.

See below for resultant code (irrelevent blocks removed):

    private val keyGenParameterSpec = KeyGenParameterSpec.Builder(
        "TroupetentKeyAlias",
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .build()

Also, when decrypting, the previous spec ivParameterSpec() couldn't be used, as GCM requires a tag length. That was also changed:

    @Suppress("UNCHECKED_CAST")
    override fun <T> decryptData(ivBytes: ByteArray, data: ByteArray): T {
        val ivSpec = GCMParameterSpec(128, ivBytes)
        
        cipher.init(Cipher.DECRYPT_MODE, getKey(), ivSpec)
        val decryptedArray: ByteArray = cipher.doFinal(data)
        return fromByteArray(decryptedArray) as T
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM