mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 01:39:22 +02:00
Add App List Encryption
This commit is contained in:
parent
dc662a78f8
commit
62c83f045a
61 changed files with 4995 additions and 389 deletions
|
@ -19,6 +19,7 @@ apply plugin: 'kotlin-android'
|
|||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'com.squareup.wire'
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
|
@ -151,6 +152,10 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
wire {
|
||||
kotlin {}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def nav_version = "2.5.0"
|
||||
def room_version = "2.4.2"
|
||||
|
@ -211,4 +216,6 @@ dependencies {
|
|||
implementation 'org.whispersystems:curve25519-java:0.5.0'
|
||||
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
|
||||
api "com.squareup.wire:wire-runtime:4.4.0"
|
||||
}
|
||||
|
|
1603
app/schemas/io.timelimit.android.data.RoomDatabase/43.json
Normal file
1603
app/schemas/io.timelimit.android.data.RoomDatabase/43.json
Normal file
File diff suppressed because it is too large
Load diff
138
app/src/main/java/io/timelimit/android/crypto/CryptContainer.kt
Normal file
138
app/src/main/java/io/timelimit/android/crypto/CryptContainer.kt
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* 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.crypto
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.AEADBadTagException
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object CryptContainer {
|
||||
const val KEY_SIZE = 16
|
||||
private const val AUTH_TAG_BITS = 128
|
||||
private const val AUTH_TAG_BYTES = AUTH_TAG_BITS / 8
|
||||
|
||||
data class EncryptParameters(val generation: Long, val counter: Long, val key: ByteArray) {
|
||||
companion object {
|
||||
fun generate() = EncryptParameters(
|
||||
key = generateKey(),
|
||||
generation = 0,
|
||||
counter = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Header (
|
||||
val generation: Long,
|
||||
val counter: Long,
|
||||
val iv: Int
|
||||
) {
|
||||
companion object {
|
||||
const val SIZE = 8 + 8 + 4
|
||||
|
||||
fun read(input: ByteArray): Header {
|
||||
validate(input)
|
||||
|
||||
val buffer = ByteBuffer.wrap(input)
|
||||
|
||||
val generation = buffer.getLong(0)
|
||||
val counter = buffer.getLong(8)
|
||||
val iv = buffer.getInt(16)
|
||||
|
||||
return Header(
|
||||
generation = generation,
|
||||
counter = counter,
|
||||
iv = iv
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun write(output: ByteBuffer) {
|
||||
output.putLong(0, generation)
|
||||
output.putLong(8, counter)
|
||||
output.putInt(16, iv)
|
||||
}
|
||||
}
|
||||
|
||||
fun validate(input: ByteArray) {
|
||||
if (input.size < Header.SIZE + AUTH_TAG_BYTES) throw CryptException.InvalidContainer()
|
||||
}
|
||||
|
||||
fun generateKey() = ByteArray(KEY_SIZE).also { SecureRandom().nextBytes(it) }
|
||||
|
||||
private fun buildIV(counter: Long, iv: Int): ByteArray {
|
||||
val result = ByteArray(12)
|
||||
|
||||
ByteBuffer.wrap(result).also {
|
||||
it.putInt(0, iv)
|
||||
it.putLong(4, counter)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun buildSecretKey(key: ByteArray): SecretKeySpec {
|
||||
if (key.size != KEY_SIZE) throw CryptException.InvalidKey()
|
||||
|
||||
return SecretKeySpec(key, "AES")
|
||||
}
|
||||
|
||||
private fun buildAAD(generation: Long): ByteArray = ByteArray(8).also { result ->
|
||||
ByteBuffer.wrap(result).putLong(0, generation)
|
||||
}
|
||||
|
||||
fun decrypt(key: ByteArray, input: ByteArray): ByteArray {
|
||||
val header = Header.read(input)
|
||||
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding").also {
|
||||
it.init(Cipher.DECRYPT_MODE, buildSecretKey(key), GCMParameterSpec(AUTH_TAG_BITS, buildIV(header.counter, header.iv)))
|
||||
it.updateAAD(buildAAD(header.generation))
|
||||
}
|
||||
|
||||
try {
|
||||
return cipher.doFinal(input, Header.SIZE, input.size - Header.SIZE)
|
||||
} catch (ex: AEADBadTagException) {
|
||||
throw CryptException.WrongKey()
|
||||
}
|
||||
}
|
||||
|
||||
fun encrypt(input: ByteArray, params: EncryptParameters): ByteArray {
|
||||
val iv = SecureRandom().nextInt()
|
||||
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding").also {
|
||||
it.init(Cipher.ENCRYPT_MODE, buildSecretKey(params.key), GCMParameterSpec(AUTH_TAG_BITS, buildIV(params.counter, iv)))
|
||||
it.updateAAD(buildAAD(params.generation))
|
||||
}
|
||||
|
||||
val result = ByteArray(Header.SIZE + input.size + AUTH_TAG_BYTES)
|
||||
val buffer = ByteBuffer.wrap(result)
|
||||
|
||||
Header(
|
||||
iv = iv,
|
||||
counter = params.counter,
|
||||
generation = params.generation
|
||||
).write(buffer)
|
||||
|
||||
if (cipher.doFinal(input, 0, input.size, result, Header.SIZE) != input.size + AUTH_TAG_BYTES) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -53,6 +53,9 @@ object Curve25519 {
|
|||
fun sign(privateKey: ByteArray, message: ByteArray): ByteArray = instance.calculateSignature(privateKey, message)
|
||||
|
||||
fun validateSignature(publicKey: ByteArray, message: ByteArray, signature: ByteArray) = instance.verifySignature(publicKey, message, signature)
|
||||
|
||||
// 32 bytes
|
||||
fun sharedSecret(publicKey: ByteArray, privateKey: ByteArray) = instance.calculateAgreement(publicKey, privateKey)
|
||||
}
|
||||
|
||||
fun Curve25519KeyPair.serialize(): ByteArray {
|
||||
|
|
23
app/src/main/java/io/timelimit/android/crypto/Exception.kt
Normal file
23
app/src/main/java/io/timelimit/android/crypto/Exception.kt
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.crypto
|
||||
|
||||
sealed class CryptException: RuntimeException() {
|
||||
class InvalidContainer: CryptException()
|
||||
class InvalidKey: CryptException()
|
||||
class WrongKey: CryptException()
|
||||
}
|
|
@ -41,6 +41,10 @@ interface Database {
|
|||
fun categoryNetworkId(): CategoryNetworkIdDao
|
||||
fun childTasks(): ChildTaskDao
|
||||
fun timeWarning(): CategoryTimeWarningDao
|
||||
fun cryptContainer(): CryptContainerDao
|
||||
fun cryptContainerKeyRequest(): CryptContainerKeyRequestDao
|
||||
fun cryptContainerKeyResult(): CryptContainerKeyResultDao
|
||||
fun deviceKey(): DeviceKeyDao
|
||||
|
||||
fun <T> runInTransaction(block: () -> T): T
|
||||
fun <T> runInUnobservedTransaction(block: () -> T): T
|
||||
|
|
|
@ -299,6 +299,25 @@ object DatabaseMigrations {
|
|||
}
|
||||
}
|
||||
|
||||
private val MIGRATE_TP_V43 = object: Migration(42, 43) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_metadata` (`crypt_container_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `device_id` TEXT, `category_id` TEXT, `type` INTEGER NOT NULL, `server_version` TEXT NOT NULL, `current_generation` INTEGER NOT NULL, `current_generation_first_timestamp` INTEGER NOT NULL, `next_counter` INTEGER NOT NULL, `current_generation_key` BLOB, `status` INTEGER NOT NULL, FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_metadata_device_id` ON `crypt_container_metadata` (`device_id`)")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_metadata_category_id` ON `crypt_container_metadata` (`category_id`)")
|
||||
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_data` (`crypt_container_id` INTEGER NOT NULL, `encrypted_data` BLOB NOT NULL, PRIMARY KEY(`crypt_container_id`), FOREIGN KEY(`crypt_container_id`) REFERENCES `crypt_container_metadata`(`crypt_container_id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_pending_key_request` (`crypt_container_id` INTEGER NOT NULL, `request_time_crypt_container_generation` INTEGER NOT NULL, `request_sequence_id` INTEGER NOT NULL, `request_key` BLOB NOT NULL, PRIMARY KEY(`crypt_container_id`), FOREIGN KEY(`crypt_container_id`) REFERENCES `crypt_container_metadata`(`crypt_container_id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_crypt_container_pending_key_request_request_sequence_id` ON `crypt_container_pending_key_request` (`request_sequence_id`)")
|
||||
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `crypt_container_key_result` (`request_sequence_id` INTEGER NOT NULL, `device_id` TEXT NOT NULL, `status` INTEGER NOT NULL, PRIMARY KEY(`request_sequence_id`, `device_id`), FOREIGN KEY(`request_sequence_id`) REFERENCES `crypt_container_pending_key_request`(`request_sequence_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_key_result_request_sequence_id` ON `crypt_container_key_result` (`request_sequence_id`)")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_crypt_container_key_result_device_id` ON `crypt_container_key_result` (`device_id`)")
|
||||
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `device_public_key` (`device_id` TEXT NOT NULL, `public_key` BLOB NOT NULL, `next_sequence_number` INTEGER NOT NULL, PRIMARY KEY(`device_id`), FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
}
|
||||
}
|
||||
|
||||
val ALL = arrayOf(
|
||||
MIGRATE_TO_V2,
|
||||
MIGRATE_TO_V3,
|
||||
|
@ -340,6 +359,7 @@ object DatabaseMigrations {
|
|||
MIGRATE_TO_V39,
|
||||
MIGRATE_TO_V40,
|
||||
MIGRATE_TO_V41,
|
||||
MIGRATE_TO_V42
|
||||
MIGRATE_TO_V42,
|
||||
MIGRATE_TP_V43
|
||||
)
|
||||
}
|
||||
|
|
|
@ -52,8 +52,13 @@ import java.util.concurrent.TimeUnit
|
|||
UserLimitLoginCategory::class,
|
||||
CategoryNetworkId::class,
|
||||
ChildTask::class,
|
||||
CategoryTimeWarning::class
|
||||
], version = 42)
|
||||
CategoryTimeWarning::class,
|
||||
CryptContainerMetadata::class,
|
||||
CryptContainerData::class,
|
||||
CryptContainerPendingKeyRequest::class,
|
||||
CryptContainerKeyResult::class,
|
||||
DevicePublicKey::class
|
||||
], version = 43)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
|
|
|
@ -66,7 +66,7 @@ abstract class CategoryDao {
|
|||
abstract fun updateCategoryTemporarilyBlocked(categoryId: String, blocked: Boolean, endTime: Long)
|
||||
|
||||
@Query("SELECT id, base_version, apps_version, rules_version, usedtimes_version, tasks_version FROM category")
|
||||
abstract fun getCategoriesWithVersionNumbers(): LiveData<List<CategoryWithVersionNumbers>>
|
||||
abstract fun getCategoriesWithVersionNumbersSybc(): List<CategoryWithVersionNumbers>
|
||||
|
||||
@Query("UPDATE category SET apps_version = :assignedAppsVersion WHERE id = :categoryId")
|
||||
abstract fun updateCategoryAssignedAppsVersion(categoryId: String, assignedAppsVersion: String)
|
||||
|
|
|
@ -25,6 +25,8 @@ import io.timelimit.android.data.model.ConfigurationItem
|
|||
import io.timelimit.android.data.model.ConfigurationItemType
|
||||
import io.timelimit.android.data.model.ConfigurationItemTypeConverter
|
||||
import io.timelimit.android.data.model.ConfigurationItemTypeUtil
|
||||
import io.timelimit.android.extensions.base64
|
||||
import io.timelimit.android.extensions.parseBase64
|
||||
import io.timelimit.android.extensions.toJsonReader
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.map
|
||||
|
@ -86,28 +88,16 @@ abstract class ConfigDao {
|
|||
updateValueSync(ConfigurationItemType.OwnDeviceId, deviceId)
|
||||
}
|
||||
|
||||
fun getDeviceListVersion(): LiveData<String> {
|
||||
return getValueOfKeyAsync(ConfigurationItemType.DeviceListVersion).map {
|
||||
if (it == null) {
|
||||
""
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
fun getDeviceListVersionSync(): String {
|
||||
return getValueOfKeySync(ConfigurationItemType.DeviceListVersion) ?: ""
|
||||
}
|
||||
|
||||
fun setDeviceListVersionSync(deviceListVersion: String) {
|
||||
updateValueSync(ConfigurationItemType.DeviceListVersion, deviceListVersion)
|
||||
}
|
||||
|
||||
fun getUserListVersion(): LiveData<String> {
|
||||
return getValueOfKeyAsync(ConfigurationItemType.UserListVersion).map {
|
||||
if (it == null) {
|
||||
""
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
fun getUserListVersionSync(): String {
|
||||
return getValueOfKeySync(ConfigurationItemType.UserListVersion) ?: ""
|
||||
}
|
||||
|
||||
fun setUserListVersionSync(userListVersion: String) {
|
||||
|
@ -354,4 +344,23 @@ abstract class ConfigDao {
|
|||
(getConsentFlagsSync() and (flags.inv())).toString(16)
|
||||
)
|
||||
}
|
||||
|
||||
fun getSigningKeySync() = getValueOfKeySync(ConfigurationItemType.SigningKey)?.parseBase64()
|
||||
fun getSigningKeyAsync() = getValueOfKeyAsync(ConfigurationItemType.SigningKey).map { v -> v?.let { Base64.decode(it, 0) } }
|
||||
fun setSigningKeySync(value: ByteArray) = updateValueSync(ConfigurationItemType.SigningKey, value.base64())
|
||||
|
||||
private fun getNextSigningSequenceNumberSync(): Long = getValueOfKeySync(ConfigurationItemType.SignSequenceNumber)?.toLong() ?: 0L
|
||||
private fun setNextSigningSequenceNumberSync(value: Long) = updateValueSync(ConfigurationItemType.SignSequenceNumber, value.toString())
|
||||
fun getNextSigningSequenceNumberAndIncrementIt(): Long {
|
||||
val current = getNextSigningSequenceNumberSync()
|
||||
setNextSigningSequenceNumberSync(current + 1)
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
fun getLastServerKeyRequestSequenceSync(): Long? = getValueOfKeySync(ConfigurationItemType.LastServerKeyRequestSequence)?.toLong()
|
||||
fun setLastServerKeyRequestSequenceSync(value: Long) = updateValueSync(ConfigurationItemType.LastServerKeyRequestSequence, value.toString())
|
||||
|
||||
fun getLastServerKeyResponseSequenceSync(): Long? = getValueOfKeySync(ConfigurationItemType.LastKeyResponseSequence)?.toLong()
|
||||
fun setLastServerKeyResponseSequenceSync(value: Long) = updateValueSync(ConfigurationItemType.LastKeyResponseSequence, value.toString())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.model.CryptContainerData
|
||||
import io.timelimit.android.data.model.CryptContainerMetadata
|
||||
import io.timelimit.android.data.model.CryptContainerMetadataProcessingStatusConverter
|
||||
|
||||
@Dao
|
||||
@TypeConverters(CryptContainerMetadataProcessingStatusConverter::class)
|
||||
interface CryptContainerDao {
|
||||
@Query("SELECT * FROM crypt_container_metadata WHERE crypt_container_id = :containerId")
|
||||
fun getCryptoMetadataSyncByContainerId(containerId: Long): CryptContainerMetadata?
|
||||
|
||||
@Query("SELECT * FROM crypt_container_metadata JOIN crypt_container_data USING (crypt_container_id) WHERE category_id IS NULL AND device_id = :deviceId AND type = :type")
|
||||
fun getCryptoFullDataSyncByDeviceId(deviceId: String, type: Int): MetadataAndContent?
|
||||
|
||||
@Query("SELECT * FROM crypt_container_metadata WHERE category_id IS NULL AND device_id IS NULL AND type = :type")
|
||||
fun getCryptoMetadataSyncByType(type: Int): CryptContainerMetadata?
|
||||
|
||||
@Query("SELECT * FROM crypt_container_metadata WHERE category_id IS NULL AND device_id = :deviceId AND type = :type")
|
||||
fun getCryptoMetadataSyncByDeviceId(deviceId: String, type: Int): CryptContainerMetadata?
|
||||
|
||||
@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?
|
||||
|
||||
@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")
|
||||
fun getMetadataByProcessingStatus(processingStatus: CryptContainerMetadata.ProcessingStatus): List<CryptContainerMetadata>
|
||||
|
||||
@Insert
|
||||
fun insertMetadata(container: CryptContainerMetadata): Long
|
||||
|
||||
@Update
|
||||
fun updateMetadata(container: List<CryptContainerMetadata>)
|
||||
|
||||
@Update
|
||||
fun updateMetadata(container: CryptContainerMetadata)
|
||||
|
||||
@Insert
|
||||
fun insertData(data: CryptContainerData)
|
||||
|
||||
@Update
|
||||
fun updateData(data: List<CryptContainerData>)
|
||||
|
||||
@Update
|
||||
fun updateData(data: CryptContainerData)
|
||||
|
||||
@Query("SELECT * FROM crypt_container_data WHERE crypt_container_id = :containerId")
|
||||
fun getData(containerId: Long): CryptContainerData?
|
||||
|
||||
@Query("UPDATE crypt_container_metadata SET server_version = ''")
|
||||
fun deleteAllServerVersionNumbers()
|
||||
|
||||
@Entity
|
||||
data class MetadataAndContent(
|
||||
@Embedded
|
||||
val metadata: CryptContainerMetadata,
|
||||
@ColumnInfo(name = "encrypted_data")
|
||||
val encryptedData: ByteArray
|
||||
)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import io.timelimit.android.data.model.CryptContainerPendingKeyRequest
|
||||
|
||||
@Dao
|
||||
interface CryptContainerKeyRequestDao {
|
||||
@Query("SELECT * FROM crypt_container_pending_key_request WHERE crypt_container_id = :cryptContainerId")
|
||||
fun byCryptContainerId(cryptContainerId: Long): CryptContainerPendingKeyRequest?
|
||||
|
||||
@Query("SELECT * FROM crypt_container_pending_key_request WHERE request_sequence_id = :requestId")
|
||||
fun byRequestId(requestId: Long): CryptContainerPendingKeyRequest?
|
||||
|
||||
@Insert
|
||||
fun insert(item: CryptContainerPendingKeyRequest)
|
||||
|
||||
@Delete
|
||||
fun delete(item: CryptContainerPendingKeyRequest)
|
||||
|
||||
@Query("DELETE FROM crypt_container_pending_key_request")
|
||||
fun deleteAll()
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import io.timelimit.android.data.model.CryptContainerKeyResult
|
||||
|
||||
@Dao
|
||||
interface CryptContainerKeyResultDao {
|
||||
@Query("SELECT COUNT(*) FROM crypt_container_key_result WHERE request_sequence_id = :requestSequenceNumber AND device_id = :deviceId")
|
||||
fun countResultItems(requestSequenceNumber: Long, deviceId: String): Long
|
||||
|
||||
@Insert
|
||||
fun insert(item: CryptContainerKeyResult)
|
||||
}
|
|
@ -55,7 +55,7 @@ abstract class DeviceDao {
|
|||
abstract fun updateDeviceDefaultUser(deviceId: String, defaultUserId: String)
|
||||
|
||||
@Query("SELECT id, apps_version FROM device")
|
||||
abstract fun getInstalledAppsVersions(): LiveData<List<DeviceWithAppVersion>>
|
||||
abstract fun getInstalledAppsVersionsSync(): List<DeviceWithAppVersion>
|
||||
|
||||
@Query("DELETE FROM device WHERE id IN (:deviceIds)")
|
||||
abstract fun removeDevicesById(deviceIds: List<String>)
|
||||
|
@ -92,6 +92,14 @@ abstract class DeviceDao {
|
|||
|
||||
@Query("SELECT COUNT(*) FROM device JOIN user ON (device.current_user_id = user.id) WHERE user.type = \"child\"")
|
||||
abstract fun countDevicesWithChildUser(): LiveData<Long>
|
||||
|
||||
fun getDeviceDetailDataSync() = getDeviceDetailDataSyncInternal(
|
||||
CryptContainerMetadata.TYPE_APP_LIST_BASE,
|
||||
CryptContainerMetadata.TYPE_APP_LIST_DIFF
|
||||
)
|
||||
|
||||
@Query("SELECT d.id AS device_id, c1.server_version AS app_base_version, c2.server_version AS app_diff_version FROM device d LEFT JOIN crypt_container_metadata c1 ON (c1.device_id = d.id AND c1.type = :baseType) LEFT JOIN crypt_container_metadata c2 ON (c2.device_id = d.id AND c2.type = :diffType)")
|
||||
protected abstract fun getDeviceDetailDataSyncInternal(baseType: Int, diffType: Int): List<DeviceDetailDataBase>
|
||||
}
|
||||
|
||||
data class DeviceWithAppVersion(
|
||||
|
@ -100,3 +108,12 @@ data class DeviceWithAppVersion(
|
|||
@ColumnInfo(name = "apps_version")
|
||||
val installedAppsVersions: String
|
||||
)
|
||||
|
||||
data class DeviceDetailDataBase(
|
||||
@ColumnInfo(name = "device_id")
|
||||
val deviceId: String,
|
||||
@ColumnInfo(name = "app_base_version")
|
||||
val appBaseVersion: String?,
|
||||
@ColumnInfo(name = "app_diff_version")
|
||||
val appDiffVersion: String?
|
||||
)
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import io.timelimit.android.data.model.DevicePublicKey
|
||||
|
||||
@Dao
|
||||
interface DeviceKeyDao {
|
||||
@Insert
|
||||
fun insert(key: DevicePublicKey)
|
||||
|
||||
@Update
|
||||
fun update(key: DevicePublicKey)
|
||||
|
||||
@Query("SELECT * FROM device_public_key WHERE device_id = :deviceId")
|
||||
fun getSync(deviceId: String): DevicePublicKey?
|
||||
|
||||
@Query("SELECT * FROM device_public_key WHERE device_id = :deviceId")
|
||||
fun getLive(deviceId: String): LiveData<DevicePublicKey?>
|
||||
}
|
|
@ -74,8 +74,6 @@ data class ConfigurationItem(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: validate config item values
|
||||
|
||||
enum class ConfigurationItemType {
|
||||
OwnDeviceId,
|
||||
UserListVersion,
|
||||
|
@ -102,6 +100,10 @@ enum class ConfigurationItemType {
|
|||
ServerApiLevel,
|
||||
AnnoyManualUnblockCounter,
|
||||
ConsentFlags,
|
||||
SigningKey,
|
||||
SignSequenceNumber,
|
||||
LastServerKeyRequestSequence,
|
||||
LastKeyResponseSequence
|
||||
}
|
||||
|
||||
object ConfigurationItemTypeUtil {
|
||||
|
@ -130,6 +132,10 @@ object ConfigurationItemTypeUtil {
|
|||
private const val SERVER_API_LEVEL = 24
|
||||
private const val ANNOY_MANUAL_UNBLOCK_COUNTER = 25
|
||||
private const val CONSENT_FLAGS = 26
|
||||
private const val SIGNING_KEY = 27
|
||||
private const val SIGN_SEQUENCE_NUMBER = 28
|
||||
private const val LAST_SERVER_KEY_REQUEST_SEQUENCE = 29
|
||||
private const val LAST_SERVER_KEY_RESPONSE_SEQUENCE = 30
|
||||
|
||||
val TYPES = listOf(
|
||||
ConfigurationItemType.OwnDeviceId,
|
||||
|
@ -156,7 +162,11 @@ object ConfigurationItemTypeUtil {
|
|||
ConfigurationItemType.CustomOrganizationName,
|
||||
ConfigurationItemType.ServerApiLevel,
|
||||
ConfigurationItemType.AnnoyManualUnblockCounter,
|
||||
ConfigurationItemType.ConsentFlags
|
||||
ConfigurationItemType.ConsentFlags,
|
||||
ConfigurationItemType.SigningKey,
|
||||
ConfigurationItemType.SignSequenceNumber,
|
||||
ConfigurationItemType.LastServerKeyRequestSequence,
|
||||
ConfigurationItemType.LastKeyResponseSequence
|
||||
)
|
||||
|
||||
fun serialize(value: ConfigurationItemType) = when(value) {
|
||||
|
@ -185,6 +195,10 @@ object ConfigurationItemTypeUtil {
|
|||
ConfigurationItemType.ServerApiLevel -> SERVER_API_LEVEL
|
||||
ConfigurationItemType.AnnoyManualUnblockCounter -> ANNOY_MANUAL_UNBLOCK_COUNTER
|
||||
ConfigurationItemType.ConsentFlags -> CONSENT_FLAGS
|
||||
ConfigurationItemType.SigningKey -> SIGNING_KEY
|
||||
ConfigurationItemType.SignSequenceNumber -> SIGN_SEQUENCE_NUMBER
|
||||
ConfigurationItemType.LastServerKeyRequestSequence -> LAST_SERVER_KEY_REQUEST_SEQUENCE
|
||||
ConfigurationItemType.LastKeyResponseSequence -> LAST_SERVER_KEY_RESPONSE_SEQUENCE
|
||||
}
|
||||
|
||||
fun parse(value: Int) = when(value) {
|
||||
|
@ -213,6 +227,10 @@ object ConfigurationItemTypeUtil {
|
|||
SERVER_API_LEVEL -> ConfigurationItemType.ServerApiLevel
|
||||
ANNOY_MANUAL_UNBLOCK_COUNTER -> ConfigurationItemType.AnnoyManualUnblockCounter
|
||||
CONSENT_FLAGS -> ConfigurationItemType.ConsentFlags
|
||||
SIGNING_KEY -> ConfigurationItemType.SigningKey
|
||||
SIGN_SEQUENCE_NUMBER -> ConfigurationItemType.SignSequenceNumber
|
||||
LAST_SERVER_KEY_REQUEST_SEQUENCE -> ConfigurationItemType.LastServerKeyRequestSequence
|
||||
LAST_SERVER_KEY_RESPONSE_SEQUENCE -> ConfigurationItemType.LastKeyResponseSequence
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
@ -256,6 +274,7 @@ object ExperimentalFlags {
|
|||
// const val INSTANCE_ID_FG_APP_DETECTION = 65536L
|
||||
// private const val OBSOLETE_DISABLE_FG_APP_DETECTION_FALLBACK = 131072L
|
||||
const val STRICT_OVERLAY_CHECKING = 0x40000L
|
||||
const val DISABLE_LEGACY_APP_SENDING = 0x80000L
|
||||
}
|
||||
|
||||
object ConsentFlags {
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "crypt_container_data",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = CryptContainerMetadata::class,
|
||||
childColumns = ["crypt_container_id"],
|
||||
parentColumns = ["crypt_container_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class CryptContainerData (
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "crypt_container_id")
|
||||
val cryptContainerId: Long,
|
||||
@ColumnInfo(name = "encrypted_data")
|
||||
val encryptedData: ByteArray
|
||||
)
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.data.model
|
||||
|
||||
import androidx.room.*
|
||||
|
||||
@Entity(
|
||||
tableName = "crypt_container_key_result",
|
||||
primaryKeys = ["request_sequence_id", "device_id"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = CryptContainerPendingKeyRequest::class,
|
||||
childColumns = ["request_sequence_id"],
|
||||
parentColumns = ["request_sequence_id"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = Device::class,
|
||||
childColumns = ["device_id"],
|
||||
parentColumns = ["id"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
@TypeConverters(CryptContainerKeyResultStatusConverter::class)
|
||||
data class CryptContainerKeyResult (
|
||||
@ColumnInfo(name = "request_sequence_id", index = true)
|
||||
val requestSequenceId: Long,
|
||||
@ColumnInfo(name = "device_id", index = true)
|
||||
val deviceId: String,
|
||||
val status: Status
|
||||
) {
|
||||
enum class Status {
|
||||
InvalidKey
|
||||
}
|
||||
}
|
||||
|
||||
class CryptContainerKeyResultStatusConverter {
|
||||
@TypeConverter
|
||||
fun toStatus(input: Int): CryptContainerKeyResult.Status = when (input) {
|
||||
0 -> CryptContainerKeyResult.Status.InvalidKey
|
||||
else -> CryptContainerKeyResult.Status.InvalidKey
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toInt(input: CryptContainerKeyResult.Status): Int = when (input) {
|
||||
CryptContainerKeyResult.Status.InvalidKey -> 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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.data.model
|
||||
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.crypto.CryptContainer
|
||||
|
||||
@Entity(
|
||||
tableName = "crypt_container_metadata",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Device::class,
|
||||
childColumns = ["device_id"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = Category::class,
|
||||
childColumns = ["category_id"],
|
||||
parentColumns = ["id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
@TypeConverters(CryptContainerMetadataProcessingStatusConverter::class)
|
||||
data class CryptContainerMetadata (
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = "crypt_container_id")
|
||||
val cryptContainerId: Long,
|
||||
@ColumnInfo(name = "device_id", index = true)
|
||||
val deviceId: String?,
|
||||
@ColumnInfo(name = "category_id", index = true)
|
||||
val categoryId: String?,
|
||||
val type: Int,
|
||||
@ColumnInfo(name = "server_version")
|
||||
val serverVersion: String,
|
||||
@ColumnInfo(name = "current_generation")
|
||||
val currentGeneration: Long,
|
||||
@ColumnInfo(name = "current_generation_first_timestamp")
|
||||
val currentGenerationFirstTimestamp: Long,
|
||||
@ColumnInfo(name = "next_counter")
|
||||
val nextCounter: Long,
|
||||
@ColumnInfo(name = "current_generation_key")
|
||||
val currentGenerationKey: ByteArray?,
|
||||
val status: ProcessingStatus
|
||||
) {
|
||||
companion object {
|
||||
private const val GENERATION_DURATION_LIMIT = 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||
private const val GENERATION_COUNTER_LIMIT = 16L
|
||||
|
||||
const val TYPE_APP_LIST_BASE = 1
|
||||
const val TYPE_APP_LIST_DIFF = 2
|
||||
|
||||
fun buildFor(deviceId: String?, categoryId: String?, type: Int, params: CryptContainer.EncryptParameters) = CryptContainerMetadata(
|
||||
cryptContainerId = 0,
|
||||
deviceId = deviceId,
|
||||
categoryId = categoryId,
|
||||
type = type,
|
||||
serverVersion = "",
|
||||
currentGeneration = params.generation,
|
||||
currentGenerationFirstTimestamp = System.currentTimeMillis(),
|
||||
nextCounter = params.counter + 1,
|
||||
currentGenerationKey = params.key,
|
||||
status = ProcessingStatus.Finished
|
||||
)
|
||||
|
||||
fun isTypeValid(type: Int) = type == TYPE_APP_LIST_BASE || type == TYPE_APP_LIST_DIFF
|
||||
}
|
||||
|
||||
enum class ProcessingStatus {
|
||||
MissingKey,
|
||||
DowngradeDetected,
|
||||
Unprocessed,
|
||||
CryptoDamage,
|
||||
ContentDamage,
|
||||
Finished
|
||||
}
|
||||
|
||||
fun needsNewGeneration(): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val timeWentBackwards = now < currentGenerationFirstTimestamp
|
||||
val timeLimitReached = now >= currentGenerationFirstTimestamp + GENERATION_DURATION_LIMIT
|
||||
val counterLimitReached = nextCounter >= GENERATION_COUNTER_LIMIT
|
||||
|
||||
return timeWentBackwards or timeLimitReached or counterLimitReached
|
||||
}
|
||||
|
||||
fun prepareEncryption(forceNewGeneration: Boolean): PrepareEncryptionResult {
|
||||
return if (needsNewGeneration() || forceNewGeneration || currentGenerationKey == null) {
|
||||
val newKey = CryptContainer.generateKey()
|
||||
val generation = currentGeneration + 1
|
||||
|
||||
PrepareEncryptionResult(
|
||||
params = CryptContainer.EncryptParameters(
|
||||
key = newKey,
|
||||
generation = generation,
|
||||
counter = 0
|
||||
),
|
||||
newMetadata = copy(
|
||||
currentGeneration = generation,
|
||||
nextCounter = 1,
|
||||
currentGenerationFirstTimestamp = System.currentTimeMillis(),
|
||||
currentGenerationKey = newKey
|
||||
)
|
||||
)
|
||||
} else {
|
||||
PrepareEncryptionResult(
|
||||
params = CryptContainer.EncryptParameters(
|
||||
key = currentGenerationKey,
|
||||
generation = currentGeneration,
|
||||
counter = nextCounter
|
||||
),
|
||||
newMetadata = copy(
|
||||
nextCounter = nextCounter + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class PrepareEncryptionResult (
|
||||
val params: CryptContainer.EncryptParameters,
|
||||
val newMetadata: CryptContainerMetadata
|
||||
)
|
||||
}
|
||||
|
||||
class CryptContainerMetadataProcessingStatusConverter {
|
||||
@TypeConverter
|
||||
fun toStatus(input: Int): CryptContainerMetadata.ProcessingStatus = when (input) {
|
||||
0 -> CryptContainerMetadata.ProcessingStatus.MissingKey
|
||||
1 -> CryptContainerMetadata.ProcessingStatus.DowngradeDetected
|
||||
2 -> CryptContainerMetadata.ProcessingStatus.Unprocessed
|
||||
3 -> CryptContainerMetadata.ProcessingStatus.CryptoDamage
|
||||
4 -> CryptContainerMetadata.ProcessingStatus.ContentDamage
|
||||
5 -> CryptContainerMetadata.ProcessingStatus.Finished
|
||||
else -> CryptContainerMetadata.ProcessingStatus.CryptoDamage
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toInt(input: CryptContainerMetadata.ProcessingStatus): Int = when (input) {
|
||||
CryptContainerMetadata.ProcessingStatus.MissingKey -> 0
|
||||
CryptContainerMetadata.ProcessingStatus.DowngradeDetected -> 1
|
||||
CryptContainerMetadata.ProcessingStatus.Unprocessed -> 2
|
||||
CryptContainerMetadata.ProcessingStatus.CryptoDamage -> 3
|
||||
CryptContainerMetadata.ProcessingStatus.ContentDamage -> 4
|
||||
CryptContainerMetadata.ProcessingStatus.Finished -> 5
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.data.model
|
||||
|
||||
import androidx.room.*
|
||||
|
||||
@Entity(
|
||||
tableName = "crypt_container_pending_key_request",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = CryptContainerMetadata::class,
|
||||
childColumns = ["crypt_container_id"],
|
||||
parentColumns = ["crypt_container_id"],
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
onUpdate = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(
|
||||
unique = true,
|
||||
value = ["request_sequence_id"]
|
||||
)
|
||||
]
|
||||
)
|
||||
data class CryptContainerPendingKeyRequest (
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "crypt_container_id")
|
||||
val cryptContainerId: Long,
|
||||
@ColumnInfo(name = "request_time_crypt_container_generation")
|
||||
val requestTimeCryptContainerGeneration: Long,
|
||||
@ColumnInfo(name = "request_sequence_id")
|
||||
val requestSequenceId: Long,
|
||||
@ColumnInfo(name = "request_key")
|
||||
val requestKey: ByteArray
|
||||
)
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.data.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "device_public_key",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Device::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["device_id"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class DevicePublicKey (
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "device_id")
|
||||
val deviceId: String,
|
||||
@ColumnInfo(name = "public_key")
|
||||
val publicKey: ByteArray,
|
||||
@ColumnInfo(name = "next_sequence_number")
|
||||
val nextSequenceNumber: Long
|
||||
)
|
|
@ -22,6 +22,7 @@ import androidx.room.*
|
|||
import io.timelimit.android.crypto.Curve25519
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.extensions.parseBase64
|
||||
|
||||
@Entity(
|
||||
tableName = "user_key",
|
||||
|
@ -64,7 +65,7 @@ data class UserKey(
|
|||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
USER_ID -> userId = reader.nextString()
|
||||
PUBLIC_KEY -> publicKey = Base64.decode(reader.nextString(), 0)
|
||||
PUBLIC_KEY -> publicKey = reader.nextString().parseBase64()
|
||||
LAST_USE -> lastUse = reader.nextLong()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
|
|
21
app/src/main/java/io/timelimit/android/extensions/String.kt
Normal file
21
app/src/main/java/io/timelimit/android/extensions/String.kt
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 android.util.Base64
|
||||
|
||||
fun String.parseBase64() = Base64.decode(this, Base64.DEFAULT)
|
||||
fun ByteArray.base64() = Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP)
|
|
@ -91,6 +91,8 @@ abstract class PlatformIntegration(
|
|||
|
||||
abstract fun getExitLog(length: Int): List<ExitLogItem>
|
||||
|
||||
abstract fun showNewDeviceNotification(title: String)
|
||||
|
||||
var installedAppsChangeListener: Runnable? = null
|
||||
var systemClockChangeListener: Runnable? = null
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ import kotlinx.coroutines.channels.consumeEach
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
||||
|
@ -821,4 +822,25 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
|||
.map { ExitLogItem.fromApplicationExitInfo(it) }
|
||||
} else emptyList()
|
||||
}
|
||||
|
||||
override fun showNewDeviceNotification(title: String) {
|
||||
NotificationChannels.createNotificationChannels(notificationManager, context)
|
||||
|
||||
notificationManager.notify(
|
||||
UUID.randomUUID().toString(),
|
||||
NotificationIds.NEW_DEVICE,
|
||||
NotificationCompat.Builder(context, NotificationChannels.NEW_DEVICE)
|
||||
.setSmallIcon(R.drawable.ic_stat_timelapse)
|
||||
.setContentTitle(context.getString(R.string.notification_new_device_title))
|
||||
.setContentText(title)
|
||||
.setContentIntent(BackgroundActionService.getOpenAppIntent(context))
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setShowWhen(true)
|
||||
.setLocalOnly(true)
|
||||
.setAutoCancel(false)
|
||||
.setOngoing(false)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ object NotificationIds {
|
|||
const val LOCAL_UPDATE_NOTIFICATION = 7
|
||||
const val WORKER_REPORT_UNINSTALL = 8
|
||||
const val WORKER_SYNC_BACKGROUND = 9
|
||||
const val NEW_DEVICE = 10
|
||||
}
|
||||
|
||||
object NotificationChannels {
|
||||
|
@ -45,6 +46,7 @@ object NotificationChannels {
|
|||
const val BACKGROUND_SYNC_NOTIFICATION = "background sync"
|
||||
const val TEMP_ALLOWED_APP = "temporarily allowed App"
|
||||
const val APP_RESET = "app reset"
|
||||
const val NEW_DEVICE = "new device"
|
||||
|
||||
private fun createAppStatusChannel(notificationManager: NotificationManager, context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
@ -193,6 +195,25 @@ object NotificationChannels {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createNewDeviceChannel(notificationManager: NotificationManager, context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NEW_DEVICE,
|
||||
context.getString(R.string.notification_channel_new_device_title),
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = context.getString(R.string.notification_channel_new_device_description)
|
||||
enableLights(false)
|
||||
setSound(null, null)
|
||||
enableVibration(false)
|
||||
setShowBadge(true)
|
||||
lockscreenVisibility = NotificationCompat.VISIBILITY_SECRET
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createNotificationChannels(notificationManager: NotificationManager, context: Context) {
|
||||
createAppStatusChannel(notificationManager, context)
|
||||
createBlockedNotificationChannel(notificationManager, context)
|
||||
|
@ -203,6 +224,7 @@ object NotificationChannels {
|
|||
createBackgroundSyncChannel(notificationManager, context)
|
||||
createTempAllowedAppChannel(notificationManager, context)
|
||||
createAppResetChannel(notificationManager, context)
|
||||
createNewDeviceChannel(notificationManager, context)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -191,4 +191,6 @@ class DummyIntegration(
|
|||
): Boolean = false
|
||||
|
||||
override fun getExitLog(length: Int): List<ExitLogItem> = emptyList()
|
||||
|
||||
override fun showNewDeviceNotification(title: String) = Unit
|
||||
}
|
||||
|
|
|
@ -167,6 +167,8 @@ fun <T1, T2> mergeLiveDataWaitForValues(d1: LiveData<T1>, d2: LiveData<T2>): Liv
|
|||
return result
|
||||
}
|
||||
|
||||
data class SevenTuple<A, B, C, D, E, F, G>(val first: A, val second: B, val third: C, val forth: D, val fifth: E, val sixth: F, val seventh: G)
|
||||
|
||||
fun <T1, T2, T3> mergeLiveDataWaitForValues(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>): LiveData<Triple<T1, T2, T3>> {
|
||||
val result = MediatorLiveData<Triple<T1, T2, T3>>()
|
||||
var state = Triple<Option<T1>, Option<T2>, Option<T3>>(Option.None(), Option.None(), Option.None())
|
||||
|
@ -281,5 +283,113 @@ fun <T1, T2, T3, T4, T5> mergeLiveDataWaitForValues(d1: LiveData<T1>, d2: LiveDa
|
|||
update()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun <T1, T2, T3, T4, T5, T6> mergeLiveDataWaitForValues(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>, d5: LiveData<T5>, d6: LiveData<T6>): LiveData<SixTuple<T1, T2, T3, T4, T5, T6>> {
|
||||
val result = MediatorLiveData<SixTuple<T1, T2, T3, T4, T5, T6>>()
|
||||
var state = SixTuple<Option<T1>, Option<T2>, Option<T3>, Option<T4>, Option<T5>, Option<T6>>(Option.None(), Option.None(), Option.None(), Option.None(), Option.None(), Option.None())
|
||||
|
||||
fun update() {
|
||||
val (a, b, c, d, e, f) = state
|
||||
|
||||
if (a is Option.Some && b is Option.Some && c is Option.Some && d is Option.Some && e is Option.Some && f is Option.Some) {
|
||||
result.value = SixTuple(a.value, b.value, c.value, d.value, e.value, f.value)
|
||||
}
|
||||
}
|
||||
|
||||
result.addSource(d1) {
|
||||
state = state.copy(first = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d2) {
|
||||
state = state.copy(second = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d3) {
|
||||
state = state.copy(third = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d4) {
|
||||
state = state.copy(forth = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d5) {
|
||||
state = state.copy(fifth = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d6) {
|
||||
state = state.copy(sixth = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun <T1, T2, T3, T4, T5, T6, T7> mergeLiveDataWaitForValues(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>, d5: LiveData<T5>, d6: LiveData<T6>, d7: LiveData<T7>): LiveData<SevenTuple<T1, T2, T3, T4, T5, T6, T7>> {
|
||||
val result = MediatorLiveData<SevenTuple<T1, T2, T3, T4, T5, T6, T7>>()
|
||||
var state = SevenTuple<Option<T1>, Option<T2>, Option<T3>, Option<T4>, Option<T5>, Option<T6>, Option<T7>>(Option.None(), Option.None(), Option.None(), Option.None(), Option.None(), Option.None(), Option.None())
|
||||
|
||||
fun update() {
|
||||
val (a, b, c, d, e, f, g) = state
|
||||
|
||||
if (a is Option.Some && b is Option.Some && c is Option.Some && d is Option.Some && e is Option.Some && f is Option.Some && g is Option.Some) {
|
||||
result.value = SevenTuple(a.value, b.value, c.value, d.value, e.value, f.value, g.value)
|
||||
}
|
||||
}
|
||||
|
||||
result.addSource(d1) {
|
||||
state = state.copy(first = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d2) {
|
||||
state = state.copy(second = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d3) {
|
||||
state = state.copy(third = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d4) {
|
||||
state = state.copy(forth = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d5) {
|
||||
state = state.copy(fifth = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d6) {
|
||||
state = state.copy(sixth = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d7) {
|
||||
state = state.copy(seventh = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -21,14 +21,13 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.Transformations
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.Device
|
||||
import io.timelimit.android.data.model.ExperimentalFlags
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.integration.platform.PlatformIntegration
|
||||
import io.timelimit.android.integration.time.TimeApi
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.applist.SyncInstalledAppsLogic
|
||||
import io.timelimit.android.sync.SyncUtil
|
||||
import io.timelimit.android.sync.network.api.ServerApi
|
||||
import io.timelimit.android.sync.websocket.NetworkStatus
|
||||
import io.timelimit.android.sync.websocket.NetworkStatusInterface
|
||||
import io.timelimit.android.sync.websocket.WebsocketClientCreator
|
||||
|
||||
|
|
|
@ -15,16 +15,26 @@
|
|||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
|
||||
class ServerApiLevelLogic(logic: AppLogic) {
|
||||
val infoLive = logic.database.config().getDeviceAuthTokenAsync().switchMap { authToken ->
|
||||
companion object {
|
||||
fun getSync(database: Database): ServerApiLevelInfo = if (database.config().getDeviceAuthTokenSync().isEmpty())
|
||||
ServerApiLevelInfo.Offline
|
||||
else
|
||||
ServerApiLevelInfo.Online(serverLevel = database.config().getServerApiLevelSync())
|
||||
}
|
||||
|
||||
private val database = logic.database
|
||||
|
||||
val infoLive = database.config().getDeviceAuthTokenAsync().switchMap { authToken ->
|
||||
if (authToken.isEmpty())
|
||||
liveDataFromNonNullValue(ServerApiLevelInfo.Offline)
|
||||
else
|
||||
logic.database.config().getServerApiLevelLive().map { apiLevel ->
|
||||
database.config().getServerApiLevelLive().map { apiLevel ->
|
||||
ServerApiLevelInfo.Online(serverLevel = apiLevel)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,261 +0,0 @@
|
|||
/*
|
||||
* 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.logic
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.data.model.ConsentFlags
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.sync.actions.*
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "SyncInstalledAppsLogic"
|
||||
}
|
||||
|
||||
private val doSyncLock = Mutex()
|
||||
private var requestSync = MutableLiveData<Boolean>().apply { value = false }
|
||||
|
||||
private fun requestSync() {
|
||||
requestSync.value = true
|
||||
}
|
||||
|
||||
private val deviceStateLive = mergeLiveDataWaitForValues(
|
||||
appLogic.deviceEntryIfEnabled,
|
||||
appLogic.database.config().isConsentFlagSetAsync(ConsentFlags.APP_LIST_SYNC),
|
||||
appLogic.database.config().getDeviceAuthTokenAsync().map { it.isEmpty() },
|
||||
appLogic.deviceUserEntry,
|
||||
appLogic.deviceEntryIfEnabled.switchMap { deviceEntry ->
|
||||
val defaultUser = deviceEntry?.defaultUser
|
||||
|
||||
if (defaultUser.isNullOrEmpty()) liveDataFromNullableValue(null)
|
||||
else appLogic.database.user().getUserByIdLive(defaultUser)
|
||||
}
|
||||
).map { (deviceEntry, hasSyncConsent, isLocalMode, deviceUser, deviceDefaultUser) ->
|
||||
deviceEntry?.let { device ->
|
||||
DeviceState(
|
||||
id = device.id,
|
||||
isCurrentUserChild = deviceUser?.type == UserType.Child,
|
||||
isDefaultUserChild = deviceDefaultUser?.type == UserType.Child,
|
||||
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
|
||||
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner,
|
||||
hasSyncConsent = hasSyncConsent,
|
||||
isLocalMode = isLocalMode
|
||||
)
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
|
||||
val shouldAskForConsent = deviceStateLive.map { it?.shouldAskForConsent ?: false }.ignoreUnchanged()
|
||||
|
||||
private fun getDeviceStateSync(): DeviceState? {
|
||||
val userAndDeviceData = appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync() ?: return null
|
||||
val deviceRelatedData = userAndDeviceData.deviceRelatedData
|
||||
val device = deviceRelatedData.deviceEntry
|
||||
val defaultUser = if (device.defaultUser.isNotEmpty()) appLogic.database.user().getUserByIdSync(device.defaultUser) else null
|
||||
|
||||
return DeviceState(
|
||||
id = device.id,
|
||||
isCurrentUserChild = userAndDeviceData.userRelatedData?.user?.type == UserType.Child,
|
||||
isDefaultUserChild = defaultUser?.type == UserType.Child,
|
||||
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
|
||||
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner,
|
||||
hasSyncConsent = deviceRelatedData.consentFlags and ConsentFlags.APP_LIST_SYNC == ConsentFlags.APP_LIST_SYNC,
|
||||
isLocalMode = deviceRelatedData.isLocalMode
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() }
|
||||
deviceStateLive.observeForever { requestSync() }
|
||||
|
||||
runAsyncExpectForever { syncLoop() }
|
||||
}
|
||||
|
||||
private suspend fun syncLoop() {
|
||||
// wait a moment before the first sync
|
||||
appLogic.timeApi.sleep(15 * 1000)
|
||||
|
||||
while (true) {
|
||||
requestSync.waitUntilValueMatches { it == true }
|
||||
requestSync.value = false
|
||||
|
||||
try {
|
||||
doSyncNow()
|
||||
|
||||
// maximal 1 time per 5 seconds
|
||||
appLogic.timeApi.sleep(5 * 1000)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "could not sync installed app list", ex)
|
||||
}
|
||||
|
||||
Toast.makeText(appLogic.context, R.string.background_logic_toast_sync_apps, Toast.LENGTH_SHORT).show()
|
||||
|
||||
appLogic.timeApi.sleep(45 * 1000)
|
||||
requestSync.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doSyncNow() {
|
||||
doSyncLock.withLock {
|
||||
val deviceState = Threads.database.executeAndWait { getDeviceStateSync() } ?: return
|
||||
|
||||
if (deviceState.isLocalMode) {
|
||||
// local mode -> sync always
|
||||
} else {
|
||||
// connected mode -> don't sync always
|
||||
if (!deviceState.hasSyncConsent) return@withLock
|
||||
if (!deviceState.hasAnyChildUser) return@withLock
|
||||
}
|
||||
|
||||
val deviceId = deviceState.id
|
||||
|
||||
val currentlyInstalledApps = getCurrentApps(deviceId)
|
||||
|
||||
run {
|
||||
val currentlySaved = appLogic.database.app().getAppsByDeviceIdAsync(deviceId = deviceId).waitForNonNullValue().associateBy { app -> app.packageName }
|
||||
|
||||
// skip all items for removal which are still saved locally
|
||||
val itemsToRemove = HashMap(currentlySaved)
|
||||
currentlyInstalledApps.forEach { (packageName, _) -> itemsToRemove.remove(packageName) }
|
||||
|
||||
// only add items which are not the same locally
|
||||
val itemsToAdd = currentlyInstalledApps.filter { (packageName, app) -> currentlySaved[packageName] != app }
|
||||
|
||||
// save the changes
|
||||
if (itemsToRemove.isNotEmpty()) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
action = RemoveInstalledAppsAction(packageNames = itemsToRemove.keys.toList()),
|
||||
appLogic = appLogic,
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
)
|
||||
}
|
||||
|
||||
if (itemsToAdd.isNotEmpty()) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
action = AddInstalledAppsAction(
|
||||
apps = itemsToAdd.map {
|
||||
(_, app) ->
|
||||
|
||||
InstalledApp(
|
||||
packageName = app.packageName,
|
||||
title = app.title,
|
||||
recommendation = app.recommendation,
|
||||
isLaunchable = app.isLaunchable
|
||||
)
|
||||
}
|
||||
),
|
||||
appLogic = appLogic,
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
fun buildKey(activity: AppActivity) = "${activity.appPackageName}:${activity.activityClassName}"
|
||||
|
||||
val currentlyInstalled = if (deviceState.enableActivityLevelBlocking)
|
||||
Threads.backgroundOSInteraction.executeAndWait {
|
||||
val realActivities = appLogic.platformIntegration.getLocalAppActivities(deviceId = deviceId)
|
||||
val dummyActivities = currentlyInstalledApps.keys.map { packageName ->
|
||||
AppActivity(
|
||||
deviceId = deviceId,
|
||||
appPackageName = packageName,
|
||||
activityClassName = DummyApps.ACTIVITY_BACKGROUND_AUDIO,
|
||||
title = appLogic.context.getString(R.string.dummy_app_activity_audio)
|
||||
)
|
||||
}
|
||||
|
||||
val allActivities = realActivities + dummyActivities
|
||||
|
||||
allActivities.associateBy { buildKey(it) }
|
||||
}
|
||||
else
|
||||
emptyMap()
|
||||
|
||||
val currentlySaved = appLogic.database.appActivity().getAppActivitiesByDeviceIds(deviceIds = listOf(deviceId)).waitForNonNullValue().associateBy { buildKey(it) }
|
||||
|
||||
// skip all items for removal which are still saved locally
|
||||
val itemsToRemove = HashMap(currentlySaved)
|
||||
currentlyInstalled.forEach { (packageName, _) -> itemsToRemove.remove(packageName) }
|
||||
|
||||
// only add items which are not the same locally
|
||||
val itemsToAdd = currentlyInstalled.filter { (packageName, app) -> currentlySaved[packageName] != app }
|
||||
|
||||
// save the changes
|
||||
if (itemsToRemove.isNotEmpty() or itemsToAdd.isNotEmpty()) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
action = UpdateAppActivitiesAction(
|
||||
removedActivities = itemsToRemove.map { it.value.appPackageName to it.value.activityClassName },
|
||||
updatedOrAddedActivities = itemsToAdd.map { item ->
|
||||
AppActivityItem(
|
||||
packageName = item.value.appPackageName,
|
||||
className = item.value.activityClassName,
|
||||
title = item.value.title
|
||||
)
|
||||
}
|
||||
),
|
||||
appLogic = appLogic,
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCurrentApps(deviceId: String): Map<String, App> {
|
||||
val currentlyInstalled = Threads.backgroundOSInteraction.executeAndWait {
|
||||
appLogic.platformIntegration.getLocalApps(deviceId = deviceId).associateBy { app -> app.packageName }
|
||||
}
|
||||
|
||||
val featureDummyApps = appLogic.platformIntegration.getFeatures().map {
|
||||
DummyApps.forFeature(
|
||||
id = it.id,
|
||||
title = it.title,
|
||||
deviceId = deviceId
|
||||
)
|
||||
}.associateBy { it.packageName }
|
||||
|
||||
return currentlyInstalled + featureDummyApps
|
||||
}
|
||||
|
||||
internal data class DeviceState(
|
||||
val id: String,
|
||||
val isCurrentUserChild: Boolean,
|
||||
val isDefaultUserChild: Boolean,
|
||||
val enableActivityLevelBlocking: Boolean,
|
||||
val isDeviceOwner: Boolean,
|
||||
val hasSyncConsent: Boolean,
|
||||
val isLocalMode: Boolean
|
||||
) {
|
||||
val hasAnyChildUser = isCurrentUserChild || isDefaultUserChild
|
||||
val shouldAskForConsent = hasAnyChildUser && !isLocalMode && !hasSyncConsent
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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.logic.applist
|
||||
|
||||
import io.timelimit.android.proto.toAppActivityItem
|
||||
import io.timelimit.android.proto.toInstalledApp
|
||||
import io.timelimit.android.sync.actions.AddInstalledAppsAction
|
||||
import io.timelimit.android.sync.actions.AppLogicAction
|
||||
import io.timelimit.android.sync.actions.RemoveInstalledAppsAction
|
||||
import io.timelimit.android.sync.actions.UpdateAppActivitiesAction
|
||||
import io.timelimit.proto.applist.InstalledAppsDifferenceProto
|
||||
import io.timelimit.proto.applist.InstalledAppsProto
|
||||
import io.timelimit.proto.applist.RemovedAppActivityProto
|
||||
|
||||
object AppsDifferenceUtil {
|
||||
fun calculateAppsDifference(old: InstalledAppsProto, current: InstalledAppsProto): InstalledAppsDifferenceProto {
|
||||
val oldAppsByPackageName = old.apps.associateBy { it.package_name }
|
||||
val packageNamesToRemove = (oldAppsByPackageName.keys - current.apps.map { it.package_name }.toSet()).toList()
|
||||
val appsToAdd = current.apps.filter { app -> oldAppsByPackageName[app.package_name] != app }
|
||||
|
||||
val oldActivitiesIndexed = old.activities.associateBy { Pair(it.package_name, it.class_name) }
|
||||
val currentActivitiesIndexed = current.activities.associateBy { Pair(it.package_name, it.class_name) }
|
||||
val activitiesToRemove = (oldActivitiesIndexed.keys - currentActivitiesIndexed.keys)
|
||||
.map { activity -> RemovedAppActivityProto(package_name = activity.first, class_name = activity.second) }
|
||||
val activitiesToAdd = currentActivitiesIndexed.filter { (key, activity) -> oldActivitiesIndexed[key] != activity }
|
||||
.values.toList()
|
||||
|
||||
return InstalledAppsDifferenceProto(
|
||||
added = InstalledAppsProto(
|
||||
apps = appsToAdd,
|
||||
activities = activitiesToAdd
|
||||
),
|
||||
removed_packages = packageNamesToRemove,
|
||||
removed_activities = activitiesToRemove
|
||||
)
|
||||
}
|
||||
|
||||
fun calculateAppsDifferenceActions(difference: InstalledAppsDifferenceProto, deviceId: String): List<AppLogicAction> {
|
||||
val result = mutableListOf<AppLogicAction>()
|
||||
|
||||
if (difference.removed_packages.isNotEmpty()) {
|
||||
result.add(RemoveInstalledAppsAction(packageNames = difference.removed_packages))
|
||||
}
|
||||
|
||||
if (difference.added != null && difference.added.apps.isNotEmpty()) {
|
||||
result.add(AddInstalledAppsAction(apps = difference.added.apps.map { it.toInstalledApp() }))
|
||||
}
|
||||
|
||||
val addedActivities = difference.added?.activities ?: emptyList()
|
||||
val removedActivities = difference.removed_activities
|
||||
|
||||
if (addedActivities.isNotEmpty() || removedActivities.isNotEmpty()) {
|
||||
result.add(UpdateAppActivitiesAction(
|
||||
removedActivities = removedActivities.map { it.package_name to it.class_name },
|
||||
updatedOrAddedActivities = addedActivities.map { it.toAppActivityItem() }
|
||||
))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* 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.logic.applist
|
||||
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.crypto.CryptContainer
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.CryptContainerData
|
||||
import io.timelimit.android.data.model.CryptContainerMetadata
|
||||
import io.timelimit.android.proto.build
|
||||
import io.timelimit.android.proto.encodeDeflated
|
||||
import io.timelimit.android.sync.SyncUtil
|
||||
import io.timelimit.android.sync.actions.AppLogicAction
|
||||
import io.timelimit.android.sync.actions.UpdateInstalledAppsAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.proto.applist.InstalledAppsDifferenceProto
|
||||
import io.timelimit.proto.applist.InstalledAppsProto
|
||||
import io.timelimit.proto.applist.SavedAppsDifferenceProto
|
||||
|
||||
object CryptoAppListSync {
|
||||
private const val SIZE_LIMIT_COMPRESSED = 1024 * 256
|
||||
|
||||
class TooLargeException: RuntimeException()
|
||||
|
||||
suspend fun sync(
|
||||
deviceState: DeviceState,
|
||||
database: Database,
|
||||
installed: InstalledAppsProto,
|
||||
syncUtil: SyncUtil,
|
||||
disableLegacySync: Boolean
|
||||
) {
|
||||
fun dispatch(action: AppLogicAction) {
|
||||
if (deviceState.isConnectedMode) {
|
||||
ApplyActionUtil.addAppLogicActionToDatabaseSync(action, database)
|
||||
}
|
||||
}
|
||||
|
||||
val savedCrypt = Threads.database.executeAndWait {
|
||||
InstalledAppsUtil.getEncryptedInstalledAppsFromDatabaseSync(database, deviceState.id)
|
||||
}
|
||||
|
||||
if (savedCrypt == null) {
|
||||
val baseKey = CryptContainer.EncryptParameters.generate()
|
||||
val diffKey = CryptContainer.EncryptParameters.generate()
|
||||
|
||||
val baseEncrypted = CryptContainer.encrypt(installed.encodeDeflated(), baseKey)
|
||||
val diffEncrypted = CryptContainer.encrypt(SavedAppsDifferenceProto.build(baseEncrypted, InstalledAppsDifferenceProto()).encodeDeflated(), diffKey)
|
||||
|
||||
if (baseEncrypted.size > SIZE_LIMIT_COMPRESSED) throw TooLargeException()
|
||||
|
||||
Threads.database.executeAndWait {
|
||||
database.cryptContainer().removeDeviceCryptoMetadata(
|
||||
deviceId = deviceState.id,
|
||||
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(
|
||||
CryptContainerData(
|
||||
cryptContainerId = baseId,
|
||||
encryptedData = baseEncrypted
|
||||
)
|
||||
)
|
||||
|
||||
database.cryptContainer().insertData(
|
||||
CryptContainerData(
|
||||
cryptContainerId = diffId,
|
||||
encryptedData = diffEncrypted
|
||||
)
|
||||
)
|
||||
|
||||
dispatch(UpdateInstalledAppsAction(
|
||||
base = baseEncrypted,
|
||||
diff = diffEncrypted,
|
||||
wipe = disableLegacySync
|
||||
))
|
||||
}
|
||||
|
||||
syncUtil.requestImportantSync()
|
||||
} else {
|
||||
val diffCrypto = AppsDifferenceUtil.calculateAppsDifference(savedCrypt.base, installed)
|
||||
|
||||
if (diffCrypto != savedCrypt.diff) {
|
||||
val baseSize = savedCrypt.base.adapter.encodedSize(savedCrypt.base)
|
||||
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 > SIZE_LIMIT_COMPRESSED) throw TooLargeException()
|
||||
|
||||
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
|
||||
)
|
||||
))
|
||||
|
||||
dispatch(UpdateInstalledAppsAction(
|
||||
base = baseEncrypted,
|
||||
diff = diffEncrypted,
|
||||
wipe = disableLegacySync
|
||||
))
|
||||
}
|
||||
|
||||
syncUtil.requestImportantSync()
|
||||
} else {
|
||||
val diffEncrypted = CryptContainer.encrypt(
|
||||
SavedAppsDifferenceProto.build(savedCrypt.baseHeader, diffCrypto).encodeDeflated(),
|
||||
diffCryptParams.params
|
||||
)
|
||||
|
||||
if (diffEncrypted.size > SIZE_LIMIT_COMPRESSED) throw TooLargeException()
|
||||
|
||||
Threads.database.executeAndWait {
|
||||
database.cryptContainer().updateMetadata(diffCryptParams.newMetadata)
|
||||
|
||||
database.cryptContainer().updateData(
|
||||
CryptContainerData(
|
||||
cryptContainerId = savedCrypt.diffMeta.cryptContainerId,
|
||||
encryptedData = diffEncrypted
|
||||
)
|
||||
)
|
||||
|
||||
dispatch(UpdateInstalledAppsAction(
|
||||
base = null,
|
||||
diff = diffEncrypted,
|
||||
wipe = disableLegacySync
|
||||
))
|
||||
}
|
||||
|
||||
syncUtil.requestImportantSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.logic.applist
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.ConsentFlags
|
||||
import io.timelimit.android.data.model.ExperimentalFlags
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.ServerApiLevelInfo
|
||||
import io.timelimit.android.logic.ServerApiLevelLogic
|
||||
|
||||
data class DeviceState(
|
||||
val id: String,
|
||||
val isCurrentUserChild: Boolean,
|
||||
val isDefaultUserChild: Boolean,
|
||||
val enableActivityLevelBlocking: Boolean,
|
||||
val isDeviceOwner: Boolean,
|
||||
val hasSyncConsent: Boolean,
|
||||
val isLocalMode: Boolean,
|
||||
val disableLegacySync: Boolean,
|
||||
val serverApiLevel: ServerApiLevelInfo
|
||||
) {
|
||||
companion object {
|
||||
fun getSync(database: Database): DeviceState? {
|
||||
val userAndDeviceData = database.derivedDataDao().getUserAndDeviceRelatedDataSync() ?: return null
|
||||
val deviceRelatedData = userAndDeviceData.deviceRelatedData
|
||||
val device = deviceRelatedData.deviceEntry
|
||||
val defaultUser = if (device.defaultUser.isNotEmpty()) database.user().getUserByIdSync(device.defaultUser) else null
|
||||
val disableLegacySync = deviceRelatedData.isExperimentalFlagSetSync(ExperimentalFlags.DISABLE_LEGACY_APP_SENDING)
|
||||
val serverApiLevel = ServerApiLevelLogic.getSync(database)
|
||||
|
||||
return DeviceState(
|
||||
id = device.id,
|
||||
isCurrentUserChild = userAndDeviceData.userRelatedData?.user?.type == UserType.Child,
|
||||
isDefaultUserChild = defaultUser?.type == UserType.Child,
|
||||
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
|
||||
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner,
|
||||
hasSyncConsent = deviceRelatedData.consentFlags and ConsentFlags.APP_LIST_SYNC == ConsentFlags.APP_LIST_SYNC,
|
||||
isLocalMode = deviceRelatedData.isLocalMode,
|
||||
disableLegacySync = disableLegacySync,
|
||||
serverApiLevel = serverApiLevel
|
||||
)
|
||||
}
|
||||
|
||||
fun getLive(appLogic: AppLogic): LiveData<DeviceState?> = mergeLiveDataWaitForValues(
|
||||
appLogic.deviceEntryIfEnabled,
|
||||
appLogic.database.config().isConsentFlagSetAsync(ConsentFlags.APP_LIST_SYNC),
|
||||
appLogic.database.config().getDeviceAuthTokenAsync().map { it.isEmpty() },
|
||||
appLogic.deviceUserEntry,
|
||||
appLogic.deviceEntryIfEnabled.switchMap { deviceEntry ->
|
||||
val defaultUser = deviceEntry?.defaultUser
|
||||
|
||||
if (defaultUser.isNullOrEmpty()) liveDataFromNullableValue(null)
|
||||
else appLogic.database.user().getUserByIdLive(defaultUser)
|
||||
},
|
||||
appLogic.database.config().isExperimentalFlagsSetAsync(ExperimentalFlags.DISABLE_LEGACY_APP_SENDING),
|
||||
appLogic.serverApiLevelLogic.infoLive
|
||||
).map { (deviceEntry, hasSyncConsent, isLocalMode, deviceUser, deviceDefaultUser, disableLegacySync, serverApiLevel) ->
|
||||
deviceEntry?.let { device ->
|
||||
DeviceState(
|
||||
id = device.id,
|
||||
isCurrentUserChild = deviceUser?.type == UserType.Child,
|
||||
isDefaultUserChild = deviceDefaultUser?.type == UserType.Child,
|
||||
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
|
||||
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner,
|
||||
hasSyncConsent = hasSyncConsent,
|
||||
isLocalMode = isLocalMode,
|
||||
disableLegacySync = disableLegacySync,
|
||||
serverApiLevel = serverApiLevel
|
||||
)
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
}
|
||||
|
||||
val hasAnyChildUser = isCurrentUserChild || isDefaultUserChild
|
||||
val shouldAskForConsent = hasAnyChildUser && !isLocalMode && !hasSyncConsent
|
||||
val isConnectedMode = !isLocalMode
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* 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.logic.applist
|
||||
|
||||
import android.util.Log
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.crypto.CryptContainer
|
||||
import io.timelimit.android.crypto.CryptException
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.data.model.CryptContainerMetadata
|
||||
import io.timelimit.android.livedata.waitForNonNullValue
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import io.timelimit.android.proto.decodeInflated
|
||||
import io.timelimit.android.proto.toProto
|
||||
import io.timelimit.proto.applist.InstalledAppsDifferenceProto
|
||||
import io.timelimit.proto.applist.InstalledAppsProto
|
||||
import io.timelimit.proto.applist.SavedAppsDifferenceProto
|
||||
import java.io.IOException
|
||||
|
||||
object InstalledAppsUtil {
|
||||
private const val LOG_TAG = "InstalledAppsUtil"
|
||||
|
||||
suspend fun getInstalledAppsFromPlainDatabaseAsync(database: Database, deviceId: String): InstalledAppsProto {
|
||||
return InstalledAppsProto(
|
||||
apps = database.app().getAppsByDeviceIdAsync(deviceId = deviceId).waitForNonNullValue().map { it.toProto() },
|
||||
activities = database.appActivity().getAppActivitiesByDeviceIds(deviceIds = listOf(deviceId)).waitForNonNullValue().map { it.toProto() }
|
||||
)
|
||||
}
|
||||
|
||||
fun getEncryptedInstalledAppsFromDatabaseSync(database: Database, deviceId: String): DecryptedInstalledApps? {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "getEncryptedInstalledAppsFromDatabaseSync()")
|
||||
}
|
||||
|
||||
val baseValue = database.cryptContainer().getCryptoFullDataSyncByDeviceId(
|
||||
deviceId = deviceId,
|
||||
type = CryptContainerMetadata.TYPE_APP_LIST_BASE
|
||||
)
|
||||
|
||||
val diffValue = database.cryptContainer().getCryptoFullDataSyncByDeviceId(
|
||||
deviceId = deviceId,
|
||||
type = CryptContainerMetadata.TYPE_APP_LIST_DIFF
|
||||
)
|
||||
|
||||
if (
|
||||
baseValue == null ||
|
||||
baseValue.metadata.currentGenerationKey == null ||
|
||||
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 baseDecrypted = CryptContainer.decrypt(
|
||||
baseValue.metadata.currentGenerationKey,
|
||||
baseValue.encryptedData
|
||||
)
|
||||
|
||||
val diffDecrypted = CryptContainer.decrypt(
|
||||
diffValue.metadata.currentGenerationKey,
|
||||
diffValue.encryptedData
|
||||
)
|
||||
|
||||
Triple(baseHeader, baseDecrypted, diffDecrypted)
|
||||
} catch (ex: CryptException) {
|
||||
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()
|
||||
|
||||
Pair(base, diff)
|
||||
} catch (ex: IOException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "could not decode data", ex)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return DecryptedInstalledApps(
|
||||
base = base,
|
||||
baseMeta = baseValue.metadata,
|
||||
baseHeader = baseHeader,
|
||||
diff = diff,
|
||||
diffMeta = diffValue.metadata
|
||||
)
|
||||
}
|
||||
|
||||
data class DecryptedInstalledApps(
|
||||
val base: InstalledAppsProto,
|
||||
val baseHeader: CryptContainer.Header,
|
||||
val baseMeta: CryptContainerMetadata,
|
||||
val diff: InstalledAppsDifferenceProto,
|
||||
val diffMeta: CryptContainerMetadata
|
||||
)
|
||||
|
||||
suspend fun getInstalledAppsFromOs(appLogic: AppLogic, deviceState: DeviceState): InstalledAppsProto {
|
||||
val apps = kotlin.run {
|
||||
val currentlyInstalled = Threads.backgroundOSInteraction.executeAndWait {
|
||||
appLogic.platformIntegration.getLocalApps(deviceId = deviceState.id)
|
||||
}
|
||||
|
||||
val featureDummyApps = appLogic.platformIntegration.getFeatures().map {
|
||||
DummyApps.forFeature(
|
||||
id = it.id,
|
||||
title = it.title,
|
||||
deviceId = deviceState.id
|
||||
)
|
||||
}
|
||||
|
||||
(currentlyInstalled + featureDummyApps).map { it.toProto() }
|
||||
}
|
||||
|
||||
val activities = if (deviceState.enableActivityLevelBlocking)
|
||||
Threads.backgroundOSInteraction.executeAndWait {
|
||||
val realActivities = appLogic.platformIntegration.getLocalAppActivities(deviceId = deviceState.id)
|
||||
val dummyActivities = apps.map { app ->
|
||||
AppActivity(
|
||||
deviceId = deviceState.id,
|
||||
appPackageName = app.package_name,
|
||||
activityClassName = DummyApps.ACTIVITY_BACKGROUND_AUDIO,
|
||||
title = appLogic.context.getString(R.string.dummy_app_activity_audio)
|
||||
)
|
||||
}
|
||||
|
||||
(realActivities + dummyActivities).map { it.toProto() }
|
||||
}
|
||||
else
|
||||
emptyList()
|
||||
|
||||
return InstalledAppsProto(
|
||||
apps = apps,
|
||||
activities = activities
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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.logic.applist
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseAppLogicActionDispatcher
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "SyncInstalledAppsLogic"
|
||||
}
|
||||
|
||||
private val doSyncLock = Mutex()
|
||||
private var requestSync = MutableLiveData<Boolean>().apply { value = false }
|
||||
|
||||
private fun requestSync() {
|
||||
requestSync.value = true
|
||||
}
|
||||
|
||||
private val deviceStateLive = DeviceState.getLive(appLogic)
|
||||
|
||||
val shouldAskForConsent = deviceStateLive.map { it?.shouldAskForConsent ?: false }.ignoreUnchanged()
|
||||
|
||||
init {
|
||||
appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() }
|
||||
deviceStateLive.observeForever { requestSync() }
|
||||
|
||||
runAsyncExpectForever { syncLoop() }
|
||||
}
|
||||
|
||||
private suspend fun syncLoop() {
|
||||
// wait a moment before the first sync
|
||||
appLogic.timeApi.sleep(15 * 1000)
|
||||
|
||||
while (true) {
|
||||
requestSync.waitUntilValueMatches { it == true }
|
||||
requestSync.value = false
|
||||
|
||||
try {
|
||||
doSyncNow()
|
||||
|
||||
// maximal 1 time per 5 seconds
|
||||
appLogic.timeApi.sleep(5 * 1000)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "could not sync installed app list", ex)
|
||||
}
|
||||
|
||||
Toast.makeText(appLogic.context, R.string.background_logic_toast_sync_apps, Toast.LENGTH_SHORT).show()
|
||||
|
||||
appLogic.timeApi.sleep(45 * 1000)
|
||||
requestSync.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun doSyncNow() {
|
||||
doSyncLock.withLock {
|
||||
val deviceState = Threads.database.executeAndWait { DeviceState.getSync(appLogic.database) } ?: return
|
||||
|
||||
if (deviceState.isLocalMode) {
|
||||
// local mode -> sync always
|
||||
} else {
|
||||
// connected mode -> don't sync always
|
||||
if (!deviceState.hasSyncConsent) return@withLock
|
||||
if (!deviceState.hasAnyChildUser) return@withLock
|
||||
}
|
||||
|
||||
val installed = InstalledAppsUtil.getInstalledAppsFromOs(appLogic, deviceState)
|
||||
|
||||
val savedPlain = InstalledAppsUtil.getInstalledAppsFromPlainDatabaseAsync(appLogic.database, deviceState.id)
|
||||
|
||||
val diffPlain = AppsDifferenceUtil.calculateAppsDifference(savedPlain, installed)
|
||||
val diffPlainActions = AppsDifferenceUtil.calculateAppsDifferenceActions(diffPlain, deviceState.id)
|
||||
|
||||
if (deviceState.disableLegacySync) {
|
||||
if (diffPlainActions.isNotEmpty()) {
|
||||
Threads.database.executeAndWait {
|
||||
diffPlainActions.forEach {
|
||||
LocalDatabaseAppLogicActionDispatcher.dispatchAppLogicActionSync(
|
||||
it, appLogic.database.config().getOwnDeviceIdSync()!!, appLogic.database
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
diffPlainActions.forEach { action ->
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
action = action,
|
||||
appLogic = appLogic,
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceState.isConnectedMode && deviceState.serverApiLevel.hasLevelOrIsOffline(4)) {
|
||||
CryptoAppListSync.sync(
|
||||
deviceState = deviceState,
|
||||
database = appLogic.database,
|
||||
installed = installed,
|
||||
syncUtil = appLogic.syncUtil,
|
||||
disableLegacySync = deviceState.disableLegacySync
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* 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.logic.crypto
|
||||
|
||||
import io.timelimit.android.crypto.CryptContainer
|
||||
import io.timelimit.android.crypto.CryptException
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.sync.actions.SendKeyRequestAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.sync.network.ServerCryptContainer
|
||||
|
||||
object CryptDataHandler {
|
||||
fun process(database: Database, data: ServerCryptContainer, type: Int, deviceId: String?, categoryId: String?): Result {
|
||||
val oldItem = if (deviceId != null)
|
||||
database.cryptContainer().getCryptoMetadataSyncByDeviceId(deviceId, type)
|
||||
else if (categoryId != null)
|
||||
database.cryptContainer().getCryptoMetadataSyncByCategoryId(categoryId, type)
|
||||
else
|
||||
database.cryptContainer().getCryptoMetadataSyncByType(type)
|
||||
|
||||
val isUnmodified = if (oldItem != null) {
|
||||
val oldSavedData = database.cryptContainer().getData(oldItem.cryptContainerId)
|
||||
|
||||
oldSavedData != null && oldSavedData.encryptedData.contentEquals(data.data)
|
||||
} else false
|
||||
|
||||
val currentItem = if (oldItem == null) {
|
||||
val baseItem = CryptContainerMetadata(
|
||||
cryptContainerId = 0,
|
||||
deviceId = deviceId,
|
||||
categoryId = categoryId,
|
||||
type = type,
|
||||
serverVersion = data.version,
|
||||
currentGeneration = 0,
|
||||
currentGenerationFirstTimestamp = System.currentTimeMillis(),
|
||||
currentGenerationKey = null,
|
||||
nextCounter = 0,
|
||||
status = CryptContainerMetadata.ProcessingStatus.MissingKey
|
||||
)
|
||||
|
||||
val cryptContainerId = database.cryptContainer().insertMetadata(baseItem)
|
||||
|
||||
baseItem.copy(cryptContainerId = cryptContainerId)
|
||||
} else oldItem.copy(serverVersion = data.version)
|
||||
|
||||
if (deviceId == database.config().getOwnDeviceIdSync()) {
|
||||
database.cryptContainer().updateMetadata(currentItem)
|
||||
|
||||
return Result(didCreateKeyRequests = false)
|
||||
}
|
||||
|
||||
if (!isUnmodified) {
|
||||
CryptContainerData(
|
||||
cryptContainerId = currentItem.cryptContainerId,
|
||||
encryptedData = data.data
|
||||
).also { item ->
|
||||
if (oldItem == null) database.cryptContainer().insertData(item)
|
||||
else database.cryptContainer().updateData(item)
|
||||
}
|
||||
}
|
||||
|
||||
val header = try {
|
||||
CryptContainer.Header.read(data.data)
|
||||
} catch (ex: CryptException.InvalidContainer) {
|
||||
null
|
||||
}
|
||||
|
||||
val updatedMetadata = if (isUnmodified) {
|
||||
currentItem
|
||||
} else if (header == null) {
|
||||
currentItem.copy(status = CryptContainerMetadata.ProcessingStatus.CryptoDamage)
|
||||
} else if (header.generation < currentItem.currentGeneration) {
|
||||
currentItem.copy(status = CryptContainerMetadata.ProcessingStatus.DowngradeDetected)
|
||||
} else if (header.generation > currentItem.currentGeneration) {
|
||||
currentItem.copy(status = CryptContainerMetadata.ProcessingStatus.MissingKey)
|
||||
} else if (header.counter < currentItem.nextCounter) {
|
||||
currentItem.copy(status = CryptContainerMetadata.ProcessingStatus.DowngradeDetected)
|
||||
} else if (currentItem.currentGenerationKey == null) {
|
||||
currentItem.copy(status = CryptContainerMetadata.ProcessingStatus.MissingKey)
|
||||
} else {
|
||||
try {
|
||||
CryptContainer.decrypt(currentItem.currentGenerationKey, data.data)
|
||||
|
||||
currentItem.copy(
|
||||
status = CryptContainerMetadata.ProcessingStatus.Unprocessed,
|
||||
nextCounter = header.counter + 1
|
||||
)
|
||||
} catch (ex: CryptException.WrongKey) {
|
||||
currentItem.copy(status = CryptContainerMetadata.ProcessingStatus.CryptoDamage)
|
||||
}
|
||||
}
|
||||
|
||||
database.cryptContainer().updateMetadata(updatedMetadata)
|
||||
|
||||
if (updatedMetadata.status == CryptContainerMetadata.ProcessingStatus.MissingKey && header != null) {
|
||||
if (database.cryptContainerKeyRequest().byCryptContainerId(currentItem.cryptContainerId) == null) {
|
||||
val signingKey = DeviceSigningKey.getPublicAndPrivateKeySync(database)!!.let { Curve25519.getPrivateKey(it) }
|
||||
val requestKeyPair = Curve25519.generateKeyPair()
|
||||
val sequenceNumber = database.config().getNextSigningSequenceNumberAndIncrementIt()
|
||||
|
||||
ApplyActionUtil.addAppLogicActionToDatabaseSync(
|
||||
SendKeyRequestAction(
|
||||
deviceSequenceNumber = sequenceNumber,
|
||||
deviceId = deviceId,
|
||||
categoryId = categoryId,
|
||||
type = type,
|
||||
tempKey = Curve25519.getPublicKey(requestKeyPair),
|
||||
signature = Curve25519.sign(
|
||||
signingKey,
|
||||
KeyRequestSignedData(
|
||||
deviceSequenceNumber = sequenceNumber,
|
||||
deviceId = deviceId,
|
||||
categoryId = categoryId,
|
||||
type = type,
|
||||
tempKey = Curve25519.getPublicKey(
|
||||
requestKeyPair
|
||||
)
|
||||
).serialize()
|
||||
)
|
||||
), database
|
||||
)
|
||||
|
||||
database.cryptContainerKeyRequest().insert(
|
||||
CryptContainerPendingKeyRequest(
|
||||
cryptContainerId = currentItem.cryptContainerId,
|
||||
requestTimeCryptContainerGeneration = header.generation,
|
||||
requestSequenceId = sequenceNumber,
|
||||
requestKey = requestKeyPair
|
||||
)
|
||||
)
|
||||
|
||||
return Result(didCreateKeyRequests = true)
|
||||
}
|
||||
}
|
||||
|
||||
return Result(didCreateKeyRequests = false)
|
||||
}
|
||||
|
||||
data class Result (val didCreateKeyRequests: Boolean) {
|
||||
fun or(other: Result) = Result(this.didCreateKeyRequests or other.didCreateKeyRequests)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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.logic.crypto
|
||||
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.logic.crypto.decrypt.DecryptProcessor
|
||||
import io.timelimit.android.sync.network.ServerKeyRequest
|
||||
import io.timelimit.android.sync.network.ServerKeyResponse
|
||||
|
||||
object CryptoSyncLogic {
|
||||
suspend fun postPullHook(
|
||||
database: Database,
|
||||
keyRequests: List<ServerKeyRequest>,
|
||||
keyResponses: List<ServerKeyResponse>
|
||||
): Result {
|
||||
return Threads.database.executeAndWait {
|
||||
val privateKeyAndPublicKey = DeviceSigningKey.getPublicAndPrivateKeySync(database, uploadIfMissingAtServer = true)
|
||||
?: return@executeAndWait Result(didSendReplies = false)
|
||||
|
||||
val keyRequestResult = KeyRequestProcessor.process(
|
||||
keyRequests = keyRequests,
|
||||
database = database,
|
||||
privateKeyAndPublicKey = privateKeyAndPublicKey
|
||||
)
|
||||
|
||||
val keyResponseResult = KeyResponseProcessor.process(
|
||||
keyResponses = keyResponses,
|
||||
database = database,
|
||||
privateKeyAndPublicKey = privateKeyAndPublicKey
|
||||
)
|
||||
|
||||
DecryptProcessor.handleEncryptedApps(database)
|
||||
|
||||
Result(
|
||||
didSendReplies = keyRequestResult.didSendReplies or keyResponseResult.gotNewKeys
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Result(val didSendReplies: Boolean)
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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.logic.crypto
|
||||
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.sync.actions.UploadDevicePublicKeyAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
|
||||
object DeviceSigningKey {
|
||||
fun getPublicAndPrivateKeySync(database: Database, uploadIfMissingAtServer: Boolean = false): ByteArray? = database.runInTransaction {
|
||||
if (database.config().getServerApiLevelSync() < 4) return@runInTransaction null
|
||||
|
||||
val oldData = database.config().getSigningKeySync()
|
||||
|
||||
if (oldData != null) {
|
||||
if (database.deviceKey().getSync(database.config().getOwnDeviceIdSync()!!) == null && uploadIfMissingAtServer) {
|
||||
ApplyActionUtil.addAppLogicActionToDatabaseSync(
|
||||
UploadDevicePublicKeyAction(publicKey = Curve25519.getPublicKey(oldData)),
|
||||
database
|
||||
)
|
||||
}
|
||||
|
||||
return@runInTransaction oldData
|
||||
}
|
||||
|
||||
val newData = Curve25519.generateKeyPair()
|
||||
|
||||
database.config().setSigningKeySync(newData)
|
||||
ApplyActionUtil.addAppLogicActionToDatabaseSync(
|
||||
UploadDevicePublicKeyAction(publicKey = Curve25519.getPublicKey(newData)),
|
||||
database
|
||||
)
|
||||
|
||||
return@runInTransaction newData
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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.logic.crypto
|
||||
|
||||
import android.util.Log
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.crypto.CryptContainer
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.CryptContainerMetadata
|
||||
import io.timelimit.android.sync.actions.ReplyToKeyRequestAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.sync.network.ServerKeyRequest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object KeyRequestProcessor {
|
||||
private const val LOG_TAG = "KeyRequestProcessor"
|
||||
|
||||
fun process(
|
||||
keyRequests: List<ServerKeyRequest>,
|
||||
database: Database,
|
||||
privateKeyAndPublicKey: ByteArray
|
||||
): Result {
|
||||
var didSendReplies = false
|
||||
|
||||
if (keyRequests.isNotEmpty()) {
|
||||
val lastServerSequence = database.config().getLastServerKeyRequestSequenceSync()
|
||||
var newServerSequence: Long? = null
|
||||
|
||||
for (request in keyRequests) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got key request: $request")
|
||||
}
|
||||
|
||||
if (lastServerSequence != null && request.serverRequestSequenceNumber <= lastServerSequence) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because server sequence number repeated")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (newServerSequence == null || newServerSequence < request.serverRequestSequenceNumber) {
|
||||
newServerSequence = request.serverRequestSequenceNumber
|
||||
}
|
||||
|
||||
val deviceKey = database.deviceKey().getSync(request.senderDeviceId)
|
||||
|
||||
if (deviceKey == null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because no device key is known")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (request.senderSequenceNumber < deviceKey.nextSequenceNumber) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because client sequence number repeated")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (deviceKey.publicKey.size != Curve25519.PUBLIC_KEY_SIZE || request.signature.size != Curve25519.SIGNATURE_SIZE) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because cryptographic data has the wrong size")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val requestSignedData = KeyRequestSignedData(
|
||||
deviceSequenceNumber = request.senderSequenceNumber,
|
||||
deviceId = request.deviceId,
|
||||
categoryId = request.categoryId,
|
||||
type = request.type,
|
||||
tempKey = request.tempKey
|
||||
)
|
||||
|
||||
if (
|
||||
!Curve25519.validateSignature(
|
||||
deviceKey.publicKey,
|
||||
requestSignedData.serialize(),
|
||||
request.signature
|
||||
)
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because the signature is invalid")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
database.deviceKey().update(deviceKey.copy(nextSequenceNumber = request.senderSequenceNumber + 1))
|
||||
|
||||
val metadata = if (request.deviceId != null) {
|
||||
database.cryptContainer().getCryptoMetadataSyncByDeviceId(request.deviceId, request.type)
|
||||
} else if (request.categoryId != null) {
|
||||
database.cryptContainer().getCryptoMetadataSyncByCategoryId(request.categoryId, request.type)
|
||||
} else {
|
||||
database.cryptContainer().getCryptoMetadataSyncByType(request.type)
|
||||
}
|
||||
|
||||
if (
|
||||
metadata?.currentGenerationKey == null ||
|
||||
metadata.status == CryptContainerMetadata.ProcessingStatus.MissingKey
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because the key is unknown")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (metadata.currentGenerationKey.size != CryptContainer.KEY_SIZE) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because the key has the wrong size")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val selfTempKey = Curve25519.generateKeyPair()
|
||||
|
||||
val sharedSecret = Curve25519.sharedSecret(
|
||||
publicKey = request.tempKey,
|
||||
privateKey = Curve25519.getPrivateKey(selfTempKey)
|
||||
)
|
||||
|
||||
val encryptedKey = Cipher.getInstance("AES/ECB/NoPadding").let {
|
||||
// this encrypt just a single block and the integrity is checked using async crypto
|
||||
// actually, xor could be enough for this case
|
||||
it.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sharedSecret.copyOfRange(0, CryptContainer.KEY_SIZE), "AES"))
|
||||
it.doFinal(metadata.currentGenerationKey)
|
||||
}
|
||||
|
||||
ApplyActionUtil.addAppLogicActionToDatabaseSync(
|
||||
action = ReplyToKeyRequestAction(
|
||||
requestServerSequenceNumber = request.serverRequestSequenceNumber,
|
||||
tempKey = Curve25519.getPublicKey(selfTempKey),
|
||||
encryptedKey = encryptedKey,
|
||||
signature = Curve25519.sign(
|
||||
privateKey = Curve25519.getPrivateKey(privateKeyAndPublicKey),
|
||||
message = KeyResponseSignedData(
|
||||
request = requestSignedData,
|
||||
senderDevicePublicKey = deviceKey.publicKey,
|
||||
tempKey = Curve25519.getPublicKey(selfTempKey),
|
||||
encryptedKey = encryptedKey
|
||||
).serialize()
|
||||
)
|
||||
),
|
||||
database = database
|
||||
)
|
||||
|
||||
didSendReplies = true
|
||||
}
|
||||
|
||||
newServerSequence?.let {
|
||||
database.config().setLastServerKeyRequestSequenceSync(it)
|
||||
}
|
||||
}
|
||||
|
||||
return Result(
|
||||
didSendReplies = didSendReplies
|
||||
)
|
||||
}
|
||||
|
||||
data class Result(val didSendReplies: Boolean)
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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.logic.crypto
|
||||
|
||||
import okio.Buffer
|
||||
|
||||
data class KeyRequestSignedData (
|
||||
val deviceSequenceNumber: Long,
|
||||
val deviceId: String?,
|
||||
val categoryId: String?,
|
||||
val type: Int,
|
||||
val tempKey: ByteArray
|
||||
) {
|
||||
init {
|
||||
if (tempKey.size != 32) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(): ByteArray = Buffer().also { buffer ->
|
||||
fun writeBool(value: Boolean) = buffer.writeByte(when (value) {
|
||||
false -> 0
|
||||
true -> 1
|
||||
})
|
||||
|
||||
fun writeString(value: String) = value.toByteArray(Charsets.UTF_8).also {
|
||||
buffer.writeInt(it.size)
|
||||
buffer.write(it)
|
||||
}
|
||||
|
||||
fun writeOptionalString(value: String?) {
|
||||
if (value == null) {
|
||||
writeBool(false)
|
||||
} else {
|
||||
writeBool(true)
|
||||
writeString(value)
|
||||
}
|
||||
}
|
||||
|
||||
writeString("KeyRequestSignedData")
|
||||
buffer.writeLong(deviceSequenceNumber)
|
||||
writeBool(deviceId != null)
|
||||
writeOptionalString(deviceId)
|
||||
writeOptionalString(categoryId)
|
||||
buffer.writeInt(type)
|
||||
buffer.write(tempKey) // fixed size => no length prefix required
|
||||
}.readByteArray()
|
||||
}
|
||||
|
||||
data class KeyResponseSignedData (
|
||||
val request: KeyRequestSignedData,
|
||||
val senderDevicePublicKey: ByteArray,
|
||||
val tempKey: ByteArray,
|
||||
val encryptedKey: ByteArray
|
||||
) {
|
||||
fun serialize(): ByteArray = Buffer().also { buffer ->
|
||||
fun writeByteArray(array: ByteArray) {
|
||||
buffer.writeInt(array.size)
|
||||
buffer.write(array)
|
||||
}
|
||||
|
||||
fun writeString(value: String) = writeByteArray(value.toByteArray(Charsets.UTF_8))
|
||||
|
||||
writeString("KeyResponseSignedData")
|
||||
writeByteArray(senderDevicePublicKey)
|
||||
writeByteArray(tempKey)
|
||||
writeByteArray(encryptedKey)
|
||||
buffer.write(request.serialize())
|
||||
}.readByteArray()
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
* 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.logic.crypto
|
||||
|
||||
import android.util.Log
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.crypto.CryptContainer
|
||||
import io.timelimit.android.crypto.CryptException
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.CryptContainerKeyResult
|
||||
import io.timelimit.android.data.model.CryptContainerMetadata
|
||||
import io.timelimit.android.sync.actions.FinishKeyRequestAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.sync.network.ServerKeyResponse
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object KeyResponseProcessor {
|
||||
private const val LOG_TAG = "KeyResponseProcessor"
|
||||
|
||||
fun process(
|
||||
keyResponses: List<ServerKeyResponse>,
|
||||
database: Database,
|
||||
privateKeyAndPublicKey: ByteArray
|
||||
): Result {
|
||||
var gotNewKeys = false
|
||||
|
||||
if (keyResponses.isNotEmpty()) {
|
||||
val lastServerSequence = database.config().getLastServerKeyResponseSequenceSync()
|
||||
var newServerSequence: Long? = null
|
||||
|
||||
for (response in keyResponses) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got key response: $response")
|
||||
}
|
||||
|
||||
if (lastServerSequence != null && response.serverResponseSequenceNumber <= lastServerSequence) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because server sequence number repeated")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (newServerSequence == null || newServerSequence < response.serverResponseSequenceNumber) {
|
||||
newServerSequence = response.serverResponseSequenceNumber
|
||||
}
|
||||
|
||||
val deviceKey = database.deviceKey().getSync(response.senderDeviceId)
|
||||
|
||||
if (deviceKey == null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because no device key is known")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
deviceKey.publicKey.size != Curve25519.PUBLIC_KEY_SIZE ||
|
||||
response.tempKey.size != Curve25519.PUBLIC_KEY_SIZE ||
|
||||
response.signature.size != Curve25519.SIGNATURE_SIZE ||
|
||||
response.encryptedKey.size != CryptContainer.KEY_SIZE
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because cryptographic data has the wrong size")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val requestMeta = database.cryptContainerKeyRequest().byRequestId(response.requestSequenceId)
|
||||
|
||||
if (requestMeta == null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because no request was found")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val cryptContainerMeta = database.cryptContainer().getCryptoMetadataSyncByContainerId(requestMeta.cryptContainerId)!!
|
||||
|
||||
if (
|
||||
database.cryptContainerKeyResult().countResultItems(
|
||||
requestSequenceNumber = requestMeta.requestSequenceId,
|
||||
deviceId = response.senderDeviceId
|
||||
) > 0
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because this device sent already an invalid reply")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val requestSignedData = KeyRequestSignedData(
|
||||
deviceSequenceNumber = requestMeta.requestSequenceId,
|
||||
deviceId = cryptContainerMeta.deviceId,
|
||||
categoryId = cryptContainerMeta.categoryId,
|
||||
type = cryptContainerMeta.type,
|
||||
tempKey = Curve25519.getPublicKey(requestMeta.requestKey)
|
||||
)
|
||||
|
||||
val responseSignedData = KeyResponseSignedData(
|
||||
request = requestSignedData,
|
||||
senderDevicePublicKey = Curve25519.getPublicKey(privateKeyAndPublicKey),
|
||||
encryptedKey = response.encryptedKey,
|
||||
tempKey = response.tempKey
|
||||
)
|
||||
|
||||
if (
|
||||
!Curve25519.validateSignature(
|
||||
deviceKey.publicKey,
|
||||
responseSignedData.serialize(),
|
||||
response.signature
|
||||
)
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "ignore because the signature is invalid")
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val sharedSecret = Curve25519.sharedSecret(
|
||||
publicKey = response.tempKey,
|
||||
privateKey = Curve25519.getPrivateKey(requestMeta.requestKey)
|
||||
)
|
||||
|
||||
val decryptedKey = Cipher.getInstance("AES/ECB/NoPadding").let {
|
||||
// this encrypt just a single block and the integrity is checked using async crypto
|
||||
// actually, xor could be enough for this case
|
||||
it.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret.copyOfRange(0, CryptContainer.KEY_SIZE), "AES"))
|
||||
it.doFinal(response.encryptedKey)
|
||||
}
|
||||
|
||||
val encryptedData = database.cryptContainer().getData(cryptContainerMeta.cryptContainerId)!!
|
||||
val encryptedDataHeader = try {
|
||||
CryptContainer.Header.read(encryptedData.encryptedData)
|
||||
} catch (ex: CryptException.InvalidContainer) {
|
||||
null
|
||||
}
|
||||
|
||||
val isKeyValid = try {
|
||||
CryptContainer.decrypt(decryptedKey, encryptedData.encryptedData)
|
||||
|
||||
true
|
||||
} catch (ex: CryptException.WrongKey) {
|
||||
false
|
||||
}
|
||||
|
||||
if (isKeyValid && encryptedDataHeader != null) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got valid key")
|
||||
}
|
||||
|
||||
gotNewKeys = true
|
||||
|
||||
ApplyActionUtil.addAppLogicActionToDatabaseSync(
|
||||
action = FinishKeyRequestAction(deviceSequenceNumber = requestMeta.requestSequenceId),
|
||||
database = database
|
||||
)
|
||||
|
||||
database.cryptContainerKeyRequest().delete(requestMeta)
|
||||
|
||||
database.cryptContainer().updateMetadata(
|
||||
cryptContainerMeta.copy(
|
||||
currentGeneration = encryptedDataHeader.generation,
|
||||
currentGenerationFirstTimestamp = System.currentTimeMillis(),
|
||||
nextCounter = encryptedDataHeader.counter + 1,
|
||||
currentGenerationKey = decryptedKey,
|
||||
status = CryptContainerMetadata.ProcessingStatus.Unprocessed
|
||||
)
|
||||
)
|
||||
} else {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "got invalid key")
|
||||
}
|
||||
|
||||
database.cryptContainerKeyResult().insert(
|
||||
CryptContainerKeyResult(
|
||||
requestSequenceId = requestMeta.requestSequenceId,
|
||||
deviceId = response.senderDeviceId,
|
||||
status = CryptContainerKeyResult.Status.InvalidKey
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
newServerSequence?.let {
|
||||
database.config().setLastServerKeyResponseSequenceSync(it)
|
||||
}
|
||||
}
|
||||
|
||||
return Result(gotNewKeys = gotNewKeys)
|
||||
}
|
||||
|
||||
data class Result(
|
||||
val gotNewKeys: Boolean
|
||||
)
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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.logic.crypto.decrypt
|
||||
|
||||
import android.util.Log
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.crypto.CryptContainer
|
||||
import io.timelimit.android.crypto.CryptException
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.dao.CryptContainerDao
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.data.model.CryptContainerMetadata
|
||||
import io.timelimit.android.proto.decodeInflated
|
||||
import io.timelimit.android.proto.toDb
|
||||
import io.timelimit.proto.applist.InstalledAppsProto
|
||||
import io.timelimit.proto.applist.SavedAppsDifferenceProto
|
||||
import java.io.IOException
|
||||
|
||||
object DecryptProcessor {
|
||||
private const val LOG_TAG = "DecryptProcessor"
|
||||
|
||||
fun handleEncryptedApps(database: Database) {
|
||||
val unprocessed = database.cryptContainer().getMetadataByProcessingStatus(CryptContainerMetadata.ProcessingStatus.Unprocessed)
|
||||
val finishedDeviceIds = mutableSetOf<String>()
|
||||
|
||||
for (metadata in unprocessed) {
|
||||
if (
|
||||
metadata.type == CryptContainerMetadata.TYPE_APP_LIST_BASE ||
|
||||
metadata.type == CryptContainerMetadata.TYPE_APP_LIST_DIFF
|
||||
) {
|
||||
if (
|
||||
metadata.deviceId == null ||
|
||||
finishedDeviceIds.contains(metadata.deviceId) ||
|
||||
metadata.deviceId == database.config().getOwnDeviceIdSync()
|
||||
) continue
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "found data for ${metadata.deviceId}")
|
||||
}
|
||||
|
||||
finishedDeviceIds.add(metadata.deviceId)
|
||||
|
||||
val baseData = database.cryptContainer().getCryptoFullDataSyncByDeviceId(metadata.deviceId, CryptContainerMetadata.TYPE_APP_LIST_BASE) ?: continue
|
||||
val diffData = database.cryptContainer().getCryptoFullDataSyncByDeviceId(metadata.deviceId, CryptContainerMetadata.TYPE_APP_LIST_DIFF) ?: continue
|
||||
|
||||
if (!(isReadyForProcessing(baseData) && isReadyForProcessing(diffData))) continue
|
||||
|
||||
val (baseDecrypted, baseHeader) = try {
|
||||
CryptContainer.decrypt(
|
||||
baseData.metadata.currentGenerationKey ?: continue,
|
||||
baseData.encryptedData
|
||||
) to CryptContainer.Header.read(baseData.encryptedData)
|
||||
} catch (ex: CryptException) {
|
||||
database.cryptContainer().updateMetadata(baseData.metadata.copy(status = CryptContainerMetadata.ProcessingStatus.CryptoDamage))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val diffDecrypted = try {
|
||||
CryptContainer.decrypt(
|
||||
diffData.metadata.currentGenerationKey ?: continue,
|
||||
diffData.encryptedData
|
||||
)
|
||||
} catch (ex: CryptException) {
|
||||
database.cryptContainer().updateMetadata(diffData.metadata.copy(status = CryptContainerMetadata.ProcessingStatus.CryptoDamage))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val baseContent = try {
|
||||
InstalledAppsProto.ADAPTER.decodeInflated(baseDecrypted)
|
||||
} catch (ex: IOException) {
|
||||
database.cryptContainer().updateMetadata(baseData.metadata.copy(status = CryptContainerMetadata.ProcessingStatus.ContentDamage))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
val diffContent = try {
|
||||
SavedAppsDifferenceProto.ADAPTER.decodeInflated(diffDecrypted)
|
||||
} catch (ex: IOException) {
|
||||
database.cryptContainer().updateMetadata(diffData.metadata.copy(status = CryptContainerMetadata.ProcessingStatus.ContentDamage))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
diffContent.base_generation != baseHeader.generation ||
|
||||
diffContent.base_counter != baseHeader.counter
|
||||
) {
|
||||
database.cryptContainer().updateMetadata(diffData.metadata.copy(status = CryptContainerMetadata.ProcessingStatus.ContentDamage))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
database.app().deleteAllAppsByDeviceId(metadata.deviceId)
|
||||
database.appActivity().deleteAppActivitiesByDeviceIds(listOf(metadata.deviceId))
|
||||
|
||||
database.app().addAppsSync(
|
||||
baseContent.apps.map {
|
||||
App(
|
||||
deviceId = metadata.deviceId,
|
||||
packageName = it.package_name,
|
||||
title = it.title,
|
||||
isLaunchable = it.is_launchable,
|
||||
recommendation = it.recommendation.toDb()
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
database.appActivity().addAppActivitiesSync(
|
||||
baseContent.activities.map {
|
||||
AppActivity(
|
||||
deviceId = metadata.deviceId,
|
||||
appPackageName = it.package_name,
|
||||
activityClassName = it.class_name,
|
||||
title = it.title
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
database.app().removeAppsByDeviceIdAndPackageNamesSync(
|
||||
metadata.deviceId,
|
||||
diffContent.apps?.removed_packages ?: emptyList()
|
||||
)
|
||||
|
||||
diffContent.apps?.removed_activities?.forEach {
|
||||
database.appActivity().deleteAppActivitiesSync(
|
||||
deviceId = metadata.deviceId,
|
||||
packageName = it.package_name,
|
||||
activities = listOf(it.class_name)
|
||||
)
|
||||
}
|
||||
|
||||
database.app().addAppsSync(
|
||||
diffContent.apps?.added?.apps?.map {
|
||||
App(
|
||||
deviceId = metadata.deviceId,
|
||||
packageName = it.package_name,
|
||||
title = it.title,
|
||||
isLaunchable = it.is_launchable,
|
||||
recommendation = it.recommendation.toDb()
|
||||
)
|
||||
} ?: emptyList()
|
||||
)
|
||||
|
||||
database.appActivity().addAppActivitiesSync(
|
||||
diffContent.apps?.added?.activities?.map {
|
||||
AppActivity(
|
||||
deviceId = metadata.deviceId,
|
||||
appPackageName = it.package_name,
|
||||
activityClassName = it.class_name,
|
||||
title = it.title
|
||||
)
|
||||
} ?: emptyList()
|
||||
)
|
||||
|
||||
database.cryptContainer().updateMetadata(listOf(
|
||||
baseData.metadata.copy(status = CryptContainerMetadata.ProcessingStatus.Finished),
|
||||
diffData.metadata.copy(status = CryptContainerMetadata.ProcessingStatus.Finished)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isReadyForProcessing(value: CryptContainerDao.MetadataAndContent) = when (value.metadata.status) {
|
||||
CryptContainerMetadata.ProcessingStatus.Unprocessed -> true
|
||||
CryptContainerMetadata.ProcessingStatus.Finished -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
75
app/src/main/java/io/timelimit/android/proto/AppList.kt
Normal file
75
app/src/main/java/io/timelimit/android/proto/AppList.kt
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.proto
|
||||
|
||||
import io.timelimit.android.crypto.CryptContainer
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.data.model.AppRecommendation
|
||||
import io.timelimit.android.sync.actions.AppActivityItem
|
||||
import io.timelimit.android.sync.actions.InstalledApp
|
||||
import io.timelimit.proto.applist.InstalledAppActivityProto
|
||||
import io.timelimit.proto.applist.InstalledAppProto
|
||||
import io.timelimit.proto.applist.InstalledAppsDifferenceProto
|
||||
import io.timelimit.proto.applist.SavedAppsDifferenceProto
|
||||
|
||||
fun InstalledAppProto.Recommendation.toDb(): AppRecommendation = when (this) {
|
||||
InstalledAppProto.Recommendation.NONE -> AppRecommendation.None
|
||||
InstalledAppProto.Recommendation.WHITELIST -> AppRecommendation.Whitelist
|
||||
InstalledAppProto.Recommendation.BLACKLIST -> AppRecommendation.Blacklist
|
||||
}
|
||||
|
||||
fun AppRecommendation.toProto(): InstalledAppProto.Recommendation = when (this) {
|
||||
AppRecommendation.None -> InstalledAppProto.Recommendation.NONE
|
||||
AppRecommendation.Whitelist -> InstalledAppProto.Recommendation.WHITELIST
|
||||
AppRecommendation.Blacklist -> InstalledAppProto.Recommendation.BLACKLIST
|
||||
}
|
||||
|
||||
fun InstalledAppProto.toInstalledApp(): InstalledApp = InstalledApp(
|
||||
packageName = this.package_name,
|
||||
title = this.title,
|
||||
recommendation = this.recommendation.toDb(),
|
||||
isLaunchable = this.is_launchable
|
||||
)
|
||||
|
||||
fun App.toProto(): InstalledAppProto = InstalledAppProto(
|
||||
package_name = this.packageName,
|
||||
title = this.title,
|
||||
is_launchable = this.isLaunchable,
|
||||
recommendation = this.recommendation.toProto()
|
||||
)
|
||||
|
||||
fun InstalledAppActivityProto.toAppActivityItem(): AppActivityItem = AppActivityItem(
|
||||
packageName = this.package_name,
|
||||
className = this.class_name,
|
||||
title = this.title
|
||||
)
|
||||
|
||||
fun AppActivity.toProto(): InstalledAppActivityProto = InstalledAppActivityProto(
|
||||
package_name = this.appPackageName,
|
||||
class_name = this.activityClassName,
|
||||
title = this.title
|
||||
)
|
||||
|
||||
fun SavedAppsDifferenceProto.Companion.build(header: CryptContainer.Header, diff: InstalledAppsDifferenceProto): SavedAppsDifferenceProto =
|
||||
SavedAppsDifferenceProto(
|
||||
base_generation = header.generation,
|
||||
base_counter = header.counter,
|
||||
apps = diff
|
||||
)
|
||||
|
||||
fun SavedAppsDifferenceProto.Companion.build(encryptedBaseData: ByteArray, diff: InstalledAppsDifferenceProto): SavedAppsDifferenceProto =
|
||||
build(CryptContainer.Header.read(encryptedBaseData), diff)
|
32
app/src/main/java/io/timelimit/android/proto/Compression.kt
Normal file
32
app/src/main/java/io/timelimit/android/proto/Compression.kt
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.proto
|
||||
|
||||
import com.squareup.wire.Message
|
||||
import com.squareup.wire.ProtoAdapter
|
||||
import okio.Buffer
|
||||
import okio.use
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.zip.DeflaterOutputStream
|
||||
import java.util.zip.InflaterInputStream
|
||||
|
||||
fun <A : Message<A, B>, B : Message.Builder<A, B>> Message<A, B>.encodeDeflated(): ByteArray = Buffer().also { buffer ->
|
||||
DeflaterOutputStream(buffer.outputStream()).use { this.encode(it) }
|
||||
}.readByteArray()
|
||||
|
||||
fun <T> ProtoAdapter<T>.decodeInflated(input: ByteArray): T = InflaterInputStream(ByteArrayInputStream(input)).use {
|
||||
this.decode(it)
|
||||
}
|
|
@ -21,21 +21,21 @@ import io.timelimit.android.coroutines.runAsync
|
|||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.integration.platform.PlatformIntegration
|
||||
import io.timelimit.android.sync.actions.DatabaseValidation
|
||||
import io.timelimit.android.sync.actions.DeleteCategoryAction
|
||||
import io.timelimit.android.sync.actions.RemoveUserAction
|
||||
import io.timelimit.android.logic.crypto.CryptDataHandler
|
||||
import io.timelimit.android.sync.actions.*
|
||||
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseParentActionDispatcher
|
||||
import io.timelimit.android.sync.network.ServerCryptContainer
|
||||
import io.timelimit.android.sync.network.ServerDataStatus
|
||||
|
||||
object ApplyServerDataStatus {
|
||||
suspend fun applyServerDataStatusCoroutine(status: ServerDataStatus, database: Database, platformIntegration: PlatformIntegration) {
|
||||
Threads.database.executeAndWait {
|
||||
suspend fun applyServerDataStatusCoroutine(status: ServerDataStatus, database: Database, platformIntegration: PlatformIntegration): Result {
|
||||
return Threads.database.executeAndWait {
|
||||
applyServerDataStatusSync(status, database, platformIntegration)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyServerDataStatusSync(status: ServerDataStatus, database: Database, platformIntegration: PlatformIntegration) {
|
||||
database.runInTransaction {
|
||||
fun applyServerDataStatusSync(status: ServerDataStatus, database: Database, platformIntegration: PlatformIntegration): Result {
|
||||
return database.runInTransaction {
|
||||
// this would override some local data which was not sent yet
|
||||
// so it's better to cancel in this case (or, more complicated,
|
||||
// apply the delta which was not sent locally)
|
||||
|
@ -53,6 +53,8 @@ object ApplyServerDataStatus {
|
|||
database.config().setServerApiLevelSync(status.apiLevel)
|
||||
}
|
||||
|
||||
var didCreateNewActions = false
|
||||
|
||||
run {
|
||||
val newUserList = status.newUserList
|
||||
|
||||
|
@ -123,9 +125,10 @@ object ApplyServerDataStatus {
|
|||
}
|
||||
}
|
||||
|
||||
run {
|
||||
val newDeviceTitles = run {
|
||||
// apply new device list
|
||||
val newDeviceList = status.newDeviceList
|
||||
val newDeviceTitles = mutableListOf<String>()
|
||||
|
||||
if (newDeviceList != null) {
|
||||
val oldDeviceList = database.device().getAllDevicesSync()
|
||||
|
@ -191,6 +194,8 @@ object ApplyServerDataStatus {
|
|||
qOrLater = newDevice.qOrLater,
|
||||
manipulationFlags = newDevice.manipulationFlags
|
||||
))
|
||||
|
||||
newDeviceTitles.add(newDevice.name)
|
||||
} else {
|
||||
// eventually update old entry
|
||||
|
||||
|
@ -239,44 +244,85 @@ object ApplyServerDataStatus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newDevice.publicKey != null) {
|
||||
val entry = database.deviceKey().getSync(newDevice.deviceId)
|
||||
|
||||
if (entry == null) {
|
||||
database.deviceKey().insert(DevicePublicKey(
|
||||
deviceId = newDevice.deviceId,
|
||||
publicKey = newDevice.publicKey,
|
||||
nextSequenceNumber = 0
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
database.config().setDeviceListVersionSync(newDeviceList.version)
|
||||
}
|
||||
|
||||
newDeviceTitles.toList()
|
||||
}
|
||||
|
||||
run {
|
||||
status.newInstalledApps.forEach {
|
||||
item ->
|
||||
status.updatedExtendedDeviceData.forEach { item ->
|
||||
fun handle(data: ServerCryptContainer, type: Int) {
|
||||
val result = CryptDataHandler.process(
|
||||
database = database,
|
||||
data = data,
|
||||
type = type,
|
||||
categoryId = null,
|
||||
deviceId = item.deviceId
|
||||
)
|
||||
|
||||
if (result.didCreateKeyRequests) {
|
||||
didCreateNewActions = true
|
||||
}
|
||||
}
|
||||
|
||||
item.appsBase?.let { handle(it, CryptContainerMetadata.TYPE_APP_LIST_BASE) }
|
||||
item.appsDiff?.let { handle(it, CryptContainerMetadata.TYPE_APP_LIST_DIFF) }
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
val disableLegacySync = database.config().isExperimentalFlagsSetSync(ExperimentalFlags.DISABLE_LEGACY_APP_SENDING)
|
||||
|
||||
for (item in status.newInstalledApps) {
|
||||
DatabaseValidation.assertDeviceExists(database, item.deviceId)
|
||||
|
||||
run {
|
||||
// apply apps
|
||||
database.app().deleteAllAppsByDeviceId(item.deviceId)
|
||||
database.app().addAppsSync(item.apps.map {
|
||||
App(
|
||||
if (
|
||||
database.cryptContainer().getCryptoMetadataSyncByDeviceId(item.deviceId, CryptContainerMetadata.TYPE_APP_LIST_BASE) == null ||
|
||||
(item.deviceId == database.config().getOwnDeviceIdSync() && !disableLegacySync)
|
||||
) {
|
||||
run {
|
||||
// apply apps
|
||||
database.app().deleteAllAppsByDeviceId(item.deviceId)
|
||||
database.app().addAppsSync(item.apps.map {
|
||||
App(
|
||||
deviceId = item.deviceId,
|
||||
packageName = it.packageName,
|
||||
title = it.title,
|
||||
isLaunchable = it.isLaunchable,
|
||||
recommendation = it.recommendation
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
run {
|
||||
// apply activities
|
||||
database.appActivity().deleteAppActivitiesByDeviceIds(listOf(item.deviceId))
|
||||
database.appActivity().addAppActivitiesSync(item.activities.map {
|
||||
AppActivity(
|
||||
run {
|
||||
// apply activities
|
||||
database.appActivity()
|
||||
.deleteAppActivitiesByDeviceIds(listOf(item.deviceId))
|
||||
database.appActivity().addAppActivitiesSync(item.activities.map {
|
||||
AppActivity(
|
||||
deviceId = item.deviceId,
|
||||
appPackageName = it.packageName,
|
||||
activityClassName = it.className,
|
||||
title = it.title
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
run {
|
||||
|
@ -565,8 +611,24 @@ object ApplyServerDataStatus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result(
|
||||
newDeviceTitles = newDeviceTitles,
|
||||
didCreateNewActions = didCreateNewActions
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun postNotifications(result: Result, platformIntegration: PlatformIntegration) {
|
||||
result.newDeviceTitles.forEach { deviceTitle ->
|
||||
platformIntegration.showNewDeviceNotification(deviceTitle)
|
||||
}
|
||||
}
|
||||
|
||||
class PendingSyncActionException: RuntimeException()
|
||||
|
||||
data class Result (
|
||||
val newDeviceTitles: List<String>,
|
||||
val didCreateNewActions: Boolean
|
||||
)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import io.timelimit.android.coroutines.runAsync
|
|||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.ServerLogic
|
||||
import io.timelimit.android.logic.crypto.CryptoSyncLogic
|
||||
import io.timelimit.android.sync.actions.apply.UploadActionsUtil
|
||||
import io.timelimit.android.sync.network.ClientDataStatus
|
||||
import io.timelimit.android.sync.network.api.UnauthorizedHttpError
|
||||
|
@ -246,9 +247,19 @@ class SyncUtil (private val logic: AppLogic) {
|
|||
}
|
||||
|
||||
private suspend fun pullStatus(server: ServerLogic.ServerConfig) {
|
||||
val currentStatus = ClientDataStatus.getClientDataStatusAsync(logic.database)
|
||||
val currentStatus = Threads.database.executeAndWait { ClientDataStatus.getClientDataStatusSync(logic.database) }
|
||||
val serverResponse = server.api.pullChanges(server.deviceAuthToken, currentStatus)
|
||||
ApplyServerDataStatus.applyServerDataStatusCoroutine(serverResponse, logic.database, logic.platformIntegration)
|
||||
val applyResult = ApplyServerDataStatus.applyServerDataStatusCoroutine(serverResponse, logic.database, logic.platformIntegration)
|
||||
ApplyServerDataStatus.postNotifications(applyResult, logic.platformIntegration)
|
||||
val cryptoResult = CryptoSyncLogic.postPullHook(logic.database, serverResponse.pendingKeyRequests, serverResponse.keyResponses)
|
||||
|
||||
if (applyResult.didCreateNewActions or cryptoResult.didSendReplies) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "request next sync by crypto handling")
|
||||
}
|
||||
|
||||
requestImportantSync()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun wipeCacheIfUpdated() {
|
||||
|
|
|
@ -23,6 +23,7 @@ import io.timelimit.android.data.customtypes.ImmutableBitmask
|
|||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.extensions.base64
|
||||
import io.timelimit.android.integration.platform.*
|
||||
import io.timelimit.android.sync.network.ParentPassword
|
||||
import io.timelimit.android.sync.validation.ListValidation
|
||||
|
@ -468,6 +469,144 @@ data class UpdateAppActivitiesAction(
|
|||
writer.endObject()
|
||||
}
|
||||
}
|
||||
data class UpdateInstalledAppsAction (
|
||||
val base: ByteArray?,
|
||||
val diff: ByteArray?,
|
||||
val wipe: Boolean
|
||||
): AppLogicAction() {
|
||||
companion object {
|
||||
private const val TYPE_VALUE = "UPDATE_INSTALLED_APPS"
|
||||
private const val BASE = "b"
|
||||
private const val DIFF = "d"
|
||||
private const val WIPE = "w"
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(TYPE).value(TYPE_VALUE)
|
||||
|
||||
base?.let { writer.name(BASE).value(it.base64()) }
|
||||
diff?.let { writer.name(DIFF).value(it.base64()) }
|
||||
|
||||
writer.name(WIPE).value(wipe)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
data class UploadDevicePublicKeyAction (val publicKey: ByteArray): AppLogicAction() {
|
||||
companion object {
|
||||
private const val TYPE_VALUE = "UPLOAD_DEVICE_PUBLIC_KEY"
|
||||
private const val KEY = "key"
|
||||
}
|
||||
|
||||
init {
|
||||
if (publicKey.size != 32) throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
writer.name(TYPE).value(TYPE_VALUE)
|
||||
writer.name(KEY).value(publicKey.base64())
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
data class SendKeyRequestAction(
|
||||
val deviceSequenceNumber: Long,
|
||||
val deviceId: String?,
|
||||
val categoryId: String?,
|
||||
val type: Int,
|
||||
val tempKey: ByteArray,
|
||||
val signature: ByteArray
|
||||
): AppLogicAction() {
|
||||
companion object {
|
||||
private const val TYPE_VALUE = "SEND_KEY_REQUEST"
|
||||
private const val SEQ_NUM = "dsn"
|
||||
private const val DEVICE_ID = "deviceId"
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val DATA_TYPE = "dataType"
|
||||
private const val TEMP_KEY = "tempKey"
|
||||
private const val SIGNATURE = "signature"
|
||||
}
|
||||
|
||||
init {
|
||||
if (tempKey.size != 32 || signature.size != 64) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (deviceId != null && categoryId != null) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
deviceId?.also { IdGenerator.assertIdValid(it) }
|
||||
categoryId?.also { IdGenerator.assertIdValid(it) }
|
||||
|
||||
if (!CryptContainerMetadata.isTypeValid(type)) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(TYPE).value(TYPE_VALUE)
|
||||
writer.name(SEQ_NUM).value(deviceSequenceNumber)
|
||||
deviceId?.let { writer.name(DEVICE_ID).value(it) }
|
||||
categoryId?.let { writer.name(CATEGORY_ID).value(it) }
|
||||
writer.name(DATA_TYPE).value(type)
|
||||
writer.name(TEMP_KEY).value(tempKey.base64())
|
||||
writer.name(SIGNATURE).value(signature.base64())
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
data class FinishKeyRequestAction(val deviceSequenceNumber: Long): AppLogicAction() {
|
||||
companion object {
|
||||
private const val TYPE_VALUE = "FINISH_KEY_REQUEST"
|
||||
private const val SEQ_NUM = "dsn"
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(TYPE).value(TYPE_VALUE)
|
||||
writer.name(SEQ_NUM).value(deviceSequenceNumber)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
data class ReplyToKeyRequestAction(
|
||||
val requestServerSequenceNumber: Long,
|
||||
val tempKey: ByteArray,
|
||||
val encryptedKey: ByteArray,
|
||||
val signature: ByteArray
|
||||
): AppLogicAction() {
|
||||
companion object {
|
||||
private const val TYPE_VALUE = "REPLY_TO_KEY_REQUEST"
|
||||
private const val REQUEST_SEQUENCE_NUMBER = "rsn"
|
||||
private const val TEMP_KEY = "tempKey"
|
||||
private const val ENCRYPTED_KEY = "encryptedKey"
|
||||
private const val SIGNATURE = "signature"
|
||||
}
|
||||
|
||||
init {
|
||||
if (tempKey.size != 32 || encryptedKey.size != 16 || signature.size != 64) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(TYPE).value(TYPE_VALUE)
|
||||
writer.name(REQUEST_SEQUENCE_NUMBER).value(requestServerSequenceNumber)
|
||||
writer.name(TEMP_KEY).value(tempKey.base64())
|
||||
writer.name(ENCRYPTED_KEY).value(encryptedKey.base64())
|
||||
writer.name(SIGNATURE).value(signature.base64())
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
object SignOutAtDeviceAction: AppLogicAction() {
|
||||
const val TYPE_VALUE = "SIGN_OUT_AT_DEVICE"
|
||||
|
||||
|
|
|
@ -38,6 +38,19 @@ import java.io.StringWriter
|
|||
object ApplyActionUtil {
|
||||
private const val LOG_TAG = "ApplyActionUtil"
|
||||
|
||||
fun addAppLogicActionToDatabaseSync(action: AppLogicAction, database: Database) = database.runInUnobservedTransaction {
|
||||
database.pendingSyncAction().addSyncActionSync(
|
||||
PendingSyncAction(
|
||||
sequenceNumber = database.config().getNextSyncActionSequenceActionAndIncrementIt(),
|
||||
scheduledForUpload = false,
|
||||
type = PendingSyncActionType.AppLogic,
|
||||
userId = "",
|
||||
integrity = "",
|
||||
encodedAction = SerializationUtil.serializeAction(action)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun applyAppLogicAction(
|
||||
action: AppLogicAction,
|
||||
appLogic: AppLogic,
|
||||
|
@ -163,20 +176,7 @@ object ApplyActionUtil {
|
|||
}
|
||||
}
|
||||
|
||||
val serializedAction = StringWriter().apply {
|
||||
JsonWriter(this).apply {
|
||||
action.serialize(this)
|
||||
}
|
||||
}.toString()
|
||||
|
||||
database.pendingSyncAction().addSyncActionSync(PendingSyncAction(
|
||||
sequenceNumber = database.config().getNextSyncActionSequenceActionAndIncrementIt(),
|
||||
encodedAction = serializedAction,
|
||||
integrity = "",
|
||||
scheduledForUpload = false,
|
||||
type = PendingSyncActionType.AppLogic,
|
||||
userId = ""
|
||||
))
|
||||
addAppLogicActionToDatabaseSync(action, database)
|
||||
|
||||
if (action is AddUsedTimeActionVersion2) {
|
||||
syncUtil.requestVeryUnimportantSync()
|
||||
|
|
|
@ -26,12 +26,15 @@ class UploadActionsUtil(private val database: Database, private val syncConflict
|
|||
companion object {
|
||||
private const val BATCH_SIZE = 25
|
||||
|
||||
fun deleteAllVersionNumbersSync(database: Database) {
|
||||
fun deleteAllVersionNumbersSync(database: Database, wipeCryptoRequests: Boolean = false) {
|
||||
database.runInTransaction {
|
||||
database.config().setUserListVersionSync("")
|
||||
database.config().setDeviceListVersionSync("")
|
||||
database.device().deleteAllInstalledAppsVersions()
|
||||
database.category().deleteAllCategoriesVersionNumbers()
|
||||
database.cryptContainer().deleteAllServerVersionNumbers()
|
||||
|
||||
if (wipeCryptoRequests) { database.cryptContainerKeyRequest().deleteAll() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -388,6 +388,11 @@ object LocalDatabaseAppLogicActionDispatcher {
|
|||
|
||||
database.childTasks().updateItemSync(task.copy(pendingRequest = true))
|
||||
}
|
||||
is UpdateInstalledAppsAction -> {/* nothing to do, this is only for the server */}
|
||||
is UploadDevicePublicKeyAction -> {/* nothing to do, this is only for the server */}
|
||||
is SendKeyRequestAction -> {/* nothing to do, this is only for the server */}
|
||||
is FinishKeyRequestAction -> {/* nothing to do, this is only for the server */}
|
||||
is ReplyToKeyRequestAction -> {/* nothing to do, this is only for the server */}
|
||||
}.let { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -17,16 +17,15 @@ package io.timelimit.android.sync.network
|
|||
|
||||
import android.util.JsonWriter
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.waitForNonNullValue
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
data class ClientDataStatus(
|
||||
val deviceListVersion: String,
|
||||
val installedAppsVersionsByDeviceId: Map<String, String>,
|
||||
val deviceDetailData: Map<String, DeviceDataStatus>,
|
||||
val categories: Map<String, CategoryDataStatus>,
|
||||
val userListVersion: String
|
||||
val userListVersion: String,
|
||||
val lastKeyRequestServerSequence: Long?,
|
||||
val lastKeyResponseServerSequence: Long?
|
||||
) {
|
||||
companion object {
|
||||
private const val DEVICES = "devices"
|
||||
|
@ -34,44 +33,59 @@ data class ClientDataStatus(
|
|||
private const val CATEGORIES = "categories"
|
||||
private const val USERS = "users"
|
||||
private const val CLIENT_LEVEL = "clientLevel"
|
||||
private const val CLIENT_LEVEL_VALUE = 3
|
||||
private const val DEVICES_DETAIL = "devicesDetail"
|
||||
private const val LAST_KEY_REQUEST_SEQUENCE = "kri"
|
||||
private const val LAST_KEY_RESPONSE_SEQUENCE = "kr"
|
||||
private const val CLIENT_LEVEL_VALUE = 4
|
||||
|
||||
val empty = ClientDataStatus(
|
||||
deviceListVersion = "",
|
||||
installedAppsVersionsByDeviceId = Collections.emptyMap(),
|
||||
categories = Collections.emptyMap(),
|
||||
userListVersion = ""
|
||||
deviceListVersion = "",
|
||||
installedAppsVersionsByDeviceId = emptyMap(),
|
||||
deviceDetailData = emptyMap(),
|
||||
categories = emptyMap(),
|
||||
userListVersion = "",
|
||||
lastKeyRequestServerSequence = null,
|
||||
lastKeyResponseServerSequence = null
|
||||
)
|
||||
|
||||
suspend fun getClientDataStatusAsync(database: Database): ClientDataStatus {
|
||||
return ClientDataStatus(
|
||||
deviceListVersion = database.config().getDeviceListVersion().waitForNonNullValue(),
|
||||
installedAppsVersionsByDeviceId = database.device().getInstalledAppsVersions().map {
|
||||
val devicesWithAppVersions = it
|
||||
val result = HashMap<String, String>()
|
||||
fun getClientDataStatusSync(database: Database): ClientDataStatus {
|
||||
return database.runInUnobservedTransaction {
|
||||
ClientDataStatus(
|
||||
deviceListVersion = database.config().getDeviceListVersionSync(),
|
||||
installedAppsVersionsByDeviceId = database.device()
|
||||
.getInstalledAppsVersionsSync()
|
||||
.associateBy { it.deviceId }
|
||||
.mapValues { it.value.installedAppsVersions },
|
||||
deviceDetailData = if (database.config().getServerApiLevelSync() >= 4)
|
||||
database.device().getDeviceDetailDataSync()
|
||||
.associateBy { it.deviceId }
|
||||
.mapValues {
|
||||
val item = it.value
|
||||
|
||||
devicesWithAppVersions.forEach { result[it.deviceId] = it.installedAppsVersions }
|
||||
DeviceDataStatus(
|
||||
appsBaseVersion = item.appBaseVersion,
|
||||
appsDiffVersion = item.appDiffVersion
|
||||
)
|
||||
}
|
||||
else emptyMap(),
|
||||
categories = database.category().getCategoriesWithVersionNumbersSybc()
|
||||
.associateBy { it.categoryId }
|
||||
.mapValues {
|
||||
val item = it.value
|
||||
|
||||
Collections.unmodifiableMap(result)
|
||||
}.waitForNonNullValue(),
|
||||
categories = database.category().getCategoriesWithVersionNumbers().map {
|
||||
val categoriesWithVersions = it
|
||||
val result = HashMap<String, CategoryDataStatus>()
|
||||
|
||||
categoriesWithVersions.forEach {
|
||||
result[it.categoryId] = CategoryDataStatus(
|
||||
baseVersion = it.baseVersion,
|
||||
assignedAppsVersion = it.assignedAppsVersion,
|
||||
timeLimitRulesVersion = it.timeLimitRulesVersion,
|
||||
usedTimeItemsVersion = it.usedTimeItemsVersion,
|
||||
taskListVersion = it.taskListVersion
|
||||
CategoryDataStatus(
|
||||
baseVersion = item.baseVersion,
|
||||
assignedAppsVersion = item.assignedAppsVersion,
|
||||
timeLimitRulesVersion = item.timeLimitRulesVersion,
|
||||
usedTimeItemsVersion = item.usedTimeItemsVersion,
|
||||
taskListVersion = item.taskListVersion
|
||||
)
|
||||
}
|
||||
|
||||
Collections.unmodifiableMap(result)
|
||||
}.waitForNonNullValue(),
|
||||
userListVersion = database.config().getUserListVersion().waitForNonNullValue()
|
||||
)
|
||||
},
|
||||
userListVersion = database.config().getUserListVersionSync(),
|
||||
lastKeyRequestServerSequence = database.config().getLastServerKeyRequestSequenceSync(),
|
||||
lastKeyResponseServerSequence = database.config().getLastServerKeyResponseSequenceSync()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,6 +103,16 @@ data class ClientDataStatus(
|
|||
}
|
||||
writer.endObject()
|
||||
|
||||
if (deviceDetailData.isNotEmpty()) {
|
||||
writer.name(DEVICES_DETAIL)
|
||||
writer.beginObject()
|
||||
deviceDetailData.entries.forEach {
|
||||
writer.name(it.key)
|
||||
it.value.serialize(writer)
|
||||
}
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
writer.name(CATEGORIES)
|
||||
writer.beginObject()
|
||||
categories.entries.forEach {
|
||||
|
@ -97,6 +121,9 @@ data class ClientDataStatus(
|
|||
}
|
||||
writer.endObject()
|
||||
|
||||
lastKeyRequestServerSequence?.let { writer.name(LAST_KEY_REQUEST_SEQUENCE).value(it) }
|
||||
lastKeyResponseServerSequence?.let { writer.name(LAST_KEY_RESPONSE_SEQUENCE).value(it) }
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
@ -126,6 +153,25 @@ data class CategoryDataStatus(
|
|||
|
||||
if (taskListVersion.isNotEmpty()) writer.name(TASK_LIST_VERSION).value(taskListVersion)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
|
||||
data class DeviceDataStatus (
|
||||
val appsBaseVersion: String?,
|
||||
val appsDiffVersion: String?
|
||||
) {
|
||||
companion object {
|
||||
private const val APPS_BASE_VERSION = "appsB"
|
||||
private const val APPS_DIFF_VERSION = "appsD"
|
||||
}
|
||||
|
||||
fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
appsBaseVersion?.let { writer.name(APPS_BASE_VERSION).value(it) }
|
||||
appsDiffVersion?.let { writer.name(APPS_DIFF_VERSION).value(it) }
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import io.timelimit.android.data.customtypes.ImmutableBitmask
|
|||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.extensions.parseBase64
|
||||
import io.timelimit.android.extensions.parseList
|
||||
import io.timelimit.android.integration.platform.*
|
||||
import io.timelimit.android.sync.actions.AppActivityItem
|
||||
|
@ -34,6 +35,7 @@ import kotlin.collections.ArrayList
|
|||
|
||||
data class ServerDataStatus(
|
||||
val newDeviceList: ServerDeviceList?,
|
||||
val updatedExtendedDeviceData: List<ServerExtendedDeviceData>,
|
||||
val newInstalledApps: List<ServerInstalledAppsData>,
|
||||
val removedCategories: List<String>,
|
||||
val newCategoryBaseData: List<ServerUpdatedCategoryBaseData>,
|
||||
|
@ -42,12 +44,15 @@ data class ServerDataStatus(
|
|||
val newCategoryTimeLimitRules: List<ServerUpdatedTimeLimitRules>,
|
||||
val newCategoryTasks: List<ServerUpdatedCategoryTasks>,
|
||||
val newUserList: ServerUserList?,
|
||||
val pendingKeyRequests: List<ServerKeyRequest>,
|
||||
val keyResponses: List<ServerKeyResponse>,
|
||||
val fullVersionUntil: Long,
|
||||
val message: String?,
|
||||
val apiLevel: Int
|
||||
) {
|
||||
companion object {
|
||||
private const val NEW_DEVICE_LIST = "devices"
|
||||
private const val UPDATED_EXTENDED_DEVICE_DATA = "devices2"
|
||||
private const val NEW_INSTALLED_APPS = "apps"
|
||||
private const val REMOVED_CATEGORIES = "rmCategories"
|
||||
private const val NEW_CATEGORIES_BASE_DATA = "categoryBase"
|
||||
|
@ -56,12 +61,15 @@ data class ServerDataStatus(
|
|||
private const val NEW_CATEGORY_TIME_LIMIT_RULES = "rules"
|
||||
private const val NEW_CATEGORY_TASKS = "tasks"
|
||||
private const val NEW_USER_LIST = "users"
|
||||
private const val PENDING_KEY_REQUESTS = "krq"
|
||||
private const val KEY_RESPONSES = "kr"
|
||||
private const val FULL_VERSION_UNTIL = "fullVersion"
|
||||
private const val MESSAGE = "message"
|
||||
private const val API_LEVEL = "apiLevel"
|
||||
|
||||
fun parse(reader: JsonReader): ServerDataStatus {
|
||||
var newDeviceList: ServerDeviceList? = null
|
||||
var updatedExtendedDeviceData: List<ServerExtendedDeviceData> = emptyList()
|
||||
var newInstalledApps: List<ServerInstalledAppsData> = Collections.emptyList()
|
||||
var removedCategories: List<String> = Collections.emptyList()
|
||||
var newCategoryBaseData: List<ServerUpdatedCategoryBaseData> = Collections.emptyList()
|
||||
|
@ -70,6 +78,8 @@ data class ServerDataStatus(
|
|||
var newCategoryTimeLimitRules: List<ServerUpdatedTimeLimitRules> = Collections.emptyList()
|
||||
var newCategoryTasks: List<ServerUpdatedCategoryTasks> = emptyList()
|
||||
var newUserList: ServerUserList? = null
|
||||
var pendingKeyRequests = emptyList<ServerKeyRequest>()
|
||||
var keyResponses = emptyList<ServerKeyResponse>()
|
||||
var fullVersionUntil: Long? = null
|
||||
var message: String? = null
|
||||
var apiLevel = 0
|
||||
|
@ -78,6 +88,7 @@ data class ServerDataStatus(
|
|||
while (reader.hasNext()) {
|
||||
when(reader.nextName()) {
|
||||
NEW_DEVICE_LIST -> newDeviceList = ServerDeviceList.parse(reader)
|
||||
UPDATED_EXTENDED_DEVICE_DATA -> updatedExtendedDeviceData = parseJsonArray(reader, { ServerExtendedDeviceData.parse(reader) })
|
||||
NEW_INSTALLED_APPS -> newInstalledApps = ServerInstalledAppsData.parseList(reader)
|
||||
REMOVED_CATEGORIES -> removedCategories = parseJsonStringArray(reader)
|
||||
NEW_CATEGORIES_BASE_DATA -> newCategoryBaseData = ServerUpdatedCategoryBaseData.parseList(reader)
|
||||
|
@ -86,6 +97,8 @@ data class ServerDataStatus(
|
|||
NEW_CATEGORY_TIME_LIMIT_RULES -> newCategoryTimeLimitRules = ServerUpdatedTimeLimitRules.parseList(reader)
|
||||
NEW_CATEGORY_TASKS -> newCategoryTasks = ServerUpdatedCategoryTasks.parseList(reader)
|
||||
NEW_USER_LIST -> newUserList = ServerUserList.parse(reader)
|
||||
PENDING_KEY_REQUESTS -> pendingKeyRequests = ServerKeyRequest.parseList(reader)
|
||||
KEY_RESPONSES -> keyResponses = ServerKeyResponse.parseList(reader)
|
||||
FULL_VERSION_UNTIL -> fullVersionUntil = reader.nextLong()
|
||||
MESSAGE -> message = reader.nextString()
|
||||
API_LEVEL -> apiLevel = reader.nextInt()
|
||||
|
@ -95,18 +108,21 @@ data class ServerDataStatus(
|
|||
reader.endObject()
|
||||
|
||||
return ServerDataStatus(
|
||||
newDeviceList = newDeviceList,
|
||||
newInstalledApps = newInstalledApps,
|
||||
removedCategories = removedCategories,
|
||||
newCategoryBaseData = newCategoryBaseData,
|
||||
newCategoryAssignedApps = newCategoryAssignedApps,
|
||||
newCategoryUsedTimes = newCategoryUsedTimes,
|
||||
newCategoryTimeLimitRules = newCategoryTimeLimitRules,
|
||||
newCategoryTasks = newCategoryTasks,
|
||||
newUserList = newUserList,
|
||||
fullVersionUntil = fullVersionUntil!!,
|
||||
message = message,
|
||||
apiLevel = apiLevel
|
||||
newDeviceList = newDeviceList,
|
||||
updatedExtendedDeviceData = updatedExtendedDeviceData,
|
||||
newInstalledApps = newInstalledApps,
|
||||
removedCategories = removedCategories,
|
||||
newCategoryBaseData = newCategoryBaseData,
|
||||
newCategoryAssignedApps = newCategoryAssignedApps,
|
||||
newCategoryUsedTimes = newCategoryUsedTimes,
|
||||
newCategoryTimeLimitRules = newCategoryTimeLimitRules,
|
||||
newCategoryTasks = newCategoryTasks,
|
||||
newUserList = newUserList,
|
||||
pendingKeyRequests = pendingKeyRequests,
|
||||
keyResponses = keyResponses,
|
||||
fullVersionUntil = fullVersionUntil!!,
|
||||
message = message,
|
||||
apiLevel = apiLevel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -299,7 +315,8 @@ data class ServerDeviceData(
|
|||
val wasAccessibilityServiceEnabled: Boolean,
|
||||
val enableActivityLevelBlocking: Boolean,
|
||||
val qOrLater: Boolean,
|
||||
val manipulationFlags: Long
|
||||
val manipulationFlags: Long,
|
||||
val publicKey: ByteArray?
|
||||
) {
|
||||
companion object {
|
||||
private const val DEVICE_ID = "deviceId"
|
||||
|
@ -333,6 +350,7 @@ data class ServerDeviceData(
|
|||
private const val ENABLE_ACTIVITY_LEVEL_BLOCKING = "activityLevelBlocking"
|
||||
private const val Q_OR_LATER = "qOrLater"
|
||||
private const val MANIPULATION_FLAGS = "mFlags"
|
||||
private const val PUBLIC_KEY = "pk"
|
||||
|
||||
fun parse(reader: JsonReader): ServerDeviceData {
|
||||
var deviceId: String? = null
|
||||
|
@ -366,6 +384,7 @@ data class ServerDeviceData(
|
|||
var enableActivityLevelBlocking = false
|
||||
var qOrLater = false
|
||||
var manipulationFlags = 0L
|
||||
var publicKey: ByteArray? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
|
@ -401,6 +420,7 @@ data class ServerDeviceData(
|
|||
ENABLE_ACTIVITY_LEVEL_BLOCKING -> enableActivityLevelBlocking = reader.nextBoolean()
|
||||
Q_OR_LATER -> qOrLater = reader.nextBoolean()
|
||||
MANIPULATION_FLAGS -> manipulationFlags = reader.nextLong()
|
||||
PUBLIC_KEY -> publicKey = reader.nextString().parseBase64()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
@ -437,7 +457,8 @@ data class ServerDeviceData(
|
|||
wasAccessibilityServiceEnabled = wasAccessibilityServiceEnabled!!,
|
||||
enableActivityLevelBlocking = enableActivityLevelBlocking,
|
||||
qOrLater = qOrLater,
|
||||
manipulationFlags = manipulationFlags
|
||||
manipulationFlags = manipulationFlags,
|
||||
publicKey = publicKey
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1065,3 +1086,182 @@ data class ServerInstalledAppsData(
|
|||
fun parseList(reader: JsonReader) = parseJsonArray(reader) { parse(reader) }
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerExtendedDeviceData(
|
||||
val deviceId: String,
|
||||
val appsBase: ServerCryptContainer?,
|
||||
val appsDiff: ServerCryptContainer?
|
||||
) {
|
||||
companion object {
|
||||
private const val DEVICE_ID = "deviceId"
|
||||
private const val APPS_BASE = "appsBase"
|
||||
private const val APPS_DIFF = "appsDiff"
|
||||
|
||||
fun parse(reader: JsonReader): ServerExtendedDeviceData {
|
||||
var deviceId: String? = null
|
||||
var appsBase: ServerCryptContainer? = null
|
||||
var appsDiff: ServerCryptContainer? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
DEVICE_ID -> deviceId = reader.nextString()
|
||||
APPS_BASE -> appsBase = ServerCryptContainer.parse(reader)
|
||||
APPS_DIFF -> appsDiff = ServerCryptContainer.parse(reader)
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return ServerExtendedDeviceData(
|
||||
deviceId = deviceId!!,
|
||||
appsBase = appsBase,
|
||||
appsDiff = appsDiff
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerCryptContainer(
|
||||
val version: String,
|
||||
val data: ByteArray
|
||||
) {
|
||||
companion object {
|
||||
private const val VERSION = "version"
|
||||
private const val DATA = "data"
|
||||
|
||||
fun parse(reader: JsonReader): ServerCryptContainer {
|
||||
var version: String? = null
|
||||
var data: ByteArray? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
VERSION -> version = reader.nextString()
|
||||
DATA -> data = reader.nextString().parseBase64()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return ServerCryptContainer(
|
||||
version = version!!,
|
||||
data = data!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerKeyRequest(
|
||||
val serverRequestSequenceNumber: Long,
|
||||
val senderDeviceId: String,
|
||||
val senderSequenceNumber: Long,
|
||||
val tempKey: ByteArray,
|
||||
val deviceId: String?,
|
||||
val categoryId: String?,
|
||||
val type: Int,
|
||||
val signature: ByteArray
|
||||
) {
|
||||
companion object {
|
||||
private const val SERVER_REQUEST_SEQUENCE_NUMBER = "srvSeq"
|
||||
private const val SENDER_DEVICE_ID = "senId"
|
||||
private const val SENDER_SEQUENCE_NUMBER = "senSeq"
|
||||
private const val DEVICE_ID = "deviceId"
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val TYPE = "type"
|
||||
private const val TEMP_KEY = "tempKey"
|
||||
private const val SIGNATURE = "signature"
|
||||
|
||||
fun parse(reader: JsonReader): ServerKeyRequest {
|
||||
var serverRequestSequenceNumber: Long? = null
|
||||
var senderDeviceId: String? = null
|
||||
var senderSequenceNumber: Long? = null
|
||||
var tempKey: ByteArray? = null
|
||||
var deviceId: String? = null
|
||||
var categoryId: String? = null
|
||||
var type: Int? = null
|
||||
var signature: ByteArray? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
SERVER_REQUEST_SEQUENCE_NUMBER -> serverRequestSequenceNumber = reader.nextLong()
|
||||
SENDER_DEVICE_ID -> senderDeviceId = reader.nextString()
|
||||
SENDER_SEQUENCE_NUMBER -> senderSequenceNumber = reader.nextLong()
|
||||
TEMP_KEY -> tempKey = reader.nextString().parseBase64()
|
||||
DEVICE_ID -> deviceId = reader.nextString()
|
||||
CATEGORY_ID -> categoryId = reader.nextString()
|
||||
TYPE -> type = reader.nextInt()
|
||||
SIGNATURE -> signature = reader.nextString().parseBase64()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return ServerKeyRequest(
|
||||
serverRequestSequenceNumber = serverRequestSequenceNumber!!,
|
||||
senderDeviceId = senderDeviceId!!,
|
||||
senderSequenceNumber = senderSequenceNumber!!,
|
||||
tempKey = tempKey!!,
|
||||
deviceId = deviceId,
|
||||
categoryId = categoryId,
|
||||
type = type!!,
|
||||
signature = signature!!
|
||||
)
|
||||
}
|
||||
|
||||
fun parseList(reader: JsonReader): List<ServerKeyRequest> = parseJsonArray(reader) { parse(reader) }
|
||||
}
|
||||
}
|
||||
|
||||
data class ServerKeyResponse(
|
||||
val serverResponseSequenceNumber: Long,
|
||||
val senderDeviceId: String,
|
||||
val requestSequenceId: Long,
|
||||
val tempKey: ByteArray,
|
||||
val encryptedKey: ByteArray,
|
||||
val signature: ByteArray
|
||||
) {
|
||||
companion object {
|
||||
private const val SERVER_RESPONSE_SEQUENCE = "srvSeq"
|
||||
private const val SENDER_DEVICE_ID = "sender"
|
||||
private const val REQUEST_SEQUENCE_ID = "rqSeq"
|
||||
private const val TEMP_KEY = "tempKey"
|
||||
private const val ENCRYPTED_KEY = "cryptKey"
|
||||
private const val SIGNATURE = "signature"
|
||||
|
||||
fun parse(reader: JsonReader): ServerKeyResponse {
|
||||
var serverResponseSequenceNumber: Long? = null
|
||||
var senderDeviceId: String? = null
|
||||
var requestSequenceId: Long? = null
|
||||
var tempKey: ByteArray? = null
|
||||
var encryptedKey: ByteArray? = null
|
||||
var signature: ByteArray? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
SERVER_RESPONSE_SEQUENCE -> serverResponseSequenceNumber = reader.nextLong()
|
||||
SENDER_DEVICE_ID -> senderDeviceId = reader.nextString()
|
||||
REQUEST_SEQUENCE_ID -> requestSequenceId = reader.nextLong()
|
||||
TEMP_KEY -> tempKey = reader.nextString().parseBase64()
|
||||
ENCRYPTED_KEY -> encryptedKey = reader.nextString().parseBase64()
|
||||
SIGNATURE -> signature = reader.nextString().parseBase64()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return ServerKeyResponse(
|
||||
serverResponseSequenceNumber = serverResponseSequenceNumber!!,
|
||||
senderDeviceId = senderDeviceId!!,
|
||||
requestSequenceId = requestSequenceId!!,
|
||||
tempKey = tempKey!!,
|
||||
encryptedKey = encryptedKey!!,
|
||||
signature = signature!!
|
||||
)
|
||||
}
|
||||
|
||||
fun parseList(reader: JsonReader) = parseJsonArray(reader) { parse(reader) }
|
||||
}
|
||||
}
|
|
@ -195,6 +195,12 @@ data class DiagnoseExperimentalFlagItem(
|
|||
enableFlags = ExperimentalFlags.STRICT_OVERLAY_CHECKING,
|
||||
disableFlags = ExperimentalFlags.STRICT_OVERLAY_CHECKING,
|
||||
enable = { true }
|
||||
),
|
||||
DiagnoseExperimentalFlagItem(
|
||||
label = R.string.diagnose_exf_dls,
|
||||
enableFlags = ExperimentalFlags.DISABLE_LEGACY_APP_SENDING,
|
||||
disableFlags = ExperimentalFlags.DISABLE_LEGACY_APP_SENDING,
|
||||
enable = { true }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class DiagnoseSyncFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
binding.clearCacheBtn.setOnClickListener {
|
||||
Threads.database.execute {
|
||||
UploadActionsUtil.deleteAllVersionNumbersSync(logic.database)
|
||||
UploadActionsUtil.deleteAllVersionNumbersSync(logic.database, wipeCryptoRequests = true)
|
||||
}
|
||||
|
||||
Toast.makeText(requireContext(), R.string.diagnose_sync_btn_clear_cache_toast, Toast.LENGTH_SHORT).show()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* 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
|
||||
|
@ -24,15 +24,15 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.lifecycle.switchMap
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import io.timelimit.android.crypto.HexString
|
||||
import io.timelimit.android.data.model.Device
|
||||
import io.timelimit.android.databinding.FragmentManageDeviceBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.RealTime
|
||||
|
@ -150,6 +150,26 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
binding.isThisDevice = it
|
||||
})
|
||||
|
||||
val signingKey = isThisDevice.switchMap { isLocalDevice ->
|
||||
if (isLocalDevice) {
|
||||
logic.fullVersion.isLocalMode.switchMap { isLocalMode ->
|
||||
if (isLocalMode) liveDataFromNullableValue(null)
|
||||
else
|
||||
logic.database.config().getSigningKeyAsync().map {
|
||||
if (it != null) Curve25519.getPublicKey(it)
|
||||
else null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logic.database.deviceKey().getLive(args.deviceId).map {
|
||||
it?.publicKey
|
||||
}
|
||||
}
|
||||
}.map {
|
||||
if (it == null) null
|
||||
else HexString.toHex(it)
|
||||
}
|
||||
|
||||
ManageDeviceIntroduction.bind(
|
||||
view = binding.introduction,
|
||||
database = logic.database,
|
||||
|
@ -182,6 +202,8 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
binding.userCardText = it?.name ?: getString(R.string.manage_device_current_user_none)
|
||||
})
|
||||
|
||||
signingKey.observe(viewLifecycleOwner) { binding.devicePublicKey = it }
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
|
59
app/src/main/proto/io/timelimit/proto/applist.proto
Normal file
59
app/src/main/proto/io/timelimit/proto/applist.proto
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
syntax = "proto3";
|
||||
|
||||
package io.timelimit.proto.applist;
|
||||
|
||||
message InstalledAppProto {
|
||||
string package_name = 1;
|
||||
string title = 2;
|
||||
bool is_launchable = 3;
|
||||
Recommendation recommendation = 4;
|
||||
|
||||
enum Recommendation {
|
||||
NONE = 0;
|
||||
WHITELIST = 1;
|
||||
BLACKLIST = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message InstalledAppActivityProto {
|
||||
string package_name = 1;
|
||||
string class_name = 2;
|
||||
string title = 3;
|
||||
}
|
||||
|
||||
message RemovedAppActivityProto {
|
||||
string package_name = 1;
|
||||
string class_name = 2;
|
||||
}
|
||||
|
||||
message InstalledAppsProto {
|
||||
repeated InstalledAppProto apps = 1;
|
||||
repeated InstalledAppActivityProto activities = 2;
|
||||
}
|
||||
|
||||
message InstalledAppsDifferenceProto {
|
||||
InstalledAppsProto added = 1;
|
||||
repeated string removed_packages = 2;
|
||||
repeated RemovedAppActivityProto removed_activities = 3;
|
||||
}
|
||||
|
||||
message SavedAppsDifferenceProto {
|
||||
InstalledAppsDifferenceProto apps = 1;
|
||||
int64 base_generation = 2;
|
||||
int64 base_counter = 3;
|
||||
}
|
|
@ -50,8 +50,13 @@
|
|||
name="handlers"
|
||||
type="io.timelimit.android.ui.manage.device.manage.ManageDeviceFragmentHandlers" />
|
||||
|
||||
<variable
|
||||
name="devicePublicKey"
|
||||
type="String" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
<import type="io.timelimit.android.BuildConfig" />
|
||||
<import type="android.text.TextUtils" />
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
|
@ -107,6 +112,14 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{TextUtils.isEmpty(devicePublicKey) ? View.GONE : View.VISIBLE}"
|
||||
android:text="@{@string/manage_device_signing_key(devicePublicKey)}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
tools:text="Signing Key: 1234"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{safeUnbox(isThisDevice) ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
|
|
|
@ -531,6 +531,7 @@
|
|||
<string name="diagnose_exf_esb">Overlay und Home-Button nicht zum Sperren verwenden</string>
|
||||
<string name="diagnose_exf_srn">Toasts zur Synchronisation anzeigen</string>
|
||||
<string name="diagnose_exf_soc">strengere Prüfung der Überlagerungs-Berechtigung aktivieren</string>
|
||||
<string name="diagnose_exf_dls">deaktiviere Senden der App-Liste im alten Format</string>
|
||||
|
||||
<string name="diagnose_bg_task_loop_ex">Hintergrundaufgabenschleifenfehler</string>
|
||||
|
||||
|
@ -894,6 +895,7 @@
|
|||
|
||||
<string name="manage_device_added_at">Hinzugefügt: %s</string>
|
||||
<string name="manage_device_is_this_device">Das ist das Gerät, das Sie gerade vor sich haben</string>
|
||||
<string name="manage_device_signing_key">Signaturschlüssel: %s</string>
|
||||
|
||||
<string name="manage_device_current_user_title">Benutzer des Geräts</string>
|
||||
<string name="manage_device_current_user_none">Keine Angabe - keine Begrenzungen</string>
|
||||
|
@ -1144,6 +1146,9 @@
|
|||
<string name="notification_channel_reset_title">Reset-Benachrichtigung</string>
|
||||
<string name="notification_channel_reset_text">Zeigt eine Benachrichtigung an, wenn TimeLimit an diesem Gerät zurückgesetzt wurde</string>
|
||||
|
||||
<string name="notification_channel_new_device_title">neue Geräte</string>
|
||||
<string name="notification_channel_new_device_description">Zeigt eine Benachrichtigung an, wenn TimeLimit mit Vernetzung verwendet wird und ein neues Gerät verknüpft wurde</string>
|
||||
|
||||
<string name="notification_filter_not_blocked_title">TimeLimit hat eine Benachrichtigung blockiert</string>
|
||||
<string name="notification_filter_blocking_failed_title">TimeLimit konnte eine Benachrichtigung nicht blockieren</string>
|
||||
|
||||
|
@ -1157,6 +1162,8 @@
|
|||
<string name="notification_background_sync_title">TimeLimit ist aktiv</string>
|
||||
<string name="notification_background_sync_text">eine Synchronisation läuft</string>
|
||||
|
||||
<string name="notification_new_device_title">Gerät hinzugefügt</string>
|
||||
|
||||
<string name="obsolete_message">Sie verwenden TimeLimit auf einer älteren Android-Version.
|
||||
Das kann funktionieren, aber es wird nicht empfohlen.
|
||||
</string>
|
||||
|
|
|
@ -105,6 +105,8 @@
|
|||
(<a href="https://github.com/signalapp/curve25519-java/blob/master/LICENSE">GNU General Public License v3.0</a>)
|
||||
\n<a href="https://github.com/zxing/zxing">ZXing</a>
|
||||
(<a href="https://github.com/zxing/zxing/blob/master/LICENSE">Apache License 2.0</a>)
|
||||
\n<a href="https://github.com/square/wire/blob/master/README.md">Wire</a>
|
||||
(<a href="https://github.com/square/wire/blob/master/LICENSE.txt">Apache License 2.0</a>)
|
||||
</string>
|
||||
|
||||
<string name="about_diagnose_title">Error diagnose</string>
|
||||
|
@ -581,6 +583,7 @@
|
|||
<string name="diagnose_exf_esb">Do not use a overlay or the home button for blocking</string>
|
||||
<string name="diagnose_exf_srn">Show sync related toasts</string>
|
||||
<string name="diagnose_exf_soc">Enable strict overlay permission check</string>
|
||||
<string name="diagnose_exf_dls">Disable sending the App List with the legacy encoding</string>
|
||||
|
||||
<string name="diagnose_bg_task_loop_ex">Background task loop exception</string>
|
||||
|
||||
|
@ -937,6 +940,7 @@
|
|||
|
||||
<string name="manage_device_added_at">Added: %s</string>
|
||||
<string name="manage_device_is_this_device">This is the device which is in front of you</string>
|
||||
<string name="manage_device_signing_key">Signing Key: %s</string>
|
||||
|
||||
<string name="manage_device_current_user_title">User of the device</string>
|
||||
<string name="manage_device_current_user_none">No selection - no limits</string>
|
||||
|
@ -1186,6 +1190,9 @@
|
|||
<string name="notification_channel_reset_title">Reset Notification</string>
|
||||
<string name="notification_channel_reset_text">Shows a notification if TimeLimit was reset at this device</string>
|
||||
|
||||
<string name="notification_channel_new_device_title">New Device</string>
|
||||
<string name="notification_channel_new_device_description">Shows a notification if the connected mode is used and a new device was linked</string>
|
||||
|
||||
<string name="notification_filter_not_blocked_title">TimeLimit has blocked a notification</string>
|
||||
<string name="notification_filter_blocking_failed_title">TimeLimit could not block a notification</string>
|
||||
|
||||
|
@ -1199,6 +1206,8 @@
|
|||
<string name="notification_background_sync_title">TimeLimit is running</string>
|
||||
<string name="notification_background_sync_text">a background synchronisation is in progress</string>
|
||||
|
||||
<string name="notification_new_device_title">Device added</string>
|
||||
|
||||
<string name="obsolete_message">You are using TimeLimit at a obsolete Android version.
|
||||
Although this can work, it is not recommend.
|
||||
</string>
|
||||
|
|
|
@ -26,6 +26,7 @@ buildscript {
|
|||
classpath 'com.android.tools.build:gradle:7.2.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.2"
|
||||
classpath 'com.squareup.wire:wire-gradle-plugin:4.4.0'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue