Improve handling when own app lists at the server are defect

This commit is contained in:
Jonas Lochmann 2022-08-08 02:00:00 +02:00
parent f3e83f9954
commit 56b7fe7e36
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
6 changed files with 260 additions and 188 deletions

View file

@ -38,9 +38,6 @@ interface CryptContainerDao {
@Query("SELECT * FROM crypt_container_metadata WHERE category_id = :categoryId AND device_id IS NULL AND type = :type") @Query("SELECT * FROM crypt_container_metadata WHERE category_id = :categoryId AND device_id IS NULL AND type = :type")
fun getCryptoMetadataSyncByCategoryId(categoryId: String, type: Int): CryptContainerMetadata? fun getCryptoMetadataSyncByCategoryId(categoryId: String, type: Int): CryptContainerMetadata?
@Query("DELETE FROM crypt_container_metadata WHERE category_id IS NULL AND device_id = :deviceId AND type in (:types)")
fun removeDeviceCryptoMetadata(deviceId: String, types: List<Int>)
@Query("SELECT * FROM crypt_container_metadata WHERE status = :processingStatus") @Query("SELECT * FROM crypt_container_metadata WHERE status = :processingStatus")
fun getMetadataByProcessingStatus(processingStatus: CryptContainerMetadata.ProcessingStatus): List<CryptContainerMetadata> fun getMetadataByProcessingStatus(processingStatus: CryptContainerMetadata.ProcessingStatus): List<CryptContainerMetadata>

View file

@ -117,7 +117,10 @@ data class CryptContainerMetadata (
nextCounter = 1, nextCounter = 1,
currentGenerationFirstTimestamp = System.currentTimeMillis(), currentGenerationFirstTimestamp = System.currentTimeMillis(),
currentGenerationKey = newKey currentGenerationKey = newKey
) ),
type =
if (currentGenerationKey == null) PrepareEncryptionResult.Type.NewContainer
else PrepareEncryptionResult.Type.IncrementedGeneration
) )
} else { } else {
PrepareEncryptionResult( PrepareEncryptionResult(
@ -128,15 +131,23 @@ data class CryptContainerMetadata (
), ),
newMetadata = copy( newMetadata = copy(
nextCounter = nextCounter + 1 nextCounter = nextCounter + 1
) ),
type = PrepareEncryptionResult.Type.IncrementedCounter
) )
} }
} }
data class PrepareEncryptionResult ( data class PrepareEncryptionResult (
val params: CryptContainer.EncryptParameters, val params: CryptContainer.EncryptParameters,
val newMetadata: CryptContainerMetadata val newMetadata: CryptContainerMetadata,
) val type: Type
) {
enum class Type {
NewContainer,
IncrementedGeneration,
IncrementedCounter
}
}
} }
class CryptContainerMetadataProcessingStatusConverter { class CryptContainerMetadataProcessingStatusConverter {

View file

@ -0,0 +1,20 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.extensions
import com.squareup.wire.Message
fun <M : Message<M, B>, B : Message.Builder<M, B>> Message<M, B>.encodedSize() = this.adapter.encodedSize(this as M)

View file

@ -21,6 +21,7 @@ import io.timelimit.android.crypto.CryptContainer
import io.timelimit.android.data.Database import io.timelimit.android.data.Database
import io.timelimit.android.data.model.CryptContainerData import io.timelimit.android.data.model.CryptContainerData
import io.timelimit.android.data.model.CryptContainerMetadata import io.timelimit.android.data.model.CryptContainerMetadata
import io.timelimit.android.extensions.encodedSize
import io.timelimit.android.logic.ServerApiLevelInfo import io.timelimit.android.logic.ServerApiLevelInfo
import io.timelimit.android.proto.build import io.timelimit.android.proto.build
import io.timelimit.android.proto.encodeDeflated import io.timelimit.android.proto.encodeDeflated
@ -43,55 +44,92 @@ object CryptoAppListSync {
disableLegacySync: Boolean, disableLegacySync: Boolean,
serverApiLevelInfo: ServerApiLevelInfo serverApiLevelInfo: ServerApiLevelInfo
) { ) {
fun dispatch(action: AppLogicAction) { val compressedDataSizeLimit =
if (serverApiLevelInfo.hasLevelOrIsOffline(5)) 1024 * 512
else 1024 * 256
fun dispatchSync(action: AppLogicAction) {
if (deviceState.isConnectedMode) { if (deviceState.isConnectedMode) {
ApplyActionUtil.addAppLogicActionToDatabaseSync(action, database) ApplyActionUtil.addAppLogicActionToDatabaseSync(action, database)
} }
} }
val compressedDataSizeLimit = fun <T> prepareEncryption(encrypted: InstalledAppsUtil.Encrypted<T>?, type: Int, forceNewGeneration: Boolean = false) = if (encrypted == null) {
if (serverApiLevelInfo.hasLevelOrIsOffline(5)) 1024 * 512 val key = CryptContainer.EncryptParameters.generate()
else 1024 * 256
val savedCrypt = Threads.database.executeAndWait { val metadata = CryptContainerMetadata.buildFor(
InstalledAppsUtil.getEncryptedInstalledAppsFromDatabaseSync(database, deviceState.id) deviceId = deviceState.id,
categoryId = null,
type = type,
params = key
)
CryptContainerMetadata.PrepareEncryptionResult(key, metadata, CryptContainerMetadata.PrepareEncryptionResult.Type.NewContainer)
} else {
if (encrypted.meta.type != type) throw IllegalStateException()
encrypted.meta.copy(
status = CryptContainerMetadata.ProcessingStatus.Finished
).prepareEncryption(forceNewGeneration)
} }
if (savedCrypt == null) { fun throwIfTooLarge(data: ByteArray) {
val baseKey = CryptContainer.EncryptParameters.generate() data.size.let {
val diffKey = CryptContainer.EncryptParameters.generate() if (it > compressedDataSizeLimit) throw TooLargeException(it)
}
}
val baseEncrypted = CryptContainer.encrypt(installed.encodeDeflated(), baseKey) val savedCrypt = InstalledAppsUtil.getEncryptedInstalledAppsFromDatabase(database, deviceState.id)
val diffEncrypted = CryptContainer.encrypt(SavedAppsDifferenceProto.build(baseEncrypted, InstalledAppsDifferenceProto()).encodeDeflated(), diffKey)
if (baseEncrypted.size > compressedDataSizeLimit) throw TooLargeException(baseEncrypted.size) val baseCryptConfig = prepareEncryption(
encrypted = savedCrypt.base,
type = CryptContainerMetadata.TYPE_APP_LIST_BASE
)
val diffCryptConfig = prepareEncryption(
encrypted = savedCrypt.diff,
type = CryptContainerMetadata.TYPE_APP_LIST_DIFF,
forceNewGeneration = baseCryptConfig.type != CryptContainerMetadata.PrepareEncryptionResult.Type.IncrementedCounter || savedCrypt.base?.decrypted == null
)
val diffCrypto: InstalledAppsDifferenceProto? = if (savedCrypt.base?.decrypted == null) null
else AppsDifferenceUtil.calculateAppsDifference(savedCrypt.base.decrypted.data, installed)
if (
savedCrypt.base?.decrypted == null ||
savedCrypt.diff?.decrypted == null ||
diffCrypto != savedCrypt.diff.decrypted.data
) {
if (
savedCrypt.base?.decrypted == null ||
savedCrypt.diff?.decrypted == null ||
diffCrypto == null ||
baseCryptConfig.type != CryptContainerMetadata.PrepareEncryptionResult.Type.IncrementedCounter ||
diffCrypto.encodedSize() >= savedCrypt.base.decrypted.data.encodedSize() / 10
) {
val (baseEncrypted, diffEncrypted) = Threads.crypto.executeAndWait {
val baseEncrypted = CryptContainer.encrypt(
installed.encodeDeflated(),
baseCryptConfig.params
)
val diffEncrypted = CryptContainer.encrypt(
SavedAppsDifferenceProto.build(
baseEncrypted,
InstalledAppsDifferenceProto()
).encodeDeflated(),
diffCryptConfig.params
)
Pair(baseEncrypted, diffEncrypted)
}
throwIfTooLarge(baseEncrypted)
throwIfTooLarge(diffEncrypted)
Threads.database.executeAndWait { Threads.database.executeAndWait {
database.cryptContainer().removeDeviceCryptoMetadata( if (savedCrypt.base == null) {
deviceId = deviceState.id, val baseId = database.cryptContainer().insertMetadata(baseCryptConfig.newMetadata)
types = listOf(
CryptContainerMetadata.TYPE_APP_LIST_BASE,
CryptContainerMetadata.TYPE_APP_LIST_DIFF
)
)
val baseId = database.cryptContainer().insertMetadata(
CryptContainerMetadata.buildFor(
deviceId = deviceState.id,
categoryId = null,
type = CryptContainerMetadata.TYPE_APP_LIST_BASE,
params = baseKey
)
)
val diffId = database.cryptContainer().insertMetadata(
CryptContainerMetadata.buildFor(
deviceId = deviceState.id,
categoryId = null,
type = CryptContainerMetadata.TYPE_APP_LIST_DIFF,
params = diffKey
)
)
database.cryptContainer().insertData( database.cryptContainer().insertData(
CryptContainerData( CryptContainerData(
@ -99,6 +137,17 @@ object CryptoAppListSync {
encryptedData = baseEncrypted encryptedData = baseEncrypted
) )
) )
} else {
database.cryptContainer().updateMetadata(baseCryptConfig.newMetadata)
database.cryptContainer().updateData(CryptContainerData(
cryptContainerId = savedCrypt.base.meta.cryptContainerId,
encryptedData = baseEncrypted
))
}
if (savedCrypt.diff == null) {
val diffId = database.cryptContainer().insertMetadata(diffCryptConfig.newMetadata)
database.cryptContainer().insertData( database.cryptContainer().insertData(
CryptContainerData( CryptContainerData(
@ -106,57 +155,16 @@ object CryptoAppListSync {
encryptedData = diffEncrypted encryptedData = diffEncrypted
) )
) )
dispatch(UpdateInstalledAppsAction(
base = baseEncrypted,
diff = diffEncrypted,
wipe = disableLegacySync
))
}
syncUtil.requestImportantSync()
} else { } else {
val diffCrypto = AppsDifferenceUtil.calculateAppsDifference(savedCrypt.base, installed) database.cryptContainer().updateMetadata(diffCryptConfig.newMetadata)
if (diffCrypto != savedCrypt.diff) { database.cryptContainer().updateData(CryptContainerData(
val baseSize = savedCrypt.base.adapter.encodedSize(savedCrypt.base) cryptContainerId = savedCrypt.diff.meta.cryptContainerId,
val diffSize = diffCrypto.adapter.encodedSize(diffCrypto)
val needsNewBySize = diffSize >= baseSize / 10
val baseNeedsNewGeneration = savedCrypt.baseMeta.needsNewGeneration()
val diffNeedsNewGeneration = savedCrypt.diffMeta.needsNewGeneration() or baseNeedsNewGeneration
val diffCryptParams = savedCrypt.diffMeta.prepareEncryption(diffNeedsNewGeneration)
if (needsNewBySize or baseNeedsNewGeneration) {
val baseCryptParams = savedCrypt.baseMeta.prepareEncryption(baseNeedsNewGeneration)
val baseEncrypted = CryptContainer.encrypt(installed.encodeDeflated(), baseCryptParams.params)
val diffEncrypted = CryptContainer.encrypt(
SavedAppsDifferenceProto.build(baseEncrypted, InstalledAppsDifferenceProto()).encodeDeflated(),
diffCryptParams.params
)
if (baseEncrypted.size > compressedDataSizeLimit) throw TooLargeException(baseEncrypted.size)
Threads.database.executeAndWait {
database.cryptContainer().updateMetadata(listOf(
baseCryptParams.newMetadata,
diffCryptParams.newMetadata
))
database.cryptContainer().updateData(listOf(
CryptContainerData(
cryptContainerId = savedCrypt.baseMeta.cryptContainerId,
encryptedData = baseEncrypted
),
CryptContainerData(
cryptContainerId = savedCrypt.diffMeta.cryptContainerId,
encryptedData = diffEncrypted encryptedData = diffEncrypted
)
)) ))
}
dispatch(UpdateInstalledAppsAction( dispatchSync(UpdateInstalledAppsAction(
base = baseEncrypted, base = baseEncrypted,
diff = diffEncrypted, diff = diffEncrypted,
wipe = disableLegacySync wipe = disableLegacySync
@ -165,24 +173,29 @@ object CryptoAppListSync {
syncUtil.requestImportantSync() syncUtil.requestImportantSync()
} else { } else {
val diffEncrypted = CryptContainer.encrypt( val diffEncrypted = Threads.crypto.executeAndWait {
SavedAppsDifferenceProto.build(savedCrypt.baseHeader, diffCrypto).encodeDeflated(), CryptContainer.encrypt(
diffCryptParams.params SavedAppsDifferenceProto.build(
savedCrypt.base.decrypted.header,
diffCrypto
).encodeDeflated(),
diffCryptConfig.params
) )
}
if (diffEncrypted.size > compressedDataSizeLimit) throw TooLargeException(diffEncrypted.size) throwIfTooLarge(diffEncrypted)
Threads.database.executeAndWait { Threads.database.executeAndWait {
database.cryptContainer().updateMetadata(diffCryptParams.newMetadata) database.cryptContainer().updateMetadata(diffCryptConfig.newMetadata)
database.cryptContainer().updateData( database.cryptContainer().updateData(
CryptContainerData( CryptContainerData(
cryptContainerId = savedCrypt.diffMeta.cryptContainerId, cryptContainerId = savedCrypt.diff.meta.cryptContainerId,
encryptedData = diffEncrypted encryptedData = diffEncrypted
) )
) )
dispatch(UpdateInstalledAppsAction( dispatchSync(UpdateInstalledAppsAction(
base = null, base = null,
diff = diffEncrypted, diff = diffEncrypted,
wipe = disableLegacySync wipe = disableLegacySync
@ -193,5 +206,4 @@ object CryptoAppListSync {
} }
} }
} }
}
} }

View file

@ -45,11 +45,13 @@ object InstalledAppsUtil {
) )
} }
fun getEncryptedInstalledAppsFromDatabaseSync(database: Database, deviceId: String): DecryptedInstalledApps? { suspend fun getEncryptedInstalledAppsFromDatabase(database: Database, deviceId: String): EncryptedInstalledApps {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "getEncryptedInstalledAppsFromDatabaseSync()") Log.d(LOG_TAG, "getEncryptedInstalledAppsFromDatabase()")
} }
val (baseValue, diffValue) = Threads.database.executeAndWait {
database.runInTransaction {
val baseValue = database.cryptContainer().getCryptoFullDataSyncByDeviceId( val baseValue = database.cryptContainer().getCryptoFullDataSyncByDeviceId(
deviceId = deviceId, deviceId = deviceId,
type = CryptContainerMetadata.TYPE_APP_LIST_BASE type = CryptContainerMetadata.TYPE_APP_LIST_BASE
@ -60,22 +62,17 @@ object InstalledAppsUtil {
type = CryptContainerMetadata.TYPE_APP_LIST_DIFF type = CryptContainerMetadata.TYPE_APP_LIST_DIFF
) )
Pair(baseValue, diffValue)
}
}
return Threads.crypto.executeAndWait {
val baseDecrypted = try {
if ( if (
baseValue == null || baseValue != null &&
baseValue.metadata.currentGenerationKey == null || baseValue.metadata.currentGenerationKey != null &&
baseValue.metadata.status != CryptContainerMetadata.ProcessingStatus.Finished || baseValue.metadata.status == CryptContainerMetadata.ProcessingStatus.Finished
diffValue == null ||
diffValue.metadata.currentGenerationKey == null ||
diffValue.metadata.status != CryptContainerMetadata.ProcessingStatus.Finished
) { ) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "incomplete data")
}
return null
}
val (baseHeader, baseDecrypted, diffDecrypted) = try {
val baseHeader = CryptContainer.Header.read(baseValue.encryptedData) val baseHeader = CryptContainer.Header.read(baseValue.encryptedData)
val baseDecrypted = CryptContainer.decrypt( val baseDecrypted = CryptContainer.decrypt(
@ -83,52 +80,79 @@ object InstalledAppsUtil {
baseValue.encryptedData baseValue.encryptedData
) )
val baseData = InstalledAppsProto.ADAPTER.decodeInflated(baseDecrypted)
Decrypted(
data = baseData,
header = baseHeader
)
} else null
} catch (ex: CryptException) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not decrypt previous base data", ex)
}
null
} catch (ex: IOException) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not decode previous base data", ex)
}
null
}
val diffDecrypted = try {
if (
diffValue != null &&
diffValue.metadata.currentGenerationKey != null &&
diffValue.metadata.status == CryptContainerMetadata.ProcessingStatus.Finished
) {
val diffHeader = CryptContainer.Header.read(diffValue.encryptedData)
val diffDecrypted = CryptContainer.decrypt( val diffDecrypted = CryptContainer.decrypt(
diffValue.metadata.currentGenerationKey, diffValue.metadata.currentGenerationKey,
diffValue.encryptedData diffValue.encryptedData
) )
Triple(baseHeader, baseDecrypted, diffDecrypted) val diffData =
} catch (ex: CryptException) { SavedAppsDifferenceProto.ADAPTER.decodeInflated(diffDecrypted).apps
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not decrypt previous data", ex)
}
return null
}
val (base, diff) = try {
val base = InstalledAppsProto.ADAPTER.decodeInflated(baseDecrypted)
val diff = SavedAppsDifferenceProto.ADAPTER.decodeInflated(diffDecrypted).apps
?: InstalledAppsDifferenceProto() ?: InstalledAppsDifferenceProto()
Pair(base, diff) Decrypted(
data = diffData,
header = diffHeader
)
} else null
} catch (ex: CryptException) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not decrypt previous diff data", ex)
}
null
} catch (ex: IOException) { } catch (ex: IOException) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not decode data", ex) Log.d(LOG_TAG, "could not decode previous diff data", ex)
} }
return null null
} }
return DecryptedInstalledApps( EncryptedInstalledApps(
base = base, base = baseValue?.let { Encrypted(it.metadata, baseDecrypted) },
baseMeta = baseValue.metadata, diff = diffValue?.let { Encrypted(it.metadata, diffDecrypted) }
baseHeader = baseHeader,
diff = diff,
diffMeta = diffValue.metadata
) )
} }
}
data class DecryptedInstalledApps( data class EncryptedInstalledApps(
val base: InstalledAppsProto, val base: Encrypted<InstalledAppsProto>?,
val baseHeader: CryptContainer.Header, val diff: Encrypted<InstalledAppsDifferenceProto>?
val baseMeta: CryptContainerMetadata,
val diff: InstalledAppsDifferenceProto,
val diffMeta: CryptContainerMetadata
) )
data class Encrypted<T>(val meta: CryptContainerMetadata, val decrypted: Decrypted<T>?)
data class Decrypted<T>(val data: T, val header: CryptContainer.Header)
suspend fun getInstalledAppsFromOs(appLogic: AppLogic, deviceState: DeviceState): InstalledAppsProto { suspend fun getInstalledAppsFromOs(appLogic: AppLogic, deviceState: DeviceState): InstalledAppsProto {
val apps = kotlin.run { val apps = kotlin.run {
val currentlyInstalled = Threads.backgroundOSInteraction.executeAndWait { val currentlyInstalled = Threads.backgroundOSInteraction.executeAndWait {

View file

@ -59,7 +59,15 @@ object CryptDataHandler {
} else oldItem.copy(serverVersion = data.version) } else oldItem.copy(serverVersion = data.version)
if (deviceId == database.config().getOwnDeviceIdSync()) { if (deviceId == database.config().getOwnDeviceIdSync()) {
database.cryptContainer().updateMetadata(currentItem) val updatedItem = if (isUnmodified)
currentItem
else
// this tells the sync logic to not use the existing encrypted data and start a new generation
// there is no need to actually save the data from the server because it is not used at all
// the DecryptProcessor ignores data for the device itself
currentItem.copy(status = CryptContainerMetadata.ProcessingStatus.Unprocessed)
database.cryptContainer().updateMetadata(updatedItem)
return Result(didCreateKeyRequests = false) return Result(didCreateKeyRequests = false)
} }