From e6905c03653467d633e36a1ffb04fd5903d80b0a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 3 Sep 2024 15:43:43 -0300 Subject: [PATCH] Move tink library into core module and expose via CoreCrypto This also moves key derivation via HKDF into the core. --- .../stevesoltys/seedvault/crypto/Crypto.kt | 118 +++++++++++++++--- .../seedvault/crypto/CryptoModule.kt | 3 +- .../seedvault/metadata/MetadataReader.kt | 2 +- .../seedvault/metadata/MetadataWriter.kt | 2 +- .../seedvault/transport/backup/FullBackup.kt | 2 +- .../seedvault/transport/backup/KVBackup.kt | 2 +- .../transport/restore/FullRestore.kt | 2 +- .../seedvault/transport/restore/KVRestore.kt | 2 +- .../seedvault/worker/IconManager.kt | 4 +- .../java/com/stevesoltys/seedvault/TestApp.kt | 4 +- .../seedvault/crypto/CryptoImplTest.kt | 5 +- .../seedvault/crypto/CryptoIntegrationTest.kt | 33 ++++- .../seedvault/crypto/CryptoTest.kt | 4 +- .../metadata/MetadataReadWriteTest.kt | 5 +- .../seedvault/metadata/MetadataV0ReadTest.kt | 5 +- .../transport/CoordinatorIntegrationTest.kt | 6 +- .../transport/backup/FullBackupTest.kt | 6 +- .../transport/backup/KVBackupTest.kt | 4 +- .../transport/restore/FullRestoreTest.kt | 14 ++- .../transport/restore/KVRestoreTest.kt | 6 +- .../restore/RestoreV0IntegrationTest.kt | 6 +- core/Android.bp | 1 + core/build.gradle.kts | 1 + .../seedvault/core/crypto/CoreCrypto.kt | 63 ++++++++++ .../calyxos/seedvault/core}/crypto/Hkdf.kt | 13 +- .../seedvault/core}/crypto/HkdfTest.kt | 2 +- storage/lib/Android.bp | 1 - storage/lib/build.gradle.kts | 1 - .../backup/storage/crypto/ChunkCrypto.kt | 12 +- .../backup/storage/crypto/StreamCrypto.kt | 37 ++---- .../backup/storage/BackupRestoreTest.kt | 4 +- .../backup/storage/backup/ChunkWriterTest.kt | 2 +- .../backup/storage/backup/ChunkerTest.kt | 2 +- .../backup/SmallFileBackupIntegrationTest.kt | 2 +- .../backup/storage/prune/PrunerTest.kt | 14 +-- 35 files changed, 276 insertions(+), 114 deletions(-) create mode 100644 core/src/main/java/org/calyxos/seedvault/core/crypto/CoreCrypto.kt rename {storage/lib/src/main/java/org/calyxos/backup/storage => core/src/main/java/org/calyxos/seedvault/core}/crypto/Hkdf.kt (86%) rename {storage/lib/src/test/java/org/calyxos/backup/storage => core/src/test/java/org/calyxos/seedvault/core}/crypto/HkdfTest.kt (98%) diff --git a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt b/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt index acb6902e..7d69a2d9 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt @@ -5,23 +5,33 @@ package com.stevesoltys.seedvault.crypto +import android.annotation.SuppressLint +import android.content.Context +import android.provider.Settings +import android.provider.Settings.Secure.ANDROID_ID import com.google.crypto.tink.subtle.AesGcmHkdfStreaming import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.header.HeaderReader import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH import com.stevesoltys.seedvault.header.MAX_VERSION_HEADER_SIZE import com.stevesoltys.seedvault.header.SegmentHeader +import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VersionHeader -import org.calyxos.backup.storage.crypto.StreamCrypto -import org.calyxos.backup.storage.crypto.StreamCrypto.deriveStreamKey +import org.calyxos.seedvault.core.crypto.CoreCrypto +import org.calyxos.seedvault.core.crypto.CoreCrypto.ALGORITHM_HMAC +import org.calyxos.seedvault.core.crypto.CoreCrypto.deriveKey +import org.calyxos.seedvault.core.toByteArrayFromHex +import org.calyxos.seedvault.core.toHexString import java.io.EOFException import java.io.IOException import java.io.InputStream import java.io.OutputStream +import java.nio.ByteBuffer import java.security.GeneralSecurityException import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.security.SecureRandom +import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec /** @@ -47,13 +57,18 @@ internal interface Crypto { */ fun getRandomBytes(size: Int): ByteArray - fun getNameForPackage(salt: String, packageName: String): String + /** + * Returns the ID of the backup repository as a 64 char hex string. + */ + val repoId: String /** - * Returns the name that identifies an APK in the backup storage plugin. - * @param suffix empty string for normal APKs and the name of the split in case of an APK split + * A secret key of size [KEY_SIZE_BYTES] + * only used to create a gear table specific to each main key. */ - fun getNameForApk(salt: String, packageName: String, suffix: String = ""): String + val gearTableKey: ByteArray + + fun sha256(bytes: ByteArray): ByteArray /** * Returns a [AesGcmHkdfStreaming] encrypting stream @@ -75,6 +90,40 @@ internal interface Crypto { associatedData: ByteArray, ): InputStream + fun getAdForVersion(version: Byte = VERSION): ByteArray + + @Deprecated("only for v1") + fun getNameForPackage(salt: String, packageName: String): String + + /** + * Returns the name that identifies an APK in the backup storage plugin. + * @param suffix empty string for normal APKs and the name of the split in case of an APK split + */ + @Deprecated("only for v1") + fun getNameForApk(salt: String, packageName: String, suffix: String = ""): String + + /** + * Returns a [AesGcmHkdfStreaming] encrypting stream + * that gets encrypted and authenticated the given associated data. + */ + @Deprecated("only for v1") + @Throws(IOException::class, GeneralSecurityException::class) + fun newEncryptingStreamV1( + outputStream: OutputStream, + associatedData: ByteArray, + ): OutputStream + + /** + * Returns a [AesGcmHkdfStreaming] decrypting stream + * that gets decrypted and authenticated the given associated data. + */ + @Deprecated("only for v1") + @Throws(IOException::class, GeneralSecurityException::class) + fun newDecryptingStreamV1( + inputStream: InputStream, + associatedData: ByteArray, + ): InputStream + /** * Reads and decrypts a [VersionHeader] from the given [InputStream] * and ensures that the expected version, package name and key match @@ -123,29 +172,64 @@ internal const val TYPE_BACKUP_FULL: Byte = 0x02 internal const val TYPE_ICONS: Byte = 0x03 internal class CryptoImpl( + private val context: Context, private val keyManager: KeyManager, private val cipherFactory: CipherFactory, private val headerReader: HeaderReader, ) : Crypto { - private val key: ByteArray by lazy { - deriveStreamKey(keyManager.getMainKey(), "app data key".toByteArray()) + private val keyV1: ByteArray by lazy { + deriveKey(keyManager.getMainKey(), "app data key".toByteArray()) } - private val secureRandom: SecureRandom by lazy { SecureRandom() } + private val streamKey: ByteArray by lazy { + deriveKey(keyManager.getMainKey(), "app backup stream key".toByteArray()) + } + private val secureRandom: SecureRandom by lazy { SecureRandom.getInstanceStrong() } override fun getRandomBytes(size: Int) = ByteArray(size).apply { secureRandom.nextBytes(this) } + override val repoId: String + get() { // TODO maybe cache this, but what if main key changes during run-time? + @SuppressLint("HardwareIds") + val androidId = Settings.Secure.getString(context.contentResolver, ANDROID_ID) + val repoIdKey = + deriveKey(keyManager.getMainKey(), "app backup repoId key".toByteArray()) + val hmacHasher: Mac = Mac.getInstance(ALGORITHM_HMAC).apply { + init(SecretKeySpec(repoIdKey, ALGORITHM_HMAC)) + } + return hmacHasher.doFinal(androidId.toByteArrayFromHex()).toHexString() + } + + override val gearTableKey: ByteArray + get() = deriveKey(keyManager.getMainKey(), "app backup gear table key".toByteArray()) + + override fun newEncryptingStream( + outputStream: OutputStream, + associatedData: ByteArray, + ): OutputStream = CoreCrypto.newEncryptingStream(streamKey, outputStream, associatedData) + + override fun newDecryptingStream( + inputStream: InputStream, + associatedData: ByteArray, + ): InputStream = CoreCrypto.newDecryptingStream(streamKey, inputStream, associatedData) + + override fun getAdForVersion(version: Byte): ByteArray = ByteBuffer.allocate(1) + .put(version) + .array() + + @Deprecated("only for v1") override fun getNameForPackage(salt: String, packageName: String): String { return sha256("$salt$packageName".toByteArray()).encodeBase64() } + @Deprecated("only for v1") override fun getNameForApk(salt: String, packageName: String, suffix: String): String { return sha256("${salt}APK$packageName$suffix".toByteArray()).encodeBase64() } - private fun sha256(bytes: ByteArray): ByteArray { + override fun sha256(bytes: ByteArray): ByteArray { val messageDigest: MessageDigest = try { MessageDigest.getInstance("SHA-256") } catch (e: NoSuchAlgorithmException) { @@ -155,21 +239,19 @@ internal class CryptoImpl( return messageDigest.digest() } + @Deprecated("only for v1") @Throws(IOException::class, GeneralSecurityException::class) - override fun newEncryptingStream( + override fun newEncryptingStreamV1( outputStream: OutputStream, associatedData: ByteArray, - ): OutputStream { - return StreamCrypto.newEncryptingStream(key, outputStream, associatedData) - } + ): OutputStream = CoreCrypto.newEncryptingStream(keyV1, outputStream, associatedData) + @Deprecated("only for v1") @Throws(IOException::class, GeneralSecurityException::class) - override fun newDecryptingStream( + override fun newDecryptingStreamV1( inputStream: InputStream, associatedData: ByteArray, - ): InputStream { - return StreamCrypto.newDecryptingStream(key, inputStream, associatedData) - } + ): InputStream = CoreCrypto.newDecryptingStream(keyV1, inputStream, associatedData) @Suppress("Deprecation") @Throws(IOException::class, SecurityException::class) diff --git a/app/src/main/java/com/stevesoltys/seedvault/crypto/CryptoModule.kt b/app/src/main/java/com/stevesoltys/seedvault/crypto/CryptoModule.kt index 168818e2..7d52e9f9 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/crypto/CryptoModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/crypto/CryptoModule.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.crypto +import org.koin.android.ext.koin.androidContext import org.koin.dsl.module import java.security.KeyStore @@ -20,5 +21,5 @@ val cryptoModule = module { } KeyManagerImpl(keyStore) } - single { CryptoImpl(get(), get(), get()) } + single { CryptoImpl(androidContext(), get(), get(), get()) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt index 98ffa9cb..61cbac94 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt @@ -56,7 +56,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { if (version == 0.toByte()) return readMetadataV0(inputStream, expectedToken) val metadataBytes = try { - crypto.newDecryptingStream(inputStream, getAD(version, expectedToken)).readBytes() + crypto.newDecryptingStreamV1(inputStream, getAD(version, expectedToken)).readBytes() } catch (e: GeneralSecurityException) { throw DecryptionFailedException(e) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt index 49e3c348..2cbb6ace 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataWriter.kt @@ -25,7 +25,7 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter { @Throws(IOException::class) override fun write(metadata: BackupMetadata, outputStream: OutputStream) { outputStream.write(ByteArray(1).apply { this[0] = metadata.version }) - crypto.newEncryptingStream(outputStream, getAD(metadata.version, metadata.token)).use { + crypto.newEncryptingStreamV1(outputStream, getAD(metadata.version, metadata.token)).use { it.write(encode(metadata)) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt index 65f8da7c..0925eaa7 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/FullBackup.kt @@ -139,7 +139,7 @@ internal class FullBackup( // store version header val state = this.state ?: throw AssertionError() outputStream.write(ByteArray(1) { VERSION }) - crypto.newEncryptingStream(outputStream, getADForFull(VERSION, state.packageName)) + crypto.newEncryptingStreamV1(outputStream, getADForFull(VERSION, state.packageName)) } // this lambda is only called before we actually write backup data the first time return TRANSPORT_OK } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt index acdfc855..f5a2e91b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt @@ -259,7 +259,7 @@ internal class KVBackup( backend.save(handle).use { outputStream -> outputStream.write(ByteArray(1) { VERSION }) val ad = getADForKV(VERSION, packageName) - crypto.newEncryptingStream(outputStream, ad).use { encryptedStream -> + crypto.newEncryptingStreamV1(outputStream, ad).use { encryptedStream -> GZIPOutputStream(encryptedStream).use { gZipStream -> dbManager.getDbInputStream(packageName).use { inputStream -> inputStream.copyTo(gZipStream) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt index ec563165..28fe6054 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/FullRestore.kt @@ -119,7 +119,7 @@ internal class FullRestore( val inputStream = backend.load(handle) val version = headerReader.readVersion(inputStream, state.version) val ad = getADForFull(version, packageName) - state.inputStream = crypto.newDecryptingStream(inputStream, ad) + state.inputStream = crypto.newDecryptingStreamV1(inputStream, ad) } } catch (e: IOException) { Log.w(TAG, "Error getting input stream for $packageName", e) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt index 78069d8a..93214b78 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/KVRestore.kt @@ -161,7 +161,7 @@ internal class KVRestore( backend.load(handle).use { inputStream -> headerReader.readVersion(inputStream, state.version) val ad = getADForKV(VERSION, packageName) - crypto.newDecryptingStream(inputStream, ad).use { decryptedStream -> + crypto.newDecryptingStreamV1(inputStream, ad).use { decryptedStream -> GZIPInputStream(decryptedStream).use { gzipStream -> dbManager.getDbOutputStream(packageName).use { outputStream -> gzipStream.copyTo(outputStream) diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/IconManager.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/IconManager.kt index 629dd05e..821a527d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/IconManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/IconManager.kt @@ -49,7 +49,7 @@ internal class IconManager( fun uploadIcons(token: Long, outputStream: OutputStream) { Log.d(TAG, "Start uploading icons") val packageManager = context.packageManager - crypto.newEncryptingStream(outputStream, getAD(VERSION, token)).use { cryptoStream -> + crypto.newEncryptingStreamV1(outputStream, getAD(VERSION, token)).use { cryptoStream -> ZipOutputStream(cryptoStream).use { zip -> zip.setLevel(BEST_SPEED) val entries = mutableSetOf() @@ -89,7 +89,7 @@ internal class IconManager( if (!folder.isDirectory && !folder.mkdirs()) throw IOException("Can't create cache folder for icons") val set = mutableSetOf() - crypto.newDecryptingStream(inputStream, getAD(version, token)).use { cryptoStream -> + crypto.newDecryptingStreamV1(inputStream, getAD(version, token)).use { cryptoStream -> ZipInputStream(cryptoStream).use { zip -> var entry = zip.nextEntry while (entry != null) { diff --git a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt index 305bbc6c..f2d45e25 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/TestApp.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault +import com.stevesoltys.seedvault.backend.saf.storagePluginModuleSaf import com.stevesoltys.seedvault.crypto.CipherFactory import com.stevesoltys.seedvault.crypto.CipherFactoryImpl import com.stevesoltys.seedvault.crypto.Crypto @@ -13,7 +14,6 @@ import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl import com.stevesoltys.seedvault.header.headerModule import com.stevesoltys.seedvault.metadata.metadataModule -import com.stevesoltys.seedvault.backend.saf.storagePluginModuleSaf import com.stevesoltys.seedvault.restore.install.installModule import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.PackageService @@ -33,7 +33,7 @@ class TestApp : App() { private val testCryptoModule = module { factory { CipherFactoryImpl(get()) } single { KeyManagerTestImpl() } - single { CryptoImpl(get(), get(), get()) } + single { CryptoImpl(this@TestApp, get(), get(), get()) } } private val packageService: PackageService = mockk() private val appModule = module { diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt index 348dee6b..c0c3e355 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoImplTest.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.crypto +import android.content.Context import com.stevesoltys.seedvault.getRandomBase64 import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.header.HeaderReaderImpl @@ -19,14 +20,16 @@ import org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD import java.io.ByteArrayInputStream import java.io.IOException +@Suppress("DEPRECATION") @TestInstance(PER_METHOD) class CryptoImplTest { + private val context = mockk() private val keyManager = mockk() private val cipherFactory = mockk() private val headerReader = HeaderReaderImpl() - private val crypto = CryptoImpl(keyManager, cipherFactory, headerReader) + private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader) @Test fun `decrypting multiple segments on empty stream throws`() { diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt index 67d6b077..4862b6c6 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoIntegrationTest.kt @@ -5,8 +5,10 @@ package com.stevesoltys.seedvault.crypto +import android.content.Context import com.stevesoltys.seedvault.assertReadEquals import com.stevesoltys.seedvault.header.HeaderReaderImpl +import io.mockk.mockk import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.not @@ -19,13 +21,15 @@ import java.io.ByteArrayOutputStream import java.io.IOException import kotlin.random.Random +@Suppress("DEPRECATION") @TestInstance(PER_METHOD) class CryptoIntegrationTest { + private val context = mockk() private val keyManager = KeyManagerTestImpl() private val cipherFactory = CipherFactoryImpl(keyManager) private val headerReader = HeaderReaderImpl() - private val crypto = CryptoImpl(keyManager, cipherFactory, headerReader) + private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader) private val cleartext = Random.nextBytes(Random.nextInt(1, 422300)) @@ -38,7 +42,18 @@ class CryptoIntegrationTest { } @Test - fun `decrypting encrypted cleartext works`() { + fun `decrypting encrypted cleartext works v1`() { + val ad = Random.nextBytes(42) + val outputStream = ByteArrayOutputStream() + crypto.newEncryptingStreamV1(outputStream, ad).use { it.write(cleartext) } + val inputStream = ByteArrayInputStream(outputStream.toByteArray()) + crypto.newDecryptingStreamV1(inputStream, ad).use { + assertReadEquals(cleartext, it) + } + } + + @Test + fun `decrypting encrypted cleartext works v2`() { val ad = Random.nextBytes(42) val outputStream = ByteArrayOutputStream() crypto.newEncryptingStream(outputStream, ad).use { it.write(cleartext) } @@ -49,7 +64,19 @@ class CryptoIntegrationTest { } @Test - fun `decrypting encrypted cleartext fails with different AD`() { + fun `decrypting encrypted cleartext fails with different AD v1`() { + val outputStream = ByteArrayOutputStream() + crypto.newEncryptingStreamV1(outputStream, Random.nextBytes(42)).use { it.write(cleartext) } + val inputStream = ByteArrayInputStream(outputStream.toByteArray()) + assertThrows(IOException::class.java) { + crypto.newDecryptingStreamV1(inputStream, Random.nextBytes(41)).use { + it.read() + } + } + } + + @Test + fun `decrypting encrypted cleartext fails with different AD v2`() { val outputStream = ByteArrayOutputStream() crypto.newEncryptingStream(outputStream, Random.nextBytes(42)).use { it.write(cleartext) } val inputStream = ByteArrayInputStream(outputStream.toByteArray()) diff --git a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt index d7d12e7c..912247b5 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/crypto/CryptoTest.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.crypto +import android.content.Context import com.stevesoltys.seedvault.assertContains import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomString @@ -36,11 +37,12 @@ import kotlin.random.Random @TestInstance(PER_METHOD) class CryptoTest { + private val context = mockk() private val keyManager = mockk() private val cipherFactory = mockk() private val headerReader = mockk() - private val crypto = CryptoImpl(keyManager, cipherFactory, headerReader) + private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader) private val cipher = mockk() diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt index 38f56263..66df0b9b 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataReadWriteTest.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.metadata +import android.content.Context import com.stevesoltys.seedvault.crypto.CipherFactoryImpl import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES @@ -15,6 +16,7 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED +import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @@ -30,10 +32,11 @@ internal class MetadataReadWriteTest { private val secretKey = SecretKeySpec( "This is a legacy backup key 1234".toByteArray(), 0, KEY_SIZE_BYTES, "AES" ) + private val context = mockk() private val keyManager = KeyManagerTestImpl(secretKey) private val cipherFactory = CipherFactoryImpl(keyManager) private val headerReader = HeaderReaderImpl() - private val cryptoImpl = CryptoImpl(keyManager, cipherFactory, headerReader) + private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader) private val writer = MetadataWriterImpl(cryptoImpl) private val reader = MetadataReaderImpl(cryptoImpl) diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt index d150a3ea..19362f38 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataV0ReadTest.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.metadata +import android.content.Context import com.stevesoltys.seedvault.crypto.CipherFactoryImpl import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES @@ -13,6 +14,7 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.toByteArrayFromHex +import io.mockk.mockk import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @@ -29,10 +31,11 @@ internal class MetadataV0ReadTest { private val secretKey = SecretKeySpec( "This is a legacy backup key 1234".toByteArray(), 0, KEY_SIZE_BYTES, "AES" ) + private val context = mockk() private val keyManager = KeyManagerTestImpl(secretKey) private val cipherFactory = CipherFactoryImpl(keyManager) private val headerReader = HeaderReaderImpl() - private val cryptoImpl = CryptoImpl(keyManager, cipherFactory, headerReader) + private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader) private val reader = MetadataReaderImpl(cryptoImpl) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt index 84c6fb61..7e506ce7 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -12,6 +12,8 @@ import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.RestoreDescription import android.app.backup.RestoreDescription.TYPE_FULL_STREAM import android.os.ParcelFileDescriptor +import com.stevesoltys.seedvault.backend.BackendManager +import com.stevesoltys.seedvault.backend.LegacyStoragePlugin import com.stevesoltys.seedvault.crypto.CipherFactoryImpl import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl @@ -20,8 +22,6 @@ import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.PackageMetadata -import com.stevesoltys.seedvault.backend.LegacyStoragePlugin -import com.stevesoltys.seedvault.backend.BackendManager import com.stevesoltys.seedvault.transport.backup.BackupCoordinator import com.stevesoltys.seedvault.transport.backup.FullBackup import com.stevesoltys.seedvault.transport.backup.InputFactory @@ -59,7 +59,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val keyManager = KeyManagerTestImpl() private val cipherFactory = CipherFactoryImpl(keyManager) private val headerReader = HeaderReaderImpl() - private val cryptoImpl = CryptoImpl(keyManager, cipherFactory, headerReader) + private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader) private val metadataReader = MetadataReaderImpl(cryptoImpl) private val notificationManager = mockk() private val dbManager = TestKvDbManager() diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt index 816c496d..8333ed5f 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/FullBackupTest.kt @@ -150,7 +150,7 @@ internal class FullBackupTest : BackupTest() { every { inputFactory.getInputStream(data) } returns inputStream expectInitializeOutputStream() every { settingsManager.isQuotaUnlimited() } returns false - every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream + every { crypto.newEncryptingStreamV1(outputStream, ad) } returns encryptedOutputStream every { inputStream.read(any(), any(), bytes.size) } throws IOException() expectClearState() @@ -204,7 +204,7 @@ internal class FullBackupTest : BackupTest() { every { inputFactory.getInputStream(data) } returns inputStream expectInitializeOutputStream() every { settingsManager.isQuotaUnlimited() } returns false - every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream + every { crypto.newEncryptingStreamV1(outputStream, ad) } returns encryptedOutputStream every { inputStream.read(any(), any(), bytes.size) } returns bytes.size every { encryptedOutputStream.write(any()) } throws IOException() expectClearState() @@ -345,7 +345,7 @@ internal class FullBackupTest : BackupTest() { private fun expectSendData(numBytes: Int, readBytes: Int = numBytes) { every { inputStream.read(any(), any(), numBytes) } returns readBytes - every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream + every { crypto.newEncryptingStreamV1(outputStream, ad) } returns encryptedOutputStream every { encryptedOutputStream.write(any()) } just Runs } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt index 95bfbe90..422bf71d 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt @@ -233,7 +233,7 @@ internal class KVBackupTest : BackupTest() { coEvery { backend.save(handle) } returns outputStream every { outputStream.write(ByteArray(1) { VERSION }) } just Runs val ad = getADForKV(VERSION, packageInfo.packageName) - every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream + every { crypto.newEncryptingStreamV1(outputStream, ad) } returns encryptedOutputStream every { encryptedOutputStream.write(any()) } throws IOException() assertEquals(TRANSPORT_ERROR, backup.finishBackup()) @@ -304,7 +304,7 @@ internal class KVBackupTest : BackupTest() { coEvery { backend.save(handle) } returns outputStream every { outputStream.write(ByteArray(1) { VERSION }) } just Runs val ad = getADForKV(VERSION, packageInfo.packageName) - every { crypto.newEncryptingStream(outputStream, ad) } returns encryptedOutputStream + every { crypto.newEncryptingStreamV1(outputStream, ad) } returns encryptedOutputStream every { encryptedOutputStream.write(any()) } just Runs // gzip header every { encryptedOutputStream.write(any(), any(), any()) } just Runs // stream copy every { dbManager.getDbInputStream(packageName) } returns inputStream diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt index c176d78c..527da441 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/FullRestoreTest.kt @@ -9,6 +9,8 @@ import android.app.backup.BackupTransport.NO_MORE_DATA import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED +import com.stevesoltys.seedvault.backend.BackendManager +import com.stevesoltys.seedvault.backend.LegacyStoragePlugin import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.header.MAX_SEGMENT_LENGTH @@ -16,8 +18,6 @@ import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VersionHeader import com.stevesoltys.seedvault.header.getADForFull -import com.stevesoltys.seedvault.backend.LegacyStoragePlugin -import com.stevesoltys.seedvault.backend.BackendManager import io.mockk.CapturingSlot import io.mockk.Runs import io.mockk.coEvery @@ -135,7 +135,7 @@ internal class FullRestoreTest : RestoreTest() { coEvery { backend.load(handle) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION - every { crypto.newDecryptingStream(inputStream, ad) } throws IOException() + every { crypto.newDecryptingStreamV1(inputStream, ad) } throws IOException() every { fileDescriptor.close() } just Runs assertEquals( @@ -151,7 +151,9 @@ internal class FullRestoreTest : RestoreTest() { coEvery { backend.load(handle) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION - every { crypto.newDecryptingStream(inputStream, ad) } throws GeneralSecurityException() + every { + crypto.newDecryptingStreamV1(inputStream, ad) + } throws GeneralSecurityException() every { fileDescriptor.close() } just Runs assertEquals(TRANSPORT_ERROR, restore.getNextFullRestoreDataChunk(fileDescriptor)) @@ -217,7 +219,7 @@ internal class FullRestoreTest : RestoreTest() { coEvery { backend.load(handle) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION - every { crypto.newDecryptingStream(inputStream, ad) } returns decryptedInputStream + every { crypto.newDecryptingStreamV1(inputStream, ad) } returns decryptedInputStream every { outputFactory.getOutputStream(fileDescriptor) } returns outputStream every { fileDescriptor.close() } just Runs every { inputStream.close() } just Runs @@ -250,7 +252,7 @@ internal class FullRestoreTest : RestoreTest() { private fun initInputStream() { coEvery { backend.load(handle) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION - every { crypto.newDecryptingStream(inputStream, ad) } returns decryptedInputStream + every { crypto.newDecryptingStreamV1(inputStream, ad) } returns decryptedInputStream } private fun readAndEncryptInputStream(encryptedBytes: ByteArray) { diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt index 4e3caf9a..6214bf41 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/KVRestoreTest.kt @@ -105,7 +105,7 @@ internal class KVRestoreTest : RestoreTest() { coEvery { backend.load(handle) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION - every { crypto.newDecryptingStream(inputStream, ad) } throws GeneralSecurityException() + every { crypto.newDecryptingStreamV1(inputStream, ad) } throws GeneralSecurityException() every { dbManager.deleteDb(packageInfo.packageName, true) } returns true streamsGetClosed() @@ -123,7 +123,7 @@ internal class KVRestoreTest : RestoreTest() { coEvery { backend.load(handle) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION - every { crypto.newDecryptingStream(inputStream, ad) } returns decryptInputStream + every { crypto.newDecryptingStreamV1(inputStream, ad) } returns decryptInputStream every { dbManager.getDbOutputStream(packageInfo.packageName) } returns ByteArrayOutputStream() @@ -148,7 +148,7 @@ internal class KVRestoreTest : RestoreTest() { coEvery { backend.load(handle) } returns inputStream every { headerReader.readVersion(inputStream, VERSION) } returns VERSION - every { crypto.newDecryptingStream(inputStream, ad) } returns decryptInputStream + every { crypto.newDecryptingStreamV1(inputStream, ad) } returns decryptInputStream every { dbManager.getDbOutputStream(packageInfo.packageName) } returns ByteArrayOutputStream() diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt index 9bdab3a6..752eab1a 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreV0IntegrationTest.kt @@ -11,6 +11,8 @@ import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.RestoreDescription import android.app.backup.RestoreDescription.TYPE_FULL_STREAM import android.os.ParcelFileDescriptor +import com.stevesoltys.seedvault.backend.BackendManager +import com.stevesoltys.seedvault.backend.LegacyStoragePlugin import com.stevesoltys.seedvault.crypto.CipherFactoryImpl import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES @@ -18,8 +20,6 @@ import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.header.HeaderReaderImpl import com.stevesoltys.seedvault.metadata.MetadataReaderImpl -import com.stevesoltys.seedvault.backend.LegacyStoragePlugin -import com.stevesoltys.seedvault.backend.BackendManager import com.stevesoltys.seedvault.toByteArrayFromHex import com.stevesoltys.seedvault.transport.TransportTest import com.stevesoltys.seedvault.transport.backup.KvDbManager @@ -50,7 +50,7 @@ internal class RestoreV0IntegrationTest : TransportTest() { private val keyManager = KeyManagerTestImpl(secretKey) private val cipherFactory = CipherFactoryImpl(keyManager) private val headerReader = HeaderReaderImpl() - private val cryptoImpl = CryptoImpl(keyManager, cipherFactory, headerReader) + private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader) private val dbManager = mockk() private val metadataReader = MetadataReaderImpl(cryptoImpl) private val notificationManager = mockk() diff --git a/core/Android.bp b/core/Android.bp index 6922e7f5..96379108 100644 --- a/core/Android.bp +++ b/core/Android.bp @@ -14,6 +14,7 @@ android_library { "src/main/java/org/calyxos/seedvault/core/backends/BackendTest.kt", ], static_libs: [ + "seedvault-lib-tink-android", "androidx.core_core-ktx", "androidx.documentfile_documentfile", "kotlinx-coroutines-android", diff --git a/core/build.gradle.kts b/core/build.gradle.kts index daed7a8f..b0752895 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(libs.bundles.coroutines) implementation(libs.androidx.documentfile) implementation(libs.androidx.core.ktx) + implementation(libs.google.tink.android) implementation(fileTree("${rootProject.rootDir}/libs/dav4jvm").include("*.jar")) implementation(libs.squareup.okio) implementation(libs.kotlin.logging) diff --git a/core/src/main/java/org/calyxos/seedvault/core/crypto/CoreCrypto.kt b/core/src/main/java/org/calyxos/seedvault/core/crypto/CoreCrypto.kt new file mode 100644 index 00000000..acc3877c --- /dev/null +++ b/core/src/main/java/org/calyxos/seedvault/core/crypto/CoreCrypto.kt @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.calyxos.seedvault.core.crypto + +import com.google.crypto.tink.subtle.AesGcmHkdfStreaming +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.security.GeneralSecurityException +import javax.crypto.SecretKey + +public object CoreCrypto { + + private const val KEY_SIZE = 256 + private const val SIZE_SEGMENT = 1 shl 20 // 1024 * 1024 + public const val KEY_SIZE_BYTES: Int = KEY_SIZE / 8 + public const val ALGORITHM_HMAC: String = "HmacSHA256" + + @Throws(GeneralSecurityException::class) + public fun deriveKey(mainKey: SecretKey, info: ByteArray): ByteArray = Hkdf.expand( + secretKey = mainKey, + info = info, + outLengthBytes = KEY_SIZE_BYTES, + ) + + /** + * Returns a [AesGcmHkdfStreaming] encrypting stream + * that gets encrypted with the given secret. + */ + @Throws(IOException::class, GeneralSecurityException::class) + public fun newEncryptingStream( + secret: ByteArray, + outputStream: OutputStream, + associatedData: ByteArray = ByteArray(0), + ): OutputStream { + return AesGcmHkdfStreaming( + secret, + ALGORITHM_HMAC, + KEY_SIZE_BYTES, + SIZE_SEGMENT, + 0, + ).newEncryptingStream(outputStream, associatedData) + } + + @Throws(IOException::class, GeneralSecurityException::class) + public fun newDecryptingStream( + secret: ByteArray, + inputStream: InputStream, + associatedData: ByteArray = ByteArray(0), + ): InputStream { + return AesGcmHkdfStreaming( + secret, + ALGORITHM_HMAC, + KEY_SIZE_BYTES, + SIZE_SEGMENT, + 0, + ).newDecryptingStream(inputStream, associatedData) + } + +} diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/Hkdf.kt b/core/src/main/java/org/calyxos/seedvault/core/crypto/Hkdf.kt similarity index 86% rename from storage/lib/src/main/java/org/calyxos/backup/storage/crypto/Hkdf.kt rename to core/src/main/java/org/calyxos/seedvault/core/crypto/Hkdf.kt index 64a058c4..1efc66ed 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/Hkdf.kt +++ b/core/src/main/java/org/calyxos/seedvault/core/crypto/Hkdf.kt @@ -3,8 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.calyxos.backup.storage.crypto +package org.calyxos.seedvault.core.crypto +import org.calyxos.seedvault.core.crypto.CoreCrypto.ALGORITHM_HMAC import java.nio.ByteBuffer import java.security.GeneralSecurityException import javax.crypto.Mac @@ -14,19 +15,17 @@ import kotlin.math.min internal object Hkdf { - private const val KEY_SIZE = 256 - internal const val KEY_SIZE_BYTES = KEY_SIZE / 8 - internal const val ALGORITHM_HMAC = "HmacSHA256" - /** * Step 2 of RFC 5869. * * Based on the Apache2 licensed HKDF library by Patrick Favre-Bulle. * Link: https://github.com/patrickfav/hkdf * - * @param secretKey a pseudorandom key of at least hmac hash length in bytes (usually, the output from the extract step) + * @param secretKey a pseudorandom key of at least hmac hash length in bytes + * (usually, the output from the extract step) * @param info optional context and application specific information; may be null - * @param outLengthBytes length of output keying material in bytes (must be <= 255 * mac hash length) + * @param outLengthBytes length of output keying material in bytes + * (must be <= 255 * mac hash length) * @return new byte array of output keying material (OKM) */ @Throws(GeneralSecurityException::class) diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/crypto/HkdfTest.kt b/core/src/test/java/org/calyxos/seedvault/core/crypto/HkdfTest.kt similarity index 98% rename from storage/lib/src/test/java/org/calyxos/backup/storage/crypto/HkdfTest.kt rename to core/src/test/java/org/calyxos/seedvault/core/crypto/HkdfTest.kt index 58011425..9a3e4070 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/crypto/HkdfTest.kt +++ b/core/src/test/java/org/calyxos/seedvault/core/crypto/HkdfTest.kt @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.calyxos.backup.storage.crypto +package org.calyxos.seedvault.core.crypto import org.junit.Assert.assertArrayEquals import org.junit.Test diff --git a/storage/lib/Android.bp b/storage/lib/Android.bp index 2cac46ca..b2649be6 100644 --- a/storage/lib/Android.bp +++ b/storage/lib/Android.bp @@ -20,7 +20,6 @@ android_library { }, static_libs: [ "seedvault-lib-core", - "seedvault-lib-tink-android", "libprotobuf-java-lite", "androidx.core_core-ktx", "androidx.fragment_fragment-ktx", diff --git a/storage/lib/build.gradle.kts b/storage/lib/build.gradle.kts index 9719b4b8..e54c1286 100644 --- a/storage/lib/build.gradle.kts +++ b/storage/lib/build.gradle.kts @@ -93,7 +93,6 @@ dependencies { implementation(libs.google.material) implementation(libs.androidx.room.runtime) implementation(libs.google.protobuf.javalite) - implementation(libs.google.tink.android) ksp(group = "androidx.room", name = "room-compiler", version = libs.versions.room.get()) lintChecks(libs.thirdegg.lint.rules) diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/ChunkCrypto.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/ChunkCrypto.kt index 3ce8c80b..9f878800 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/ChunkCrypto.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/ChunkCrypto.kt @@ -6,8 +6,9 @@ package org.calyxos.backup.storage.crypto import org.calyxos.backup.storage.backup.Chunker -import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC -import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES +import org.calyxos.seedvault.core.crypto.CoreCrypto +import org.calyxos.seedvault.core.crypto.CoreCrypto.ALGORITHM_HMAC +import org.calyxos.seedvault.core.crypto.CoreCrypto.KEY_SIZE_BYTES import java.security.GeneralSecurityException import java.security.KeyStore import javax.crypto.Mac @@ -25,12 +26,11 @@ internal object ChunkCrypto { */ @Throws(GeneralSecurityException::class) fun deriveChunkIdKey( - masterKey: SecretKey, + mainKey: SecretKey, info: ByteArray = INFO_CHUNK_ID.toByteArray(), - ): ByteArray = Hkdf.expand( - secretKey = masterKey, + ): ByteArray = CoreCrypto.deriveKey( + mainKey = mainKey, info = info, - outLengthBytes = KEY_SIZE_BYTES ) /** diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/StreamCrypto.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/StreamCrypto.kt index ad9f350c..a3aae5ec 100644 --- a/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/StreamCrypto.kt +++ b/storage/lib/src/main/java/org/calyxos/backup/storage/crypto/StreamCrypto.kt @@ -5,10 +5,9 @@ package org.calyxos.backup.storage.crypto -import com.google.crypto.tink.subtle.AesGcmHkdfStreaming import org.calyxos.backup.storage.backup.Backup.Companion.VERSION -import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC -import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES +import org.calyxos.seedvault.core.crypto.CoreCrypto +import org.calyxos.seedvault.core.crypto.CoreCrypto.KEY_SIZE_BYTES import org.calyxos.seedvault.core.toByteArrayFromHex import java.io.IOException import java.io.InputStream @@ -20,18 +19,16 @@ import javax.crypto.SecretKey public object StreamCrypto { private const val INFO_STREAM_KEY = "stream key" - private const val SIZE_SEGMENT = 1 shl 20 // 1024 * 1024 private const val TYPE_CHUNK: Byte = 0x00 private const val TYPE_SNAPSHOT: Byte = 0x01 @Throws(GeneralSecurityException::class) public fun deriveStreamKey( - masterKey: SecretKey, + mainKey: SecretKey, info: ByteArray = INFO_STREAM_KEY.toByteArray(), - ): ByteArray = Hkdf.expand( - secretKey = masterKey, + ): ByteArray = CoreCrypto.deriveKey( + mainKey = mainKey, info = info, - outLengthBytes = KEY_SIZE_BYTES ) internal fun getAssociatedDataForChunk(chunkId: String, version: Byte = VERSION): ByteArray = @@ -48,39 +45,19 @@ public object StreamCrypto { .put(timestamp.toByteArray()) .array() - /** - * Returns a [AesGcmHkdfStreaming] encrypting stream - * that gets encrypted with the given secret. - */ @Throws(IOException::class, GeneralSecurityException::class) public fun newEncryptingStream( secret: ByteArray, outputStream: OutputStream, associatedData: ByteArray = ByteArray(0), - ): OutputStream { - return AesGcmHkdfStreaming( - secret, - ALGORITHM_HMAC, - KEY_SIZE_BYTES, - SIZE_SEGMENT, - 0 - ).newEncryptingStream(outputStream, associatedData) - } + ): OutputStream = CoreCrypto.newEncryptingStream(secret, outputStream, associatedData) @Throws(IOException::class, GeneralSecurityException::class) public fun newDecryptingStream( secret: ByteArray, inputStream: InputStream, associatedData: ByteArray = ByteArray(0), - ): InputStream { - return AesGcmHkdfStreaming( - secret, - ALGORITHM_HMAC, - KEY_SIZE_BYTES, - SIZE_SEGMENT, - 0 - ).newDecryptingStream(inputStream, associatedData) - } + ): InputStream = CoreCrypto.newDecryptingStream(secret, inputStream, associatedData) public fun Long.toByteArray(): ByteArray = ByteArray(8).apply { var l = this@toByteArray diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/BackupRestoreTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/BackupRestoreTest.kt index f5310b19..e421d0cf 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/BackupRestoreTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/BackupRestoreTest.kt @@ -33,8 +33,6 @@ import org.calyxos.backup.storage.backup.ChunksCacheRepopulater import org.calyxos.backup.storage.content.ContentFile import org.calyxos.backup.storage.content.DocFile import org.calyxos.backup.storage.content.MediaFile -import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC -import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES import org.calyxos.backup.storage.db.CachedChunk import org.calyxos.backup.storage.db.CachedFile import org.calyxos.backup.storage.db.ChunksCache @@ -48,6 +46,8 @@ import org.calyxos.backup.storage.scanner.FileScannerResult import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob import org.calyxos.seedvault.core.backends.FileBackupFileType.Snapshot +import org.calyxos.seedvault.core.crypto.CoreCrypto.ALGORITHM_HMAC +import org.calyxos.seedvault.core.crypto.CoreCrypto.KEY_SIZE_BYTES import org.calyxos.seedvault.core.crypto.KeyManager import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunkWriterTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunkWriterTest.kt index 8dd886ac..c55809aa 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunkWriterTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunkWriterTest.kt @@ -13,13 +13,13 @@ import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.calyxos.backup.storage.backup.Backup.Companion.VERSION -import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES import org.calyxos.backup.storage.crypto.StreamCrypto import org.calyxos.backup.storage.db.ChunksCache import org.calyxos.backup.storage.getRandomString import org.calyxos.backup.storage.mockLog import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob +import org.calyxos.seedvault.core.crypto.CoreCrypto.KEY_SIZE_BYTES import org.calyxos.seedvault.core.toHexString import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunkerTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunkerTest.kt index 5e1808f7..7f5d0c70 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunkerTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/ChunkerTest.kt @@ -6,7 +6,7 @@ package org.calyxos.backup.storage.backup import org.calyxos.backup.storage.crypto.ChunkCrypto -import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES +import org.calyxos.seedvault.core.crypto.CoreCrypto.KEY_SIZE_BYTES import org.junit.Assert.assertEquals import org.junit.Test import java.io.ByteArrayInputStream diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt index 3c46f07c..d35b78be 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/backup/SmallFileBackupIntegrationTest.kt @@ -14,7 +14,6 @@ import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.runBlocking import org.calyxos.backup.storage.api.BackupObserver -import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES import org.calyxos.backup.storage.crypto.StreamCrypto import org.calyxos.backup.storage.db.CachedChunk import org.calyxos.backup.storage.db.ChunksCache @@ -23,6 +22,7 @@ import org.calyxos.backup.storage.getRandomDocFile import org.calyxos.backup.storage.getRandomString import org.calyxos.backup.storage.mockLog import org.calyxos.seedvault.core.backends.Backend +import org.calyxos.seedvault.core.crypto.CoreCrypto.KEY_SIZE_BYTES import org.calyxos.seedvault.core.toHexString import org.junit.Assert.assertEquals import org.junit.Test diff --git a/storage/lib/src/test/java/org/calyxos/backup/storage/prune/PrunerTest.kt b/storage/lib/src/test/java/org/calyxos/backup/storage/prune/PrunerTest.kt index 23d65edb..956ae59a 100644 --- a/storage/lib/src/test/java/org/calyxos/backup/storage/prune/PrunerTest.kt +++ b/storage/lib/src/test/java/org/calyxos/backup/storage/prune/PrunerTest.kt @@ -13,22 +13,22 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.slot import kotlinx.coroutines.runBlocking +import org.calyxos.backup.storage.SnapshotRetriever import org.calyxos.backup.storage.api.StoredSnapshot import org.calyxos.backup.storage.backup.BackupDocumentFile import org.calyxos.backup.storage.backup.BackupMediaFile import org.calyxos.backup.storage.backup.BackupSnapshot -import org.calyxos.backup.storage.crypto.Hkdf.ALGORITHM_HMAC -import org.calyxos.backup.storage.crypto.Hkdf.KEY_SIZE_BYTES import org.calyxos.backup.storage.crypto.StreamCrypto import org.calyxos.backup.storage.db.CachedChunk import org.calyxos.backup.storage.db.ChunksCache import org.calyxos.backup.storage.db.Db +import org.calyxos.backup.storage.getCurrentBackupSnapshots import org.calyxos.backup.storage.getRandomString import org.calyxos.backup.storage.mockLog -import org.calyxos.backup.storage.SnapshotRetriever -import org.calyxos.backup.storage.getCurrentBackupSnapshots import org.calyxos.seedvault.core.backends.Backend import org.calyxos.seedvault.core.backends.FileBackupFileType.Blob +import org.calyxos.seedvault.core.crypto.CoreCrypto.ALGORITHM_HMAC +import org.calyxos.seedvault.core.crypto.CoreCrypto.KEY_SIZE_BYTES import org.calyxos.seedvault.core.crypto.KeyManager import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -48,15 +48,15 @@ internal class PrunerTest { private val retentionManager: RetentionManager = mockk() private val streamCrypto: StreamCrypto = mockk() private val streamKey = "This is a backup key for testing".toByteArray() - private val masterKey = SecretKeySpec(streamKey, 0, KEY_SIZE_BYTES, ALGORITHM_HMAC) + private val mainKey = SecretKeySpec(streamKey, 0, KEY_SIZE_BYTES, ALGORITHM_HMAC) init { mockLog(false) mockkStatic("org.calyxos.backup.storage.SnapshotRetrieverKt") every { backendGetter() } returns backend every { db.getChunksCache() } returns chunksCache - every { keyManager.getMainKey() } returns masterKey - every { streamCrypto.deriveStreamKey(masterKey) } returns streamKey + every { keyManager.getMainKey() } returns mainKey + every { streamCrypto.deriveStreamKey(mainKey) } returns streamKey } private val pruner = Pruner(