Split key concern out of storage plugin
This creates a KeyManager interface in the new core module which the storage module can use to get the key from.
This commit is contained in:
parent
9e56384cb2
commit
27eb95f768
21 changed files with 64 additions and 72 deletions
|
@ -24,7 +24,7 @@ internal const val KEY_ALIAS_MAIN = "com.stevesoltys.seedvault.main"
|
||||||
private const val KEY_ALGORITHM_BACKUP = "AES"
|
private const val KEY_ALGORITHM_BACKUP = "AES"
|
||||||
private const val KEY_ALGORITHM_MAIN = "HmacSHA256"
|
private const val KEY_ALGORITHM_MAIN = "HmacSHA256"
|
||||||
|
|
||||||
interface KeyManager {
|
interface KeyManager : org.calyxos.seedvault.core.crypto.KeyManager {
|
||||||
/**
|
/**
|
||||||
* Store a new backup key derived from the given [seed].
|
* Store a new backup key derived from the given [seed].
|
||||||
*
|
*
|
||||||
|
@ -57,14 +57,6 @@ interface KeyManager {
|
||||||
* because the key can not leave the [KeyStore]'s hardware security module.
|
* because the key can not leave the [KeyStore]'s hardware security module.
|
||||||
*/
|
*/
|
||||||
fun getBackupKey(): SecretKey
|
fun getBackupKey(): SecretKey
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the main key, so it can be used for deriving sub-keys.
|
|
||||||
*
|
|
||||||
* Note that any attempt to export the key will return null or an empty [ByteArray],
|
|
||||||
* because the key can not leave the [KeyStore]'s hardware security module.
|
|
||||||
*/
|
|
||||||
fun getMainKey(): SecretKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class KeyManagerImpl(
|
internal class KeyManagerImpl(
|
||||||
|
|
|
@ -8,12 +8,10 @@ package com.stevesoltys.seedvault.plugins.webdav
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
|
||||||
class WebDavFactory(
|
class WebDavFactory(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val keyManager: KeyManager,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun createAppStoragePlugin(config: WebDavConfig): StoragePlugin<WebDavConfig> {
|
fun createAppStoragePlugin(config: WebDavConfig): StoragePlugin<WebDavConfig> {
|
||||||
|
@ -27,7 +25,6 @@ class WebDavFactory(
|
||||||
val androidId =
|
val androidId =
|
||||||
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
return com.stevesoltys.seedvault.storage.WebDavStoragePlugin(
|
return com.stevesoltys.seedvault.storage.WebDavStoragePlugin(
|
||||||
keyManager = keyManager,
|
|
||||||
androidId = androidId,
|
androidId = androidId,
|
||||||
webDavConfig = config,
|
webDavConfig = config,
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,6 +9,6 @@ import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val storagePluginModuleWebDav = module {
|
val storagePluginModuleWebDav = module {
|
||||||
single { WebDavFactory(androidContext(), get()) }
|
single { WebDavFactory(androidContext()) }
|
||||||
single { WebDavHandler(androidContext(), get(), get(), get()) }
|
single { WebDavHandler(androidContext(), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.getStorageContext
|
import com.stevesoltys.seedvault.getStorageContext
|
||||||
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
|
||||||
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
|
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
|
||||||
import javax.crypto.SecretKey
|
|
||||||
|
|
||||||
internal class SeedvaultSafStoragePlugin(
|
internal class SeedvaultSafStoragePlugin(
|
||||||
private val appContext: Context,
|
private val appContext: Context,
|
||||||
|
@ -24,6 +23,4 @@ internal class SeedvaultSafStoragePlugin(
|
||||||
override val context: Context get() = appContext.getStorageContext { storage.safStorage.isUsb }
|
override val context: Context get() = appContext.getStorageContext { storage.safStorage.isUsb }
|
||||||
override val root: DocumentFile get() = storage.rootBackupDir ?: error("No storage set")
|
override val root: DocumentFile get() = storage.rootBackupDir ?: error("No storage set")
|
||||||
|
|
||||||
override fun getMasterKey(): SecretKey = keyManager.getMainKey()
|
|
||||||
override fun hasMasterKey(): Boolean = keyManager.hasMainKey()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
|
|
||||||
package com.stevesoltys.seedvault.storage
|
package com.stevesoltys.seedvault.storage
|
||||||
|
|
||||||
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
import com.stevesoltys.seedvault.plugins.StoragePluginManager
|
||||||
import org.calyxos.backup.storage.api.StorageBackup
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val storageModule = module {
|
val storageModule = module {
|
||||||
single { StorageBackup(get(), { get<StoragePluginManager>().filesPlugin }) }
|
single { StorageBackup(get(), { get<StoragePluginManager>().filesPlugin }, get<KeyManager>()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import at.bitfire.dav4jvm.Response.HrefRelation.SELF
|
||||||
import at.bitfire.dav4jvm.exception.NotFoundException
|
import at.bitfire.dav4jvm.exception.NotFoundException
|
||||||
import at.bitfire.dav4jvm.property.webdav.DisplayName
|
import at.bitfire.dav4jvm.property.webdav.DisplayName
|
||||||
import at.bitfire.dav4jvm.property.webdav.ResourceType
|
import at.bitfire.dav4jvm.property.webdav.ResourceType
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
|
||||||
import com.stevesoltys.seedvault.plugins.chunkFolderRegex
|
import com.stevesoltys.seedvault.plugins.chunkFolderRegex
|
||||||
import com.stevesoltys.seedvault.plugins.webdav.DIRECTORY_ROOT
|
import com.stevesoltys.seedvault.plugins.webdav.DIRECTORY_ROOT
|
||||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavConfig
|
import com.stevesoltys.seedvault.plugins.webdav.WebDavConfig
|
||||||
|
@ -26,12 +25,10 @@ import org.koin.core.time.measureDuration
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import javax.crypto.SecretKey
|
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
internal class WebDavStoragePlugin(
|
internal class WebDavStoragePlugin(
|
||||||
private val keyManager: KeyManager,
|
|
||||||
/**
|
/**
|
||||||
* The result of Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
* The result of Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
*/
|
*/
|
||||||
|
@ -121,9 +118,6 @@ internal class WebDavStoragePlugin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMasterKey(): SecretKey = keyManager.getMainKey()
|
|
||||||
override fun hasMasterKey(): Boolean = keyManager.hasMainKey()
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override suspend fun getChunkOutputStream(chunkId: String): OutputStream {
|
override suspend fun getChunkOutputStream(chunkId: String): OutputStream {
|
||||||
val chunkFolderName = chunkId.substring(0, 2)
|
val chunkFolderName = chunkId.substring(0, 2)
|
||||||
|
|
|
@ -5,12 +5,10 @@
|
||||||
|
|
||||||
package com.stevesoltys.seedvault.storage
|
package com.stevesoltys.seedvault.storage
|
||||||
|
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
|
||||||
import com.stevesoltys.seedvault.getRandomByteArray
|
import com.stevesoltys.seedvault.getRandomByteArray
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.plugins.webdav.WebDavTestConfig
|
import com.stevesoltys.seedvault.plugins.webdav.WebDavTestConfig
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupTest
|
import com.stevesoltys.seedvault.transport.backup.BackupTest
|
||||||
import io.mockk.mockk
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.calyxos.backup.storage.api.StoredSnapshot
|
import org.calyxos.backup.storage.api.StoredSnapshot
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
|
@ -21,8 +19,7 @@ import java.io.IOException
|
||||||
|
|
||||||
internal class WebDavStoragePluginTest : BackupTest() {
|
internal class WebDavStoragePluginTest : BackupTest() {
|
||||||
|
|
||||||
private val keyManager: KeyManager = mockk()
|
private val plugin = WebDavStoragePlugin("foo", WebDavTestConfig.getConfig())
|
||||||
private val plugin = WebDavStoragePlugin(keyManager, "foo", WebDavTestConfig.getConfig())
|
|
||||||
|
|
||||||
private val snapshot = StoredSnapshot("foo.sv", System.currentTimeMillis())
|
private val snapshot = StoredSnapshot("foo.sv", System.currentTimeMillis())
|
||||||
|
|
||||||
|
@ -85,7 +82,7 @@ internal class WebDavStoragePluginTest : BackupTest() {
|
||||||
)
|
)
|
||||||
|
|
||||||
// other device writes another snapshot
|
// other device writes another snapshot
|
||||||
val otherPlugin = WebDavStoragePlugin(keyManager, "bar", WebDavTestConfig.getConfig())
|
val otherPlugin = WebDavStoragePlugin("bar", WebDavTestConfig.getConfig())
|
||||||
val otherSnapshot = StoredSnapshot("bar.sv", System.currentTimeMillis())
|
val otherSnapshot = StoredSnapshot("bar.sv", System.currentTimeMillis())
|
||||||
val otherSnapshotBytes = getRandomByteArray()
|
val otherSnapshotBytes = getRandomByteArray()
|
||||||
assertEquals(emptyList<String>(), otherPlugin.getAvailableChunkIds())
|
assertEquals(emptyList<String>(), otherPlugin.getAvailableChunkIds())
|
||||||
|
@ -110,7 +107,6 @@ internal class WebDavStoragePluginTest : BackupTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `test missing root dir`() = runBlocking {
|
fun `test missing root dir`() = runBlocking {
|
||||||
val plugin = WebDavStoragePlugin(
|
val plugin = WebDavStoragePlugin(
|
||||||
keyManager = keyManager,
|
|
||||||
androidId = "foo",
|
androidId = "foo",
|
||||||
webDavConfig = WebDavTestConfig.getConfig(),
|
webDavConfig = WebDavTestConfig.getConfig(),
|
||||||
root = getRandomString(),
|
root = getRandomString(),
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.calyxos.seedvault.core.crypto
|
||||||
|
|
||||||
|
import java.security.KeyStore
|
||||||
|
import javax.crypto.SecretKey
|
||||||
|
|
||||||
|
public interface KeyManager {
|
||||||
|
/**
|
||||||
|
* Returns the main key, so it can be used for deriving sub-keys.
|
||||||
|
*
|
||||||
|
* Note that any attempt to export the key will return null or an empty [ByteArray],
|
||||||
|
* because the key can not leave the [KeyStore]'s hardware security module.
|
||||||
|
*/
|
||||||
|
public fun getMainKey(): SecretKey
|
||||||
|
}
|
|
@ -69,6 +69,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(project(":core"))
|
||||||
implementation(project(":storage:lib"))
|
implementation(project(":storage:lib"))
|
||||||
|
|
||||||
implementation(libs.bundles.kotlin)
|
implementation(libs.bundles.kotlin)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.app.Application
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import android.os.StrictMode.VmPolicy
|
import android.os.StrictMode.VmPolicy
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import de.grobox.storagebackuptester.crypto.KeyManager
|
||||||
import de.grobox.storagebackuptester.plugin.TestSafStoragePlugin
|
import de.grobox.storagebackuptester.plugin.TestSafStoragePlugin
|
||||||
import de.grobox.storagebackuptester.settings.SettingsManager
|
import de.grobox.storagebackuptester.settings.SettingsManager
|
||||||
import org.calyxos.backup.storage.api.StorageBackup
|
import org.calyxos.backup.storage.api.StorageBackup
|
||||||
|
@ -19,7 +20,7 @@ class App : Application() {
|
||||||
val settingsManager: SettingsManager by lazy { SettingsManager(applicationContext) }
|
val settingsManager: SettingsManager by lazy { SettingsManager(applicationContext) }
|
||||||
val storageBackup: StorageBackup by lazy {
|
val storageBackup: StorageBackup by lazy {
|
||||||
val plugin = TestSafStoragePlugin(this) { settingsManager.getBackupLocation() }
|
val plugin = TestSafStoragePlugin(this) { settingsManager.getBackupLocation() }
|
||||||
StorageBackup(this, { plugin })
|
StorageBackup(this, { plugin }, KeyManager)
|
||||||
}
|
}
|
||||||
val fileSelectionManager: FileSelectionManager get() = FileSelectionManager()
|
val fileSelectionManager: FileSelectionManager get() = FileSelectionManager()
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
KeyManager.storeMasterKey()
|
KeyManager.storeMasterKey()
|
||||||
|
|
||||||
if (!KeyManager.hasMasterKey()) {
|
if (!KeyManager.hasMainKey()) {
|
||||||
Log.e("TEST", "storing new key")
|
Log.e("TEST", "storing new key")
|
||||||
KeyManager.storeMasterKey()
|
KeyManager.storeMasterKey()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -14,7 +14,7 @@ import java.security.KeyStore
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
object KeyManager {
|
object KeyManager: org.calyxos.seedvault.core.crypto.KeyManager {
|
||||||
|
|
||||||
private const val KEY_SIZE = 256
|
private const val KEY_SIZE = 256
|
||||||
internal const val KEY_SIZE_BYTES = KEY_SIZE / 8
|
internal const val KEY_SIZE_BYTES = KEY_SIZE / 8
|
||||||
|
@ -42,9 +42,9 @@ object KeyManager {
|
||||||
keyStore.setEntry(KEY_ALIAS_MASTER, ksEntry, getKeyProtection())
|
keyStore.setEntry(KEY_ALIAS_MASTER, ksEntry, getKeyProtection())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasMasterKey(): Boolean = keyStore.containsAlias(KEY_ALIAS_MASTER)
|
fun hasMainKey(): Boolean = keyStore.containsAlias(KEY_ALIAS_MASTER)
|
||||||
|
|
||||||
fun getMasterKey(): SecretKey {
|
override fun getMainKey(): SecretKey {
|
||||||
val ksEntry = keyStore.getEntry(KEY_ALIAS_MASTER, null) as KeyStore.SecretKeyEntry
|
val ksEntry = keyStore.getEntry(KEY_ALIAS_MASTER, null) as KeyStore.SecretKeyEntry
|
||||||
return ksEntry.secretKey
|
return ksEntry.secretKey
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,10 @@ package de.grobox.storagebackuptester.plugin
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import de.grobox.storagebackuptester.crypto.KeyManager
|
|
||||||
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
|
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import javax.crypto.SecretKey
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
class TestSafStoragePlugin(
|
class TestSafStoragePlugin(
|
||||||
appContext: Context,
|
appContext: Context,
|
||||||
private val getLocationUri: () -> Uri?,
|
private val getLocationUri: () -> Uri?,
|
||||||
|
@ -33,14 +30,6 @@ class TestSafStoragePlugin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMasterKey(): SecretKey {
|
|
||||||
return KeyManager.getMasterKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hasMasterKey(): Boolean {
|
|
||||||
return KeyManager.hasMasterKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override suspend fun getChunkOutputStream(chunkId: String): OutputStream {
|
override suspend fun getChunkOutputStream(chunkId: String): OutputStream {
|
||||||
if (getLocationUri() == null) return nullStream
|
if (getLocationUri() == null) return nullStream
|
||||||
|
|
|
@ -81,6 +81,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(project(":core"))
|
||||||
implementation(libs.bundles.kotlin)
|
implementation(libs.bundles.kotlin)
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.fragment)
|
implementation(libs.androidx.fragment)
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.calyxos.backup.storage.scanner.DocumentScanner
|
||||||
import org.calyxos.backup.storage.scanner.FileScanner
|
import org.calyxos.backup.storage.scanner.FileScanner
|
||||||
import org.calyxos.backup.storage.scanner.MediaScanner
|
import org.calyxos.backup.storage.scanner.MediaScanner
|
||||||
import org.calyxos.backup.storage.toStoredUri
|
import org.calyxos.backup.storage.toStoredUri
|
||||||
|
import org.calyxos.seedvault.core.crypto.KeyManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
@ -39,6 +40,7 @@ private const val TAG = "StorageBackup"
|
||||||
public class StorageBackup(
|
public class StorageBackup(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val pluginGetter: () -> StoragePlugin,
|
private val pluginGetter: () -> StoragePlugin,
|
||||||
|
private val keyManager: KeyManager,
|
||||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -54,13 +56,16 @@ public class StorageBackup(
|
||||||
private val backup by lazy {
|
private val backup by lazy {
|
||||||
val documentScanner = DocumentScanner(context)
|
val documentScanner = DocumentScanner(context)
|
||||||
val fileScanner = FileScanner(uriStore, mediaScanner, documentScanner)
|
val fileScanner = FileScanner(uriStore, mediaScanner, documentScanner)
|
||||||
Backup(context, db, fileScanner, pluginGetter, chunksCacheRepopulater)
|
Backup(context, db, fileScanner, pluginGetter, keyManager, chunksCacheRepopulater)
|
||||||
}
|
}
|
||||||
private val restore by lazy {
|
private val restore by lazy {
|
||||||
Restore(context, pluginGetter, snapshotRetriever, FileRestore(context, mediaScanner))
|
val fileRestore = FileRestore(context, mediaScanner)
|
||||||
|
Restore(context, pluginGetter, keyManager, snapshotRetriever, fileRestore)
|
||||||
}
|
}
|
||||||
private val retention = RetentionManager(context)
|
private val retention = RetentionManager(context)
|
||||||
private val pruner by lazy { Pruner(db, retention, pluginGetter, snapshotRetriever) }
|
private val pruner by lazy {
|
||||||
|
Pruner(db, retention, pluginGetter, keyManager, snapshotRetriever)
|
||||||
|
}
|
||||||
|
|
||||||
private val backupRunning = AtomicBoolean(false)
|
private val backupRunning = AtomicBoolean(false)
|
||||||
private val restoreRunning = AtomicBoolean(false)
|
private val restoreRunning = AtomicBoolean(false)
|
||||||
|
|
|
@ -8,8 +8,6 @@ package org.calyxos.backup.storage.api
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.KeyStore
|
|
||||||
import javax.crypto.SecretKey
|
|
||||||
|
|
||||||
public interface StoragePlugin {
|
public interface StoragePlugin {
|
||||||
|
|
||||||
|
@ -28,16 +26,6 @@ public interface StoragePlugin {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
public suspend fun getAvailableChunkIds(): List<String>
|
public suspend fun getAvailableChunkIds(): List<String>
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a [SecretKey] for HmacSHA256, ideally stored in the [KeyStore].
|
|
||||||
*/
|
|
||||||
public fun getMasterKey(): SecretKey
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the key for [getMasterKey] exists, false otherwise.
|
|
||||||
*/
|
|
||||||
public fun hasMasterKey(): Boolean
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
public suspend fun getChunkOutputStream(chunkId: String): OutputStream
|
public suspend fun getChunkOutputStream(chunkId: String): OutputStream
|
||||||
|
|
||||||
|
@ -48,8 +36,7 @@ public interface StoragePlugin {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns *all* [StoredSnapshot]s that are available on storage
|
* Returns *all* [StoredSnapshot]s that are available on storage
|
||||||
* independent of user ID and whether they can be decrypted
|
* independent of user ID and whether they can be decrypted with the main key.
|
||||||
* with the key returned by [getMasterKey].
|
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
public suspend fun getBackupSnapshotsForRestore(): List<StoredSnapshot>
|
public suspend fun getBackupSnapshotsForRestore(): List<StoredSnapshot>
|
||||||
|
|
|
@ -19,6 +19,7 @@ import org.calyxos.backup.storage.db.Db
|
||||||
import org.calyxos.backup.storage.measure
|
import org.calyxos.backup.storage.measure
|
||||||
import org.calyxos.backup.storage.scanner.FileScanner
|
import org.calyxos.backup.storage.scanner.FileScanner
|
||||||
import org.calyxos.backup.storage.scanner.FileScannerResult
|
import org.calyxos.backup.storage.scanner.FileScannerResult
|
||||||
|
import org.calyxos.seedvault.core.crypto.KeyManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
@ -42,6 +43,7 @@ internal class Backup(
|
||||||
private val db: Db,
|
private val db: Db,
|
||||||
private val fileScanner: FileScanner,
|
private val fileScanner: FileScanner,
|
||||||
private val storagePluginGetter: () -> StoragePlugin,
|
private val storagePluginGetter: () -> StoragePlugin,
|
||||||
|
keyManager: KeyManager,
|
||||||
private val cacheRepopulater: ChunksCacheRepopulater,
|
private val cacheRepopulater: ChunksCacheRepopulater,
|
||||||
chunkSizeMax: Int = CHUNK_SIZE_MAX,
|
chunkSizeMax: Int = CHUNK_SIZE_MAX,
|
||||||
private val streamCrypto: StreamCrypto = StreamCrypto,
|
private val streamCrypto: StreamCrypto = StreamCrypto,
|
||||||
|
@ -60,12 +62,12 @@ internal class Backup(
|
||||||
private val chunksCache = db.getChunksCache()
|
private val chunksCache = db.getChunksCache()
|
||||||
|
|
||||||
private val mac = try {
|
private val mac = try {
|
||||||
ChunkCrypto.getMac(ChunkCrypto.deriveChunkIdKey(storagePlugin.getMasterKey()))
|
ChunkCrypto.getMac(ChunkCrypto.deriveChunkIdKey(keyManager.getMainKey()))
|
||||||
} catch (e: GeneralSecurityException) {
|
} catch (e: GeneralSecurityException) {
|
||||||
throw AssertionError(e)
|
throw AssertionError(e)
|
||||||
}
|
}
|
||||||
private val streamKey = try {
|
private val streamKey = try {
|
||||||
streamCrypto.deriveStreamKey(storagePlugin.getMasterKey())
|
streamCrypto.deriveStreamKey(keyManager.getMainKey())
|
||||||
} catch (e: GeneralSecurityException) {
|
} catch (e: GeneralSecurityException) {
|
||||||
throw AssertionError(e)
|
throw AssertionError(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.calyxos.backup.storage.crypto.StreamCrypto
|
||||||
import org.calyxos.backup.storage.db.Db
|
import org.calyxos.backup.storage.db.Db
|
||||||
import org.calyxos.backup.storage.measure
|
import org.calyxos.backup.storage.measure
|
||||||
import org.calyxos.backup.storage.plugin.SnapshotRetriever
|
import org.calyxos.backup.storage.plugin.SnapshotRetriever
|
||||||
|
import org.calyxos.seedvault.core.crypto.KeyManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
import kotlin.time.ExperimentalTime
|
import kotlin.time.ExperimentalTime
|
||||||
|
@ -23,6 +24,7 @@ internal class Pruner(
|
||||||
private val db: Db,
|
private val db: Db,
|
||||||
private val retentionManager: RetentionManager,
|
private val retentionManager: RetentionManager,
|
||||||
private val storagePluginGetter: () -> StoragePlugin,
|
private val storagePluginGetter: () -> StoragePlugin,
|
||||||
|
keyManager: KeyManager,
|
||||||
private val snapshotRetriever: SnapshotRetriever,
|
private val snapshotRetriever: SnapshotRetriever,
|
||||||
streamCrypto: StreamCrypto = StreamCrypto,
|
streamCrypto: StreamCrypto = StreamCrypto,
|
||||||
) {
|
) {
|
||||||
|
@ -30,7 +32,7 @@ internal class Pruner(
|
||||||
private val storagePlugin get() = storagePluginGetter()
|
private val storagePlugin get() = storagePluginGetter()
|
||||||
private val chunksCache = db.getChunksCache()
|
private val chunksCache = db.getChunksCache()
|
||||||
private val streamKey = try {
|
private val streamKey = try {
|
||||||
streamCrypto.deriveStreamKey(storagePlugin.getMasterKey())
|
streamCrypto.deriveStreamKey(keyManager.getMainKey())
|
||||||
} catch (e: GeneralSecurityException) {
|
} catch (e: GeneralSecurityException) {
|
||||||
throw AssertionError(e)
|
throw AssertionError(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import org.calyxos.backup.storage.backup.BackupSnapshot
|
||||||
import org.calyxos.backup.storage.crypto.StreamCrypto
|
import org.calyxos.backup.storage.crypto.StreamCrypto
|
||||||
import org.calyxos.backup.storage.measure
|
import org.calyxos.backup.storage.measure
|
||||||
import org.calyxos.backup.storage.plugin.SnapshotRetriever
|
import org.calyxos.backup.storage.plugin.SnapshotRetriever
|
||||||
|
import org.calyxos.seedvault.core.crypto.KeyManager
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.security.GeneralSecurityException
|
import java.security.GeneralSecurityException
|
||||||
|
@ -28,6 +29,7 @@ private const val TAG = "Restore"
|
||||||
internal class Restore(
|
internal class Restore(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val storagePluginGetter: () -> StoragePlugin,
|
private val storagePluginGetter: () -> StoragePlugin,
|
||||||
|
private val keyManager: KeyManager,
|
||||||
private val snapshotRetriever: SnapshotRetriever,
|
private val snapshotRetriever: SnapshotRetriever,
|
||||||
fileRestore: FileRestore,
|
fileRestore: FileRestore,
|
||||||
streamCrypto: StreamCrypto = StreamCrypto,
|
streamCrypto: StreamCrypto = StreamCrypto,
|
||||||
|
@ -39,7 +41,7 @@ internal class Restore(
|
||||||
// so we need to get it lazily here to prevent crashes. We can still crash later,
|
// so we need to get it lazily here to prevent crashes. We can still crash later,
|
||||||
// if the plugin is not providing a key as it should when performing calls into this class.
|
// if the plugin is not providing a key as it should when performing calls into this class.
|
||||||
try {
|
try {
|
||||||
streamCrypto.deriveStreamKey(storagePlugin.getMasterKey())
|
streamCrypto.deriveStreamKey(keyManager.getMainKey())
|
||||||
} catch (e: GeneralSecurityException) {
|
} catch (e: GeneralSecurityException) {
|
||||||
throw AssertionError(e)
|
throw AssertionError(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ import org.calyxos.backup.storage.restore.RestorableFile
|
||||||
import org.calyxos.backup.storage.restore.Restore
|
import org.calyxos.backup.storage.restore.Restore
|
||||||
import org.calyxos.backup.storage.scanner.FileScanner
|
import org.calyxos.backup.storage.scanner.FileScanner
|
||||||
import org.calyxos.backup.storage.scanner.FileScannerResult
|
import org.calyxos.backup.storage.scanner.FileScannerResult
|
||||||
|
import org.calyxos.seedvault.core.crypto.KeyManager
|
||||||
import org.junit.Assert.assertArrayEquals
|
import org.junit.Assert.assertArrayEquals
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
@ -71,6 +72,7 @@ internal class BackupRestoreTest {
|
||||||
|
|
||||||
private val fileScanner: FileScanner = mockk()
|
private val fileScanner: FileScanner = mockk()
|
||||||
private val pluginGetter: () -> StoragePlugin = mockk()
|
private val pluginGetter: () -> StoragePlugin = mockk()
|
||||||
|
private val keyManager: KeyManager = mockk()
|
||||||
private val plugin: StoragePlugin = mockk()
|
private val plugin: StoragePlugin = mockk()
|
||||||
private val fileRestore: FileRestore = mockk()
|
private val fileRestore: FileRestore = mockk()
|
||||||
private val snapshotRetriever = SnapshotRetriever(pluginGetter)
|
private val snapshotRetriever = SnapshotRetriever(pluginGetter)
|
||||||
|
@ -87,7 +89,7 @@ internal class BackupRestoreTest {
|
||||||
every { pluginGetter() } returns plugin
|
every { pluginGetter() } returns plugin
|
||||||
every { db.getFilesCache() } returns filesCache
|
every { db.getFilesCache() } returns filesCache
|
||||||
every { db.getChunksCache() } returns chunksCache
|
every { db.getChunksCache() } returns chunksCache
|
||||||
every { plugin.getMasterKey() } returns SecretKeySpec(
|
every { keyManager.getMainKey() } returns SecretKeySpec(
|
||||||
"This is a backup key for testing".toByteArray(),
|
"This is a backup key for testing".toByteArray(),
|
||||||
0, KEY_SIZE_BYTES, ALGORITHM_HMAC
|
0, KEY_SIZE_BYTES, ALGORITHM_HMAC
|
||||||
)
|
)
|
||||||
|
@ -95,11 +97,11 @@ internal class BackupRestoreTest {
|
||||||
every { context.contentResolver } returns contentResolver
|
every { context.contentResolver } returns contentResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
private val restore = Restore(context, pluginGetter, snapshotRetriever, fileRestore)
|
private val restore = Restore(context, pluginGetter, keyManager, snapshotRetriever, fileRestore)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testZipAndSingleRandom(): Unit = runBlocking {
|
fun testZipAndSingleRandom(): Unit = runBlocking {
|
||||||
val backup = Backup(context, db, fileScanner, pluginGetter, cacheRepopulater)
|
val backup = Backup(context, db, fileScanner, pluginGetter, keyManager, cacheRepopulater)
|
||||||
|
|
||||||
val smallFileMBytes = Random.nextBytes(Random.nextInt(SMALL_FILE_SIZE_MAX))
|
val smallFileMBytes = Random.nextBytes(Random.nextInt(SMALL_FILE_SIZE_MAX))
|
||||||
val smallFileM = getRandomMediaFile(smallFileMBytes.size)
|
val smallFileM = getRandomMediaFile(smallFileMBytes.size)
|
||||||
|
@ -236,7 +238,8 @@ internal class BackupRestoreTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testMultiChunks(): Unit = runBlocking {
|
fun testMultiChunks(): Unit = runBlocking {
|
||||||
val backup = Backup(context, db, fileScanner, pluginGetter, cacheRepopulater, 4)
|
val backup =
|
||||||
|
Backup(context, db, fileScanner, pluginGetter, keyManager, cacheRepopulater, 4)
|
||||||
|
|
||||||
val chunk1 = byteArrayOf(0x00, 0x01, 0x02, 0x03)
|
val chunk1 = byteArrayOf(0x00, 0x01, 0x02, 0x03)
|
||||||
val chunk2 = byteArrayOf(0x04, 0x05, 0x06, 0x07)
|
val chunk2 = byteArrayOf(0x04, 0x05, 0x06, 0x07)
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.calyxos.backup.storage.db.Db
|
||||||
import org.calyxos.backup.storage.getRandomString
|
import org.calyxos.backup.storage.getRandomString
|
||||||
import org.calyxos.backup.storage.mockLog
|
import org.calyxos.backup.storage.mockLog
|
||||||
import org.calyxos.backup.storage.plugin.SnapshotRetriever
|
import org.calyxos.backup.storage.plugin.SnapshotRetriever
|
||||||
|
import org.calyxos.seedvault.core.crypto.KeyManager
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -37,6 +38,7 @@ internal class PrunerTest {
|
||||||
private val db: Db = mockk()
|
private val db: Db = mockk()
|
||||||
private val chunksCache: ChunksCache = mockk()
|
private val chunksCache: ChunksCache = mockk()
|
||||||
private val pluginGetter: () -> StoragePlugin = mockk()
|
private val pluginGetter: () -> StoragePlugin = mockk()
|
||||||
|
private val keyManager: KeyManager = mockk()
|
||||||
private val plugin: StoragePlugin = mockk()
|
private val plugin: StoragePlugin = mockk()
|
||||||
private val snapshotRetriever: SnapshotRetriever = mockk()
|
private val snapshotRetriever: SnapshotRetriever = mockk()
|
||||||
private val retentionManager: RetentionManager = mockk()
|
private val retentionManager: RetentionManager = mockk()
|
||||||
|
@ -48,11 +50,12 @@ internal class PrunerTest {
|
||||||
mockLog(false)
|
mockLog(false)
|
||||||
every { pluginGetter() } returns plugin
|
every { pluginGetter() } returns plugin
|
||||||
every { db.getChunksCache() } returns chunksCache
|
every { db.getChunksCache() } returns chunksCache
|
||||||
every { plugin.getMasterKey() } returns masterKey
|
every { keyManager.getMainKey() } returns masterKey
|
||||||
every { streamCrypto.deriveStreamKey(masterKey) } returns streamKey
|
every { streamCrypto.deriveStreamKey(masterKey) } returns streamKey
|
||||||
}
|
}
|
||||||
|
|
||||||
private val pruner = Pruner(db, retentionManager, pluginGetter, snapshotRetriever, streamCrypto)
|
private val pruner =
|
||||||
|
Pruner(db, retentionManager, pluginGetter, keyManager, snapshotRetriever, streamCrypto)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test() = runBlocking {
|
fun test() = runBlocking {
|
||||||
|
|
Loading…
Reference in a new issue