Persist notification data. Note that it will break the key storage for the session database.
This commit is contained in:
parent
2696348d46
commit
c0ef4804a1
8 changed files with 90 additions and 39 deletions
|
|
@ -27,5 +27,6 @@ dependencies {
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.androidx.activity.activity)
|
implementation(libs.androidx.activity.activity)
|
||||||
|
implementation(libs.androidx.security.crypto)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.androidutils.file
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.security.crypto.EncryptedFile
|
||||||
|
import androidx.security.crypto.MasterKeys
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class EncryptedFileFactory(
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
fun create(file: File): EncryptedFile {
|
||||||
|
// We need to use the same key for all the encrypted files.
|
||||||
|
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||||
|
return EncryptedFile.Builder(
|
||||||
|
file,
|
||||||
|
context,
|
||||||
|
masterKeyAlias,
|
||||||
|
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,4 +34,6 @@ dependencies {
|
||||||
implementation(libs.sqlcipher)
|
implementation(libs.sqlcipher)
|
||||||
implementation(libs.sqlite)
|
implementation(libs.sqlite)
|
||||||
implementation(libs.androidx.security.crypto)
|
implementation(libs.androidx.security.crypto)
|
||||||
|
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.encrypteddb.passphrase
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.security.crypto.EncryptedFile
|
import androidx.security.crypto.EncryptedFile
|
||||||
|
import io.element.android.libraries.androidutils.file.EncryptedFileFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
|
@ -25,23 +26,16 @@ import java.security.SecureRandom
|
||||||
* Provides a secure passphrase for SQLCipher by generating a random secret and storing it into an [EncryptedFile].
|
* Provides a secure passphrase for SQLCipher by generating a random secret and storing it into an [EncryptedFile].
|
||||||
* @param context Android [Context], used by [EncryptedFile] for cryptographic operations.
|
* @param context Android [Context], used by [EncryptedFile] for cryptographic operations.
|
||||||
* @param file Destination file where the key will be stored.
|
* @param file Destination file where the key will be stored.
|
||||||
* @param alias Alias of the key used to encrypt & decrypt the [EncryptedFile]'s contents.
|
|
||||||
* @param secretSize Length of the generated secret.
|
* @param secretSize Length of the generated secret.
|
||||||
*/
|
*/
|
||||||
class RandomSecretPassphraseProvider(
|
class RandomSecretPassphraseProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val file: File,
|
private val file: File,
|
||||||
private val alias: String,
|
|
||||||
private val secretSize: Int = 256,
|
private val secretSize: Int = 256,
|
||||||
) : PassphraseProvider {
|
) : PassphraseProvider {
|
||||||
|
|
||||||
override fun getPassphrase(): ByteArray {
|
override fun getPassphrase(): ByteArray {
|
||||||
val encryptedFile = EncryptedFile.Builder(
|
val encryptedFile = EncryptedFileFactory(context).create(file)
|
||||||
file,
|
|
||||||
context,
|
|
||||||
alias,
|
|
||||||
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
|
|
||||||
).build()
|
|
||||||
return if (!file.exists()) {
|
return if (!file.exists()) {
|
||||||
val secret = generateSecret()
|
val secret = generateSecret()
|
||||||
encryptedFile.openFileOutput().use { it.write(secret) }
|
encryptedFile.openFileOutput().use { it.write(secret) }
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(libs.androidx.lifecycle.process)
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
implementation(libs.androidx.security.crypto)
|
||||||
implementation(libs.network.retrofit)
|
implementation(libs.network.retrofit)
|
||||||
implementation(libs.serialization.json)
|
implementation(libs.serialization.json)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,60 +17,75 @@
|
||||||
package io.element.android.libraries.push.impl.notifications
|
package io.element.android.libraries.push.impl.notifications
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import io.element.android.libraries.androidutils.file.EncryptedFileFactory
|
||||||
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
|
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
|
import io.element.android.libraries.push.impl.log.notificationLoggerTag
|
||||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.ObjectInputStream
|
||||||
|
import java.io.ObjectOutputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
// TODO Multi-account
|
private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache"
|
||||||
private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
|
private const val FILE_NAME = "notifications.bin"
|
||||||
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
|
|
||||||
|
private val loggerTag = LoggerTag("NotificationEventPersistence", notificationLoggerTag)
|
||||||
|
|
||||||
class NotificationEventPersistence @Inject constructor(
|
class NotificationEventPersistence @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
// private val matrix: Matrix,
|
|
||||||
) {
|
) {
|
||||||
|
private val file by lazy {
|
||||||
|
deleteLegacyFileIfAny()
|
||||||
|
context.getDatabasePath(FILE_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val encryptedFile by lazy {
|
||||||
|
EncryptedFileFactory(context).create(file)
|
||||||
|
}
|
||||||
|
|
||||||
fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue {
|
fun loadEvents(factory: (List<NotifiableEvent>) -> NotificationEventQueue): NotificationEventQueue {
|
||||||
try {
|
val rawEvents: ArrayList<NotifiableEvent>? = file
|
||||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
.takeIf { it.exists() }
|
||||||
if (file.exists()) {
|
?.let {
|
||||||
file.inputStream().use {
|
try {
|
||||||
val events: ArrayList<NotifiableEvent>? = null // TODO EAx matrix.secureStorageService().loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
|
encryptedFile.openFileInput().use { fis ->
|
||||||
if (events != null) {
|
ObjectInputStream(fis).use { ois ->
|
||||||
return factory(events)
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
ois.readObject() as? ArrayList<NotifiableEvent>
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
Timber.tag(loggerTag.value).d("Deserializing ${it?.size} NotifiableEvent(s)")
|
||||||
}
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Timber.tag(loggerTag.value).e(e, "## Failed to load cached notification info")
|
||||||
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
return factory(rawEvents.orEmpty())
|
||||||
Timber.e(e, "## Failed to load cached notification info")
|
|
||||||
}
|
|
||||||
return factory(emptyList())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun persistEvents(queuedEvents: NotificationEventQueue) {
|
fun persistEvents(queuedEvents: NotificationEventQueue) {
|
||||||
if (queuedEvents.isEmpty()) {
|
Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)")
|
||||||
deleteCachedRoomNotifications(context)
|
// Always delete file before writing, or encryptedFile.openFileOutput() will throw
|
||||||
return
|
file.delete()
|
||||||
}
|
if (queuedEvents.isEmpty()) return
|
||||||
try {
|
try {
|
||||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
encryptedFile.openFileOutput().use { fos ->
|
||||||
if (!file.exists()) file.createNewFile()
|
ObjectOutputStream(fos).use { oos ->
|
||||||
FileOutputStream(file).use {
|
oos.writeObject(queuedEvents.rawEvents())
|
||||||
// TODO EAx
|
}
|
||||||
// matrix.secureStorageService().securelyStoreObject(queuedEvents.rawEvents(), KEY_ALIAS_SECRET_STORAGE, it)
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.e(e, "## Failed to save cached notification info")
|
Timber.tag(loggerTag.value).e(e, "## Failed to save cached notification info")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteCachedRoomNotifications(context: Context) {
|
private fun deleteLegacyFileIfAny() {
|
||||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
tryOrNull {
|
||||||
if (file.exists()) {
|
File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY).delete()
|
||||||
file.delete()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ anvil {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.dagger)
|
implementation(libs.dagger)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.encryptedDb)
|
implementation(projects.libraries.encryptedDb)
|
||||||
api(projects.libraries.sessionStorage.api)
|
api(projects.libraries.sessionStorage.api)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ object SessionStorageModule {
|
||||||
fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase {
|
fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase {
|
||||||
val name = "session_database"
|
val name = "session_database"
|
||||||
val secretFile = context.getDatabasePath("$name.key")
|
val secretFile = context.getDatabasePath("$name.key")
|
||||||
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile, name)
|
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile)
|
||||||
val driver = SqlCipherDriverFactory(passphraseProvider)
|
val driver = SqlCipherDriverFactory(passphraseProvider)
|
||||||
.create(SessionDatabase.Schema, "$name.db", context)
|
.create(SessionDatabase.Schema, "$name.db", context)
|
||||||
return SessionDatabase(driver)
|
return SessionDatabase(driver)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue