Persist notification data. Note that it will break the key storage for the session database.
This commit is contained in:
committed by
Benoit Marty
parent
2696348d46
commit
c0ef4804a1
@@ -27,5 +27,6 @@ dependencies {
|
||||
implementation(libs.timber)
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.activity.activity)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
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.sqlite)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
|
||||
implementation(projects.libraries.androidutils)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package io.element.encrypteddb.passphrase
|
||||
|
||||
import android.content.Context
|
||||
import androidx.security.crypto.EncryptedFile
|
||||
import io.element.android.libraries.androidutils.file.EncryptedFileFactory
|
||||
import java.io.File
|
||||
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].
|
||||
* @param context Android [Context], used by [EncryptedFile] for cryptographic operations.
|
||||
* @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.
|
||||
*/
|
||||
class RandomSecretPassphraseProvider(
|
||||
private val context: Context,
|
||||
private val file: File,
|
||||
private val alias: String,
|
||||
private val secretSize: Int = 256,
|
||||
) : PassphraseProvider {
|
||||
|
||||
override fun getPassphrase(): ByteArray {
|
||||
val encryptedFile = EncryptedFile.Builder(
|
||||
file,
|
||||
context,
|
||||
alias,
|
||||
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
|
||||
).build()
|
||||
val encryptedFile = EncryptedFileFactory(context).create(file)
|
||||
return if (!file.exists()) {
|
||||
val secret = generateSecret()
|
||||
encryptedFile.openFileOutput().use { it.write(secret) }
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies {
|
||||
implementation(libs.androidx.corektx)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.security.crypto)
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
|
||||
@@ -17,60 +17,75 @@
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
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.push.impl.log.notificationLoggerTag
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
// TODO Multi-account
|
||||
private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
|
||||
private const val KEY_ALIAS_SECRET_STORAGE = "notificationMgr"
|
||||
private const val ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY = "im.vector.notifications.cache"
|
||||
private const val FILE_NAME = "notifications.bin"
|
||||
|
||||
private val loggerTag = LoggerTag("NotificationEventPersistence", notificationLoggerTag)
|
||||
|
||||
class NotificationEventPersistence @Inject constructor(
|
||||
@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 {
|
||||
try {
|
||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||
if (file.exists()) {
|
||||
file.inputStream().use {
|
||||
val events: ArrayList<NotifiableEvent>? = null // TODO EAx matrix.secureStorageService().loadSecureSecret(it, KEY_ALIAS_SECRET_STORAGE)
|
||||
if (events != null) {
|
||||
return factory(events)
|
||||
val rawEvents: ArrayList<NotifiableEvent>? = file
|
||||
.takeIf { it.exists() }
|
||||
?.let {
|
||||
try {
|
||||
encryptedFile.openFileInput().use { fis ->
|
||||
ObjectInputStream(fis).use { ois ->
|
||||
@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) {
|
||||
Timber.e(e, "## Failed to load cached notification info")
|
||||
}
|
||||
return factory(emptyList())
|
||||
return factory(rawEvents.orEmpty())
|
||||
}
|
||||
|
||||
fun persistEvents(queuedEvents: NotificationEventQueue) {
|
||||
if (queuedEvents.isEmpty()) {
|
||||
deleteCachedRoomNotifications(context)
|
||||
return
|
||||
}
|
||||
Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)")
|
||||
// Always delete file before writing, or encryptedFile.openFileOutput() will throw
|
||||
file.delete()
|
||||
if (queuedEvents.isEmpty()) return
|
||||
try {
|
||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||
if (!file.exists()) file.createNewFile()
|
||||
FileOutputStream(file).use {
|
||||
// TODO EAx
|
||||
// matrix.secureStorageService().securelyStoreObject(queuedEvents.rawEvents(), KEY_ALIAS_SECRET_STORAGE, it)
|
||||
encryptedFile.openFileOutput().use { fos ->
|
||||
ObjectOutputStream(fos).use { oos ->
|
||||
oos.writeObject(queuedEvents.rawEvents())
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
private fun deleteLegacyFileIfAny() {
|
||||
tryOrNull {
|
||||
File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME_LEGACY).delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ anvil {
|
||||
|
||||
dependencies {
|
||||
implementation(libs.dagger)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.encryptedDb)
|
||||
api(projects.libraries.sessionStorage.api)
|
||||
|
||||
@@ -35,7 +35,7 @@ object SessionStorageModule {
|
||||
fun provideMatrixDatabase(@ApplicationContext context: Context): SessionDatabase {
|
||||
val name = "session_database"
|
||||
val secretFile = context.getDatabasePath("$name.key")
|
||||
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile, name)
|
||||
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile)
|
||||
val driver = SqlCipherDriverFactory(passphraseProvider)
|
||||
.create(SessionDatabase.Schema, "$name.db", context)
|
||||
return SessionDatabase(driver)
|
||||
|
||||
Reference in New Issue
Block a user