Move tink library into core module and expose via CoreCrypto

This also moves key derivation via HKDF into the core.
This commit is contained in:
Torsten Grote 2024-09-03 15:43:43 -03:00
parent c19787a7fa
commit e6905c0365
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
35 changed files with 276 additions and 114 deletions

View file

@ -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)

View file

@ -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<Crypto> { CryptoImpl(get(), get(), get()) }
single<Crypto> { CryptoImpl(androidContext(), get(), get(), get()) }
}

View file

@ -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)
}

View file

@ -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))
}
}

View file

@ -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
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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<String>()
@ -89,7 +89,7 @@ internal class IconManager(
if (!folder.isDirectory && !folder.mkdirs())
throw IOException("Can't create cache folder for icons")
val set = mutableSetOf<String>()
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) {

View file

@ -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<CipherFactory> { CipherFactoryImpl(get()) }
single<KeyManager> { KeyManagerTestImpl() }
single<Crypto> { CryptoImpl(get(), get(), get()) }
single<Crypto> { CryptoImpl(this@TestApp, get(), get(), get()) }
}
private val packageService: PackageService = mockk()
private val appModule = module {

View file

@ -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<Context>()
private val keyManager = mockk<KeyManager>()
private val cipherFactory = mockk<CipherFactory>()
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`() {

View file

@ -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<Context>()
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())

View file

@ -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<Context>()
private val keyManager = mockk<KeyManager>()
private val cipherFactory = mockk<CipherFactory>()
private val headerReader = mockk<HeaderReader>()
private val crypto = CryptoImpl(keyManager, cipherFactory, headerReader)
private val crypto = CryptoImpl(context, keyManager, cipherFactory, headerReader)
private val cipher = mockk<Cipher>()

View file

@ -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<Context>()
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)

View file

@ -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<Context>()
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)

View file

@ -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<BackupNotificationManager>()
private val dbManager = TestKvDbManager()

View file

@ -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<ByteArray>()) } 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<ByteArray>()) } just Runs
}

View file

@ -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<ByteArray>()) } 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<ByteArray>()) } just Runs // gzip header
every { encryptedOutputStream.write(any(), any(), any()) } just Runs // stream copy
every { dbManager.getDbInputStream(packageName) } returns inputStream

View file

@ -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) {

View file

@ -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()

View file

@ -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<KvDbManager>()
private val metadataReader = MetadataReaderImpl(cryptoImpl)
private val notificationManager = mockk<BackupNotificationManager>()

View file

@ -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",

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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

View file

@ -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",

View file

@ -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)

View file

@ -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
)
/**

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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(